[
  {
    "path": ".devcontainer/.gitignore",
    "content": ".local"
  },
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm\n\nENV PATH=/usr/local/bin:${PATH}\n# Install latest pnpm\n# RUN npm install -g pnpm@9.0.2\n\n# [Optional] Uncomment this section to install additional OS packages.\n# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\\n#     && apt-get -y install --no-install-recommends <your-package-list-here>\n\n# [Optional] Uncomment if you want to install an additional version of node using nvm\n# ARG EXTRA_NODE_VERSION=10\n# RUN su node -c \"source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}\"\n\n# [Optional] Uncomment if you want to install more global node modules\n# RUN su node -c \"npm install -g <your-package-list-here>\"\n\nCOPY library-scripts/*.sh /tmp/library-scripts/\n\nENV DOCKER_BUILDKIT=1\nRUN apt-get update\nRUN /bin/bash /tmp/library-scripts/docker-in-docker-debian.sh\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node-postgres\n{\n  \"name\": \"Node.js & PostgreSQL\",\n  \"dockerComposeFile\": \"docker-compose.yml\",\n  \"service\": \"app\",\n  \"workspaceFolder\": \"/workspaces/webstudio\",\n  \"features\": {\n    \"ghcr.io/robbert229/devcontainer-features/postgresql-client:1\": {\n      \"version\": \"15\"\n    }\n  },\n\n  // Features to add to the dev container. More info: https://containers.dev/features.\n  // \"features\": {},\n\n  // Use 'forwardPorts' to make a list of ports inside the container available locally.\n  // This can be used to network with other containers or with the host.\n  // \"forwardPorts\": [3000, 5432],\n  \"forwardPorts\": [5173],\n\n  // Use 'postCreateCommand' to run commands after the container is created.\n\n  \"postCreateCommand\": \".devcontainer/postinstall.sh\",\n  // \"postStartCommand\": \"git config --global --add safe.directory ${containerWorkspaceFolder}\",\n  \"customizations\": {\n    \"vscode\": {\n      \"extensions\": [\n        \"esbenp.prettier-vscode\",\n        \"redhat.vscode-yaml\",\n        \"me-dutour-mathieu.vscode-github-actions\",\n        \"eamodio.gitlens\",\n        \"bradymholt.pgformatter\",\n        \"YoavBls.pretty-ts-errors\",\n        \"typescriptteam.native-preview\"\n      ]\n    }\n  }\n\n  // Configure tool-specific properties.\n  // \"customizations\": {},\n\n  // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.\n  // \"remoteUser\": \"root\"\n}\n"
  },
  {
    "path": ".devcontainer/docker-compose.yml",
    "content": "version: \"3.8\"\n\nservices:\n  app:\n    init: true\n    privileged: true\n    build:\n      context: .\n      dockerfile: Dockerfile\n\n    volumes:\n      - ..:/workspaces/webstudio:cached\n      # preserve history\n      - ./.local:/home/node/.local\n      - docker-data:/var/lib/docker\n      - ${HOME}/.github/instructions:/home/node/.github/instructions\n\n    entrypoint: [\"/usr/local/share/docker-init.sh\"]\n    # Overrides default command so things don't shut down after the process ends.\n    command: sleep infinity\n    # Runs app on the same network as the database container, allows \"forwardPorts\" in devcontainer.json function.\n    network_mode: service:db\n\n    depends_on:\n      db:\n        condition: service_healthy\n\n  db:\n    image: ghcr.io/supabase/postgres:15.1.1.55\n    # Uncomment to log all queries\n    command: [\"postgres\", \"-c\", \"log_statement=all\", \"-c\", \"listen_addresses=*\"]\n    restart: unless-stopped\n    volumes:\n      - postgres-data:/var/lib/postgresql/data\n    environment:\n      POSTGRES_PASSWORD: pass\n      POSTGRES_DB: webstudio\n\n    ports:\n      - ${PGPORT:-5432}:5432\n      - 3000:3000\n\n    healthcheck:\n      test: [\"CMD-SHELL\", \"pg_isready -U postgres -d webstudio\"]\n      interval: 10s\n      timeout: 5s\n      retries: 25\n\n  rest:\n    container_name: supabase-rest\n    image: postgrest/postgrest:v12.2.0\n    depends_on:\n      db:\n        # Disable this if you are using an external Postgres database\n        condition: service_healthy\n    restart: unless-stopped\n    environment:\n      PGRST_DB_URI: postgresql://postgres:pass@localhost/webstudio\n      PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS:-public}\n      PGRST_DB_ANON_ROLE: anon\n      PGRST_JWT_SECRET: ${JWT_SECRET:-jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret}\n      PGRST_DB_USE_LEGACY_GUCS: \"false\"\n      PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET:-jwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecretjwtsecret}\n      PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}\n    command: \"postgrest\"\n\n    # Runs app on the same network as the database container, allows \"forwardPorts\" in devcontainer.json function.\n    network_mode: service:db\n\nvolumes:\n  postgres-data:\n  docker-data:\n"
  },
  {
    "path": ".devcontainer/library-scripts/docker-in-docker-debian.sh",
    "content": "#!/usr/bin/env bash\n#-------------------------------------------------------------------------------------------------------------\n# Copyright (c) Microsoft Corporation. All rights reserved.\n# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.\n#-------------------------------------------------------------------------------------------------------------\n#\n# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md\n# Maintainer: The Dev Container spec maintainers\n\n\nDOCKER_VERSION=\"${VERSION:-\"latest\"}\" # The Docker/Moby Engine + CLI should match in version\nUSE_MOBY=\"${MOBY:-\"true\"}\"\nMOBY_BUILDX_VERSION=\"${MOBYBUILDXVERSION:-\"latest\"}\"\nDOCKER_DASH_COMPOSE_VERSION=\"${DOCKERDASHCOMPOSEVERSION:-\"latest\"}\" #latest, v2 or none\nAZURE_DNS_AUTO_DETECTION=\"${AZUREDNSAUTODETECTION:-\"true\"}\"\nDOCKER_DEFAULT_ADDRESS_POOL=\"${DOCKERDEFAULTADDRESSPOOL:-\"\"}\"\nUSERNAME=\"${USERNAME:-\"${_REMOTE_USER:-\"automatic\"}\"}\"\nINSTALL_DOCKER_BUILDX=\"${INSTALLDOCKERBUILDX:-\"true\"}\"\nINSTALL_DOCKER_COMPOSE_SWITCH=\"${INSTALLDOCKERCOMPOSESWITCH:-\"true\"}\"\nMICROSOFT_GPG_KEYS_URI=\"https://packages.microsoft.com/keys/microsoft.asc\"\nDOCKER_MOBY_ARCHIVE_VERSION_CODENAMES=\"bookworm buster bullseye bionic focal jammy noble\"\nDOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES=\"bookworm buster bullseye bionic focal hirsute impish jammy noble\"\n\n# Default: Exit on any failure.\nset -e\n\n# Clean up\nrm -rf /var/lib/apt/lists/*\n\n# Setup STDERR.\nerr() {\n    echo \"(!) $*\" >&2\n}\n\nif [ \"$(id -u)\" -ne 0 ]; then\n    err 'Script must be run as root. Use sudo, su, or add \"USER root\" to your Dockerfile before running this script.'\n    exit 1\nfi\n\n###################\n# Helper Functions\n# See: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/shared/utils.sh\n###################\n\n# Determine the appropriate non-root user\nif [ \"${USERNAME}\" = \"auto\" ] || [ \"${USERNAME}\" = \"automatic\" ]; then\n    USERNAME=\"\"\n    POSSIBLE_USERS=(\"vscode\" \"node\" \"codespace\" \"$(awk -v val=1000 -F \":\" '$3==val{print $1}' /etc/passwd)\")\n    for CURRENT_USER in \"${POSSIBLE_USERS[@]}\"; do\n        if id -u ${CURRENT_USER} > /dev/null 2>&1; then\n            USERNAME=${CURRENT_USER}\n            break\n        fi\n    done\n    if [ \"${USERNAME}\" = \"\" ]; then\n        USERNAME=root\n    fi\nelif [ \"${USERNAME}\" = \"none\" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then\n    USERNAME=root\nfi\n\napt_get_update()\n{\n    if [ \"$(find /var/lib/apt/lists/* | wc -l)\" = \"0\" ]; then\n        echo \"Running apt-get update...\"\n        apt-get update -y\n    fi\n}\n\n# Checks if packages are installed and installs them if not\ncheck_packages() {\n    if ! dpkg -s \"$@\" > /dev/null 2>&1; then\n        apt_get_update\n        apt-get -y install --no-install-recommends \"$@\"\n    fi\n}\n\n# Figure out correct version of a three part version number is not passed\nfind_version_from_git_tags() {\n    local variable_name=$1\n    local requested_version=${!variable_name}\n    if [ \"${requested_version}\" = \"none\" ]; then return; fi\n    local repository=$2\n    local prefix=${3:-\"tags/v\"}\n    local separator=${4:-\".\"}\n    local last_part_optional=${5:-\"false\"}\n    if [ \"$(echo \"${requested_version}\" | grep -o \".\" | wc -l)\" != \"2\" ]; then\n        local escaped_separator=${separator//./\\\\.}\n        local last_part\n        if [ \"${last_part_optional}\" = \"true\" ]; then\n            last_part=\"(${escaped_separator}[0-9]+)?\"\n        else\n            last_part=\"${escaped_separator}[0-9]+\"\n        fi\n        local regex=\"${prefix}\\\\K[0-9]+${escaped_separator}[0-9]+${last_part}$\"\n        local version_list=\"$(git ls-remote --tags ${repository} | grep -oP \"${regex}\" | tr -d ' ' | tr \"${separator}\" \".\" | sort -rV)\"\n        if [ \"${requested_version}\" = \"latest\" ] || [ \"${requested_version}\" = \"current\" ] || [ \"${requested_version}\" = \"lts\" ]; then\n            declare -g ${variable_name}=\"$(echo \"${version_list}\" | head -n 1)\"\n        else\n            set +e\n                declare -g ${variable_name}=\"$(echo \"${version_list}\" | grep -E -m 1 \"^${requested_version//./\\\\.}([\\\\.\\\\s]|$)\")\"\n            set -e\n        fi\n    fi\n    if [ -z \"${!variable_name}\" ] || ! echo \"${version_list}\" | grep \"^${!variable_name//./\\\\.}$\" > /dev/null 2>&1; then\n        err \"Invalid ${variable_name} value: ${requested_version}\\nValid values:\\n${version_list}\" >&2\n        exit 1\n    fi\n    echo \"${variable_name}=${!variable_name}\"\n}\n\n# Use semver logic to decrement a version number then look for the closest match\nfind_prev_version_from_git_tags() {\n    local variable_name=$1\n    local current_version=${!variable_name}\n    local repository=$2\n    # Normally a \"v\" is used before the version number, but support alternate cases\n    local prefix=${3:-\"tags/v\"}\n    # Some repositories use \"_\" instead of \".\" for version number part separation, support that\n    local separator=${4:-\".\"}\n    # Some tools release versions that omit the last digit (e.g. go)\n    local last_part_optional=${5:-\"false\"}\n    # Some repositories may have tags that include a suffix (e.g. actions/node-versions)\n    local version_suffix_regex=$6\n    # Try one break fix version number less if we get a failure. Use \"set +e\" since \"set -e\" can cause failures in valid scenarios.\n    set +e\n        major=\"$(echo \"${current_version}\" | grep -oE '^[0-9]+' || echo '')\"\n        minor=\"$(echo \"${current_version}\" | grep -oP '^[0-9]+\\.\\K[0-9]+' || echo '')\"\n        breakfix=\"$(echo \"${current_version}\" | grep -oP '^[0-9]+\\.[0-9]+\\.\\K[0-9]+' 2>/dev/null || echo '')\"\n\n        if [ \"${minor}\" = \"0\" ] && [ \"${breakfix}\" = \"0\" ]; then\n            ((major=major-1))\n            declare -g ${variable_name}=\"${major}\"\n            # Look for latest version from previous major release\n            find_version_from_git_tags \"${variable_name}\" \"${repository}\" \"${prefix}\" \"${separator}\" \"${last_part_optional}\"\n        # Handle situations like Go's odd version pattern where \"0\" releases omit the last part\n        elif [ \"${breakfix}\" = \"\" ] || [ \"${breakfix}\" = \"0\" ]; then\n            ((minor=minor-1))\n            declare -g ${variable_name}=\"${major}.${minor}\"\n            # Look for latest version from previous minor release\n            find_version_from_git_tags \"${variable_name}\" \"${repository}\" \"${prefix}\" \"${separator}\" \"${last_part_optional}\"\n        else\n            ((breakfix=breakfix-1))\n            if [ \"${breakfix}\" = \"0\" ] && [ \"${last_part_optional}\" = \"true\" ]; then\n                declare -g ${variable_name}=\"${major}.${minor}\"\n            else\n                declare -g ${variable_name}=\"${major}.${minor}.${breakfix}\"\n            fi\n        fi\n    set -e\n}\n\n# Function to fetch the version released prior to the latest version\nget_previous_version() {\n    local url=$1\n    local repo_url=$2\n    local variable_name=$3\n    prev_version=${!variable_name}\n\n    output=$(curl -s \"$repo_url\");\n    message=$(echo \"$output\" | jq -r '.message')\n\n    if [[ $message == \"API rate limit exceeded\"* ]]; then\n        echo -e \"\\nAn attempt to find latest version using GitHub Api Failed... \\nReason: ${message}\"\n        echo -e \"\\nAttempting to find latest version using GitHub tags.\"\n        find_prev_version_from_git_tags prev_version \"$url\" \"tags/v\"\n        declare -g ${variable_name}=\"${prev_version}\"\n    else\n        echo -e \"\\nAttempting to find latest version using GitHub Api.\"\n        version=$(echo \"$output\" | jq -r '.tag_name')\n        declare -g ${variable_name}=\"${version#v}\"\n    fi\n    echo \"${variable_name}=${!variable_name}\"\n}\n\nget_github_api_repo_url() {\n    local url=$1\n    echo \"${url/https:\\/\\/github.com/https:\\/\\/api.github.com\\/repos}/releases/latest\"\n}\n\n###########################################\n# Start docker-in-docker installation\n###########################################\n\n# Ensure apt is in non-interactive to avoid prompts\nexport DEBIAN_FRONTEND=noninteractive\n\n\n# Source /etc/os-release to get OS info\n. /etc/os-release\n# Fetch host/container arch.\narchitecture=\"$(dpkg --print-architecture)\"\n\n# Check if distro is supported\nif [ \"${USE_MOBY}\" = \"true\" ]; then\n    if [[ \"${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}\" != *\"${VERSION_CODENAME}\"* ]]; then\n        err \"Unsupported  distribution version '${VERSION_CODENAME}'. To resolve, either: (1) set feature option '\\\"moby\\\": false' , or (2) choose a compatible OS distribution\"\n        err \"Support distributions include:  ${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}\"\n        exit 1\n    fi\n    echo \"Distro codename  '${VERSION_CODENAME}'  matched filter  '${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}'\"\nelse\n    if [[ \"${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}\" != *\"${VERSION_CODENAME}\"* ]]; then\n        err \"Unsupported distribution version '${VERSION_CODENAME}'. To resolve, please choose a compatible OS distribution\"\n        err \"Support distributions include:  ${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}\"\n        exit 1\n    fi\n    echo \"Distro codename  '${VERSION_CODENAME}'  matched filter  '${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}'\"\nfi\n\n# Install dependencies\ncheck_packages apt-transport-https curl ca-certificates pigz iptables gnupg2 dirmngr wget jq\nif ! type git > /dev/null 2>&1; then\n    check_packages git\nfi\n\n# Swap to legacy iptables for compatibility\nif type iptables-legacy > /dev/null 2>&1; then\n    update-alternatives --set iptables /usr/sbin/iptables-legacy\n    update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy\nfi\n\n\n\n# Set up the necessary apt repos (either Microsoft's or Docker's)\nif [ \"${USE_MOBY}\" = \"true\" ]; then\n\n    # Name of open source engine/cli\n    engine_package_name=\"moby-engine\"\n    cli_package_name=\"moby-cli\"\n\n    # Import key safely and import Microsoft apt repo\n    curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg\n    echo \"deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main\" > /etc/apt/sources.list.d/microsoft.list\nelse\n    # Name of licensed engine/cli\n    engine_package_name=\"docker-ce\"\n    cli_package_name=\"docker-ce-cli\"\n\n    # Import key safely and import Docker apt repo\n    curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg\n    echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable\" > /etc/apt/sources.list.d/docker.list\nfi\n\n# Refresh apt lists\napt-get update\n\n# Soft version matching\nif [ \"${DOCKER_VERSION}\" = \"latest\" ] || [ \"${DOCKER_VERSION}\" = \"lts\" ] || [ \"${DOCKER_VERSION}\" = \"stable\" ]; then\n    # Empty, meaning grab whatever \"latest\" is in apt repo\n    engine_version_suffix=\"\"\n    cli_version_suffix=\"\"\nelse\n    # Fetch a valid version from the apt-cache (eg: the Microsoft repo appends +azure, breakfix, etc...)\n    docker_version_dot_escaped=\"${DOCKER_VERSION//./\\\\.}\"\n    docker_version_dot_plus_escaped=\"${docker_version_dot_escaped//+/\\\\+}\"\n    # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/\n    docker_version_regex=\"^(.+:)?${docker_version_dot_plus_escaped}([\\\\.\\\\+ ~:-]|$)\"\n    set +e # Don't exit if finding version fails - will handle gracefully\n        cli_version_suffix=\"=$(apt-cache madison ${cli_package_name} | awk -F\"|\" '{print $2}' | sed -e 's/^[ \\t]*//' | grep -E -m 1 \"${docker_version_regex}\")\"\n        engine_version_suffix=\"=$(apt-cache madison ${engine_package_name} | awk -F\"|\" '{print $2}' | sed -e 's/^[ \\t]*//' | grep -E -m 1 \"${docker_version_regex}\")\"\n    set -e\n    if [ -z \"${engine_version_suffix}\" ] || [ \"${engine_version_suffix}\" = \"=\" ] || [ -z \"${cli_version_suffix}\" ] || [ \"${cli_version_suffix}\" = \"=\" ] ; then\n        err \"No full or partial Docker / Moby version match found for \\\"${DOCKER_VERSION}\\\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:\"\n        apt-cache madison ${cli_package_name} | awk -F\"|\" '{print $2}' | grep -oP '^(.+:)?\\K.+'\n        exit 1\n    fi\n    echo \"engine_version_suffix ${engine_version_suffix}\"\n    echo \"cli_version_suffix ${cli_version_suffix}\"\nfi\n\n# Version matching for moby-buildx\nif [ \"${USE_MOBY}\" = \"true\" ]; then\n    if [ \"${MOBY_BUILDX_VERSION}\" = \"latest\" ]; then\n        # Empty, meaning grab whatever \"latest\" is in apt repo\n        buildx_version_suffix=\"\"\n    else\n        buildx_version_dot_escaped=\"${MOBY_BUILDX_VERSION//./\\\\.}\"\n        buildx_version_dot_plus_escaped=\"${buildx_version_dot_escaped//+/\\\\+}\"\n        buildx_version_regex=\"^(.+:)?${buildx_version_dot_plus_escaped}([\\\\.\\\\+ ~:-]|$)\"\n        set +e\n            buildx_version_suffix=\"=$(apt-cache madison moby-buildx | awk -F\"|\" '{print $2}' | sed -e 's/^[ \\t]*//' | grep -E -m 1 \"${buildx_version_regex}\")\"\n        set -e\n        if [ -z \"${buildx_version_suffix}\" ] || [ \"${buildx_version_suffix}\" = \"=\" ]; then\n            err \"No full or partial moby-buildx version match found for \\\"${MOBY_BUILDX_VERSION}\\\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:\"\n            apt-cache madison moby-buildx | awk -F\"|\" '{print $2}' | grep -oP '^(.+:)?\\K.+'\n            exit 1\n        fi\n        echo \"buildx_version_suffix ${buildx_version_suffix}\"\n    fi\nfi\n\n# Install Docker / Moby CLI if not already installed\nif type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then\n    echo \"Docker / Moby CLI and Engine already installed.\"\nelse\n    if [ \"${USE_MOBY}\" = \"true\" ]; then\n        # Install engine\n        set +e # Handle error gracefully\n            apt-get -y install --no-install-recommends moby-cli${cli_version_suffix} moby-buildx${buildx_version_suffix} moby-engine${engine_version_suffix}\n            exit_code=$?\n        set -e\n\n        if [ ${exit_code} -ne 0 ]; then\n            err \"Packages for moby not available in OS ${ID} ${VERSION_CODENAME} (${architecture}). To resolve, either: (1) set feature option '\\\"moby\\\": false' , or (2) choose a compatible OS version (eg: 'ubuntu-20.04').\"\n            exit 1\n        fi\n\n        # Install compose\n        apt-get -y install --no-install-recommends moby-compose || err \"Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping.\"\n    else\n        apt-get -y install --no-install-recommends docker-ce-cli${cli_version_suffix} docker-ce${engine_version_suffix}\n        # Install compose\n        apt-get -y install --no-install-recommends docker-compose-plugin || echo \"(*) Package docker-compose-plugin (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping.\"\n    fi\nfi\n\necho \"Finished installing docker / moby!\"\n\ndocker_home=\"/usr/libexec/docker\"\ncli_plugins_dir=\"${docker_home}/cli-plugins\"\n\n# fallback for docker-compose\nfallback_compose(){\n    local url=$1\n    local repo_url=$(get_github_api_repo_url \"$url\")\n    echo -e \"\\n(!) Failed to fetch the latest artifacts for docker-compose v${compose_version}...\"\n    get_previous_version \"${url}\" \"${repo_url}\" compose_version\n    echo -e \"\\nAttempting to install v${compose_version}\"\n    curl -fsSL \"https://github.com/docker/compose/releases/download/v${compose_version}/docker-compose-linux-${target_compose_arch}\" -o ${docker_compose_path}\n}\n\n# If 'docker-compose' command is to be included\nif [ \"${DOCKER_DASH_COMPOSE_VERSION}\" != \"none\" ]; then\n    case \"${architecture}\" in\n        amd64) target_compose_arch=x86_64 ;;\n        arm64) target_compose_arch=aarch64 ;;\n        *)\n            echo \"(!) Docker in docker does not support machine architecture '$architecture'. Please use an x86-64 or ARM64 machine.\"\n            exit 1\n    esac\n\n    docker_compose_path=\"/usr/local/bin/docker-compose\"\n    if [ \"${DOCKER_DASH_COMPOSE_VERSION}\" = \"v1\" ]; then\n        err \"The final Compose V1 release, version 1.29.2, was May 10, 2021. These packages haven't received any security updates since then. Use at your own risk.\"\n        INSTALL_DOCKER_COMPOSE_SWITCH=\"false\"\n\n        if [ \"${target_compose_arch}\" = \"x86_64\" ]; then\n            echo \"(*) Installing docker compose v1...\"\n            curl -fsSL \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64\" -o ${docker_compose_path}\n            chmod +x ${docker_compose_path}\n\n            # Download the SHA256 checksum\n            DOCKER_COMPOSE_SHA256=\"$(curl -sSL \"https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64.sha256\" | awk '{print $1}')\"\n            echo \"${DOCKER_COMPOSE_SHA256}  ${docker_compose_path}\" > docker-compose.sha256sum\n            sha256sum -c docker-compose.sha256sum --ignore-missing\n        elif [ \"${VERSION_CODENAME}\" = \"bookworm\" ]; then\n            err \"Docker compose v1 is unavailable for 'bookworm' on Arm64. Kindly switch to use v2\"\n            exit 1\n        else\n            # Use pip to get a version that runs on this architecture\n            check_packages python3-minimal python3-pip libffi-dev python3-venv\n            echo \"(*) Installing docker compose v1 via pip...\"\n            export PYTHONUSERBASE=/usr/local\n            pip3 install --disable-pip-version-check --no-cache-dir --user \"Cython<3.0\" pyyaml wheel docker-compose --no-build-isolation\n        fi\n    else\n        compose_version=${DOCKER_DASH_COMPOSE_VERSION#v}\n        docker_compose_url=\"https://github.com/docker/compose\"\n        find_version_from_git_tags compose_version \"$docker_compose_url\" \"tags/v\"\n        echo \"(*) Installing docker-compose ${compose_version}...\"\n        curl -fsSL \"https://github.com/docker/compose/releases/download/v${compose_version}/docker-compose-linux-${target_compose_arch}\" -o ${docker_compose_path} || {\n            if [[ $DOCKER_DASH_COMPOSE_VERSION == \"latest\" ]]; then\n                fallback_compose \"$docker_compose_url\"\n            else\n                echo -e \"Error: Failed to install docker-compose v${compose_version}\"\n            fi\n        }\n\n        chmod +x ${docker_compose_path}\n\n        # Download the SHA256 checksum\n        DOCKER_COMPOSE_SHA256=\"$(curl -sSL \"https://github.com/docker/compose/releases/download/v${compose_version}/docker-compose-linux-${target_compose_arch}.sha256\" | awk '{print $1}')\"\n        echo \"${DOCKER_COMPOSE_SHA256}  ${docker_compose_path}\" > docker-compose.sha256sum\n        sha256sum -c docker-compose.sha256sum --ignore-missing\n\n        mkdir -p ${cli_plugins_dir}\n        cp ${docker_compose_path} ${cli_plugins_dir}\n    fi\nfi\n\n# fallback method for compose-switch\nfallback_compose-switch() {\n    local url=$1\n    local repo_url=$(get_github_api_repo_url \"$url\")\n    echo -e \"\\n(!) Failed to fetch the latest artifacts for compose-switch v${compose_switch_version}...\"\n    get_previous_version \"$url\" \"$repo_url\" compose_switch_version\n    echo -e \"\\nAttempting to install v${compose_switch_version}\"\n    curl -fsSL \"https://github.com/docker/compose-switch/releases/download/v${compose_switch_version}/docker-compose-linux-${architecture}\" -o /usr/local/bin/compose-switch\n}\n\n# Install docker-compose switch if not already installed - https://github.com/docker/compose-switch#manual-installation\nif [ \"${INSTALL_DOCKER_COMPOSE_SWITCH}\" = \"true\" ] && ! type compose-switch > /dev/null 2>&1; then\n    if type docker-compose > /dev/null 2>&1; then\n        echo \"(*) Installing compose-switch...\"\n        current_compose_path=\"$(which docker-compose)\"\n        target_compose_path=\"$(dirname \"${current_compose_path}\")/docker-compose-v1\"\n        compose_switch_version=\"latest\"\n        compose_switch_url=\"https://github.com/docker/compose-switch\"\n        find_version_from_git_tags compose_switch_version \"$compose_switch_url\"\n        curl -fsSL \"https://github.com/docker/compose-switch/releases/download/v${compose_switch_version}/docker-compose-linux-${architecture}\" -o /usr/local/bin/compose-switch || fallback_compose-switch \"$compose_switch_url\"\n        chmod +x /usr/local/bin/compose-switch\n        # TODO: Verify checksum once available: https://github.com/docker/compose-switch/issues/11\n        # Setup v1 CLI as alternative in addition to compose-switch (which maps to v2)\n        mv \"${current_compose_path}\" \"${target_compose_path}\"\n        update-alternatives --install ${docker_compose_path} docker-compose /usr/local/bin/compose-switch 99\n        update-alternatives --install ${docker_compose_path} docker-compose \"${target_compose_path}\" 1\n    else\n        err \"Skipping installation of compose-switch as docker compose is unavailable...\"\n    fi\nfi\n\n# If init file already exists, exit\nif [ -f \"/usr/local/share/docker-init.sh\" ]; then\n    echo \"/usr/local/share/docker-init.sh already exists, so exiting.\"\n    # Clean up\n    rm -rf /var/lib/apt/lists/*\n    exit 0\nfi\necho \"docker-init doesn't exist, adding...\"\n\nif ! cat /etc/group | grep -e \"^docker:\" > /dev/null 2>&1; then\n        groupadd -r docker\nfi\n\nusermod -aG docker ${USERNAME}\n\n# fallback for docker/buildx\nfallback_buildx() {\n    local url=$1\n    local repo_url=$(get_github_api_repo_url \"$url\")\n    echo -e \"\\n(!) Failed to fetch the latest artifacts for docker buildx v${buildx_version}...\"\n    get_previous_version \"$url\" \"$repo_url\" buildx_version\n    buildx_file_name=\"buildx-v${buildx_version}.linux-${architecture}\"\n    echo -e \"\\nAttempting to install v${buildx_version}\"\n    wget https://github.com/docker/buildx/releases/download/v${buildx_version}/${buildx_file_name}\n}\n\nif [ \"${INSTALL_DOCKER_BUILDX}\" = \"true\" ]; then\n    buildx_version=\"latest\"\n    docker_buildx_url=\"https://github.com/docker/buildx\"\n    find_version_from_git_tags buildx_version \"$docker_buildx_url\" \"refs/tags/v\"\n    echo \"(*) Installing buildx ${buildx_version}...\"\n    buildx_file_name=\"buildx-v${buildx_version}.linux-${architecture}\"\n\n    cd /tmp\n    wget https://github.com/docker/buildx/releases/download/v${buildx_version}/${buildx_file_name} || fallback_buildx \"$docker_buildx_url\"\n\n    docker_home=\"/usr/libexec/docker\"\n    cli_plugins_dir=\"${docker_home}/cli-plugins\"\n\n    mkdir -p ${cli_plugins_dir}\n    mv ${buildx_file_name} ${cli_plugins_dir}/docker-buildx\n    chmod +x ${cli_plugins_dir}/docker-buildx\n\n    chown -R \"${USERNAME}:docker\" \"${docker_home}\"\n    chmod -R g+r+w \"${docker_home}\"\n    find \"${docker_home}\" -type d -print0 | xargs -n 1 -0 chmod g+s\nfi\n\ntee /usr/local/share/docker-init.sh > /dev/null \\\n<< EOF\n#!/bin/sh\n#-------------------------------------------------------------------------------------------------------------\n# Copyright (c) Microsoft Corporation. All rights reserved.\n# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.\n#-------------------------------------------------------------------------------------------------------------\n\nset -e\n\nAZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION}\nDOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL}\nEOF\n\ntee -a /usr/local/share/docker-init.sh > /dev/null \\\n<< 'EOF'\ndockerd_start=\"AZURE_DNS_AUTO_DETECTION=${AZURE_DNS_AUTO_DETECTION} DOCKER_DEFAULT_ADDRESS_POOL=${DOCKER_DEFAULT_ADDRESS_POOL} $(cat << 'INNEREOF'\n    # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly\n    find /run /var/run -iname 'docker*.pid' -delete || :\n    find /run /var/run -iname 'container*.pid' -delete || :\n\n    # -- Start: dind wrapper script --\n    # Maintained: https://github.com/moby/moby/blob/master/hack/dind\n\n    export container=docker\n\n    if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then\n        mount -t securityfs none /sys/kernel/security || {\n            echo >&2 'Could not mount /sys/kernel/security.'\n            echo >&2 'AppArmor detection and --privileged mode might break.'\n        }\n    fi\n\n    # Mount /tmp (conditionally)\n    if ! mountpoint -q /tmp; then\n        mount -t tmpfs none /tmp\n    fi\n\n    set_cgroup_nesting()\n    {\n        # cgroup v2: enable nesting\n        if [ -f /sys/fs/cgroup/cgroup.controllers ]; then\n            # move the processes from the root group to the /init group,\n            # otherwise writing subtree_control fails with EBUSY.\n            # An error during moving non-existent process (i.e., \"cat\") is ignored.\n            mkdir -p /sys/fs/cgroup/init\n            xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || :\n            # enable controllers\n            sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \\\n                > /sys/fs/cgroup/cgroup.subtree_control\n        fi\n    }\n\n    # Set cgroup nesting, retrying if necessary\n    retry_cgroup_nesting=0\n\n    until [ \"${retry_cgroup_nesting}\" -eq \"5\" ];\n    do\n        set +e\n            set_cgroup_nesting\n\n            if [ $? -ne 0 ]; then\n                echo \"(*) cgroup v2: Failed to enable nesting, retrying...\"\n            else\n                break\n            fi\n\n            retry_cgroup_nesting=`expr $retry_cgroup_nesting + 1`\n        set -e\n    done\n\n    # -- End: dind wrapper script --\n\n    # Handle DNS\n    set +e\n        cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' > /dev/null 2>&1\n        if [ $? -eq 0 ] && [ \"${AZURE_DNS_AUTO_DETECTION}\" = \"true\" ]\n        then\n            echo \"Setting dockerd Azure DNS.\"\n            CUSTOMDNS=\"--dns 168.63.129.16\"\n        else\n            echo \"Not setting dockerd DNS manually.\"\n            CUSTOMDNS=\"\"\n        fi\n    set -e\n\n    if [ -z \"$DOCKER_DEFAULT_ADDRESS_POOL\" ]\n    then\n        DEFAULT_ADDRESS_POOL=\"\"\n    else\n        DEFAULT_ADDRESS_POOL=\"--default-address-pool $DOCKER_DEFAULT_ADDRESS_POOL\"\n    fi\n\n    # Start docker/moby engine\n    ( dockerd $CUSTOMDNS $DEFAULT_ADDRESS_POOL > /tmp/dockerd.log 2>&1 ) &\nINNEREOF\n)\"\n\nsudo_if() {\n    COMMAND=\"$*\"\n\n    if [ \"$(id -u)\" -ne 0 ]; then\n        sudo $COMMAND\n    else\n        $COMMAND\n    fi\n}\n\nretry_docker_start_count=0\ndocker_ok=\"false\"\n\nuntil [ \"${docker_ok}\" = \"true\"  ] || [ \"${retry_docker_start_count}\" -eq \"5\" ];\ndo\n    # Start using sudo if not invoked as root\n    if [ \"$(id -u)\" -ne 0 ]; then\n        sudo /bin/sh -c \"${dockerd_start}\"\n    else\n        eval \"${dockerd_start}\"\n    fi\n\n    retry_count=0\n    until [ \"${docker_ok}\" = \"true\"  ] || [ \"${retry_count}\" -eq \"5\" ];\n    do\n        sleep 1s\n        set +e\n            docker info > /dev/null 2>&1 && docker_ok=\"true\"\n        set -e\n\n        retry_count=`expr $retry_count + 1`\n    done\n\n    if [ \"${docker_ok}\" != \"true\" ] && [ \"${retry_docker_start_count}\" != \"4\" ]; then\n        echo \"(*) Failed to start docker, retrying...\"\n        set +e\n            sudo_if pkill dockerd\n            sudo_if pkill containerd\n        set -e\n    fi\n\n    retry_docker_start_count=`expr $retry_docker_start_count + 1`\ndone\n\n# Execute whatever commands were passed in (if any). This allows us\n# to set this script to ENTRYPOINT while still executing the default CMD.\nexec \"$@\"\nEOF\n\nchmod +x /usr/local/share/docker-init.sh\nchown ${USERNAME}:root /usr/local/share/docker-init.sh\n\n# Clean up\nrm -rf /var/lib/apt/lists/*\n\necho 'docker-in-docker-debian script has completed!'"
  },
  {
    "path": ".devcontainer/postinstall.sh",
    "content": "#!/bin/bash\n\necho \"Running postinstall.sh\"\n\n# Aggressively clean npm and corepack caches\nnpm cache clean -f\nsudo rm -rf /tmp/corepack-cache\nsudo rm -rf /usr/local/lib/node_modules/corepack # Manually remove global corepack\n\n# Reinstall corepack globally via npm\nnpm install -g corepack@latest --force # Install latest corepack version\nsudo corepack enable # Re-enable corepack\n\n# Check corepack version after reinstall\ncorepack --version\n\n# Prepare pnpm (again, after corepack reinstall)\ncorepack prepare pnpm@9.14.4 --activate\n\n# Go to workspace directory\ncd /workspaces/webstudio\n\n# Configure pnpm store directory\npnpm config set store-dir $HOME/.pnpm-store\n\n# Clean up directories (optional)\nfind . -name 'node_modules' -type d -prune -exec rm -rf '{}' +\nfind . -name 'lib' -type d -prune -exec rm -rf '{}' +\nfind . -name 'build' -type d -prune -exec rm -rf '{}' +\nfind . -name 'dist' -type d -prune -exec rm -rf '{}' +\nfind . -name '.cache' -type d -prune -exec rm -rf '{}' +\nfind . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +\n\n# Install dependencies, build, and migrate\npnpm install\npnpm build\npnpm migrations migrate\n\n# Add git aliases\ncat << 'EOF' >> /home/node/.bashrc\nalias gitclean=\"(git remote | xargs git remote prune) && git branch -vv | egrep '('\\$(git remote | xargs | sed -e 's/ /|/g')')/.*: gone]' | awk '{print \\$1}'  | xargs -r git branch -D\"\nalias gitrebase=\"git rebase --interactive main\"\nEOF\n\n# Symlink workspace GitHub instructions to mounted user instructions (for Copilot)\nif [ -d \"/home/node/.github/instructions\" ]; then\n  mkdir -p /workspaces/webstudio/.github\n  ln -sfn /home/node/.github/instructions /workspaces/webstudio/.github/instructions\nfi\n\n\necho \"postinstall.sh finished\"\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\n"
  },
  {
    "path": ".github/actions/add-status/action.yaml",
    "content": "name: Add status to commit\ndescription: Add status to commit\ninputs:\n  url:\n    description: \"URL\"\n    required: true\n  title:\n    description: \"Title\"\n    required: true\n  description:\n    description: \"Description\"\n    required: true\n\nruns:\n  using: \"composite\"\n  steps:\n    - name: Add URL to vercel deployment through *.prs.webstudio.is\n      uses: actions/github-script@v7\n      with:\n        script: |\n          const branch = context.payload.pull_request?.head?.ref ?? context.payload.ref?.replace('refs/heads/', '')\n          const sha = context.payload.pull_request?.head?.sha ?? context.sha;\n\n          const status = {\n            state: 'success',\n            target_url: '${{ inputs.url }}',\n            description: '${{ inputs.description }}',\n            context: '${{ inputs.title }}'\n          };\n\n          github.rest.repos.createCommitStatus({\n            ...context.repo,\n            sha,\n            ...status\n          });\n"
  },
  {
    "path": ".github/actions/ci-setup/action.yml",
    "content": "name: CI setup\n\ndescription: |\n  Sets up the CI environment for the project.\n\nruns:\n  using: \"composite\"\n\n  steps:\n    - uses: pnpm/action-setup@v4\n    - uses: actions/setup-node@v4\n      with:\n        node-version: 20\n        cache: pnpm\n    - run: pnpm install --frozen-lockfile --ignore-scripts\n      shell: bash\n"
  },
  {
    "path": ".github/actions/submodules-checkout/action.yml",
    "content": "name: CI setup\n\ndescription: |\n  Sets up the CI environment for the project.\n\ninputs:\n  submodules-ssh-key:\n    description: \"The SSH key to private submodules to use for the checkout\"\n    required: true\n\nruns:\n  using: \"composite\"\n\n  steps:\n    - name: Set up SSH for Git\n      if: ${{ inputs.submodules-ssh-key }}\n      run: |\n        mkdir -p ~/.ssh\n        echo \"${{ inputs.submodules-ssh-key }}\" > ~/.ssh/id_ed25519\n        chmod 600 ~/.ssh/id_ed25519\n        ssh-keyscan github.com >> ~/.ssh/known_hosts\n      shell: bash\n\n    - name: Verify SSH Connection (Optional)\n      if: ${{ inputs.submodules-ssh-key }}\n      run: |\n        ssh -T git@github.com || true\n      shell: bash\n\n    - name: Verify SSH Connection (Optional)\n      if: ${{ inputs.submodules-ssh-key }}\n      run: |\n        echo Branch is ${{ github.event.pull_request.head.ref || github.ref_name }}\n      shell: bash\n\n    - name: Try checkout submodules to the same branch as main repo\n      if: ${{ inputs.submodules-ssh-key }}\n      run: |\n        ./submodules.sh ${{ github.event.pull_request.head.ref || github.ref_name }}\n      shell: bash\n\n    - name: Show main readme\n      if: ${{ inputs.submodules-ssh-key }}\n      run: |\n        cat ./packages/sdk-components-animation/private-src/README.md || echo \"No README found\"\n      shell: bash\n"
  },
  {
    "path": ".github/actions/vercel/action.yaml",
    "content": "name: \"VERCEL BUILD AND DEPLOY\"\ndescription: \"Builds and deploy vercel project\"\n\ninputs:\n  vercel-token:\n    description: \"Vercel token\"\n    required: true\n  vercel-org-id:\n    description: \"Vercel Organization ID\"\n    required: true\n  vercel-project-id:\n    description: \"Vercel Project ID\"\n    required: true\n  ref-name:\n    description: \"Branch\"\n    required: true\n  sha:\n    description: \"Sha\"\n    required: true\n  environment:\n    description: \"Sha\"\n    required: true\n\noutputs:\n  domain:\n    description: \"Domain\"\n    value: ${{ steps.deploy.outputs.domain }}\n  inspect-url:\n    description: \"Inspect URL\"\n    value: ${{ steps.deploy.outputs.inspect-url }}\n  alias:\n    description: \"Alias\"\n    value: ${{ steps.alias.outputs.value }}\n\nruns:\n  using: \"composite\"\n  steps:\n    - id: branch\n      run: |\n        CLEAN_NAME=\"${REF_NAME/.staging/}\"\n        CLEAN_NAME=$( echo \"${CLEAN_NAME}\" | sed 's/[^a-zA-Z0-9_-]//g' | tr A-Z a-z | tr _ - | sed 's/-\\{2,\\}/-/g' )\n        echo \"value=${CLEAN_NAME}\" >> $GITHUB_OUTPUT\n      shell: bash\n      env:\n        REF_NAME: ${{ inputs.ref-name }}\n\n    - id: short_sha\n      run: |\n        SHORT_SHA=$( echo \"value=$(echo ${{ inputs.sha }} | cut -c1-7)\" )\n        echo \"value=${SHORT_SHA}\" >> $GITHUB_OUTPUT\n      shell: bash\n\n    - name: CREATE VERCEL PROJECT FILE\n      run: |\n        mkdir -p .vercel\n        cat <<\"EOF\" > .vercel/project.json\n        {\n          \"projectId\": \"${{ inputs.vercel-project-id }}\",\n          \"orgId\": \"${{ inputs.vercel-org-id }}\",\n          \"settings\": {\n            \"framework\": \"remix\",\n            \"devCommand\": \"pnpm dev\",\n            \"installCommand\": \"pnpm install\",\n            \"buildCommand\": \"pnpm --filter=@webstudio-is/http-client build && pnpm --filter=@webstudio-is/builder build\",\n            \"outputDirectory\": null,\n            \"rootDirectory\": \"apps/builder\",\n            \"directoryListing\": false,\n            \"nodeVersion\": \"20.x\"\n          }\n        }\n        EOF\n      shell: bash\n\n    - name: Build\n      run: |\n        export GITHUB_SHA=${{ inputs.sha }}\n        export GITHUB_REF_NAME=${{ inputs.ref-name }}\n\n        pnpx vercel build\n      shell: bash\n\n    - name: Patch\n      run: |\n        # When we deploy on Vercel, it generates a URL like webstudio-saas-mahqcavgo-getwebstudio.vercel.app.\n        # We use the alias oauth-wstd-00-staging.vercel.app for routing, which maps to oauth.staging.webstudio.is on the worker.\n        # Issue: Vercel proxies also set x-forwarded-host, which overrides our header.\n        # We are adding x-forwarded-ws-host on the worker as a workaround, but issues with request.url persist.\n        # Remix and the Vercel adapter lack support for header selection https://github.com/vercel/vercel/blob/9d4d4b6deb6294506016106f78e71f1984adcc7f/packages/remix/defaults/server-node.mjs#L44\n        # Patching server-node.mjs directly without installing Vercel CLI was unsuccessful, so we are using string replacement instead.\n\n        mapfile -t matching_files < <(grep -rl \"req\\.headers\\['x-forwarded-host'\\] || req\\.headers\\.host\" \"./apps/builder/build\")\n        if [ ${#matching_files[@]} -eq 0 ]; then\n          echo \"No files found containing the specified string.\"\n          exit 1\n        fi\n\n        echo \"Files containing 'req.headers['x-forwarded-host'] || req.headers.host':\"\n        printf '%s\\n' \"${matching_files[@]}\"\n\n        find ./apps/builder/build -type f -exec sed -i \"s/req\\.headers\\['x-forwarded-host'\\] || req\\.headers\\.host/req.headers['x-forwarded-ws-host'] || req.headers['x-forwarded-host'] || req.headers.host/g\" {} +\n      shell: bash\n\n    - name: Deploy\n      id: deploy\n      run: |\n        pnpx vercel deploy \\\n        --prebuilt \\\n        --token ${{ inputs.vercel-token }} \\\n        2> >(tee info.txt >&2) | tee domain.txt\n\n        echo \"domain=$(cat ./domain.txt)\" >> $GITHUB_OUTPUT\n        echo \"inspect-url=$(cat info.txt | grep 'Inspect:' | awk '{print $2}')\" >> $GITHUB_OUTPUT\n\n      shell: bash\n\n    - name: Set Alias\n      id: alias\n      run: |\n        ALIAS=\"${{ steps.branch.outputs.value }}\"\n\n        pnpx vercel alias set \\\n        \"${{ steps.deploy.outputs.domain }}\" \\\n        \"${ALIAS}-wstd-00-${{ inputs.environment }}\" \\\n        --token ${{ inputs.vercel-token }} \\\n        --scope getwebstudio\n\n        echo \"value=${ALIAS}\" >> $GITHUB_OUTPUT\n      shell: bash\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for more information:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n# https://containers.dev/guide/dependabot\n\nversion: 2\nupdates:\n - package-ecosystem: \"devcontainers\"\n   directory: \"/\"\n   schedule:\n     interval: weekly\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Description\n\n1. What is this PR about (link the issue and add a short description)\n\n## Steps for reproduction\n\n1. click button\n2. expect xyz\n\n## Code Review\n\n- [ ] hi @kof, I need you to do\n  - conceptual review (architecture, feature-correctness)\n  - detailed review (read every line)\n  - test it on preview\n\n## Before requesting a review\n\n- [ ] made a self-review\n- [ ] added inline comments where things may be not obvious (the \"why\", not \"what\")\n\n## Before merging\n\n- [ ] tested locally and on preview environment (preview dev login: 0000)\n- [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document\n- [ ] added tests\n- [ ] if any new env variables are added, added them to `.env` file\n"
  },
  {
    "path": ".github/workflows/build-figma-tokens.yml",
    "content": "name: Build and commit Figma tokens\n\non:\n  push:\n    branches:\n      - figma-tokens\n    paths:\n      - packages/design-system/src/__generated__/figma-design-tokens.json\n\njobs:\n  main:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          # We don't need a token to push from an action,\n          # but we need it if want the commit to trigger other workflows as normal\n          token: ${{ secrets.ACCESS_TOKEN_FOR_FIGMA_TOKENS }}\n\n      - uses: ./.github/actions/ci-setup\n\n      - name: Configure git\n        run: |\n          git config --global user.name 'Bot (build-figma-tokens.yml)'\n          git config --global user.email 'bot@localhost'\n\n      - name: Switch branch\n        run: git checkout figma-tokens\n\n      - name: Build tokens\n        run: pnpm build-figma-tokens\n\n      - name: Commit and push\n        run: |\n          [[ -z `git status | grep figma-design-tokens.ts` ]] || git commit -m \"Update figma-design-tokens.ts\" packages/design-system/src/__generated__/figma-design-tokens.ts\n          git push\n"
  },
  {
    "path": ".github/workflows/check-submodules.yml",
    "content": "name: Check submodules\n\non:\n  pull_request:\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  checks:\n    timeout-minutes: 20\n\n    environment:\n      name: development\n\n    env:\n      DATABASE_URL: postgres://\n      AUTH_SECRET: test\n\n    runs-on: ubuntu-24.04-arm\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - name: Check if any submodule branch matches github.ref_name\n        run: |\n          echo \"C ${{ github.workflow }}-${{ github.event.number || github.sha }}\"\n          # Get the current branch or tag name\n          REF_NAME=\"${{ github.event.pull_request.head.ref || github.ref_name }}\"\n\n          echo \"Branch is:\" $REF_NAME\n\n          # List all submodule paths\n          SUBMODULES=$(git submodule status | awk '{print $2}')\n\n          # Check each submodule's branch\n          for SUBMODULE in $SUBMODULES; do\n            echo \"Checking submodule: $SUBMODULE\"\n            (\n              cd \"$SUBMODULE\"\n              # Get the current branch of the submodule\n              SUBMODULE_BRANCH=$(git rev-parse --abbrev-ref HEAD)\n              echo \"Submodule branch: $SUBMODULE_BRANCH\"\n\n              # Compare the submodule branch to the ref_name\n              if [ \"$SUBMODULE_BRANCH\" = \"$REF_NAME\" ]; then\n                echo \"::error::Submodule '$SUBMODULE' is on branch '$SUBMODULE_BRANCH', which matches the current ref '$REF_NAME'.\"\n                exit 1\n              fi\n            )\n            if [ $? -ne 0 ]; then\n              exit 1 # Fail the workflow if any submodule branch matches\n            fi\n          done\n\n          echo \"No submodule is on the same branch as the current ref '$REF_NAME'.\"\n"
  },
  {
    "path": ".github/workflows/chromatic.yml",
    "content": "name: Chromatic\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  pull_request_target:\n    types: [labeled]\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  chromatic:\n    # Run on push, regular PR (for repo branches), or labeled PR with safe-to-deploy (for forks)\n    if: |\n      github.event_name == 'push' ||\n      github.event_name == 'pull_request' ||\n      (github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-deploy')\n    \n    timeout-minutes: 20\n\n    runs-on: ubuntu-latest\n\n    environment:\n      name: development\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 2 # we need to fetch at least parent commit to satisfy Chromatic\n          ref: ${{ github.event.pull_request.head.sha || github.sha }} # HEAD commit instead of merge commit\n\n      # Storybook with submodules\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: ./.github/actions/ci-setup\n\n      - name: Chromatic\n        id: chromatic\n        uses: chromaui/action@v11.3.0\n        with:\n          projectToken: bea8dc1981d4\n          buildScriptName: storybook:build\n"
  },
  {
    "path": ".github/workflows/cli-r2-static.yaml",
    "content": "name: CLI R2 SSG\n\non:\n  push:\n    branches:\n      - \"*.staging\"\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: vercel-cli-r2-static-${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n  statuses: write # This is required for the GitHub Script createCommitStatus to work\n  packages: write\n\njobs:\n  build:\n    env:\n      COMPATIBILITY_DATE: 2024-04-10\n\n    environment:\n      name: \"staging\"\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.sha }} # HEAD commit instead of merge commit\n\n      # We need submodules here as this is used for the cloudflare build\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: pnpm/action-setup@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      # TRY FIX cloudlare incident\n      - uses: unfor19/install-aws-cli-action@v1\n        with:\n          version: \"2.22.35\"\n          verbose: false\n          arch: amd64\n\n      - name: pnpm instal\n        run: pnpm install --ignore-scripts\n\n      - name: pnpm build\n        run: pnpm --filter 'ssg^...' run build\n\n      # Ideally, execute 'pnpm deploy --prod', but @remix-run/dev doesn't support this flag.\n      # Despite being listed as a dependency, @remix-run/dev does not install the remix cli.\n      # TODO: Minimize artefact size due to frequent downloads on each publish.\n      - name: pnpm deploy\n        run: pnpm --filter 'ssg' deploy \"${{ github.workspace }}/../ssg-template\"\n\n      - name: Make archive\n        run: |\n          tar --use-compress-program=\"zstd -19\" -cf ssg-template.tar.zst ssg-template\n        working-directory: ${{ github.workspace }}/..\n\n      - name: Copy artifact\n        run: |\n          # For staging\n          aws s3 cp ssg-template.tar.zst \"s3://${ARTEFACT_BUCKET_NAME}/public/ssg-template/${{ github.ref_name }}.tar.zst\"\n\n          # For production can be cached forever\n          aws s3 cp \\\n          --metadata-directive REPLACE --cache-control \"public,max-age=31536102,immutable\" \\\n          ssg-template.tar.zst \"s3://${ARTEFACT_BUCKET_NAME}/public/ssg-template/${{ github.sha }}.tar.zst\"\n\n        working-directory: ${{ github.workspace }}/..\n        env:\n          AWS_ENDPOINT_URL_S3: ${{ secrets.AWS_ENDPOINT_URL_S3 }}\n          AWS_REGION: ${{ secrets.AWS_REGION }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          ARTEFACT_BUCKET_NAME: ${{ secrets.ARTEFACT_BUCKET_NAME }}\n\n  checks:\n    environment:\n      name: \"staging\"\n\n    runs-on: ubuntu-latest\n\n    needs: build\n\n    steps:\n      - uses: pnpm/action-setup@v4\n        with:\n          version: \"9\"\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Copy atrifact via http\n        run: curl -o ssg-template.tar.zst ${{ secrets.ARTEFACT_BUCKET_URL }}/public/ssg-template/${{ github.ref_name }}.tar.zst\n\n      - name: Extract archive\n        run: tar --use-compress-program=\"zstd -d\" -xf ssg-template.tar.zst -C .\n\n      - name: Webstudio Build\n        run: pnpm webstudio build --template ssg --template internal\n        working-directory: ${{ github.workspace }}/ssg-template\n\n      - name: Build\n        run: pnpm build\n        working-directory: ${{ github.workspace }}/ssg-template\n"
  },
  {
    "path": ".github/workflows/cli-r2.yaml",
    "content": "name: CLI R2\n\non:\n  push:\n    branches:\n      - \"*.staging\"\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: vercel-cli-r2-${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n  statuses: write # This is required for the GitHub Script createCommitStatus to work\n  packages: write\n  deployments: write\n\njobs:\n  build:\n    env:\n      COMPATIBILITY_DATE: 2024-04-10\n\n    environment:\n      name: \"staging\"\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.sha }} # HEAD commit instead of merge commit\n\n      # We need submodules here as this is used for the cloudflare build\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: pnpm/action-setup@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      # TRY FIX cloudlare incident\n      - uses: unfor19/install-aws-cli-action@v1\n        with:\n          version: \"2.22.35\"\n          verbose: false\n          arch: amd64\n\n      - name: pnpm instal\n        run: pnpm install --ignore-scripts\n\n      - name: pnpm build\n        run: pnpm --filter 'webstudio-cloudflare-template^...' run build\n\n      # Ideally, execute 'pnpm deploy --prod', but @remix-run/dev doesn't support this flag.\n      # Despite being listed as a dependency, @remix-run/dev does not install the remix cli.\n      # TODO: Minimize artefact size due to frequent downloads on each publish.\n      - name: pnpm deploy\n        run: pnpm --filter 'webstudio-cloudflare-template' deploy \"${{ github.workspace }}/../cloudflare-template\"\n\n      - name: Make archive\n        run: |\n          tar --use-compress-program=\"zstd -19\" -cf cloudflare-template.tar.zst cloudflare-template\n        working-directory: ${{ github.workspace }}/..\n\n      - name: Copy artifact\n        run: |\n          # For staging\n          aws s3 cp cloudflare-template.tar.zst \"s3://${ARTEFACT_BUCKET_NAME}/public/cloudflare-template/${{ github.ref_name }}.tar.zst\"\n\n          # For production can be cached forever\n          aws s3 cp \\\n          --metadata-directive REPLACE --cache-control \"public,max-age=31536102,immutable\" \\\n          cloudflare-template.tar.zst \"s3://${ARTEFACT_BUCKET_NAME}/public/cloudflare-template/${{ github.sha }}.tar.zst\"\n\n        working-directory: ${{ github.workspace }}/..\n        env:\n          AWS_ENDPOINT_URL_S3: ${{ secrets.AWS_ENDPOINT_URL_S3 }}\n          AWS_REGION: ${{ secrets.AWS_REGION }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          ARTEFACT_BUCKET_NAME: ${{ secrets.ARTEFACT_BUCKET_NAME }}\n\n  checks:\n    environment:\n      name: \"staging\"\n\n    runs-on: ubuntu-latest\n\n    needs: build\n\n    steps:\n      - uses: pnpm/action-setup@v4\n        with:\n          version: \"9\"\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Copy atrifact via http\n        run: curl -o cloudflare-template.tar.zst ${{ secrets.ARTEFACT_BUCKET_URL }}/public/cloudflare-template/${{ github.ref_name }}.tar.zst\n\n      - name: Extract archive\n        run: tar --use-compress-program=\"zstd -d\" -xf cloudflare-template.tar.zst -C .\n\n      - name: Webstudio Build\n        run: pnpm webstudio build --template internal --template saas-helpers --template cloudflare --assets false\n        working-directory: ${{ github.workspace }}/cloudflare-template\n\n      - name: Remix Build\n        run: pnpm build\n        working-directory: ${{ github.workspace }}/cloudflare-template\n\n      - name: WRANGLER Build\n        run: |\n          NODE_ENV=production pnpm wrangler deploy \\\n          --name build \\\n          --compatibility-date '${COMPATIBILITY_DATE}' \\\n          --minify true \\\n          --logpush true \\\n          --dry-run \\\n          --outdir dist \\\n          './functions/[[path]].ts'\n\n        working-directory: ${{ github.workspace }}/cloudflare-template\n\n  delete-github-deployments:\n    needs: checks\n    uses: ./.github/workflows/delete-github-deployments.yml\n    with:\n      ref: ${{ github.ref_name }}\n"
  },
  {
    "path": ".github/workflows/delete-github-deployments.yml",
    "content": "# https://github.com/orgs/community/discussions/36919\nname: Delete github deployments\n\non:\n  workflow_call:\n    inputs:\n      ref:\n        type: string\n        required: true\n\npermissions:\n  deployments: write\n\njobs:\n  delete_github_deployments:\n    runs-on: ubuntu-latest\n    if: ${{ always() }}\n    steps:\n      - name: Delete Previous deployments\n        uses: actions/github-script@v7\n        env:\n          REF: ${{ inputs.ref }}\n        with:\n          script: |\n            const { REF } = process.env;\n\n            console.log(REF);\n\n            const deployments = await github.rest.repos.listDeployments({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              ref: REF,\n              per_page: 100\n            });\n\n            console.log(deployments);\n\n            await Promise.allSettled(\n              deployments.data.map(async (deployment) => {\n                await github.rest.repos.createDeploymentStatus({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  deployment_id: deployment.id,\n                  state: 'inactive'\n                });\n                return github.rest.repos.deleteDeployment({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  deployment_id: deployment.id\n                });\n              })\n            );\n"
  },
  {
    "path": ".github/workflows/fixtures-test.yml",
    "content": "name: Fixtures tests\n\non:\n  workflow_call:\n    inputs:\n      builder-url:\n        required: true\n        type: string\n      builder-host:\n        required: true\n        type: string\n      environment:\n        required: true\n        type: string\n    secrets:\n      PRIVATE_GITHUB_DEPLOY_TOKEN:\n        required: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  checks:\n    timeout-minutes: 20\n\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest]\n\n    runs-on: ${{ matrix.os }}\n\n    environment:\n      name: ${{ inputs.environment }}\n\n    env:\n      DATABASE_URL: postgres://\n      AUTH_SECRET: test\n      BUILDER_URL_DEPRECATED: ${{ inputs.builder-url }}\n      BUILDER_HOST: ${{ inputs.builder-host }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n      # Test that everything is working with submodules\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: ./.github/actions/ci-setup\n\n        # Testing fixtures for vercel template\n      - name: Test cli --help flag\n        working-directory: ./fixtures/webstudio-features\n        run: pnpm cli --help\n\n      - name: Testing cli link command\n        run: pnpm --filter='./fixtures/*' --sequential run fixtures:link\n\n      - name: Testing cli sync command\n        run: pnpm --filter='./fixtures/*' run --parallel fixtures:sync\n\n      - name: Testing cli build command\n        run: pnpm --filter='./fixtures/*' run --parallel fixtures:build\n\n      - name: Prepare for diffing\n        shell: bash\n        run: |\n          find . -type f -path \"./fixtures/*/.webstudio/data.json\" -exec sed -i 's|\"origin\": \".*\"|\"origin\": \"https://main.development.webstudio.is\"|g' {} +\n\n      - name: Test git diff\n        # This command will fail if there are uncommitted changes, i.e something has broken\n        run: git diff --name-only HEAD --exit-code\n\n      - name: Show changed files and diff\n        if: ${{ failure() }}\n        run: |\n          echo \"Changed files are:\"\n          git diff --name-only HEAD\n          git diff HEAD | head -n 1000\n"
  },
  {
    "path": ".github/workflows/lint-pull-request.yaml",
    "content": "name: \"Lint PR\"\n\non:\n  pull_request:\n    types:\n      - opened\n      - edited\n      - synchronize\n\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n\npermissions:\n  pull-requests: write\n\njobs:\n  main:\n    name: Validate PR title\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v5\n        id: lint_pr_title\n        with:\n          # Configure which types are allowed (newline-delimited).\n          # Default: https://github.com/commitizen/conventional-commit-types\n          types: |\n            feat\n            fix\n            docs\n            style\n            refactor\n            perf\n            test\n            build\n            ci\n            chore\n            revert\n            experimental\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - if: always() && (steps.lint_pr_title.outputs.error_message != null)\n        uses: marocchino/sticky-pull-request-comment@v2\n        # When the previous steps fails, the workflow would stop. By adding this\n        # condition you can continue the execution with the populated error message.\n\n        with:\n          header: pr-title-lint-error\n          message: |\n            Hey there and thank you for opening this pull request! 👋🏼\n\n            We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.\n\n            Details:\n\n            ```\n            ${{ steps.lint_pr_title.outputs.error_message }}\n            ```\n            <details>\n              <summary>Release types</summary>\n\n            - **feat** - A new feature\n            - **fix** - A bug fix\n            - **docs** - Documentation only changes\n            - **style** - Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)\n            - **refactor** - A code change that neither fixes a bug nor adds a feature\n            - **perf** - A code change that improves performance\n            - **test** - Adding missing tests or correcting existing tests\n            - **build** - Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)\n            - **ci** - Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)\n            - **chore** - Other changes that don't modify src or test files\n            - **revert** - Reverts a previous commit\n            - **experimental** - Flagged feature\n\n            </details>\n\n        # Delete a previous comment when the issue has been resolved\n      - if: ${{ steps.lint_pr_title.outputs.error_message == null }}\n        uses: marocchino/sticky-pull-request-comment@v2\n        with:\n          header: pr-title-lint-error\n          delete: true\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Main workflow\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  pull_request_target:\n    types: [labeled]\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  checks:\n    # Run on push, regular PR (for repo branches), or labeled PR with safe-to-deploy (for forks)\n    if: |\n      github.event_name == 'push' ||\n      github.event_name == 'pull_request' ||\n      (github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-deploy')\n    \n    timeout-minutes: 20\n\n    strategy:\n      matrix:\n        environment:\n          - empty\n          - development\n\n    environment:\n      name: ${{ matrix.environment }}\n\n    env:\n      DATABASE_URL: postgres://\n      AUTH_SECRET: test\n\n    runs-on: ubuntu-24.04-arm\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      # Will not checkout submodules on empty environment, and will on development\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: pnpm/action-setup@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: pnpm\n\n      - name: Pnpm install\n        run: |\n          pnpm install\n\n      - uses: actions/cache@v4\n        with:\n          path: |\n            ./node_modules/.cache/prettier/.prettier-cache\n          key: checks-${{ github.sha }}\n          restore-keys: checks-\n\n      - run: echo ===SHA USED=== ${{ github.event.pull_request.head.sha || github.sha }} # todo: remove after check whats happening on main\n\n      - run: |\n          pnpm prettier --cache --check \"**/*.{js,md,ts,tsx}\"\n\n      - name: Lint\n        run: |\n          pnpm lint\n\n      - name: Cache Playwright browsers\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/ms-playwright\n          key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            playwright-${{ runner.os }}-\n\n      - name: Playwright init\n        run: |\n          pnpm playwright install\n        working-directory: packages/sdk-components-animation\n\n      - name: Test\n        run: |\n          pnpm -r test\n\n      - name: Typecheck\n        run: |\n          pnpm -r typecheck\n\n  check-size:\n    runs-on: ubuntu-24.04-arm\n\n    environment:\n      name: development\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }}\n\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: ./.github/actions/ci-setup\n\n      - run: pnpm --filter \"{./fixtures/*}...\" build\n\n      - uses: actions/github-script@v7\n        with:\n          script: |\n            const assertSize = async (directory, maxSize) => {\n              let result = ''\n              await exec.exec('du', ['-sk', directory], {\n                silent: true,\n                listeners: {\n                  stdout: (data) => {\n                    result += data.toString()\n                  }\n                }\n              })\n              const size = Number.parseInt(result, 10)\n              return {\n                passed: size <= maxSize,\n                size,\n                diff: size - maxSize,\n                directory,\n              }\n            }\n            const results = [\n              await assertSize('./fixtures/ssg/dist/client', 356),\n              await assertSize('./fixtures/react-router-netlify/build/client', 376),\n              await assertSize('./fixtures/webstudio-features/build/client', 3312),\n            ]\n            for (const result of results) {\n              if (result.passed) {\n                console.info(`${result.directory}: ${result.size}kB (${result.diff}kB)`)\n              } else {\n                console.info('')\n                console.error(`${result.directory}: ${result.size}kB (+${result.diff}kB)`)\n              }\n            }\n            if (results.some(result => result.passed === false)) {\n              console.error('Some fixtures exceeded limits')\n              process.exit(1)\n            }\n"
  },
  {
    "path": ".github/workflows/migrate.yaml",
    "content": "name: Migrate\n\non:\n  push:\n    branches:\n      - \"migrate\"\n      - \"main\"\n      - \"*.staging\"\n      - \"*.migrate\"\n\n# Pending if other migration from the same branch is running\nconcurrency: migrate-${{ github.ref_name }}\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n  statuses: write # This is required for the GitHub Script createCommitStatus to work\n\njobs:\n  migrate:\n    # This workflow is triggered only on pushes to the `main`, `*.staging` or 'migrate' branches.\n    # For `*.staging` and `migrate` it specifically checks if the commit message starts with `::migrate::`,\n    # indicating a migration-related change.\n    #\n    # Example usage:\n    #   Execute a commit with a migration flag using:\n    #   git commit --allow-empty -m \"::migrate::test description\"\n    # Note:\n    #   This setup is a temporary measure. The intention is to transition to a fully automated publish and release process via GitHub Actions in the future.\n    if: (github.ref_name == 'main') || ((github.ref_name == 'migrate' || endsWith(github.ref_name, '.migrate') || endsWith(github.ref_name, '.staging')) && startsWith(github.event.head_commit.message, '::migrate::'))\n\n    runs-on: ubuntu-latest\n\n    environment:\n      name: ${{ (startsWith(github.ref_name, 'release') && endsWith(github.ref_name, '.staging')) && 'postgres_production' || 'postgres_development' }}\n\n    timeout-minutes: 20\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.sha }} # HEAD commit instead of merge commit\n\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: pnpm instal\n        run: pnpm install --ignore-scripts\n\n      - name: generate prisma\n        run: pnpm --filter=@webstudio-is/prisma-client generate\n\n      - name: execute migration\n        run: pnpm --filter '@webstudio-is/prisma-client' run migrations migrate\n        env:\n          DIRECT_URL: ${{ secrets.DIRECT_URL }}\n\n  # Execute db tests (runs after migrations, even if they fail)\n  db-tests:\n    # Always run tests to see failures on CI, but only after migrate job completes\n    if: always()\n\n    needs: [migrate]\n\n    runs-on: ubuntu-latest\n\n    environment:\n      name: ${{ (startsWith(github.ref_name, 'release') && endsWith(github.ref_name, '.staging')) && 'postgres_production' || 'postgres_development' }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.sha }} # HEAD commit instead of merge commit\n\n      - uses: pnpm/action-setup@v4\n\n      - name: Run database tests\n        run: pnpm -r db-test\n        env:\n          DIRECT_URL: ${{ secrets.DIRECT_URL }}\n\n  # Prints pending migrations\n  pending:\n    if: always()\n\n    needs: [migrate]\n\n    runs-on: ubuntu-latest\n\n    environment:\n      name: ${{ (startsWith(github.ref_name, 'release') && endsWith(github.ref_name, '.staging')) && 'postgres_production' || 'postgres_development' }}\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.sha }} # HEAD commit instead of merge commit\n\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: pnpm instal\n        run: pnpm install --ignore-scripts\n\n      - name: generate prisma\n        run: pnpm --filter=@webstudio-is/prisma-client generate\n\n      - name: get pending\n        id: pending\n        run: |\n          echo \"value=$(pnpm --filter '@webstudio-is/prisma-client' run migrations pending-count | grep ::pending-count::)\" >> $GITHUB_OUTPUT\n        env:\n          DIRECT_URL: ${{ secrets.DIRECT_URL }}\n\n      - uses: ./.github/actions/add-status\n        with:\n          title: \"⭕ Pending Migrations\"\n          description: ${{ steps.pending.outputs.value }}\n          url: \"https://webstudio.is\"\n"
  },
  {
    "path": ".github/workflows/publish-beta.yml",
    "content": "name: Publish beta packages on NPM 📦\n\non:\n  pull_request:\n    types:\n      - labeled\n\njobs:\n  publish:\n    # prevents this action from running on forks\n    if: |\n      github.repository_owner == 'webstudio-is' &&\n      startsWith(github.event.label.name, 'publish:')\n\n    timeout-minutes: 20\n\n    runs-on: ubuntu-latest\n\n    env:\n      DATABASE_URL: postgres://\n      AUTH_SECRET: test\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }} # HEAD commit instead of merge commit\n\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: pnpm\n\n      - name: Creating .npmrc\n        run: |\n          cat << EOF > \"$HOME/.npmrc\"\n            //registry.npmjs.org/:_authToken=$NPM_TOKEN\n          EOF\n        env:\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n\n      # compute short sha\n      - id: short_sha\n        run: echo \"value=$(echo ${{ github.event.pull_request.head.sha || github.sha }} | cut -c1-7)\" >> $GITHUB_OUTPUT\n\n      - id: tag\n        run: echo \"value=$(echo ${{ github.event.label.name }} | cut -d ':' -f2)\" >> $GITHUB_OUTPUT\n\n      - name: bump version to 0.0.0-${{ steps.short_sha.outputs.value }}\n        run: |\n          pnpx replace-in-files-cli \\\n            --string=\"0.0.0-webstudio-version\" \\\n            --replacement=\"0.0.0-${{ steps.short_sha.outputs.value }}\" \\\n            \"**/package.json\"\n\n      - run: pnpm install --ignore-scripts\n      - run: pnpm --filter=\"webstudio...\" build\n      - run: pnpm --filter=\"webstudio...\" dts\n\n      - name: Publishing ${{ steps.tag.outputs.value }} tag with sha ${{ steps.short_sha.outputs.value }}\n        run: pnpm -r publish --tag \"${{ steps.tag.outputs.value }}\" --no-git-checks --access public\n"
  },
  {
    "path": ".github/workflows/re-create-figma-tokens-branch.yml",
    "content": "name: Re-create branch for Figma tokens\n\non: delete\n\npermissions:\n  contents: write\n\njobs:\n  main:\n    runs-on: ubuntu-latest\n\n    # run if figma-tokens was deleted\n    if: ${{ github.event.ref == 'figma-tokens'}}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Re-create branch\n        run: |\n          git checkout main\n          git checkout -b figma-tokens\n          git push --set-upstream origin figma-tokens\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - '[0-9]+.[0-9]+.[0-9]+'\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n    - uses: actions/github-script@v7\n      with:\n        script: |\n          const latestRelease = await github.rest.repos.getLatestRelease({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n          })\n          const commits = await github.rest.repos.compareCommitsWithBasehead({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            basehead: `refs/tags/${latestRelease.data.tag_name}...${context.ref}`,\n          })\n\n          const groups = {\n            feat: [`## Features\\n`],\n            fix: [`## Fixes\\n`],\n            docs: [`## Documentation\\n`],\n            experimental: [`## Experimental\\n`],\n            other: [`## Other changes\\n`],\n          }\n          for (const commit of commits.data.commits) {\n            const match = commit.commit.message.match(/^(?<type>\\w+)\\s*:\\s*(?<message>.+)\\n*/)\n            const type = match?.groups?.type\n            const message = match?.groups?.message\n            if (type && message) {\n              const availableType = type in groups ? type : 'other'\n              const capitalized = message[0].toLocaleUpperCase() + message.slice(1)\n              groups[availableType].push(`- ${capitalized} by @${commit.author.login}`)\n            }\n          }\n\n          const tag_name = context.ref.slice('refs/tags/'.length)\n          const fullChangelog = `**Full Changelog**: https://github.com/${context.repo.owner}/${context.repo.repo}/compare/${latestRelease.data.tag_name}...${tag_name}`\n          const changelog = Object.values(groups)\n            .filter(lines => lines.length > 1)\n            .map(lines => lines.join('\\n'))\n            .concat(fullChangelog)\n            .join('\\n\\n')\n          console.info(changelog)\n\n          await github.rest.repos.createRelease({\n            owner: context.repo.owner,\n            repo: context.repo.repo,\n            tag_name,\n            name: tag_name,\n            body: changelog,\n          })\n\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    environment:\n      name: development\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.ref }} # tag name\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: pnpm/action-setup@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: pnpm\n\n      - id: version\n        run: echo \"value=$(echo ${{ github.ref }} | sed 's/refs\\/tags\\///')\" >> $GITHUB_OUTPUT\n      - name: bump version to ${{ steps.version.outputs.value }}\n        run: |\n          pnpx replace-in-files-cli \\\n            --string=\"0.0.0-webstudio-version\" \\\n            --replacement=\"${{ steps.version.outputs.value }}\" \\\n            \"**/package.json\"\n\n      - name: pnpm instal\n        run: pnpm install --ignore-scripts\n      - run: pnpm --filter=\"webstudio...\" build\n      - run: pnpm --filter=\"webstudio...\" dts\n\n      - name: Creating .npmrc\n        run: |\n          cat << EOF > \"$HOME/.npmrc\"\n            //registry.npmjs.org/:_authToken=$NPM_TOKEN\n          EOF\n        env:\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n      - run: pnpm -r publish --access public --no-git-checks\n"
  },
  {
    "path": ".github/workflows/vercel-deploy-staging.yml",
    "content": "name: Vercel Deploy Staging\n\non:\n  push:\n  pull_request_target:\n    types: [labeled, synchronize]\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: vercel-deploy-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n  statuses: write # This is required for the GitHub Script createCommitStatus to work\n  deployments: write\n  pull-requests: write # needed to remove labels and comment\n\njobs:\n  # Remove the safe-to-deploy label when new commits are pushed to a PR (requires re-review)\n  remove-label-on-update:\n    if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize'\n    runs-on: ubuntu-latest\n    steps:\n      - name: Remove safe-to-deploy label\n        uses: actions/github-script@v7\n        with:\n          script: |\n            await github.rest.issues.removeLabel({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n              name: 'safe-to-deploy'\n            }).catch(() => {});\n            console.log('Removed safe-to-deploy label due to new commits. Re-review required.');\n\n  deployment:\n    # Run on push (for repo branches) OR on labeled event with safe-to-deploy label (for fork PRs)\n    if: |\n      github.event_name == 'push' ||\n      (github.event_name == 'pull_request_target' && github.event.action == 'labeled' && github.event.label.name == 'safe-to-deploy')\n    \n    # Execute development and staging on staging branches\n    # Execute only development on all other branches\n    strategy:\n      matrix:\n        environment:\n          - staging\n          - development\n        is-staging:\n          - ${{ github.event_name == 'push' && endsWith(github.ref_name, '.staging') }}\n        exclude:\n          - environment: staging\n            is-staging: false\n\n    environment:\n      name: ${{ matrix.environment }}\n\n    timeout-minutes: 20\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}\n\n      - uses: ./.github/actions/submodules-checkout\n        with:\n          submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n      - uses: pnpm/action-setup@v4\n\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 22\n          cache: pnpm\n\n      - uses: ./.github/actions/vercel\n        id: vercel\n        name: Deploy to Vercel\n        with:\n          vercel-token: ${{ secrets.VERCEL_TOKEN }}\n          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}\n          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}\n          ref-name: ${{ github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}\n          sha: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}\n          environment: ${{ matrix.environment }}\n\n      - name: Debug Vercel Outputs\n        run: |\n          echo \"domain=${{ steps.vercel.outputs.domain }}\"\n          echo \"inspect-url=${{ steps.vercel.outputs.inspect-url }}\"\n          echo \"alias=${{ steps.vercel.outputs.alias }}\"\n\n      - uses: ./.github/actions/add-status\n        with:\n          title: \"⏰ [${{ matrix.environment }}] Vercel Inspection\"\n          description: \"[${{ matrix.environment }}] Vercel logs\"\n          url: \"${{ steps.vercel.outputs.inspect-url }}\"\n\n      - uses: ./.github/actions/add-status\n        with:\n          title: \"⭐ [${{ matrix.environment }}] Apps Webstudio URL\"\n          description: \"[${{ matrix.environment }}] Site url\"\n          url: \"https://${{ steps.vercel.outputs.alias }}.${{ matrix.environment }}.webstudio.is\"\n\n      - name: Comment on PR with deployment URL\n        if: github.event_name == 'pull_request_target' && matrix.environment == 'development'\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const deployUrl = 'https://${{ steps.vercel.outputs.alias }}.${{ matrix.environment }}.webstudio.is';\n            await github.rest.issues.createComment({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number,\n              body: `🚀 **Deployed!**\\n\\n📍 Preview: ${deployUrl}\\n\\n_Note: Adding new commits will remove the \\`safe-to-deploy\\` label and require re-approval._`\n            });\n\n    outputs:\n      builder-url: \"https://${{ steps.vercel.outputs.alias }}.${{ matrix.environment }}.webstudio.is\"\n      builder-host: \"${{ steps.vercel.outputs.alias }}.${{ matrix.environment }}.webstudio.is\"\n\n  fixtures-test:\n    needs: deployment\n    uses: ./.github/workflows/fixtures-test.yml\n    with:\n      builder-url: ${{ needs.deployment.outputs.builder-url }}\n      builder-host: ${{ needs.deployment.outputs.builder-host }}\n      environment: development\n    secrets:\n      # We are not passing the secret here (as it does not exist in the current environment).\n      # Instead, this serves as a signal to the calling workflow that it has permission to extract it from the environment.\n      PRIVATE_GITHUB_DEPLOY_TOKEN: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }}\n\n  delete-github-deployments:\n    needs: fixtures-test\n    uses: ./.github/workflows/delete-github-deployments.yml\n    with:\n      ref: ${{ github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}\n"
  },
  {
    "path": ".github/workflows/vis-reg-tests.yml",
    "content": "name: Visual Regression Tests\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  pull_request_target:\n    types: [labeled]\n\n# cancel in-progress runs on new commits to same PR (gitub.event.number)\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.event.number || github.sha }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # to fetch code (actions/checkout)\n\njobs:\n  lost-pixel:\n    # Run on push, regular PR (for repo branches), or labeled PR with safe-to-deploy (for forks)\n    if: |\n      github.event_name == 'push' ||\n      github.event_name == 'pull_request' ||\n      (github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-deploy')\n    \n    timeout-minutes: 20\n    runs-on: ubuntu-latest\n    env:\n      DATABASE_URL: postgres://\n      AUTH_SECRET: test\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          ref: ${{ github.event.pull_request.head.sha || github.sha }} # HEAD commit instead of merge commit\n\n      - uses: ./.github/actions/ci-setup\n\n      - run: VISUAL_TESTING=true pnpm storybook:build\n\n      - name: Lost Pixel\n        uses: lost-pixel/lost-pixel@v3.16.0\n        env:\n          LOST_PIXEL_API_KEY: 8b76db6c-b9f0-46d1-982f-70900a02690a\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n.pnp\n.pnp.js\nyarn.lock\n\n# testing\ncoverage\n\n# remix\nbuild\n_build\n.cache\n.vercel\n.output\n.netlify\n\n# misc\n.DS_Store\n*.pem\n!https/*.pem\n/.idea\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n# .env\n.env.development\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# data\n/data\n*.db\n*.db-journal\n\n# migrations\n**/prisma/migrations/lockfile\n**/prisma/migrations/*/client\n\n# builds\npackages/**/lib\ngenerated\nstorybook-static\ntsconfig.tsbuildinfo\n\n# to save thunder https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client files\n.thunder\n.env*.local\n\n# wrangler builds\ndist\n\n# should be here otherwise if placed inside prisma-client pnpm deploy doesn't copy it\npackages/prisma-client/src/__generated__\n\n.temp\n*.timestamp-*.mjs\n\n# copilot instructions\n.github/instructions\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"packages/sdk-components-animation/private-src\"]\n\tpath = packages/sdk-components-animation/private-src\n\turl = git@github.com:webstudio-is/sdk-components-animation.git\n\tbranch = main\n"
  },
  {
    "path": ".nvmrc",
    "content": "22\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json\",\n  \"plugins\": [\"react\", \"unicorn\", \"typescript\"],\n  \"categories\": {\n    \"correctness\": \"off\"\n  },\n  \"rules\": {\n    \"no-console\": [\n      \"error\",\n      { \"allow\": [\"info\", \"warn\", \"error\", \"time\", \"timeEnd\"] }\n    ],\n    \"func-style\": [\"error\", \"expression\", { \"allowArrowFunctions\": true }],\n    \"curly\": \"error\",\n    \"eqeqeq\": [\"error\", \"always\", { \"null\": \"ignore\" }],\n    \"radix\": \"error\",\n    \"react/rules-of-hooks\": \"error\",\n    \"react/exhaustive-deps\": \"warn\",\n    \"typescript/no-explicit-any\": \"error\",\n    \"unicorn/filename-case\": [\"error\", { \"case\": \"kebabCase\" }],\n    \"unicorn/prefer-node-protocol\": \"error\"\n  },\n  \"ignorePatterns\": [\n    \"**/*.js\",\n    \"**/*.d.ts\",\n    \"**/__generated__/**\",\n    \"codemod/**\",\n    \"packages/*/lib/**\",\n    \"packages/prisma-client/prisma/migrations/**\",\n    \"packages/cli/templates/**\",\n    \"fixtures/**\",\n    \"packages/sdk-components-animation/private-src/polyfill/**\",\n    \"packages/sdk-components-animation/private-src/perf/**\"\n  ]\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "pnpm-lock.yaml\npackages/prisma-client/prisma/migrations/*/client\npackages/prisma-client/**/*.d.ts"
  },
  {
    "path": ".storybook/main.ts",
    "content": "import * as path from \"node:path\";\nimport { existsSync, readdirSync } from \"node:fs\";\nimport { defaultClientConditions } from \"vite\";\nimport type { StorybookConfig } from \"@storybook/react-vite\";\n\nconst isFolderEmpty = (folderPath: string) => {\n  if (!existsSync(folderPath)) {\n    return true; // Folder does not exist\n  }\n  const contents = readdirSync(folderPath);\n\n  return contents.length === 0;\n};\n\nconst hasPrivateFolders = !isFolderEmpty(\n  path.join(__dirname, \"../../packages/sdk-components-animation/private-src\")\n);\n\nconst visualTestingStories: StorybookConfig[\"stories\"] = [\n  {\n    directory: \"../apps/builder\",\n    titlePrefix: \"Builder\",\n    files: \"**/*.stories.tsx\",\n  },\n  {\n    directory: \"../packages/design-system/src/components\",\n    titlePrefix: \"Design system\",\n    files: \"**/*.stories.tsx\",\n  },\n];\n\nexport default {\n  stories: process.env.VISUAL_TESTING\n    ? visualTestingStories\n    : [\n        ...visualTestingStories,\n        {\n          directory: \"../packages/css-engine/src\",\n          titlePrefix: \"CSS engine\",\n          files: \"**/*.stories.tsx\",\n        },\n        {\n          directory: \"../packages/image/src\",\n          titlePrefix: \"Image\",\n          files: \"**/*.stories.tsx\",\n        },\n        {\n          directory: \"../packages/icons\",\n          titlePrefix: \"Icons\",\n          files: \"**/*.stories.tsx\",\n        },\n        {\n          directory: \"../packages/sdk-components-react\",\n          titlePrefix: \"SDK components React\",\n          files: \"**/*.stories.tsx\",\n        },\n        {\n          directory: \"../packages/sdk-components-react-radix\",\n          titlePrefix: \"SDK components React Radix\",\n          files: \"**/*.stories.tsx\",\n        },\n        {\n          directory: \"../packages/sdk-components-animation\",\n          titlePrefix: \"SDK components animation\",\n          files: \"**/*.stories.tsx\",\n        },\n      ],\n  framework: {\n    name: \"@storybook/react-vite\",\n    options: {},\n  },\n  addons: [\n    \"@storybook/addon-controls\",\n    \"@storybook/addon-actions\",\n    \"@storybook/addon-backgrounds\",\n  ],\n  async viteFinal(config) {\n    return {\n      ...config,\n      optimizeDeps: {\n        exclude: [\"scroll-timeline-polyfill\"],\n      },\n\n      define: {\n        ...config.define,\n        // storybook use \"util\" package internally which is bundled with stories\n        // and gives an error that process is undefined\n        \"process.env.NODE_DEBUG\": \"undefined\",\n        \"process.env.IS_STROYBOOK\": \"true\",\n      },\n      resolve: {\n        ...config.resolve,\n        conditions: hasPrivateFolders\n          ? [\"webstudio-private\", \"webstudio\", ...defaultClientConditions]\n          : [\"webstudio\", ...defaultClientConditions],\n\n        alias: [\n          {\n            find: \"~\",\n            replacement: path.resolve(\"./apps/builder/app\"),\n          },\n        ],\n      },\n    };\n  },\n} satisfies StorybookConfig;\n"
  },
  {
    "path": ".storybook/preview-body.html",
    "content": "<script>\n  // for styling radix components\n  document.body.setAttribute(\"data-ws-component\", \"Body\");\n</script>\n"
  },
  {
    "path": ".storybook/preview.tsx",
    "content": "import type { Preview } from \"@storybook/react\";\nimport * as React from \"react\";\nimport { useEffect } from \"react\";\nimport { TooltipProvider } from \"@radix-ui/react-tooltip\";\nimport { setEnv } from \"../packages/feature-flags/src/index\";\nimport { theme, globalCss } from \"../packages/design-system/src/index\";\nimport { color } from \"../packages/design-system/src/__generated__/figma-design-tokens\";\n\n// this adds <style> tags to the <head> of the document\nimport \"@fontsource-variable/inter\";\nimport \"@fontsource-variable/manrope\";\nimport \"@fontsource/roboto-mono\";\n\nconst WaitForFonts = ({ children }) => {\n  const [isFontsLoaded, setIsFontsLoaded] = React.useState(false);\n\n  useEffect(() => {\n    let isUnsubscribed = false;\n    document.fonts.ready.then(() => {\n      if (isUnsubscribed === false) {\n        setIsFontsLoaded(true);\n      }\n    });\n    return () => {\n      isUnsubscribed = true;\n    };\n  }, []);\n\n  return isFontsLoaded ? (\n    children\n  ) : (\n    <div>\n      Waiting for fonts to load ...\n      {/* not rendering children initially breaks backgrounds addon,\n       * so we always render it */}\n      <div style={{ display: \"none\" }}>{children}</div>\n    </div>\n  );\n};\n\nconst globalStyles = globalCss({\n  body: {\n    color: theme.colors.foregroundMain,\n    fontFamily: theme.fonts.sans,\n  },\n});\n\nexport const decorators: Preview[\"decorators\"] = [\n  (Story) => {\n    globalStyles();\n    setEnv(\"*\");\n    return (\n      // waiting for fonts makes screenshot tests more stable\n      <WaitForFonts>\n        <TooltipProvider>\n          <Story />\n        </TooltipProvider>\n      </WaitForFonts>\n    );\n  },\n];\n\nconst parameters: Preview[\"parameters\"] = {\n  actions: { argTypesRegex: \"^on[A-Z].*\" },\n  controls: {\n    matchers: {\n      color: /(background|color)$/i,\n      date: /Date$/,\n    },\n  },\n  backgrounds: {\n    default: \"White\",\n    values: [\n      { name: \"White\", value: \"#ffffff\" },\n      { name: \"Black\", value: \"#000000\" },\n      { name: \"Panel\", value: color.backgroundPanel },\n      { name: \"Maintenance Dark\", value: color.maintenanceDark },\n      { name: \"Maintenance Medium\", value: color.maintenanceMedium },\n      { name: \"Maintenance Light\", value: color.maintenanceLight },\n    ],\n  },\n};\n\nexport default {\n  decorators,\n  parameters,\n} satisfies Preview;\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"ms-vscode-remote.remote-containers\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"typescript.experimental.useTsgo\": true,\n  // Disable formatting for SQL files\n  \"[sql]\": {\n    \"editor.formatOnSave\": false,\n    \"editor.formatOnPaste\": false,\n    \"editor.formatOnType\": false\n  },\n  // Disable Prettier for SQL files specifically\n  \"prettier.enable\": true,\n  \"prettier.ignorePath\": \".prettierignore\",\n  // Exclude SQL files from auto-formatting\n  \"files.associations\": {\n    \"*.sql\": \"sql\"\n  }\n}\n"
  },
  {
    "path": "@types/canvas-iframe.d.ts",
    "content": "declare namespace React {\n  interface IframeHTMLAttributes {\n    credentialless?: \"true\";\n  }\n}\n"
  },
  {
    "path": "@types/content.d.ts",
    "content": "// CSS Containment\n// Specification: https://drafts.csswg.org/css-contain-2/\n// Repository: https://github.com/w3c/csswg-drafts/tree/main/css-contain-2\n\ndeclare var oncontentvisibilityautostatechange: ContentVisibilityAutoStateChangeEvent | null;\n\ninterface GlobalEventHandlersEventMap {\n  contentvisibilityautostatechange: ContentVisibilityAutoStateChangeEvent;\n}\n"
  },
  {
    "path": "@types/css-tree.d.ts",
    "content": "// Minimal-yet-safe TypeScript typings for css-tree v3.x\n// Covers: AST node types, parse/generate, walk/find, List, lexer API,\n// value definition syntax helpers, and common utils used in this project.\n\ndeclare module \"css-tree\" {\n  // -------------------------------------------------\n  // Common\n  // -------------------------------------------------\n  export interface SourceLocation {\n    source: string;\n    start: { offset: number; line: number; column: number };\n    end: { offset: number; line: number; column: number };\n  }\n\n  export interface BaseNode {\n    type: string;\n    loc: SourceLocation | null;\n  }\n\n  // Doubly-linked list used by csstree\n  export interface ListItem<T> {\n    prev: ListItem<T> | null;\n    next: ListItem<T> | null;\n    data: T;\n  }\n\n  export class List<T> implements Iterable<T> {\n    static createItem<T>(data: T): ListItem<T>;\n\n    constructor();\n    // iteration\n    [Symbol.iterator](): IterableIterator<T>;\n    // props\n    readonly size: number;\n    readonly isEmpty: boolean;\n    readonly first: T | null;\n    readonly last: T | null;\n    // traversal helpers\n    forEach(\n      fn: (data: T, item: ListItem<T>, list: List<T>) => void,\n      thisArg?: any\n    ): void;\n    forEachRight(\n      fn: (data: T, item: ListItem<T>, list: List<T>) => void,\n      thisArg?: any\n    ): void;\n    // conversion\n    fromArray(items: T[]): this;\n    toArray(): T[];\n    toJSON(): T[];\n    // mutation (minimal surface used by walkers)\n    clear(): void;\n    remove(item: ListItem<T>): ListItem<T>;\n    insert(item: ListItem<T>, before?: ListItem<T> | null): void;\n    insertData(data: T, before?: ListItem<T> | null): void;\n    append(item: ListItem<T>): void;\n    appendData(data: T): void;\n    prepend(item: ListItem<T>): void;\n    prependData(data: T): void;\n    replace(oldItem: ListItem<T>, newItemOrList: ListItem<T> | List<T>): void;\n  }\n\n  // -------------------------------------------------\n  // AST Nodes (as per docs/ast.md)\n  // -------------------------------------------------\n  export interface AnPlusB extends BaseNode {\n    type: \"AnPlusB\";\n    a: string | null;\n    b: string | null;\n  }\n  export interface Atrule extends BaseNode {\n    type: \"Atrule\";\n    name: string;\n    prelude: AtrulePrelude | Raw | null;\n    block: Block | null;\n  }\n  export interface AtrulePrelude extends BaseNode {\n    type: \"AtrulePrelude\";\n    children: List<CssNode>;\n  }\n  export interface AttributeSelector extends BaseNode {\n    type: \"AttributeSelector\";\n    name: Identifier;\n    matcher: string | null;\n    value: StringNode | Identifier | null;\n    flags: string | null;\n  }\n  export interface Block extends BaseNode {\n    type: \"Block\";\n    children: List<Atrule | Rule | Declaration>;\n  }\n  export interface Brackets extends BaseNode {\n    type: \"Brackets\";\n    children: List<CssNode>;\n  }\n  export interface CDC extends BaseNode {\n    type: \"CDC\";\n  }\n  export interface CDO extends BaseNode {\n    type: \"CDO\";\n  }\n  export interface ClassSelector extends BaseNode {\n    type: \"ClassSelector\";\n    name: string;\n  }\n  export interface Combinator extends BaseNode {\n    type: \"Combinator\";\n    name: string;\n  }\n  export interface Comment extends BaseNode {\n    type: \"Comment\";\n    value: string;\n  }\n  export interface Condition extends BaseNode {\n    type: \"Condition\";\n    kind: string;\n    children: List<\n      | Identifier\n      | Feature\n      | FeatureFunction\n      | FeatureRange\n      | SupportsDeclaration\n    >;\n  }\n  export interface Declaration extends BaseNode {\n    type: \"Declaration\";\n    important: boolean | string;\n    property: string;\n    value: Value | Raw;\n  }\n  export interface DeclarationList extends BaseNode {\n    type: \"DeclarationList\";\n    children: List<Declaration | Atrule | Rule>;\n  }\n  export interface Dimension extends BaseNode {\n    type: \"Dimension\";\n    value: string;\n    unit: string;\n  }\n  export interface Feature extends BaseNode {\n    type: \"Feature\";\n    kind: string;\n    name: string;\n    value: Identifier | NumberNode | Dimension | Ratio | FunctionNode | null;\n  }\n  export interface FeatureFunction extends BaseNode {\n    type: \"FeatureFunction\";\n    kind: string;\n    feature: string;\n    value: Declaration | Selector;\n  }\n  export interface FeatureRange extends BaseNode {\n    type: \"FeatureRange\";\n    kind: string;\n    left: Identifier | NumberNode | Dimension | Ratio | FunctionNode;\n    leftComparison: string;\n    middle: Identifier | NumberNode | Dimension | Ratio | FunctionNode;\n    rightComparison: string | null;\n    right: Identifier | NumberNode | Dimension | Ratio | FunctionNode | null;\n  }\n  export interface FunctionNode extends BaseNode {\n    type: \"Function\";\n    name: string;\n    children: List<CssNode>;\n  }\n  export interface GeneralEnclosed extends BaseNode {\n    type: \"GeneralEnclosed\";\n    kind: string;\n    function: string | null;\n    children: List<CssNode>;\n  }\n  export interface Hash extends BaseNode {\n    type: \"Hash\";\n    value: string;\n  }\n  export interface IdSelector extends BaseNode {\n    type: \"IdSelector\";\n    name: string;\n  }\n  export interface Identifier extends BaseNode {\n    type: \"Identifier\";\n    name: string;\n  }\n  export interface Layer extends BaseNode {\n    type: \"Layer\";\n    name: string;\n  }\n  export interface LayerList extends BaseNode {\n    type: \"LayerList\";\n    children: List<Layer>;\n  }\n  export interface MediaQuery extends BaseNode {\n    type: \"MediaQuery\";\n    modifier: string | null;\n    mediaType: string | null;\n    condition: Condition | null;\n  }\n  export interface MediaQueryList extends BaseNode {\n    type: \"MediaQueryList\";\n    children: List<MediaQuery>;\n  }\n  export interface NestingSelector extends BaseNode {\n    type: \"NestingSelector\";\n  }\n  export interface Nth extends BaseNode {\n    type: \"Nth\";\n    nth: AnPlusB | Identifier;\n    selector: SelectorList | null;\n  }\n  export interface NumberNode extends BaseNode {\n    type: \"Number\";\n    value: string;\n  }\n  export interface Operator extends BaseNode {\n    type: \"Operator\";\n    value: string;\n  }\n  export interface Parentheses extends BaseNode {\n    type: \"Parentheses\";\n    children: List<CssNode>;\n  }\n  export interface Percentage extends BaseNode {\n    type: \"Percentage\";\n    value: string;\n  }\n  export interface PseudoClassSelector extends BaseNode {\n    type: \"PseudoClassSelector\";\n    name: string;\n    children: List<Raw> | null;\n  }\n  export interface PseudoElementSelector extends BaseNode {\n    type: \"PseudoElementSelector\";\n    name: string;\n    children: List<Raw> | null;\n  }\n  export interface Ratio extends BaseNode {\n    type: \"Ratio\";\n    left: NumberNode | FunctionNode;\n    right: NumberNode | FunctionNode | null;\n  }\n  export interface Raw extends BaseNode {\n    type: \"Raw\";\n    value: string;\n  }\n  export interface Rule extends BaseNode {\n    type: \"Rule\";\n    prelude: SelectorList | Raw;\n    block: Block;\n  }\n  export interface Scope extends BaseNode {\n    type: \"Scope\";\n    root: SelectorList | Raw | null;\n    limit: SelectorList | Raw | null;\n  }\n  export interface Selector extends BaseNode {\n    type: \"Selector\";\n    children: List<\n      | TypeSelector\n      | IdSelector\n      | ClassSelector\n      | AttributeSelector\n      | PseudoClassSelector\n      | PseudoElementSelector\n      | Combinator\n      | NestingSelector\n    >;\n  }\n  export interface SelectorList extends BaseNode {\n    type: \"SelectorList\";\n    children: List<Selector | Raw>;\n  }\n  export interface StringNode extends BaseNode {\n    type: \"String\";\n    value: string;\n  }\n  export interface StyleSheet extends BaseNode {\n    type: \"StyleSheet\";\n    children: List<Comment | CDO | CDC | Atrule | Rule | Raw>;\n  }\n  export interface SupportsDeclaration extends BaseNode {\n    type: \"SupportsDeclaration\";\n    declaration: Declaration;\n  }\n  export interface TypeSelector extends BaseNode {\n    type: \"TypeSelector\";\n    name: string;\n  }\n  export interface UnicodeRange extends BaseNode {\n    type: \"UnicodeRange\";\n    value: string;\n  }\n  export interface Url extends BaseNode {\n    type: \"Url\";\n    value: string;\n  }\n  export interface Value extends BaseNode {\n    type: \"Value\";\n    children: List<CssNode>;\n  }\n  export interface WhiteSpace extends BaseNode {\n    type: \"WhiteSpace\";\n    value: string;\n  }\n\n  export type CssNode =\n    | AnPlusB\n    | Atrule\n    | AtrulePrelude\n    | AttributeSelector\n    | Block\n    | Brackets\n    | CDC\n    | CDO\n    | ClassSelector\n    | Combinator\n    | Comment\n    | Condition\n    | Declaration\n    | DeclarationList\n    | Dimension\n    | Feature\n    | FeatureFunction\n    | FeatureRange\n    | FunctionNode\n    | GeneralEnclosed\n    | Hash\n    | IdSelector\n    | Identifier\n    | Layer\n    | LayerList\n    | MediaQuery\n    | MediaQueryList\n    | NestingSelector\n    | Nth\n    | NumberNode\n    | Operator\n    | Parentheses\n    | Percentage\n    | PseudoClassSelector\n    | PseudoElementSelector\n    | Ratio\n    | Raw\n    | Rule\n    | Scope\n    | Selector\n    | SelectorList\n    | StringNode\n    | StyleSheet\n    | SupportsDeclaration\n    | TypeSelector\n    | UnicodeRange\n    | Url\n    | Value\n    | WhiteSpace;\n\n  // A plain-object form where children are arrays (for JSON, etc.)\n  export type CssNodePlain = Omit<CssNode, \"children\" | \"loc\"> & {\n    children?: CssNodePlain[] | null;\n    loc?: SourceLocation | null;\n  };\n\n  // -------------------------------------------------\n  // Parsing / Generation\n  // -------------------------------------------------\n  export type ParseContext =\n    | \"stylesheet\"\n    | \"atrule\"\n    | \"atrulePrelude\"\n    | \"mediaQueryList\"\n    | \"mediaQuery\"\n    | \"rule\"\n    | \"selectorList\"\n    | \"selector\"\n    | \"block\"\n    | \"declarationList\"\n    | \"declaration\"\n    | \"value\";\n\n  export interface ParseOptions {\n    context?: ParseContext;\n    atrule?: string | null;\n    positions?: boolean;\n    onParseError?: (error: any, fallbackNode?: Raw) => void;\n    onComment?: (value: string, loc: SourceLocation | null) => void;\n    onToken?:\n      | ((type: number, start: number, end: number, index: number) => void)\n      | Array<{ type: number; start: number; end: number }>\n      | null;\n    filename?: string;\n    offset?: number;\n    line?: number;\n    column?: number;\n    parseAtrulePrelude?: boolean;\n    parseRulePrelude?: boolean;\n    parseValue?: boolean;\n    parseCustomProperty?: boolean;\n  }\n\n  // Strict overloads by context\n  export function parse(source: string): StyleSheet;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context?: \"stylesheet\" }\n  ): StyleSheet;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"atrule\" }\n  ): Atrule;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"atrulePrelude\" }\n  ): AtrulePrelude;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"mediaQueryList\" }\n  ): MediaQueryList;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"mediaQuery\" }\n  ): MediaQuery;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"rule\" }\n  ): Rule;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"selectorList\" }\n  ): SelectorList;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"selector\" }\n  ): Selector;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"block\" }\n  ): Block;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"declarationList\" }\n  ): DeclarationList;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"declaration\" }\n  ): Declaration;\n  export function parse(\n    source: string,\n    options: ParseOptions & { context: \"value\" }\n  ): Value;\n  // Fallback\n  export function parse(source: string, options: ParseOptions): CssNode;\n\n  export function generate(\n    ast: CssNode,\n    options: {\n      sourceMap: true;\n      decorator?: (node: CssNode) => any;\n      mode?: \"spec\" | \"safe\";\n    }\n  ): { css: string; map: any };\n  export function generate(\n    ast: CssNode,\n    options?: {\n      sourceMap?: false;\n      decorator?: (node: CssNode) => any;\n      mode?: \"spec\" | \"safe\";\n    }\n  ): string;\n\n  // -------------------------------------------------\n  // Traversal helpers\n  // -------------------------------------------------\n  export type WalkHandler = (\n    node: CssNode,\n    item?: ListItem<CssNode>,\n    list?: List<CssNode>\n  ) => any;\n  export interface WalkOptions {\n    enter?: WalkHandler;\n    leave?: WalkHandler;\n    visit?: CssNode[\"type\"] | null;\n    reverse?: boolean;\n  }\n  export function walk(ast: CssNode, options: WalkOptions | WalkHandler): void;\n  export function find(ast: CssNode, fn: WalkHandler): CssNode | null;\n  export function findLast(ast: CssNode, fn: WalkHandler): CssNode | null;\n  export function findAll(ast: CssNode, fn: WalkHandler): CssNode[];\n\n  // -------------------------------------------------\n  // Utils\n  // -------------------------------------------------\n  export function clone<T extends CssNode>(ast: T): T;\n  export function fromPlainObject<T extends { children?: any }>(obj: T): T;\n  export function toPlainObject<T extends { children?: any }>(ast: T): T;\n\n  export interface PropertyInfo {\n    basename: string;\n    name: string;\n    hack: string;\n    vendor: string;\n    prefix: string;\n    custom: boolean;\n  }\n  export function property(name: string): PropertyInfo;\n\n  export interface KeywordInfo {\n    basename: string;\n    name: string;\n    vendor: string;\n    prefix: string;\n    custom: boolean;\n  }\n  export function keyword(name: string): KeywordInfo;\n\n  export namespace ident {\n    function decode(value: string): string;\n    function encode(value: string): string;\n  }\n  export namespace string {\n    function decode(value: string): string;\n    function encode(value: string, preferSingleQuotes?: boolean): string;\n  }\n  export namespace url {\n    function decode(value: string): string;\n    function encode(value: string): string;\n  }\n\n  // -------------------------------------------------\n  // Definition syntax (values) sub-API\n  // -------------------------------------------------\n  export namespace definitionSyntax {\n    // AST nodes for definition syntax are intentionally minimal here\n    interface Base {\n      type: string;\n    }\n    interface Group extends Base {\n      type: \"Group\";\n      terms: Base[];\n      combinator: \" \" | \"|\" | \"||\" | \"&&\";\n      disallowEmpty: boolean;\n      explicit: boolean;\n    }\n    interface Keyword extends Base {\n      type: \"Keyword\";\n      name: string;\n    }\n    interface Function extends Base {\n      type: \"Function\";\n      name: string;\n    }\n    interface String extends Base {\n      type: \"String\";\n      value: string;\n    }\n    interface Property extends Base {\n      type: \"Property\";\n      name: string;\n    }\n    interface Type extends Base {\n      type: \"Type\";\n      name: string;\n      opts: Range | null;\n    }\n    interface Range extends Base {\n      type: \"Range\";\n      min: number | null;\n      max: number | null;\n    }\n    interface Multiplier extends Base {\n      type: \"Multiplier\";\n      comma: boolean;\n      min: number;\n      max: number;\n      term: Base;\n    }\n\n    type Node =\n      | Group\n      | Keyword\n      | Function\n      | String\n      | Property\n      | Type\n      | Range\n      | Multiplier\n      | Base;\n\n    function parse(source: string): Node;\n    function walk(\n      node: Node,\n      options:\n        | { enter?: (n: Node) => void; leave?: (n: Node) => void }\n        | ((n: Node) => void),\n      context?: any\n    ): void;\n    function generate(\n      node: Node,\n      options?: {\n        forceBraces?: boolean;\n        compact?: boolean;\n        decorate?: (content: string, node: Node) => string;\n      }\n    ): string;\n  }\n\n  // -------------------------------------------------\n  // Lexer\n  // -------------------------------------------------\n  export interface AtruleSyntaxConfig {\n    prelude?: string | definitionSyntax.Node | ((ref?: any) => any) | null;\n    descriptors?: Record<\n      string,\n      string | definitionSyntax.Node | ((ref?: any) => any)\n    > | null;\n  }\n\n  export interface LexerConfig {\n    generic?: boolean;\n    cssWideKeywords?: string[];\n    units?: Record<string, string[]>;\n    types?: Record<\n      string,\n      string | definitionSyntax.Node | ((ref?: any) => any)\n    >;\n    atrules?: Record<string, AtruleSyntaxConfig>;\n    properties?: Record<\n      string,\n      string | definitionSyntax.Node | ((ref?: any) => any)\n    >;\n  }\n\n  export interface MatchResult {\n    matched: any;\n    error: Error | null;\n    iterations: number;\n    isType(node: CssNode | null | undefined, name: string): boolean;\n    getTrace(\n      node: CssNode | null | undefined\n    ): Array<{ type: string; name: string }>;\n  }\n\n  export interface FragmentResult {\n    parent: List<CssNode>;\n    nodes: List<CssNode>;\n  }\n\n  export class Lexer {\n    constructor(config?: LexerConfig, syntax?: any, structure?: any);\n\n    cssWideKeywords: string[];\n    units: Record<string, string[]>;\n\n    checkStructure(\n      ast: CssNode\n    ): false | Array<{ node: CssNode; message: string }>;\n\n    checkAtruleName(atruleName: string): Error | void;\n    checkAtrulePrelude(\n      atruleName: string,\n      prelude?: string | CssNode | null\n    ): Error | void;\n    checkAtruleDescriptorName(\n      atruleName: string,\n      descriptorName: string\n    ): Error | void;\n    checkPropertyName(propertyName: string): Error | void;\n\n    matchAtrulePrelude(\n      atruleName: string,\n      prelude?: string | CssNode | null\n    ): MatchResult;\n    matchAtruleDescriptor(\n      atruleName: string,\n      descriptorName: string,\n      value: string | CssNode\n    ): MatchResult;\n    matchDeclaration(node: CssNode): MatchResult;\n    matchProperty(propertyName: string, value: string | CssNode): MatchResult;\n    matchType(typeName: string, value: string | CssNode): MatchResult;\n    match(\n      syntax: string | definitionSyntax.Node,\n      value: string | CssNode\n    ): MatchResult;\n\n    findValueFragments(\n      propertyName: string,\n      value: Value,\n      type: string,\n      name: string\n    ): FragmentResult[];\n    findDeclarationValueFragments(\n      declaration: Declaration,\n      type: string,\n      name: string\n    ): FragmentResult[];\n    findAllFragments(\n      ast: CssNode,\n      type: string,\n      name: string\n    ): FragmentResult[];\n\n    // descriptor getters\n    getAtrule(atruleName: string, fallbackBasename?: boolean): any | null;\n    getAtrulePrelude(\n      atruleName: string,\n      fallbackBasename?: boolean\n    ): any | null;\n    getAtruleDescriptor(atruleName: string, name: string): any | null;\n    getProperty(propertyName: string, fallbackBasename?: boolean): any | null;\n    getType(name: string): any | null;\n\n    validate(): null | {\n      errors: string[];\n      types: string[];\n      properties: string[];\n    };\n    dump(syntaxAsAst?: boolean, pretty?: boolean): any;\n    toString(): string;\n  }\n\n  export const lexer: Lexer;\n  export function createLexer(config?: LexerConfig): Lexer;\n\n  // -------------------------------------------------\n  // Tokenizer / misc exports (lightly typed)\n  // -------------------------------------------------\n  export const tokenTypes: Record<string, number>;\n  export const tokenNames: string[];\n  export function tokenize(input: string): IterableIterator<any>;\n\n  // Syntax factory and forking\n  export function createSyntax(config: any): any;\n  export function fork(ext?: any): any;\n}\n\n// Optional subpath module shims to enable tree-shake-style imports\ndeclare module \"css-tree/lexer\" {\n  export * from \"css-tree\";\n}\ndeclare module \"css-tree/parser\" {\n  export type { ParseOptions, CssNode } from \"css-tree\";\n  export { parse as default } from \"css-tree\";\n}\ndeclare module \"css-tree/generator\" {\n  export { generate as default } from \"css-tree\";\n}\ndeclare module \"css-tree/walker\" {\n  export {\n    walk,\n    find,\n    findAll,\n    findLast,\n    WalkOptions,\n    WalkHandler,\n  } from \"css-tree\";\n}\ndeclare module \"css-tree/definition-syntax\" {\n  export { definitionSyntax } from \"css-tree\";\n}\ndeclare module \"css-tree/utils\" {\n  export {\n    List,\n    ListItem,\n    clone,\n    fromPlainObject,\n    toPlainObject,\n    property,\n    keyword,\n    ident,\n    string,\n    url,\n  } from \"css-tree\";\n}\n"
  },
  {
    "path": "@types/navigator.d.ts",
    "content": "interface Navigator {\n  userAgentData?: {\n    brands: Array<{ brand: string; version: string }>;\n    getHighEntropyValues(hints: string[]): Promise<{\n      fullVersionList?: Array<{ brand: string; version: string }>;\n    }>;\n  };\n}\n"
  },
  {
    "path": "@types/scroll-timeline.d.ts",
    "content": "type ScrollAxis = \"block\" | \"inline\" | \"x\" | \"y\";\n\ninterface ScrollTimelineOptions {\n  source?: Element | Document | null;\n  axis?: ScrollAxis;\n}\n\ndeclare class ScrollTimeline extends AnimationTimeline {\n  constructor(options?: ScrollTimelineOptions);\n}\n\ninterface ViewTimelineOptions {\n  subject?: Element | Document | null;\n  axis?: ScrollAxis;\n  inset?: string;\n}\n\ndeclare class ViewTimeline extends ScrollTimeline {\n  constructor(options?: ViewTimelineOptions);\n}\n"
  },
  {
    "path": "@types/warn-once.d.ts",
    "content": "declare module \"warn-once\" {\n  export default function warnOnce(condition: boolean, message: string): void;\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\noleg@webstudio.is.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "README.md",
    "content": "<img width=\"1512\" alt=\"builder-screenshot\" src=\"https://github.com/webstudio-is/.github/blob/main/assets/builder-screenshot.png?raw=true\">\n<br /><br />\n\n<section align=\"center\">\n  Webstudio is an Open Source Visual Development Platform for developers, designers, and cross-functional teams. You own the data, components, and infrastructure. You can use the hosted version or roll out your own.\n</section>\n<br /><br />\n\n## Learning Resources\n\n- [Blog](https://webstudio.is/blog)\n- [Documentation](https://docs.webstudio.is/)\n- [Brand and Product Design](https://docs.webstudio.is/contributing/contributing-for-designers)\n- [Contributing Guide for Devs](https://docs.webstudio.is/contributing/contributing-for-developers)\n- [Github Discussions](https://github.com/webstudio-is/webstudio-community/discussions)\n- [Wishlist](https://github.com/webstudio-is/webstudio-community/discussions/categories/wishlist)\n- [Builder Issues Tracker](https://github.com/webstudio-is/webstudio/issues)\n- [Roadmap](https://github.com/orgs/webstudio-is/projects/11)\n\n## Social Media\n\n- [Twitter](https://twitter.com/getwebstudio)\n- [Youtube](https://www.youtube.com/@getwebstudio)\n- [Discord](https://wstd.us/community)\n\n## Thanks\n\n<a href=\"https://www.lost-pixel.com/\"><img src=\"https://user-images.githubusercontent.com/29632358/168112844-77e76a0d-b96f-4bc8-b753-cd39f4afd428.png\" width=\"50\" height=\"50\" alt=\"Lost Pixel\" /></a>\n\nThanks to [Lost Pixel](https://www.lost-pixel.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.\n\n## License\n\n- **Webstudio core** (all functionality in this repository) is free/open-source under AGPL-3.0-or-later.\n- **sdk-components-animation** package (optional) is proprietary. You must accept the Webstudio, Inc. EULA located in [sdk-components-animation/LICENSE](./packages/sdk-components-animation/LICENSE) before using it.\n"
  },
  {
    "path": "apps/builder/.gitignore",
    "content": ".cache\n.vercel\n.output\n\n/build/\n/public/build\n/api/index.js\n/api/index.js.map\n/api/_assets\n/public/s/uploads\n/public/cgi/asset\n\n"
  },
  {
    "path": "apps/builder/app/auth/index.client.ts",
    "content": "export * from \"./login\";\n"
  },
  {
    "path": "apps/builder/app/auth/login.stories.tsx",
    "content": "import type { JSX } from \"react\";\nimport type { StoryFn } from \"@storybook/react\";\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { Login as LoginComponent } from \"./login\";\n\nexport default {\n  title: \"Auth\",\n  component: LoginComponent,\n};\n\nconst createRouter = (element: JSX.Element) =>\n  createBrowserRouter([\n    {\n      path: \"*\",\n      element,\n      loader: () => null,\n    },\n  ]);\n\nexport const Auth: StoryFn<typeof LoginComponent> = () => {\n  const router = createRouter(\n    <LoginComponent isGoogleEnabled={false} isSecretLoginEnabled />\n  );\n  return (\n    <StorySection title=\"Auth\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/auth/login.tsx",
    "content": "import { TooltipProvider } from \"@radix-ui/react-tooltip\";\nimport {\n  Button,\n  Flex,\n  globalCss,\n  rawTheme,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { GithubIcon, GoogleIcon, WebstudioIcon } from \"@webstudio-is/icons\";\nimport { Form } from \"@remix-run/react\";\nimport { authPath } from \"~/shared/router-utils\";\nimport { SecretLogin } from \"./secret-login\";\n\nconst globalStyles = globalCss({\n  body: {\n    margin: 0,\n    overflow: \"hidden\",\n  },\n});\n\nexport type LoginProps = {\n  errorMessage?: string;\n  isGithubEnabled?: boolean;\n  isGoogleEnabled?: boolean;\n  isSecretLoginEnabled?: boolean;\n};\n\nexport const Login = ({\n  errorMessage,\n  isGithubEnabled,\n  isGoogleEnabled,\n  isSecretLoginEnabled,\n}: LoginProps) => {\n  globalStyles();\n  return (\n    <Flex\n      align=\"center\"\n      justify=\"center\"\n      css={{\n        height: \"100vh\",\n        background: theme.colors.brandBackgroundDashboard,\n      }}\n    >\n      <Flex\n        direction=\"column\"\n        align=\"center\"\n        gap=\"6\"\n        css={{\n          width: theme.spacing[35],\n          minWidth: theme.spacing[20],\n          padding: theme.spacing[17],\n          borderRadius: theme.spacing[5],\n          [`@media (min-width: ${rawTheme.spacing[35]})`]: {\n            backgroundColor: `rgba(255, 255, 255, 0.5)`,\n          },\n        }}\n      >\n        <WebstudioIcon size={48} />\n        <Text variant=\"brandSectionTitle\" as=\"h1\" align=\"center\">\n          Welcome to Webstudio\n        </Text>\n\n        <TooltipProvider>\n          <Flex direction=\"column\" gap=\"3\" css={{ width: \"100%\" }}>\n            <Form method=\"post\" style={{ display: \"contents\" }}>\n              <Button\n                disabled={isGoogleEnabled === false}\n                prefix={<GoogleIcon size={22} />}\n                color=\"primary\"\n                css={{ height: theme.spacing[15] }}\n                formAction={authPath({ provider: \"google\" })}\n              >\n                Sign in with Google\n              </Button>\n              <Button\n                disabled={isGithubEnabled === false}\n                prefix={<GithubIcon size={22} fill=\"currentColor\" />}\n                color=\"ghost\"\n                css={{\n                  border: `1px solid ${theme.colors.borderDark}`,\n                  height: theme.spacing[15],\n                }}\n                formAction={authPath({ provider: \"github\" })}\n              >\n                Sign in with GitHub\n              </Button>\n            </Form>\n            {isSecretLoginEnabled && <SecretLogin />}\n          </Flex>\n        </TooltipProvider>\n        {errorMessage ? (\n          <Text align=\"center\" color=\"destructive\">\n            {errorMessage}\n          </Text>\n        ) : null}\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/auth/secret-login.tsx",
    "content": "import { Button, Flex, InputField, theme } from \"@webstudio-is/design-system\";\nimport { useState } from \"react\";\nimport { authPath } from \"~/shared/router-utils\";\n\nexport const SecretLogin = () => {\n  const [show, setShow] = useState(false);\n  if (show) {\n    return (\n      <form\n        method=\"post\"\n        action={authPath({ provider: \"dev\" })}\n        style={{ display: \"contents\" }}\n      >\n        <Flex gap=\"2\">\n          <InputField\n            name=\"secret\"\n            type=\"text\"\n            minLength={2}\n            required\n            autoFocus\n            placeholder=\"Auth secret\"\n            css={{ flexGrow: 1 }}\n          />\n          <Button type=\"submit\">Login</Button>\n        </Flex>\n      </form>\n    );\n  }\n\n  return (\n    <Button\n      onClick={() => setShow(true)}\n      color=\"neutral\"\n      css={{ height: theme.spacing[15] }}\n    >\n      Login with Secret\n    </Button>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/builder.css",
    "content": "html {\n  overflow: hidden;\n}\n\nbody {\n  overflow: hidden;\n  overscroll-behavior: contain;\n  -webkit-font-smoothing: antialiased;\n  /* \n    This is the top bar and loading screen color (--colors-backgroundTopbar).\n    We are setting it to avoid a white screen flash when opening project from the dashboard.\n   */\n  background-color: #2d2d2d;\n}\n\n[data-radix-scroll-area-viewport] {\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n  -webkit-overflow-scrolling: touch;\n}\n\n[data-radix-scroll-area-viewport]::-webkit-scrollbar {\n  display: none;\n}\n\n* {\n  scrollbar-width: thin;\n  scrollbar-color: var(--colors-foregroundScrollBar) transparent;\n}\n"
  },
  {
    "path": "apps/builder/app/builder/builder.tsx",
    "content": "import { useEffect, useMemo, useState, type JSX, type ReactNode } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { TooltipProvider } from \"@radix-ui/react-tooltip\";\nimport {\n  theme,\n  Box,\n  Toaster,\n  type CSS,\n  Flex,\n  Grid,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport type { AuthPermit } from \"@webstudio-is/trpc-interface/index.server\";\nimport { initializeClientSync, getSyncClient } from \"~/shared/sync/sync-client\";\nimport { usePreventUnload } from \"~/shared/sync/project-queue\";\nimport { usePublish, $publisher } from \"~/shared/pubsub\";\nimport { Inspector } from \"./inspector\";\nimport { Topbar } from \"./shared/topbar\";\nimport { Footer } from \"./features/footer\";\nimport {\n  CanvasIframe,\n  CanvasToolsContainer,\n  Workspace,\n} from \"./features/workspace\";\nimport {\n  $authPermit,\n  $authToken,\n  $isPreviewMode,\n  $pages,\n  $project,\n  subscribeResources,\n  $authTokenPermissions,\n  $isDesignMode,\n  $isContentMode,\n  $userPlanFeatures,\n  subscribeModifierKeys,\n  $stagingUsername,\n  $stagingPassword,\n} from \"~/shared/nano-states\";\nimport { $settings, type Settings } from \"./shared/client-settings\";\nimport { builderUrl, getCanvasUrl } from \"~/shared/router-utils\";\nimport { BlockingAlerts } from \"./features/blocking-alerts\";\nimport { useSyncPageUrl } from \"~/shared/pages\";\nimport { useMount, useUnmount } from \"~/shared/hook-utils/use-mount\";\nimport { subscribeCommands } from \"~/builder/shared/commands\";\nimport { ProjectSettings } from \"~/shared/project-settings\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\nimport {\n  $activeSidebarPanel,\n  $dataLoadingState,\n  $isCloneDialogOpen,\n  $loadingState,\n} from \"./shared/nano-states\";\nimport { CloneProjectDialog } from \"~/shared/clone-project\";\nimport type { TokenPermissions } from \"@webstudio-is/authorization-token\";\nimport { useToastErrors } from \"~/shared/error/toast-error\";\nimport { initBuilderApi } from \"~/shared/builder-api\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { migrateWebstudioDataMutable } from \"~/shared/webstudio-data-migrator\";\nimport { Loading, LoadingBackground } from \"./shared/loading\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { CommandPanel } from \"./features/command-panel\";\nimport { DeleteUnusedTokensDialog } from \"~/builder/shared/style-source-actions\";\nimport { DeleteUnusedDataVariablesDialog } from \"~/builder/shared/data-variable-utils\";\nimport { DeleteUnusedCssVariablesDialog } from \"~/builder/shared/css-variable-utils\";\nimport { DeleteUnusedAssetsDialog } from \"~/builder/shared/asset-manager/delete-unused-assets\";\nimport { KeyboardShortcutsDialog } from \"./features/keyboard-shortcuts-dialog\";\nimport { TokenConflictDialog } from \"~/shared/token-conflict-dialog\";\n\nimport {\n  initCopyPaste,\n  initCopyPasteForContentEditMode,\n} from \"~/shared/copy-paste/init-copy-paste\";\nimport { useInertHandlers } from \"./shared/inert-handlers\";\nimport { TextToolbar } from \"./features/workspace/canvas-tools/text-toolbar\";\nimport { RemoteDialog } from \"./features/help/remote-dialog\";\nimport type { SidebarPanelName } from \"./sidebar-left/types\";\nimport { SidebarLeft } from \"./sidebar-left/sidebar-left\";\nimport { useDisableContextMenu } from \"./shared/use-disable-context-menu\";\n\nconst useSetWindowTitle = () => {\n  const project = useStore($project);\n  useEffect(() => {\n    document.title = `${project?.title} | Webstudio`;\n  }, [project?.title]);\n};\n\ntype SidePanelProps = {\n  children: JSX.Element | Array<JSX.Element>;\n  isPreviewMode?: boolean;\n  css?: CSS;\n  gridArea: \"inspector\" | \"sidebar\" | \"navigator\";\n};\n\nconst SidePanel = ({\n  children,\n  isPreviewMode = false,\n  gridArea,\n  css,\n}: SidePanelProps) => {\n  return (\n    <Box\n      as=\"aside\"\n      css={{\n        position: \"relative\",\n        isolation: \"isolate\",\n        gridArea,\n        display: isPreviewMode ? \"none\" : \"flex\",\n        flexDirection: \"column\",\n        px: 0,\n        fg: 0,\n        // Left sidebar tabs won't be able to pop out to the right if we set overflowX to auto.\n        //overflowY: \"auto\",\n        backgroundColor: theme.colors.backgroundPanel,\n        height: \"100%\",\n        ...css,\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nconst Main = ({ children, css }: { children: ReactNode; css?: CSS }) => (\n  <Flex\n    as=\"main\"\n    direction=\"column\"\n    css={{\n      gridArea: \"main\",\n      position: \"relative\",\n      isolation: \"isolate\",\n      ...css,\n    }}\n  >\n    {children}\n  </Flex>\n);\n\ntype ChromeWrapperProps = {\n  children: Array<JSX.Element | null | false>;\n  isPreviewMode: boolean;\n  navigatorLayout: Settings[\"navigatorLayout\"];\n};\n\nconst getChromeLayout = ({\n  isPreviewMode,\n  navigatorLayout,\n  activeSidebarPanel,\n  leftSidebarWidth,\n}: {\n  isPreviewMode: boolean;\n  navigatorLayout: Settings[\"navigatorLayout\"];\n  activeSidebarPanel?: SidebarPanelName;\n  leftSidebarWidth: number;\n}) => {\n  if (isPreviewMode) {\n    return {\n      gridTemplateColumns: \"auto 1fr\",\n      gridTemplateAreas: `\n            \"header header\"\n            \"sidebar main\"\n            \"footer footer\"\n          `,\n    };\n  }\n\n  if (navigatorLayout === \"undocked\" && activeSidebarPanel !== \"none\") {\n    return {\n      gridTemplateColumns: `auto ${leftSidebarWidth}px 1fr ${theme.sizes.sidebarWidth}`,\n      gridTemplateAreas: `\n            \"header header header header\"\n            \"sidebar navigator main inspector\"\n            \"footer footer footer footer\"\n          `,\n    };\n  }\n\n  return {\n    gridTemplateColumns: `auto 1fr ${theme.sizes.sidebarWidth}`,\n    gridTemplateAreas: `\n          \"header header header\"\n          \"sidebar main inspector\"\n          \"footer footer footer\"\n        `,\n  };\n};\n\nconst defaultSidebarWidth = Number.parseFloat(rawTheme.spacing[30]);\n\nconst ChromeWrapper = ({\n  children,\n  isPreviewMode,\n  navigatorLayout,\n}: ChromeWrapperProps) => {\n  const activeSidebarPanel = useStore($activeSidebarPanel);\n  const settings = useStore($settings);\n  const leftSidebarWidth =\n    activeSidebarPanel === \"none\"\n      ? defaultSidebarWidth\n      : (settings.sidebarPanelWidths[activeSidebarPanel] ??\n        defaultSidebarWidth);\n\n  const gridLayout = getChromeLayout({\n    isPreviewMode,\n    navigatorLayout,\n    activeSidebarPanel,\n    leftSidebarWidth,\n  });\n\n  return (\n    <Grid\n      css={{\n        height: \"100vh\",\n        overflow: \"hidden\",\n        display: \"grid\",\n        gridTemplateRows: \"auto 1fr auto\",\n        ...gridLayout,\n      }}\n    >\n      {children}\n    </Grid>\n  );\n};\n\nexport type BuilderProps = {\n  projectId: string;\n  authToken?: string;\n  authPermit: AuthPermit;\n  authTokenPermissions: TokenPermissions;\n  userPlanFeatures: UserPlanFeatures;\n  stagingUsername: string;\n  stagingPassword: string;\n};\n\nexport const Builder = ({\n  projectId,\n  authToken,\n  authPermit,\n  userPlanFeatures,\n  authTokenPermissions,\n  stagingUsername,\n  stagingPassword,\n}: BuilderProps) => {\n  useMount(initBuilderApi);\n\n  useMount(() => {\n    // additional data stores\n    $authPermit.set(authPermit);\n    $authToken.set(authToken);\n    $userPlanFeatures.set(userPlanFeatures);\n    $authTokenPermissions.set(authTokenPermissions);\n    $stagingUsername.set(stagingUsername);\n    $stagingPassword.set(stagingPassword);\n\n    const controller = new AbortController();\n\n    $dataLoadingState.set(\"loading\");\n    initializeClientSync({\n      projectId,\n      authPermit,\n      authToken,\n      signal: controller.signal,\n      onReady() {\n        updateWebstudioData((data) => {\n          migrateWebstudioDataMutable(data);\n        });\n\n        // render canvas only after all data is loaded\n        // so builder is started listening for connect event\n        // when canvas is rendered\n        $dataLoadingState.set(\"loaded\");\n\n        // @todo make needs error handling and error state? e.g. a toast\n      },\n    });\n\n    return () => {\n      $dataLoadingState.set(\"idle\");\n      controller.abort(\"unmount\");\n    };\n  });\n\n  useToastErrors();\n  useEffect(subscribeCommands, []);\n  useEffect(subscribeResources, []);\n  useDisableContextMenu();\n\n  useUnmount(() => {\n    $pages.set(undefined);\n  });\n\n  useSyncPageUrl();\n\n  const [publish, publishRef] = usePublish();\n  useEffect(() => {\n    $publisher.set({ publish });\n  }, [publish]);\n\n  const project = useStore($project);\n\n  usePreventUnload();\n  const isCloneDialogOpen = useStore($isCloneDialogOpen);\n  const isPreviewMode = useStore($isPreviewMode);\n  const isDesignMode = useStore($isDesignMode);\n  const isContentMode = useStore($isContentMode);\n\n  useSetWindowTitle();\n\n  const iframeRefCallback = useMemo(\n    () =>\n      mergeRefs((element: HTMLIFrameElement | null) => {\n        if (element?.contentWindow) {\n          const client = getSyncClient();\n          if (client) {\n            // added to iframe window and stored in local variable right away to prevent\n            // overriding in emebedded scripts on canvas\n            element.contentWindow.__webstudioSharedSyncEmitter__ =\n              client.emitter;\n          }\n        }\n      }, publishRef),\n    [publishRef]\n  );\n\n  const { navigatorLayout } = useStore($settings);\n  const dataLoadingState = useStore($dataLoadingState);\n  const [loadingState, setLoadingState] = useState(() => $loadingState.get());\n\n  useEffect(() => {\n    const abortController = new AbortController();\n\n    if (isDesignMode) {\n      // We need to initialize this in both canvas and builder,\n      // because the events will fire in either one, depending on where the focus is\n      // @todo we need to forward the events from canvas to builder and avoid importing this\n      // in both places\n      initCopyPaste(abortController);\n      subscribeModifierKeys({ signal: abortController.signal });\n    }\n\n    if (isContentMode) {\n      initCopyPasteForContentEditMode(abortController);\n      subscribeModifierKeys({ signal: abortController.signal });\n    }\n\n    return () => {\n      abortController.abort();\n    };\n  }, [isContentMode, isDesignMode]);\n\n  useEffect(() => {\n    const unsubscribe = $loadingState.subscribe((loadingState) => {\n      setLoadingState(loadingState);\n      // We need to stop updating it once it's ready in case in the future it changes again.\n      if (loadingState.state === \"ready\") {\n        unsubscribe();\n      }\n    });\n    return unsubscribe;\n  }, []);\n\n  const canvasUrl = getCanvasUrl();\n\n  const inertHandlers = useInertHandlers();\n\n  // Show loading screen if project isn't ready yet\n  if (!project || dataLoadingState !== \"loaded\") {\n    return (\n      <TooltipProvider>\n        <Loading state={loadingState} />\n      </TooltipProvider>\n    );\n  }\n\n  return (\n    <TooltipProvider>\n      <div\n        style={{ display: \"contents\" }}\n        onPointerDown={inertHandlers.onPointerDown}\n        onInput={inertHandlers.onInput}\n        onKeyDown={inertHandlers.onKeyDown}\n      >\n        <ChromeWrapper\n          isPreviewMode={isPreviewMode}\n          navigatorLayout={navigatorLayout}\n        >\n          <Box\n            data-dialog-boundary\n            css={{\n              gridArea: \"sidebar / sidebar / main / inspector\",\n              pointerEvents: \"none\",\n            }}\n          />\n          <ProjectSettings />\n\n          {/* Main must be after left sidebar panels because in content mode the Plus button must be above the left sidebar, otherwise it won't be visible when content is full width */}\n          <Main>\n            <Workspace>\n              {dataLoadingState === \"loaded\" && project && (\n                <CanvasIframe\n                  ref={iframeRefCallback}\n                  src={canvasUrl}\n                  title={project.title}\n                />\n              )}\n            </Workspace>\n          </Main>\n          <Main css={{ pointerEvents: \"none\" }}>\n            <CanvasToolsContainer />\n          </Main>\n          <SidePanel\n            gridArea=\"sidebar\"\n            css={{\n              order: navigatorLayout === \"docked\" ? 1 : undefined,\n            }}\n          >\n            <SidebarLeft publish={publish} />\n          </SidePanel>\n\n          <SidePanel\n            gridArea=\"inspector\"\n            isPreviewMode={isPreviewMode}\n            css={{\n              overflow: \"hidden\",\n              // Drawing border this way to ensure content still has full width, avoid subpixels and give layout round numbers\n              \"&::after\": {\n                content: \"''\",\n                position: \"absolute\",\n                top: 0,\n                left: 0,\n                bottom: 0,\n                width: 1,\n                background: theme.colors.borderMain,\n              },\n            }}\n          >\n            <Inspector navigatorLayout={navigatorLayout} />\n          </SidePanel>\n          <Main css={{ pointerEvents: \"none\" }}>\n            <CanvasToolsContainer />\n          </Main>\n          {project ? (\n            <Topbar\n              project={project}\n              css={{ gridArea: \"header\" }}\n              loading={\n                <LoadingBackground\n                  // Looks nicer when topbar is already visible earlier, so user has more sense of progress.\n                  show={\n                    loadingState.readyStates.get(\"dataLoadingState\")\n                      ? false\n                      : true\n                  }\n                />\n              }\n            />\n          ) : null}\n          <Main css={{ pointerEvents: \"none\" }}>\n            <TextToolbar />\n          </Main>\n          {isPreviewMode === false && <Footer />}\n          {project ? (\n            <CloneProjectDialog\n              isOpen={isCloneDialogOpen}\n              onOpenChange={$isCloneDialogOpen.set}\n              project={project}\n              onCreate={(projectId) => {\n                window.location.href = builderUrl({\n                  origin: window.origin,\n                  projectId: projectId,\n                });\n              }}\n            />\n          ) : null}\n        </ChromeWrapper>\n        <Loading state={loadingState} />\n        <BlockingAlerts />\n        <CommandPanel />\n        <DeleteUnusedTokensDialog />\n        <DeleteUnusedDataVariablesDialog />\n        <DeleteUnusedCssVariablesDialog />\n        <DeleteUnusedAssetsDialog />\n        <KeyboardShortcutsDialog />\n        <TokenConflictDialog />\n        <RemoteDialog />\n        <Toaster />\n      </div>\n    </TooltipProvider>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/address-bar.stories.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport type { Meta, StoryFn } from \"@storybook/react\";\nimport { StorySection, Text } from \"@webstudio-is/design-system\";\nimport { ToolbarButton } from \"@webstudio-is/design-system\";\nimport { WebstudioIcon } from \"@webstudio-is/icons\";\nimport { TopbarLayout } from \"~/builder/shared/topbar-layout\";\nimport { AddressBarPopover } from \"./address-bar\";\nimport { $dataSources, $pages } from \"~/shared/sync/data-stores\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { $awareness, $selectedPage } from \"~/shared/awareness\";\nimport { $currentSystem } from \"~/shared/system\";\n\nregisterContainers();\n\n$dataSources.set(new Map());\n\n$pages.set({\n  folders: [\n    {\n      id: \"rootId\",\n      name: \"\",\n      slug: \"\",\n      children: [\"homeId\", \"dynamicId\"],\n    },\n  ],\n  homePage: {\n    id: \"homeId\",\n    path: \"\",\n    name: \"\",\n    title: \"\",\n    meta: {},\n    rootInstanceId: \"\",\n  },\n  pages: [\n    {\n      id: \"dynamicId\",\n      path: \"/blog/:date/post/:slug\",\n      name: \"\",\n      title: \"\",\n      meta: {},\n      rootInstanceId: \"rootInstanceId\",\n    },\n  ],\n});\n\nconst SystemInspect = () => {\n  const system = useStore($currentSystem);\n  return (\n    <Text variant=\"mono\" css={{ whiteSpace: \"pre\" }}>\n      {JSON.stringify(system, null, 2)}\n    </Text>\n  );\n};\n\nconst HistoryInspect = () => {\n  const page = useStore($selectedPage);\n  return (\n    <Text variant=\"mono\" css={{ whiteSpace: \"pre\" }}>\n      {JSON.stringify(page?.history, null, 2)}\n    </Text>\n  );\n};\n\nexport default {\n  title: \"Address Bar\",\n  component: AddressBarPopover,\n} satisfies Meta;\n\nexport const AddressBar: StoryFn = () => {\n  $awareness.set({ pageId: \"dynamicId\" });\n  return (\n    <StorySection title=\"Address Bar\">\n      <TopbarLayout\n        menu={\n          <ToolbarButton aria-label=\"Menu\">\n            <WebstudioIcon size={22} />\n          </ToolbarButton>\n        }\n        left={<AddressBarPopover />}\n      />\n      <SystemInspect />\n      <HistoryInspect />\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/address-bar.tsx",
    "content": "import { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport {\n  forwardRef,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type RefObject,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\nimport {\n  Flex,\n  InputField,\n  Tooltip,\n  theme,\n  textVariants,\n  InputErrorsTooltip,\n  ToolbarButton,\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  IconButton,\n  MenuItemButton,\n  MenuList,\n} from \"@webstudio-is/design-system\";\nimport { CheckMarkIcon, CopyIcon, DynamicPageIcon } from \"@webstudio-is/icons\";\nimport { $publishedOrigin } from \"~/shared/nano-states\";\nimport {\n  compilePathnamePattern,\n  isPathnamePattern,\n  matchPathnamePattern,\n  tokenizePathnamePattern,\n} from \"~/builder/shared/url-pattern\";\nimport { $selectedPage, $selectedPagePath } from \"~/shared/awareness\";\nimport { $currentSystem, updateCurrentSystem } from \"~/shared/system\";\n\nconst $selectedPageHistory = computed(\n  $selectedPage,\n  (page) => page?.history ?? []\n);\n\nconst useCopyUrl = (pageUrl: string) => {\n  const [copyState, setCopyState] = useState<\"copy\" | \"copied\">(\"copy\");\n  // reset copied state after 2 seconds\n  useEffect(() => {\n    if (copyState === \"copied\") {\n      const timeoutId = setTimeout(() => {\n        setCopyState(\"copy\");\n      }, 2000);\n      return () => {\n        clearTimeout(timeoutId);\n      };\n    }\n  }, [copyState]);\n  let copyIcon = <CopyIcon />;\n  if (copyState === \"copied\") {\n    copyIcon = <CheckMarkIcon />;\n  }\n  const onClick = () => {\n    navigator.clipboard.writeText(pageUrl);\n    setCopyState(\"copied\");\n  };\n  return {\n    tooltipProps: {\n      // keep tooltip open when user just copied\n      open: copyState === \"copied\" ? true : undefined,\n      content: copyState === \"copied\" ? \"Copied\" : `Copy ${pageUrl}`,\n    } satisfies Partial<ComponentProps<typeof Tooltip>>,\n    buttonProps: {\n      onClick,\n      children: copyIcon,\n    } satisfies Partial<ComponentProps<\"button\">>,\n  };\n};\n\nconst moveSelection = (menu: HTMLElement, diff: number) => {\n  const options = Array.from(menu.querySelectorAll(\"[role=option]\"));\n  const index = options.findIndex((element) => element.ariaSelected === \"true\");\n  const newIndex = Math.max(-1, Math.min(index + diff, options.length - 1));\n  if (index >= 0) {\n    options[index].ariaSelected = null;\n  }\n  if (newIndex >= 0) {\n    options[newIndex].ariaSelected = \"true\";\n  }\n};\n\n/**\n * Suggestions are opened whenever user\n * - types in input\n * - focuses input\n * - press arrow down or arrow up\n *\n * and closed when\n * - input is lost focus\n * - escape or enter are pressed\n *\n * option selection is managed by arrow up, arrow down and hover\n */\nconst Suggestions = ({\n  containerRef,\n  options,\n  onSelect,\n}: {\n  containerRef: RefObject<null | HTMLFormElement>;\n  options: string[];\n  onSelect: (option: string) => void;\n}) => {\n  const list = options;\n\n  const menuRef = useRef<HTMLDivElement>(null);\n  const [isListOpen, setIsListOpen] = useState(false);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (container === null) {\n      return;\n    }\n    const handleInput = () => {\n      setIsListOpen(true);\n    };\n    let frameId: undefined | number;\n    const handleFocusIn = () => {\n      if (frameId) {\n        cancelAnimationFrame(frameId);\n      }\n      setIsListOpen(true);\n    };\n    const handleFocusOut = () => {\n      frameId = requestAnimationFrame(() => {\n        setIsListOpen(false);\n      });\n    };\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"ArrowDown\") {\n        // avoid moving cursor to the end\n        event.preventDefault();\n        // trigger menu with up and down like in chrome\n        if (menuRef.current === null) {\n          setIsListOpen(true);\n          return;\n        }\n        moveSelection(menuRef.current, +1);\n      }\n      if (event.key === \"ArrowUp\") {\n        // avoid moving cursor to the start\n        event.preventDefault();\n        if (menuRef.current === null) {\n          setIsListOpen(true);\n          return;\n        }\n        moveSelection(menuRef.current, -1);\n      }\n      if (event.key === \"Escape\" && menuRef.current) {\n        // avoid closing popovers and dialogs when list is open\n        event.stopPropagation();\n        setIsListOpen(false);\n      }\n      if (event.key === \"Enter\" && menuRef.current) {\n        const selected = menuRef.current?.querySelector(\n          \"[role=option][aria-selected=true]\"\n        );\n        if (selected instanceof HTMLElement) {\n          // avoid submitting form when item is selected\n          event.preventDefault();\n          selected.click();\n        }\n      }\n    };\n    container.addEventListener(\"input\", handleInput);\n    container.addEventListener(\"focusin\", handleFocusIn);\n    container.addEventListener(\"focusout\", handleFocusOut);\n    container.addEventListener(\"keydown\", handleKeyDown);\n    return () => {\n      container.removeEventListener(\"input\", handleInput);\n      container.removeEventListener(\"focusin\", handleFocusIn);\n      container.removeEventListener(\"focusout\", handleFocusOut);\n      container.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [containerRef]);\n\n  if (isListOpen === false || list.length === 0) {\n    return;\n  }\n  return (\n    <MenuList\n      ref={menuRef}\n      role=\"listbox\"\n      css={{\n        position: \"absolute\",\n        left: 0,\n        top: \"calc(100% + 4px)\",\n        minWidth: \"100%\",\n      }}\n      // close after selecting option\n      onClick={() => setIsListOpen(false)}\n    >\n      {list.map((option) => (\n        <MenuItemButton\n          key={option}\n          type=\"button\"\n          role=\"option\"\n          tabIndex={-1}\n          css={{ textTransform: \"none\", whiteSpace: \"nowrap\" }}\n          onMouseEnter={(event) => {\n            // select option on hover\n            const options =\n              menuRef.current?.querySelectorAll(\"[role=option]\") ?? [];\n            for (const element of options) {\n              if (element.ariaSelected === \"true\") {\n                element.ariaSelected = null;\n              }\n              if (element === event.currentTarget) {\n                element.ariaSelected = \"true\";\n              }\n            }\n          }}\n          onClick={() => onSelect(option)}\n        >\n          {option}\n        </MenuItemButton>\n      ))}\n    </MenuList>\n  );\n};\n\nconst AddressBar = forwardRef<\n  HTMLFormElement,\n  {\n    onSubmit: () => void;\n  }\n>(({ onSubmit }, ref) => {\n  const publishedOrigin = useStore($publishedOrigin);\n  const path = useStore($selectedPagePath);\n  let history = useStore($selectedPageHistory);\n  history = useMemo(() => {\n    return history.filter((item) => matchPathnamePattern(path, item));\n  }, [history, path]);\n  const [pathParams, setPathParams] = useState(\n    () => $currentSystem.get().params\n  );\n  const tokens = tokenizePathnamePattern(path);\n  const compiledPath = compilePathnamePattern(tokens, pathParams);\n  const { tooltipProps, buttonProps } = useCopyUrl(\n    `${publishedOrigin}${compiledPath}`\n  );\n\n  const errors = new Map<string, string>();\n  for (const token of tokens) {\n    if (token.type === \"param\") {\n      const value = (pathParams[token.name] ?? \"\").trim();\n      if (value === \"\" && token.optional === false) {\n        errors.set(token.name, `\"${token.name}\" is required`);\n      }\n      if (value.includes(\"/\") && token.splat === false) {\n        errors.set(\n          token.name,\n          `\"${token.name}\" should be splat parameter to contain slashes`\n        );\n      }\n    }\n  }\n\n  const containerRef = useRef<HTMLFormElement>(null);\n\n  return (\n    <form\n      ref={mergeRefs(ref, containerRef)}\n      onSubmit={(event) => {\n        event.preventDefault();\n        const formData = new FormData(event.currentTarget);\n        const params = Object.fromEntries(formData) as Record<string, string>;\n        updateCurrentSystem({ params });\n        if (errors.size === 0) {\n          onSubmit();\n        }\n      }}\n    >\n      {/* submit is not triggered when press enter on input without submit button */}\n      <button style={{ display: \"none\" }}>submit</button>\n      <Suggestions\n        containerRef={containerRef}\n        options={history}\n        onSelect={(option) => {\n          flushSync(() => {\n            setPathParams(matchPathnamePattern(path, option) ?? {});\n          });\n          containerRef.current?.requestSubmit();\n        }}\n      />\n      <InputErrorsTooltip errors={Array.from(errors.values())}>\n        <Flex gap={1} css={{ padding: theme.spacing[5] }}>\n          <Flex align=\"center\" gap={1} css={textVariants.mono}>\n            {tokens.map((token, index) => {\n              if (token.type === \"fragment\") {\n                return token.value;\n              }\n              if (token.type === \"param\") {\n                return (\n                  <InputField\n                    key={index}\n                    name={token.name}\n                    fieldSizing=\"content\"\n                    autoComplete=\"off\"\n                    css={{ minWidth: theme.spacing[15] }}\n                    color={errors.has(token.name) ? \"error\" : undefined}\n                    placeholder={token.name}\n                    value={pathParams[token.name] ?? \"\"}\n                    onChange={(event) =>\n                      setPathParams((prevPathParams) => ({\n                        ...prevPathParams,\n                        [token.name]: event.target.value,\n                      }))\n                    }\n                  />\n                );\n              }\n              token satisfies never;\n            })}\n          </Flex>\n\n          <Tooltip {...tooltipProps}>\n            <IconButton\n              {...buttonProps}\n              disabled={errors.size > 0}\n              type=\"button\"\n            />\n          </Tooltip>\n        </Flex>\n      </InputErrorsTooltip>\n    </form>\n  );\n});\n\nexport const AddressBarPopover = () => {\n  const [isOpen, setIsOpen] = useState(false);\n  const path = useStore($selectedPagePath);\n  const publishedOrigin = useStore($publishedOrigin);\n  const { tooltipProps, buttonProps } = useCopyUrl(`${publishedOrigin}${path}`);\n  const formRef = useRef<HTMLFormElement>(null);\n\n  // show only copy button when path is static\n  if (isPathnamePattern(path) === false) {\n    return (\n      <Tooltip {...tooltipProps}>\n        <ToolbarButton\n          {...buttonProps}\n          aria-label=\"Copy page URL\"\n          tabIndex={0}\n        />\n      </Tooltip>\n    );\n  }\n\n  return (\n    <Popover\n      open={isOpen}\n      onOpenChange={(newIsOpen) => {\n        formRef.current?.requestSubmit();\n        setIsOpen(newIsOpen);\n      }}\n    >\n      <PopoverTrigger asChild>\n        <ToolbarButton aria-label=\"Toggle dynamic page address\" tabIndex={0}>\n          <DynamicPageIcon />\n        </ToolbarButton>\n      </PopoverTrigger>\n      <PopoverContent sideOffset={0} collisionPadding={4} align=\"start\">\n        <AddressBar ref={formRef} onSubmit={() => setIsOpen(false)} />\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/assets/assets.tsx",
    "content": "import {\n  IconButton,\n  PanelTitle,\n  Separator,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { BrushCleaningIcon } from \"@webstudio-is/icons\";\nimport { AssetManager } from \"~/builder/shared/asset-manager\";\nimport { AssetUpload } from \"~/builder/shared/assets\";\nimport { openDeleteUnusedAssetsDialog } from \"~/builder/shared/asset-manager/delete-unused-assets\";\n\nexport const AssetsPanel = (_props: { onClose: () => void }) => {\n  return (\n    <>\n      <PanelTitle\n        suffix={\n          <>\n            <Tooltip content=\"Delete unused assets\">\n              <IconButton onClick={openDeleteUnusedAssetsDialog}>\n                <BrushCleaningIcon />\n              </IconButton>\n            </Tooltip>\n            <AssetUpload type=\"file\" />\n          </>\n        }\n      >\n        Assets\n      </PanelTitle>\n      <Separator />\n      <AssetManager />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/assets/index.ts",
    "content": "export * from \"./assets\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/blocking-alerts/alert.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport { Alert } from \"./alert\";\n\nexport default { title: \"Blocking Alerts\", component: Alert };\n\nexport const BlockingAlerts = () => (\n  <StorySection title=\"Blocking Alerts\">\n    <Alert\n      message={\n        \"Your browser window is too small. Resize your browser to at least 900px wide to continue building with Webstudio.\"\n      }\n    />\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/blocking-alerts/alert.tsx",
    "content": "import { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport type { ReactNode } from \"react\";\nimport {\n  Button,\n  css,\n  Flex,\n  Popover,\n  PopoverContent,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { AlertIcon } from \"@webstudio-is/icons\";\n\nconst containerStyle = css({\n  position: \"absolute\",\n  top: theme.spacing[15],\n  left: 0,\n  width: \"100vw\",\n  height: \"100vh\",\n  background: \"rgba(0, 0, 0, 0.9)\",\n});\n\nconst contentStyle = css({\n  width: theme.spacing[33],\n  color: theme.colors.foregroundDestructive,\n});\n\nconst $isAlertDismissed = atom(false);\n\nexport const Alert = ({\n  message,\n  isDismissable,\n}: {\n  message: string | ReactNode;\n  isDismissable?: boolean;\n}) => {\n  const isAlertDismissed = useStore($isAlertDismissed);\n  if (isAlertDismissed) {\n    return;\n  }\n  return (\n    <Popover open>\n      <PopoverContent css={{ zIndex: theme.zIndices.max }}>\n        <Flex align=\"center\" justify=\"center\" className={containerStyle()}>\n          <Flex\n            direction=\"column\"\n            align=\"center\"\n            gap=\"2\"\n            className={contentStyle()}\n          >\n            <AlertIcon size={22} />\n            <Text color=\"contrast\" align=\"center\">\n              {message}\n            </Text>\n            {isDismissable && (\n              <Button\n                color=\"destructive\"\n                onClick={() => $isAlertDismissed.set(true)}\n              >\n                Dismiss\n              </Button>\n            )}\n          </Flex>\n        </Flex>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/blocking-alerts/blocking-alerts.tsx",
    "content": "import { useEffect, useState, type ReactNode } from \"react\";\nimport { Alert } from \"./alert\";\nimport { useWindowResizeDebounced } from \"~/shared/dom-hooks\";\nimport { isFeatureEnabled } from \"@webstudio-is/feature-flags\";\nimport { Link } from \"@webstudio-is/design-system\";\nimport { $isPreviewMode } from \"~/shared/nano-states\";\nimport { useStore } from \"@nanostores/react\";\nimport { $loadingState } from \"~/builder/shared/nano-states\";\n\nconst useTooSmallMessage = () => {\n  const [message, setMessage] = useState<string>();\n  const check = () => {\n    // To have more space for Chrome DevTools, we allow a smaller window size in development\n    const minWidth = process.env.NODE_ENV === \"production\" ? 900 : 700;\n    const message =\n      window.innerWidth >= minWidth\n        ? undefined\n        : `Your browser window is too small. Resize your browser to at least ${minWidth}px wide to continue building with Webstudio.`;\n    setMessage(message);\n  };\n\n  useWindowResizeDebounced(check);\n  useEffect(check, []);\n  return message;\n};\n\nconst useUnsupportedBrowser = () => {\n  const [message, setMessage] = useState<ReactNode>();\n  useEffect(() => {\n    if (\"chrome\" in window || isFeatureEnabled(\"unsupportedBrowsers\")) {\n      return;\n    }\n\n    setMessage(\n      <>\n        The Webstudio Builder UI currently supports any{\" \"}\n        <Link\n          href=\"https://en.wikipedia.org/wiki/Chromium_(web_browser)\"\n          target=\"_blank\"\n          color=\"inherit\"\n          variant=\"inherit\"\n        >\n          Chromium-based\n        </Link>{\" \"}\n        browsers such as{\" \"}\n        <Link\n          href=\"https://www.google.com/chrome\"\n          target=\"_blank\"\n          color=\"inherit\"\n          variant=\"inherit\"\n        >\n          Google Chrome\n        </Link>\n        ,{\" \"}\n        <Link\n          href=\"https://www.microsoft.com/en-us/edge\"\n          target=\"_blank\"\n          color=\"inherit\"\n          variant=\"inherit\"\n        >\n          Microsoft Edge\n        </Link>\n        ,{\" \"}\n        <Link\n          href=\"https://brave.com/\"\n          target=\"_blank\"\n          color=\"inherit\"\n          variant=\"inherit\"\n        >\n          Brave\n        </Link>\n        ,{\" \"}\n        <Link\n          href=\"https://arc.net/\"\n          target=\"_blank\"\n          color=\"inherit\"\n          variant=\"inherit\"\n        >\n          Arc\n        </Link>{\" \"}\n        and many more. We plan to support Firefox and Safari in the near future.\n        <br />\n        <br />\n        The website you&apos;re building should function correctly across all\n        browsers!\n      </>\n    );\n  }, []);\n  return message;\n};\n\nexport const BlockingAlerts = () => {\n  const isPreviewMode = useStore($isPreviewMode);\n  const loadingState = useStore($loadingState);\n\n  const unsupportedBrowsersMessage = useUnsupportedBrowser();\n  // Takes the latest message, order matters\n  const message = [useTooSmallMessage(), unsupportedBrowsersMessage]\n    .filter(Boolean)\n    .pop();\n\n  if (\n    message === undefined ||\n    // We want user to be able to test in unsupported browsers in preview mode.\n    isPreviewMode ||\n    loadingState.state !== \"ready\"\n  ) {\n    return;\n  }\n\n  return (\n    <Alert\n      message={message}\n      isDismissable={unsupportedBrowsersMessage !== undefined}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/blocking-alerts/index.tsx",
    "content": "export { BlockingAlerts } from \"./blocking-alerts\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoint-editor-utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  isConditionBasedBreakpoint,\n  isWidthBasedBreakpoint,\n  isValidBreakpoint,\n  buildBreakpointFromEditorState,\n  hasUnsavedChanges,\n} from \"./breakpoint-editor-utils\";\n\ndescribe(\"breakpoint-editor-utils\", () => {\n  describe(\"isConditionBasedBreakpoint\", () => {\n    test(\"returns true for breakpoint with condition\", () => {\n      expect(\n        isConditionBasedBreakpoint({ condition: \"orientation:portrait\" })\n      ).toBe(true);\n    });\n\n    test(\"returns false for breakpoint with empty condition\", () => {\n      expect(isConditionBasedBreakpoint({ condition: \"\" })).toBe(false);\n      expect(isConditionBasedBreakpoint({ condition: \"  \" })).toBe(false);\n    });\n\n    test(\"returns false for breakpoint without condition\", () => {\n      expect(isConditionBasedBreakpoint({})).toBe(false);\n    });\n  });\n\n  describe(\"isWidthBasedBreakpoint\", () => {\n    test(\"returns true for breakpoint with minWidth\", () => {\n      expect(isWidthBasedBreakpoint({ minWidth: 1024 })).toBe(true);\n    });\n\n    test(\"returns true for breakpoint with maxWidth\", () => {\n      expect(isWidthBasedBreakpoint({ maxWidth: 768 })).toBe(true);\n    });\n\n    test(\"returns false for breakpoint without width\", () => {\n      expect(isWidthBasedBreakpoint({})).toBe(false);\n    });\n  });\n\n  describe(\"isValidBreakpoint\", () => {\n    test(\"returns true for condition-based breakpoint\", () => {\n      expect(isValidBreakpoint({ condition: \"orientation:portrait\" })).toBe(\n        true\n      );\n    });\n\n    test(\"returns true for width-based breakpoint\", () => {\n      expect(isValidBreakpoint({ minWidth: 1024 })).toBe(true);\n      expect(isValidBreakpoint({ maxWidth: 768 })).toBe(true);\n    });\n\n    test(\"returns false for breakpoint with neither condition nor width\", () => {\n      expect(isValidBreakpoint({})).toBe(false);\n    });\n  });\n\n  describe(\"buildBreakpointFromEditorState\", () => {\n    test(\"builds condition-based breakpoint\", () => {\n      const result = buildBreakpointFromEditorState(\n        \"id1\",\n        \"Portrait\",\n        \"orientation:portrait\",\n        \"minWidth\",\n        0,\n        undefined\n      );\n\n      expect(result).toEqual({\n        id: \"id1\",\n        label: \"Portrait\",\n        condition: \"orientation:portrait\",\n      });\n    });\n\n    test(\"builds width-based breakpoint with minWidth\", () => {\n      const result = buildBreakpointFromEditorState(\n        \"id2\",\n        \"Desktop\",\n        \"\",\n        \"minWidth\",\n        1024,\n        undefined\n      );\n\n      expect(result).toEqual({\n        id: \"id2\",\n        label: \"Desktop\",\n        minWidth: 1024,\n      });\n    });\n\n    test(\"builds width-based breakpoint with maxWidth\", () => {\n      const result = buildBreakpointFromEditorState(\n        \"id3\",\n        \"Mobile\",\n        \"\",\n        \"maxWidth\",\n        768,\n        undefined\n      );\n\n      expect(result).toEqual({\n        id: \"id3\",\n        label: \"Mobile\",\n        maxWidth: 768,\n      });\n    });\n\n    test(\"allows zero as valid width (e.g., base breakpoint)\", () => {\n      const result = buildBreakpointFromEditorState(\n        \"id4\",\n        \"Base\",\n        \"\",\n        \"minWidth\",\n        0,\n        undefined\n      );\n\n      expect(result).toEqual({\n        id: \"id4\",\n        label: \"Base\",\n        minWidth: 0,\n      });\n    });\n\n    test(\"converts to width-based when condition is cleared\", () => {\n      const original = {\n        id: \"id5\",\n        label: \"Portrait\",\n        condition: \"orientation:portrait\",\n      };\n\n      const result = buildBreakpointFromEditorState(\n        \"id5\",\n        \"Portrait\",\n        \"\", // cleared condition\n        \"minWidth\",\n        0,\n        original\n      );\n\n      // When condition is cleared with valid width (including 0), convert to width-based\n      expect(result).toEqual({\n        id: \"id5\",\n        label: \"Portrait\",\n        minWidth: 0,\n      });\n    });\n\n    test(\"uses original label if new label is empty\", () => {\n      const original = {\n        id: \"id6\",\n        label: \"Original\",\n        minWidth: 1024,\n      };\n\n      const result = buildBreakpointFromEditorState(\n        \"id6\",\n        \"  \", // empty label\n        \"\",\n        \"minWidth\",\n        1024,\n        original\n      );\n\n      expect(result).toEqual({\n        id: \"id6\",\n        label: \"Original\",\n        minWidth: 1024,\n      });\n    });\n  });\n\n  describe(\"hasUnsavedChanges\", () => {\n    test(\"returns true when label changed\", () => {\n      const original = { id: \"1\", label: \"Old\", minWidth: 1024 };\n      expect(hasUnsavedChanges(original, \"New\", \"\", \"minWidth\", 1024)).toBe(\n        true\n      );\n    });\n\n    test(\"returns true when condition changed\", () => {\n      const original = {\n        id: \"1\",\n        label: \"Test\",\n        condition: \"orientation:portrait\",\n      };\n      expect(\n        hasUnsavedChanges(\n          original,\n          \"Test\",\n          \"orientation:landscape\",\n          \"minWidth\",\n          0\n        )\n      ).toBe(true);\n    });\n\n    test(\"returns true when width value changed\", () => {\n      const original = { id: \"1\", label: \"Test\", minWidth: 1024 };\n      expect(hasUnsavedChanges(original, \"Test\", \"\", \"minWidth\", 768)).toBe(\n        true\n      );\n    });\n\n    test(\"returns false when nothing changed\", () => {\n      const original = { id: \"1\", label: \"Test\", minWidth: 1024 };\n      expect(hasUnsavedChanges(original, \"Test\", \"\", \"minWidth\", 1024)).toBe(\n        false\n      );\n    });\n\n    test(\"returns false for condition-based breakpoint with unchanged condition\", () => {\n      const original = {\n        id: \"1\",\n        label: \"Test\",\n        condition: \"orientation:portrait\",\n      };\n      expect(\n        hasUnsavedChanges(\n          original,\n          \"Test\",\n          \"orientation:portrait\",\n          \"minWidth\",\n          0\n        )\n      ).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoint-editor-utils.ts",
    "content": "import type { Breakpoint } from \"@webstudio-is/sdk\";\n\n/**\n * Determines if a breakpoint is condition-based (has a custom media query condition).\n */\nexport const isConditionBasedBreakpoint = (breakpoint: {\n  condition?: string;\n}): boolean => {\n  return (\n    breakpoint.condition !== undefined && breakpoint.condition.trim() !== \"\"\n  );\n};\n\n/**\n * Determines if a breakpoint is width-based (has minWidth or maxWidth).\n */\nexport const isWidthBasedBreakpoint = (breakpoint: {\n  minWidth?: number;\n  maxWidth?: number;\n}): boolean => {\n  return breakpoint.minWidth !== undefined || breakpoint.maxWidth !== undefined;\n};\n\n/**\n * Validates if a breakpoint has valid data (either condition or width).\n */\nexport const isValidBreakpoint = (breakpoint: {\n  condition?: string;\n  minWidth?: number;\n  maxWidth?: number;\n}): boolean => {\n  return (\n    isConditionBasedBreakpoint(breakpoint) || isWidthBasedBreakpoint(breakpoint)\n  );\n};\n\n/**\n * Builds a breakpoint from editor state, ensuring only condition OR width is set.\n * Returns undefined if the breakpoint is invalid.\n */\nexport const buildBreakpointFromEditorState = (\n  id: string,\n  label: string,\n  conditionValue: string,\n  widthType: \"minWidth\" | \"maxWidth\",\n  widthValue: number,\n  originalBreakpoint?: Breakpoint\n): Breakpoint | undefined => {\n  const trimmedLabel = label.trim();\n  const trimmedCondition = conditionValue.trim();\n  const hasCondition = trimmedCondition !== \"\";\n\n  const newBreakpoint: Breakpoint = {\n    id,\n    label: trimmedLabel || originalBreakpoint?.label || \"\",\n  };\n\n  if (hasCondition) {\n    // Condition-based: only set condition\n    newBreakpoint.condition = trimmedCondition;\n  } else if (widthValue !== undefined && widthValue >= 0) {\n    // Width-based: only set width (zero is valid for base breakpoints)\n    newBreakpoint[widthType] = widthValue;\n  } else if (originalBreakpoint?.condition !== undefined) {\n    // Preserve existing condition if input was cleared\n    newBreakpoint.condition = originalBreakpoint.condition;\n  } else {\n    // Invalid: no condition and no valid width\n    return;\n  }\n\n  return newBreakpoint;\n};\n\n/**\n * Detects if there are unsaved changes in the editor state compared to original breakpoint.\n */\nexport const hasUnsavedChanges = (\n  originalBreakpoint: Breakpoint,\n  label: string,\n  conditionValue: string,\n  widthType: \"minWidth\" | \"maxWidth\",\n  widthValue: number\n): boolean => {\n  if (label !== originalBreakpoint.label) {\n    return true;\n  }\n\n  if (conditionValue !== (originalBreakpoint.condition ?? \"\")) {\n    return true;\n  }\n\n  // For width-based breakpoints, check if width changed\n  if (originalBreakpoint.condition === undefined) {\n    const originalWidth = originalBreakpoint[widthType] ?? 0;\n    if (widthValue !== originalWidth) {\n      return true;\n    }\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoints-container.tsx",
    "content": "import { useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { Flex } from \"@webstudio-is/design-system\";\nimport { $breakpoints, $styles } from \"~/shared/sync/data-stores\";\nimport {\n  $selectedBreakpoint,\n  $selectedBreakpointId,\n} from \"~/shared/nano-states\";\nimport { CanvasSettingsPopover } from \"./canvas-settings-popover\";\nimport { BreakpointsSelector } from \"./breakpoints-selector\";\nimport { BreakpointsMenu } from \"./breakpoints-menu\";\nimport { BreakpointsEditor } from \"./breakpoints-editor\";\nimport { ConfirmationDialog } from \"./confirmation-dialog\";\nimport { isBaseBreakpoint } from \"~/shared/breakpoints\";\nimport { setCanvasWidth } from \"../../shared/calc-canvas-width\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport type { Breakpoint } from \"@webstudio-is/sdk\";\n\nconst hideOnMobile = {\n  \"@media (max-width: 800px)\": {\n    display: \"none\",\n  },\n} as const;\n\nexport const BreakpointsContainer = () => {\n  const breakpoints = useStore($breakpoints);\n  const selectedBreakpoint = useStore($selectedBreakpoint);\n  const [editorOpen, setEditorOpen] = useState(false);\n  const [breakpointToDelete, setBreakpointToDelete] = useState<\n    Breakpoint | undefined\n  >();\n  const [confirmationOpen, setConfirmationOpen] = useState(false);\n\n  const handleDelete = () => {\n    if (breakpointToDelete === undefined) {\n      return;\n    }\n    serverSyncStore.createTransaction(\n      [$breakpoints, $styles],\n      (breakpoints, styles) => {\n        const breakpointId = breakpointToDelete.id;\n        breakpoints.delete(breakpointId);\n        for (const [styleDeclKey, styleDecl] of styles) {\n          if (styleDecl.breakpointId === breakpointId) {\n            styles.delete(styleDeclKey);\n          }\n        }\n      }\n    );\n\n    if (\n      breakpointToDelete.id === selectedBreakpoint?.id &&\n      selectedBreakpoint\n    ) {\n      const breakpointsArray = Array.from(breakpoints.values());\n      const base =\n        breakpointsArray.find(isBaseBreakpoint) ?? breakpointsArray[0];\n      $selectedBreakpointId.set(base.id);\n      setCanvasWidth(base.id);\n    }\n    setBreakpointToDelete(undefined);\n    setConfirmationOpen(false);\n  };\n\n  return (\n    <Flex>\n      <Flex justify=\"end\" css={hideOnMobile}>\n        <CanvasSettingsPopover />\n      </Flex>\n      <Flex align=\"center\" justify=\"center\" css={hideOnMobile}>\n        <BreakpointsSelector />\n      </Flex>\n      {selectedBreakpoint && (\n        <BreakpointsMenu\n          breakpoints={breakpoints}\n          selectedBreakpoint={selectedBreakpoint}\n          onEditClick={() => setEditorOpen(true)}\n        />\n      )}\n      <BreakpointsEditor\n        open={editorOpen}\n        onOpenChange={setEditorOpen}\n        onDelete={(breakpoint) => {\n          setBreakpointToDelete(breakpoint);\n          setEditorOpen(false);\n          setConfirmationOpen(true);\n        }}\n      >\n        <div style={{ height: \"100%\", width: 0 }} />\n      </BreakpointsEditor>\n      {breakpointToDelete && (\n        <ConfirmationDialog\n          open={confirmationOpen}\n          breakpoint={breakpointToDelete}\n          onAbort={() => {\n            setBreakpointToDelete(undefined);\n            setConfirmationOpen(false);\n            setEditorOpen(true);\n          }}\n          onConfirm={handleDelete}\n        />\n      )}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoints-editor.tsx",
    "content": "import { Fragment, useState, useMemo } from \"react\";\nimport { nanoid } from \"nanoid\";\nimport type { Breakpoint } from \"@webstudio-is/sdk\";\nimport {\n  theme,\n  Flex,\n  PanelTitle,\n  Select,\n  IconButton,\n  InputField,\n  Text,\n  PopoverSeparator,\n  Separator,\n  Box,\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n} from \"@webstudio-is/design-system\";\nimport { MinusIcon, PlusIcon } from \"@webstudio-is/icons\";\nimport { useStore } from \"@nanostores/react\";\nimport { $breakpoints } from \"~/shared/sync/data-stores\";\nimport { groupBreakpoints, isBaseBreakpoint } from \"~/shared/breakpoints\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { ConditionInput } from \"./condition-input\";\nimport { CssValueInput } from \"~/builder/features/style-panel/shared/css-value-input\";\nimport { useLocalValue } from \"~/builder/features/settings-panel/shared\";\nimport { buildBreakpointFromEditorState } from \"./breakpoint-editor-utils\";\n\ntype BreakpointEditorItemProps = {\n  breakpoint: Breakpoint;\n  autoFocus?: boolean;\n  onChangeComplete: (breakpoint: Breakpoint) => void;\n  onDelete: (breakpoint: Breakpoint) => void;\n};\n\nconst BreakpointEditorItem = ({\n  breakpoint,\n  autoFocus,\n  onChangeComplete,\n  onDelete,\n}: BreakpointEditorItemProps) => {\n  const [type, setType] = useState<\"minWidth\" | \"maxWidth\">(\n    breakpoint.maxWidth !== undefined ? \"maxWidth\" : \"minWidth\"\n  );\n\n  const initialValue = useMemo(\n    () => ({\n      label: breakpoint.label,\n      condition: breakpoint.condition ?? \"\",\n      width: breakpoint.minWidth ?? breakpoint.maxWidth ?? 0,\n    }),\n    [\n      breakpoint.label,\n      breakpoint.condition,\n      breakpoint.minWidth,\n      breakpoint.maxWidth,\n    ]\n  );\n\n  const localValue = useLocalValue(\n    initialValue,\n    (value) => {\n      const newBreakpoint = buildBreakpointFromEditorState(\n        breakpoint.id,\n        value.label,\n        value.condition,\n        type,\n        value.width,\n        breakpoint\n      );\n\n      if (newBreakpoint !== undefined) {\n        onChangeComplete(newBreakpoint);\n      }\n    },\n    { autoSave: true }\n  );\n\n  const hasCondition = localValue.value.condition.trim() !== \"\";\n\n  return (\n    <Flex gap=\"2\">\n      <Flex direction=\"column\" gap=\"1\">\n        <InputField\n          type=\"text\"\n          value={localValue.value.label}\n          onChange={(event) =>\n            localValue.set({ ...localValue.value, label: event.target.value })\n          }\n          onBlur={localValue.save}\n          placeholder=\"Breakpoint name\"\n          minLength={1}\n          required\n          autoFocus={autoFocus}\n        />\n        <Flex gap=\"2\" css={{ width: theme.spacing[26] }}>\n          <Select\n            css={{ width: theme.spacing[28] }}\n            options={[\"maxWidth\", \"minWidth\"]}\n            getLabel={(option) =>\n              option === \"maxWidth\" ? \"Max Width\" : \"Min Width\"\n            }\n            value={type}\n            onChange={(value) => {\n              setType(value as \"minWidth\" | \"maxWidth\");\n              localValue.save();\n            }}\n            disabled={hasCondition}\n          />\n          <Box css={{ flexShrink: 1 }}>\n            <CssValueInput\n              styleSource=\"local\"\n              property=\"width\"\n              value={{\n                type: \"unit\",\n                value: localValue.value.width,\n                unit: \"px\",\n              }}\n              intermediateValue={undefined}\n              disabled={hasCondition}\n              getOptions={() => []}\n              onChange={(value) => {\n                if (value?.type === \"unit\") {\n                  localValue.set({\n                    ...localValue.value,\n                    width: Math.max(0, value.value),\n                  });\n                } else if (value?.type === \"intermediate\") {\n                  const parsed = parseFloat(value.value);\n                  if (!isNaN(parsed)) {\n                    localValue.set({\n                      ...localValue.value,\n                      width: Math.max(0, parsed),\n                    });\n                  }\n                }\n              }}\n              onChangeComplete={(event) => {\n                if (event.value.type === \"unit\") {\n                  localValue.set({\n                    ...localValue.value,\n                    width: Math.max(0, event.value.value),\n                  });\n                  localValue.save();\n                }\n              }}\n              onHighlight={() => {}}\n              onAbort={() => {\n                localValue.set({\n                  ...localValue.value,\n                  width: breakpoint.minWidth ?? breakpoint.maxWidth ?? 0,\n                });\n              }}\n              onReset={() => {\n                localValue.set({ ...localValue.value, width: 0 });\n                localValue.save();\n              }}\n            />\n          </Box>\n        </Flex>\n        <ConditionInput\n          value={localValue.value.condition}\n          onChange={(value) => {\n            localValue.set({ ...localValue.value, condition: value });\n          }}\n          onBlur={localValue.save}\n        />\n      </Flex>\n      <IconButton\n        onClick={() => {\n          onDelete(breakpoint);\n        }}\n      >\n        <MinusIcon />\n      </IconButton>\n    </Flex>\n  );\n};\n\ntype BreakpointsEditorProps = {\n  onDelete: (breakpoint: Breakpoint) => void;\n  children: React.ReactNode;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\n\nexport const BreakpointsEditor = ({\n  onDelete,\n  children,\n  open,\n  onOpenChange,\n}: BreakpointsEditorProps) => {\n  const breakpoints = useStore($breakpoints);\n  const [addedBreakpoints, setAddedBreakpoints] = useState<Breakpoint[]>([]);\n\n  // Use current breakpoints from store, not stale cached version\n  const grouped = groupBreakpoints(Array.from(breakpoints.values()));\n  const currentBreakpointsFlat = [...grouped.widthBased, ...grouped.custom];\n\n  const allBreakpoints = [\n    ...addedBreakpoints,\n    ...currentBreakpointsFlat.filter(\n      (breakpoint) =>\n        addedBreakpoints.find((added) => added.id === breakpoint.id) ===\n        undefined\n    ),\n  ].filter(\n    (breakpoint) =>\n      breakpoint.condition !== undefined ||\n      isBaseBreakpoint(breakpoint) === false\n  );\n\n  const handleChangeComplete = (breakpoint: Breakpoint) => {\n    serverSyncStore.createTransaction([$breakpoints], (breakpoints) => {\n      breakpoints.set(breakpoint.id, breakpoint);\n    });\n  };\n\n  const handleOpenChange = (newOpen: boolean) => {\n    if (newOpen === false) {\n      setAddedBreakpoints([]);\n    }\n    onOpenChange?.(newOpen);\n  };\n\n  return (\n    <Popover open={open} onOpenChange={handleOpenChange} modal>\n      <PopoverTrigger asChild>{children}</PopoverTrigger>\n      <PopoverContent>\n        <Flex direction=\"column\">\n          <PanelTitle\n            css={{ paddingInline: theme.panel.paddingInline }}\n            suffix={\n              <IconButton\n                onClick={() => {\n                  const newBreakpoint: Breakpoint = {\n                    id: nanoid(),\n                    label: \"\",\n                    minWidth: 0,\n                  };\n                  setAddedBreakpoints([newBreakpoint, ...addedBreakpoints]);\n                }}\n              >\n                <PlusIcon />\n              </IconButton>\n            }\n          >\n            {\"Breakpoints\"}\n          </PanelTitle>\n          <Separator />\n          <Fragment>\n            {allBreakpoints.map((breakpoint, index, all) => {\n              return (\n                <Fragment key={breakpoint.id}>\n                  <Box css={{ p: theme.panel.padding }}>\n                    <BreakpointEditorItem\n                      breakpoint={breakpoint}\n                      onChangeComplete={handleChangeComplete}\n                      onDelete={onDelete}\n                      autoFocus={index === 0}\n                    />\n                  </Box>\n                  {index < all.length - 1 && <PopoverSeparator />}\n                </Fragment>\n              );\n            })}\n          </Fragment>\n          {allBreakpoints.length === 0 && (\n            <Text css={{ margin: theme.spacing[10] }}>\n              No breakpoints found\n            </Text>\n          )}\n        </Flex>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoints-menu.tsx",
    "content": "import { useState } from \"react\";\nimport type { Breakpoint, Breakpoints } from \"@webstudio-is/sdk\";\nimport {\n  Flex,\n  Text,\n  ToolbarButton,\n  theme,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n  DropdownMenuCheckboxItem,\n  DropdownMenuSeparator,\n  Button,\n} from \"@webstudio-is/design-system\";\nimport { EllipsesIcon } from \"@webstudio-is/icons\";\nimport { groupBreakpoints } from \"~/shared/breakpoints\";\nimport { $selectedBreakpointId } from \"~/shared/nano-states\";\nimport { setCanvasWidth } from \"../../shared/calc-canvas-width\";\n\ntype BreakpointsMenuProps = {\n  breakpoints: Breakpoints;\n  selectedBreakpoint: Breakpoint;\n  onEditClick: () => void;\n};\n\nexport const BreakpointsMenu = ({\n  breakpoints,\n  selectedBreakpoint,\n  onEditClick,\n}: BreakpointsMenuProps) => {\n  const [dropdownOpen, setDropdownOpen] = useState(false);\n  const grouped = groupBreakpoints(Array.from(breakpoints.values()));\n  const selectedCustom = grouped.custom.find(\n    (bp) => bp.id === selectedBreakpoint.id\n  );\n\n  return (\n    <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>\n      <DropdownMenuTrigger asChild>\n        <ToolbarButton\n          variant=\"subtle\"\n          aria-label=\"Breakpoints with custom conditions\"\n          data-state={selectedCustom ? \"on\" : \"off\"}\n        >\n          {selectedCustom ? selectedCustom.label : <EllipsesIcon />}\n        </ToolbarButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent css={{ width: theme.spacing[30] }}>\n        {grouped.widthBased.map((breakpoint) => {\n          let description = \"All Sizes\";\n          if (breakpoint.minWidth !== undefined) {\n            description = `≥ ${breakpoint.minWidth} PX`;\n          } else if (breakpoint.maxWidth !== undefined) {\n            description = `≤ ${breakpoint.maxWidth} PX`;\n          }\n          return (\n            <DropdownMenuCheckboxItem\n              key={breakpoint.id}\n              checked={breakpoint.id === selectedBreakpoint.id}\n              onSelect={() => {\n                $selectedBreakpointId.set(breakpoint.id);\n                setCanvasWidth(breakpoint.id);\n              }}\n            >\n              <Flex justify=\"between\" grow gap=\"2\">\n                <Text truncate css={{ flexBasis: \"50%\" }}>\n                  {breakpoint.label}\n                </Text>\n                <Text color=\"subtle\" truncate>\n                  {description}\n                </Text>\n              </Flex>\n            </DropdownMenuCheckboxItem>\n          );\n        })}\n        {grouped.widthBased.length > 0 && grouped.custom.length > 0 && (\n          <DropdownMenuSeparator />\n        )}\n        {grouped.custom.map((breakpoint) => (\n          <DropdownMenuCheckboxItem\n            key={breakpoint.id}\n            checked={breakpoint.id === selectedBreakpoint.id}\n            onSelect={() => {\n              $selectedBreakpointId.set(breakpoint.id);\n              setCanvasWidth(breakpoint.id);\n            }}\n          >\n            <Flex justify=\"between\" grow gap=\"2\">\n              <Text truncate css={{ flexBasis: \"50%\" }}>\n                {breakpoint.label}\n              </Text>\n              <Text color=\"subtle\" truncate>\n                {breakpoint.condition}\n              </Text>\n            </Flex>\n          </DropdownMenuCheckboxItem>\n        ))}\n        {(grouped.widthBased.length > 0 || grouped.custom.length > 0) && (\n          <DropdownMenuSeparator />\n        )}\n\n        <Flex\n          align=\"center\"\n          justify=\"center\"\n          css={{ padding: theme.panel.padding }}\n        >\n          <Button\n            color=\"neutral\"\n            onClick={() => {\n              setDropdownOpen(false);\n              onEditClick();\n            }}\n            css={{ width: \"100%\" }}\n          >\n            Edit breakpoints\n          </Button>\n        </Flex>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoints-selector.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport { BreakpointsSelector as BreakpointsSelectorComponent } from \"./breakpoints-selector\";\nimport { $breakpoints } from \"~/shared/nano-states\";\nimport { $selectedBreakpointId } from \"~/shared/nano-states/breakpoints\";\n\nconst breakpointsMap = new Map([\n  [\"1\", { id: \"1\", label: \"Mobile\", maxWidth: 479 }],\n  [\"2\", { id: \"2\", label: \"Tablet\", maxWidth: 991 }],\n  [\"3\", { id: \"3\", label: \"\" }],\n  [\"4\", { id: \"4\", label: \"Desktop\", minWidth: 1280 }],\n  [\"5\", { id: \"5\", label: \"Wide\", minWidth: 1440 }],\n]);\n\n$breakpoints.set(breakpointsMap);\n$selectedBreakpointId.set(\"3\");\n\nexport const BreakpointsSelector = () => {\n  return (\n    <StorySection title=\"Breakpoints selector\">\n      <BreakpointsSelectorComponent />\n    </StorySection>\n  );\n};\n\nexport default {\n  title: \"Breakpoints selector\",\n  component: BreakpointsSelector,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/breakpoints-selector.tsx",
    "content": "import { useCallback, useRef } from \"react\";\nimport type { Breakpoint } from \"@webstudio-is/sdk\";\nimport {\n  Flex,\n  Text,\n  Toolbar,\n  ToolbarToggleGroup,\n  ToolbarToggleItem,\n  Tooltip,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { AlertIcon, AsteriskIcon } from \"@webstudio-is/icons\";\nimport { useStore } from \"@nanostores/react\";\nimport { CascadeIndicator } from \"./cascade-indicator\";\nimport {\n  $selectedBreakpoint,\n  $selectedBreakpointId,\n  $breakpoints,\n} from \"~/shared/nano-states\";\nimport { groupBreakpoints, isBaseBreakpoint } from \"~/shared/breakpoints\";\nimport { setCanvasWidth } from \"../../shared/calc-canvas-width\";\nimport { $canvasWidth } from \"~/builder/shared/nano-states\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { useEffect, useState } from \"react\";\n\nconst getTooltipContent = (breakpoint: Breakpoint) => {\n  const conditionText = breakpoint.condition\n    ? ` (${breakpoint.condition})`\n    : \"\";\n\n  if (isBaseBreakpoint(breakpoint)) {\n    return (\n      <Text>\n        <Text variant=\"regularBold\">Base{conditionText}</Text>\n        <br />\n        Styles on Base apply to all viewport sizes unless overwritten by another\n        breakpoint. Start your styling here.\n      </Text>\n    );\n  }\n\n  if (breakpoint.condition) {\n    return (\n      <Text>\n        <Text variant=\"regularBold\">{breakpoint.condition}</Text>\n        <br />\n        Styles on this breakpoint apply when {breakpoint.condition}.\n      </Text>\n    );\n  }\n\n  if (breakpoint.maxWidth !== undefined) {\n    return (\n      <Text>\n        <Text variant=\"regularBold\">{breakpoint.maxWidth}px and down</Text>\n        <br />\n        Styles on this breakpoint apply to viewport widths {breakpoint.maxWidth}\n        px and down, unless overwritten by a smaller breakpoint.\n      </Text>\n    );\n  }\n  if (breakpoint.minWidth !== undefined) {\n    return (\n      <Text>\n        <Text variant=\"regularBold\">{breakpoint.minWidth}px and up</Text>\n        <br />\n        Styles on this breakpoint apply to viewport widths {breakpoint.minWidth}\n        px and up, unless overwritten by a larger breakpoint.\n      </Text>\n    );\n  }\n};\n\n// We are testing a specific canvas width using matchMedia to see if a CSS breakpoint would apply.\n// This is needed because browser zoom can cause a mismatch between the actual media query and the displayed breakpoint.\nconst breakpointMatchesMediaQuery = (\n  breakpoint?: Breakpoint,\n  canvasWidth?: number\n) => {\n  // Custom condition breakpoints depend on runtime browser/device features (orientation, hover, color-scheme, etc.)\n  // rather than canvas width, so there's no width-related zoom mismatch to detect. Returning true prevents the\n  // ZoomWarning component from showing false warnings for breakpoints where canvas width is irrelevant.\n  if (breakpoint?.condition !== undefined) {\n    return true;\n  }\n\n  if (\n    canvasWidth === undefined ||\n    (breakpoint?.minWidth === undefined && breakpoint?.maxWidth === undefined)\n  ) {\n    // We don't know in this case if there is a mismatch, so we say it's fine.\n    return true;\n  }\n\n  const iframe = document.createElement(\"iframe\");\n  iframe.style.visibility = \"hidden\";\n  iframe.style.top = \"-100000px\";\n  iframe.style.width = `${canvasWidth}px`;\n  document.body.appendChild(iframe);\n  const queryList = iframe.contentWindow?.matchMedia(\n    `(${breakpoint.minWidth ? \"min\" : \"max\"}-width: ${canvasWidth}px)`\n  );\n  // For some reason we don't get the same results if delete the iframe immediately.\n  requestAnimationFrame(() => {\n    document.body.removeChild(iframe);\n  });\n  return queryList?.matches ?? false;\n};\n\n// When browser zoom is used we can't guarantee that the displayed selected breakpoint is actually matching the media query on the canvas.\n// Actual media query will vary unpredictably, sometimes resulting in 1 px difference and we better warn user they are zooming.\nconst ZoomWarning = () => {\n  const [matches, setMatches] = useState(true);\n  const setMatchesDebounced = useDebouncedCallback((canvasWidth) => {\n    const matches = breakpointMatchesMediaQuery(\n      $selectedBreakpoint.get(),\n      canvasWidth\n    );\n    setMatches(matches);\n  }, 1000);\n\n  useEffect(() => {\n    const unsubscribe = $canvasWidth.listen(setMatchesDebounced);\n    return () => {\n      unsubscribe();\n    };\n  });\n\n  if (matches === true) {\n    return;\n  }\n\n  return (\n    <Tooltip\n      variant=\"wrapped\"\n      content={`Your browser zoom is causing a mismatch between breakpoints and the actual media query on the canvas.`}\n    >\n      <Flex\n        align=\"center\"\n        css={{\n          px: theme.spacing[5],\n          height: \"100%\",\n          color: theme.colors.backgroundAlertMain,\n        }}\n      >\n        <AlertIcon />\n      </Flex>\n    </Tooltip>\n  );\n};\n\nexport const BreakpointsSelector = () => {\n  const breakpoints = useStore($breakpoints);\n  const selectedBreakpoint = useStore($selectedBreakpoint);\n  const refs = useRef(new Map<string, HTMLButtonElement>());\n  const getButtonById = useCallback((id: string) => refs.current.get(id), []);\n\n  if (selectedBreakpoint === undefined) {\n    return;\n  }\n\n  return (\n    <Toolbar>\n      <ToolbarToggleGroup\n        type=\"single\"\n        value={selectedBreakpoint.id}\n        onValueChange={(breakpointId: string) => {\n          // onValueChange gives empty string when unselected\n          // which is not part of breakpoints so do nothing in this case\n          if (breakpoints.has(breakpointId) === false) {\n            return;\n          }\n          $selectedBreakpointId.set(breakpointId);\n          setCanvasWidth(breakpointId);\n        }}\n        css={{ position: \"relative\" }}\n      >\n        {(() => {\n          const grouped = groupBreakpoints(Array.from(breakpoints.values()));\n\n          // Render width-based breakpoints\n          return grouped.widthBased.map((breakpoint) => (\n            <Tooltip\n              key={breakpoint.id}\n              content={getTooltipContent(breakpoint)}\n              variant=\"wrapped\"\n              disableHoverableContent\n            >\n              <ToolbarToggleItem\n                variant=\"subtle\"\n                ref={(node) => {\n                  if (node) {\n                    refs.current.set(breakpoint.id, node);\n                    return;\n                  }\n                  refs.current.delete(breakpoint.id);\n                }}\n                value={breakpoint.id}\n              >\n                {breakpoint.minWidth ?? breakpoint.maxWidth ?? (\n                  <AsteriskIcon size={22} />\n                )}\n              </ToolbarToggleItem>\n            </Tooltip>\n          ));\n        })()}\n        {selectedBreakpoint.condition === undefined && (\n          <CascadeIndicator\n            getButtonById={getButtonById}\n            selectedBreakpoint={selectedBreakpoint}\n            breakpoints={breakpoints}\n          />\n        )}\n      </ToolbarToggleGroup>\n      <ZoomWarning />\n    </Toolbar>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/canvas-settings-popover.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  theme,\n  Flex,\n  Label,\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n  InputField,\n  ToolbarButton,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { WidthInput } from \"./width-input\";\nimport { minCanvasWidth } from \"~/shared/breakpoints\";\nimport { $canvasWidth, $scale } from \"~/builder/shared/nano-states\";\nimport { $selectedBreakpoint } from \"~/shared/nano-states\";\nimport { ChevronDownIcon } from \"@webstudio-is/icons\";\nimport { useState } from \"react\";\n\nexport const CanvasSettingsPopover = () => {\n  const selectedBreakpoint = useStore($selectedBreakpoint);\n  const scale = useStore($scale);\n  const canvasWidth = useStore($canvasWidth);\n  const [isOpen, setIsOpen] = useState(false);\n  if (selectedBreakpoint === undefined || canvasWidth === undefined) {\n    return;\n  }\n  const roundedScale = Math.round(scale);\n\n  return (\n    <Popover open={isOpen} onOpenChange={setIsOpen}>\n      <PopoverTrigger aria-label=\"Canvas Settings\" asChild>\n        <ToolbarButton>\n          <Text\n            css={{\n              display: \"flex\",\n              gap: \"1ch\",\n              fontVariantNumeric: \"tabular-nums\",\n            }}\n            color={isOpen ? \"contrast\" : \"moreSubtle\"}\n          >\n            {Math.round(canvasWidth)}px\n            {roundedScale !== 100 && <span>{`${roundedScale}%`}</span>}\n            <ChevronDownIcon />\n          </Text>\n        </ToolbarButton>\n      </PopoverTrigger>\n      <PopoverContent\n        sideOffset={0}\n        collisionPadding={4}\n        align=\"start\"\n        css={{ width: theme.spacing[30] }}\n      >\n        <Flex css={{ padding: theme.panel.padding }} gap=\"3\">\n          <WidthInput min={minCanvasWidth} />\n          <Flex align=\"center\" gap=\"2\">\n            <Label>Scale</Label>\n            <InputField\n              value={`${Math.round(scale)}%`}\n              tabIndex={-1}\n              readOnly\n            />\n          </Flex>\n        </Flex>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/cascade-indicator.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { css, theme } from \"@webstudio-is/design-system\";\nimport type { Breakpoint, Breakpoints } from \"@webstudio-is/sdk\";\nimport { $breakpoints } from \"~/shared/sync/data-stores\";\nimport { isBaseBreakpoint } from \"~/shared/breakpoints\";\n\nconst cascadeIndicatorStyle = css({\n  position: \"absolute\",\n  bottom: 0,\n  height: 3,\n  borderRadius: 2,\n  transition: \"150ms width, 150ms left, 150ms right\",\n  '&[data-direction=\"left\"]': {\n    background: theme.colors.backgroundGradientHorizontal,\n  },\n  '&[data-direction=\"right\"]': {\n    background: theme.colors.backgroundGradientHorizontalReverse,\n  },\n});\n\nconst hasMinAndMax = (breakpoints: Breakpoints) => {\n  let hasMin = false;\n  let hasMax = false;\n  for (const breakpoint of breakpoints.values()) {\n    if (breakpoint.minWidth !== undefined) {\n      hasMin = true;\n    }\n    if (breakpoint.maxWidth !== undefined) {\n      hasMax = true;\n    }\n  }\n\n  return { hasMin, hasMax };\n};\n\nconst calcIndicatorStyle = ({\n  buttonLeft,\n  buttonWidth,\n  containerWidth,\n  selectedBreakpoint,\n  breakpoints,\n}: {\n  buttonLeft: number;\n  buttonWidth: number;\n  containerWidth: number;\n  selectedBreakpoint: Breakpoint;\n  breakpoints: Breakpoints;\n}) => {\n  if (isBaseBreakpoint(selectedBreakpoint)) {\n    const { hasMin, hasMax } = hasMinAndMax(breakpoints);\n    if (hasMin && hasMax) {\n      return {\n        left: {\n          left: 0,\n          width: buttonLeft + buttonWidth / 2,\n          borderTopRightRadius: 0,\n          borderBottomRightRadius: 0,\n        },\n        right: {\n          width: containerWidth - buttonLeft - buttonWidth / 2,\n          left: buttonLeft + buttonWidth / 2,\n          borderTopLeftRadius: 0,\n          borderBottomLeftRadius: 0,\n        },\n      };\n    }\n    if (hasMin) {\n      return {\n        left: {\n          left: 0,\n          width: buttonLeft + buttonWidth,\n        },\n        right: {\n          width: 0,\n          right: 0,\n        },\n      };\n    }\n  }\n\n  if (selectedBreakpoint.minWidth !== undefined) {\n    return {\n      left: {\n        right: buttonLeft + buttonWidth,\n        width: buttonLeft + buttonWidth,\n        left: 0,\n      },\n      right: {\n        left: 0,\n        width: 0,\n      },\n    };\n  }\n\n  return {\n    left: {\n      left: 0,\n      width: 0,\n    },\n    right: {\n      width: containerWidth - buttonLeft,\n      left: buttonLeft,\n    },\n  };\n};\n\nconst useSizes = ({\n  getButtonById,\n  selectedBreakpoint,\n}: {\n  getButtonById: (id: string) => HTMLButtonElement | undefined;\n  selectedBreakpoint?: Breakpoint;\n}) => {\n  const [buttonLeft, setButtonLeft] = useState<number>();\n  const [buttonWidth, setButtonWidth] = useState<number>();\n  const [containerWidth, setContainerWidth] = useState<number>();\n  const breakpoints = useStore($breakpoints);\n\n  useEffect(() => {\n    if (selectedBreakpoint === undefined) {\n      return;\n    }\n\n    const button = getButtonById(selectedBreakpoint?.id);\n\n    if (button === undefined) {\n      return;\n    }\n\n    setButtonLeft(button.offsetLeft);\n    setButtonWidth(button.offsetWidth);\n    setContainerWidth(button.parentElement?.offsetWidth);\n  }, [\n    selectedBreakpoint,\n    getButtonById,\n    // needed to update sizes when breakpoints are changed\n    breakpoints,\n  ]);\n\n  if (\n    buttonLeft === undefined ||\n    containerWidth === undefined ||\n    buttonWidth === undefined\n  ) {\n    return;\n  }\n  return { buttonLeft, containerWidth, buttonWidth };\n};\n\n// There are 2 indicators left and right.\n// Left one is shown for min breakpoints, right one for max breakpoints.\n// When you have base breakpoint selected which has neither min nor max width, both indicators are shown.\nexport const CascadeIndicator = ({\n  getButtonById,\n  selectedBreakpoint,\n  breakpoints,\n}: {\n  getButtonById: (id: string) => HTMLButtonElement | undefined;\n  selectedBreakpoint: Breakpoint;\n  breakpoints: Breakpoints;\n}) => {\n  const sizes = useSizes({ getButtonById, selectedBreakpoint });\n  if (selectedBreakpoint === undefined || sizes === undefined) {\n    return null;\n  }\n  const indicatorStyle = calcIndicatorStyle({\n    ...sizes,\n    selectedBreakpoint,\n    breakpoints,\n  });\n  return (\n    <>\n      <div\n        className={cascadeIndicatorStyle()}\n        data-direction=\"left\"\n        style={indicatorStyle.left}\n      />\n      <div\n        className={cascadeIndicatorStyle()}\n        data-direction=\"right\"\n        style={indicatorStyle.right}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/condition-input.tsx",
    "content": "import { useMemo } from \"react\";\nimport { Combobox } from \"@webstudio-is/design-system\";\n\nconst PREDEFINED_CONDITIONS = [\n  {\n    value: \"orientation:portrait\",\n    label: \"Orientation: Portrait\",\n    description: \"Device is in portrait mode (height > width)\",\n  },\n  {\n    value: \"orientation:landscape\",\n    label: \"Orientation: Landscape\",\n    description: \"Device is in landscape mode (width > height)\",\n  },\n  {\n    value: \"hover:hover\",\n    label: \"Hover: Hover\",\n    description: \"Primary input can hover over elements\",\n  },\n  {\n    value: \"hover:none\",\n    label: \"Hover: None\",\n    description: \"Primary input cannot hover (e.g., touch screens)\",\n  },\n  {\n    value: \"prefers-color-scheme:dark\",\n    label: \"Color Scheme: Dark\",\n    description: \"User prefers dark color scheme\",\n  },\n  {\n    value: \"prefers-color-scheme:light\",\n    label: \"Color Scheme: Light\",\n    description: \"User prefers light color scheme\",\n  },\n  {\n    value: \"prefers-reduced-motion:reduce\",\n    label: \"Reduced Motion: Reduce\",\n    description: \"User prefers reduced motion/animations\",\n  },\n  {\n    value: \"prefers-reduced-motion:no-preference\",\n    label: \"Reduced Motion: No Preference\",\n    description: \"User has no preference for reduced motion\",\n  },\n  {\n    value: \"pointer:coarse\",\n    label: \"Pointer: Coarse\",\n    description: \"Primary input has limited accuracy (e.g., touch)\",\n  },\n  {\n    value: \"pointer:fine\",\n    label: \"Pointer: Fine\",\n    description: \"Primary input has fine accuracy (e.g., mouse)\",\n  },\n  {\n    value: \"pointer:none\",\n    label: \"Pointer: None\",\n    description: \"No pointing device available\",\n  },\n  {\n    value: \"any-hover:hover\",\n    label: \"Any Hover: Hover\",\n    description: \"At least one input can hover\",\n  },\n  {\n    value: \"any-hover:none\",\n    label: \"Any Hover: None\",\n    description: \"No inputs can hover\",\n  },\n  {\n    value: \"any-pointer:coarse\",\n    label: \"Any Pointer: Coarse\",\n    description: \"At least one input has limited accuracy\",\n  },\n  {\n    value: \"any-pointer:fine\",\n    label: \"Any Pointer: Fine\",\n    description: \"At least one input has fine accuracy\",\n  },\n  {\n    value: \"any-pointer:none\",\n    label: \"Any Pointer: None\",\n    description: \"No pointing devices available\",\n  },\n  {\n    value: \"prefers-contrast:more\",\n    label: \"Contrast: More\",\n    description: \"User prefers higher contrast\",\n  },\n  {\n    value: \"prefers-contrast:less\",\n    label: \"Contrast: Less\",\n    description: \"User prefers lower contrast\",\n  },\n  {\n    value: \"prefers-contrast:no-preference\",\n    label: \"Contrast: No Preference\",\n    description: \"User has no contrast preference\",\n  },\n  {\n    value: \"display-mode:fullscreen\",\n    label: \"Display Mode: Fullscreen\",\n    description: \"App is in fullscreen mode\",\n  },\n  {\n    value: \"display-mode:standalone\",\n    label: \"Display Mode: Standalone\",\n    description: \"App is in standalone mode (PWA)\",\n  },\n  {\n    value: \"display-mode:minimal-ui\",\n    label: \"Display Mode: Minimal UI\",\n    description: \"App with minimal browser UI (PWA)\",\n  },\n  {\n    value: \"display-mode:browser\",\n    label: \"Display Mode: Browser\",\n    description: \"App in regular browser tab\",\n  },\n];\n\ntype Condition = { value: string; label: string; description?: string };\n\ntype ConditionInputProps = {\n  name?: string;\n  value: string;\n  onChange: (value: string) => void;\n  onBlur?: () => void;\n  placeholder?: string;\n};\n\nexport const ConditionInput = ({\n  name,\n  value,\n  onChange,\n  onBlur,\n  placeholder = \"e.g., orientation:portrait\",\n}: ConditionInputProps) => {\n  // Find the matching condition item or create a custom one\n  const selectedItem: Condition | null = useMemo(() => {\n    const found = PREDEFINED_CONDITIONS.find((c) => c.value === value);\n    if (found) {\n      return found;\n    }\n    // If value doesn't match any predefined condition, create a custom item\n    if (value) {\n      return { value, label: value };\n    }\n    return null;\n  }, [value]);\n\n  return (\n    <Combobox<Condition>\n      value={selectedItem}\n      itemToString={(item) => item?.value ?? \"\"}\n      getItems={() => PREDEFINED_CONDITIONS}\n      match={(search, items, itemToString) => {\n        if (!search) {\n          return items;\n        }\n        const searchLower = search.toLowerCase();\n        return items.filter(\n          (item) =>\n            item.label.toLowerCase().includes(searchLower) ||\n            itemToString(item).toLowerCase().includes(searchLower)\n        );\n      }}\n      getItemProps={(item) => ({\n        children: item.label,\n      })}\n      getDescription={(item) => item?.description}\n      onItemSelect={(item) => {\n        if (item) {\n          onChange(item.value);\n        }\n      }}\n      onChange={(value) => {\n        onChange(value ?? \"\");\n      }}\n      name={name}\n      onBlur={onBlur}\n      placeholder={placeholder}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/confirmation-dialog.tsx",
    "content": "import type { Breakpoint } from \"@webstudio-is/sdk\";\nimport {\n  theme,\n  Button,\n  Flex,\n  Text,\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogActions,\n} from \"@webstudio-is/design-system\";\n\ntype ConfirmationDialogProps = {\n  onAbort: () => void;\n  onConfirm: () => void;\n  breakpoint: Breakpoint;\n  open: boolean;\n};\n\nexport const ConfirmationDialog = ({\n  breakpoint,\n  onConfirm,\n  onAbort,\n  open,\n}: ConfirmationDialogProps) => {\n  return (\n    <Dialog open={open} onOpenChange={(isOpen) => !isOpen && onAbort()}>\n      <DialogContent>\n        <DialogTitle>Delete breakpoint</DialogTitle>\n        <Flex gap=\"2\" direction=\"column\" css={{ padding: theme.spacing[5] }}>\n          <Text>{`Are you sure you want to delete \"${breakpoint.label}\"?`}</Text>\n          <Text>\n            {`Deleting a breakpoint will also delete all styles associated with this\n        breakpoint.`}\n          </Text>\n        </Flex>\n        <DialogActions>\n          <Button color=\"neutral\" onClick={onAbort}>\n            Cancel\n          </Button>\n          <Button onClick={onConfirm} color=\"destructive\">\n            Delete\n          </Button>\n        </DialogActions>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/index.ts",
    "content": "export * from \"./breakpoints-container\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/breakpoints/width-input.tsx",
    "content": "import { useState, type KeyboardEvent, useEffect, useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { findApplicableMedia } from \"@webstudio-is/css-engine\";\nimport {\n  theme,\n  Text,\n  Flex,\n  Label,\n  type NumericScrubValue,\n  InputField,\n  useScrub,\n  handleNumericInputArrowKeys,\n} from \"@webstudio-is/design-system\";\nimport { $breakpoints } from \"~/shared/sync/data-stores\";\nimport { $isResizingCanvas } from \"~/shared/nano-states\";\nimport {\n  $selectedBreakpointId,\n  $selectedBreakpoint,\n} from \"~/shared/nano-states\";\nimport { $canvasWidth } from \"~/builder/shared/nano-states\";\n\nconst useEnhancedInput = ({\n  onChange,\n  onChangeComplete,\n  value,\n  min,\n}: {\n  onChange: (value: NumericScrubValue) => void;\n  onChangeComplete: (value: NumericScrubValue) => void;\n  value: number;\n  min: number;\n}) => {\n  const [intermediateValue, setIntermediateValue] = useState<number>();\n\n  const currentValue = intermediateValue ?? value;\n\n  const handleChange = (nextValue: number) => {\n    onChange(Math.max(nextValue, min));\n    setIntermediateValue(undefined);\n  };\n\n  const handleChangeComplete = (nextValue: number) => {\n    onChangeComplete(Math.max(nextValue, min));\n    setIntermediateValue(undefined);\n  };\n\n  const { scrubRef, inputRef } = useScrub({\n    distanceThreshold: 2,\n    value,\n    onChange: handleChange,\n    onChangeComplete: handleChangeComplete,\n  });\n\n  const getValue = () => {\n    const value = inputRef.current?.valueAsNumber;\n    return typeof value === \"number\" && Number.isNaN(value) === false\n      ? value\n      : min;\n  };\n\n  return {\n    ref: scrubRef,\n    inputRef,\n    onChange() {\n      setIntermediateValue(getValue());\n    },\n    onKeyDown(event: KeyboardEvent<HTMLInputElement>) {\n      if (event.key === \"Enter\") {\n        handleChangeComplete(getValue());\n        return;\n      }\n      const nextValue = handleNumericInputArrowKeys(currentValue, event);\n      if (nextValue !== currentValue) {\n        event.preventDefault();\n        handleChange(nextValue);\n      }\n    },\n    onBlur() {\n      handleChangeComplete(getValue());\n    },\n    type: \"number\" as const,\n    value: currentValue,\n  };\n};\n\nexport const WidthInput = ({ min }: { min: number }) => {\n  const id = useId();\n  const canvasWidth = useStore($canvasWidth);\n  const selectedBreakpoint = useStore($selectedBreakpoint);\n  const breakpoints = useStore($breakpoints);\n\n  const onChange = (value: number) => {\n    $canvasWidth.set(value);\n    const applicableBreakpoint = findApplicableMedia(\n      Array.from(breakpoints.values()),\n      value\n    );\n    if (applicableBreakpoint) {\n      $selectedBreakpointId.set(applicableBreakpoint.id);\n    }\n    if ($isResizingCanvas.get() === false) {\n      $isResizingCanvas.set(true);\n    }\n  };\n\n  const onChangeComplete = (value: number) => {\n    onChange(value);\n    $isResizingCanvas.set(false);\n  };\n\n  useEffect(() => {\n    return () => {\n      // Just in case we haven't received onChangeComplete, make sure we have set $isResizingCanvas to false,\n      // otherwise the canvas will be stuck in a resizing state.\n      if ($isResizingCanvas.get()) {\n        $isResizingCanvas.set(false);\n      }\n    };\n  }, []);\n\n  const inputProps = useEnhancedInput({\n    value: canvasWidth ?? 0,\n    onChange,\n    onChangeComplete,\n    min,\n  });\n\n  if (canvasWidth === undefined || selectedBreakpoint === undefined) {\n    return null;\n  }\n\n  return (\n    <Flex gap=\"2\" align=\"center\">\n      <Label htmlFor={id}>Width</Label>\n      <InputField\n        {...inputProps}\n        id={id}\n        suffix={\n          <Text\n            variant=\"unit\"\n            color=\"subtle\"\n            align=\"center\"\n            css={{ paddingInline: theme.spacing[3] }}\n          >\n            PX\n          </Text>\n        }\n      />\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/builder-mode.stories.tsx",
    "content": "import { StorySection, Toolbar } from \"@webstudio-is/design-system\";\nimport { BuilderModeDropDown } from \"./builder-mode\";\nimport {\n  $builderMode,\n  $authPermit,\n  $authToken,\n  $userPlanFeatures,\n} from \"~/shared/nano-states\";\n\nexport default {\n  title: \"Builder mode\",\n};\n\nexport const BuilderMode = () => {\n  $builderMode.set(\"design\");\n  $authPermit.set(\"own\");\n  $authToken.set(undefined);\n  $userPlanFeatures.set({\n    ...$userPlanFeatures.get(),\n    allowContentMode: true,\n  });\n\n  return (\n    <>\n      <StorySection title=\"Design mode\">\n        <Toolbar>\n          <BuilderModeDropDown />\n        </Toolbar>\n      </StorySection>\n\n      <StorySection title=\"Preview mode\">\n        <Toolbar>\n          <BuilderModeDropDown />\n        </Toolbar>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/builder-mode.tsx",
    "content": "import { useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  ChevronDownIcon,\n  NotebookAndPenIcon,\n  PaintBrushIcon,\n  PlayIcon,\n} from \"@webstudio-is/icons\";\nimport {\n  Box,\n  DropdownMenuItemRightSlot,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  Flex,\n  Kbd,\n  MenuCheckedIcon,\n  menuItemCss,\n  theme,\n  ToolbarButton,\n  ToolbarToggleGroup,\n  ToolbarToggleItem,\n  Tooltip,\n  Text,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"@webstudio-is/design-system\";\nimport {\n  $builderMode,\n  $isContentModeAllowed,\n  $isDesignModeAllowed,\n  isBuilderMode,\n  setBuilderMode,\n} from \"~/shared/nano-states\";\nimport { emitCommand } from \"~/builder/shared/commands\";\n\nexport const BuilderModeDropDown = () => {\n  const builderMode = useStore($builderMode);\n  const isContentModeAllowed = useStore($isContentModeAllowed);\n  const isDesignModeAllowed = useStore($isDesignModeAllowed);\n\n  const menuItems = {\n    design: {\n      icon: <PaintBrushIcon />,\n      description: \"Edit components, styles, and properties\",\n      title: \"Design\",\n      shortcut: [\"meta\", \"shift\", \"d\"],\n      enabled: isDesignModeAllowed,\n    },\n    content: {\n      icon: <NotebookAndPenIcon />,\n      description: \"Modify the page content\",\n      title: \"Content\",\n      shortcut: [\"meta\", \"shift\", \"c\"],\n      enabled: isContentModeAllowed,\n    },\n  } as const;\n\n  const [activeMode, setActiveMode] = useState<\n    keyof typeof menuItems | undefined\n  >();\n\n  const handleFocus = (mode: keyof typeof menuItems) => () => {\n    setActiveMode(mode);\n  };\n\n  const handleBlur = () => {\n    setActiveMode(undefined);\n  };\n\n  return (\n    <Flex align=\"center\">\n      <Tooltip\n        content={\n          <Flex gap=\"1\">\n            <Text variant=\"regular\">Toggle preview</Text>\n            <Kbd value={[\"meta\", \"shift\", \"p\"]} />\n          </Flex>\n        }\n      >\n        <ToolbarToggleGroup\n          type=\"single\"\n          value={builderMode}\n          onValueChange={() => {\n            emitCommand(\"togglePreviewMode\");\n          }}\n        >\n          <ToolbarToggleItem variant=\"preview\" value=\"preview\">\n            <PlayIcon />\n          </ToolbarToggleItem>\n        </ToolbarToggleGroup>\n      </Tooltip>\n      <DropdownMenu>\n        <Tooltip content={\"Choose mode\"}>\n          <DropdownMenuTrigger asChild>\n            <ToolbarButton\n              tabIndex={0}\n              aria-label=\"Choose mode\"\n              variant=\"chevron\"\n            >\n              <ChevronDownIcon />\n            </ToolbarButton>\n          </DropdownMenuTrigger>\n        </Tooltip>\n        <DropdownMenuContent\n          sideOffset={4}\n          collisionPadding={16}\n          side=\"bottom\"\n          loop\n        >\n          <DropdownMenuRadioGroup\n            value={builderMode}\n            onValueChange={(value) => {\n              if (isBuilderMode(value)) {\n                setBuilderMode(value);\n              }\n            }}\n          >\n            {Object.entries(menuItems)\n              .filter(([_, { enabled }]) => enabled)\n              .map(([mode, { icon, title, shortcut }]) => (\n                <DropdownMenuRadioItem\n                  key={mode}\n                  value={mode}\n                  onFocus={handleFocus(mode as keyof typeof menuItems)}\n                  onBlur={handleBlur}\n                  icon={<MenuCheckedIcon />}\n                >\n                  <Flex css={{ px: theme.spacing[3] }} gap={2}>\n                    {icon}\n                    <Box>{title}</Box>\n                  </Flex>\n                  <DropdownMenuItemRightSlot>\n                    <Kbd value={shortcut} />\n                  </DropdownMenuItemRightSlot>\n                  &nbsp;\n                </DropdownMenuRadioItem>\n              ))}\n          </DropdownMenuRadioGroup>\n          <DropdownMenuSeparator />\n\n          <div className={menuItemCss({ hint: true })}>\n            <Box css={{ width: theme.spacing[25] }}>\n              {activeMode\n                ? menuItems[activeMode].description\n                : \"Select Design or Content mode\"}\n            </Box>\n          </div>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/clone.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { buttonStyle, Link } from \"@webstudio-is/design-system\";\nimport { $authToken, $authTokenPermissions } from \"~/shared/nano-states\";\nimport { cloneProjectUrl } from \"~/shared/router-utils/path-utils\";\n\nexport const CloneButton = () => {\n  const authTokenPermission = useStore($authTokenPermissions);\n  const authToken = useStore($authToken);\n\n  if (authToken === undefined || false === authTokenPermission.canClone) {\n    return;\n  }\n\n  return (\n    <Link\n      data-state=\"auto\"\n      className={buttonStyle({\n        color: \"positive\",\n      })}\n      color=\"contrast\"\n      href={cloneProjectUrl({\n        origin: window.origin,\n        sourceAuthToken: authToken,\n      })}\n      underline=\"none\"\n    >\n      Clone\n    </Link>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/command-panel.stories.tsx",
    "content": "import type { Meta, StoryFn } from \"@storybook/react\";\nimport { useEffect } from \"react\";\nimport { initialBreakpoints, coreMetas } from \"@webstudio-is/sdk\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport {\n  $breakpoints,\n  $pages,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { CommandPanel as CommandPanelComponent } from \"./command-panel\";\nimport { openCommandPanel } from \"./command-state\";\n\nconst meta: Meta = {\n  title: \"Command Panel\",\n};\nexport default meta;\n\nregisterContainers();\n\n$registeredComponentMetas.set(\n  new Map(\n    Object.entries({\n      ...coreMetas,\n      ...baseComponentMetas,\n    })\n  )\n);\n\n$breakpoints.set(\n  new Map(\n    initialBreakpoints.map((breakpoint, index) => [\n      index.toString(),\n      { ...breakpoint, id: index.toString() },\n    ])\n  )\n);\n\nconst pages = createDefaultPages({ rootInstanceId: \"\" });\npages.pages.push({\n  id: \"page2\",\n  path: \"\",\n  name: \"Second Page\",\n  rootInstanceId: \"\",\n  title: \"\",\n  meta: {},\n});\npages.pages.push({\n  id: \"page3\",\n  path: \"\",\n  name: \"Thrid Page\",\n  rootInstanceId: \"\",\n  title: \"\",\n  meta: {},\n});\n$pages.set(pages);\n$awareness.set({ pageId: pages.homePage.id });\n\nexport const CommandPanel: StoryFn = () => {\n  useEffect(() => {\n    openCommandPanel();\n  }, []);\n  return (\n    <StorySection title=\"Command Panel\">\n      <CommandPanelComponent />\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/command-panel.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { matchSorter } from \"match-sorter\";\nimport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  ScrollArea,\n  Flex,\n  CommandFooter,\n} from \"@webstudio-is/design-system\";\nimport { mapGroupBy } from \"~/shared/shim\";\nimport {\n  $commandContent,\n  $isCommandPanelOpen,\n  $commandSearch,\n  $commandContentKey,\n  closeCommandPanel,\n} from \"./command-state\";\nimport { $allOptions, groups, type Option } from \"./groups\";\nimport { useAutoSelectFirstItem } from \"./shared/auto-select\";\n\nconst renderGroup = (type: Option[\"type\"], matches: Option[]): JSX.Element => {\n  const Group = groups[type];\n  // Type assertion is safe here because matches are filtered by type before calling renderGroup\n  return <Group key={type} options={matches as never} />;\n};\n\nconst CommandDialogContent = () => {\n  const search = useStore($commandSearch);\n  const options = useStore($allOptions);\n  const listRef = useAutoSelectFirstItem(search);\n\n  let matches = options;\n  // prevent searching when value is empty\n  // to preserve original items order\n  if (search.trim().length > 0) {\n    for (const word of search.trim().split(/\\s+/)) {\n      matches = matchSorter(matches, word, {\n        keys: [\"terms\"],\n      });\n    }\n  }\n  const matchGroups = mapGroupBy<Option, Option[\"type\"]>(\n    matches,\n    (match) => match.type\n  );\n\n  return (\n    <>\n      <CommandInput\n        value={search}\n        onValueChange={(value) => $commandSearch.set(value)}\n      />\n      <Flex direction=\"column\" css={{ maxHeight: 300 }}>\n        <ScrollArea>\n          <CommandList ref={listRef}>\n            {Array.from(matchGroups).map(([type, matches]) =>\n              renderGroup(type, matches)\n            )}\n          </CommandList>\n        </ScrollArea>\n      </Flex>\n      <CommandFooter />\n    </>\n  );\n};\n\nexport const CommandPanel = () => {\n  const isOpen = useStore($isCommandPanelOpen);\n  const commandContent = useStore($commandContent);\n  const contentKey = useStore($commandContentKey);\n\n  return (\n    <CommandDialog\n      open={isOpen}\n      onOpenChange={() => closeCommandPanel({ restoreFocus: true })}\n    >\n      <Command key={contentKey} shouldFilter={false}>\n        {commandContent ?? <CommandDialogContent />}\n      </Command>\n    </CommandDialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/command-state.ts",
    "content": "import { atom, computed } from \"nanostores\";\nimport type { ReactNode } from \"react\";\n\nconst $commandPanel = atom<\n  | undefined\n  | {\n      lastFocusedElement: null | HTMLElement;\n    }\n>();\n\nexport const $isCommandPanelOpen = computed(\n  $commandPanel,\n  (commandPanel) => commandPanel !== undefined\n);\n\nexport const openCommandPanel = () => {\n  const activeElement =\n    document.activeElement instanceof HTMLElement\n      ? document.activeElement\n      : null;\n  // store last focused element\n  $commandPanel.set({\n    lastFocusedElement: activeElement,\n  });\n};\n\nexport const closeCommandPanel = ({\n  restoreFocus = false,\n}: { restoreFocus?: boolean } = {}) => {\n  const commandPanel = $commandPanel.get();\n  $commandPanel.set(undefined);\n  $commandContent.set(undefined);\n  $commandSearch.set(\"\");\n  // restore focus in the next frame\n  if (restoreFocus && commandPanel?.lastFocusedElement) {\n    requestAnimationFrame(() => {\n      commandPanel.lastFocusedElement?.focus();\n    });\n  }\n};\n\nexport const $commandContent = atom<ReactNode>();\n\n// Counter to force Command component remount when content changes\nexport const $commandContentKey = atom(0);\n\nexport const $commandSearch = atom(\"\");\n\nexport const focusCommandPanel = () => {\n  requestAnimationFrame(() => {\n    const input = document.querySelector<HTMLInputElement>(\n      \"[cmdk-root] [cmdk-input]\"\n    );\n    input?.focus();\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/breakpoints-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  Kbd,\n} from \"@webstudio-is/design-system\";\nimport { computed } from \"nanostores\";\nimport { compareMedia } from \"@webstudio-is/css-engine\";\nimport type { Breakpoint } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $selectedBreakpoint,\n  $selectedBreakpointId,\n} from \"~/shared/nano-states\";\nimport { closeCommandPanel, $isCommandPanelOpen } from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\nimport { setCanvasWidth } from \"~/builder/shared/calc-canvas-width\";\n\nexport type BreakpointOption = BaseOption & {\n  type: \"breakpoint\";\n  breakpoint: Breakpoint;\n  keys: string[];\n};\n\nexport const $breakpointOptions = computed(\n  [$isCommandPanelOpen, $breakpoints, $selectedBreakpoint],\n  (isOpen, breakpoints, selectedBreakpoint) => {\n    if (!isOpen) {\n      return [];\n    }\n    const allBreakpoints = Array.from(breakpoints.values());\n\n    // Separate custom condition breakpoints from width-based\n    const customBreakpoints = allBreakpoints.filter(\n      (bp) => bp.condition !== undefined\n    );\n    const widthBasedBreakpoints = allBreakpoints\n      .filter((bp) => bp.condition === undefined)\n      .sort(compareMedia);\n\n    // Combine with custom conditions at the end\n    const sortedBreakpoints = [...widthBasedBreakpoints, ...customBreakpoints];\n\n    const breakpointOptions: BreakpointOption[] = [];\n    for (let index = 0; index < sortedBreakpoints.length; index += 1) {\n      const breakpoint = sortedBreakpoints[index];\n      if (breakpoint.id === selectedBreakpoint?.id) {\n        continue;\n      }\n      const width =\n        breakpoint.condition ??\n        (breakpoint.minWidth ?? breakpoint.maxWidth)?.toString() ??\n        \"\";\n      breakpointOptions.push({\n        terms: [\"breakpoints\", breakpoint.label, width],\n        type: \"breakpoint\",\n        breakpoint,\n        keys: [(index + 1).toString()],\n      });\n    }\n    return breakpointOptions;\n  }\n);\n\nconst getBreakpointLabel = (breakpoint: Breakpoint) => {\n  if (breakpoint.condition) {\n    return `${breakpoint.label}: ${breakpoint.condition}`;\n  }\n\n  let label = \"All Sizes\";\n  if (breakpoint.minWidth !== undefined) {\n    label = `≥ ${breakpoint.minWidth} PX`;\n  }\n  if (breakpoint.maxWidth !== undefined) {\n    label = `≤ ${breakpoint.maxWidth} PX`;\n  }\n  return `${breakpoint.label}: ${label}`;\n};\n\nexport const BreakpointsGroup = ({\n  options,\n}: {\n  options: BreakpointOption[];\n}) => {\n  return (\n    <CommandGroup\n      name=\"breakpoint\"\n      heading={\n        <CommandGroupHeading>\n          Breakpoints ({options.length})\n        </CommandGroupHeading>\n      }\n      actions={[{ name: \"select\", label: \"Select\" }]}\n    >\n      {options.map(({ breakpoint, keys }) => (\n        <CommandItem\n          key={breakpoint.id}\n          // preserve selected state when rerender\n          value={breakpoint.id}\n          onSelect={() => {\n            closeCommandPanel({ restoreFocus: true });\n            $selectedBreakpointId.set(breakpoint.id);\n            setCanvasWidth(breakpoint.id);\n          }}\n        >\n          <Text>{getBreakpointLabel(breakpoint)}</Text>\n          <Kbd value={keys} />\n        </CommandItem>\n      ))}\n    </CommandGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/commands-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  Kbd,\n} from \"@webstudio-is/design-system\";\nimport { computed } from \"nanostores\";\nimport { $commandMetas } from \"~/shared/commands-emitter\";\nimport { emitCommand } from \"~/builder/shared/commands\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { closeCommandPanel } from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\n\nexport type CommandOption = BaseOption & {\n  type: \"command\";\n  name: string;\n  label: string;\n  keys?: string[];\n  keepCommandPanelOpen?: boolean;\n};\n\nimport { $isCommandPanelOpen } from \"../command-state\";\n\nexport const $commandOptions = computed(\n  [$isCommandPanelOpen, $commandMetas],\n  (isOpen, commandMetas) => {\n    if (!isOpen) {\n      return [];\n    }\n    const commandOptions: CommandOption[] = [];\n    for (const [name, meta] of commandMetas) {\n      if (!meta.hidden) {\n        const label = meta.label ?? humanizeString(name);\n        const keys = meta.defaultHotkeys?.[0]?.split(\"+\");\n        commandOptions.push({\n          terms: [\"shortcuts\", \"commands\", label],\n          type: \"command\",\n          name,\n          label,\n          keys,\n          keepCommandPanelOpen: meta.keepCommandPanelOpen,\n        });\n      }\n    }\n    commandOptions.sort(\n      (left, right) => (left.keys ? 0 : 1) - (right.keys ? 0 : 1)\n    );\n    return commandOptions;\n  }\n);\n\nexport const CommandsGroup = ({ options }: { options: CommandOption[] }) => {\n  return (\n    <CommandGroup\n      name=\"command\"\n      heading={\n        <CommandGroupHeading>Commands ({options.length})</CommandGroupHeading>\n      }\n      actions={[{ name: \"execute\", label: \"Execute\" }]}\n    >\n      {options.map(({ name, label, keys, keepCommandPanelOpen }) => (\n        <CommandItem\n          key={name}\n          // preserve selected state when rerender\n          value={name}\n          onSelect={() => {\n            if (!keepCommandPanelOpen) {\n              closeCommandPanel();\n            }\n            emitCommand(name as never);\n          }}\n        >\n          <Text>{label}</Text>\n          {keys && <Kbd value={keys} />}\n        </CommandItem>\n      ))}\n    </CommandGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/components-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  CommandIcon,\n  Flex,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { computed } from \"nanostores\";\nimport type { TemplateMeta } from \"@webstudio-is/template\";\nimport { collectionComponent, elementComponent } from \"@webstudio-is/sdk\";\nimport {\n  $registeredComponentMetas,\n  $registeredTemplates,\n} from \"~/shared/nano-states\";\nimport {\n  getComponentTemplateData,\n  insertWebstudioElementAt,\n  insertWebstudioFragmentAt,\n} from \"~/shared/instance-utils\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { $selectedPage } from \"~/shared/awareness\";\nimport {\n  getInstanceLabel,\n  InstanceIcon,\n} from \"~/builder/shared/instance-label\";\nimport { closeCommandPanel, $isCommandPanelOpen } from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\nimport {\n  shouldFilterCategory,\n  getComponentScore,\n} from \"../shared/component-utils\";\n\nexport type ComponentOption = BaseOption & {\n  type: \"component\";\n  component: string;\n  label: string;\n  category: TemplateMeta[\"category\"];\n  icon: undefined | string;\n  order: undefined | number;\n  firstInstance: { component: string };\n};\n\nexport const $componentOptions = computed(\n  [\n    $isCommandPanelOpen,\n    $registeredComponentMetas,\n    $registeredTemplates,\n    $selectedPage,\n  ],\n  (isOpen, metas, templates, selectedPage) => {\n    const componentOptions: ComponentOption[] = [];\n    if (!isOpen) {\n      return componentOptions;\n    }\n\n    const addComponentOption = ({\n      name,\n      category,\n      label,\n      icon,\n      order,\n      firstInstance,\n    }: {\n      name: string;\n      category: TemplateMeta[\"category\"];\n      label: string;\n      icon?: string;\n      order?: number;\n      firstInstance: { component: string };\n    }) => {\n      // show only xml category and collection component in xml documents\n      if (selectedPage?.meta.documentType === \"xml\") {\n        if (category !== \"xml\" && name !== collectionComponent) {\n          return;\n        }\n      } else {\n        // show everything except xml category in html documents\n        if (category === \"xml\") {\n          return;\n        }\n      }\n\n      componentOptions.push({\n        terms: [\"components\", label, category],\n        type: \"component\",\n        component: name,\n        label,\n        category,\n        icon,\n        order,\n        firstInstance,\n      });\n    };\n\n    for (const [name, meta] of metas) {\n      if (shouldFilterCategory(meta.category)) {\n        continue;\n      }\n      const category = meta.category ?? \"hidden\";\n      const label = meta.label ?? getInstanceLabel({ component: name });\n\n      addComponentOption({\n        name,\n        category,\n        label,\n        icon: meta.icon,\n        order: meta.order,\n        firstInstance: { component: name },\n      });\n    }\n\n    for (const [name, meta] of templates) {\n      if (shouldFilterCategory(meta.category)) {\n        continue;\n      }\n\n      const componentMeta = metas.get(name);\n      const label =\n        meta.label ??\n        componentMeta?.label ??\n        getInstanceLabel({ component: name });\n\n      addComponentOption({\n        name,\n        category: meta.category,\n        label,\n        icon: meta.icon ?? componentMeta?.icon,\n        order: meta.order,\n        firstInstance: meta.template.instances[0],\n      });\n    }\n\n    componentOptions.sort(\n      (leftOption, rightOption) =>\n        getComponentScore(leftOption) - getComponentScore(rightOption)\n    );\n    return componentOptions;\n  }\n);\n\nexport const ComponentsGroup = ({\n  options,\n}: {\n  options: ComponentOption[];\n}) => {\n  return (\n    <CommandGroup\n      name=\"component\"\n      heading={\n        <CommandGroupHeading>Components ({options.length})</CommandGroupHeading>\n      }\n      actions={[{ name: \"add\", label: \"Add\" }]}\n    >\n      {options.map(({ component, label, category, icon, firstInstance }) => {\n        return (\n          <CommandItem\n            key={component}\n            // preserve selected state when rerender\n            value={component}\n            onSelect={() => {\n              closeCommandPanel();\n              if (component === elementComponent) {\n                insertWebstudioElementAt();\n              } else {\n                const fragment = getComponentTemplateData(component);\n                if (fragment) {\n                  insertWebstudioFragmentAt(fragment);\n                }\n              }\n            }}\n          >\n            <Flex gap={2}>\n              <CommandIcon>\n                <InstanceIcon instance={firstInstance} icon={icon} />\n              </CommandIcon>\n              <Text>\n                {label}{\" \"}\n                <Text as=\"span\" color=\"moreSubtle\">\n                  {humanizeString(category)}\n                </Text>\n              </Text>\n            </Flex>\n          </CommandItem>\n        );\n      })}\n    </CommandGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/convert-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandIcon,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandBackButton,\n  CommandFooter,\n  Flex,\n  ScrollArea,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { matchSorter } from \"match-sorter\";\nimport { computed } from \"nanostores\";\nimport { elementComponent, tags } from \"@webstudio-is/sdk\";\nimport {\n  $instances,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { $selectedInstancePath } from \"~/shared/awareness\";\nimport {\n  getInstanceLabel,\n  InstanceIcon,\n} from \"~/builder/shared/instance-label\";\nimport { canConvertInstance } from \"~/shared/instance-utils\";\nimport {\n  $commandContent,\n  $isCommandPanelOpen,\n  closeCommandPanel,\n  openCommandPanel,\n} from \"../command-state\";\nimport { useState } from \"react\";\nimport { convertInstance } from \"~/shared/instance-utils\";\n\ntype ConvertOption = {\n  component: string;\n  tag?: string;\n  label: string;\n  category?: string;\n  order?: number;\n};\n\nconst $convertOptions = computed(\n  [\n    $isCommandPanelOpen,\n    $selectedInstancePath,\n    $instances,\n    $props,\n    $registeredComponentMetas,\n  ],\n  (isOpen, instancePath, instances, props, metas) => {\n    const convertOptions: ConvertOption[] = [];\n    if (!isOpen) {\n      return convertOptions;\n    }\n    if (instancePath === undefined || instancePath.length === 1) {\n      return convertOptions;\n    }\n    const [selectedItem] = instancePath;\n\n    // Test all registered components\n    for (const [componentName, meta] of metas) {\n      // Skip the current component\n      if (componentName === selectedItem.instance.component) {\n        continue;\n      }\n\n      if (\n        canConvertInstance(\n          selectedItem.instance.id,\n          selectedItem.instanceSelector,\n          componentName,\n          undefined,\n          instances,\n          props,\n          metas\n        )\n      ) {\n        const label = getInstanceLabel({ component: componentName });\n        convertOptions.push({\n          component: componentName,\n          label,\n          category: meta?.category,\n          order: meta?.order,\n        });\n      }\n    }\n\n    // Test all valid HTML tags (for Element component)\n    for (const tag of tags) {\n      if (\n        canConvertInstance(\n          selectedItem.instance.id,\n          selectedItem.instanceSelector,\n          elementComponent,\n          tag,\n          instances,\n          props,\n          metas\n        )\n      ) {\n        const label = getInstanceLabel({ component: elementComponent, tag });\n        convertOptions.push({\n          component: elementComponent,\n          tag,\n          label,\n        });\n      }\n    }\n\n    return convertOptions;\n  }\n);\n\nconst ConvertComponentsList = () => {\n  const [search, setSearch] = useState(\"\");\n  const convertOptions = $convertOptions.get();\n\n  let matches = convertOptions;\n  if (search.trim().length > 0) {\n    for (const word of search.trim().split(/\\s+/)) {\n      matches = matchSorter(matches, word, {\n        keys: [\"label\"],\n      });\n    }\n  }\n\n  const goBack = () => {\n    $commandContent.set(undefined);\n  };\n\n  return (\n    <>\n      <CommandInput\n        action={{ name: \"convert\", label: \"Convert\" }}\n        placeholder=\"Search components to convert...\"\n        value={search}\n        onValueChange={setSearch}\n        prefix={<CommandBackButton onClick={goBack} />}\n        onBack={goBack}\n      />\n      <Flex direction=\"column\" css={{ maxHeight: 300 }}>\n        <ScrollArea>\n          <CommandList>\n            {matches.length === 0 ? (\n              <Flex justify=\"center\" align=\"center\" css={{ minHeight: 100 }}>\n                <Text color=\"subtle\" align=\"center\">\n                  No components found that this instance can be converted into\n                </Text>\n              </Flex>\n            ) : (\n              <CommandGroup\n                name=\"convert-components\"\n                actions={[{ name: \"convert\", label: \"Convert\" }]}\n              >\n                {matches.map(({ component, tag, label }) => {\n                  const key = tag ? `${component}:${tag}` : component;\n                  return (\n                    <CommandItem\n                      key={key}\n                      value={key}\n                      onSelect={() => {\n                        convertInstance(component, tag);\n                        closeCommandPanel();\n                      }}\n                    >\n                      <Flex gap={2}>\n                        <CommandIcon>\n                          <InstanceIcon instance={{ component, tag }} />\n                        </CommandIcon>\n                        <Text>{label}</Text>\n                      </Flex>\n                    </CommandItem>\n                  );\n                })}\n              </CommandGroup>\n            )}\n          </CommandList>\n        </ScrollArea>\n      </Flex>\n      <CommandFooter />\n    </>\n  );\n};\n\nexport const showConvertComponentsList = () => {\n  openCommandPanel();\n  $commandContent.set(<ConvertComponentsList />);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/css-variables-group.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  toast,\n  useSelectedAction,\n  useResetActionIndex,\n} from \"@webstudio-is/design-system\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  DeleteCssVariableDialog,\n  RenameCssVariableDialog,\n  $cssVariableInstancesByVariable,\n  $cssVariableDefinitionsByVariable,\n  $unusedCssVariables,\n} from \"~/builder/shared/css-variable-utils\";\nimport { deleteProperty } from \"~/builder/features/style-panel/shared/use-style-data\";\nimport { InstanceList, showInstance } from \"../shared/instance-list\";\nimport {\n  $commandContent,\n  $isCommandPanelOpen,\n  closeCommandPanel,\n  focusCommandPanel,\n} from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\nimport { formatUsageCount, getUsageSearchTerms } from \"../shared/usage-utils\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\n\nexport type CssVariableOption = BaseOption & {\n  type: \"cssVariable\";\n  property: string;\n  instanceId: Instance[\"id\"];\n  usages: number;\n};\n\nexport const $cssVariableOptions = computed(\n  [$isCommandPanelOpen, $cssVariableDefinitionsByVariable, $unusedCssVariables],\n  (isOpen, definitionsByVariable, unusedVariables) => {\n    const cssVariableOptions: CssVariableOption[] = [];\n    if (!isOpen) {\n      return cssVariableOptions;\n    }\n\n    // Create options for each defined CSS variable on each instance\n    for (const [property, instanceIds] of definitionsByVariable) {\n      for (const instanceId of instanceIds) {\n        const usages = unusedVariables.has(property) ? 0 : 1; // 0 if unused, 1+ otherwise\n        cssVariableOptions.push({\n          terms: [\n            \"css variables\",\n            property,\n            property.slice(2), // Include name without --\n            getInstanceLabel(instanceId),\n            ...getUsageSearchTerms(usages),\n          ],\n          type: \"cssVariable\",\n          property,\n          instanceId,\n          usages,\n        });\n      }\n    }\n\n    return cssVariableOptions;\n  }\n);\n\nconst CssVariableInstances = ({ property }: { property: string }) => {\n  const instancesByVariable = useStore($cssVariableInstancesByVariable);\n  const usedInInstanceIds = instancesByVariable.get(property) ?? new Set();\n\n  return (\n    <InstanceList\n      instanceIds={usedInInstanceIds}\n      onSelect={(instanceId) => {\n        showInstance(instanceId, \"style\");\n        closeCommandPanel();\n      }}\n    />\n  );\n};\n\nexport const CssVariablesGroup = ({\n  options,\n}: {\n  options: CssVariableOption[];\n}) => {\n  const action = useSelectedAction();\n  const resetActionIndex = useResetActionIndex();\n  const [variableDialog, setVariableDialog] = useState<\n    { action: \"rename\" | \"delete\"; property: string } | undefined\n  >();\n\n  return (\n    <>\n      <CommandGroup\n        name=\"cssVariable\"\n        heading={\n          <CommandGroupHeading>\n            CSS Variables ({options.length})\n          </CommandGroupHeading>\n        }\n        actions={[\n          { name: \"select\", label: \"Select\" },\n          { name: \"findUsages\", label: \"Find usages\" },\n          { name: \"rename\", label: \"Rename\" },\n          { name: \"delete\", label: \"Delete\" },\n        ]}\n      >\n        {options.map(({ property, instanceId, usages, terms }) => {\n          return (\n            <CommandItem\n              keywords={terms}\n              key={`${property}-${instanceId}`}\n              // preserve selected state when rerender\n              value={`${property}-${instanceId}`}\n              onSelect={() => {\n                if (action?.name === \"select\") {\n                  showInstance(instanceId, \"style\");\n                  closeCommandPanel();\n                }\n                if (action?.name === \"findUsages\") {\n                  $commandContent.set(\n                    <CssVariableInstances property={property} />\n                  );\n                }\n                if (action?.name === \"rename\") {\n                  setVariableDialog({ action: \"rename\", property });\n                }\n                if (action?.name === \"delete\") {\n                  setVariableDialog({ action: \"delete\", property });\n                }\n              }}\n            >\n              <Text>\n                {property}{\" \"}\n                <Text as=\"span\" color=\"moreSubtle\">\n                  {formatUsageCount(usages)}\n                </Text>\n              </Text>\n              <Text\n                as=\"span\"\n                css={{ maxWidth: \"20ch\" }}\n                truncate\n                color=\"moreSubtle\"\n              >\n                {getInstanceLabel(instanceId)}\n              </Text>\n            </CommandItem>\n          );\n        })}\n      </CommandGroup>\n      <RenameCssVariableDialog\n        cssVariable={\n          variableDialog?.action === \"rename\" ? variableDialog : undefined\n        }\n        onClose={() => {\n          setVariableDialog(undefined);\n          resetActionIndex();\n          focusCommandPanel();\n        }}\n        onConfirm={(_oldProperty, newProperty) => {\n          toast.success(\n            `CSS variable renamed from \"${variableDialog?.property}\" to \"${newProperty}\"`\n          );\n          setVariableDialog(undefined);\n        }}\n      />\n      <DeleteCssVariableDialog\n        cssVariable={\n          variableDialog?.action === \"delete\" ? variableDialog : undefined\n        }\n        onClose={() => {\n          setVariableDialog(undefined);\n          resetActionIndex();\n          focusCommandPanel();\n        }}\n        onConfirm={(property) => {\n          deleteProperty(property as CssProperty);\n          toast.success(`CSS variable \"${variableDialog?.property}\" deleted`);\n          setVariableDialog(undefined);\n        }}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/data-variables-group.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  toast,\n  useSelectedAction,\n  useResetActionIndex,\n} from \"@webstudio-is/design-system\";\nimport { $dataSources } from \"~/shared/sync/data-stores\";\nimport {\n  $commandContent,\n  $isCommandPanelOpen,\n  closeCommandPanel,\n  focusCommandPanel,\n} from \"../command-state\";\nimport { InstanceList, showInstance } from \"../shared/instance-list\";\nimport { deleteVariableMutable } from \"~/shared/data-variables\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport {\n  DeleteDataVariableDialog,\n  RenameDataVariableDialog,\n  $usedVariablesInInstances,\n} from \"~/builder/shared/data-variable-utils\";\nimport type { BaseOption } from \"../shared/types\";\nimport { formatUsageCount, getUsageSearchTerms } from \"../shared/usage-utils\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\n\nexport type DataVariableOption = BaseOption & {\n  type: \"dataVariable\";\n  id: string;\n  name: string;\n  instanceId: string;\n  usages: number;\n};\n\nexport const $dataVariableOptions = computed(\n  [$isCommandPanelOpen, $dataSources, $usedVariablesInInstances],\n  (isOpen, dataSources, usedInInstances) => {\n    const dataVariableOptions: DataVariableOption[] = [];\n    if (!isOpen) {\n      return dataVariableOptions;\n    }\n\n    for (const dataSource of dataSources.values()) {\n      if (\n        dataSource.type === \"variable\" &&\n        dataSource.scopeInstanceId !== undefined\n      ) {\n        const usages = usedInInstances.get(dataSource.id)?.size ?? 0;\n        dataVariableOptions.push({\n          terms: [\n            \"variable\",\n            \"variables\",\n            \"data\",\n            dataSource.name,\n            getInstanceLabel(dataSource.scopeInstanceId) ?? \"Unused\",\n            ...getUsageSearchTerms(usages),\n          ],\n          type: \"dataVariable\",\n          id: dataSource.id,\n          name: dataSource.name,\n          instanceId: dataSource.scopeInstanceId,\n          usages,\n        });\n      }\n    }\n\n    return dataVariableOptions;\n  }\n);\n\nconst DataVariableInstances = ({ variableId }: { variableId: string }) => {\n  const usedInInstances = $usedVariablesInInstances.get();\n  const instanceIds = usedInInstances.get(variableId) ?? new Set();\n\n  return (\n    <InstanceList\n      instanceIds={instanceIds}\n      onSelect={(instanceId) => {\n        showInstance(instanceId, \"settings\");\n        closeCommandPanel();\n      }}\n    />\n  );\n};\n\nexport const DataVariablesGroup = ({\n  options,\n}: {\n  options: DataVariableOption[];\n}) => {\n  const action = useSelectedAction();\n  const resetActionIndex = useResetActionIndex();\n  const [variableDialog, setVariableDialog] = useState<\n    (DataVariableOption & { action: \"rename\" | \"delete\" }) | undefined\n  >();\n\n  return (\n    <>\n      <CommandGroup\n        name=\"dataVariable\"\n        heading={\n          <CommandGroupHeading>\n            Data variables ({options.length})\n          </CommandGroupHeading>\n        }\n        actions={[\n          { name: \"select\", label: \"Select\" },\n          { name: \"findUsages\", label: \"Find usages\" },\n          { name: \"rename\", label: \"Rename\" },\n          { name: \"delete\", label: \"Delete\" },\n        ]}\n      >\n        {options.map((option) => {\n          return (\n            <CommandItem\n              keywords={option.terms}\n              key={option.id}\n              value={option.id}\n              onSelect={() => {\n                if (action?.name === \"select\") {\n                  showInstance(option.instanceId, \"settings\");\n                  closeCommandPanel();\n                }\n                if (action?.name === \"findUsages\") {\n                  $commandContent.set(\n                    <DataVariableInstances variableId={option.id} />\n                  );\n                }\n                if (action?.name === \"rename\") {\n                  setVariableDialog({ ...option, action: \"rename\" });\n                }\n                if (action?.name === \"delete\") {\n                  setVariableDialog({ ...option, action: \"delete\" });\n                }\n              }}\n            >\n              <Text>\n                {option.name}{\" \"}\n                <Text as=\"span\" color=\"moreSubtle\">\n                  {formatUsageCount(option.usages)}\n                </Text>\n              </Text>\n              <Text as=\"span\" color=\"moreSubtle\">\n                {getInstanceLabel(option.instanceId)}\n              </Text>\n            </CommandItem>\n          );\n        })}\n      </CommandGroup>\n      <RenameDataVariableDialog\n        variable={\n          variableDialog?.action === \"rename\" ? variableDialog : undefined\n        }\n        onClose={() => {\n          setVariableDialog(undefined);\n          resetActionIndex();\n          focusCommandPanel();\n        }}\n        onConfirm={(_variableId, newName) => {\n          toast.success(\n            `Variable renamed from \"${variableDialog?.name}\" to \"${newName}\"`\n          );\n          setVariableDialog(undefined);\n        }}\n      />\n      <DeleteDataVariableDialog\n        variable={\n          variableDialog?.action === \"delete\" ? variableDialog : undefined\n        }\n        onClose={() => {\n          setVariableDialog(undefined);\n          resetActionIndex();\n          focusCommandPanel();\n        }}\n        onConfirm={(variableId) => {\n          updateWebstudioData((data) => {\n            deleteVariableMutable(data, variableId);\n          });\n          toast.success(`Variable \"${variableDialog?.name}\" deleted`);\n          setVariableDialog(undefined);\n        }}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/duplicate-tokens-group.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { matchSorter } from \"match-sorter\";\nimport {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandFooter,\n  CommandItem,\n  CommandInput,\n  CommandList,\n  CommandBackButton,\n  Text,\n  Flex,\n  ScrollArea,\n  toast,\n  useSelectedAction,\n} from \"@webstudio-is/design-system\";\nimport type { Instance, StyleSource } from \"@webstudio-is/sdk\";\nimport {\n  $styleSources,\n  $styles,\n  $breakpoints,\n} from \"~/shared/sync/data-stores\";\nimport { $selectedStyleSources } from \"~/shared/nano-states\";\nimport { findDuplicateTokens } from \"~/shared/style-source-utils\";\nimport { $styleSourceUsages } from \"~/builder/shared/style-source-actions\";\nimport { InstanceList, showInstance } from \"../shared/instance-list\";\nimport {\n  $commandContent,\n  $commandContentKey,\n  closeCommandPanel,\n  openCommandPanel,\n} from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\nimport { formatUsageCount, getUsageSearchTerms } from \"../shared/usage-utils\";\n\nexport type DuplicateTokenOption = BaseOption & {\n  type: \"duplicateToken\";\n  token: Extract<StyleSource, { type: \"token\" }>;\n  duplicates: StyleSource[\"id\"][];\n  usages: number;\n};\n\nexport const $duplicateTokenOptions = computed(\n  [$styleSources, $styles, $breakpoints, $styleSourceUsages],\n  (styleSources, styles, breakpoints, styleSourceUsages) => {\n    const duplicateTokenOptions: DuplicateTokenOption[] = [];\n\n    const duplicatesMap = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    for (const [tokenId, duplicateIds] of duplicatesMap) {\n      const styleSource = styleSources.get(tokenId);\n      if (styleSource?.type !== \"token\") {\n        continue;\n      }\n      const usages = styleSourceUsages.get(tokenId)?.size ?? 0;\n      duplicateTokenOptions.push({\n        terms: [\n          \"duplicate tokens\",\n          \"duplicates\",\n          styleSource.name,\n          ...getUsageSearchTerms(usages),\n        ],\n        type: \"duplicateToken\",\n        token: styleSource,\n        duplicates: duplicateIds,\n        usages,\n      });\n    }\n    return duplicateTokenOptions;\n  }\n);\n\n/**\n * Show the duplicate tokens view in the command panel\n */\nexport const showDuplicateTokensView = () => {\n  const options = $duplicateTokenOptions.get();\n  if (options.length === 0) {\n    toast.info(\"No duplicate tokens found\");\n    return;\n  }\n  openCommandPanel();\n  $commandContent.set(<DuplicateTokensGroup options={options} />);\n};\n\nconst selectToken = (\n  instanceId: Instance[\"id\"],\n  tokenId: StyleSource[\"id\"]\n) => {\n  showInstance(instanceId, \"style\");\n  const selectedStyleSources = new Map($selectedStyleSources.get());\n  selectedStyleSources.set(instanceId, tokenId);\n  $selectedStyleSources.set(selectedStyleSources);\n};\n\nconst DuplicateTokensList = ({\n  duplicateIds,\n  tokenName,\n}: {\n  duplicateIds: StyleSource[\"id\"][];\n  tokenName: string;\n}) => {\n  const styleSources = useStore($styleSources);\n  const usages = useStore($styleSourceUsages);\n  const duplicateTokenOptions = $duplicateTokenOptions.get();\n  const action = useSelectedAction();\n  const [search, setSearch] = useState(\"\");\n\n  // Build options for search\n  const options = duplicateIds\n    .map((duplicateId) => {\n      const duplicate = styleSources.get(duplicateId);\n      if (duplicate?.type !== \"token\") {\n        return null;\n      }\n      const duplicateUsages = usages.get(duplicateId)?.size ?? 0;\n      return {\n        id: duplicateId,\n        token: duplicate,\n        usages: duplicateUsages,\n        terms: [duplicate.name, ...getUsageSearchTerms(duplicateUsages)],\n      };\n    })\n    .filter((opt): opt is NonNullable<typeof opt> => opt !== null);\n\n  let matches = options;\n  // prevent searching when value is empty to preserve original items order\n  if (search.trim().length > 0) {\n    for (const word of search.trim().split(/\\s+/)) {\n      matches = matchSorter(matches, word, {\n        keys: [\"terms\"],\n      });\n    }\n  }\n\n  const goBack = () => {\n    // Go back to duplicate tokens view\n    $commandContentKey.set($commandContentKey.get() + 1);\n    $commandContent.set(\n      <DuplicateTokensGroup options={duplicateTokenOptions} />\n    );\n  };\n\n  return (\n    <>\n      <CommandInput\n        placeholder=\"Search duplicates...\"\n        value={search}\n        onValueChange={(value) => setSearch(value)}\n        prefix={<CommandBackButton onClick={goBack} />}\n        onBack={goBack}\n      />\n      <Flex direction=\"column\" css={{ maxHeight: 300 }}>\n        <ScrollArea>\n          <CommandList key={tokenName}>\n            <CommandGroup\n              name=\"duplicates\"\n              heading={\n                <CommandGroupHeading>\n                  Duplicates of {tokenName}\n                </CommandGroupHeading>\n              }\n              actions={[\n                { name: \"show duplicates\", label: \"Show duplicates\" },\n                { name: \"show instances\", label: \"Show instances\" },\n              ]}\n            >\n              {matches.map(({ id, token, usages: duplicateUsages }) => (\n                <CommandItem\n                  key={id}\n                  value={id}\n                  onSelect={() => {\n                    if (action?.name === \"showDuplicates\" || !action) {\n                      // Show duplicates of this duplicate token\n                      const allDuplicates = duplicateTokenOptions.find(\n                        (opt) => opt.token.id === id\n                      )?.duplicates;\n                      if (allDuplicates) {\n                        $commandContentKey.set($commandContentKey.get() + 1);\n                        $commandContent.set(\n                          <DuplicateTokensList\n                            duplicateIds={allDuplicates}\n                            tokenName={token.name}\n                          />\n                        );\n                      }\n                    }\n                    if (\n                      action?.name === \"showInstances\" ||\n                      action?.name === \"findInstances\" ||\n                      action?.name === \"find\"\n                    ) {\n                      $commandContentKey.set($commandContentKey.get() + 1);\n                      $commandContent.set(<TokenInstances tokenId={id} />);\n                    }\n                  }}\n                >\n                  <Text>\n                    {token.name}{\" \"}\n                    <Text as=\"span\" color=\"moreSubtle\">\n                      {formatUsageCount(duplicateUsages)}\n                    </Text>\n                  </Text>\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </ScrollArea>\n      </Flex>\n      <CommandFooter />\n    </>\n  );\n};\n\nconst TokenInstances = ({ tokenId }: { tokenId: StyleSource[\"id\"] }) => {\n  const usages = useStore($styleSourceUsages);\n  const usedInInstanceIds = usages.get(tokenId) ?? new Set();\n  const duplicateTokenOptions = $duplicateTokenOptions.get();\n\n  return (\n    <InstanceList\n      instanceIds={usedInInstanceIds}\n      onSelect={(instanceId) => {\n        selectToken(instanceId, tokenId);\n        closeCommandPanel();\n      }}\n      onBack={() => {\n        // Go back to duplicate tokens view\n        $commandContentKey.set($commandContentKey.get() + 1);\n        $commandContent.set(\n          <DuplicateTokensGroup options={duplicateTokenOptions} />\n        );\n      }}\n    />\n  );\n};\n\nexport const DuplicateTokensGroup = ({\n  options,\n}: {\n  options: DuplicateTokenOption[];\n}) => {\n  const action = useSelectedAction();\n  const [search, setSearch] = useState(\"\");\n\n  let matches = options;\n  // prevent searching when value is empty to preserve original items order\n  if (search.trim().length > 0) {\n    for (const word of search.trim().split(/\\s+/)) {\n      matches = matchSorter(matches, word, {\n        keys: [\"terms\"],\n      });\n    }\n  }\n\n  const goBack = () => {\n    // Go back to main command panel\n    $commandContentKey.set($commandContentKey.get() + 1);\n    $commandContent.set(undefined);\n  };\n\n  return (\n    <>\n      <CommandInput\n        placeholder=\"Search duplicate tokens...\"\n        value={search}\n        onValueChange={(value) => setSearch(value)}\n        prefix={<CommandBackButton onClick={goBack} />}\n        onBack={goBack}\n      />\n      <Flex direction=\"column\" css={{ maxHeight: 300 }}>\n        <ScrollArea>\n          <CommandList key=\"duplicate-tokens-main\">\n            <CommandGroup\n              name=\"duplicateToken\"\n              heading={\n                <CommandGroupHeading>Duplicate tokens</CommandGroupHeading>\n              }\n              actions={[\n                { name: \"showDuplicates\", label: \"Show duplicates\" },\n                { name: \"showInstances\", label: \"Show instances\" },\n              ]}\n            >\n              {matches.map(({ token, duplicates, usages }) => (\n                <CommandItem\n                  key={token.id}\n                  // preserve selected state when rerender\n                  value={token.id}\n                  onSelect={() => {\n                    if (action?.name === \"showDuplicates\" || !action) {\n                      $commandContentKey.set($commandContentKey.get() + 1);\n                      $commandContent.set(\n                        <DuplicateTokensList\n                          duplicateIds={duplicates}\n                          tokenName={token.name}\n                        />\n                      );\n                    }\n                    if (\n                      action?.name === \"showInstances\" ||\n                      action?.name === \"findInstances\" ||\n                      action?.name === \"find\"\n                    ) {\n                      $commandContentKey.set($commandContentKey.get() + 1);\n                      $commandContent.set(\n                        <TokenInstances tokenId={token.id} />\n                      );\n                    }\n                  }}\n                >\n                  <Text variant=\"labels\">\n                    {token.name}{\" \"}\n                    <Text as=\"span\" color=\"moreSubtle\">\n                      {formatUsageCount(usages)} · {duplicates.length} duplicate\n                      {duplicates.length !== 1 ? \"s\" : \"\"}\n                    </Text>\n                  </Text>\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </ScrollArea>\n      </Flex>\n      <CommandFooter />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/index.ts",
    "content": "import { computed } from \"nanostores\";\nimport {\n  $componentOptions,\n  ComponentsGroup,\n  type ComponentOption,\n} from \"./components-group\";\nimport { $tagOptions, TagsGroup, type TagOption } from \"./tags-group\";\nimport {\n  $breakpointOptions,\n  BreakpointsGroup,\n  type BreakpointOption,\n} from \"./breakpoints-group\";\nimport { $pageOptions, PagesGroup, type PageOption } from \"./pages-group\";\nimport {\n  $commandOptions,\n  CommandsGroup,\n  type CommandOption,\n} from \"./commands-group\";\nimport { $tokenOptions, TokensGroup, type TokenOption } from \"./tokens-group\";\nimport {\n  DuplicateTokensGroup,\n  type DuplicateTokenOption,\n} from \"./duplicate-tokens-group\";\nimport {\n  $dataVariableOptions,\n  DataVariablesGroup,\n  type DataVariableOption,\n} from \"./data-variables-group\";\nimport {\n  $cssVariableOptions,\n  CssVariablesGroup,\n  type CssVariableOption,\n} from \"./css-variables-group\";\nimport {\n  $instanceOptions,\n  InstancesGroup,\n  type InstanceOption,\n} from \"./instances-group\";\n\nexport type Option =\n  | ComponentOption\n  | TagOption\n  | BreakpointOption\n  | PageOption\n  | CommandOption\n  | TokenOption\n  | DuplicateTokenOption\n  | DataVariableOption\n  | CssVariableOption\n  | InstanceOption;\n\nexport type OptionByType<T extends Option[\"type\"]> = Extract<\n  Option,\n  { type: T }\n>;\n\nexport const $allOptions = computed(\n  [\n    $componentOptions,\n    $tagOptions,\n    $breakpointOptions,\n    $pageOptions,\n    $commandOptions,\n    $tokenOptions,\n    $dataVariableOptions,\n    $cssVariableOptions,\n    $instanceOptions,\n  ],\n  (\n    componentOptions,\n    tagOptions,\n    breakpointOptions,\n    pageOptions,\n    commandOptions,\n    tokenOptions,\n    dataVariableOptions,\n    cssVariableOptions,\n    instanceOptions\n  ) => [\n    ...commandOptions,\n    ...componentOptions,\n    ...pageOptions,\n    ...breakpointOptions,\n    ...tagOptions,\n    ...tokenOptions,\n    ...dataVariableOptions,\n    ...cssVariableOptions,\n    ...instanceOptions,\n  ]\n);\n\ntype GroupComponent<T extends Option[\"type\"]> = (props: {\n  options: OptionByType<T>[];\n}) => JSX.Element;\n\nexport const groups: {\n  [K in Option[\"type\"]]: GroupComponent<K>;\n} = {\n  component: ComponentsGroup,\n  tag: TagsGroup,\n  breakpoint: BreakpointsGroup,\n  page: PagesGroup,\n  command: CommandsGroup,\n  token: TokensGroup,\n  duplicateToken: DuplicateTokensGroup,\n  dataVariable: DataVariablesGroup,\n  cssVariable: CssVariablesGroup,\n  instance: InstancesGroup,\n};\n\nexport type {\n  ComponentOption,\n  TagOption,\n  BreakpointOption,\n  PageOption,\n  CommandOption,\n  TokenOption,\n  DuplicateTokenOption,\n  DataVariableOption,\n  CssVariableOption,\n  InstanceOption,\n};\n\nexport {\n  ComponentsGroup,\n  TagsGroup,\n  BreakpointsGroup,\n  PagesGroup,\n  CommandsGroup,\n  TokensGroup,\n  DuplicateTokensGroup,\n  DataVariablesGroup,\n  CssVariablesGroup,\n  InstancesGroup,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/instances-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  useCommandState,\n  useSetFooterContent,\n} from \"@webstudio-is/design-system\";\nimport { computed } from \"nanostores\";\nimport { parseComponentName } from \"@webstudio-is/sdk\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { $instances, $pages } from \"~/shared/sync/data-stores\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\nimport { $awareness, findAwarenessByInstanceId } from \"~/shared/awareness\";\nimport { closeCommandPanel, $isCommandPanelOpen } from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\nimport { setActiveSidebarPanel } from \"~/builder/shared/nano-states\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { useEffect } from \"react\";\nimport { InstancePathFooter } from \"../shared/instance-path-footer\";\n\nexport type InstanceOption = BaseOption & {\n  type: \"instance\";\n  instance: Instance;\n};\n\nexport const $instanceOptions = computed(\n  [$isCommandPanelOpen, $instances, $pages],\n  (isOpen, instances, pages) => {\n    if (!isOpen || !pages) {\n      return [];\n    }\n    const instanceOptions: InstanceOption[] = [];\n    for (const instance of instances.values()) {\n      const label = getInstanceLabel(instance);\n      // Include instance label and component name in search terms\n      instanceOptions.push({\n        terms: [\"instances\", label, instance.component],\n        type: \"instance\",\n        instance,\n      });\n    }\n    return instanceOptions;\n  }\n);\n\nexport const InstancesGroup = ({ options }: { options: InstanceOption[] }) => {\n  const highlightedValue = useCommandState((state) => state.value);\n  const setFooterContent = useSetFooterContent();\n\n  useEffect(() => {\n    setFooterContent(\n      highlightedValue ? (\n        <InstancePathFooter instanceId={highlightedValue} />\n      ) : undefined\n    );\n  }, [highlightedValue, setFooterContent]);\n\n  return (\n    <CommandGroup\n      name=\"instance\"\n      heading={\n        <CommandGroupHeading>Instances ({options.length})</CommandGroupHeading>\n      }\n      actions={[{ name: \"select\", label: \"Select\" }]}\n    >\n      {options.map(({ instance }) => {\n        const label = getInstanceLabel(instance);\n        const [_namespace, componentName] = parseComponentName(\n          instance.component\n        );\n        const componentLabel = humanizeString(componentName);\n        return (\n          <CommandItem\n            key={instance.id}\n            value={instance.id}\n            onSelect={() => {\n              closeCommandPanel();\n              const pages = $pages.get();\n              const instances = $instances.get();\n              if (pages && instances) {\n                const awareness = findAwarenessByInstanceId(\n                  pages,\n                  instances,\n                  instance.id\n                );\n                if (awareness) {\n                  $awareness.set(awareness);\n                  setActiveSidebarPanel(\"auto\");\n                }\n              }\n            }}\n          >\n            <Text>{label}</Text>\n            <Text color=\"moreSubtle\" truncate css={{ maxWidth: \"30ch\" }}>\n              {componentLabel}\n            </Text>\n          </CommandItem>\n        );\n      })}\n    </CommandGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/pages-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  useSelectedAction,\n} from \"@webstudio-is/design-system\";\nimport { computed } from \"nanostores\";\nimport type { Page } from \"@webstudio-is/sdk\";\nimport { $pages } from \"~/shared/sync/data-stores\";\nimport { $editingPageId } from \"~/shared/nano-states\";\nimport { $selectedPage, selectPage } from \"~/shared/awareness\";\nimport { setActiveSidebarPanel } from \"~/builder/shared/nano-states\";\nimport { closeCommandPanel, $isCommandPanelOpen } from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\n\nexport type PageOption = BaseOption & {\n  type: \"page\";\n  page: Page;\n};\n\nexport const $pageOptions = computed(\n  [$isCommandPanelOpen, $pages, $selectedPage],\n  (isOpen, pages, selectedPage) => {\n    if (!isOpen) {\n      return [];\n    }\n    const pageOptions: PageOption[] = [];\n    if (pages) {\n      for (const page of [pages.homePage, ...pages.pages]) {\n        if (page.id === selectedPage?.id) {\n          continue;\n        }\n        pageOptions.push({\n          terms: [\"pages\", page.name],\n          type: \"page\",\n          page,\n        });\n      }\n    }\n    return pageOptions;\n  }\n);\n\nexport const PagesGroup = ({ options }: { options: PageOption[] }) => {\n  const action = useSelectedAction();\n  return (\n    <CommandGroup\n      name=\"page\"\n      heading={\n        <CommandGroupHeading>Pages ({options.length})</CommandGroupHeading>\n      }\n      actions={[\n        { name: \"select\", label: \"Select\" },\n        { name: \"settings\", label: \"Settings\" },\n      ]}\n    >\n      {options.map(({ page }) => (\n        <CommandItem\n          key={page.id}\n          // preserve selected state when rerender\n          value={page.id}\n          onSelect={() => {\n            closeCommandPanel();\n            if (action?.name === \"select\") {\n              selectPage(page.id);\n              setActiveSidebarPanel(\"auto\");\n              $editingPageId.set(undefined);\n            }\n            if (action?.name === \"settings\") {\n              selectPage(page.id);\n              setActiveSidebarPanel(\"pages\");\n              $editingPageId.set(page.id);\n            }\n          }}\n        >\n          <Text>{page.name}</Text>\n        </CommandItem>\n      ))}\n    </CommandGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/tags-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  CommandIcon,\n  Flex,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { computed } from \"nanostores\";\nimport { elementComponent, tags } from \"@webstudio-is/sdk\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport {\n  $instances,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { insertWebstudioFragmentAt } from \"~/shared/instance-utils\";\nimport { $selectedInstancePath } from \"~/shared/awareness\";\nimport { InstanceIcon } from \"~/builder/shared/instance-label\";\nimport { isTreeSatisfyingContentModel } from \"~/shared/content-model\";\nimport { closeCommandPanel, $isCommandPanelOpen } from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\n\nexport type TagOption = BaseOption & {\n  type: \"tag\";\n  tag: string;\n};\n\nexport const $tagOptions = computed(\n  [\n    $isCommandPanelOpen,\n    $selectedInstancePath,\n    $instances,\n    $props,\n    $registeredComponentMetas,\n  ],\n  (isOpen, instancePath, instances, props, metas) => {\n    const tagOptions: TagOption[] = [];\n    if (!isOpen) {\n      return tagOptions;\n    }\n    if (instancePath === undefined) {\n      return tagOptions;\n    }\n    const [{ instance, instanceSelector }] = instancePath;\n    const childInstance: Instance = {\n      type: \"instance\",\n      id: \"new_instance\",\n      component: elementComponent,\n      children: [],\n    };\n    const newInstances = new Map(instances);\n    newInstances.set(childInstance.id, childInstance);\n    newInstances.set(instance.id, {\n      ...instance,\n      // avoid preserving original children to not invalidate tag\n      // when some descendants do not satisfy content model\n      children: [{ type: \"id\", value: childInstance.id }],\n    });\n    for (const tag of tags) {\n      childInstance.tag = tag;\n      const isSatisfying = isTreeSatisfyingContentModel({\n        instances: newInstances,\n        props,\n        metas,\n        instanceSelector,\n      });\n      if (isSatisfying) {\n        tagOptions.push({\n          terms: [\"tags\", tag, `<${tag}>`],\n          type: \"tag\",\n          tag,\n        });\n      }\n    }\n    return tagOptions;\n  }\n);\n\nexport const TagsGroup = ({ options }: { options: TagOption[] }) => {\n  return (\n    <CommandGroup\n      name=\"tag\"\n      heading={\n        <CommandGroupHeading>Tags ({options.length})</CommandGroupHeading>\n      }\n      actions={[{ name: \"add\", label: \"Add\" }]}\n    >\n      {options.map(({ tag }) => {\n        return (\n          <CommandItem\n            key={tag}\n            // preserve selected state when rerender\n            value={tag}\n            onSelect={() => {\n              closeCommandPanel();\n              const newInstance: Instance = {\n                type: \"instance\",\n                id: \"new_instance\",\n                component: elementComponent,\n                tag,\n                children: [],\n              };\n              insertWebstudioFragmentAt({\n                children: [{ type: \"id\", value: newInstance.id }],\n                instances: [newInstance],\n                props: [],\n                dataSources: [],\n                styleSourceSelections: [],\n                styleSources: [],\n                styles: [],\n                breakpoints: [],\n                assets: [],\n                resources: [],\n              });\n            }}\n          >\n            <Flex gap={2}>\n              <CommandIcon>\n                <InstanceIcon instance={{ component: elementComponent, tag }} />\n              </CommandIcon>\n              <Text>{`<${tag}>`}</Text>\n            </Flex>\n          </CommandItem>\n        );\n      })}\n    </CommandGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/tokens-group.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  CommandGroup,\n  CommandGroupHeading,\n  CommandItem,\n  Text,\n  toast,\n  useSelectedAction,\n  useResetActionIndex,\n} from \"@webstudio-is/design-system\";\nimport type { Instance, StyleSource } from \"@webstudio-is/sdk\";\nimport { $styleSources } from \"~/shared/sync/data-stores\";\nimport { $selectedStyleSources } from \"~/shared/nano-states\";\nimport {\n  deleteStyleSource,\n  DeleteStyleSourceDialog,\n  RenameStyleSourceDialog,\n  $styleSourceUsages,\n} from \"~/builder/shared/style-source-actions\";\nimport { InstanceList, showInstance } from \"../shared/instance-list\";\nimport {\n  $commandContent,\n  $isCommandPanelOpen,\n  closeCommandPanel,\n  focusCommandPanel,\n} from \"../command-state\";\nimport type { BaseOption } from \"../shared/types\";\nimport { formatUsageCount, getUsageSearchTerms } from \"../shared/usage-utils\";\n\nexport type TokenOption = BaseOption & {\n  type: \"token\";\n  token: Extract<StyleSource, { type: \"token\" }>;\n  usages: number;\n};\n\nexport const $tokenOptions = computed(\n  [$isCommandPanelOpen, $styleSources, $styleSourceUsages],\n  (isOpen, styleSources, styleSourceUsages) => {\n    const tokenOptions: TokenOption[] = [];\n    if (!isOpen) {\n      return tokenOptions;\n    }\n    for (const styleSource of styleSources.values()) {\n      if (styleSource.type !== \"token\") {\n        continue;\n      }\n      const usages = styleSourceUsages.get(styleSource.id)?.size ?? 0;\n      tokenOptions.push({\n        terms: [\"tokens\", styleSource.name, ...getUsageSearchTerms(usages)],\n        type: \"token\",\n        token: styleSource,\n        usages,\n      });\n    }\n    return tokenOptions;\n  }\n);\n\nconst selectToken = (\n  instanceId: Instance[\"id\"],\n  tokenId: StyleSource[\"id\"]\n) => {\n  showInstance(instanceId, \"style\");\n  const selectedStyleSources = new Map($selectedStyleSources.get());\n  selectedStyleSources.set(instanceId, tokenId);\n  $selectedStyleSources.set(selectedStyleSources);\n};\n\nconst TokenInstances = ({ tokenId }: { tokenId: StyleSource[\"id\"] }) => {\n  const usages = useStore($styleSourceUsages);\n  const usedInInstanceIds = usages.get(tokenId) ?? new Set();\n\n  return (\n    <InstanceList\n      instanceIds={usedInInstanceIds}\n      onSelect={(instanceId) => {\n        selectToken(instanceId, tokenId);\n        closeCommandPanel();\n      }}\n    />\n  );\n};\n\nexport const TokensGroup = ({ options }: { options: TokenOption[] }) => {\n  const action = useSelectedAction();\n  const resetActionIndex = useResetActionIndex();\n  const [tokenDialog, setTokenDialog] = useState<\n    | (Extract<StyleSource, { type: \"token\" }> & {\n        action: \"rename\" | \"delete\";\n      })\n    | undefined\n  >();\n\n  return (\n    <>\n      <CommandGroup\n        name=\"token\"\n        heading={\n          <CommandGroupHeading>Tokens ({options.length})</CommandGroupHeading>\n        }\n        actions={[\n          { name: \"showInstances\", label: \"Show instances\" },\n          { name: \"rename\", label: \"Rename\" },\n          { name: \"delete\", label: \"Delete\" },\n        ]}\n      >\n        {options.map(({ token, usages }) => (\n          <CommandItem\n            key={token.id}\n            // preserve selected state when rerender\n            value={token.id}\n            onSelect={() => {\n              if (\n                action?.name === \"showInstances\" ||\n                action?.name === \"findInstances\" ||\n                action?.name === \"find\"\n              ) {\n                $commandContent.set(<TokenInstances tokenId={token.id} />);\n              }\n              if (action?.name === \"rename\") {\n                setTokenDialog({ ...token, action: \"rename\" });\n              }\n              if (action?.name === \"delete\") {\n                setTokenDialog({ ...token, action: \"delete\" });\n              }\n            }}\n          >\n            <Text>\n              {token.name}{\" \"}\n              <Text as=\"span\" color=\"moreSubtle\">\n                {formatUsageCount(usages)}\n              </Text>\n            </Text>\n          </CommandItem>\n        ))}\n      </CommandGroup>\n      <RenameStyleSourceDialog\n        styleSource={tokenDialog?.action === \"rename\" ? tokenDialog : undefined}\n        onClose={() => {\n          setTokenDialog(undefined);\n          resetActionIndex();\n          focusCommandPanel();\n        }}\n        onConfirm={(_styleSourceId, newName) => {\n          toast.success(\n            `Token renamed from \"${tokenDialog?.name}\" to \"${newName}\"`\n          );\n          setTokenDialog(undefined);\n        }}\n      />\n      <DeleteStyleSourceDialog\n        styleSource={tokenDialog?.action === \"delete\" ? tokenDialog : undefined}\n        onClose={() => {\n          setTokenDialog(undefined);\n          resetActionIndex();\n          focusCommandPanel();\n        }}\n        onConfirm={(styleSourceId) => {\n          deleteStyleSource(styleSourceId);\n          toast.success(`Token \"${tokenDialog?.name}\" deleted`);\n          setTokenDialog(undefined);\n        }}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/wrap-group.test.tsx",
    "content": "import { describe, expect, test, beforeEach } from \"vitest\";\nimport { coreMetas, elementComponent } from \"@webstudio-is/sdk\";\nimport * as baseMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport * as animationMetas from \"@webstudio-is/sdk-components-animation/metas\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $, renderData } from \"@webstudio-is/template\";\nimport {\n  $instances,\n  $pages,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { $awareness, selectInstance } from \"~/shared/awareness\";\nimport { __testing__ } from \"./wrap-group\";\n\nconst { canWrapInstance } = __testing__;\n\nregisterContainers();\n\nconst metas = new Map(\n  Object.entries({ ...coreMetas, ...baseMetas, ...animationMetas })\n);\n\nbeforeEach(() => {\n  $registeredComponentMetas.set(metas);\n  $pages.set(createDefaultPages({ rootInstanceId: \"\" }));\n  $awareness.set({ pageId: \"\" });\n});\n\ndescribe(\"canWrapInstance for components\", () => {\n  test(\"should allow wrapping text in a Link\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Text ws:id=\"text\">Hello</$.Text>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"text\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"text\",\n      [\"text\", \"body\"],\n      \"body\",\n      \"Link\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping text in a Heading\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Text ws:id=\"text\">Hello</$.Text>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"text\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"text\",\n      [\"text\", \"body\"],\n      \"body\",\n      \"Heading\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping box in a Form\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"Form\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should reject invalid wrapping (text in CodeText)\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    // CodeText only accepts text content, not boxes\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"CodeText\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(false);\n  });\n});\n\ndescribe(\"canWrapInstance for HTML elements\", () => {\n  test(\"should allow wrapping text in a span\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Text ws:id=\"text\">Hello</$.Text>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"text\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"text\",\n      [\"text\", \"body\"],\n      \"body\",\n      elementComponent,\n      \"span\",\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping box in a div\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      elementComponent,\n      \"div\",\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping box in span (content model allows it)\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    // Note: The content model currently allows this even though\n    // it's not valid phrasing content in strict HTML\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      elementComponent,\n      \"span\",\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping list item in ul\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.ListItem ws:id=\"li\">Item</$.ListItem>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"li\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"li\",\n      [\"li\", \"body\"],\n      \"body\",\n      elementComponent,\n      \"ul\",\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should reject wrapping box in ul (ul requires li children)\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    // ul can only contain li elements\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      elementComponent,\n      \"ul\",\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(false);\n  });\n});\n\ndescribe(\"canWrapInstance edge cases\", () => {\n  test(\"should handle wrapping with Slot\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"Slot\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping with Collection\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"ws:collection\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping with AnimateChildren\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"@webstudio-is/sdk-components-animation:AnimateChildren\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping with AnimateText\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"@webstudio-is/sdk-components-animation:AnimateText\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping with StaggerAnimation\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"@webstudio-is/sdk-components-animation:StaggerAnimation\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should allow wrapping with VideoAnimation\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"box\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"body\",\n      \"@webstudio-is/sdk-components-animation:VideoAnimation\",\n      undefined,\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n\n  test(\"should handle wrapping multiple elements\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"parent\">\n            <$.Box ws:id=\"child1\"></$.Box>\n            <$.Box ws:id=\"child2\"></$.Box>\n          </$.Box>\n        </$.Body>\n      ).instances\n    );\n    selectInstance([\"child1\", \"parent\", \"body\"]);\n\n    const result = canWrapInstance(\n      \"child1\",\n      [\"child1\", \"parent\", \"body\"],\n      \"parent\",\n      elementComponent,\n      \"div\",\n      $instances.get(),\n      $props.get(),\n      $registeredComponentMetas.get()\n    );\n    expect(result).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/groups/wrap-group.tsx",
    "content": "import {\n  CommandGroup,\n  CommandIcon,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandBackButton,\n  CommandFooter,\n  Flex,\n  ScrollArea,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { matchSorter } from \"match-sorter\";\nimport { computed } from \"nanostores\";\nimport { elementComponent, tags } from \"@webstudio-is/sdk\";\nimport type {\n  Instance,\n  Instances,\n  Props,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport {\n  $instances,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { $selectedInstancePath } from \"~/shared/awareness\";\nimport {\n  getInstanceLabel,\n  InstanceIcon,\n} from \"~/builder/shared/instance-label\";\nimport { isTreeSatisfyingContentModel } from \"~/shared/content-model\";\nimport {\n  $commandContent,\n  $isCommandPanelOpen,\n  closeCommandPanel,\n  openCommandPanel,\n} from \"../command-state\";\nimport { useState } from \"react\";\nimport { wrapInstance } from \"~/shared/instance-utils\";\n\ntype WrapOption = {\n  component: string;\n  tag?: string;\n  label: string;\n  category?: string;\n  order?: number;\n};\n\n// Component names we want to allow as wrappers\n// These will be looked up in the registered metas to get the full namespaced name\nconst wrapperComponentNames = [\n  \"Element\",\n  \"Slot\",\n  \"Collection\",\n  \"AnimateChildren\",\n  \"AnimateText\",\n  \"StaggerAnimation\",\n  \"VideoAnimation\",\n  \"Form\",\n];\n\n// Check if an instance can be wrapped with a specific component or tag\nconst canWrapInstance = (\n  selectedInstanceId: string,\n  selectedInstanceSelector: string[],\n  parentInstanceId: string,\n  component: string,\n  tag: string | undefined,\n  instances: Instances,\n  props: Props,\n  metas: Map<Instance[\"component\"], WsComponentMeta>\n): boolean => {\n  const selectedInstance = instances.get(selectedInstanceId);\n  const parentInstance = instances.get(parentInstanceId);\n\n  if (!selectedInstance || !parentInstance) {\n    return false;\n  }\n\n  const wrapperInstance: Instance = {\n    type: \"instance\",\n    id: \"wrapper_instance\",\n    component,\n    children: [{ type: \"id\", value: selectedInstanceId }],\n  };\n\n  if (tag || component === elementComponent) {\n    wrapperInstance.tag = tag ?? \"div\";\n  } else {\n    // For components with presetStyle (like Heading, Box), infer default tag\n    const meta = metas.get(component);\n    const defaultTag = Object.keys(\n      (meta as { presetStyle?: Record<string, unknown> })?.presetStyle ?? {}\n    ).at(0);\n    if (defaultTag) {\n      wrapperInstance.tag = defaultTag;\n    }\n  }\n\n  const newInstances = new Map(instances);\n  newInstances.set(wrapperInstance.id, wrapperInstance);\n\n  // Update parent to point to wrapper\n  const newParentInstance = { ...parentInstance };\n  newParentInstance.children = parentInstance.children.map((child) => {\n    if (child.type === \"id\" && child.value === selectedInstanceId) {\n      return { type: \"id\", value: wrapperInstance.id };\n    }\n    return child;\n  });\n  newInstances.set(parentInstance.id, newParentInstance);\n\n  // Validate the wrapper in the parent\n  const wrapperValid = isTreeSatisfyingContentModel({\n    instances: newInstances,\n    props,\n    metas,\n    instanceSelector: [\n      wrapperInstance.id,\n      ...selectedInstanceSelector.slice(1),\n    ],\n  });\n\n  if (!wrapperValid) {\n    return false;\n  }\n\n  // Validate the selected instance inside the wrapper\n  const childValid = isTreeSatisfyingContentModel({\n    instances: newInstances,\n    props,\n    metas,\n    instanceSelector: [\n      selectedInstanceId,\n      wrapperInstance.id,\n      ...selectedInstanceSelector.slice(1),\n    ],\n  });\n\n  return childValid;\n};\n\nconst $wrapOptions = computed(\n  [\n    $isCommandPanelOpen,\n    $selectedInstancePath,\n    $instances,\n    $props,\n    $registeredComponentMetas,\n  ],\n  (isOpen, instancePath, instances, props, metas) => {\n    const wrapOptions: WrapOption[] = [];\n    if (!isOpen) {\n      return wrapOptions;\n    }\n    if (instancePath === undefined || instancePath.length === 1) {\n      return wrapOptions;\n    }\n    const [selectedItem, parentItem] = instancePath;\n\n    // Build list of allowed wrappers from registered metas\n    const allowedComponents: string[] = [];\n    for (const [componentName, meta] of metas) {\n      // Check if this component is in our wrapper list by:\n      // 1. Exact component name match\n      // 2. Label match\n      // 3. Ends with wrapper name after namespace (e.g., \"namespace:WrapperName\")\n      const matchesName = wrapperComponentNames.includes(componentName);\n      const matchesLabel =\n        meta.label && wrapperComponentNames.includes(meta.label);\n      const matchesNamespacedName = wrapperComponentNames.some((wrapperName) =>\n        componentName.endsWith(`:${wrapperName}`)\n      );\n\n      if (matchesName || matchesLabel || matchesNamespacedName) {\n        allowedComponents.push(componentName);\n      }\n    }\n\n    // Test each allowed component\n    for (const component of allowedComponents) {\n      if (\n        canWrapInstance(\n          selectedItem.instance.id,\n          selectedItem.instanceSelector,\n          parentItem.instance.id,\n          component,\n          undefined,\n          instances,\n          props,\n          metas\n        )\n      ) {\n        const meta = metas.get(component);\n        const label = getInstanceLabel({ component });\n        wrapOptions.push({\n          component,\n          label,\n          category: meta?.category,\n          order: meta?.order,\n        });\n      }\n    }\n\n    // Test all valid HTML tags\n    for (const tag of tags) {\n      if (\n        canWrapInstance(\n          selectedItem.instance.id,\n          selectedItem.instanceSelector,\n          parentItem.instance.id,\n          elementComponent,\n          tag,\n          instances,\n          props,\n          metas\n        )\n      ) {\n        const label = getInstanceLabel({ component: elementComponent, tag });\n        wrapOptions.push({\n          component: elementComponent,\n          tag,\n          label,\n        });\n      }\n    }\n\n    return wrapOptions;\n  }\n);\n\nconst WrapComponentsList = () => {\n  const [search, setSearch] = useState(\"\");\n  const wrapOptions = $wrapOptions.get();\n\n  let matches = wrapOptions;\n  if (search.trim().length > 0) {\n    for (const word of search.trim().split(/\\s+/)) {\n      matches = matchSorter(matches, word, {\n        keys: [\"label\"],\n      });\n    }\n  }\n\n  const goBack = () => {\n    $commandContent.set(undefined);\n  };\n\n  return (\n    <>\n      <CommandInput\n        action={{ name: \"wrap\", label: \"Wrap\" }}\n        placeholder=\"Search components to wrap with...\"\n        value={search}\n        onValueChange={setSearch}\n        prefix={<CommandBackButton onClick={goBack} />}\n        onBack={goBack}\n      />\n      <Flex direction=\"column\" css={{ maxHeight: 300 }}>\n        <ScrollArea>\n          <CommandList>\n            {matches.length === 0 ? (\n              <Flex justify=\"center\" align=\"center\" css={{ minHeight: 100 }}>\n                <Text color=\"subtle\" align=\"center\">\n                  No components found that are allowed to wrap this instance\n                </Text>\n              </Flex>\n            ) : (\n              <CommandGroup\n                name=\"wrap-components\"\n                actions={[{ name: \"wrap\", label: \"Wrap\" }]}\n              >\n                {matches.map(({ component, tag, label }) => {\n                  const key = tag ? `${component}:${tag}` : component;\n                  return (\n                    <CommandItem\n                      key={key}\n                      value={key}\n                      onSelect={() => {\n                        wrapInstance(component, tag);\n                        closeCommandPanel();\n                      }}\n                    >\n                      <Flex gap={2}>\n                        <CommandIcon>\n                          <InstanceIcon instance={{ component, tag }} />\n                        </CommandIcon>\n                        <Text>{label}</Text>\n                      </Flex>\n                    </CommandItem>\n                  );\n                })}\n              </CommandGroup>\n            )}\n          </CommandList>\n        </ScrollArea>\n      </Flex>\n      <CommandFooter />\n    </>\n  );\n};\n\nexport const showWrapComponentsList = () => {\n  openCommandPanel();\n  $commandContent.set(<WrapComponentsList />);\n};\n\nexport const __testing__ = {\n  canWrapInstance,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/index.ts",
    "content": "export { openCommandPanel, $commandSearch } from \"./command-state\";\nexport * from \"./command-panel\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/shared/auto-select.ts",
    "content": "import { useEffect, useRef } from \"react\";\n\n/**\n * Auto-selects the first item in a command list when search changes.\n * Returns a ref that should be attached to the CommandList element.\n *\n * Note: This implementation uses keyboard event simulation as a workaround because cmdk\n * doesn't expose an imperative API for programmatically highlighting items.\n * The library is designed to work through keyboard/mouse interactions only.\n */\nexport const useAutoSelectFirstItem = (search: string) => {\n  const listRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!listRef.current) {\n      return;\n    }\n\n    requestAnimationFrame(() => {\n      const root = listRef.current?.closest(\"[cmdk-root]\") as HTMLElement;\n      if (!root) {\n        return;\n      }\n\n      // Workaround: Simulate keyboard navigation to select the first item\n      // First press End to go to the last item (ensures we're in a known position)\n      const endEvent = new KeyboardEvent(\"keydown\", {\n        key: \"End\",\n        bubbles: true,\n      });\n      root.dispatchEvent(endEvent);\n\n      // Then press Home to go to the first item\n      const homeEvent = new KeyboardEvent(\"keydown\", {\n        key: \"Home\",\n        bubbles: true,\n      });\n      root.dispatchEvent(homeEvent);\n    });\n  }, [search]);\n\n  return listRef;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/shared/component-utils.ts",
    "content": "import { componentCategories } from \"@webstudio-is/sdk\";\n\n/**\n * Normalizes a component's category, defaulting undefined to \"hidden\"\n */\nexport const normalizeCategory = (\n  category: string | undefined\n): (typeof componentCategories)[number] => {\n  return (category ?? \"hidden\") as (typeof componentCategories)[number];\n};\n\n/**\n * Checks if a component should be filtered out based on its category\n */\nexport const shouldFilterCategory = (category: string | undefined): boolean => {\n  const normalized = normalizeCategory(category);\n  return normalized === \"hidden\" || normalized === \"internal\";\n};\n\ntype ComponentMetaLike = {\n  category?: string;\n  order?: number;\n};\n\n/**\n * Calculates a sort score for a component based on category and order.\n * Used to sort components in the same way across the application.\n */\nexport const getComponentScore = (meta: ComponentMetaLike): number => {\n  const category = normalizeCategory(meta.category);\n  const categoryScore = componentCategories.indexOf(category);\n  const componentScore = meta.order ?? Number.MAX_SAFE_INTEGER;\n  // Shift category score to ensure category takes precedence over order\n  return categoryScore * 1000 + componentScore;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/shared/instance-list.tsx",
    "content": "import { useState } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport {\n  CommandGroup,\n  CommandFooter,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandBackButton,\n  Flex,\n  ScrollArea,\n  Text,\n  useSelectedAction,\n  useCommandState,\n} from \"@webstudio-is/design-system\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { $instances, $pages } from \"~/shared/sync/data-stores\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\nimport { buildInstancePath } from \"~/shared/instance-utils\";\nimport { $commandContent } from \"~/builder/features/command-panel/command-state\";\nimport { findAwarenessByInstanceId } from \"~/shared/awareness\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { $activeInspectorPanel } from \"~/builder/shared/nano-states\";\nimport { useAutoSelectFirstItem } from \"./auto-select\";\nimport { InstancePathFooter } from \"./instance-path-footer\";\n\nexport type InstanceOption = {\n  label: string;\n  id: string;\n  path: string[];\n  pageName: string;\n};\n\ntype InstanceListProps = {\n  instanceIds: Set<Instance[\"id\"]>;\n  onSelect: (instanceId: Instance[\"id\"]) => void;\n  onBack?: () => void;\n};\n\nexport const InstanceList = ({\n  instanceIds,\n  onSelect,\n  onBack,\n}: InstanceListProps) => {\n  const instances = $instances.get();\n  const pages = $pages.get();\n  const action = useSelectedAction();\n  const highlightedValue = useCommandState((state) => state.value);\n  const usedInInstances: InstanceOption[] = [];\n  for (const instanceId of instanceIds) {\n    const instance = instances.get(instanceId);\n    if (!instance || !pages) {\n      continue;\n    }\n    const path = buildInstancePath(instanceId, pages, instances);\n    const awareness = findAwarenessByInstanceId(pages, instances, instanceId);\n    const page = pages.pages.find((p) => p.id === awareness.pageId);\n    usedInInstances.push({\n      label: getInstanceLabel(instance),\n      id: instance.id,\n      path,\n      pageName: page?.name ?? \"\",\n    });\n  }\n  const [search, setSearch] = useState(\"\");\n\n  const listRef = useAutoSelectFirstItem(search);\n\n  const goBack = () => {\n    if (onBack) {\n      onBack();\n    } else {\n      $commandContent.set(undefined);\n    }\n  };\n\n  let matches = usedInInstances;\n  // prevent searching when value is empty\n  // to preserve original items order\n  if (search.trim().length > 0) {\n    for (const word of search.trim().split(/\\s+/)) {\n      matches = matchSorter(matches, word, {\n        keys: [\"label\"],\n      });\n    }\n  }\n\n  const selectedInstance = usedInInstances.find(\n    (instance) => instance.id === highlightedValue\n  );\n\n  return (\n    <>\n      <CommandInput\n        action={{ name: \"select\", label: \"Select\" }}\n        placeholder=\"Search instances...\"\n        value={search}\n        onValueChange={setSearch}\n        prefix={<CommandBackButton onClick={goBack} />}\n        onBack={goBack}\n      />\n      <Flex direction=\"column\" css={{ maxHeight: 300 }}>\n        <ScrollArea>\n          <CommandList ref={listRef}>\n            <CommandGroup\n              name=\"instance\"\n              actions={[\n                { name: \"select\", label: \"Select\" },\n                { name: \"settings\", label: \"Settings\" },\n              ]}\n            >\n              {matches.length === 0 ? (\n                <Flex justify=\"center\" align=\"center\" css={{ minHeight: 100 }}>\n                  <Text color=\"subtle\">No instances found</Text>\n                </Flex>\n              ) : (\n                matches.map(({ id, label, pageName }) => (\n                  <CommandItem\n                    key={id}\n                    value={id}\n                    onSelect={() => {\n                      if (action?.name === \"select\" || !action) {\n                        onSelect(id);\n                      }\n                      if (action?.name === \"settings\") {\n                        showInstance(id, \"settings\");\n                      }\n                    }}\n                  >\n                    <Text>{label}</Text>\n                    <Text color=\"moreSubtle\">{pageName}</Text>\n                  </CommandItem>\n                ))\n              )}\n            </CommandGroup>\n          </CommandList>\n        </ScrollArea>\n      </Flex>\n      <CommandFooter>\n        {selectedInstance && (\n          <InstancePathFooter instanceId={selectedInstance.id} />\n        )}\n      </CommandFooter>\n    </>\n  );\n};\n\nexport const showInstance = (\n  instanceId: Instance[\"id\"],\n  panel?: \"style\" | \"settings\"\n) => {\n  const instances = $instances.get();\n  const pagesData = $pages.get();\n  if (pagesData === undefined) {\n    return;\n  }\n  const awareness = findAwarenessByInstanceId(pagesData, instances, instanceId);\n  $awareness.set(awareness);\n  if (panel !== undefined) {\n    $activeInspectorPanel.set(panel);\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/shared/instance-path-footer.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { Flex, Text } from \"@webstudio-is/design-system\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { $instances, $pages } from \"~/shared/sync/data-stores\";\nimport { buildInstancePath } from \"~/shared/instance-utils\";\n\ntype InstancePathFooterProps = {\n  instanceId: Instance[\"id\"];\n};\n\nexport const InstancePathFooter = ({ instanceId }: InstancePathFooterProps) => {\n  const instances = useStore($instances);\n  const pages = useStore($pages);\n\n  const path = useMemo(() => {\n    if (!pages || !instances || !instances.has(instanceId)) {\n      return;\n    }\n    return buildInstancePath(instanceId, pages, instances);\n  }, [instanceId, pages, instances]);\n\n  if (!path) {\n    return;\n  }\n\n  return (\n    <Flex grow>\n      <Text color=\"moreSubtle\" truncate>\n        /{path.join(\"/\")}\n      </Text>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/shared/types.ts",
    "content": "export type BaseOption = {\n  terms: string[];\n  type: string;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/command-panel/shared/usage-utils.ts",
    "content": "/**\n * Formats usage count for display\n */\nexport const formatUsageCount = (usages: number): string => {\n  if (usages === 0) {\n    return \"unused\";\n  }\n  return `${usages} ${usages === 1 ? \"usage\" : \"usages\"}`;\n};\n\n/**\n * Returns search terms for usage count\n */\nexport const getUsageSearchTerms = (usages: number): string[] => {\n  return usages === 0 ? [\"unused\"] : [\"usage\", \"usages\"];\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/components/components.tsx",
    "content": "import { useState } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { isFeatureEnabled } from \"@webstudio-is/feature-flags\";\nimport {\n  type WsComponentMeta,\n  componentCategories,\n  collectionComponent,\n  elementComponent,\n} from \"@webstudio-is/sdk\";\nimport {\n  theme,\n  Flex,\n  ComponentCard,\n  ScrollArea,\n  List,\n  ListItem,\n  SearchField,\n  Separator,\n  useSearchFieldKeys,\n  findNextListItemIndex,\n  Text,\n  Box,\n  PanelTitle,\n} from \"@webstudio-is/design-system\";\nimport { CollapsibleSection } from \"~/builder/shared/collapsible-section\";\nimport { dragItemAttribute, useDraggable } from \"./use-draggable\";\nimport {\n  $registeredComponentMetas,\n  $registeredTemplates,\n} from \"~/shared/nano-states\";\nimport {\n  getComponentTemplateData,\n  insertWebstudioElementAt,\n  insertWebstudioFragmentAt,\n} from \"~/shared/instance-utils\";\nimport type { Publish } from \"~/shared/pubsub\";\nimport { $selectedPage } from \"~/shared/awareness\";\nimport { mapGroupBy } from \"~/shared/shim\";\nimport {\n  getInstanceLabel,\n  InstanceIcon,\n} from \"~/builder/shared/instance-label\";\n\ntype Meta = {\n  name: string;\n  category: string;\n  order: undefined | number;\n  label: string;\n  description: undefined | string;\n  icon?: string;\n  firstInstance: { component: string; tag?: string };\n};\n\nconst $metas = computed(\n  [$registeredComponentMetas, $registeredTemplates],\n  (componentMetas, templates) => {\n    const availableComponents = new Set<string>();\n    const metas: Meta[] = [];\n    for (const [name, componentMeta] of componentMetas) {\n      // only set available components from component meta\n      availableComponents.add(name);\n      metas.push({\n        name,\n        category: componentMeta.category ?? \"hidden\",\n        order: componentMeta.order,\n        label: getInstanceLabel({ component: name }),\n        description: componentMeta.description,\n        firstInstance: { component: name },\n      });\n    }\n    for (const [name, templateMeta] of templates) {\n      const componentMeta = componentMetas.get(name);\n      availableComponents.add(name);\n      metas.push({\n        name,\n        category: templateMeta.category ?? \"hidden\",\n        order: templateMeta.order,\n        label:\n          templateMeta.label ??\n          componentMeta?.label ??\n          getInstanceLabel({ component: name }),\n        description: templateMeta.description,\n        icon: templateMeta.icon,\n        firstInstance: templateMeta.template.instances[0],\n      });\n    }\n    const metasByCategory = mapGroupBy(metas, (meta) => meta.category);\n    for (const meta of metasByCategory.values()) {\n      meta.sort((metaA, metaB) => {\n        return (\n          (metaA.order ?? Number.MAX_SAFE_INTEGER) -\n          (metaB.order ?? Number.MAX_SAFE_INTEGER)\n        );\n      });\n    }\n    return { metasByCategory, availableComponents };\n  }\n);\n\ntype Groups = Array<{\n  category: Exclude<WsComponentMeta[\"category\"], undefined> | \"found\";\n  metas: Array<Meta>;\n}>;\n\nconst filterAndGroupComponents = ({\n  documentType = \"html\",\n  metasByCategory,\n  search,\n}: {\n  documentType?: \"html\" | \"xml\";\n  metasByCategory: Map<string, Array<Meta>>;\n  search: string;\n}): Groups => {\n  const categories = componentCategories.filter((category) => {\n    if (category === \"hidden\") {\n      return false;\n    }\n\n    // Only xml category is allowed for xml document type\n    if (documentType === \"xml\") {\n      return category === \"xml\" || category === \"data\";\n    }\n    // Hide xml category for non-xml document types\n    if (category === \"xml\") {\n      return false;\n    }\n\n    if (\n      isFeatureEnabled(\"internalComponents\") === false &&\n      category === \"internal\"\n    ) {\n      return false;\n    }\n\n    return true;\n  });\n\n  let groups: Groups = categories.map((category) => {\n    const metas = (metasByCategory.get(category) ?? []).filter((meta) => {\n      if (documentType === \"xml\" && meta.category === \"data\") {\n        return meta.name === collectionComponent;\n      }\n      return true;\n    });\n\n    return { category, metas };\n  });\n\n  if (search.length > 0) {\n    const metas = groups.map((group) => group.metas).flat();\n    const matched = matchSorter(metas, search, {\n      keys: [\"label\"],\n    });\n    groups = [{ category: \"found\", metas: matched }];\n  }\n\n  groups = groups.filter((group) => group.metas.length > 0);\n\n  return groups;\n};\n\nconst findComponentIndex = (groups: Groups, selectedComponent?: string) => {\n  if (selectedComponent === undefined) {\n    return { index: -1, metas: groups[0].metas };\n  }\n\n  for (const { metas } of groups) {\n    const index = metas.findIndex((meta) => meta.name === selectedComponent);\n    if (index === -1) {\n      continue;\n    }\n    return { index, metas };\n  }\n\n  return { index: -1, metas: [] };\n};\n\nexport const ComponentsPanel = ({\n  publish,\n  onClose,\n}: {\n  publish: Publish;\n  onClose: () => void;\n}) => {\n  const selectedPage = useStore($selectedPage);\n  const [selectedComponent, setSelectedComponent] = useState<string>();\n\n  const handleInsert = (component: string) => {\n    if (component === elementComponent) {\n      insertWebstudioElementAt();\n    } else {\n      const fragment = getComponentTemplateData(component);\n      if (fragment) {\n        insertWebstudioFragmentAt(fragment);\n      }\n    }\n    onClose();\n  };\n\n  const resetSelectedComponent = () => {\n    setSelectedComponent(undefined);\n  };\n\n  const getSelectedComponent = () => {\n    // When user didn't select a component but they have search input,\n    // we want to always have the first component selected, so that user can just hit enter.\n    if (selectedComponent === undefined && searchFieldProps.value) {\n      return groups[0].metas[0].name;\n    }\n    return selectedComponent;\n  };\n\n  const searchFieldProps = useSearchFieldKeys({\n    onChange: resetSelectedComponent,\n    onAbort: resetSelectedComponent,\n    onMove({ direction }) {\n      if (direction === \"current\") {\n        const component = getSelectedComponent();\n        if (component !== undefined) {\n          handleInsert(component);\n        }\n        return;\n      }\n\n      const { index, metas } = findComponentIndex(groups, selectedComponent);\n\n      const nextIndex = findNextListItemIndex(index, metas.length, direction);\n      const nextComponent = metas[nextIndex]?.name;\n      if (nextComponent) {\n        setSelectedComponent(nextComponent);\n      }\n    },\n  });\n\n  const { metasByCategory, availableComponents } = useStore($metas);\n\n  const { dragCard, draggableContainerRef, isDragging } = useDraggable({\n    publish,\n    availableComponents,\n  });\n\n  const groups = filterAndGroupComponents({\n    documentType: selectedPage?.meta.documentType,\n    metasByCategory,\n    search: searchFieldProps.value,\n  });\n\n  return (\n    <>\n      <PanelTitle>Components</PanelTitle>\n      <Separator />\n\n      <Box css={{ padding: theme.panel.padding }}>\n        <SearchField\n          {...searchFieldProps}\n          autoFocus\n          placeholder=\"Find components\"\n        />\n      </Box>\n      <Separator />\n\n      <ScrollArea ref={draggableContainerRef}>\n        {groups.map((group) => (\n          <CollapsibleSection\n            label={group.category}\n            key={group.category}\n            fullWidth\n          >\n            <List asChild>\n              <Flex\n                gap=\"1\"\n                wrap=\"wrap\"\n                css={{\n                  paddingInline: theme.panel.paddingInline,\n                  overflow: \"auto\",\n                }}\n              >\n                {group.metas.map((meta, index) => (\n                  <ListItem\n                    asChild\n                    state={\n                      meta.name === getSelectedComponent()\n                        ? \"selected\"\n                        : undefined\n                    }\n                    index={index}\n                    key={meta.name}\n                    onSelect={() => handleInsert(meta.name)}\n                    onFocus={() => setSelectedComponent(meta.name)}\n                  >\n                    <ComponentCard\n                      {...{ [dragItemAttribute]: meta.name }}\n                      // Too hard to calculate, goal was to have 3 cards in one row on the smallest width and to fill it at the same ime\n                      css={{ width: 69 }}\n                      label={meta.label}\n                      description={meta.description}\n                      disableTooltip={isDragging}\n                      icon={\n                        <InstanceIcon\n                          size=\"auto\"\n                          instance={meta.firstInstance}\n                          // for cases like Sheet template\n                          icon={meta.icon}\n                        />\n                      }\n                    />\n                  </ListItem>\n                ))}\n                {dragCard}\n                {group.metas.length === 0 && (\n                  <Flex grow justify=\"center\" css={{ py: theme.spacing[10] }}>\n                    <Text>No matching component</Text>\n                  </Flex>\n                )}\n              </Flex>\n            </List>\n          </CollapsibleSection>\n        ))}\n      </ScrollArea>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/components/index.ts",
    "content": "export * from \"./components\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/components/use-draggable.tsx",
    "content": "import { useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { createPortal } from \"react-dom\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport {\n  type Point,\n  Flex,\n  useDrag,\n  ComponentCard,\n  useDisableCanvasPointerEvents,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport { useSubscribe, type Publish } from \"~/shared/pubsub\";\nimport { $canvasRect, $scale } from \"~/builder/shared/nano-states\";\nimport {\n  InstanceIcon,\n  getInstanceLabel,\n} from \"~/builder/shared/instance-label\";\n\nconst DragLayer = ({\n  component,\n  point,\n}: {\n  component: Instance[\"component\"];\n  point: Point;\n}) => {\n  return createPortal(\n    <Flex\n      // Container is used to position card\n      css={{\n        position: \"absolute\",\n        // Prevents flickering between hover/outside mouse position\n        pointerEvents: \"none\",\n        inset: 0,\n      }}\n    >\n      <ComponentCard\n        label={getInstanceLabel({ component })}\n        icon={<InstanceIcon size=\"auto\" instance={{ component }} />}\n        style={{\n          transform: `translate3d(${point.x}px, ${point.y}px, 0)`,\n          width: rawTheme.spacing[20],\n          height: rawTheme.spacing[20],\n        }}\n      />\n    </Flex>,\n    document.body\n  );\n};\n\nexport const dragItemAttribute = \"data-drag-component\";\n\nconst toCanvasCoordinates = (\n  { x, y }: Point,\n  scale: number,\n  canvasRect?: DOMRect\n) => {\n  if (canvasRect === undefined) {\n    return { x: 0, y: 0 };\n  }\n  const scaleFraction = scale / 100;\n  return {\n    x: (x - canvasRect.x) / scaleFraction,\n    y: (y - canvasRect.y) / scaleFraction,\n  };\n};\n\nconst elementToComponentName = (\n  element: Element,\n  availableComponents: Set<string>\n) => {\n  // If drag doesn't start on the button element directly but on one of its children,\n  // we need to trace back to the button that has the data.\n  const parentWithData = element.closest(`[${dragItemAttribute}]`);\n\n  if (parentWithData instanceof HTMLElement) {\n    const dragComponent = parentWithData.dataset.dragComponent as string;\n    if (availableComponents.has(dragComponent)) {\n      return dragComponent;\n    }\n  }\n  return false;\n};\n\nexport const useDraggable = ({\n  publish,\n  availableComponents,\n}: {\n  publish: Publish;\n  availableComponents: Set<string>;\n}) => {\n  const [dragComponent, setDragComponent] = useState<Instance[\"component\"]>();\n  const [isDragging, setIsDragging] = useState(false);\n  const [point, setPoint] = useState<Point>({ x: 0, y: 0 });\n  const canvasRect = useStore($canvasRect);\n  const scale = useStore($scale);\n  const { enableCanvasPointerEvents, disableCanvasPointerEvents } =\n    useDisableCanvasPointerEvents();\n\n  const dragHandlers = useDrag<Instance[\"component\"]>({\n    elementToData(element) {\n      return elementToComponentName(element, availableComponents);\n    },\n    onStart({ data: componentName }) {\n      setDragComponent(componentName);\n      setIsDragging(true);\n      publish({\n        type: \"dragStart\",\n        payload: {\n          origin: \"panel\",\n          type: \"insert\",\n          dragComponent: componentName,\n        },\n      });\n      disableCanvasPointerEvents();\n    },\n    onMove: (point) => {\n      setPoint(point);\n      publish({\n        type: \"dragMove\",\n        payload: {\n          canvasCoordinates: toCanvasCoordinates(point, scale, canvasRect),\n        },\n      });\n    },\n    onEnd({ isCanceled }) {\n      setDragComponent(undefined);\n      setIsDragging(false);\n      publish({\n        type: \"dragEnd\",\n        payload: { isCanceled },\n      });\n\n      enableCanvasPointerEvents();\n    },\n  });\n\n  useSubscribe(\"cancelCurrentDrag\", () => {\n    dragHandlers.cancelCurrentDrag();\n  });\n\n  const dragCard = dragComponent ? (\n    <DragLayer component={dragComponent} point={point} />\n  ) : undefined;\n\n  return {\n    dragCard,\n    draggableContainerRef: dragHandlers.rootRef,\n    isDragging,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/footer/breadcrumbs.tsx",
    "content": "import { Fragment } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { ChevronRightIcon } from \"@webstudio-is/icons\";\nimport { theme, Button, Flex, Text } from \"@webstudio-is/design-system\";\nimport { $textEditingInstanceSelector } from \"~/shared/nano-states\";\nimport { $selectedInstancePath, selectInstance } from \"~/shared/awareness\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\n\nexport const Breadcrumbs = () => {\n  const instancePath = useStore($selectedInstancePath);\n  return (\n    <Flex align=\"center\" css={{ height: \"100%\", px: theme.spacing[3] }}>\n      {instancePath === undefined ? (\n        <Text>No instance selected</Text>\n      ) : (\n        instancePath\n          // start breadcrumbs from the root\n          .slice()\n          .reverse()\n          .map(({ instance, instanceSelector }, index) => {\n            return (\n              <Fragment key={index}>\n                <Button\n                  color=\"dark-ghost\"\n                  css={{ color: \"inherit\" }}\n                  key={instance.id}\n                  onClick={() => {\n                    selectInstance(instanceSelector);\n                    $textEditingInstanceSelector.set(undefined);\n                  }}\n                >\n                  {getInstanceLabel(instance)}\n                </Button>\n                {/* hide the last one */}\n                {index < instancePath.length - 1 && <ChevronRightIcon />}\n              </Fragment>\n            );\n          })\n      )}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/footer/footer.tsx",
    "content": "import { Flex, theme } from \"@webstudio-is/design-system\";\nimport { Breadcrumbs } from \"./breadcrumbs\";\n\nexport const Footer = () => {\n  return (\n    <Flex\n      as=\"footer\"\n      align=\"center\"\n      css={{\n        isolation: \"isolate\",\n        gridArea: \"footer\",\n        height: theme.spacing[11],\n        background: theme.colors.backgroundTopbar,\n        color: theme.colors.foregroundContrastMain,\n      }}\n    >\n      <Breadcrumbs />\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/footer/index.ts",
    "content": "export * from \"./breadcrumbs\";\nexport * from \"./footer\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/help/help-center.stories.tsx",
    "content": "import { Button, Flex, StorySection } from \"@webstudio-is/design-system\";\nimport { HelpIcon } from \"@webstudio-is/icons\";\nimport { HelpCenter as HelpCenterComponent } from \"./help-center\";\n\nexport default {\n  title: \"Builder/Help/Help Center\",\n  component: HelpCenterComponent,\n};\n\nexport const HelpCenter = () => (\n  <StorySection title=\"Help Center\">\n    <Flex css={{ padding: 100 }}>\n      <HelpCenterComponent open>\n        <HelpCenterComponent.Trigger asChild>\n          <Button prefix={<HelpIcon />} color=\"ghost\">\n            Help\n          </Button>\n        </HelpCenterComponent.Trigger>\n      </HelpCenterComponent>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/help/help-center.tsx",
    "content": "import {\n  Button,\n  Flex,\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { type ComponentProps } from \"react\";\nimport { $remoteDialog } from \"../../shared/nano-states\";\nimport { help } from \"~/shared/help\";\n\nexport const HelpCenter = ({\n  children,\n  side = \"right\",\n  ...popoverProps\n}: {\n  children: React.ReactNode;\n  side?: \"bottom\" | \"left\" | \"right\" | \"top\";\n} & Pick<ComponentProps<typeof Popover>, \"open\" | \"onOpenChange\">) => {\n  return (\n    <Popover {...popoverProps}>\n      {children}\n      <PopoverContent\n        avoidCollisions\n        sideOffset={0}\n        // Height of the footer\n        collisionPadding={{ bottom: Number.parseFloat(rawTheme.spacing[11]) }}\n        side={side}\n      >\n        <Flex\n          as=\"form\"\n          target=\"_blank\"\n          direction=\"column\"\n          css={{ padding: theme.spacing[5] }}\n          gap=\"2\"\n        >\n          {help.map((item) => (\n            <Button\n              key={item.url}\n              prefix={item.icon}\n              css={{ justifyContent: \"start\" }}\n              color=\"ghost\"\n              formAction={item.url}\n              onClick={() => {\n                if (\"target\" in item && item.target === \"embed\") {\n                  $remoteDialog.set({\n                    title: item.label,\n                    url: item.url,\n                  });\n                }\n              }}\n            >\n              {item.label}\n            </Button>\n          ))}\n        </Flex>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nHelpCenter.Trigger = PopoverTrigger;\n"
  },
  {
    "path": "apps/builder/app/builder/features/help/remote-dialog.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport { $remoteDialog } from \"../../shared/nano-states\";\nimport { RemoteDialog as RemoteDialogComponent } from \"./remote-dialog\";\n\nexport default {\n  title: \"Builder/Help/Remote Dialog\",\n  component: RemoteDialogComponent,\n};\n\n$remoteDialog.set({\n  title: \"Video tutorials\",\n  url: \"https://docs.webstudio.is\",\n});\n\nexport const RemoteDialog = () => (\n  <StorySection title=\"Remote Dialog\">\n    <RemoteDialogComponent />\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/help/remote-dialog.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { $remoteDialog } from \"../../shared/nano-states\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogMaximize,\n  DialogTitle,\n  DialogTitleActions,\n  IconButton,\n} from \"@webstudio-is/design-system\";\nimport { ExternalLinkIcon } from \"@webstudio-is/icons\";\n\nexport const RemoteDialog = () => {\n  const options = useStore($remoteDialog);\n  if (options === undefined) {\n    return;\n  }\n\n  return (\n    <Dialog\n      open\n      onOpenChange={() => {\n        $remoteDialog.set(undefined);\n      }}\n      modal={false}\n      draggable\n      resize=\"both\"\n    >\n      <DialogContent width={640} height={640}>\n        <iframe\n          src={options.url}\n          style={{\n            border: 0,\n            height: \"100%\",\n          }}\n        ></iframe>\n        {/* Title is at the end intentionally,\n         * to make the close button last in the tab order\n         */}\n        <DialogTitle\n          suffix={\n            <DialogTitleActions>\n              <IconButton\n                onClick={() => {\n                  window.open(options.url, \"_blank\");\n                }}\n              >\n                <ExternalLinkIcon />\n              </IconButton>\n              <DialogMaximize />\n              <DialogClose />\n            </DialogTitleActions>\n          }\n        >\n          {options.title}\n        </DialogTitle>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/keyboard-shortcuts-dialog/index.ts",
    "content": "export {\n  KeyboardShortcutsDialog,\n  openKeyboardShortcutsDialog,\n} from \"./keyboard-shortcuts-dialog\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.stories.tsx",
    "content": "import type { Meta } from \"@storybook/react\";\nimport { useEffect } from \"react\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { subscribeCommands } from \"~/builder/shared/commands\";\nimport {\n  KeyboardShortcutsDialog as KeyboardShortcutsDialogComponent,\n  openKeyboardShortcutsDialog,\n} from \"./keyboard-shortcuts-dialog\";\n\n// Initialize commands\nsubscribeCommands();\n\nexport default {\n  title: \"Keyboard shortcuts dialog\",\n} satisfies Meta;\n\nexport const KeyboardShortcuts = () => {\n  useEffect(openKeyboardShortcutsDialog, []);\n  return (\n    <StorySection title=\"Keyboard Shortcuts\">\n      <KeyboardShortcutsDialogComponent />\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/keyboard-shortcuts-dialog/keyboard-shortcuts-dialog.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  Dialog,\n  DialogTitle,\n  DialogContent,\n  Grid,\n  Text,\n  Kbd,\n  filterHotkeyByOs,\n  ScrollArea,\n  theme,\n  Box,\n} from \"@webstudio-is/design-system\";\nimport { atom } from \"nanostores\";\nimport { Fragment } from \"react\";\nimport { $commandMetas } from \"~/shared/commands-emitter\";\n\nconst $isKeyboardShortcutsOpen = atom(false);\n\nexport const openKeyboardShortcutsDialog = () => {\n  $isKeyboardShortcutsOpen.set(true);\n};\n\n// Additional shortcuts not in commands system but should be shown\nconst additionalShortcuts = [\n  {\n    name: \"expandNavigatorItem\",\n    label: \"Expand Navigator item\",\n    description: \"Expand navigator item\",\n    category: \"Navigator\",\n    defaultHotkeys: [\"→\"],\n  },\n  {\n    name: \"collapseNavigatorItem\",\n    label: \"Collapse Navigator item\",\n    description: \"Collapse navigator item\",\n    category: \"Navigator\",\n    defaultHotkeys: [\"←\"],\n  },\n  {\n    name: \"expandAllNavigatorNodes\",\n    label: \"Expand all items\",\n    description: \"Click on arrow to expand or collapse all child items\",\n    category: \"Navigator\",\n    defaultHotkeys: [\"alt+click\"],\n  },\n  {\n    name: \"switchBreakpoint\",\n    label: \"Switch breakpoints\",\n    description: \"Switch to breakpoint by number (1-9)\",\n    category: \"Top bar\",\n    defaultHotkeys: [\"1-9\"],\n  },\n];\n\nexport const KeyboardShortcutsDialog = () => {\n  const isOpen = useStore($isKeyboardShortcutsOpen);\n  const commandMetas = useStore($commandMetas);\n\n  // Filter commands that have hotkeys and are not hidden\n  const commandsWithHotkeys = Array.from(commandMetas.values()).filter(\n    (command) =>\n      command.defaultHotkeys &&\n      command.defaultHotkeys.length > 0 &&\n      !command.hidden\n  );\n\n  // Combine with additional shortcuts\n  const allShortcuts = [...commandsWithHotkeys, ...additionalShortcuts];\n\n  // Extract valid command names as a type\n  type ValidCommandName = (typeof allShortcuts)[number][\"name\"];\n\n  // Group filtered commands by category\n  const groupedCommands = allShortcuts.reduce<\n    Record<string, Array<(typeof allShortcuts)[number]>>\n  >((acc, command) => {\n    const category = command.category ?? \"Other\";\n    if (!acc[category]) {\n      acc[category] = [];\n    }\n    acc[category].push(command);\n    return acc;\n  }, {});\n\n  // Define popularity order for shortcuts within each category\n  const shortcutOrder: Record<string, ValidCommandName[]> = {\n    General: [\n      \"cancelCurrentDrag\",\n      \"openCommandPanel\",\n      \"openPublishDialog\",\n      \"openExportDialog\",\n      \"openKeyboardShortcuts\",\n    ],\n    \"Top bar\": [\"toggleDesignMode\", \"togglePreviewMode\", \"toggleContentMode\"],\n    Navigator: [\n      \"copy\",\n      \"cut\",\n      \"paste\",\n      \"duplicateInstance\",\n      \"wrap\",\n      \"unwrap\",\n      \"expandNavigatorItem\",\n      \"collapseNavigatorItem\",\n      \"expandAllNavigatorNodes\",\n    ],\n    Panels: [\n      \"toggleComponentsPanel\",\n      \"toggleNavigatorPanel\",\n      \"openStylePanel\",\n      \"openSettingsPanel\",\n    ],\n\n    \"Style panel\": [\n      \"focusStyleSources\",\n      \"toggleStylePanelFocusMode\",\n      \"toggleStylePanelAdvancedMode\",\n    ],\n  };\n\n  // Extract valid category names as a type\n  type ValidCategory = keyof typeof groupedCommands;\n\n  // Sort shortcuts within each category by popularity\n  Object.keys(groupedCommands).forEach((category) => {\n    const order = shortcutOrder[category] || [];\n    groupedCommands[category].sort((a, b) => {\n      const aIndex = order.indexOf(a.name);\n      const bIndex = order.indexOf(b.name);\n      // If both are in the order list, sort by their position\n      if (aIndex !== -1 && bIndex !== -1) {\n        return aIndex - bIndex;\n      }\n      // If only one is in the order list, prioritize it\n      if (aIndex !== -1) {\n        return -1;\n      }\n      if (bIndex !== -1) {\n        return 1;\n      }\n      // If neither is in the order list, maintain original order\n      return 0;\n    });\n  });\n\n  // Define category order by popularity\n  const categoryOrder: ValidCategory[] = [\n    \"General\",\n    \"Top bar\",\n    \"Navigator\",\n    \"Panels\",\n    \"Style panel\",\n  ];\n\n  // Sort categories by popularity\n  const sortedCategories = Object.keys(groupedCommands).sort((a, b) => {\n    const aIndex = categoryOrder.indexOf(a);\n    const bIndex = categoryOrder.indexOf(b);\n    // If both are in the order list, sort by their position\n    if (aIndex !== -1 && bIndex !== -1) {\n      return aIndex - bIndex;\n    }\n    // If only one is in the order list, prioritize it\n    if (aIndex !== -1) {\n      return -1;\n    }\n    if (bIndex !== -1) {\n      return 1;\n    }\n    // If neither is in the order list, maintain original order\n    return 0;\n  });\n\n  return (\n    <Dialog\n      open={isOpen}\n      onOpenChange={(open) => $isKeyboardShortcutsOpen.set(open)}\n    >\n      <DialogContent\n        css={{\n          width: 700,\n          display: \"flex\",\n          flexDirection: \"column\",\n        }}\n      >\n        <DialogTitle>Keyboard shortcuts</DialogTitle>\n        <ScrollArea>\n          <Grid columns={2} gap={3} css={{ padding: theme.panel.padding }}>\n            {sortedCategories.map((category) => {\n              const categoryCommands = groupedCommands[category];\n              return (\n                <Box key={category}>\n                  <Text\n                    variant=\"titles\"\n                    color=\"main\"\n                    css={{ mb: theme.spacing[5] }}\n                  >\n                    {category}\n                  </Text>\n                  <Grid\n                    gap={3}\n                    align=\"center\"\n                    css={{ gridTemplateColumns: \"10ch 1fr\" }}\n                  >\n                    {categoryCommands.map((command) => {\n                      const hotkey = command.defaultHotkeys\n                        ? filterHotkeyByOs(command.defaultHotkeys)\n                        : undefined;\n\n                      if (!hotkey) {\n                        return;\n                      }\n\n                      return (\n                        <Fragment key={command.name}>\n                          <Box css={{ textAlign: \"right\" }}>\n                            <Box\n                              as=\"span\"\n                              css={{\n                                paddingInline: 4,\n                                border: `1px solid ${theme.colors.borderNeutral}`,\n                                borderRadius: theme.borderRadius[2],\n                              }}\n                            >\n                              <Kbd\n                                value={hotkey.split(\"+\") as string[]}\n                                color=\"moreSubtle\"\n                              />\n                            </Box>\n                          </Box>\n                          <Text variant=\"regular\" color=\"subtle\">\n                            {(\"description\" in command &&\n                              command.description) ||\n                              command.label ||\n                              command.name}\n                          </Text>\n                        </Fragment>\n                      );\n                    })}\n                  </Grid>\n                </Box>\n              );\n            })}\n          </Grid>\n        </ScrollArea>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/about.stories.tsx",
    "content": "import {\n  Dialog,\n  DialogContent,\n  Flex,\n  StorySection,\n} from \"@webstudio-is/design-system\";\nimport { About as AboutComponent } from \"./about\";\nimport type { MarketplaceOverviewItem } from \"~/shared/marketplace/types\";\n\nexport default {\n  title: \"Builder/Marketplace/About\",\n  component: AboutComponent,\n};\n\nconst sampleItem: MarketplaceOverviewItem = {\n  projectId: \"project-123\",\n  authorizationToken: \"token-abc\",\n  category: \"sectionTemplates\",\n  name: \"Hero section\",\n  thumbnailAssetId: \"asset-1\",\n  author: \"Webstudio team\",\n  email: \"hello@webstudio.is\",\n  website: \"https://webstudio.is\",\n  issues: \"https://github.com/webstudio-is/webstudio/issues\",\n  description:\n    \"A versatile hero section template with a large heading, subheading, and a call-to-action button.\",\n};\n\nexport const About = () => (\n  <StorySection title=\"About\">\n    <Flex direction=\"column\" gap=\"5\" css={{ width: 320 }}>\n      <Dialog open>\n        <DialogContent>\n          <AboutComponent item={sampleItem} />\n        </DialogContent>\n      </Dialog>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/about.tsx",
    "content": "import {\n  DialogClose,\n  Flex,\n  Link,\n  PanelTitle,\n  Separator,\n  Text,\n  Tooltip,\n  buttonStyle,\n  theme,\n  truncate,\n} from \"@webstudio-is/design-system\";\nimport type { MarketplaceOverviewItem } from \"~/shared/marketplace/types\";\nimport { ExternalLinkIcon } from \"@webstudio-is/icons\";\nimport { builderUrl } from \"~/shared/router-utils\";\n\nexport const About = ({ item }: { item?: MarketplaceOverviewItem }) => {\n  if (item === undefined) {\n    return;\n  }\n\n  const hasAuthToken = item.authorizationToken != null;\n\n  return (\n    <>\n      <PanelTitle suffix={<DialogClose />}>{item.name}</PanelTitle>\n      <Separator />\n\n      <Flex\n        direction=\"column\"\n        css={{ my: theme.spacing[5], mx: theme.spacing[8] }}\n        gap=\"3\"\n      >\n        <Text>{item.description}</Text>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text truncate>Author: {item.author}</Text>\n          {item.website && (\n            <Flex gap=\"1\">\n              <Text css={{ flexShrink: 0 }}>Website:</Text>\n              <Link href={item.website} target=\"_blank\" css={truncate()}>\n                {item.website}\n              </Link>\n            </Flex>\n          )}\n          <Flex gap=\"1\">\n            <Text css={{ flexShrink: 0 }}>Email:</Text>\n            <Link href={`mailto:${item.email}`} css={truncate()}>\n              {item.email}\n            </Link>\n          </Flex>\n          {item.issues && (\n            <Flex gap=\"1\">\n              <Text css={{ flexShrink: 0 }}>Issues Tracker:</Text>\n              <Link href={item.issues} target=\"_blank\" css={truncate()}>\n                {item.issues}\n              </Link>\n            </Flex>\n          )}\n        </Flex>\n      </Flex>\n      <Separator />\n\n      <Flex gap=\"1\" css={{ my: theme.spacing[5], mx: theme.spacing[8] }}>\n        <Tooltip\n          content={\n            hasAuthToken\n              ? undefined\n              : 'The project does not have a shared link with \"View\" permission.'\n          }\n        >\n          <Link\n            className={buttonStyle({\n              color: \"neutral\",\n              css: {\n                gap: theme.spacing[3],\n              },\n            })}\n            underline=\"none\"\n            href={\n              hasAuthToken\n                ? builderUrl({\n                    projectId: item.projectId,\n                    origin: location.origin,\n                    authToken: item.authorizationToken,\n                  })\n                : undefined\n            }\n            target=\"_blank\"\n            aria-disabled={hasAuthToken ? undefined : \"true\"}\n          >\n            <ExternalLinkIcon aria-hidden /> Open project\n          </Link>\n        </Tooltip>\n      </Flex>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/card.stories.tsx",
    "content": "import {\n  Flex,\n  IconButton,\n  StorySection,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { EllipsesIcon } from \"@webstudio-is/icons\";\nimport { Card as CardComponent } from \"./card\";\n\nexport default {\n  title: \"Builder/Marketplace/Card\",\n  component: CardComponent,\n};\n\nconst placeholderImage = `data:image/svg+xml,${encodeURIComponent(\n  `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"400\" height=\"210\">\n    <rect width=\"400\" height=\"210\" fill=\"#c0d0e0\"/>\n    <text x=\"200\" y=\"105\" text-anchor=\"middle\" dominant-baseline=\"central\"\n      font-family=\"sans-serif\" font-size=\"24\" fill=\"#4a6a8a\">400 × 210</text>\n  </svg>`\n)}`;\n\nexport const Card = () => (\n  <StorySection title=\"Card\">\n    <Flex direction=\"column\" gap=\"3\" css={{ width: 300 }}>\n      <Text variant=\"labels\">Default</Text>\n      <CardComponent title=\"Section template\" />\n\n      <Text variant=\"labels\">With image</Text>\n      <CardComponent\n        title=\"Hero section\"\n        image={placeholderImage}\n        suffix={\n          <IconButton>\n            <EllipsesIcon />\n          </IconButton>\n        }\n      />\n\n      <Text variant=\"labels\">No image (placeholder)</Text>\n      <CardComponent title=\"Untitled\" image=\"\" />\n\n      <Text variant=\"labels\">Selected</Text>\n      <CardComponent title=\"Selected card\" state=\"selected\" />\n\n      <Text variant=\"labels\">Loading</Text>\n      <CardComponent\n        title=\"Loading card\"\n        image={placeholderImage}\n        state=\"loading\"\n      />\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/card.tsx",
    "content": "import { forwardRef } from \"react\";\nimport {\n  Flex,\n  Text,\n  theme,\n  focusRingStyle,\n  css,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport { SpinnerIcon } from \"@webstudio-is/icons\";\n\nconst focusOutline = focusRingStyle();\n\nconst imageContainerStyle = css({\n  position: \"relative\",\n  overflow: \"hidden\",\n  aspectRatio: \"1.91\",\n  borderRadius: theme.borderRadius[4],\n});\n\nconst spinnerStyle = css({\n  position: \"absolute\",\n  inset: 0,\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  backgroundColor: \"rgba(255, 255, 255, 0.5)\",\n  backdropFilter: \"blur(8px)\",\n});\n\nconst imageStyle = css({\n  position: \"absolute\",\n  top: 0,\n  width: \"100%\",\n  height: \"100%\",\n  transition: \"transform 100ms\",\n  \"&:hover\": {\n    transform: \"scale(1.1)\",\n  },\n  variants: {\n    hasAsset: {\n      true: {\n        objectFit: \"cover\",\n      },\n      false: {\n        background: theme.colors.white,\n        padding: rawTheme.spacing[5],\n      },\n    },\n  },\n});\n\ntype ThumbnailProps = {\n  image?: { name: string } | string;\n  state?: \"loading\";\n  alt: string;\n};\n\nconst Thumbnail = ({ image, state, alt }: ThumbnailProps) => {\n  return (\n    <Flex className={imageContainerStyle()}>\n      {image === \"\" || image === undefined ? (\n        // Placeholder\n        <Flex\n          align=\"center\"\n          justify=\"center\"\n          className={imageStyle({ hasAsset: false })}\n        >\n          <Text variant=\"brandSectionTitle\">{alt}</Text>\n        </Flex>\n      ) : typeof image === \"string\" ? (\n        // Its a URL.\n        <img src={image} className={imageStyle({ hasAsset: true })} />\n      ) : (\n        <Image\n          src={image.name}\n          width={rawTheme.spacing[28]}\n          loader={wsImageLoader}\n          className={imageStyle({ hasAsset: true })}\n        />\n      )}\n\n      {state === \"loading\" && (\n        <div className={spinnerStyle()}>\n          <SpinnerIcon size={rawTheme.spacing[15]} />\n        </div>\n      )}\n    </Flex>\n  );\n};\n\ntype CardProps = {\n  image?: ThumbnailProps[\"image\"];\n  title?: string;\n  suffix?: React.ReactNode;\n  state?: \"selected\" | \"loading\";\n};\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(\n  (\n    { image, title = \"Untitled\", suffix, state = \"initial\" as const, ...props },\n    ref\n  ) => {\n    return (\n      <Flex\n        {...props}\n        ref={ref}\n        direction=\"column\"\n        data-state={state}\n        css={{\n          padding: theme.panel.padding,\n          position: \"relative\",\n          overflow: \"hidden\",\n          outline: \"none\",\n          \"&:focus-visible, &:hover, &[data-state=selected]\": focusOutline,\n        }}\n        gap=\"1\"\n      >\n        <Thumbnail\n          image={image}\n          state={state === \"loading\" ? state : undefined}\n          alt={title}\n        />\n        <Flex align=\"center\">\n          <Text truncate css={{ flexGrow: 1 }}>\n            {image === undefined || image === \"\" ? undefined : title}\n          </Text>\n          {suffix}\n        </Flex>\n      </Flex>\n    );\n  }\n);\n\nCard.displayName = \"Card\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/index.ts",
    "content": "export * from \"./marketplace\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/marketplace.tsx",
    "content": "import { SpinnerIcon } from \"@webstudio-is/icons\";\nimport {\n  Flex,\n  PanelTitle,\n  rawTheme,\n  Separator,\n  FloatingPanel,\n} from \"@webstudio-is/design-system\";\nimport { Overview } from \"./overview\";\nimport { Templates } from \"./templates\";\nimport { useEffect, useState } from \"react\";\nimport { toWebstudioData } from \"./utils\";\nimport type { MarketplaceOverviewItem } from \"~/shared/marketplace/types\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { About } from \"./about\";\nimport { trpcClient } from \"~/shared/trpc/trpc-client\";\n\nexport const MarketplacePanel = (_props: { onClose: () => void }) => {\n  const [activeOverviewItem, setAciveOverviewItem] =\n    useState<MarketplaceOverviewItem>();\n  const [openAbout, setOpenAbout] = useState<Project[\"id\"]>();\n\n  const {\n    load: getItems,\n    data: items,\n    state: itemsLoadingState,\n  } = trpcClient.marketplace.getItems.useQuery();\n\n  const { load: getBuildData, data: buildData } =\n    trpcClient.marketplace.getBuildData.useQuery();\n\n  useEffect(() => {\n    getItems();\n  }, [getItems]);\n\n  const openAboutItem = items?.find((item) => item.projectId === openAbout);\n  const showTemplates =\n    activeOverviewItem && buildData?.projectId === activeOverviewItem.projectId;\n\n  return (\n    <div data-floating-panel-container>\n      <PanelTitle>Marketplace</PanelTitle>\n      <Separator />\n      <Overview\n        items={items}\n        activeProjectId={activeOverviewItem?.projectId}\n        onSelect={(activeOverviewItem) => {\n          setAciveOverviewItem(activeOverviewItem);\n          getBuildData({ projectId: activeOverviewItem.projectId });\n        }}\n        openAbout={openAbout}\n        onOpenAbout={setOpenAbout}\n        hidden={showTemplates}\n      />\n\n      {showTemplates && (\n        <Templates\n          projectId={activeOverviewItem.projectId}\n          name={activeOverviewItem.name}\n          authorizationToken={activeOverviewItem.authorizationToken}\n          productCategory={activeOverviewItem.category}\n          data={toWebstudioData(buildData)}\n          onOpenChange={(isOpen: boolean) => {\n            if (isOpen === false) {\n              setAciveOverviewItem(undefined);\n            }\n          }}\n        />\n      )}\n      {itemsLoadingState !== \"idle\" && (\n        <Flex justify=\"center\" css={{ mt: \"20%\" }}>\n          <SpinnerIcon size={rawTheme.spacing[15]} />\n        </Flex>\n      )}\n      {openAboutItem !== undefined && (\n        <FloatingPanel\n          content={<About item={openAboutItem} />}\n          placement=\"right-start\"\n          width={Number.parseFloat(rawTheme.spacing[35])}\n          open={true}\n          onOpenChange={(isOpen) => {\n            if (!isOpen) {\n              setOpenAbout(undefined);\n            }\n          }}\n        >\n          <span style={{ display: \"none\" }} />\n        </FloatingPanel>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/overview.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport {\n  Flex,\n  IconButton,\n  List,\n  ListItem,\n  PanelTabs,\n  PanelTabsContent,\n  PanelTabsList,\n  PanelTabsTrigger,\n  ScrollArea,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { EllipsesIcon } from \"@webstudio-is/icons\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { usePress } from \"@react-aria/interactions\";\nimport { marketplaceCategories } from \"@webstudio-is/project-build\";\nimport { mapGroupBy } from \"~/shared/shim\";\nimport type { MarketplaceOverviewItem } from \"~/shared/marketplace/types\";\nimport { Card } from \"./card\";\n\nconst GalleryOverviewItem = ({\n  item,\n  isLoading,\n  isOpen,\n  onOpenStateChange,\n  ...props\n}: {\n  item: MarketplaceOverviewItem;\n  isLoading: boolean;\n  isOpen: boolean;\n  onOpenStateChange: (isOpen: boolean) => void;\n}) => {\n  const { pressProps } = usePress({\n    onPress() {\n      onOpenStateChange(isOpen ? false : true);\n    },\n  });\n\n  return (\n    <Card\n      {...props}\n      title={item.name}\n      image={\n        item.thumbnailAssetName ? { name: item.thumbnailAssetName } : undefined\n      }\n      state={isOpen ? \"selected\" : isLoading ? \"loading\" : undefined}\n      suffix={\n        <Flex shrink={false} align=\"center\">\n          <IconButton {...pressProps} state={isOpen ? \"open\" : undefined}>\n            <EllipsesIcon />\n          </IconButton>\n        </Flex>\n      }\n    />\n  );\n};\n\nexport const Overview = ({\n  activeProjectId,\n  hidden,\n  items,\n  onSelect,\n  openAbout,\n  onOpenAbout,\n}: {\n  hidden?: boolean;\n  activeProjectId?: Project[\"id\"];\n  items?: Array<MarketplaceOverviewItem>;\n  onSelect: (item: MarketplaceOverviewItem) => void;\n  openAbout?: Project[\"id\"];\n  onOpenAbout: (projectId?: string) => void;\n}) => {\n  const itemsByCategory = useMemo(\n    () => mapGroupBy(items ?? [], (item) => item.category),\n    [items]\n  );\n  const [selectedCategory, setSelectedCategory] =\n    useState<MarketplaceOverviewItem[\"category\"]>(\"sectionTemplates\");\n\n  const categoryItems = itemsByCategory.get(selectedCategory);\n\n  return (\n    <PanelTabs\n      value={selectedCategory}\n      onValueChange={(category) => {\n        setSelectedCategory(category as MarketplaceOverviewItem[\"category\"]);\n      }}\n      asChild\n      hidden={hidden}\n    >\n      <Flex direction=\"column\">\n        <PanelTabsList>\n          {Array.from(marketplaceCategories.keys()).map((category) => {\n            return (\n              <Tooltip\n                key={category}\n                variant=\"wrapped\"\n                content={marketplaceCategories.get(category)?.description}\n              >\n                <div>\n                  <PanelTabsTrigger value={category}>\n                    {marketplaceCategories.get(category)?.label}\n                  </PanelTabsTrigger>\n                </div>\n              </Tooltip>\n            );\n          })}\n        </PanelTabsList>\n        <PanelTabsContent value={selectedCategory} tabIndex={-1}>\n          <ScrollArea>\n            <List asChild>\n              <Flex direction=\"column\">\n                {categoryItems?.map((item, index) => {\n                  return (\n                    <ListItem\n                      asChild\n                      key={item.projectId}\n                      index={index}\n                      onSelect={() => {\n                        onSelect(item);\n                        onOpenAbout(undefined);\n                      }}\n                    >\n                      <GalleryOverviewItem\n                        item={item}\n                        isLoading={item.projectId === activeProjectId}\n                        isOpen={openAbout === item.projectId}\n                        onOpenStateChange={(isOpen) => {\n                          onOpenAbout(isOpen ? item.projectId : undefined);\n                        }}\n                      />\n                    </ListItem>\n                  );\n                })}\n              </Flex>\n            </List>\n          </ScrollArea>\n        </PanelTabsContent>\n      </Flex>\n    </PanelTabs>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/templates.tsx",
    "content": "import { useMemo } from \"react\";\nimport {\n  Button,\n  Flex,\n  List,\n  ListItem,\n  ScrollArea,\n  Separator,\n  theme,\n  Link,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { ChevronLeftIcon, ExternalLinkIcon } from \"@webstudio-is/icons\";\nimport {\n  elementComponent,\n  Instance,\n  ROOT_FOLDER_ID,\n  type Asset,\n  type Page,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport type { MarketplaceProduct } from \"@webstudio-is/project-build\";\nimport { mapGroupBy } from \"~/shared/shim\";\nimport { CollapsibleSection } from \"~/builder/shared/collapsible-section\";\nimport { builderUrl } from \"~/shared/router-utils\";\nimport {\n  extractWebstudioFragment,\n  findClosestInsertable,\n  detectFragmentTokenConflicts,\n  detectPageTokenConflicts,\n  insertWebstudioFragmentAt,\n  updateWebstudioData,\n} from \"~/shared/instance-utils\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport { insertPageCopyMutable } from \"~/shared/page-utils\";\nimport { Card } from \"./card\";\nimport type { MarketplaceOverviewItem } from \"~/shared/marketplace/types\";\nimport { selectPage } from \"~/shared/awareness\";\n\nconst isBody = (instance: Instance) =>\n  instance.component === \"Body\" ||\n  (instance.component === elementComponent && instance.tag === \"body\");\n\n/**\n * Insert page as a template.\n * - Currently only supports inserting everything from the body\n * - Could be extended to support children of some other instance e.g. Marketplace Item\n */\nconst insertSection = async ({\n  data,\n  instanceId,\n}: {\n  data: WebstudioData;\n  instanceId: string;\n}) => {\n  const fragment = extractWebstudioFragment(data, instanceId);\n  const body = fragment.instances.find(isBody);\n  // remove body and use its children as root insrances\n  if (body) {\n    fragment.instances = fragment.instances.filter(\n      (instance) => !isBody(instance)\n    );\n    fragment.children = body.children;\n  }\n  const insertable = findClosestInsertable(fragment);\n  if (insertable) {\n    // numeric position means the instance already\n    // insertd after or even into ancestor\n    if (insertable.position === \"end\") {\n      insertable.position = \"after\";\n    }\n    const conflicts = detectFragmentTokenConflicts({ fragment });\n    const conflictResolution =\n      conflicts.length > 0\n        ? await builderApi.showTokenConflictDialog(conflicts)\n        : \"theirs\";\n    insertWebstudioFragmentAt(fragment, insertable, conflictResolution);\n  }\n};\n\nconst insertPage = async ({\n  data: sourceData,\n  pageId,\n}: {\n  data: WebstudioData;\n  pageId: Page[\"id\"];\n}) => {\n  const conflicts = detectPageTokenConflicts({ sourceData, pageId });\n  const conflictResolution =\n    conflicts.length > 0\n      ? await builderApi.showTokenConflictDialog(conflicts)\n      : \"theirs\";\n  let newPageId: undefined | Page[\"id\"];\n  updateWebstudioData((targetData) => {\n    newPageId = insertPageCopyMutable({\n      source: { data: sourceData, pageId },\n      target: { data: targetData, folderId: ROOT_FOLDER_ID },\n      conflictResolution,\n    });\n  });\n  if (newPageId) {\n    selectPage(newPageId);\n  }\n};\n\ntype TemplateData = {\n  title?: string;\n  thumbnailAsset?: Asset;\n  pageId: string;\n  rootInstanceId: string;\n};\n\nconst getTemplatesDataByCategory = (\n  data?: WebstudioData\n): Map<string, Array<TemplateData>> => {\n  if (data === undefined) {\n    return new Map();\n  }\n  const pages = [data.pages.homePage, ...data.pages.pages]\n    .filter((page) => page.marketplace?.include)\n    .map((page) => {\n      // category can be empty string\n      const category = page.marketplace?.category || \"Pages\";\n      const thumbnailAsset =\n        data.assets.get(page.marketplace?.thumbnailAssetId ?? \"\") ??\n        data.assets.get(page.meta.socialImageAssetId ?? \"\");\n      return {\n        category,\n        title: page.name,\n        thumbnailAsset,\n        pageId: page.id,\n        rootInstanceId: page.rootInstanceId,\n      };\n    });\n  return mapGroupBy(pages, (page) => page.category);\n};\n\nexport const Templates = ({\n  name,\n  projectId,\n  productCategory,\n  authorizationToken,\n  data,\n  onOpenChange,\n}: {\n  name: string;\n  projectId: string;\n  productCategory: MarketplaceProduct[\"category\"];\n  authorizationToken: MarketplaceOverviewItem[\"authorizationToken\"];\n  data: WebstudioData;\n  onOpenChange: (isOpen: boolean) => void;\n}) => {\n  const templatesDataByCategory = useMemo(\n    () => getTemplatesDataByCategory(data),\n    [data]\n  );\n\n  if (templatesDataByCategory === undefined || data === undefined) {\n    return;\n  }\n\n  const hasAuthToken = authorizationToken != null;\n\n  return (\n    <Flex direction=\"column\" css={{ height: \"100%\" }}>\n      <Flex\n        align=\"center\"\n        shrink=\"false\"\n        justify=\"between\"\n        css={{ padding: theme.panel.padding }}\n        gap=\"3\"\n      >\n        <Button\n          prefix={<ChevronLeftIcon />}\n          onClick={() => {\n            onOpenChange(false);\n          }}\n          color=\"neutral\"\n        >\n          {name}\n        </Button>\n        <Tooltip\n          content={\n            hasAuthToken\n              ? undefined\n              : 'The project does not have a shared link with \"View\" permission.'\n          }\n        >\n          <Link\n            underline=\"none\"\n            href={\n              hasAuthToken\n                ? builderUrl({\n                    projectId: projectId,\n                    origin: location.origin,\n                    authToken: authorizationToken,\n                  })\n                : undefined\n            }\n            target=\"_blank\"\n            aria-label=\"Open project in new tab\"\n            aria-disabled={hasAuthToken ? undefined : \"true\"}\n          >\n            <ExternalLinkIcon />\n          </Link>\n        </Tooltip>\n      </Flex>\n      <Separator />\n      <ScrollArea>\n        {Array.from(templatesDataByCategory.keys())\n          .sort()\n          .map((category) => {\n            return (\n              <CollapsibleSection label={category} key={category} fullWidth>\n                <List asChild>\n                  <Flex direction=\"column\">\n                    {templatesDataByCategory\n                      .get(category)\n                      ?.map((templateData, index) => {\n                        return (\n                          <ListItem\n                            asChild\n                            key={templateData.rootInstanceId}\n                            index={index}\n                            onSelect={() => {\n                              if (productCategory === \"sectionTemplates\") {\n                                insertSection({\n                                  data,\n                                  instanceId: templateData.rootInstanceId,\n                                }).catch(() => {\n                                  // User cancelled conflict dialog\n                                });\n                              }\n                              if (\n                                productCategory === \"pageTemplates\" ||\n                                productCategory === \"integrationTemplates\"\n                              ) {\n                                insertPage({\n                                  data,\n                                  pageId: templateData.pageId,\n                                }).catch(() => {\n                                  // User cancelled conflict dialog\n                                });\n                              }\n                            }}\n                          >\n                            <Card\n                              title={templateData.title}\n                              image={templateData.thumbnailAsset}\n                            />\n                          </ListItem>\n                        );\n                      })}\n                  </Flex>\n                </List>\n              </CollapsibleSection>\n            );\n          })}\n      </ScrollArea>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/marketplace/utils.ts",
    "content": "import {\n  getStyleDeclKey,\n  type Asset,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport type { CompactBuild } from \"@webstudio-is/project-build\";\n\nconst getPair = <Item extends { id: string }>(item: Item) =>\n  [item.id, item] as const;\n\nexport const toWebstudioData = (\n  data: CompactBuild & { assets: Asset[] }\n): WebstudioData => ({\n  assets: new Map(data.assets.map(getPair)),\n  instances: new Map(data.instances.map(getPair)),\n  dataSources: new Map(data.dataSources.map(getPair)),\n  resources: new Map(data.resources.map(getPair)),\n  props: new Map(data.props.map(getPair)),\n  pages: data.pages,\n  breakpoints: new Map(data.breakpoints.map(getPair)),\n  styleSources: new Map(data.styleSources.map(getPair)),\n  styleSourceSelections: new Map(\n    data.styleSourceSelections.map((item) => [item.instanceId, item])\n  ),\n  styles: new Map(data.styles.map((item) => [getStyleDeclKey(item), item])),\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/menu/index.ts",
    "content": "export * from \"./menu\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/menu/menu-button.tsx",
    "content": "import {\n  css,\n  DropdownMenuTrigger,\n  rawTheme,\n  ToolbarButton,\n} from \"@webstudio-is/design-system\";\nimport { MenuIcon, WebstudioIcon } from \"@webstudio-is/icons\";\n\nconst size = rawTheme.spacing[9];\n\nconst containerTransformVar = \"--ws-menu-button-container-transform\";\n\nconst triggerStyle = css({\n  position: \"relative\",\n  [containerTransformVar]: `translateZ(-${size}) rotateY(0deg)`,\n  \"&[data-state=open], &:hover\": {\n    [containerTransformVar]: `translateZ(-${size}) rotateY(-90deg)`,\n  },\n});\n\nconst innerContainerStyle = css({\n  width: \"100%\",\n  height: \"100%\",\n  transformStyle: \"preserve-3d\",\n  transition: \"transform 200ms\",\n  transform: `var(${containerTransformVar})`,\n});\n\nconst faceStyle = css({\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  position: \"absolute\",\n  width: \"100%\",\n  height: \"100%\",\n  variants: {\n    front: {\n      true: {\n        transform: `rotateY(0deg) translateZ(${size})`,\n      },\n    },\n    back: {\n      true: {\n        transform: `rotateY(90deg) translateZ(${size})`,\n      },\n    },\n  },\n});\n\nexport const MenuButton = () => {\n  return (\n    <ToolbarButton asChild className={triggerStyle()} aria-label=\"Menu Button\">\n      <DropdownMenuTrigger>\n        <span className={innerContainerStyle()}>\n          <span className={faceStyle({ front: true })}>\n            <WebstudioIcon size={22} />\n          </span>\n          <span className={faceStyle({ back: true })}>\n            <MenuIcon size={22} />\n          </span>\n        </span>\n      </DropdownMenuTrigger>\n    </ToolbarButton>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/menu/menu.stories.tsx",
    "content": "import { Flex, StorySection, Text, Toolbar } from \"@webstudio-is/design-system\";\nimport { Menu } from \"./menu\";\nimport {\n  $authPermit,\n  $authToken,\n  $authTokenPermissions,\n  $builderMode,\n  $userPlanFeatures,\n} from \"~/shared/nano-states\";\n\nexport default {\n  title: \"Menu\",\n};\n\nconst OwnerDesignModeVariant = () => {\n  $builderMode.set(\"design\");\n  $authPermit.set(\"own\");\n  $authToken.set(undefined);\n  $authTokenPermissions.set({\n    canClone: true,\n    canCopy: true,\n    canPublish: true,\n  });\n  $userPlanFeatures.set({\n    ...$userPlanFeatures.get(),\n    allowContentMode: true,\n    purchases: [{ planName: \"Pro\" }],\n  });\n  return (\n    <Toolbar>\n      <Menu defaultOpen />\n    </Toolbar>\n  );\n};\n\nconst ViewerFreePlanVariant = () => {\n  $builderMode.set(\"content\");\n  $authPermit.set(\"view\");\n  $authToken.set(\"some-token\");\n  $authTokenPermissions.set({\n    canClone: false,\n    canCopy: false,\n    canPublish: false,\n  });\n  return (\n    <Toolbar>\n      <Menu defaultOpen />\n    </Toolbar>\n  );\n};\n\nconst AdminContentModeVariant = () => {\n  $builderMode.set(\"content\");\n  $authPermit.set(\"admin\");\n  $authToken.set(undefined);\n  $authTokenPermissions.set({\n    canClone: true,\n    canCopy: true,\n    canPublish: true,\n  });\n  $userPlanFeatures.set({\n    ...$userPlanFeatures.get(),\n    allowContentMode: true,\n    purchases: [{ planName: \"Pro\" }],\n  });\n  return (\n    <Toolbar>\n      <Menu defaultOpen />\n    </Toolbar>\n  );\n};\n\nexport const MenuStory = () => (\n  <StorySection title=\"Menu\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Owner design mode</Text>\n        <OwnerDesignModeVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Viewer free plan</Text>\n        <ViewerFreePlanVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Admin content mode</Text>\n        <AdminContentModeVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/menu/menu.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  theme,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuItemRightSlot,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuCheckboxItem,\n  DropdownMenuSeparator,\n  Tooltip,\n  Kbd,\n  menuItemCss,\n} from \"@webstudio-is/design-system\";\nimport {\n  $isCloneDialogOpen,\n  $isShareDialogOpen,\n  $publishDialog,\n  $remoteDialog,\n} from \"~/builder/shared/nano-states\";\nimport { cloneProjectUrl, dashboardUrl } from \"~/shared/router-utils\";\nimport {\n  $authPermit,\n  $authToken,\n  $authTokenPermissions,\n  $isDesignMode,\n  $userPlanFeatures,\n} from \"~/shared/nano-states\";\nimport { emitCommand } from \"~/builder/shared/commands\";\nimport { MenuButton } from \"./menu-button\";\nimport { $openProjectSettings } from \"~/shared/nano-states/project-settings\";\nimport { UpgradeIcon } from \"@webstudio-is/icons\";\nimport { getSetting, setSetting } from \"~/builder/shared/client-settings\";\nimport { help } from \"~/shared/help\";\n\nconst ViewMenuItem = () => {\n  const navigatorLayout = getSetting(\"navigatorLayout\");\n\n  return (\n    <DropdownMenuSub>\n      <DropdownMenuSubTrigger>View</DropdownMenuSubTrigger>\n      <DropdownMenuSubContent width=\"regular\">\n        <DropdownMenuCheckboxItem\n          checked={navigatorLayout === \"undocked\"}\n          onSelect={() => {\n            const setting =\n              navigatorLayout === \"undocked\" ? \"docked\" : \"undocked\";\n            setSetting(\"navigatorLayout\", setting);\n          }}\n        >\n          Undock navigator\n        </DropdownMenuCheckboxItem>\n      </DropdownMenuSubContent>\n    </DropdownMenuSub>\n  );\n};\n\nexport const Menu = ({ defaultOpen }: { defaultOpen?: boolean } = {}) => {\n  const userPlanFeatures = useStore($userPlanFeatures);\n  const hasPaidPlan = userPlanFeatures.purchases.length > 0;\n  const authPermit = useStore($authPermit);\n  const authTokenPermission = useStore($authTokenPermissions);\n  const authToken = useStore($authToken);\n  const isDesignMode = useStore($isDesignMode);\n\n  const isPublishEnabled = authPermit === \"own\" || authPermit === \"admin\";\n\n  const isShareEnabled = authPermit === \"own\";\n\n  const disabledPublishTooltipContent = isPublishEnabled\n    ? undefined\n    : \"Only owner or admin can publish projects\";\n\n  const disabledShareTooltipContent = isShareEnabled\n    ? undefined\n    : \"Only owner can share projects\";\n\n  // If authToken is defined, the user is not logged into the current project and must be redirected to the dashboard to clone the project.\n  const cloneIsExternal = authToken !== undefined;\n\n  return (\n    <DropdownMenu modal={false} defaultOpen={defaultOpen}>\n      <MenuButton />\n      <DropdownMenuContent sideOffset={4} collisionPadding={4} width=\"regular\">\n        <DropdownMenuItem\n          onSelect={() => {\n            window.location.href = dashboardUrl({ origin: window.origin });\n          }}\n        >\n          Dashboard\n        </DropdownMenuItem>\n        <Tooltip side=\"right\" content={undefined}>\n          <DropdownMenuItem\n            onSelect={() => {\n              $openProjectSettings.set(\"general\");\n            }}\n          >\n            Project settings\n          </DropdownMenuItem>\n        </Tooltip>\n        <DropdownMenuItem onSelect={() => emitCommand(\"openBreakpointsMenu\")}>\n          Breakpoints\n        </DropdownMenuItem>\n        <ViewMenuItem />\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onSelect={() => emitCommand(\"undo\")}>\n          Undo\n          <DropdownMenuItemRightSlot>\n            <Kbd value={[\"meta\", \"z\"]} />\n          </DropdownMenuItemRightSlot>\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => emitCommand(\"redo\")}>\n          Redo\n          <DropdownMenuItemRightSlot>\n            <Kbd value={[\"meta\", \"shift\", \"z\"]} />\n          </DropdownMenuItemRightSlot>\n        </DropdownMenuItem>\n        {/* https://github.com/webstudio-is/webstudio/issues/499\n\n          <DropdownMenuItem\n            onSelect={() => {\n              // TODO\n            }}\n          >\n            Copy\n            <DropdownMenuItemRightSlot><Kbd value={[\"meta\", \"c\"]} /></DropdownMenuItemRightSlot>\n          </DropdownMenuItem>\n          <DropdownMenuItem\n            onSelect={() => {\n              // TODO\n            }}\n          >\n            Paste\n            <DropdownMenuItemRightSlot><Kbd value={[\"meta\", \"v\"]} /></DropdownMenuItemRightSlot>\n          </DropdownMenuItem>\n\n          */}\n        <DropdownMenuItem onSelect={() => emitCommand(\"deleteInstanceBuilder\")}>\n          Delete\n          <DropdownMenuItemRightSlot>\n            <Kbd value={[\"backspace\"]} />\n          </DropdownMenuItemRightSlot>\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => emitCommand(\"save\")}>\n          Save\n          <DropdownMenuItemRightSlot>\n            <Kbd value={[\"meta\", \"s\"]} />\n          </DropdownMenuItemRightSlot>\n        </DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onSelect={() => emitCommand(\"togglePreviewMode\")}>\n          Preview\n          <DropdownMenuItemRightSlot>\n            <Kbd value={[\"meta\", \"shift\", \"p\"]} />\n          </DropdownMenuItemRightSlot>\n        </DropdownMenuItem>\n\n        <Tooltip\n          side=\"right\"\n          sideOffset={10}\n          content={disabledShareTooltipContent}\n        >\n          <DropdownMenuItem\n            onSelect={() => {\n              $isShareDialogOpen.set(true);\n            }}\n            disabled={isShareEnabled === false}\n          >\n            Share\n          </DropdownMenuItem>\n        </Tooltip>\n\n        <Tooltip\n          side=\"right\"\n          sideOffset={10}\n          content={disabledPublishTooltipContent}\n        >\n          <DropdownMenuItem\n            onSelect={() => {\n              $publishDialog.set(\"publish\");\n            }}\n            disabled={isPublishEnabled === false}\n          >\n            Publish\n            <DropdownMenuItemRightSlot>\n              <Kbd value={[\"shift\", \"P\"]} />\n            </DropdownMenuItemRightSlot>\n          </DropdownMenuItem>\n        </Tooltip>\n\n        <Tooltip\n          side=\"right\"\n          sideOffset={10}\n          content={disabledPublishTooltipContent}\n        >\n          <DropdownMenuItem\n            onSelect={() => {\n              $publishDialog.set(\"export\");\n            }}\n            disabled={isPublishEnabled === false}\n          >\n            Export\n            <DropdownMenuItemRightSlot>\n              <Kbd value={[\"shift\", \"E\"]} />\n            </DropdownMenuItemRightSlot>\n          </DropdownMenuItem>\n        </Tooltip>\n\n        <Tooltip\n          side=\"right\"\n          sideOffset={10}\n          content={\n            authTokenPermission.canClone === false\n              ? \"Cloning has been disabled by the project owner\"\n              : undefined\n          }\n        >\n          <DropdownMenuItem\n            onSelect={() => {\n              if ($authToken.get() === undefined) {\n                $isCloneDialogOpen.set(true);\n                return;\n              }\n            }}\n            disabled={authTokenPermission.canClone === false}\n            asChild={cloneIsExternal}\n          >\n            {cloneIsExternal ? (\n              <a\n                className={menuItemCss()}\n                href={cloneProjectUrl({\n                  origin: window.origin,\n                  sourceAuthToken: authToken,\n                })}\n              >\n                Clone\n              </a>\n            ) : (\n              \"Clone\"\n            )}\n          </DropdownMenuItem>\n        </Tooltip>\n\n        <DropdownMenuSeparator />\n\n        {isDesignMode && (\n          <DropdownMenuItem onSelect={() => emitCommand(\"openCommandPanel\")}>\n            Search & commands\n            <DropdownMenuItemRightSlot>\n              <Kbd value={[\"meta\", \"k\"]} />\n            </DropdownMenuItemRightSlot>\n          </DropdownMenuItem>\n        )}\n\n        <DropdownMenuItem onSelect={() => emitCommand(\"openKeyboardShortcuts\")}>\n          Keyboard shortcuts\n          <DropdownMenuItemRightSlot>\n            <Kbd value={[\"shift\", \"?\"]} />\n          </DropdownMenuItemRightSlot>\n        </DropdownMenuItem>\n\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger>Help</DropdownMenuSubTrigger>\n          <DropdownMenuSubContent width=\"regular\">\n            {help.map((item) => (\n              <DropdownMenuItem\n                key={item.url}\n                onSelect={(event) => {\n                  if (\"target\" in item && item.target === \"embed\") {\n                    event.preventDefault();\n                    $remoteDialog.set({\n                      title: item.label,\n                      url: item.url,\n                    });\n                    return;\n                  }\n                  window.open(item.url);\n                }}\n              >\n                {item.label}\n              </DropdownMenuItem>\n            ))}\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n\n        {hasPaidPlan === false && (\n          <>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem\n              onSelect={() => {\n                window.open(\"https://webstudio.is/pricing\");\n              }}\n              css={{ gap: theme.spacing[3] }}\n            >\n              <UpgradeIcon />\n              <div>Upgrade to Pro</div>\n            </DropdownMenuItem>\n          </>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/navigator/css-preview.tsx",
    "content": "import { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  ScrollArea,\n  css,\n  textVariants,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { generateStyleMap, mergeStyles } from \"@webstudio-is/css-engine\";\nimport type { StyleMap } from \"@webstudio-is/css-engine\";\nimport { CollapsibleSection } from \"~/builder/shared/collapsible-section\";\nimport { highlightCss } from \"~/shared/code-highlight\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { $computedStyleDeclarations } from \"~/builder/features/style-panel/shared/model\";\nimport { $selectedInstance } from \"~/shared/awareness\";\n\nconst preStyle = css(textVariants.mono, {\n  margin: 0,\n  height: theme.spacing[24],\n  userSelect: \"text\",\n  cursor: \"text\",\n});\n\n// - Compiles a CSS string from the style engine\n// - Groups by category and separates categories with comments\nconst getCssText = (\n  definedComputedStyles: ComputedStyleDecl[],\n  instanceId: string\n) => {\n  const sourceStyles: StyleMap = new Map();\n  const cascadedStyles: StyleMap = new Map();\n  const presetStyles: StyleMap = new Map();\n\n  // Aggregate styles by category so we can group them when rendering.\n  for (const styleDecl of definedComputedStyles) {\n    let group;\n    if (\n      styleDecl.source.name === \"local\" ||\n      styleDecl.source.name === \"overwritten\"\n    ) {\n      group = sourceStyles;\n    }\n    if (styleDecl.source.name === \"remote\") {\n      group = cascadedStyles;\n    }\n    if (styleDecl.source.name === \"preset\") {\n      group = presetStyles;\n    }\n    if (group) {\n      if (styleDecl.source.instanceId === instanceId) {\n        group.set(styleDecl.property, styleDecl.cascadedValue);\n      }\n    }\n  }\n\n  const result: Array<string> = [];\n\n  const add = (comment: string, style: StyleMap) => {\n    if (style.size === 0) {\n      return;\n    }\n    result.push(`/* ${comment} */`);\n    result.push(generateStyleMap(mergeStyles(style)));\n  };\n\n  add(\"Style sources\", sourceStyles);\n  add(\"Cascaded\", cascadedStyles);\n  add(\"Preset\", presetStyles);\n\n  return result.join(\"\\n\");\n};\n\nconst $highlightedCss = computed(\n  [$selectedInstance, $computedStyleDeclarations],\n  (instance, computedStyleDeclarations) => {\n    if (instance === undefined) {\n      return;\n    }\n    const cssText = getCssText(computedStyleDeclarations, instance.id);\n    return highlightCss(cssText);\n  }\n);\n\n/**\n * Will be deleted soon in favor of advanced panel as soon as it has ability to select.\n * @deprecated\n */\nexport const CssPreview = () => {\n  const code = useStore($highlightedCss);\n  if (code === undefined) {\n    return null;\n  }\n  return (\n    <CollapsibleSection label=\"CSS preview\" fullWidth>\n      <ScrollArea css={{ padding: theme.panel.padding }}>\n        <pre tabIndex={0} className={preStyle()}>\n          <div\n            style={{ whiteSpace: \"break-spaces\" }}\n            dangerouslySetInnerHTML={{ __html: code }}\n          ></div>\n        </pre>\n      </ScrollArea>\n    </CollapsibleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/navigator/index.ts",
    "content": "export * from \"./navigator\";\nexport * from \"./navigator-tree\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/navigator/navigator-tree.tsx",
    "content": "import { useEffect, useRef } from \"react\";\nimport { atom, computed } from \"nanostores\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Box,\n  keyframes,\n  rawTheme,\n  ScrollArea,\n  SmallIconButton,\n  styled,\n  Text,\n  theme,\n  toast,\n  Tooltip,\n  TreeNode,\n  TreeNodeLabel,\n  TreeRoot,\n  TreeSortableItem,\n  type TreeDropTarget,\n} from \"@webstudio-is/design-system\";\nimport { showAttribute, getCollectionEntries } from \"@webstudio-is/react-sdk\";\nimport {\n  ROOT_INSTANCE_ID,\n  collectionComponent,\n  blockComponent,\n  rootComponent,\n  blockTemplateComponent,\n  descendantComponent,\n  type Instance,\n} from \"@webstudio-is/sdk\";\nimport { animationCanPlayOnCanvasProperty } from \"@webstudio-is/sdk/runtime\";\nimport {\n  EyeClosedIcon,\n  EyeOpenIcon,\n  InfoCircleIcon,\n} from \"@webstudio-is/icons\";\nimport {\n  $dragAndDropState,\n  $blockChildOutline,\n  $editingItemSelector,\n  $hoveredInstanceSelector,\n  $instances,\n  $isContentMode,\n  $props,\n  $propsIndex,\n  $propValuesByInstanceSelector,\n  $registeredComponentMetas,\n  $selectedInstanceSelector,\n  getIndexedInstanceId,\n  type ItemDropTarget,\n  $propValuesByInstanceSelectorWithMemoryProps,\n} from \"~/shared/nano-states\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { reparentInstance, toggleInstanceShow } from \"~/shared/instance-utils\";\nimport { emitCommand } from \"~/builder/shared/commands\";\nimport { useContentEditable } from \"~/shared/dom-hooks\";\nimport {\n  $selectedPage,\n  getInstanceKey,\n  selectInstance,\n} from \"~/shared/awareness\";\nimport {\n  findClosestContainer,\n  isRichTextContent,\n  isTreeSatisfyingContentModel,\n} from \"~/shared/content-model\";\nimport {\n  getInstanceLabel,\n  InstanceIcon,\n} from \"~/builder/shared/instance-label\";\nimport { InstanceContextMenu } from \"~/builder/shared/instance-context-menu\";\n\ntype TreeItemAncestor =\n  | undefined\n  | {\n      selector: InstanceSelector;\n      indexWithinChildren: number;\n      component: string;\n    };\n\ntype TreeItem = {\n  selector: InstanceSelector;\n  visibleAncestors: TreeItemAncestor[];\n  instance: Instance;\n  isExpanded: undefined | boolean;\n  isLastChild: boolean;\n  isHidden: boolean;\n  isReusable: boolean;\n  dropTarget?: TreeDropTarget;\n};\n\nconst $expandedItems = atom(new Set<string>());\n\nconst $dropTarget = computed(\n  $dragAndDropState,\n  (dragAndDropState) => dragAndDropState.dropTarget\n);\n\nexport const $flatTree = computed(\n  [\n    $selectedPage,\n    $instances,\n    $expandedItems,\n    $propValuesByInstanceSelector,\n    $dropTarget,\n    $isContentMode,\n  ],\n  (\n    page,\n    instances,\n    expandedItems,\n    propValuesByInstanceSelector,\n    dropTarget,\n    isContentMode\n  ) => {\n    const flatTree: TreeItem[] = [];\n    if (page === undefined) {\n      return flatTree;\n    }\n    const traverse = (\n      instanceId: Instance[\"id\"],\n      selector: InstanceSelector,\n      visibleAncestors: TreeItemAncestor[] = [],\n      isParentHidden = false,\n      isParentReusable = false,\n      isLastChild = false,\n      indexWithinChildren = 0\n    ) => {\n      const instance = instances.get(instanceId);\n      if (instance === undefined) {\n        // log instead of failing navigator tree\n        console.error(`Unknown instance ${instanceId}`);\n        return;\n      }\n      const propValues = propValuesByInstanceSelector.get(\n        getInstanceKey(selector)\n      );\n      const isHidden =\n        isParentHidden ||\n        false === Boolean(propValues?.get(showAttribute) ?? true);\n      const isReusable = isParentReusable || instance.component === \"Slot\";\n      const treeItem: TreeItem = {\n        selector,\n        visibleAncestors,\n        instance,\n        isExpanded: undefined,\n        isLastChild,\n        isHidden,\n        isReusable,\n      };\n      let isVisible = true;\n      // slot fragment component is not rendered in navigator tree\n      // so should be always expanded\n      if (instance.component === \"Fragment\") {\n        isVisible = false;\n      }\n      if (isContentMode) {\n        // hide everything outside of block instances\n        const hasBlockAncestor = visibleAncestors.some(\n          (ancestor) => ancestor?.component === blockComponent\n        );\n        if (\n          instance.component !== blockComponent &&\n          hasBlockAncestor === false\n        ) {\n          isVisible = false;\n        }\n        // though hide block template along with all descendants\n        if (instance.component === blockTemplateComponent) {\n          return treeItem;\n        }\n      }\n      let lastItem = treeItem;\n      if (isVisible) {\n        const ancestor = {\n          selector,\n          indexWithinChildren,\n          component: instance.component,\n        };\n        visibleAncestors = [...visibleAncestors, ancestor];\n        treeItem.visibleAncestors = visibleAncestors;\n        flatTree.push(treeItem);\n      }\n      const level = treeItem.visibleAncestors.length - 1;\n      if (level > 0 && instance.children.some((child) => child.type === \"id\")) {\n        treeItem.isExpanded = expandedItems.has(selector.join());\n      }\n      // always expand invisible items\n      if (isVisible === false) {\n        treeItem.isExpanded = true;\n      }\n\n      // render same children for each collection item in data\n      if (instance.component === collectionComponent && treeItem.isExpanded) {\n        const originalData = propValues?.get(\"data\");\n        if (originalData && instance.children.length > 0) {\n          const entries = getCollectionEntries(originalData);\n          if (entries.length > 0) {\n            entries.forEach(([key], entryIndex) => {\n              for (\n                let index = 0;\n                index < instance.children.length;\n                index += 1\n              ) {\n                const child = instance.children[index];\n                if (child.type === \"id\") {\n                  const isLastChild = index === instance.children.length - 1;\n                  const lastDescendentItem = traverse(\n                    child.value,\n                    [\n                      child.value,\n                      getIndexedInstanceId(instance.id, key),\n                      ...selector,\n                    ],\n                    visibleAncestors,\n                    isHidden,\n                    isReusable,\n                    isLastChild,\n                    instance.children.length * entryIndex + index\n                  );\n                  if (lastDescendentItem) {\n                    lastItem = lastDescendentItem;\n                  }\n                }\n              }\n            });\n          }\n        }\n      } else if (level === 0 || treeItem.isExpanded) {\n        for (let index = 0; index < instance.children.length; index += 1) {\n          const child = instance.children[index];\n          if (child.type === \"id\") {\n            const isLastChild = index === instance.children.length - 1;\n            const lastDescendentItem = traverse(\n              child.value,\n              [child.value, ...selector],\n              visibleAncestors,\n              isHidden,\n              isReusable,\n              isLastChild,\n              index\n            );\n            if (lastDescendentItem) {\n              lastItem = lastDescendentItem;\n            }\n          }\n        }\n      }\n\n      const parentSelector = treeItem.visibleAncestors.at(-2)?.selector;\n      if (\n        dropTarget &&\n        parentSelector?.join() === dropTarget.itemSelector.join() &&\n        dropTarget.placement.closestChildIndex === indexWithinChildren\n      ) {\n        if (dropTarget.placement.indexAdjustment === 0) {\n          treeItem.dropTarget = {\n            parentLevel: level - 1,\n            beforeLevel: level,\n          };\n        } else {\n          lastItem.dropTarget = {\n            parentLevel: level - 1,\n            afterLevel: level,\n          };\n        }\n      }\n\n      return lastItem;\n    };\n    traverse(page.rootInstanceId, [page.rootInstanceId]);\n    return flatTree;\n  }\n);\n\nconst handleExpand = (item: TreeItem, isExpanded: boolean, all: boolean) => {\n  const expandedItems = new Set($expandedItems.get());\n  const instances = $instances.get();\n  const traverse = (instanceId: Instance[\"id\"], selector: InstanceSelector) => {\n    const key = selector.join();\n    if (isExpanded) {\n      expandedItems.add(key);\n    } else {\n      expandedItems.delete(key);\n    }\n    const instance = instances.get(instanceId);\n    // expand all descendants as well when alt is pressed\n    if (all && instance) {\n      for (const child of instance.children) {\n        traverse(child.value, [child.value, ...selector]);\n      }\n    }\n  };\n  traverse(item.instance.id, item.selector);\n  $expandedItems.set(expandedItems);\n};\n\nconst pulse = keyframes({\n  \"0%\": { fillOpacity: 0 },\n  \"100%\": { fillOpacity: 1 },\n});\n\nconst AnimatedEyeOpenIcon = styled(EyeOpenIcon, {\n  \"& .ws-eye-open-pupil\": {\n    transformOrigin: \"center\",\n    animation: `${pulse} 1.5s ease-in-out infinite alternate`,\n    fill: \"currentColor\",\n  },\n});\n\nconst ShowToggle = ({\n  instance,\n  value,\n  isAnimating,\n}: {\n  instance: Instance;\n  value: boolean;\n  isAnimating: boolean;\n}) => {\n  // descendant component is not actually rendered\n  // but affects styling of nested elements\n  // hiding descendant does not hide nested elements and confuse users\n  if (instance.component === descendantComponent) {\n    return;\n  }\n  const toggleShow = () => {\n    toggleInstanceShow(instance.id);\n  };\n\n  const EyeIcon = isAnimating ? AnimatedEyeOpenIcon : EyeOpenIcon;\n\n  return (\n    <Tooltip\n      // If you are changing it, change the other one too\n      content={\n        <Text>\n          Removes the instance from the DOM. Breakpoints have no effect on this\n          setting.\n          {isAnimating && value && (\n            <>\n              <br />\n              <Text css={{ color: theme.colors.foregroundPrimary }}>\n                Animation is running on canvas.\n              </Text>\n            </>\n          )}\n        </Text>\n      }\n      disableHoverableContent\n      variant=\"wrapped\"\n    >\n      <SmallIconButton\n        css={\n          value && isAnimating\n            ? {\n                color: theme.colors.foregroundPrimary,\n                \"&:hover\": {\n                  color: theme.colors.foregroundPrimary,\n                  filter: \"brightness(80%)\",\n                },\n              }\n            : undefined\n        }\n        tabIndex={-1}\n        aria-label=\"Show\"\n        onClick={toggleShow}\n        icon={value ? <EyeIcon /> : <EyeClosedIcon />}\n      />\n    </Tooltip>\n  );\n};\n\nconst EditableTreeNodeLabel = styled(\"div\", {\n  variants: {\n    isEditing: {\n      true: {\n        background: theme.colors.backgroundControls,\n        padding: theme.spacing[3],\n        borderRadius: theme.spacing[3],\n        color: theme.colors.foregroundMain,\n        outline: \"none\",\n        cursor: \"auto\",\n        textOverflow: \"clip\",\n        userSelect: \"text\",\n      },\n    },\n  },\n});\n\nconst TreeNodeContent = ({\n  instance,\n  isEditing,\n  onIsEditingChange,\n}: {\n  instance: Instance;\n  isEditing: boolean;\n  onIsEditingChange: (isEditing: boolean) => void;\n}) => {\n  const editableRef = useRef<HTMLDivElement | null>(null);\n\n  const label = getInstanceLabel(instance);\n  const { ref, handlers } = useContentEditable({\n    value: label,\n    isEditable: true,\n    isEditing,\n    onChangeValue: (value: string) => {\n      const instanceId = instance.id;\n      serverSyncStore.createTransaction([$instances], (instances) => {\n        const instance = instances.get(instanceId);\n        if (instance) {\n          instance.label = value;\n        }\n      });\n      editableRef.current?.closest(\"button\")?.focus();\n    },\n    onChangeEditing: onIsEditingChange,\n  });\n\n  return (\n    <TreeNodeLabel prefix={<InstanceIcon instance={instance} />}>\n      <EditableTreeNodeLabel\n        ref={mergeRefs(editableRef, ref)}\n        {...handlers}\n        isEditing={isEditing}\n      >\n        {label}\n      </EditableTreeNodeLabel>\n    </TreeNodeLabel>\n  );\n};\n\nconst getBuilderDropTarget = (\n  item: TreeItem,\n  treeDropTarget: undefined | TreeDropTarget\n): undefined | ItemDropTarget => {\n  if (treeDropTarget === undefined) {\n    return;\n  }\n  const parentSelector =\n    item.visibleAncestors[treeDropTarget.parentLevel]?.selector;\n  if (parentSelector === undefined) {\n    return;\n  }\n  const beforeItem = item.visibleAncestors[treeDropTarget.beforeLevel ?? -1];\n  const afterItem = item.visibleAncestors[treeDropTarget.afterLevel ?? -1];\n  let closestChildIndex = 0;\n  let indexAdjustment = 0;\n  if (beforeItem) {\n    closestChildIndex = beforeItem.indexWithinChildren;\n    indexAdjustment = 0;\n  } else if (afterItem) {\n    closestChildIndex = afterItem.indexWithinChildren;\n    indexAdjustment = 1;\n  }\n  // first position is always reserved for templates in block component\n  const instances = $instances.get();\n  const parentInstance = instances.get(parentSelector[0]);\n  if (parentInstance?.component === blockComponent) {\n    // adjust position to show indicator before second child\n    // because templates indicator is not rendered in content mode\n    if (closestChildIndex === 0) {\n      closestChildIndex = 1;\n      indexAdjustment = 0;\n    }\n  }\n  const indexWithinChildren = closestChildIndex + indexAdjustment;\n  return {\n    itemSelector: parentSelector,\n    indexWithinChildren,\n    placement: {\n      closestChildIndex,\n      indexAdjustment,\n      childrenOrientation: { type: \"vertical\", reverse: false },\n    },\n  };\n};\n\nconst canDrag = (instance: Instance, instanceSelector: InstanceSelector) => {\n  // forbid moving root instance\n  if (instanceSelector.length === 1) {\n    return false;\n  }\n\n  // Do not drag if the instance name is being edited\n  if ($editingItemSelector.get()?.join(\",\") === instanceSelector.join(\",\")) {\n    return false;\n  }\n\n  if ($isContentMode.get()) {\n    const parentId = instanceSelector[1];\n    const parentInstance = $instances.get().get(parentId);\n    if (parentInstance === undefined) {\n      return false;\n    }\n    if (parentInstance.component !== blockComponent) {\n      return false;\n    }\n  }\n  // prevent moving block template out of first position\n  if (instance.component === blockTemplateComponent) {\n    return false;\n  }\n\n  const isContent = isRichTextContent({\n    instanceSelector,\n    instances: $instances.get(),\n    props: $props.get(),\n    metas: $registeredComponentMetas.get(),\n  });\n  if (isContent) {\n    toast.error(\n      \"This instance can not be moved outside of its parent component.\"\n    );\n  }\n  return !isContent;\n};\n\nconst canDrop = (\n  dragSelector: InstanceSelector,\n  dropTarget: ItemDropTarget\n) => {\n  const dropSelector = dropTarget.itemSelector;\n  const instances = $instances.get();\n  const props = $props.get();\n  const metas = $registeredComponentMetas.get();\n  // in content mode allow drop only within same block\n  if ($isContentMode.get()) {\n    const parentInstance = instances.get(dropSelector[0]);\n    if (parentInstance?.component !== blockComponent) {\n      return false;\n    }\n    // parent of dragging item should be the same as drop target\n    if (dropSelector[0] !== dragSelector[1]) {\n      return false;\n    }\n  }\n  // prevent dropping into non-container instances\n  const containerSelector = findClosestContainer({\n    metas,\n    props,\n    instances,\n    instanceSelector: dropSelector,\n  });\n  if (dropSelector.length !== containerSelector.length) {\n    return false;\n  }\n  // make sure dragging tree can be put inside of drop instance\n  const containerInstanceSelector = [dragSelector[0], ...dropSelector];\n  const matches = isTreeSatisfyingContentModel({\n    instances,\n    metas,\n    props,\n    instanceSelector: containerInstanceSelector,\n  });\n  return matches;\n};\n\nexport const NavigatorTree = () => {\n  const isContentMode = useStore($isContentMode);\n  const flatTree = useStore($flatTree);\n  const selectedInstanceSelector = useStore($selectedInstanceSelector);\n  const selectedKey = selectedInstanceSelector?.join();\n  const hoveredInstanceSelector = useStore($hoveredInstanceSelector);\n  const hoveredKey = hoveredInstanceSelector?.join();\n  const propValuesByInstanceSelectorWithMemoryProps = useStore(\n    $propValuesByInstanceSelectorWithMemoryProps\n  );\n  const { propsByInstanceId } = useStore($propsIndex);\n\n  const metas = useStore($registeredComponentMetas);\n  const editingItemSelector = useStore($editingItemSelector);\n  const dragAndDropState = useStore($dragAndDropState);\n  const dropTargetKey = dragAndDropState.dropTarget?.itemSelector.join();\n  const rootMeta = metas.get(rootComponent);\n\n  // expand selected instance ancestors\n  useEffect(() => {\n    if (selectedInstanceSelector) {\n      const newExpandedItems = new Set($expandedItems.get());\n      let expanded = 0;\n      // do not expand the selected instance itself, start with parent\n      for (let index = 1; index < selectedInstanceSelector.length; index += 1) {\n        const key = selectedInstanceSelector.slice(index).join();\n        if (newExpandedItems.has(key) === false) {\n          newExpandedItems.add(key);\n          expanded += 1;\n        }\n      }\n      // prevent rerender if nothing new is expanded\n      if (expanded > 0) {\n        $expandedItems.set(newExpandedItems);\n      }\n    }\n  }, [selectedInstanceSelector]);\n\n  const selectInstanceAndClearSelection = (\n    instanceSelector: undefined | Instance[\"id\"][],\n    event: React.MouseEvent | React.FocusEvent\n  ) => {\n    if (event.currentTarget.querySelector(\"[contenteditable]\") === null) {\n      // Allow text selection and edits inside current TreeNode\n      // Outside if text is selected, it needs to be unselected before selecting the instance.\n      // Otherwise user will cmd+c the text instead of copying the instance.\n      window.getSelection()?.removeAllRanges();\n    }\n\n    selectInstance(instanceSelector);\n  };\n\n  return (\n    <ScrollArea\n      direction=\"both\"\n      css={{\n        width: \"100%\",\n        overflow: \"hidden\",\n        flexBasis: 0,\n        flexGrow: 1,\n      }}\n    >\n      <TreeRoot>\n        {rootMeta && isContentMode === false && (\n          <TreeNode\n            level={0}\n            isSelected={selectedKey === ROOT_INSTANCE_ID}\n            buttonProps={{\n              onClick: (event) =>\n                selectInstanceAndClearSelection([ROOT_INSTANCE_ID], event),\n              onFocus: (event) =>\n                selectInstanceAndClearSelection([ROOT_INSTANCE_ID], event),\n            }}\n            action={\n              <Tooltip\n                variant=\"wrapped\"\n                side=\"bottom\"\n                disableHoverableContent={true}\n                content={\n                  <Text>\n                    Variables defined on Global root are available on every\n                    instance on every page.\n                  </Text>\n                }\n              >\n                <InfoCircleIcon />\n              </Tooltip>\n            }\n          >\n            <TreeNodeLabel\n              prefix={<InstanceIcon instance={{ component: rootComponent }} />}\n            >\n              {rootMeta.label}\n            </TreeNodeLabel>\n          </TreeNode>\n        )}\n\n        {flatTree.map((item) => {\n          const level = item.visibleAncestors.length - 1;\n          const key = item.selector.join();\n          const propValues = propValuesByInstanceSelectorWithMemoryProps.get(\n            getInstanceKey(item.selector)\n          );\n          const show = Boolean(propValues?.get(showAttribute) ?? true);\n\n          // Hook memory prop\n          const isAnimationSelected =\n            propValues?.get(animationCanPlayOnCanvasProperty) === true;\n\n          const props = propsByInstanceId.get(item.instance.id);\n          const actionProp = props?.find(\n            (prop) => prop.type === \"animationAction\"\n          );\n\n          const isAnimationPinned = actionProp?.value?.isPinned === true;\n\n          const isAnimating = isAnimationSelected || isAnimationPinned;\n\n          const meta = metas.get(item.instance.component);\n\n          if (meta === undefined) {\n            return;\n          }\n\n          return (\n            <TreeSortableItem\n              key={key}\n              level={level}\n              isExpanded={item.isExpanded}\n              isLastChild={item.isLastChild}\n              data={item}\n              canDrag={() => canDrag(item.instance, item.selector)}\n              dropTarget={item.dropTarget}\n              onDropTargetChange={(dropTarget, draggingItem) => {\n                const builderDropTarget = getBuilderDropTarget(\n                  item,\n                  dropTarget\n                );\n                if (\n                  builderDropTarget &&\n                  canDrop(draggingItem.selector, builderDropTarget)\n                ) {\n                  $dragAndDropState.set({\n                    ...$dragAndDropState.get(),\n                    isDragging: true,\n                    dragPayload: {\n                      origin: \"panel\",\n                      type: \"reparent\",\n                      dragInstanceSelector: draggingItem.selector,\n                    },\n                    dropTarget: builderDropTarget,\n                  });\n                } else {\n                  $dragAndDropState.set({\n                    ...$dragAndDropState.get(),\n                    isDragging: false,\n                    dropTarget: undefined,\n                  });\n                }\n              }}\n              onDrop={(data) => {\n                const builderDropTarget = $dragAndDropState.get().dropTarget;\n                if (builderDropTarget) {\n                  reparentInstance(data.selector, {\n                    parentSelector: builderDropTarget.itemSelector,\n                    position: builderDropTarget.indexWithinChildren,\n                  });\n                }\n                $dragAndDropState.set({ isDragging: false });\n              }}\n              onExpand={(isExpanded) => handleExpand(item, isExpanded, false)}\n            >\n              <InstanceContextMenu>\n                <TreeNode\n                  level={level}\n                  isSelected={selectedKey === key}\n                  isHighlighted={hoveredKey === key || dropTargetKey === key}\n                  isExpanded={item.isExpanded}\n                  isActionVisible={isAnimating}\n                  onExpand={(isExpanded, all) =>\n                    handleExpand(item, isExpanded, all)\n                  }\n                  nodeProps={{\n                    style: {\n                      opacity: item.isHidden ? 0.4 : undefined,\n                      color: item.isReusable\n                        ? rawTheme.colors.foregroundReusable\n                        : undefined,\n                    },\n                  }}\n                  buttonProps={{\n                    onMouseEnter: () => {\n                      $hoveredInstanceSelector.set(item.selector);\n                      $blockChildOutline.set(undefined);\n                    },\n                    onMouseLeave: () => $hoveredInstanceSelector.set(undefined),\n                    onClick: (event) =>\n                      selectInstanceAndClearSelection(item.selector, event),\n                    onFocus: (event) =>\n                      selectInstanceAndClearSelection(item.selector, event),\n                    onKeyDown: (event) => {\n                      if (event.key === \"Enter\") {\n                        emitCommand(\"editInstanceText\");\n                      }\n                    },\n                  }}\n                  action={\n                    <ShowToggle\n                      instance={item.instance}\n                      value={show}\n                      isAnimating={isAnimating}\n                    />\n                  }\n                >\n                  <TreeNodeContent\n                    instance={item.instance}\n                    isEditing={\n                      item.selector.join() === editingItemSelector?.join()\n                    }\n                    onIsEditingChange={(isEditing) => {\n                      $editingItemSelector.set(\n                        isEditing === true ? item.selector : undefined\n                      );\n                    }}\n                  />\n                </TreeNode>\n              </InstanceContextMenu>\n            </TreeSortableItem>\n          );\n        })}\n      </TreeRoot>\n      {/* space in the end of scroll area */}\n      <Box css={{ height: theme.spacing[9] }}></Box>\n    </ScrollArea>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/navigator/navigator.tsx",
    "content": "import { Flex, PanelTitle, Separator } from \"@webstudio-is/design-system\";\nimport { CssPreview } from \"./css-preview\";\nimport { NavigatorTree } from \"./navigator-tree\";\nimport { $isDesignMode } from \"~/shared/nano-states\";\nimport { useStore } from \"@nanostores/react\";\nimport { InstanceContextMenu } from \"~/builder/shared/instance-context-menu\";\n\nexport const NavigatorPanel = (_props: { onClose: () => void }) => {\n  const isDesignMode = useStore($isDesignMode);\n  return (\n    <>\n      <PanelTitle>Navigator</PanelTitle>\n      <Separator />\n      <InstanceContextMenu>\n        <Flex grow direction=\"column\" justify=\"end\">\n          <NavigatorTree />\n        </Flex>\n      </InstanceContextMenu>\n      <Separator />\n      {isDesignMode && <CssPreview />}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/confirmation-dialogs.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport {\n  DeletePageConfirmationDialog,\n  DeleteFolderConfirmationDialog,\n} from \"./confirmation-dialogs\";\n\nexport default {\n  title: \"Builder/Pages/Confirmation dialogs\",\n};\n\nexport const DeletePage = () => (\n  <StorySection title=\"Delete Page\">\n    <DeletePageConfirmationDialog\n      page={{\n        id: \"page-1\",\n        name: \"About us\",\n        path: \"/about\",\n        title: \"About Us\",\n        rootInstanceId: \"root-1\",\n        systemDataSourceId: \"ds-1\",\n        meta: {},\n      }}\n      onClose={() => {}}\n      onConfirm={() => {}}\n    />\n  </StorySection>\n);\n\nexport const DeleteFolder = () => (\n  <StorySection title=\"Delete Folder\">\n    <DeleteFolderConfirmationDialog\n      folder={{\n        id: \"folder-1\",\n        name: \"Marketing\",\n        slug: \"marketing\",\n        children: [],\n      }}\n      onClose={() => {}}\n      onConfirm={() => {}}\n    />\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/confirmation-dialogs.tsx",
    "content": "import {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n  Flex,\n  Text,\n  Button,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport type { Page, Folder } from \"@webstudio-is/sdk\";\n\ntype DeletePageConfirmationDialogProps = {\n  onClose: () => void;\n  onConfirm: () => void;\n  page: Page;\n};\n\nexport const DeletePageConfirmationDialog = ({\n  onClose,\n  onConfirm,\n  page,\n}: DeletePageConfirmationDialogProps) => {\n  return (\n    <Dialog\n      open\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent>\n        <DialogTitle>Delete page</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Text>{`Are you sure you want to delete \"${page.name}\"?`}</Text>\n          <Text>\n            You can undo it even if you delete the page as long as you don't\n            reload.\n          </Text>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <DialogClose>\n              <Button\n                color=\"destructive\"\n                onClick={() => {\n                  onConfirm();\n                }}\n              >\n                Delete Page\n              </Button>\n            </DialogClose>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ntype DeleteFolderConfirmationDialogProps = {\n  onClose: () => void;\n  onConfirm: () => void;\n  folder: Folder;\n};\n\nexport const DeleteFolderConfirmationDialog = ({\n  onClose,\n  onConfirm,\n  folder,\n}: DeleteFolderConfirmationDialogProps) => {\n  return (\n    <Dialog\n      open\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent>\n        <DialogTitle>Delete confirmation</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Text>{`Delete folder \"${folder.name}\" including all of its pages?`}</Text>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <DialogClose>\n              <Button\n                color=\"destructive\"\n                onClick={() => {\n                  onConfirm();\n                }}\n              >\n                Delete\n              </Button>\n            </DialogClose>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/custom-metadata.stories.tsx",
    "content": "import { Box, StorySection } from \"@webstudio-is/design-system\";\nimport { CustomMetadata as CustomMetadataComponent } from \"./custom-metadata\";\nimport { useState } from \"react\";\n\nexport default {\n  component: CustomMetadataComponent,\n  title: \"Pages/Custom Metadata\",\n};\n\nexport const CustomMetadata = () => {\n  const [customMetas, setCustomMetas] = useState([\n    {\n      property: \"og:title\",\n      content: \"My title\",\n    },\n  ]);\n\n  return (\n    <StorySection title=\"Custom Metadata\">\n      <Box css={{ width: 448, margin: 20 }}>\n        <CustomMetadataComponent\n          customMetas={customMetas}\n          onChange={setCustomMetas}\n        />\n      </Box>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/custom-metadata.tsx",
    "content": "import { useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  Grid,\n  InputErrorsTooltip,\n  InputField,\n  Label,\n  SmallIconButton,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { TrashIcon, PlusIcon } from \"@webstudio-is/icons\";\nimport { isLiteralExpression } from \"@webstudio-is/sdk\";\nimport { computeExpression } from \"~/shared/data-variables\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport { $pageRootScope } from \"./page-utils\";\n\ntype Meta = {\n  property: string;\n  content: string;\n};\n\ntype CustomMetadataProps = {\n  customMetas: Meta[];\n  onChange: (value: Meta[]) => void;\n};\n\nconst MetadataItem = (props: {\n  property: string;\n  content: string;\n  onDelete: () => void;\n  onChange: (property: string, content: string) => void;\n}) => {\n  const propertyId = useId();\n  const contentId = useId();\n  const { variableValues, scope, aliases } = useStore($pageRootScope);\n\n  const content = computeExpression(props.content, variableValues);\n\n  return (\n    <Grid\n      gap={2}\n      css={{\n        gridTemplateColumns: `${theme.spacing[18]} 1fr 19px`,\n        gridTemplateAreas: `\n         \"property property-input button\"\n         \"content  content-input  button\"\n        `,\n      }}\n      align={\"center\"}\n    >\n      <Label htmlFor={propertyId} css={{ gridArea: \"property\" }}>\n        Property\n      </Label>\n      <InputErrorsTooltip errors={undefined}>\n        <InputField\n          css={{ gridArea: \"property-input\" }}\n          id={propertyId}\n          property=\"path\"\n          value={props.property}\n          onChange={(event) => {\n            props.onChange(event.target.value, props.content);\n          }}\n        />\n      </InputErrorsTooltip>\n      <Label htmlFor={contentId} css={{ gridArea: \"content\" }}>\n        Content\n      </Label>\n      <BindingControl>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(props.content) ? \"default\" : \"bound\"}\n          value={props.content}\n          onChange={(value) => {\n            props.onChange(props.property, value);\n          }}\n          onRemove={(evaluatedValue) => {\n            props.onChange(props.property, JSON.stringify(evaluatedValue));\n          }}\n        />\n        <InputErrorsTooltip errors={undefined}>\n          <InputField\n            css={{\n              gridArea: \"content-input\",\n            }}\n            disabled={isLiteralExpression(props.content) === false}\n            color={typeof content !== \"string\" ? \"error\" : undefined}\n            id={contentId}\n            property=\"path\"\n            value={content}\n            onChange={(event) => {\n              props.onChange(\n                props.property,\n                JSON.stringify(event.target.value)\n              );\n            }}\n          />\n        </InputErrorsTooltip>\n      </BindingControl>\n      <Grid\n        css={{\n          gridArea: \"button\",\n          justifyItems: \"center\",\n          gap: \"2px\",\n          color: theme.colors.foregroundIconSecondary,\n        }}\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"19\"\n          height=\"11\"\n          viewBox=\"0 0 19 11\"\n          fill=\"currentColor\"\n        >\n          <path d=\"M10 10.05V6.05005C10 2.73634 7.31371 0.0500488 4 0.0500488H0V1.05005H4C6.76142 1.05005 9 3.28863 9 6.05005V10.05H10Z\" />\n        </svg>\n\n        <SmallIconButton\n          variant=\"destructive\"\n          icon={<TrashIcon />}\n          onClick={props.onDelete}\n        />\n\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"19\"\n          height=\"11\"\n          viewBox=\"0 0 19 11\"\n          fill=\"currentColor\"\n        >\n          <path d=\"M-4.37114e-07 10.05L4 10.05C7.31371 10.05 10 7.36376 10 4.05005L10 0.0500488L9 0.0500488L9 4.05005C9 6.81147 6.76142 9.05005 4 9.05005L-3.93402e-07 9.05005L-4.37114e-07 10.05Z\" />\n        </svg>\n      </Grid>\n    </Grid>\n  );\n};\n\nexport const CustomMetadata = (props: CustomMetadataProps) => {\n  return (\n    <Grid gap={2} css={{ my: theme.spacing[5], mx: theme.spacing[8] }}>\n      <Label text=\"title\">Custom metadata</Label>\n      <Text color=\"subtle\">\n        Use this section to input metadata for the document, which will be used\n        to generate{\" \"}\n        <Text as=\"b\" variant={\"regularBold\"}>\n          &lt;meta&gt;\n        </Text>{\" \"}\n        tags. Each pair consists of a{\" \"}\n        <Text as=\"b\" variant={\"regularBold\"}>\n          property\n        </Text>{\" \"}\n        attribute, indicating the type of metadata, and a{\" \"}\n        <Text as=\"b\" variant={\"regularBold\"}>\n          content\n        </Text>{\" \"}\n        attribute, specifying its value.\n      </Text>\n      <div />\n      <Grid gap={3}>\n        {props.customMetas.map((meta, index) => (\n          <MetadataItem\n            key={index}\n            property={meta.property}\n            content={meta.content}\n            onChange={(property, content) => {\n              const newCustomMetas = [...props.customMetas];\n              newCustomMetas[index] = { property, content };\n              props.onChange(newCustomMetas);\n            }}\n            onDelete={() => {\n              const newCustomMetas = [...props.customMetas];\n              newCustomMetas.splice(index, 1);\n              props.onChange(newCustomMetas);\n            }}\n          />\n        ))}\n\n        <Button\n          type=\"button\"\n          color=\"neutral\"\n          css={{\n            justifySelf: \"center\",\n          }}\n          prefix={<PlusIcon />}\n          onClick={() => {\n            const newCustomMetas = [\n              ...props.customMetas,\n              { property: \"\", content: `\"\"` },\n            ];\n            props.onChange(newCustomMetas);\n          }}\n        >\n          Add another metadata pair\n        </Button>\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/folder-settings.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  DialogClose,\n  DialogTitle,\n  Flex,\n  Grid,\n  InputErrorsTooltip,\n  InputField,\n  Label,\n  DialogTitleActions,\n  ScrollArea,\n  TitleSuffixSpacer,\n  Tooltip,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { InfoCircleIcon, TrashIcon, CopyIcon } from \"@webstudio-is/icons\";\nimport {\n  Folder,\n  Pages,\n  ROOT_FOLDER_ID,\n  findParentFolderByChildId,\n} from \"@webstudio-is/sdk\";\nimport { nanoid } from \"nanoid\";\nimport { useCallback, useState, type FocusEventHandler } from \"react\";\nimport slugify from \"slugify\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { z } from \"zod\";\nimport { useIds } from \"~/shared/form-utils\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport { useUnmount } from \"~/shared/hook-utils/use-mount\";\nimport { $pages } from \"~/shared/sync/data-stores\";\nimport { $isDesignMode } from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { Form } from \"./form\";\nimport { isSlugAvailable, registerFolderChildMutable } from \"./page-utils\";\n\nconst Values = Folder.pick({ name: true, slug: true }).extend({\n  parentFolderId: z.string(),\n});\n\ntype Values = z.infer<typeof Values>;\n\ntype FieldName = keyof Values;\n\ntype Errors = {\n  [fieldName in FieldName]?: string[];\n};\n\nconst fieldDefaultValues = {\n  name: \"Untitled\",\n  slug: \"untitled\",\n  parentFolderId: ROOT_FOLDER_ID,\n} satisfies Values;\n\nconst fieldNames = Object.keys(fieldDefaultValues) as Array<FieldName>;\n\nconst validateValues = (\n  pages: undefined | Pages,\n  values: Values,\n  folderId?: Folder[\"id\"]\n): Errors => {\n  const parsedResult = Values.safeParse(values);\n  const errors: Errors = {};\n  if (parsedResult.success === false) {\n    return parsedResult.error.formErrors.fieldErrors;\n  }\n  if (pages !== undefined && values.slug !== undefined) {\n    if (\n      isSlugAvailable(\n        values.slug,\n        pages.folders,\n        values.parentFolderId,\n        folderId\n      ) === false\n    ) {\n      errors.slug = errors.slug ?? [];\n      errors.slug.push(`Slug \"${values.slug}\" is already in use`);\n    }\n  }\n  return errors;\n};\n\nconst toFormValues = (\n  folderId: Folder[\"id\"],\n  folders: Array<Folder>\n): Values => {\n  const folder = folders.find(({ id }) => id === folderId);\n  const parentFolder = findParentFolderByChildId(folderId, folders);\n  return {\n    name: folder?.name ?? \"\",\n    slug: folder?.slug ?? \"\",\n    parentFolderId: parentFolder?.id ?? ROOT_FOLDER_ID,\n  };\n};\n\nconst autoSelectHandler: FocusEventHandler<HTMLInputElement> = (event) =>\n  event.target.select();\n\nconst FormFields = ({\n  disabled,\n  autoSelect,\n  errors,\n  values,\n  onChange,\n}: {\n  disabled?: boolean;\n  autoSelect?: boolean;\n  errors: Errors;\n  values: Values;\n  folderId: Folder[\"id\"];\n  onChange: (\n    event: {\n      [K in keyof Values]: {\n        field: K;\n        value: Values[K];\n      };\n    }[keyof Values]\n  ) => void;\n}) => {\n  const fieldIds = useIds(fieldNames);\n  const pages = useStore($pages);\n\n  if (pages === undefined) {\n    return;\n  }\n\n  return (\n    <Grid css={{ height: \"100%\" }}>\n      <ScrollArea>\n        <Grid gap={3} css={{ padding: theme.panel.padding }}>\n          <Grid gap={1}>\n            <Label htmlFor={fieldIds.name}>Folder name</Label>\n            <InputErrorsTooltip errors={errors.name}>\n              <InputField\n                tabIndex={1}\n                color={errors.name && \"error\"}\n                id={fieldIds.name}\n                autoFocus\n                onFocus={autoSelect ? autoSelectHandler : undefined}\n                name=\"name\"\n                placeholder=\"About\"\n                disabled={disabled}\n                value={values.name}\n                onChange={(event) => {\n                  onChange({ field: \"name\", value: event.target.value });\n                }}\n              />\n            </InputErrorsTooltip>\n          </Grid>\n\n          <Grid gap={1}>\n            <Flex align=\"center\" css={{ gap: theme.spacing[3] }}>\n              <Label htmlFor={fieldIds.slug}>Slug</Label>\n              <Tooltip\n                content={\"Slug will be used as part of the path to the page\"}\n                variant=\"wrapped\"\n              >\n                <InfoCircleIcon\n                  color={rawTheme.colors.foregroundSubtle}\n                  tabIndex={0}\n                />\n              </Tooltip>\n            </Flex>\n            <InputErrorsTooltip errors={errors.slug}>\n              <InputField\n                tabIndex={1}\n                color={errors.slug && \"error\"}\n                id={fieldIds.slug}\n                name=\"slug\"\n                placeholder=\"folder\"\n                disabled={disabled}\n                value={values?.slug}\n                onChange={(event) => {\n                  onChange({\n                    field: \"slug\",\n                    value: event.target.value,\n                  });\n                }}\n              />\n            </InputErrorsTooltip>\n          </Grid>\n        </Grid>\n      </ScrollArea>\n    </Grid>\n  );\n};\n\nconst nameToSlug = (name: string) => {\n  if (name === \"\") {\n    return \"\";\n  }\n\n  return slugify(name, { lower: true, strict: true });\n};\n\nexport const newFolderId = \"new-folder\";\n\nexport const NewFolderSettings = ({\n  onClose: _onClose,\n  onSuccess,\n  onRequestDelete,\n}: {\n  onClose: () => void;\n  onSuccess: (folderId: Folder[\"id\"]) => void;\n  onRequestDelete?: () => void;\n}) => {\n  const pages = useStore($pages);\n  const isDesignMode = useStore($isDesignMode);\n\n  const [values, setValues] = useState<Values>({\n    ...fieldDefaultValues,\n    slug: nameToSlug(fieldDefaultValues.name),\n  });\n\n  const errors = validateValues(pages, values);\n\n  const handleSubmit = () => {\n    if (Object.keys(errors).length === 0) {\n      const folderId = nanoid();\n      createFolder(folderId, values);\n      onSuccess(folderId);\n    }\n  };\n\n  const handleRequestDelete = () => {\n    if (onRequestDelete) {\n      onRequestDelete();\n    }\n  };\n\n  const isSubmitting = false;\n\n  return (\n    <>\n      <DialogTitle\n        suffix={\n          <DialogTitleActions>\n            {isDesignMode && onRequestDelete && (\n              <Tooltip content=\"Delete folder\" side=\"bottom\">\n                <Button\n                  color=\"ghost\"\n                  prefix={<TrashIcon />}\n                  onClick={handleRequestDelete}\n                  aria-label=\"Delete folder\"\n                  tabIndex={2}\n                />\n              </Tooltip>\n            )}\n            <TitleSuffixSpacer />\n            <Button\n              state={isSubmitting ? \"pending\" : \"auto\"}\n              onClick={handleSubmit}\n              tabIndex={2}\n            >\n              {isSubmitting ? \"Creating\" : \"Create folder\"}\n            </Button>\n            <DialogClose />\n          </DialogTitleActions>\n        }\n      >\n        New Folder Settings\n      </DialogTitle>\n      <Form onSubmit={handleSubmit}>\n        <FormFields\n          autoSelect\n          errors={errors}\n          disabled={false}\n          values={values}\n          folderId={newFolderId}\n          onChange={(value) => {\n            setValues((values) => {\n              const changes = { [value.field]: value.value };\n\n              if (value.field === \"name\") {\n                if (values.slug === nameToSlug(values.name)) {\n                  changes.slug = nameToSlug(value.value);\n                }\n              }\n              return { ...values, ...changes };\n            });\n          }}\n        />\n      </Form>\n    </>\n  );\n};\n\nconst createFolder = (folderId: Folder[\"id\"], values: Values) => {\n  serverSyncStore.createTransaction([$pages], (pages) => {\n    if (pages === undefined) {\n      return;\n    }\n    pages.folders.push({\n      id: folderId,\n      name: values.name,\n      slug: values.slug,\n      children: [],\n    } satisfies Folder);\n    const parentFolder = pages.folders.find(\n      ({ id }) => id === values.parentFolderId\n    );\n    parentFolder?.children.push(folderId);\n  });\n};\n\nconst updateFolder = (folderId: Folder[\"id\"], values: Partial<Values>) => {\n  serverSyncStore.createTransaction([$pages], (pages) => {\n    if (pages === undefined) {\n      return;\n    }\n    const folder = pages.folders.find((folder) => folder.id === folderId);\n    if (folder === undefined) {\n      return;\n    }\n    if (values.name !== undefined) {\n      folder.name = values.name;\n    }\n    if (values.slug !== undefined) {\n      folder.slug = values.slug;\n    }\n    if (values.parentFolderId !== undefined) {\n      registerFolderChildMutable(\n        pages.folders,\n        folderId,\n        values.parentFolderId\n      );\n    }\n  });\n};\n\nexport const FolderSettings = ({\n  onClose,\n  onRequestDelete,\n  onDuplicate,\n  folderId,\n}: {\n  onClose: () => void;\n  onRequestDelete?: () => void;\n  onDuplicate?: (newFolderId: string) => void;\n  folderId: string;\n}) => {\n  const pages = useStore($pages);\n  const folder = pages?.folders.find(({ id }) => id === folderId);\n  const [unsavedValues, setUnsavedValues] = useState<Partial<Values>>({});\n  const isDesignMode = useStore($isDesignMode);\n\n  const values: Values = {\n    ...(pages ? toFormValues(folderId, pages.folders) : fieldDefaultValues),\n    ...unsavedValues,\n  };\n\n  const errors = validateValues(pages, values, folderId);\n\n  const debouncedFn = useEffectEvent(() => {\n    if (\n      Object.keys(unsavedValues).length === 0 ||\n      Object.keys(errors).length !== 0\n    ) {\n      return;\n    }\n\n    updateFolder(folderId, unsavedValues);\n\n    setUnsavedValues({});\n  });\n\n  const handleSubmitDebounced = useDebouncedCallback(debouncedFn, 1000);\n\n  const handleChange = useCallback(\n    <Name extends FieldName>(event: { field: Name; value: Values[Name] }) => {\n      setUnsavedValues((values) => ({\n        ...values,\n        [event.field]: event.value,\n      }));\n      handleSubmitDebounced();\n    },\n    [handleSubmitDebounced]\n  );\n\n  useUnmount(() => {\n    if (\n      Object.keys(unsavedValues).length === 0 ||\n      Object.keys(errors).length !== 0\n    ) {\n      return;\n    }\n    updateFolder(folderId, unsavedValues);\n  });\n\n  if (folder === undefined) {\n    return null;\n  }\n\n  const handleRequestDelete = () => {\n    if (onRequestDelete) {\n      onRequestDelete();\n    }\n  };\n\n  const handleDuplicate = () => {\n    if (onDuplicate) {\n      onDuplicate(folderId);\n    }\n  };\n\n  return (\n    <>\n      <DialogTitle\n        suffix={\n          <DialogTitleActions>\n            {isDesignMode && onRequestDelete && (\n              <Tooltip content=\"Delete folder\" side=\"bottom\">\n                <Button\n                  color=\"ghost\"\n                  prefix={<TrashIcon />}\n                  onClick={handleRequestDelete}\n                  aria-label=\"Delete folder\"\n                  tabIndex={2}\n                />\n              </Tooltip>\n            )}\n            {isDesignMode && onDuplicate && (\n              <Tooltip content=\"Duplicate folder\" side=\"bottom\">\n                <Button\n                  color=\"ghost\"\n                  prefix={<CopyIcon />}\n                  onClick={handleDuplicate}\n                  aria-label=\"Duplicate folder\"\n                  tabIndex={2}\n                />\n              </Tooltip>\n            )}\n            <DialogClose />\n          </DialogTitleActions>\n        }\n      >\n        Folder Settings\n      </DialogTitle>\n      <Form onSubmit={onClose}>\n        <FormFields\n          folderId={folderId}\n          errors={errors}\n          values={values}\n          onChange={handleChange}\n        />\n      </Form>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/form.stories.tsx",
    "content": "import {\n  Button,\n  Flex,\n  InputField,\n  StorySection,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { Form as FormComponent } from \"./form\";\n\nexport default {\n  title: \"Builder/Pages/Form\",\n  component: FormComponent,\n};\n\nexport const Form = () => (\n  <StorySection title=\"Form\">\n    <Flex direction=\"column\" gap=\"3\" css={{ width: 300, height: 200 }}>\n      <FormComponent onSubmit={() => window.alert(\"Submitted!\")}>\n        <Flex direction=\"column\" gap=\"3\" css={{ padding: 16 }}>\n          <Text variant=\"labels\">Page name</Text>\n          <InputField defaultValue=\"Home\" />\n          <Button type=\"submit\">Save</Button>\n        </Flex>\n      </FormComponent>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/form.tsx",
    "content": "import { forwardRef, type ReactNode } from \"react\";\n\nexport const Form = forwardRef<\n  HTMLFormElement,\n  { onSubmit: () => void; children: ReactNode }\n>(({ onSubmit, children }, ref) => {\n  return (\n    <form\n      ref={ref}\n      onSubmit={(event) => {\n        event.preventDefault();\n        onSubmit();\n      }}\n      style={{\n        overflow: \"auto\",\n        flexGrow: 1,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      {children}\n      <input type=\"submit\" hidden />\n    </form>\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/image-info.stories.tsx",
    "content": "import { Flex, StorySection, Text } from \"@webstudio-is/design-system\";\nimport { ImageInfo as ImageInfoComponent } from \"./image-info\";\nimport type { ImageAsset } from \"@webstudio-is/sdk\";\n\nexport default {\n  title: \"Builder/Pages/Image Info\",\n  component: ImageInfoComponent,\n};\n\nconst mockAsset: ImageAsset = {\n  id: \"asset-1\",\n  projectId: \"project-1\",\n  size: 204800,\n  name: \"hero-banner.jpg\",\n  filename: \"hero-banner.jpg\",\n  description: \"Hero banner image\",\n  createdAt: \"2025-01-01T00:00:00.000Z\",\n  format: \"jpeg\",\n  meta: { width: 1920, height: 1080 },\n  type: \"image\",\n};\n\nconst squareAsset: ImageAsset = {\n  id: \"asset-2\",\n  projectId: \"project-1\",\n  size: 102400,\n  name: \"avatar.png\",\n  filename: \"avatar.png\",\n  description: \"User avatar\",\n  createdAt: \"2025-06-15T00:00:00.000Z\",\n  format: \"png\",\n  meta: { width: 400, height: 400 },\n  type: \"image\",\n};\n\nexport const ImageInfo = () => (\n  <StorySection title=\"Image Info\">\n    <Flex direction=\"column\" gap=\"5\" css={{ width: 400 }}>\n      <Text variant=\"labels\">Landscape image</Text>\n      <ImageInfoComponent asset={mockAsset} onDelete={() => {}} />\n\n      <Text variant=\"labels\">Square image</Text>\n      <ImageInfoComponent asset={squareAsset} onDelete={() => {}} />\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/image-info.tsx",
    "content": "import {\n  IconButton,\n  Text,\n  Grid,\n  theme,\n  Flex,\n} from \"@webstudio-is/design-system\";\nimport {\n  AspectRatioIcon,\n  TrashIcon,\n  DimensionsIcon,\n  ImageIcon,\n} from \"@webstudio-is/icons\";\nimport type { ImageAsset } from \"@webstudio-is/sdk\";\nimport { formatAssetName } from \"~/builder/shared/assets/asset-utils\";\nimport { getFormattedAspectRatio } from \"~/builder/shared/asset-manager\";\n\ntype ImageInfoProps = {\n  asset: ImageAsset;\n  onDelete: () => void;\n};\n\nexport const ImageInfo = ({ asset, onDelete }: ImageInfoProps) => {\n  return (\n    <Grid gap={1} flow={\"column\"} align={\"center\"} justify={\"between\"}>\n      <Grid\n        gap={2}\n        flow={\"column\"}\n        align={\"center\"}\n        justify={\"start\"}\n        css={{\n          borderRadius: theme.borderRadius[4],\n          border: `1px solid ${theme.colors.borderMain}`,\n          backgroundColor: theme.colors.white,\n          padding: theme.spacing[4],\n          justifySelf: \"start\",\n          pr: theme.spacing[5],\n        }}\n      >\n        <Grid\n          columns={1}\n          css={{ padding: theme.panel.padding, width: theme.spacing[34] }}\n          gap={2}\n          align={\"center\"}\n        >\n          <Grid\n            gap={1}\n            align=\"center\"\n            css={{ gridTemplateColumns: \"max-content 1fr\" }}\n          >\n            <ImageIcon />\n            <Text truncate variant={\"labels\"}>\n              {formatAssetName(asset)}\n            </Text>\n          </Grid>\n          <Grid columns={2} gap={1} align={\"center\"}>\n            <Flex gap={1}>\n              <DimensionsIcon />\n              <Text variant={\"labels\"}>\n                {asset.meta.width} x {asset.meta.height} Px\n              </Text>\n            </Flex>\n            <Flex gap={1}>\n              <AspectRatioIcon />\n              <Text variant={\"labels\"}>\n                {getFormattedAspectRatio(asset.meta)}\n              </Text>\n            </Flex>\n          </Grid>\n        </Grid>\n      </Grid>\n      <IconButton onClick={onDelete}>\n        <TrashIcon />\n      </IconButton>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/index.ts",
    "content": "export * from \"./pages\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/page-context-menu.tsx",
    "content": "import { useState, type ReactNode } from \"react\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuTrigger,\n} from \"@webstudio-is/design-system\";\nimport { duplicatePage, duplicateFolder } from \"./page-utils\";\n\ntype PageContextMenuProps = {\n  children: ReactNode;\n  onRequestDeletePage: (pageId: string) => void;\n  onRequestDeleteFolder: (folderId: string) => void;\n};\n\nexport const PageContextMenu = ({\n  children,\n  onRequestDeletePage,\n  onRequestDeleteFolder,\n}: PageContextMenuProps) => {\n  const [pageId, setPageId] = useState<string | undefined>();\n  const [folderId, setFolderId] = useState<string | undefined>();\n\n  const handleDuplicate = () => {\n    if (pageId) {\n      duplicatePage(pageId);\n    } else if (folderId) {\n      duplicateFolder(folderId);\n    }\n  };\n\n  return (\n    <>\n      <ContextMenu\n        onOpenChange={(open) => {\n          if (!open) {\n            setPageId(undefined);\n            setFolderId(undefined);\n          }\n        }}\n      >\n        <ContextMenuTrigger\n          asChild\n          onPointerDown={(event) => {\n            if (!(event.target instanceof HTMLElement)) {\n              return;\n            }\n            const button =\n              event.target.closest<HTMLElement>(\"[data-tree-button]\");\n            const pageId = button?.getAttribute(\"data-page-id\");\n            const folderId = button?.getAttribute(\"data-folder-id\");\n            if (pageId) {\n              setPageId(pageId);\n              setFolderId(undefined);\n            } else if (folderId) {\n              setFolderId(folderId);\n              setPageId(undefined);\n            }\n          }}\n        >\n          {children}\n        </ContextMenuTrigger>\n        <ContextMenuContent>\n          <ContextMenuItem\n            onSelect={handleDuplicate}\n            disabled={!pageId && !folderId}\n          >\n            Duplicate\n          </ContextMenuItem>\n          <ContextMenuItem\n            onSelect={() => {\n              if (pageId) {\n                onRequestDeletePage(pageId);\n              } else if (folderId) {\n                onRequestDeleteFolder(folderId);\n              }\n            }}\n            destructive\n            disabled={!pageId && !folderId}\n          >\n            Delete\n          </ContextMenuItem>\n        </ContextMenuContent>\n      </ContextMenu>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/page-settings.stories.tsx",
    "content": "import { $pages } from \"~/shared/nano-states/pages\";\nimport { PageSettings as PageSettingsComponent } from \"./page-settings\";\nimport {\n  Grid,\n  theme,\n  Dialog,\n  DialogContent,\n  StorySection,\n} from \"@webstudio-is/design-system\";\nimport { $assets, $project } from \"~/shared/sync/data-stores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { isRootFolder } from \"@webstudio-is/sdk\";\n\nexport default {\n  title: \"Pages/Page Settings\",\n  component: PageSettingsComponent,\n  parameters: {\n    lostpixel: {\n      // this is to fix cutting off the after scroll area in the screenshot\n      waitBeforeScreenshot: 3000,\n    },\n  },\n};\n\n$assets.set(\n  new Map([\n    [\n      \"imageId\",\n      {\n        id: \"imageId\",\n        type: \"image\",\n        name: \"very-very-very-long-long-image-name.jpg\",\n        format: \"jpg\",\n        size: 100,\n        meta: {\n          width: 2 * 191,\n          height: 2 * 100,\n        },\n        projectId: \"projectId\",\n        createdAt: `${new Date()}`,\n        description: \"image-description\",\n      },\n    ],\n  ])\n);\n\nconst pages = createDefaultPages({ rootInstanceId: \"root-instance-id\" });\npages.meta = {\n  siteName: \"Project name\",\n  faviconAssetId: \"imageId\",\n  code: \"code\",\n};\npages.pages.push({\n  id: \"pageId\",\n  title: \"Page title\",\n  path: \"/page-path\",\n  name: \"page-name\",\n  meta: {},\n  rootInstanceId: \"root-instance-id\",\n});\nconst rootFolder = pages.folders.find(isRootFolder);\nrootFolder?.children.push(\"pageId\");\n\n$pages.set(pages);\n\n$project.set({\n  id: \"projectId\",\n  title: \"Project title\",\n  createdAt: `${new Date()}`,\n  isDeleted: false,\n  userId: \"userId\",\n  domain: \"new-2x9tcd\",\n  tags: [],\n\n  marketplaceApprovalStatus: \"UNLISTED\",\n\n  latestStaticBuild: null,\n  previewImageAssetId: null,\n  previewImageAsset: {\n    projectId: \"projectId\",\n    id: \"imageId\",\n    name: \"very-very-very-long-long-image-name.jpg\",\n    filename: null,\n    description: null,\n  },\n  latestBuildVirtual: null,\n  domainsVirtual: [],\n});\n\nexport const PageSettings = () => {\n  return (\n    <StorySection title=\"Page Settings\">\n      <Dialog open>\n        <DialogContent>\n          <Grid\n            css={{\n              width: theme.spacing[35],\n              margin: \"auto\",\n              border: `1px solid ${theme.colors.borderMain}`,\n              boxShadow: theme.shadows.menuDropShadow,\n              background: theme.colors.backgroundPanel,\n              borderRadius: theme.borderRadius[4],\n            }}\n          >\n            <PageSettingsComponent\n              onClose={() => {}}\n              onDuplicate={() => {}}\n              onDelete={() => {}}\n              pageId=\"pageId\"\n            />\n          </Grid>\n        </DialogContent>\n      </Dialog>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/page-settings.tsx",
    "content": "import { nanoid } from \"nanoid\";\nimport { z } from \"zod\";\nimport {\n  type FocusEventHandler,\n  useState,\n  useCallback,\n  useId,\n  useEffect,\n  type JSX,\n  useRef,\n} from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport * as bcp47 from \"bcp-47\";\nimport slugify from \"slugify\";\nimport {\n  type Page,\n  type Pages,\n  PageName,\n  HomePagePath,\n  PageTitle,\n  PagePath,\n  Folder,\n  getPagePath,\n  findPageByIdOrPath,\n  ROOT_FOLDER_ID,\n  findParentFolderByChildId,\n  ProjectNewRedirectPath,\n  isLiteralExpression,\n  documentTypes,\n  isRootFolder,\n  elementComponent,\n} from \"@webstudio-is/sdk\";\nimport {\n  theme,\n  Button,\n  Box,\n  Label,\n  TextArea,\n  InputErrorsTooltip,\n  Tooltip,\n  InputField,\n  Grid,\n  Checkbox,\n  Separator,\n  Text,\n  ScrollArea,\n  rawTheme,\n  Flex,\n  Select,\n  Link,\n  buttonStyle,\n  PanelBanner,\n  css,\n  Switch,\n  TitleSuffixSpacer,\n  ProBadge,\n  DialogClose,\n  DialogTitle,\n  DialogTitleActions,\n} from \"@webstudio-is/design-system\";\nimport {\n  CopyIcon,\n  TrashIcon,\n  HomeIcon,\n  InfoCircleIcon,\n  UploadIcon,\n} from \"@webstudio-is/icons\";\nimport { useIds } from \"~/shared/form-utils\";\nimport {\n  $assets,\n  $instances,\n  $pages,\n  $publishedOrigin,\n  $project,\n  $userPlanFeatures,\n  $isDesignMode,\n} from \"~/shared/nano-states\";\nimport { $openProjectSettings } from \"~/shared/nano-states/project-settings\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\n// @todo should be moved to shared because features should not depend on features\nimport { ImageControl } from \"~/shared/project-settings\";\n// @todo should be moved to shared because features should not depend on features\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport {\n  compilePathnamePattern,\n  tokenizePathnamePattern,\n  validatePathnamePattern,\n} from \"~/builder/shared/url-pattern\";\nimport { useUnmount } from \"~/shared/hook-utils/use-mount\";\nimport { selectInstance } from \"~/shared/awareness\";\nimport { computeExpression } from \"~/shared/data-variables\";\nimport { $currentSystem } from \"~/shared/system\";\nimport { Card } from \"../marketplace/card\";\nimport { ImageInfo } from \"./image-info\";\nimport { SearchPreview } from \"./search-preview\";\nimport { SocialPreview } from \"./social-preview\";\nimport {\n  registerFolderChildMutable,\n  $pageRootScope,\n  duplicatePage,\n  isPathAvailable,\n} from \"./page-utils\";\nimport { Form } from \"./form\";\nimport { CustomMetadata } from \"./custom-metadata\";\nimport { findMatchingRedirect } from \"~/shared/project-settings/utils\";\nimport {\n  LOOP_ERROR,\n  wouldCreateLoop,\n} from \"~/shared/redirects/redirect-loop-detection\";\n\nconst fieldDefaultValues = {\n  name: \"Untitled\",\n  parentFolderId: ROOT_FOLDER_ID,\n  path: \"/untitled\",\n  isHomePage: false,\n  title: `\"Untitled\"`,\n  description: `\"\"`,\n  excludePageFromSearch: `true`,\n  language: `\"\"`,\n  socialImageUrl: `\"\"`,\n  socialImageAssetId: \"\",\n  status: undefined as string | undefined,\n  redirect: `\"\"`,\n  documentType: \"html\" as (typeof documentTypes)[number],\n  customMetas: [{ property: \"\", content: `\"\"` }],\n  marketplaceInclude: false,\n  marketplaceCategory: \"\",\n  marketplaceThumbnailAssetId: \"\",\n};\n\nconst fieldNames = Object.keys(\n  fieldDefaultValues\n) as (keyof typeof fieldDefaultValues)[];\n\ntype FieldName = (typeof fieldNames)[number];\n\ntype Values = typeof fieldDefaultValues;\n\ntype OnChange = (\n  event: {\n    [K in keyof Values]: {\n      field: K;\n      value: Values[K];\n    };\n  }[keyof Values]\n) => void;\n\ntype Errors = {\n  [fieldName in FieldName]?: string[];\n};\n\nconst EmptyString = z.string().refine((string) => string === \"\");\n\n// 2xx, 3xx, 4xx, 5xx\nconst statusRegex = /^[2345]\\d\\d$/;\nconst Status = z\n  .number()\n  .refine(\n    (value) => statusRegex.test(String(value)),\n    \"Status code expects 2xx, 3xx, 4xx or 5xx\"\n  );\n\nconst Language = z\n  .string()\n  .refine(\n    (value) => bcp47.parse(value).language !== null,\n    \"The language is invalid\"\n  );\n\nconst SharedPageValues = z.object({\n  name: PageName,\n  title: PageTitle,\n  description: z.string().optional(),\n  excludePageFromSearch: z.boolean().optional(),\n  language: Language.or(EmptyString),\n  socialImageUrl: z.string().optional(),\n  status: Status.optional(),\n  redirect: z.optional(ProjectNewRedirectPath.or(EmptyString)),\n  documentType: z.optional(z.enum(documentTypes)),\n  customMetas: z\n    .array(\n      z.object({\n        property: z.string(),\n        content: z.string(),\n      })\n    )\n    .optional(),\n});\n\nconst HomePageValues = SharedPageValues.extend({\n  path: HomePagePath,\n});\n\nconst PageValues = SharedPageValues.extend({\n  path: PagePath,\n});\n\nconst validateValues = (\n  pages: undefined | Pages,\n  // undefined page id means new page\n  pageId: undefined | Page[\"id\"],\n  values: Values,\n  variableValues: Map<string, unknown>\n): Errors => {\n  // Null or undefined in the field value is only possible if it’s a dynamic (expression) value.\n  // We do not validate dynamic values but instead provide a default value for validation.\n  const excludeFromValidationDefault = \"exclude from validation\";\n\n  const computedValues = {\n    name: values.name,\n    path: values.path,\n    title:\n      computeExpression(values.title, variableValues) ??\n      excludeFromValidationDefault,\n    description: computeExpression(values.description, variableValues),\n    excludePageFromSearch: computeExpression(\n      values.excludePageFromSearch,\n      variableValues\n    ),\n    language: computeExpression(values.language, variableValues),\n    socialImageUrl: computeExpression(values.socialImageUrl, variableValues),\n    status: computeExpression(values.status ?? `undefined`, variableValues),\n    redirect: computeExpression(values.redirect, variableValues),\n    customMetas: values.customMetas.map((item) => ({\n      property: item.property,\n      content: computeExpression(item.content, variableValues),\n    })),\n  };\n\n  const Validator = values.isHomePage ? HomePageValues : PageValues;\n  const parsedResult = Validator.safeParse(computedValues);\n  const errors: Errors = {};\n  if (parsedResult.success === false) {\n    return parsedResult.error.formErrors.fieldErrors;\n  }\n  if (pages !== undefined && values.path !== undefined) {\n    if (\n      isPathAvailable({\n        pages,\n        path: values.path,\n        parentFolderId: values.parentFolderId,\n        pageId,\n      }) === false\n    ) {\n      errors.path = errors.path ?? [];\n      errors.path.push(\"All paths must be unique\");\n    }\n    const messages = validatePathnamePattern(values.path);\n    if (messages.length > 0) {\n      errors.path = errors.path ?? [];\n      errors.path.push(...messages);\n    }\n  }\n\n  // Validate redirect doesn't create a loop\n  if (\n    pages !== undefined &&\n    values.path !== undefined &&\n    computedValues.redirect &&\n    typeof computedValues.redirect === \"string\" &&\n    computedValues.redirect !== \"\"\n  ) {\n    const existingRedirects = pages.redirects ?? [];\n    if (\n      wouldCreateLoop(values.path, computedValues.redirect, existingRedirects)\n    ) {\n      errors.redirect = errors.redirect ?? [];\n      errors.redirect.push(LOOP_ERROR);\n    }\n  }\n\n  return errors;\n};\n\nconst toFormValues = (\n  page: Page,\n  pages: Pages,\n  isHomePage: boolean\n): Values => {\n  const parentFolder = findParentFolderByChildId(page.id, pages.folders);\n  return {\n    name: page.name,\n    parentFolderId: parentFolder?.id ?? ROOT_FOLDER_ID,\n    path: page.path,\n    title: page.title,\n    description: page.meta.description ?? fieldDefaultValues.description,\n    socialImageUrl:\n      page.meta.socialImageUrl ?? fieldDefaultValues.socialImageUrl,\n    socialImageAssetId:\n      page.meta.socialImageAssetId ?? fieldDefaultValues.socialImageAssetId,\n    excludePageFromSearch:\n      page.meta.excludePageFromSearch ??\n      fieldDefaultValues.excludePageFromSearch,\n    language: page.meta.language ?? fieldDefaultValues.language,\n    status: page.meta.status ?? fieldDefaultValues.status,\n    redirect: page.meta.redirect ?? fieldDefaultValues.redirect,\n    documentType: page.meta.documentType ?? fieldDefaultValues.documentType,\n    isHomePage,\n    customMetas: page.meta.custom ?? fieldDefaultValues.customMetas,\n    marketplaceInclude: page.marketplace?.include ?? false,\n    marketplaceCategory: page.marketplace?.category ?? \"\",\n    marketplaceThumbnailAssetId: page.marketplace?.thumbnailAssetId ?? \"\",\n  };\n};\n\nconst autoSelectHandler: FocusEventHandler<HTMLInputElement> = (event) =>\n  event.target.select();\n\nconst PathField = ({\n  errors,\n  value,\n  onChange,\n}: {\n  errors?: string[];\n  value: string;\n  onChange: (value: string) => void;\n}) => {\n  const { allowDynamicData } = useStore($userPlanFeatures);\n  const id = useId();\n  return (\n    <Grid gap={1}>\n      <Flex align=\"center\" gap={1}>\n        <Label htmlFor={id}>Path</Label>\n        {allowDynamicData === false && <ProBadge>PRO</ProBadge>}\n        <Tooltip\n          content={\n            <>\n              <Text>\n                The path can include dynamic parameters like :name, which could\n                be made optional using :name?, or have a wildcard such as /* or\n                /:name* to store whole remaining part at the end of the URL.\n              </Text>\n              {allowDynamicData === false && (\n                <>\n                  <br />\n                  <Text>\n                    To make the path dynamic and use it with CMS, you can use\n                    parameters and other features. CMS features are part of the\n                    Pro plan.\n                  </Text>\n                  <Link\n                    className={buttonStyle({ color: \"gradient\" })}\n                    css={{ marginTop: theme.spacing[5], width: \"100%\" }}\n                    color=\"contrast\"\n                    underline=\"none\"\n                    target=\"_blank\"\n                    href=\"https://webstudio.is/pricing\"\n                  >\n                    Upgrade\n                  </Link>\n                </>\n              )}\n            </>\n          }\n          variant=\"wrapped\"\n        >\n          <InfoCircleIcon\n            color={rawTheme.colors.foregroundSubtle}\n            tabIndex={-1}\n          />\n        </Tooltip>\n      </Flex>\n      <InputErrorsTooltip errors={errors}>\n        <InputField\n          color={errors && \"error\"}\n          id={id}\n          placeholder=\"/about\"\n          value={value}\n          onChange={(event) => onChange(event.target.value)}\n        />\n      </InputErrorsTooltip>\n    </Grid>\n  );\n};\n\nconst StatusField = ({\n  errors,\n  value = `undefined`,\n  onChange,\n}: {\n  errors?: string[];\n  value: undefined | string;\n  onChange: (value: undefined | string) => void;\n}) => {\n  const id = useId();\n  const { variableValues, scope, aliases } = useStore($pageRootScope);\n  return (\n    <Grid gap={1}>\n      <Flex align=\"center\" gap={1}>\n        <Label htmlFor={id}>Status code </Label>\n        <Tooltip\n          content={\n            <Text>\n              Status code value can be a{\" \"}\n              <Link\n                color=\"inherit\"\n                target=\"_blank\"\n                href=\"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status\"\n              >\n                HTTP Status\n              </Link>{\" \"}\n              number or an expression that returns the status code dynamic\n              response handling.\n            </Text>\n          }\n          variant=\"wrapped\"\n        >\n          <InfoCircleIcon\n            color={rawTheme.colors.foregroundSubtle}\n            tabIndex={-1}\n          />\n        </Tooltip>\n      </Flex>\n      <BindingControl>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(value) ? \"default\" : \"bound\"}\n          value={value}\n          onChange={onChange}\n          onRemove={(evaluatedValue) =>\n            onChange(JSON.stringify(evaluatedValue ?? \"\"))\n          }\n        />\n        <InputErrorsTooltip errors={errors}>\n          <InputField\n            inputMode=\"numeric\"\n            color={errors && \"error\"}\n            id={id}\n            placeholder=\"200\"\n            disabled={isLiteralExpression(value) === false}\n            value={String(computeExpression(value, variableValues) ?? \"\")}\n            onChange={(event) => {\n              if (event.target.value === \"\") {\n                onChange(undefined);\n              } else {\n                const number = Number(event.target.value);\n                const status =\n                  Number.isNaN(number) || String(number) !== event.target.value\n                    ? event.target.value\n                    : number;\n                onChange(JSON.stringify(status));\n              }\n            }}\n          />\n        </InputErrorsTooltip>\n      </BindingControl>\n    </Grid>\n  );\n};\n\nconst RedirectField = ({\n  errors,\n  value,\n  onChange,\n}: {\n  errors?: string[];\n  value: string;\n  onChange: (value: string) => void;\n}) => {\n  const id = useId();\n  const { allowDynamicData } = useStore($userPlanFeatures);\n  const { variableValues, scope, aliases } = useStore($pageRootScope);\n  return (\n    <Grid gap={1}>\n      <Flex align=\"center\" gap={1}>\n        <Label htmlFor={id}>Redirect </Label>\n        {allowDynamicData === false && <ProBadge>PRO</ProBadge>}\n        <Tooltip\n          content=\"Redirect value can be a path or an expression that returns a path for dynamic response handling.\"\n          variant=\"wrapped\"\n        >\n          <InfoCircleIcon\n            color={rawTheme.colors.foregroundSubtle}\n            tabIndex={-1}\n          />\n        </Tooltip>\n      </Flex>\n\n      <BindingControl>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(value) ? \"default\" : \"bound\"}\n          value={value}\n          onChange={onChange}\n          onRemove={(evaluatedValue) =>\n            onChange(JSON.stringify(evaluatedValue ?? \"\"))\n          }\n        />\n        <InputErrorsTooltip errors={errors}>\n          <InputField\n            color={errors && \"error\"}\n            id={id}\n            placeholder=\"/another-path\"\n            disabled={isLiteralExpression(value) === false}\n            value={String(computeExpression(value, variableValues))}\n            onChange={(event) => onChange(JSON.stringify(event.target.value))}\n          />\n        </InputErrorsTooltip>\n      </BindingControl>\n    </Grid>\n  );\n};\n\nconst LanguageField = ({\n  errors,\n  value,\n  onChange,\n}: {\n  errors?: string[];\n  value: string;\n  onChange: (value: string) => void;\n}) => {\n  const id = useId();\n  const { variableValues, scope, aliases } = useStore($pageRootScope);\n  return (\n    <Grid gap={1}>\n      <Label htmlFor={id}>Language</Label>\n      <BindingControl>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(value) ? \"default\" : \"bound\"}\n          value={value}\n          onChange={onChange}\n          onRemove={(evaluatedValue) =>\n            onChange(JSON.stringify(evaluatedValue ?? \"\"))\n          }\n        />\n        <InputErrorsTooltip errors={errors}>\n          <InputField\n            color={errors && \"error\"}\n            id={id}\n            placeholder=\"en-US\"\n            disabled={isLiteralExpression(value) === false}\n            value={String(computeExpression(value, variableValues))}\n            onChange={(event) => onChange(JSON.stringify(event.target.value))}\n          />\n        </InputErrorsTooltip>\n      </BindingControl>\n    </Grid>\n  );\n};\n\n/**\n * Compute the full page path from form values.\n * This combines folder path with page path, handling home page special case.\n */\nconst computePagePath = (values: Values, pages: Pages): string => {\n  if (values.isHomePage) {\n    return \"/\";\n  }\n  const foldersPath = getPagePath(values.parentFolderId, pages);\n  return [foldersPath, values.path]\n    .filter(Boolean)\n    .join(\"/\")\n    .replace(/\\/+/g, \"/\");\n};\n\nconst usePageUrl = (values: Values) => {\n  const pages = useStore($pages);\n  const path = pages === undefined ? \"\" : computePagePath(values, pages);\n\n  const system = useStore($currentSystem);\n  const publishedOrigin = useStore($publishedOrigin);\n  const tokens = tokenizePathnamePattern(path);\n  const compiledPath = compilePathnamePattern(tokens, system.params);\n  return `${publishedOrigin}${compiledPath}`;\n};\n\nconst fieldsetStyle = css({\n  all: \"unset\",\n  display: \"block\",\n  \"&:disabled\": {\n    opacity: 0.4,\n  },\n});\n\nconst MarketplaceSection = ({\n  values,\n  onChange,\n}: {\n  values: Values;\n  onChange: OnChange;\n}) => {\n  const excludeId = useId();\n  const categoryId = useId();\n  const categoryMeta = values.customMetas.find(\n    ({ property }) => property === \"ws:category\"\n  );\n  // @todo remove after all stores are migrated\n  const categoryFallback = String(\n    computeExpression(categoryMeta?.content ?? `\"\"`, new Map())\n  );\n  const category = values.marketplaceCategory ?? categoryFallback ?? \"Pages\";\n  const assets = useStore($assets);\n  const thumbnailAsset = assets.get(values.marketplaceThumbnailAssetId);\n  const thumnailFallbackAsset = assets.get(values.socialImageAssetId);\n  return (\n    <Grid gap={2} css={{ padding: theme.panel.padding }}>\n      <Label text=\"title\">Marketplace</Label>\n      <Grid\n        flow=\"column\"\n        gap={1}\n        justify=\"start\"\n        align=\"center\"\n        css={{ py: theme.spacing[2] }}\n      >\n        <Switch\n          id={excludeId}\n          checked={values.marketplaceInclude}\n          onCheckedChange={(value) =>\n            onChange({ field: \"marketplaceInclude\", value })\n          }\n        />\n        <Label htmlFor={excludeId}>Include in the marketplace</Label>\n      </Grid>\n      <Grid gap={1}>\n        <Label htmlFor={categoryId}>Category</Label>\n        <InputField\n          id={categoryId}\n          name=\"marketplaceCategory\"\n          value={values.marketplaceCategory}\n          onChange={(event) =>\n            onChange({\n              field: \"marketplaceCategory\",\n              value: event.target.value,\n            })\n          }\n        />\n      </Grid>\n      <Grid gap={1} flow=\"column\">\n        <ImageControl\n          onAssetIdChange={(value) =>\n            onChange({ field: \"marketplaceThumbnailAssetId\", value })\n          }\n        >\n          <Button color=\"neutral\" css={{ justifySelf: \"start\" }}>\n            Choose thumbnail from assets\n          </Button>\n        </ImageControl>\n      </Grid>\n      {thumbnailAsset?.type === \"image\" && (\n        <ImageInfo\n          asset={thumbnailAsset}\n          onDelete={() =>\n            onChange({ field: \"marketplaceThumbnailAssetId\", value: \"\" })\n          }\n        />\n      )}\n      <Grid gap={1}>\n        <Label>Marketplace preview</Label>\n        <Box\n          css={{\n            padding: theme.spacing[5],\n            borderRadius: theme.borderRadius[4],\n            border: `1px solid ${theme.colors.borderMain}`,\n            justifySelf: \"start\",\n          }}\n        >\n          <Grid gap={1} css={{ width: theme.spacing[30] }}>\n            {category && <Label text=\"title\">{category}</Label>}\n            <Card\n              title={values.name}\n              image={thumbnailAsset ?? thumnailFallbackAsset}\n            />\n          </Grid>\n        </Box>\n      </Grid>\n    </Grid>\n  );\n};\n\nconst FormFields = ({\n  autoSelect,\n  errors,\n  values,\n  onChange,\n}: {\n  autoSelect?: boolean;\n  errors: Errors;\n  values: Values;\n  onChange: OnChange;\n}) => {\n  const project = useStore($project);\n  const fieldIds = useIds(fieldNames);\n  const assets = useStore($assets);\n  const pages = useStore($pages);\n  const { allowDynamicData } = useStore($userPlanFeatures);\n  const { variableValues, scope, aliases } = useStore($pageRootScope);\n\n  const pageUrl = usePageUrl(values);\n\n  if (pages === undefined) {\n    return;\n  }\n\n  const socialImageAsset = assets.get(values.socialImageAssetId);\n  const faviconAsset = assets.get(pages.meta?.faviconAssetId ?? \"\");\n\n  const faviconUrl = faviconAsset?.type === \"image\" ? faviconAsset.name : \"\";\n\n  const title = String(computeExpression(values.title, variableValues));\n  const description = String(\n    computeExpression(values.description, variableValues)\n  );\n  const socialImageUrl = String(\n    computeExpression(values.socialImageUrl, variableValues)\n  );\n  const excludePageFromSearch = Boolean(\n    computeExpression(values.excludePageFromSearch, variableValues)\n  );\n\n  // Check if any redirect matches this page's path\n  const fullPagePath = computePagePath(values, pages);\n  const matchingRedirect = findMatchingRedirect(\n    fullPagePath,\n    pages.redirects ?? []\n  );\n\n  return (\n    <Grid css={{ height: \"100%\" }}>\n      <ScrollArea>\n        {matchingRedirect && (\n          <PanelBanner variant=\"warning\">\n            <Text>\n              A redirect from \"{matchingRedirect.old}\" will override this page.\n              The page will not be rendered when published.{\" \"}\n              <Link\n                color=\"inherit\"\n                underline=\"always\"\n                onClick={() => {\n                  $openProjectSettings.set(\"redirects\");\n                }}\n              >\n                Go to Redirects settings\n              </Link>\n            </Text>\n          </PanelBanner>\n        )}\n        {/**\n         * ----------------------========<<<Page props>>>>========----------------------\n         */}\n        <Grid gap={2} css={{ padding: theme.panel.padding }}>\n          <Grid gap={1}>\n            <Label htmlFor={fieldIds.name}>Page name</Label>\n            <InputErrorsTooltip errors={errors.name}>\n              <InputField\n                color={errors.name && \"error\"}\n                id={fieldIds.name}\n                autoFocus\n                onFocus={autoSelect ? autoSelectHandler : undefined}\n                name=\"name\"\n                placeholder=\"About\"\n                value={values.name}\n                onChange={(event) => {\n                  onChange({ field: \"name\", value: event.target.value });\n                }}\n              />\n            </InputErrorsTooltip>\n\n            <Grid flow={\"column\"} gap={1} justify={\"start\"} align={\"center\"}>\n              {values.isHomePage ? (\n                <>\n                  <HomeIcon />\n                  <Text\n                    css={{\n                      overflowWrap: \"anywhere\",\n                      wordBreak: \"break-all\",\n                      my: 2,\n                    }}\n                  >\n                    “{values.name}” is the home page\n                  </Text>\n                </>\n              ) : isRootFolder({ id: values.parentFolderId }) === false ? (\n                <>\n                  <HomeIcon color={rawTheme.colors.foregroundSubtle} />\n                  <Text\n                    css={{\n                      overflowWrap: \"anywhere\",\n                      wordBreak: \"break-all\",\n                      my: 2,\n                    }}\n                    color=\"subtle\"\n                  >\n                    Move this page to the “Root” folder to set it as your home\n                    page\n                  </Text>\n                </>\n              ) : values.documentType === \"xml\" ? (\n                <>\n                  <HomeIcon color={rawTheme.colors.foregroundSubtle} />\n                  <Text\n                    css={{\n                      overflowWrap: \"anywhere\",\n                      wordBreak: \"break-all\",\n                      my: 2,\n                    }}\n                    color=\"subtle\"\n                  >\n                    XML pages cannot be set as the home page\n                  </Text>\n                </>\n              ) : (\n                <>\n                  <Checkbox\n                    id={fieldIds.isHomePage}\n                    onCheckedChange={() => {\n                      onChange({ field: \"path\", value: \"\" });\n                      onChange({\n                        field: \"isHomePage\",\n                        value: !values.isHomePage,\n                      });\n                    }}\n                  />\n                  <Label\n                    css={{\n                      overflowWrap: \"anywhere\",\n                      wordBreak: \"break-all\",\n                    }}\n                    htmlFor={fieldIds.isHomePage}\n                  >\n                    Make “{values.name}” the home page\n                  </Label>\n                </>\n              )}\n            </Grid>\n          </Grid>\n\n          {values.isHomePage === false && (\n            <PathField\n              errors={errors.path}\n              value={values.path}\n              onChange={(value) => onChange({ field: \"path\", value })}\n            />\n          )}\n\n          <StatusField\n            errors={errors.status}\n            value={values.status}\n            onChange={(value) => onChange({ field: \"status\", value })}\n          />\n          <RedirectField\n            errors={errors.redirect}\n            value={values.redirect}\n            onChange={(value) => onChange({ field: \"redirect\", value })}\n          />\n          {allowDynamicData === false && (\n            <PanelBanner>\n              <Text>\n                Dynamic routing and redirect are a part of the CMS\n                functionality.\n              </Text>\n              <Flex align=\"center\" gap={1}>\n                <UploadIcon />\n                <Link\n                  color=\"inherit\"\n                  target=\"_blank\"\n                  href=\"https://webstudio.is/pricing\"\n                >\n                  Upgrade to Pro\n                </Link>\n              </Flex>\n            </PanelBanner>\n          )}\n\n          <Grid gap={1}>\n            <Label htmlFor={fieldIds.documentType}>Document type</Label>\n            <Select\n              options={documentTypes}\n              getValue={(docType: (typeof documentTypes)[number]) => docType}\n              getLabel={(docType: (typeof documentTypes)[number]) =>\n                docType.toLocaleUpperCase()\n              }\n              value={values.documentType}\n              disabled={values.isHomePage}\n              onChange={(value) => {\n                onChange({\n                  field: \"documentType\",\n                  value,\n                });\n              }}\n            />\n          </Grid>\n        </Grid>\n\n        <Separator />\n\n        {/**\n         * ----------------------========<<<Search Results>>>>========----------------------\n         */}\n        <fieldset\n          disabled={values.documentType === \"xml\"}\n          className={fieldsetStyle()}\n        >\n          <Grid gap={2} css={{ my: theme.spacing[5], mx: theme.spacing[8] }}>\n            <Grid gap={2}>\n              <Label text=\"title\">Search</Label>\n              <Text color=\"subtle\">\n                Optimize the way this page appears in search engine results\n                pages.\n              </Text>\n              <Grid gap={1}>\n                <Label>Search result preview</Label>\n                <Box\n                  css={{\n                    padding: theme.spacing[5],\n                    background: theme.colors.white,\n                    borderRadius: theme.borderRadius[4],\n                    border: `1px solid ${theme.colors.borderMain}`,\n                  }}\n                >\n                  <Box\n                    css={{\n                      transformOrigin: \"top left\",\n                      transform: \"scale(0.667)\",\n                      width: 600,\n                      height: 80,\n                    }}\n                  >\n                    <SearchPreview\n                      siteName={pages?.meta?.siteName ?? \"\"}\n                      faviconUrl={faviconUrl}\n                      pageUrl={pageUrl}\n                      titleLink={title}\n                      snippet={description}\n                    />\n                  </Box>\n                </Box>\n              </Grid>\n            </Grid>\n\n            <Grid gap={1}>\n              <Label htmlFor={fieldIds.title}>Title</Label>\n              <BindingControl>\n                <BindingPopover\n                  scope={scope}\n                  aliases={aliases}\n                  variant={\n                    isLiteralExpression(values.title) ? \"default\" : \"bound\"\n                  }\n                  value={values.title}\n                  onChange={(value) => {\n                    onChange({\n                      field: \"title\",\n                      value,\n                    });\n                  }}\n                  onRemove={(evaluatedValue) => {\n                    onChange({\n                      field: \"title\",\n                      value: JSON.stringify(evaluatedValue ?? \"\"),\n                    });\n                  }}\n                />\n                <InputErrorsTooltip errors={errors.title}>\n                  <InputField\n                    color={errors.title && \"error\"}\n                    id={fieldIds.title}\n                    name=\"title\"\n                    placeholder=\"My awesome project - About\"\n                    disabled={isLiteralExpression(values.title) === false}\n                    value={title}\n                    onChange={(event) => {\n                      onChange({\n                        field: \"title\",\n                        value: JSON.stringify(event.target.value),\n                      });\n                    }}\n                  />\n                </InputErrorsTooltip>\n              </BindingControl>\n            </Grid>\n\n            <Grid gap={1}>\n              <Label htmlFor={fieldIds.description}>Description</Label>\n              <BindingControl>\n                <BindingPopover\n                  scope={scope}\n                  aliases={aliases}\n                  variant={\n                    isLiteralExpression(values.description)\n                      ? \"default\"\n                      : \"bound\"\n                  }\n                  value={values.description}\n                  onChange={(value) => {\n                    onChange({\n                      field: \"description\",\n                      value,\n                    });\n                  }}\n                  onRemove={(evaluatedValue) => {\n                    onChange({\n                      field: \"description\",\n                      value: JSON.stringify(evaluatedValue ?? \"\"),\n                    });\n                  }}\n                />\n                <InputErrorsTooltip errors={errors.description}>\n                  <TextArea\n                    color={errors.description ? \"error\" : undefined}\n                    id={fieldIds.description}\n                    name=\"description\"\n                    disabled={isLiteralExpression(values.description) === false}\n                    value={description}\n                    onChange={(value) => {\n                      onChange({\n                        field: \"description\",\n                        value: JSON.stringify(value),\n                      });\n                    }}\n                    autoGrow\n                    maxRows={10}\n                  />\n                </InputErrorsTooltip>\n              </BindingControl>\n              <BindingControl>\n                <Grid\n                  flow={\"column\"}\n                  gap={1}\n                  justify={\"start\"}\n                  align={\"center\"}\n                  css={{ py: theme.spacing[2] }}\n                >\n                  <BindingPopover\n                    scope={scope}\n                    aliases={aliases}\n                    variant={\n                      isLiteralExpression(values.excludePageFromSearch)\n                        ? \"default\"\n                        : \"bound\"\n                    }\n                    value={values.excludePageFromSearch}\n                    onChange={(value) => {\n                      onChange({\n                        field: \"excludePageFromSearch\",\n                        value,\n                      });\n                    }}\n                    onRemove={(evaluatedValue) => {\n                      onChange({\n                        field: \"excludePageFromSearch\",\n                        value: JSON.stringify(evaluatedValue ?? \"\"),\n                      });\n                    }}\n                  />\n                  <Checkbox\n                    id={fieldIds.excludePageFromSearch}\n                    disabled={\n                      isLiteralExpression(values.excludePageFromSearch) ===\n                      false\n                    }\n                    checked={excludePageFromSearch}\n                    onCheckedChange={() => {\n                      const newValue = !excludePageFromSearch;\n                      onChange({\n                        field: \"excludePageFromSearch\",\n                        value: newValue.toString(),\n                      });\n                    }}\n                  />\n\n                  <InputErrorsTooltip errors={errors.excludePageFromSearch}>\n                    <Label htmlFor={fieldIds.excludePageFromSearch}>\n                      Exclude this page from search results\n                    </Label>\n                  </InputErrorsTooltip>\n                </Grid>\n              </BindingControl>\n            </Grid>\n\n            <LanguageField\n              errors={errors.language}\n              value={values.language}\n              onChange={(value) => onChange({ field: \"language\", value })}\n            />\n          </Grid>\n\n          <Separator />\n\n          {/**\n           * ----------------------========<<<Social Sharing>>>>========----------------------\n           */}\n          <Grid gap={2} css={{ my: theme.spacing[5], mx: theme.spacing[8] }}>\n            <Label htmlFor={fieldIds.socialImageAssetId} text=\"title\">\n              Social Image\n            </Label>\n            <Text color=\"subtle\">\n              This image appears when you share a link to this page on social\n              media sites. If no image is set here, the Social Image set in the\n              project settings will be used. The optimal dimensions for the\n              image are 1200x630 px or larger with a 1.91:1 aspect ratio.\n            </Text>\n            <BindingControl>\n              <BindingPopover\n                scope={scope}\n                aliases={aliases}\n                variant={\n                  isLiteralExpression(values.socialImageUrl)\n                    ? \"default\"\n                    : \"bound\"\n                }\n                value={values.socialImageUrl}\n                onChange={(value) => {\n                  onChange({\n                    field: \"socialImageUrl\",\n                    value,\n                  });\n                }}\n                onRemove={(evaluatedValue) => {\n                  onChange({\n                    field: \"socialImageUrl\",\n                    value: JSON.stringify(evaluatedValue ?? \"\"),\n                  });\n                }}\n              />\n              <InputErrorsTooltip errors={errors.socialImageUrl}>\n                <InputField\n                  placeholder=\"https://www.url.com\"\n                  disabled={\n                    isLiteralExpression(values.socialImageUrl) === false\n                  }\n                  color={errors.socialImageUrl && \"error\"}\n                  value={socialImageUrl}\n                  onChange={(event) => {\n                    onChange({\n                      field: \"socialImageUrl\",\n                      value: JSON.stringify(event.target.value),\n                    });\n                    onChange({ field: \"socialImageAssetId\", value: \"\" });\n                  }}\n                />\n              </InputErrorsTooltip>\n            </BindingControl>\n            <Grid gap={1} flow={\"column\"}>\n              <ImageControl\n                onAssetIdChange={(socialImageAssetId) => {\n                  onChange({\n                    field: \"socialImageAssetId\",\n                    value: socialImageAssetId,\n                  });\n                  onChange({ field: \"socialImageUrl\", value: \"\" });\n                }}\n              >\n                <Button\n                  id={fieldIds.socialImageAssetId}\n                  css={{ justifySelf: \"start\" }}\n                  color=\"neutral\"\n                >\n                  Choose Image From Assets\n                </Button>\n              </ImageControl>\n            </Grid>\n\n            {socialImageAsset?.type === \"image\" && (\n              <ImageInfo\n                asset={socialImageAsset}\n                onDelete={() => {\n                  onChange({\n                    field: \"socialImageAssetId\",\n                    value: \"\",\n                  });\n                }}\n              />\n            )}\n            <div />\n            <SocialPreview\n              ogImageUrl={\n                socialImageAsset?.type === \"image\"\n                  ? socialImageAsset.name\n                  : socialImageUrl\n              }\n              ogUrl={pageUrl}\n              ogTitle={title}\n              ogDescription={description}\n            />\n          </Grid>\n\n          <Separator />\n\n          <InputErrorsTooltip errors={errors.customMetas}>\n            <div>\n              <CustomMetadata\n                customMetas={values.customMetas}\n                onChange={(customMetas) => {\n                  onChange({\n                    field: \"customMetas\",\n                    value: customMetas,\n                  });\n                }}\n              />\n            </div>\n          </InputErrorsTooltip>\n        </fieldset>\n\n        {(project?.marketplaceApprovalStatus === \"PENDING\" ||\n          project?.marketplaceApprovalStatus === \"APPROVED\" ||\n          project?.marketplaceApprovalStatus === \"REJECTED\") && (\n          <>\n            <Separator />\n            <MarketplaceSection values={values} onChange={onChange} />\n          </>\n        )}\n\n        <Box css={{ height: theme.spacing[10] }} />\n      </ScrollArea>\n    </Grid>\n  );\n};\n\nconst nameToPath = (pages: Pages | undefined, name: string) => {\n  if (name === \"\") {\n    return \"\";\n  }\n\n  const slug = slugify(name, { lower: true, strict: true });\n  const path = `/${slug}`;\n\n  // for TypeScript\n  if (pages === undefined) {\n    return path;\n  }\n\n  if (findPageByIdOrPath(path, pages) === undefined) {\n    return path;\n  }\n\n  let suffix = 1;\n\n  while (findPageByIdOrPath(`${path}${suffix}`, pages) !== undefined) {\n    suffix++;\n  }\n\n  return `${path}${suffix}`;\n};\n\nexport const NewPageSettings = ({\n  onClose,\n  onSuccess,\n}: {\n  onClose: () => void;\n  onSuccess: (pageId: Page[\"id\"]) => void;\n}) => {\n  const pages = useStore($pages);\n\n  const [values, setValues] = useState<Values>({\n    ...fieldDefaultValues,\n    path: nameToPath(pages, fieldDefaultValues.name),\n  });\n  const { variableValues } = useStore($pageRootScope);\n  const errors = validateValues(pages, undefined, values, variableValues);\n\n  const handleSubmit = () => {\n    if (Object.keys(errors).length === 0) {\n      const pageId = nanoid();\n      createPage(pageId, values);\n      updatePage(pageId, values);\n      onSuccess(pageId);\n    }\n  };\n\n  return (\n    <NewPageSettingsView\n      onSubmit={handleSubmit}\n      onClose={onClose}\n      isSubmitting={false}\n    >\n      <FormFields\n        autoSelect\n        errors={errors}\n        values={values}\n        onChange={(val) => {\n          setValues((values) => {\n            const changes = { [val.field]: val.value };\n\n            if (val.field === \"name\") {\n              if (values.path === nameToPath(pages, values.name)) {\n                changes.path = nameToPath(pages, val.value);\n              }\n              if (values.title === values.name) {\n                changes.title = val.value;\n              }\n            }\n            return { ...values, ...changes };\n          });\n        }}\n      />\n    </NewPageSettingsView>\n  );\n};\n\nconst NewPageSettingsView = ({\n  onSubmit,\n  isSubmitting,\n  children,\n}: {\n  onSubmit: () => void;\n  isSubmitting: boolean;\n  onClose: () => void;\n  children: JSX.Element;\n}) => {\n  return (\n    <>\n      <DialogTitle\n        suffix={\n          <DialogTitleActions>\n            <TitleSuffixSpacer />\n            <Button\n              state={isSubmitting ? \"pending\" : \"auto\"}\n              onClick={onSubmit}\n              tabIndex={2}\n            >\n              {isSubmitting ? \"Creating\" : \"Create page\"}\n            </Button>\n            <DialogClose />\n          </DialogTitleActions>\n        }\n      >\n        New Page Settings\n      </DialogTitle>\n      <Form onSubmit={onSubmit}>{children}</Form>\n    </>\n  );\n};\n\nconst createPage = (pageId: Page[\"id\"], values: Values) => {\n  serverSyncStore.createTransaction(\n    [$pages, $instances],\n    (pages, instances) => {\n      if (pages === undefined) {\n        return;\n      }\n      const rootInstanceId = nanoid();\n      pages.pages.push({\n        id: pageId,\n        name: values.name,\n        path: values.path,\n        title: values.title,\n        rootInstanceId,\n        meta: {},\n      });\n      instances.set(rootInstanceId, {\n        type: \"instance\",\n        id: rootInstanceId,\n        component: elementComponent,\n        tag: \"body\",\n        children: [],\n      });\n      registerFolderChildMutable(pages.folders, pageId, values.parentFolderId);\n      selectInstance(undefined);\n    }\n  );\n};\n\nconst updatePage = (pageId: Page[\"id\"], values: Partial<Values>) => {\n  const updatePageMutable = (\n    page: Page,\n    values: Partial<Values>,\n    folders: Array<Folder>\n  ) => {\n    if (values.name !== undefined) {\n      page.name = values.name;\n    }\n    if (values.path !== undefined) {\n      page.path = values.path;\n    }\n    if (values.title !== undefined) {\n      page.title = values.title;\n    }\n\n    if (values.description !== undefined) {\n      page.meta.description = values.description;\n    }\n\n    if (values.excludePageFromSearch !== undefined) {\n      page.meta.excludePageFromSearch = values.excludePageFromSearch;\n    }\n\n    if (values.language !== undefined) {\n      page.meta.language =\n        values.language.length > 0 ? values.language : undefined;\n    }\n\n    if (\"status\" in values) {\n      page.meta.status = values.status;\n    }\n\n    if (values.redirect !== undefined) {\n      page.meta.redirect =\n        values.redirect.length > 0 ? values.redirect : undefined;\n    }\n\n    if (values.socialImageAssetId !== undefined) {\n      page.meta.socialImageAssetId =\n        values.socialImageAssetId.length > 0\n          ? values.socialImageAssetId\n          : undefined;\n    }\n    if (values.socialImageUrl !== undefined) {\n      page.meta.socialImageUrl =\n        values.socialImageUrl.length > 0 ? values.socialImageUrl : undefined;\n    }\n\n    if (values.customMetas !== undefined) {\n      page.meta.custom = values.customMetas;\n    }\n\n    if (values.documentType !== undefined) {\n      page.meta.documentType = values.documentType;\n    }\n\n    if (values.parentFolderId !== undefined) {\n      registerFolderChildMutable(folders, page.id, values.parentFolderId);\n    }\n\n    if (values.marketplaceInclude !== undefined) {\n      page.marketplace ??= {};\n      page.marketplace.include = values.marketplaceInclude;\n    }\n    if (values.marketplaceCategory !== undefined) {\n      page.marketplace ??= {};\n      page.marketplace.category =\n        values.marketplaceCategory.length > 0\n          ? values.marketplaceCategory\n          : undefined;\n    }\n    if (values.marketplaceThumbnailAssetId !== undefined) {\n      page.marketplace ??= {};\n      page.marketplace.thumbnailAssetId =\n        values.marketplaceThumbnailAssetId.length > 0\n          ? values.marketplaceThumbnailAssetId\n          : undefined;\n    }\n  };\n\n  serverSyncStore.createTransaction([$pages], (pages) => {\n    if (pages === undefined) {\n      return;\n    }\n\n    if (pages.homePage.id === pageId) {\n      updatePageMutable(pages.homePage, values, pages.folders);\n    }\n\n    const pageToUpdate = pages.pages.find((page) => page.id === pageId);\n\n    if (pageToUpdate !== undefined) {\n      updatePageMutable(pageToUpdate, values, pages.folders);\n    }\n\n    // swap home page\n    if (values.isHomePage && pages.homePage.id !== pageId) {\n      const newHomePageIndex = pages.pages.findIndex(\n        (page) => page.id === pageId\n      );\n\n      if (newHomePageIndex === -1) {\n        throw new Error(`Page with id ${pageId} not found`);\n      }\n\n      const oldHomePage = pages.homePage as (typeof pages.pages)[0];\n\n      pages.homePage = pages.pages[newHomePageIndex] as typeof pages.homePage;\n\n      pages.homePage.path = \"\";\n\n      pages.homePage.name = \"Home\";\n\n      pages.pages[newHomePageIndex] = oldHomePage;\n\n      // For simplicity skip logic in case of names are same i.e. Old Home 1, Old Home 2\n      oldHomePage.name = \"Old Home\";\n      oldHomePage.path = nameToPath(pages, oldHomePage.name);\n\n      const rootFolder = pages.folders.find((folder) => isRootFolder(folder));\n\n      if (rootFolder === undefined) {\n        throw new Error(\"Root folder not found\");\n      }\n\n      if (rootFolder.children === undefined) {\n        throw new Error(\"Root folder must have children\");\n      }\n\n      // Swap home to the first position in the root folder\n      const childIndexOfHome = rootFolder?.children.indexOf(pages.homePage.id);\n\n      if (childIndexOfHome === -1) {\n        throw new Error(\"Both pages must be children of Root folder\");\n      }\n\n      rootFolder.children[childIndexOfHome] = rootFolder.children[0];\n      rootFolder.children[0] = pages.homePage.id;\n    }\n  });\n};\n\nexport const PageSettings = ({\n  onClose,\n  onDuplicate,\n  onDelete,\n  pageId,\n}: {\n  onClose: () => void;\n  onDuplicate: (newPageId: string) => void;\n  onDelete?: () => void;\n  pageId: string;\n}) => {\n  const pages = useStore($pages);\n  const page = pages && findPageByIdOrPath(pageId, pages);\n\n  const isHomePage = page?.id === pages?.homePage.id;\n\n  const [unsavedValues, setUnsavedValues] = useState<Partial<Values>>({});\n\n  const values: Values = {\n    ...(page ? toFormValues(page, pages, isHomePage) : fieldDefaultValues),\n    ...unsavedValues,\n  };\n\n  const { variableValues } = useStore($pageRootScope);\n  const errors = validateValues(pages, pageId, values, variableValues);\n\n  const debouncedFn = useEffectEvent(() => {\n    if (\n      Object.keys(unsavedValues).length === 0 ||\n      Object.keys(errors).length !== 0\n    ) {\n      return;\n    }\n\n    updatePage(pageId, unsavedValues);\n\n    setUnsavedValues({});\n  });\n\n  const handleSubmitDebounced = useDebouncedCallback(debouncedFn, 1000);\n\n  const [refreshDebounce, setRefreshDebounce] = useState(0);\n\n  useEffect(() => {\n    // we can't flush immediately as setState haven't propagated at that time\n    handleSubmitDebounced.flush();\n  }, [refreshDebounce, handleSubmitDebounced]);\n\n  const handleChange = useCallback(\n    <Name extends FieldName>(event: { field: Name; value: Values[Name] }) => {\n      setUnsavedValues((values) => ({\n        ...values,\n        [event.field]: event.value,\n      }));\n      handleSubmitDebounced();\n\n      if (event.field === \"isHomePage\") {\n        setRefreshDebounce((prev) => prev + 1);\n      }\n    },\n    [handleSubmitDebounced]\n  );\n\n  useUnmount(() => {\n    if (\n      Object.keys(unsavedValues).length === 0 ||\n      Object.keys(errors).length !== 0\n    ) {\n      return;\n    }\n    updatePage(pageId, unsavedValues);\n  });\n\n  const handleRequestDelete = () => {\n    if (onDelete) {\n      onDelete();\n    }\n  };\n\n  if (page === undefined) {\n    return null;\n  }\n\n  return (\n    <>\n      <PageSettingsView\n        onClose={onClose}\n        onDelete={values.isHomePage === false ? handleRequestDelete : undefined}\n        onDuplicate={() => {\n          const newPageId = duplicatePage(pageId);\n          if (newPageId !== undefined) {\n            // In `canvas.tsx`, within `subscribeStyles`, we use `requestAnimationFrame` (RAF) for style recalculation.\n            // After `duplicatePage`, styles are not yet recalculated.\n            // To ensure they are properly updated, we use double RAF.\n            requestAnimationFrame(() => {\n              // At this tick styles are updating\n              requestAnimationFrame(() => {\n                // At this tick styles are updated\n                onDuplicate(newPageId);\n              });\n            });\n          }\n        }}\n      >\n        <FormFields errors={errors} values={values} onChange={handleChange} />\n      </PageSettingsView>\n    </>\n  );\n};\n\nconst PageSettingsView = ({\n  onDelete,\n  onDuplicate,\n  onClose,\n  children,\n}: {\n  onDelete?: () => void;\n  onDuplicate: () => void;\n  onClose: () => void;\n  children: JSX.Element;\n}) => {\n  const isDesignMode = useStore($isDesignMode);\n  const containerRef = useRef<HTMLFormElement>(null);\n  return (\n    <>\n      <DialogTitle\n        suffix={\n          <DialogTitleActions>\n            {isDesignMode && onDelete && (\n              <Tooltip content=\"Delete page\" side=\"bottom\">\n                <Button\n                  color=\"ghost\"\n                  prefix={<TrashIcon />}\n                  onClick={onDelete}\n                  aria-label=\"Delete page\"\n                  tabIndex={2}\n                />\n              </Tooltip>\n            )}\n            {isDesignMode && (\n              <Tooltip content=\"Duplicate page\" side=\"bottom\">\n                <Button\n                  color=\"ghost\"\n                  prefix={<CopyIcon />}\n                  onClick={onDuplicate}\n                  aria-label=\"Duplicate page\"\n                  tabIndex={2}\n                />\n              </Tooltip>\n            )}\n            <DialogClose />\n          </DialogTitleActions>\n        }\n      >\n        Page Settings\n      </DialogTitle>\n      <Form onSubmit={onClose} ref={containerRef} data-floating-panel-container>\n        <fieldset style={{ display: \"contents\" }} disabled={!isDesignMode}>\n          {children}\n        </fieldset>\n      </Form>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/page-utils.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { setEnv } from \"@webstudio-is/feature-flags\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport type { Project } from \"@webstudio-is/project\";\nimport {\n  isRootFolder,\n  type Folder,\n  ROOT_FOLDER_ID,\n  type Page,\n  SYSTEM_VARIABLE_ID,\n  Resource,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport {\n  cleanupChildRefsMutable,\n  deleteFolderWithChildrenMutable,\n  getAllChildrenAndSelf,\n  isSlugAvailable,\n  registerFolderChildMutable,\n  reparentOrphansMutable,\n  $pageRootScope,\n  isPathAvailable,\n  reparentPageOrFolderMutable,\n  deletePageMutable,\n} from \"./page-utils\";\nimport {\n  $dataSourceVariables,\n  $dataSources,\n  $pages,\n  $project,\n  $resources,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { updateCurrentSystem } from \"~/shared/system\";\nimport { $resourcesCache, getResourceKey } from \"~/shared/resources\";\n\nsetEnv(\"*\");\nregisterContainers();\n\nconst initialSystem = {\n  origin: \"https://undefined.wstd.work\",\n  params: {},\n  pathname: \"/\",\n  search: {},\n};\n\nconst createPages = () => {\n  const data = createDefaultPages({\n    rootInstanceId: \"rootInstanceId\",\n    homePageId: \"homePageId\",\n  });\n\n  const { pages, folders } = data;\n\n  function f(id: string, children?: Array<Page | Folder>): Folder;\n  function f(id: string, slug: string, children?: Array<Page | Folder>): Folder;\n  // eslint-disable-next-line func-style\n  function f(id: string, slug?: unknown, children?: unknown) {\n    if (Array.isArray(slug)) {\n      children = slug;\n      slug = id;\n    }\n    const folder = {\n      id,\n      name: id,\n      slug: slug ?? id,\n      children: register((children as Array<Page | Folder>) ?? [], false),\n    };\n\n    return folder;\n  }\n\n  const p = (id: string, path: string): Page => {\n    const page = {\n      id,\n      meta: {},\n      name: id,\n      path,\n      rootInstanceId: \"rootInstanceId\",\n      title: `\"${id}\"`,\n    };\n    return page;\n  };\n\n  const register = (children: Array<Page | Folder>, root: boolean = true) => {\n    const childIds = [];\n    const rootFolder = folders.find(isRootFolder);\n\n    for (const child of children) {\n      childIds.push(child.id);\n      if (\"meta\" in child) {\n        pages.push(child);\n        continue;\n      }\n      folders.push(child);\n\n      if (root) {\n        rootFolder?.children.push(child.id);\n      }\n    }\n\n    return childIds;\n  };\n\n  return { f, p, register, pages: data };\n};\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map((item) => [item.id, item]));\n\ndescribe(\"reparentOrphansMutable\", () => {\n  // We must deal with the fact there can be an orphaned folder or page in a collaborative mode,\n  // because user A can add a page to a folder while user B deletes the folder without receiving the create page yet.\n  test(\"reparent orphans to the root\", () => {\n    const { pages } = createPages();\n    pages.pages.push({\n      id: \"pageId\",\n      meta: {},\n      name: \"Page\",\n      path: \"/page\",\n      rootInstanceId: \"rootInstanceId\",\n      title: `\"Page\"`,\n    });\n    pages.folders.push({\n      id: \"folderId\",\n      name: \"Folder\",\n      slug: \"folder\",\n      children: [],\n    });\n    reparentOrphansMutable(pages);\n    const rootFolder = pages.folders.find(isRootFolder);\n    expect(rootFolder).toEqual({\n      id: ROOT_FOLDER_ID,\n      name: \"Root\",\n      slug: \"\",\n      children: [\"homePageId\", \"folderId\", \"pageId\"],\n    });\n  });\n});\n\ndescribe(\"cleanupChildRefsMutable\", () => {\n  test(\"cleanup refs\", () => {\n    const {\n      pages: { folders },\n    } = createPages();\n    folders.push({\n      id: \"folderId\",\n      name: \"Folder\",\n      slug: \"folder\",\n      children: [],\n    });\n    const rootFolder = folders.find(isRootFolder);\n    rootFolder?.children.push(\"folderId\");\n    cleanupChildRefsMutable(\"folderId\", folders);\n    expect(rootFolder).toEqual({\n      id: ROOT_FOLDER_ID,\n      name: \"Root\",\n      slug: \"\",\n      children: [\"homePageId\"],\n    });\n  });\n});\n\ndescribe(\"isSlugAvailable\", () => {\n  const {\n    pages: { folders },\n    register,\n    f,\n  } = createPages();\n\n  register([\n    f(\"folder1\", [f(\"folder1-1\")]),\n    f(\"folder2-1\", \"\"),\n    f(\"folder2-2\", \"\"),\n  ]);\n\n  const rootFolder = folders.find(isRootFolder)!;\n\n  test(\"available in the root\", () => {\n    expect(isSlugAvailable(\"folder\", folders, rootFolder.id)).toBe(true);\n  });\n\n  test(\"not available in the root\", () => {\n    expect(isSlugAvailable(\"folder1\", folders, rootFolder.id)).toBe(false);\n  });\n\n  test(\"available in folder1\", () => {\n    expect(isSlugAvailable(\"folder\", folders, \"folder1\")).toBe(true);\n  });\n\n  test(\"not available in folder1\", () => {\n    expect(isSlugAvailable(\"folder1-1\", folders, \"folder1\")).toBe(false);\n  });\n\n  test(\"existing folder can have a matching slug when its the same id/folder\", () => {\n    expect(isSlugAvailable(\"folder1-1\", folders, \"folder1\", \"folder1-1\")).toBe(\n      true\n    );\n  });\n\n  test(\"empty folder slug can be defined multiple times\", () => {\n    expect(isSlugAvailable(\"\", folders, \"rootInstanceId\")).toBe(true);\n  });\n});\n\ndescribe(\"isPathAvailable\", () => {\n  const { f, p, register, pages } = createPages();\n\n  register([\n    f(\"folder1\", [p(\"page1\", \"/page\")]),\n    f(\"folder2\", [p(\"page2\", \"/page\")]),\n    f(\"/\", [p(\"page3\", \"/page\")]),\n  ]);\n\n  test(\"/folder2/page existing page\", () => {\n    expect(\n      isPathAvailable({\n        pages,\n        path: \"/page\",\n        parentFolderId: \"folder2\",\n        pageId: \"page2\",\n      })\n    ).toBe(true);\n  });\n\n  test(\"/folder2/page new page\", () => {\n    expect(\n      isPathAvailable({ pages, path: \"/page\", parentFolderId: \"folder2\" })\n    ).toBe(false);\n  });\n\n  test(\"/folder2/page1 new page\", () => {\n    expect(\n      isPathAvailable({ pages, path: \"/page1\", parentFolderId: \"folder2\" })\n    ).toBe(true);\n  });\n\n  test(\"/page new page\", () => {\n    expect(isPathAvailable({ pages, path: \"/page\", parentFolderId: \"/\" })).toBe(\n      false\n    );\n  });\n});\n\ndescribe(\"registerFolderChildMutable\", () => {\n  test(\"register a folder child in the root via fallback\", () => {\n    const {\n      pages: { folders },\n    } = createPages();\n    registerFolderChildMutable(folders, \"folderId\");\n    const rootFolder = folders.find(isRootFolder);\n    expect(rootFolder?.children).toEqual([\"homePageId\", \"folderId\"]);\n  });\n\n  test(\"register a folder child in a provided folder\", () => {\n    const {\n      pages: { folders },\n    } = createPages();\n    const folder = {\n      id: \"folderId\",\n      name: \"Folder\",\n      slug: \"folder\",\n      children: [],\n    };\n    folders.push(folder);\n    registerFolderChildMutable(folders, \"folderId2\", \"folderId\");\n    expect(folder.children).toEqual([\"folderId2\"]);\n  });\n\n  test(\"register in a provided folder & cleanup old refs\", () => {\n    const {\n      pages: { folders },\n    } = createPages();\n    const folder = {\n      id: \"folderId\",\n      name: \"Folder\",\n      slug: \"folder\",\n      children: [],\n    };\n    folders.push(folder);\n    const rootFolder = folders.find(isRootFolder);\n    registerFolderChildMutable(folders, \"folderId\", ROOT_FOLDER_ID);\n    registerFolderChildMutable(folders, \"folderId2\", ROOT_FOLDER_ID);\n\n    expect(rootFolder?.children).toEqual([\n      \"homePageId\",\n      \"folderId\",\n      \"folderId2\",\n    ]);\n\n    // Moving folderId from root to folderId\n    registerFolderChildMutable(folders, \"folderId2\", \"folderId\");\n\n    expect(rootFolder?.children).toEqual([\"homePageId\", \"folderId\"]);\n    expect(folder.children).toEqual([\"folderId2\"]);\n  });\n});\n\ndescribe(\"reparent pages and folders\", () => {\n  test(\"move page up within single parent\", () => {\n    const { f, p, register, pages } = createPages();\n    register([\n      f(\"folder\", [\n        p(\"page1\", \"/page1\"),\n        p(\"page2\", \"/page2\"),\n        p(\"page3\", \"/page3\"),\n      ]),\n    ]);\n    reparentPageOrFolderMutable(pages.folders, \"page3\", \"folder\", 1);\n    const folder = pages.folders.find((folder) => folder.id === \"folder\");\n    expect(folder?.children).toEqual([\"page1\", \"page3\", \"page2\"]);\n  });\n\n  test(\"move page down within single parent\", () => {\n    const { f, p, register, pages } = createPages();\n    register([\n      f(\"folder\", [\n        p(\"page1\", \"/page1\"),\n        p(\"page2\", \"/page2\"),\n        p(\"page3\", \"/page3\"),\n      ]),\n    ]);\n    reparentPageOrFolderMutable(pages.folders, \"page1\", \"folder\", 2);\n    const folder = pages.folders.find((folder) => folder.id === \"folder\");\n    expect(folder?.children).toEqual([\"page2\", \"page1\", \"page3\"]);\n  });\n\n  test(\"move page into another folder\", () => {\n    const { f, p, register, pages } = createPages();\n    register([\n      f(\"folder1\", [p(\"page1\", \"/page1\"), p(\"page2\", \"/page2\")]),\n      f(\"folder2\", [p(\"page3\", \"/page3\")]),\n    ]);\n    reparentPageOrFolderMutable(pages.folders, \"page1\", \"folder2\", 1);\n    const folder1 = pages.folders.find((folder) => folder.id === \"folder1\");\n    const folder2 = pages.folders.find((folder) => folder.id === \"folder2\");\n    expect(folder1?.children).toEqual([\"page2\"]);\n    expect(folder2?.children).toEqual([\"page3\", \"page1\"]);\n  });\n\n  test(\"move folder into another folder\", () => {\n    const { f, register, pages } = createPages();\n    register([f(\"folder1\", []), f(\"folder2\", [])]);\n    reparentPageOrFolderMutable(pages.folders, \"folder1\", \"folder2\", 1);\n    expect(pages.folders).toEqual([\n      expect.objectContaining({\n        id: \"root\",\n        children: [\"homePageId\", \"folder2\"],\n      }),\n      expect.objectContaining({ id: \"folder1\", children: [] }),\n      expect.objectContaining({ id: \"folder2\", children: [\"folder1\"] }),\n    ]);\n  });\n\n  test(\"prevent reparanting folder into itself\", () => {\n    const { f, register, pages } = createPages();\n    register([f(\"folder1\", [])]);\n    reparentPageOrFolderMutable(pages.folders, \"folder1\", \"folder1\", 1);\n    expect(pages.folders).toEqual([\n      expect.objectContaining({\n        id: \"root\",\n        children: [\"homePageId\", \"folder1\"],\n      }),\n      expect.objectContaining({ id: \"folder1\", children: [] }),\n    ]);\n  });\n\n  test(\"prevent reparanting folder own children\", () => {\n    const { f, register, pages } = createPages();\n    register([f(\"folder1\", [f(\"folder2\", [])])]);\n    reparentPageOrFolderMutable(pages.folders, \"folder1\", \"folder2\", 1);\n    expect(pages.folders).toEqual([\n      expect.objectContaining({\n        id: \"root\",\n        children: [\"homePageId\", \"folder1\"],\n      }),\n      expect.objectContaining({ id: \"folder2\", children: [] }),\n      expect.objectContaining({ id: \"folder1\", children: [\"folder2\"] }),\n    ]);\n  });\n});\n\ndescribe(\"getAllChildrenAndSelf\", () => {\n  const folders: Array<Folder> = [\n    {\n      id: \"1\",\n      name: \"1\",\n      slug: \"1\",\n      children: [\"2\"],\n    },\n    {\n      id: \"2\",\n      name: \"2\",\n      slug: \"2\",\n      children: [\"3\", \"page1\"],\n    },\n    {\n      id: \"3\",\n      name: \"3\",\n      slug: \"3\",\n      children: [\"page2\"],\n    },\n  ];\n\n  test(\"get nested folders\", () => {\n    const result = getAllChildrenAndSelf(\"1\", folders, \"folder\");\n    expect(result).toEqual([\"1\", \"2\", \"3\"]);\n  });\n\n  test(\"get nested pages\", () => {\n    const result = getAllChildrenAndSelf(\"1\", folders, \"page\");\n    expect(result).toEqual([\"page2\", \"page1\"]);\n  });\n});\n\ndescribe(\"deleteFolderWithChildrenMutable\", () => {\n  const folders = (): Array<Folder> => [\n    {\n      id: \"1\",\n      name: \"1\",\n      slug: \"1\",\n      children: [\"2\"],\n    },\n    {\n      id: \"2\",\n      name: \"2\",\n      slug: \"2\",\n      children: [\"3\", \"page1\"],\n    },\n    {\n      id: \"3\",\n      name: \"3\",\n      slug: \"3\",\n      children: [],\n    },\n  ];\n\n  test(\"delete empty folder\", () => {\n    const result = deleteFolderWithChildrenMutable(\"3\", folders());\n    expect(result).toEqual({ folderIds: [\"3\"], pageIds: [] });\n  });\n\n  test(\"delete folder with folders and pages\", () => {\n    const result = deleteFolderWithChildrenMutable(\"1\", folders());\n    expect(result).toEqual({\n      folderIds: [\"1\", \"2\", \"3\"],\n      pageIds: [\"page1\"],\n    });\n  });\n});\n\ntest(\"page root scope should rely on selected page\", () => {\n  const pages = createDefaultPages({\n    rootInstanceId: \"homeRootId\",\n    homePageId: \"homePageId\",\n  });\n  pages.pages.push({\n    id: \"pageId\",\n    rootInstanceId: \"pageRootId\",\n    name: \"My Name\",\n    path: \"/\",\n    title: `\"My Title\"`,\n    meta: {},\n  });\n  $pages.set(pages);\n  $awareness.set({ pageId: \"pageId\" });\n  $dataSources.set(\n    toMap([\n      {\n        id: \"1\",\n        scopeInstanceId: \"homeRootId\",\n        name: \"home variable\",\n        type: \"variable\",\n        value: { type: \"string\", value: \"\" },\n      },\n      {\n        id: \"2\",\n        scopeInstanceId: \"pageRootId\",\n        name: \"page variable\",\n        type: \"variable\",\n        value: { type: \"string\", value: \"\" },\n      },\n    ])\n  );\n  expect($pageRootScope.get()).toEqual({\n    aliases: new Map([\n      [\"$ws$system\", \"system\"],\n      [\"$ws$dataSource$2\", \"page variable\"],\n    ]),\n    scope: {\n      $ws$system: initialSystem,\n      $ws$dataSource$2: \"\",\n    },\n    variableValues: new Map<string, unknown>([\n      [SYSTEM_VARIABLE_ID, initialSystem],\n      [\"2\", \"\"],\n    ]),\n  });\n});\n\ntest(\"page root scope should use variable and resource values\", () => {\n  $pages.set(\n    createDefaultPages({\n      rootInstanceId: \"homeRootId\",\n      homePageId: \"homePageId\",\n    })\n  );\n  $awareness.set({ pageId: \"homePageId\" });\n  $dataSources.set(\n    toMap([\n      {\n        id: \"valueVariableId\",\n        scopeInstanceId: \"homeRootId\",\n        name: \"value variable\",\n        type: \"variable\",\n        value: { type: \"string\", value: \"\" },\n      },\n      {\n        id: \"resourceVariableId\",\n        scopeInstanceId: \"homeRootId\",\n        name: \"resource variable\",\n        type: \"resource\",\n        resourceId: \"resourceId\",\n      },\n    ])\n  );\n  $dataSourceVariables.set(\n    new Map([[\"valueVariableId\", \"value variable value\"]])\n  );\n  const resourceKey = getResourceKey({\n    name: \"my-resource\",\n    url: \"\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  });\n  $resources.set(\n    toMap<Resource>([\n      {\n        id: \"resourceId\",\n        name: \"my-resource\",\n        url: `\"\"`,\n        method: \"get\",\n        headers: [],\n      },\n    ])\n  );\n  $resourcesCache.set(new Map([[resourceKey, \"resource variable value\"]]));\n  expect($pageRootScope.get()).toEqual({\n    aliases: new Map([\n      [\"$ws$system\", \"system\"],\n      [\"$ws$dataSource$valueVariableId\", \"value variable\"],\n      [\"$ws$dataSource$resourceVariableId\", \"resource variable\"],\n    ]),\n    scope: {\n      $ws$system: initialSystem,\n      $ws$dataSource$resourceVariableId: \"resource variable value\",\n      $ws$dataSource$valueVariableId: \"value variable value\",\n    },\n    variableValues: new Map<string, unknown>([\n      [SYSTEM_VARIABLE_ID, initialSystem],\n      [\"valueVariableId\", \"value variable value\"],\n      [\"resourceVariableId\", \"resource variable value\"],\n    ]),\n  });\n});\n\ntest(\"page root scope should provide page system variable value\", () => {\n  $pages.set(\n    createDefaultPages({\n      rootInstanceId: \"homeRootId\",\n      homePageId: \"homePageId\",\n      systemDataSourceId: \"systemId\",\n    })\n  );\n  $awareness.set({ pageId: \"homePageId\" });\n  $dataSources.set(\n    toMap([\n      {\n        id: \"systemId\",\n        scopeInstanceId: \"homeRootId\",\n        name: \"system\",\n        type: \"parameter\",\n      },\n    ])\n  );\n  expect($pageRootScope.get()).toEqual({\n    aliases: new Map([[\"$ws$dataSource$systemId\", \"system\"]]),\n    scope: {\n      $ws$dataSource$systemId: {\n        origin: \"https://undefined.wstd.work\",\n        params: {},\n        pathname: \"/\",\n        search: {},\n      },\n    },\n    variableValues: new Map([\n      [\n        \"systemId\",\n        {\n          params: {},\n          pathname: \"/\",\n          search: {},\n          origin: \"https://undefined.wstd.work\",\n        },\n      ],\n    ]),\n  });\n  updateCurrentSystem({\n    params: { slug: \"my-post\" },\n  });\n  expect($pageRootScope.get()).toEqual({\n    aliases: new Map([[\"$ws$dataSource$systemId\", \"system\"]]),\n    scope: {\n      $ws$dataSource$systemId: {\n        params: { slug: \"my-post\" },\n        pathname: \"/\",\n        search: {},\n        origin: \"https://undefined.wstd.work\",\n      },\n    },\n    variableValues: new Map([\n      [\n        \"systemId\",\n        {\n          params: { slug: \"my-post\" },\n          pathname: \"/\",\n          search: {},\n          origin: \"https://undefined.wstd.work\",\n        },\n      ],\n    ]),\n  });\n});\n\ndescribe(\"deletePageMutable\", () => {\n  test(\"should delete a page from pages array\", async () => {\n    const { pages: pagesData, register, p } = createPages();\n    register([p(\"page1\", \"/page1\"), p(\"page2\", \"/page2\")]);\n\n    // Create minimal WebstudioData\n    const data = {\n      pages: pagesData,\n      instances: new Map(),\n    } as unknown as WebstudioData;\n\n    deletePageMutable(\"page1\", data);\n\n    expect(pagesData.pages.find((page) => page.id === \"page1\")).toBeUndefined();\n    expect(pagesData.pages.find((page) => page.id === \"page2\")).toBeDefined();\n  });\n\n  test(\"should delete page instance\", async () => {\n    const { pages: pagesData, register, p } = createPages();\n    register([p(\"page1\", \"/page1\")]);\n\n    const page = pagesData.pages.find((page) => page.id === \"page1\");\n    const rootInstanceId = page?.rootInstanceId;\n\n    // Create minimal WebstudioData with all required properties\n    const data = {\n      pages: pagesData,\n      instances: new Map([\n        [\n          rootInstanceId!,\n          {\n            id: rootInstanceId,\n            type: \"instance\",\n            component: \"Body\",\n            children: [],\n          },\n        ],\n      ]),\n      styleSources: new Map(),\n      styleSourceSelections: new Map(),\n      breakpoints: new Map(),\n      styles: new Map(),\n      props: new Map(),\n      dataSources: new Map(),\n      resources: new Map(),\n    } as unknown as WebstudioData;\n\n    deletePageMutable(\"page1\", data);\n\n    expect(data.instances.has(rootInstanceId!)).toBe(false);\n  });\n\n  test(\"should remove page from folder children\", async () => {\n    const { pages: pagesData, register, p, f } = createPages();\n    register([f(\"folder1\", [p(\"page1\", \"/page1\")])]);\n\n    const folder = pagesData.folders.find((folder) => folder.id === \"folder1\");\n    expect(folder?.children).toContain(\"page1\");\n\n    // Create minimal WebstudioData\n    const data = {\n      pages: pagesData,\n      instances: new Map(),\n    } as unknown as WebstudioData;\n\n    deletePageMutable(\"page1\", data);\n\n    expect(folder?.children).not.toContain(\"page1\");\n  });\n});\n\ndescribe(\"isFolder\", () => {\n  test(\"should return true for existing folder id\", async () => {\n    const { pages: pagesData, register, f } = createPages();\n    register([f(\"folder1\", [])]);\n\n    const { isFolder } = await import(\"./page-utils\");\n    expect(isFolder(\"folder1\", pagesData.folders)).toBe(true);\n  });\n\n  test(\"should return false for non-existing folder id\", async () => {\n    const { pages: pagesData } = createPages();\n\n    const { isFolder } = await import(\"./page-utils\");\n    expect(isFolder(\"nonexistent\", pagesData.folders)).toBe(false);\n  });\n\n  test(\"should return false for page id\", async () => {\n    const { pages: pagesData, register, p } = createPages();\n    register([p(\"page1\", \"/page1\")]);\n\n    const { isFolder } = await import(\"./page-utils\");\n    expect(isFolder(\"page1\", pagesData.folders)).toBe(false);\n  });\n});\n\ndescribe(\"getStoredDropTarget\", () => {\n  test(\"should return drop target with parent id\", async () => {\n    const { pages: pagesData, register, f, p } = createPages();\n    register([f(\"folder1\", [p(\"page1\", \"/page1\")])]);\n    $pages.set(pagesData);\n\n    const { getStoredDropTarget } = await import(\"./page-utils\");\n    // selector order is [item, parent, grandparent, ...]\n    const selector = [\"page1\", \"folder1\", ROOT_FOLDER_ID];\n    const dropTarget = { parentLevel: 1, beforeLevel: 1 };\n\n    const result = getStoredDropTarget(selector, dropTarget);\n\n    expect(result).toEqual({\n      parentId: \"folder1\",\n      beforeId: \"folder1\",\n      afterId: undefined,\n      indexWithinChildren: 0,\n    });\n  });\n\n  test(\"should calculate indexWithinChildren based on beforeId\", async () => {\n    const { pages: pagesData, register, f, p } = createPages();\n    register([f(\"folder1\", [p(\"page1\", \"/page1\"), p(\"page2\", \"/page2\")])]);\n    $pages.set(pagesData);\n\n    const { getStoredDropTarget } = await import(\"./page-utils\");\n    const selector = [\"page2\", \"folder1\", ROOT_FOLDER_ID];\n    const dropTarget = { parentLevel: 1, beforeLevel: 0 };\n\n    const result = getStoredDropTarget(selector, dropTarget);\n\n    // beforeLevel: 0 means selector.at(-0-1) = selector.at(-1) = ROOT_FOLDER_ID\n    // But ROOT_FOLDER_ID is not in folder1's children, so indexWithinChildren = 0\n    expect(result?.indexWithinChildren).toBe(0);\n  });\n\n  test(\"should calculate indexWithinChildren based on afterId\", async () => {\n    const { pages: pagesData, register, f, p } = createPages();\n    register([f(\"folder1\", [p(\"page1\", \"/page1\"), p(\"page2\", \"/page2\")])]);\n    $pages.set(pagesData);\n\n    const { getStoredDropTarget } = await import(\"./page-utils\");\n    const selector = [\"page1\", \"folder1\", ROOT_FOLDER_ID];\n    const dropTarget = { parentLevel: 1, afterLevel: 0 };\n\n    const result = getStoredDropTarget(selector, dropTarget);\n\n    // afterLevel: 0 means selector.at(-0-1) = selector.at(-1) = ROOT_FOLDER_ID\n    // But ROOT_FOLDER_ID is not in folder1's children, so indexWithinChildren = 0\n    expect(result?.indexWithinChildren).toBe(0);\n  });\n\n  test(\"should return undefined when parent id is undefined\", async () => {\n    const { pages: pagesData } = createPages();\n    $pages.set(pagesData);\n\n    const { getStoredDropTarget } = await import(\"./page-utils\");\n    const selector = [\"page1\"];\n    const dropTarget = { parentLevel: 5 };\n\n    const result = getStoredDropTarget(selector, dropTarget);\n\n    expect(result).toBeUndefined();\n  });\n});\n\ndescribe(\"canDrop\", () => {\n  test(\"should allow dropping inside a folder\", async () => {\n    const { pages: pagesData, register, f } = createPages();\n    register([f(\"folder1\", [])]);\n\n    const { canDrop } = await import(\"./page-utils\");\n    const dropTarget = {\n      parentId: \"folder1\",\n      indexWithinChildren: 1,\n    };\n\n    expect(canDrop(dropTarget, pagesData.folders)).toBe(true);\n  });\n\n  test(\"should forbid dropping on non-folder\", async () => {\n    const { pages: pagesData, register, p } = createPages();\n    register([p(\"page1\", \"/page1\")]);\n\n    const { canDrop } = await import(\"./page-utils\");\n    const dropTarget = {\n      parentId: \"page1\",\n      indexWithinChildren: 0,\n    };\n\n    expect(canDrop(dropTarget, pagesData.folders)).toBe(false);\n  });\n\n  test(\"should forbid dropping at index 0 of root folder\", async () => {\n    const { pages: pagesData } = createPages();\n\n    const { canDrop } = await import(\"./page-utils\");\n    const dropTarget = {\n      parentId: ROOT_FOLDER_ID,\n      indexWithinChildren: 0,\n    };\n\n    expect(canDrop(dropTarget, pagesData.folders)).toBe(false);\n  });\n\n  test(\"should allow dropping at index > 0 of root folder\", async () => {\n    const { pages: pagesData } = createPages();\n\n    const { canDrop } = await import(\"./page-utils\");\n    const dropTarget = {\n      parentId: ROOT_FOLDER_ID,\n      indexWithinChildren: 1,\n    };\n\n    expect(canDrop(dropTarget, pagesData.folders)).toBe(true);\n  });\n});\n\ndescribe(\"duplicateFolder\", () => {\n  $project.set({ id: \"projectId\" } as Project);\n\n  test(\"should duplicate a folder with deduplicated name\", async () => {\n    const { pages: pagesData, register, f } = createPages();\n    register([f(\"folder1\", [])]);\n\n    $pages.set(pagesData);\n    updateCurrentSystem(initialSystem);\n\n    const { duplicateFolder } = await import(\"./page-utils\");\n    const newFolderId = duplicateFolder(\"folder1\");\n\n    expect(newFolderId).toBeDefined();\n    const updatedPages = $pages.get()!;\n    const newFolder = updatedPages.folders.find(\n      (folder) => folder.id === newFolderId\n    );\n    expect(newFolder).toBeDefined();\n    expect(newFolder?.name).toBe(\"folder1 (1)\");\n    expect(newFolder?.slug).toBe(\"folder1-1\");\n  });\n\n  test(\"should duplicate a folder with pages\", async () => {\n    const { pages: pagesData, register, f, p } = createPages();\n    register([f(\"folder1\", [p(\"page1\", \"/page1\"), p(\"page2\", \"/page2\")])]);\n\n    $pages.set(pagesData);\n    updateCurrentSystem(initialSystem);\n\n    const { duplicateFolder } = await import(\"./page-utils\");\n    const newFolderId = duplicateFolder(\"folder1\");\n\n    expect(newFolderId).toBeDefined();\n    const updatedPages = $pages.get()!;\n    const newFolder = updatedPages.folders.find(\n      (folder) => folder.id === newFolderId\n    );\n    expect(newFolder).toBeDefined();\n    // Note: Page duplication is handled by insertPageCopyMutable from ~/shared/page-utils\n    // which requires full WebstudioData setup. Here we just verify the folder structure.\n    expect(newFolder?.children.length).toBeGreaterThan(0);\n  });\n\n  test(\"should duplicate a folder with nested folders\", async () => {\n    const { pages: pagesData, register, f, p } = createPages();\n    register([\n      f(\"folder1\", [\n        f(\"subfolder1\", [p(\"page1\", \"/page1\")]),\n        p(\"page2\", \"/page2\"),\n      ]),\n    ]);\n\n    $pages.set(pagesData);\n    updateCurrentSystem(initialSystem);\n\n    const { duplicateFolder } = await import(\"./page-utils\");\n    const newFolderId = duplicateFolder(\"folder1\");\n\n    expect(newFolderId).toBeDefined();\n    const updatedPages = $pages.get()!;\n    const newFolder = updatedPages.folders.find(\n      (folder) => folder.id === newFolderId\n    );\n    expect(newFolder).toBeDefined();\n    expect(newFolder?.children.length).toBeGreaterThan(0);\n\n    // Check that subfolder was duplicated\n    const subFolderChild = newFolder?.children.find((childId) =>\n      updatedPages.folders.some((folder) => folder.id === childId)\n    );\n    expect(subFolderChild).toBeDefined();\n    const subFolder = updatedPages.folders.find(\n      (folder) => folder.id === subFolderChild\n    );\n    expect(subFolder).toBeDefined();\n    expect(subFolder?.name).toBe(\"subfolder1 (1)\");\n  });\n\n  test(\"should deduplicate folder names correctly with existing copies\", async () => {\n    const { pages: pagesData, register, f } = createPages();\n    register([f(\"folder1\", \"folder1\", []), f(\"folder1 (1)\", \"folder1-1\", [])]);\n\n    $pages.set(pagesData);\n    updateCurrentSystem(initialSystem);\n\n    const { duplicateFolder } = await import(\"./page-utils\");\n    const newFolderId = duplicateFolder(\"folder1\");\n\n    expect(newFolderId).toBeDefined();\n    const updatedPages = $pages.get()!;\n    const newFolder = updatedPages.folders.find(\n      (folder) => folder.id === newFolderId\n    );\n    expect(newFolder?.name).toBe(\"folder1 (2)\");\n    expect(newFolder?.slug).toBe(\"folder1-2\");\n  });\n\n  test(\"should register duplicated folder in parent folder\", async () => {\n    const { pages: pagesData, register, f } = createPages();\n    register([f(\"folder1\", [])]);\n\n    $pages.set(pagesData);\n    updateCurrentSystem(initialSystem);\n\n    const { duplicateFolder } = await import(\"./page-utils\");\n    const newFolderId = duplicateFolder(\"folder1\");\n\n    const updatedPages = $pages.get()!;\n    const rootFolder = updatedPages.folders.find(isRootFolder);\n    expect(rootFolder?.children).toContain(newFolderId);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/page-utils.ts",
    "content": "import { computed } from \"nanostores\";\nimport { nanoid } from \"nanoid\";\nimport { createRootFolder } from \"@webstudio-is/project-build\";\nimport {\n  type Page,\n  type Folder,\n  type WebstudioData,\n  Pages,\n  findPageByIdOrPath,\n  getPagePath,\n  findParentFolderByChildId,\n  encodeDataSourceVariable,\n  ROOT_FOLDER_ID,\n  isRootFolder,\n  ROOT_INSTANCE_ID,\n  systemParameter,\n  SYSTEM_VARIABLE_ID,\n} from \"@webstudio-is/sdk\";\nimport { removeByMutable } from \"~/shared/array-utils\";\nimport {\n  deleteInstanceMutable,\n  updateWebstudioData,\n} from \"~/shared/instance-utils\";\nimport {\n  $dataSources,\n  $pages,\n  $variableValuesByInstanceSelector,\n} from \"~/shared/nano-states\";\nimport { insertPageCopyMutable } from \"~/shared/page-utils\";\nimport {\n  $selectedPage,\n  getInstanceKey,\n  getInstancePath,\n  selectPage,\n} from \"~/shared/awareness\";\n\n/**\n * When page or folder needs to be deleted or moved to a different parent,\n * we want to cleanup any existing reference to it in current folder.\n * We could do this in just one folder, but I think its more robust to check all,\n * just in case we got double referencing.\n */\nexport const cleanupChildRefsMutable = (\n  id: Folder[\"id\"] | Page[\"id\"],\n  folders: Array<Folder>\n) => {\n  for (const folder of folders) {\n    const index = folder.children.indexOf(id);\n    if (index !== -1) {\n      // Not exiting here just to be safe and check all folders even though it should be impossible\n      // to have the same id in multiple folders.\n      folder.children.splice(index, 1);\n    }\n  }\n};\n\n/**\n * When page or folder is found and its not referenced in any other folder children,\n * we consider it orphaned due to collaborative changes and we put it into the root folder.\n */\nexport const reparentOrphansMutable = (pages: Pages) => {\n  const children = [ROOT_FOLDER_ID];\n  for (const folder of pages.folders) {\n    children.push(...folder.children);\n  }\n\n  let rootFolder = pages.folders.find(isRootFolder);\n  // Should never happen, but just in case.\n  if (rootFolder === undefined) {\n    rootFolder = createRootFolder();\n    pages.folders.push(rootFolder);\n  }\n\n  for (const folder of pages.folders) {\n    // It's an orphan\n    if (children.includes(folder.id) === false) {\n      rootFolder.children.push(folder.id);\n    }\n  }\n\n  for (const page of pages.pages) {\n    // It's an orphan\n    if (children.includes(page.id) === false) {\n      rootFolder.children.push(page.id);\n    }\n  }\n};\n\n/**\n * Returns true if folder's slug is unique within it's future parent folder.\n * Needed to verify if the folder can be nested under the parent folder without modifying slug.\n */\nexport const isSlugAvailable = (\n  slug: string,\n  folders: Array<Folder>,\n  parentFolderId: Folder[\"id\"],\n  // undefined folder id means new folder\n  folderId?: Folder[\"id\"]\n) => {\n  // Empty slug can appear any amount of times.\n  if (slug === \"\") {\n    return true;\n  }\n  const foldersMap = new Map(folders.map((folder) => [folder.id, folder]));\n  const parentFolder = foldersMap.get(parentFolderId);\n  // Should be impossible because at least root folder is always found.\n  if (parentFolder === undefined) {\n    return false;\n  }\n\n  return (\n    parentFolder.children.some(\n      (id) => foldersMap.get(id)?.slug === slug && id !== folderId\n    ) === false\n  );\n};\n\nexport const isPathAvailable = ({\n  pages,\n  path,\n  parentFolderId,\n  pageId,\n}: {\n  pages: Pages;\n  path: Page[\"path\"];\n  parentFolderId: Folder[\"id\"];\n  // undefined page id means new page\n  pageId?: Page[\"id\"];\n}) => {\n  const map = new Map<Page[\"path\"], Page>();\n  const allPages = [pages.homePage, ...pages.pages];\n  for (const page of allPages) {\n    map.set(getPagePath(page.id, pages), page);\n  }\n  const folderPath = getPagePath(parentFolderId, pages);\n  // When slug is empty, folderPath is \"/\".\n  const pagePath = folderPath === \"/\" ? path : `${folderPath}${path}`;\n  const existingPage = map.get(pagePath);\n  // We found another page that has the same path and the current page.\n  if (pageId && existingPage?.id === pageId) {\n    return true;\n  }\n  return existingPage === undefined;\n};\n\n/**\n * - Register a folder or a page inside children of a given parent folder.\n * - Fallback to a root folder.\n * - Cleanup any potential references in other folders.\n */\nexport const registerFolderChildMutable = (\n  folders: Array<Folder>,\n  id: Page[\"id\"] | Folder[\"id\"],\n  // In case we couldn't find the current folder during update for any reason,\n  // we will always fall back to the root folder.\n  parentFolderId?: Folder[\"id\"]\n) => {\n  const parentFolder =\n    folders.find((folder) => folder.id === parentFolderId) ??\n    folders.find(isRootFolder);\n  cleanupChildRefsMutable(id, folders);\n  parentFolder?.children.push(id);\n};\n\nexport const reparentPageOrFolderMutable = (\n  folders: Folder[],\n  pageOrFolderId: string,\n  newFolderId: string,\n  newPosition: number\n) => {\n  const childrenAndSelf = getAllChildrenAndSelf(\n    pageOrFolderId,\n    folders,\n    \"folder\"\n  );\n  // make sure target folder is not self or descendants\n  if (childrenAndSelf.includes(newFolderId)) {\n    return;\n  }\n  const prevParent = findParentFolderByChildId(pageOrFolderId, folders);\n  const nextParent = folders.find((folder) => folder.id === newFolderId);\n  if (prevParent === undefined || nextParent === undefined) {\n    return;\n  }\n  // if parent is the same, we need to adjust the position\n  // to account for the removal of the instance.\n  const prevPosition = prevParent.children.indexOf(pageOrFolderId);\n  if (prevParent.id === nextParent.id && prevPosition < newPosition) {\n    newPosition -= 1;\n  }\n  prevParent.children.splice(prevPosition, 1);\n  nextParent.children.splice(newPosition, 0, pageOrFolderId);\n};\n\n/**\n * Get all child folder ids of the current folder including itself.\n */\nexport const getAllChildrenAndSelf = (\n  id: Folder[\"id\"] | Page[\"id\"],\n  folders: Array<Folder>,\n  filter: \"folder\" | \"page\"\n) => {\n  const child = folders.find((folder) => folder.id === id);\n  const children: Array<Folder[\"id\"]> = [];\n  const type = child === undefined ? \"page\" : \"folder\";\n\n  if (type === filter) {\n    children.push(id);\n  }\n\n  if (child) {\n    for (const childId of child.children) {\n      children.push(...getAllChildrenAndSelf(childId, folders, filter));\n    }\n  }\n  return children;\n};\n\n/**\n * Deletes a page.\n */\nexport const deletePageMutable = (pageId: Page[\"id\"], data: WebstudioData) => {\n  const { pages } = data;\n  // deselect page before deleting to avoid flash of content\n  if ($selectedPage.get()?.id === pageId) {\n    selectPage(pages.homePage.id);\n  }\n  const rootInstanceId = findPageByIdOrPath(pageId, pages)?.rootInstanceId;\n  if (rootInstanceId !== undefined) {\n    deleteInstanceMutable(\n      data,\n      getInstancePath([rootInstanceId], data.instances)\n    );\n  }\n  removeByMutable(pages.pages, (page) => page.id === pageId);\n  cleanupChildRefsMutable(pageId, pages.folders);\n};\n\n/**\n * Deletes folder and child folders.\n * Doesn't delete pages, only returns pageIds.\n */\nexport const deleteFolderWithChildrenMutable = (\n  folderId: Folder[\"id\"],\n  folders: Array<Folder>\n) => {\n  const folderIds = getAllChildrenAndSelf(folderId, folders, \"folder\");\n  const pageIds = getAllChildrenAndSelf(folderId, folders, \"page\");\n  for (const folderId of folderIds) {\n    cleanupChildRefsMutable(folderId, folders);\n    removeByMutable(folders, (folder) => folder.id === folderId);\n  }\n\n  return {\n    folderIds,\n    pageIds,\n  };\n};\n\nexport const $pageRootScope = computed(\n  [$selectedPage, $variableValuesByInstanceSelector, $dataSources],\n  (page, variableValuesByInstanceSelector, dataSources) => {\n    const scope: Record<string, unknown> = {};\n    const aliases = new Map<string, string>();\n    const defaultValues = new Map<string, unknown>();\n    if (page === undefined) {\n      return { variableValues: defaultValues, scope, aliases };\n    }\n    const values =\n      variableValuesByInstanceSelector.get(\n        getInstanceKey([page.rootInstanceId, ROOT_INSTANCE_ID])\n      ) ?? new Map<string, unknown>();\n    for (const [dataSourceId, value] of values) {\n      let dataSource = dataSources.get(dataSourceId);\n      if (dataSourceId === SYSTEM_VARIABLE_ID) {\n        dataSource = systemParameter;\n      }\n      if (dataSource === undefined) {\n        continue;\n      }\n      const name = encodeDataSourceVariable(dataSourceId);\n      scope[name] = value;\n      aliases.set(name, dataSource.name);\n    }\n    return { variableValues: values, scope, aliases };\n  }\n);\n\nexport const duplicatePage = (pageId: Page[\"id\"]) => {\n  const pages = $pages.get();\n  const currentFolder = findParentFolderByChildId(pageId, pages?.folders ?? []);\n  if (currentFolder === undefined) {\n    return;\n  }\n  let newPageId: undefined | string;\n  updateWebstudioData((data) => {\n    newPageId = insertPageCopyMutable({\n      source: { data, pageId },\n      target: { data, folderId: currentFolder.id },\n    });\n  });\n  return newPageId;\n};\n\nconst deduplicateName = (usedNames: Set<string>, name: string) => {\n  const { name: baseName = name, copyNumber } =\n    // extract a number from \"name (copyNumber)\"\n    name.match(/^(?<name>.+) \\((?<copyNumber>\\d+)\\)$/)?.groups ?? {};\n  let nameNumber = Number(copyNumber ?? \"0\");\n  let newName: string;\n  do {\n    nameNumber += 1;\n    newName = `${baseName} (${nameNumber})`;\n  } while (usedNames.has(newName));\n  return newName;\n};\n\nconst deduplicateSlug = (usedSlugs: Set<string>, slug: string) => {\n  // extract a number from \"slug-N\"\n  const { slug: baseSlug = slug, copyNumber } =\n    slug.match(/^(?<slug>.+)-(?<copyNumber>\\d+)$/)?.groups ?? {};\n  let counter = Number(copyNumber ?? \"0\");\n  let newSlug: string;\n  do {\n    counter += 1;\n    newSlug = baseSlug ? `${baseSlug}-${counter}` : `copy-${counter}`;\n  } while (usedSlugs.has(newSlug));\n  return newSlug;\n};\n\nconst insertFolderCopyMutable = ({\n  source,\n  target,\n}: {\n  source: { data: WebstudioData; folderId: Folder[\"id\"] };\n  target: { data: WebstudioData; parentFolderId: Folder[\"id\"] };\n}): Folder[\"id\"] | undefined => {\n  const sourceFolder = source.data.pages.folders.find(\n    (folder) => folder.id === source.folderId\n  );\n  if (sourceFolder === undefined) {\n    return;\n  }\n\n  const parentFolder = target.data.pages.folders.find(\n    (folder) => folder.id === target.parentFolderId\n  );\n  const usedNames = new Set<string>();\n  const usedSlugs = new Set<string>();\n  for (const childId of parentFolder?.children ?? []) {\n    const childFolder = target.data.pages.folders.find(\n      (folder) => folder.id === childId\n    );\n    if (childFolder) {\n      usedNames.add(childFolder.name);\n      usedSlugs.add(childFolder.slug);\n      continue;\n    }\n    const childPage = target.data.pages.pages.find(\n      (page) => page.id === childId\n    );\n    if (childPage) {\n      usedNames.add(childPage.name);\n    }\n  }\n\n  // Create new folder with deduplicated name and slug\n  const newFolderId = nanoid();\n  const newFolder: Folder = {\n    id: newFolderId,\n    name: deduplicateName(usedNames, sourceFolder.name),\n    slug: deduplicateSlug(usedSlugs, sourceFolder.slug),\n    children: [],\n  };\n\n  // Add new folder to the folders array\n  target.data.pages.folders.push(newFolder);\n\n  // Register new folder in parent\n  for (const folder of target.data.pages.folders) {\n    if (folder.id === target.parentFolderId) {\n      folder.children.push(newFolderId);\n    }\n  }\n\n  // Duplicate all children (pages and nested folders)\n  for (const childId of sourceFolder.children) {\n    const childFolder = source.data.pages.folders.find(\n      (folder) => folder.id === childId\n    );\n\n    if (childFolder) {\n      // It's a nested folder - duplicate it recursively\n      insertFolderCopyMutable({\n        source: { data: source.data, folderId: childId },\n        target: { data: target.data, parentFolderId: newFolderId },\n      });\n    } else {\n      // It's a page - duplicate it\n      insertPageCopyMutable({\n        source: { data: source.data, pageId: childId },\n        target: { data: target.data, folderId: newFolderId },\n      });\n    }\n  }\n\n  return newFolderId;\n};\n\nexport const duplicateFolder = (folderId: Folder[\"id\"]) => {\n  const pages = $pages.get();\n  const currentFolder = findParentFolderByChildId(\n    folderId,\n    pages?.folders ?? []\n  );\n  if (currentFolder === undefined) {\n    return;\n  }\n  let newFolderId: undefined | string;\n  updateWebstudioData((data) => {\n    newFolderId = insertFolderCopyMutable({\n      source: { data, folderId },\n      target: { data, parentFolderId: currentFolder.id },\n    });\n  });\n  return newFolderId;\n};\n\nexport const isFolder = (id: string, folders: Array<Folder>) => {\n  return folders.some((folder) => folder.id === id);\n};\n\ntype DropTarget = {\n  parentId: string;\n  beforeId?: string;\n  afterId?: string;\n  indexWithinChildren: number;\n};\n\ntype TreeDropTarget = {\n  parentLevel: number;\n  beforeLevel?: number;\n  afterLevel?: number;\n};\n\nexport const getStoredDropTarget = (\n  selector: string[],\n  dropTarget: TreeDropTarget\n): undefined | DropTarget => {\n  const parentId = selector.at(-dropTarget.parentLevel - 1);\n  const beforeId =\n    dropTarget.beforeLevel === undefined\n      ? undefined\n      : selector.at(-dropTarget.beforeLevel - 1);\n  const afterId =\n    dropTarget.afterLevel === undefined\n      ? undefined\n      : selector.at(-dropTarget.afterLevel - 1);\n  const pages = $pages.get();\n  const parentFolder = pages?.folders.find((item) => item.id === parentId);\n  let indexWithinChildren = 0;\n  if (parentFolder) {\n    const beforeIndex = parentFolder.children.indexOf(beforeId ?? \"\");\n    const afterIndex = parentFolder.children.indexOf(afterId ?? \"\");\n    if (beforeIndex > -1) {\n      indexWithinChildren = beforeIndex;\n    } else if (afterIndex > -1) {\n      indexWithinChildren = afterIndex + 1;\n    }\n  }\n  if (parentId) {\n    return { parentId, beforeId, afterId, indexWithinChildren };\n  }\n};\n\nexport const canDrop = (dropTarget: DropTarget, folders: Folder[]) => {\n  // allow dropping only inside folders\n  if (isFolder(dropTarget.parentId, folders) === false) {\n    return false;\n  }\n  // forbid dropping in the beginning of root folder\n  // which is always used by home page\n  if (\n    isRootFolder({ id: dropTarget.parentId }) &&\n    dropTarget.indexWithinChildren === 0\n  ) {\n    return false;\n  }\n  return true;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/pages.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Tooltip,\n  Button,\n  SmallIconButton,\n  TreeNode,\n  TreeRoot,\n  TreeNodeLabel,\n  PanelTitle,\n  Separator,\n  TreeSortableItem,\n  type TreeDropTarget,\n  toast,\n  ScrollArea,\n  FloatingPanel,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport {\n  ChevronRightIcon,\n  FolderIcon,\n  HomeIcon,\n  EllipsesIcon,\n  NewFolderIcon,\n  NewPageIcon,\n  PageIcon,\n  DynamicPageIcon,\n} from \"@webstudio-is/icons\";\nimport { NewPageSettings, PageSettings } from \"./page-settings\";\nimport { PageContextMenu } from \"./page-context-menu\";\nimport {\n  DeletePageConfirmationDialog,\n  DeleteFolderConfirmationDialog,\n} from \"./confirmation-dialogs\";\nimport {\n  $editingPageId,\n  $isContentMode,\n  $isDesignMode,\n  $pages,\n} from \"~/shared/nano-states\";\nimport {\n  getAllChildrenAndSelf,\n  reparentOrphansMutable,\n  reparentPageOrFolderMutable,\n  deletePageMutable,\n  deleteFolderWithChildrenMutable,\n  duplicateFolder,\n  isFolder,\n  getStoredDropTarget,\n  canDrop,\n} from \"./page-utils\";\nimport {\n  FolderSettings,\n  NewFolderSettings,\n  newFolderId,\n} from \"./folder-settings\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { useMount } from \"~/shared/hook-utils/use-mount\";\nimport {\n  isRootFolder,\n  ROOT_FOLDER_ID,\n  type Folder,\n  type Page,\n  findPageByIdOrPath,\n} from \"@webstudio-is/sdk\";\nimport { atom, computed } from \"nanostores\";\nimport { isPathnamePattern } from \"~/builder/shared/url-pattern\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { $selectedPage, selectPage } from \"~/shared/awareness\";\n\nconst ItemSuffix = ({\n  isParentSelected,\n  itemId,\n  editingItemId,\n  onEdit,\n  type,\n}: {\n  isParentSelected: boolean;\n  itemId: string;\n  editingItemId: string | undefined;\n  onEdit: (itemId: string | undefined) => void;\n  type: \"folder\" | \"page\";\n}) => {\n  const isEditing = editingItemId === itemId;\n\n  const menuLabel =\n    type === \"page\"\n      ? isEditing\n        ? \"Close page settings\"\n        : \"Open page settings\"\n      : isEditing\n        ? \"Close folder settings\"\n        : \"Open folder settings\";\n\n  const buttonRef = useRef<HTMLButtonElement | null>(null);\n\n  const prevEditingItemId = useRef(editingItemId);\n  useEffect(() => {\n    // when settings panel close, move focus back to the menu button\n    if (\n      editingItemId === undefined &&\n      prevEditingItemId.current === itemId &&\n      buttonRef.current\n    ) {\n      buttonRef.current.focus();\n    }\n    prevEditingItemId.current = editingItemId;\n  }, [editingItemId, itemId]);\n\n  return (\n    <Tooltip content={menuLabel} disableHoverableContent>\n      <SmallIconButton\n        tabIndex={-1}\n        aria-label={menuLabel}\n        state={isParentSelected ? \"open\" : undefined}\n        onClick={() => onEdit(isEditing ? undefined : itemId)}\n        ref={buttonRef}\n        // forces to highlight tree node and show action\n        aria-current={isEditing}\n        icon={isEditing ? <ChevronRightIcon /> : <EllipsesIcon />}\n      />\n    </Tooltip>\n  );\n};\n\nconst useReparentOrphans = () => {\n  useMount(() => {\n    // Pages may not be loaded yet when switching betwen projects and the pages\n    // panel was already visible - it mounts faster than we load the pages.\n    if ($pages.get() === undefined) {\n      return;\n    }\n    serverSyncStore.createTransaction([$pages], (pages) => {\n      if (pages === undefined) {\n        return;\n      }\n      reparentOrphansMutable(pages);\n    });\n  });\n};\n\n// We want to keep the state when panel is closed and opened again.\nconst $expandedItems = atom(new Set<string>());\n\ntype PagesTreeItem =\n  | {\n      id: string;\n      selector: string[];\n      level: number;\n      isExpanded?: boolean;\n      type: \"page\";\n      page: Page;\n      isLastChild: boolean;\n      dropTarget?: TreeDropTarget;\n    }\n  | {\n      id: string;\n      selector: string[];\n      level: number;\n      isExpanded?: boolean;\n      type: \"folder\";\n      folder: Folder;\n      isLastChild: boolean;\n      dropTarget?: TreeDropTarget;\n    };\n\ntype DropTarget = {\n  parentId: string;\n  beforeId?: string;\n  afterId?: string;\n  indexWithinChildren: number;\n};\n\nconst $dropTarget = atom<undefined | DropTarget>();\n\nconst $flatPagesTree = computed(\n  [$pages, $expandedItems, $dropTarget],\n  (pagesData, expandedItems, dropTarget) => {\n    const flatPagesTree: PagesTreeItem[] = [];\n    if (pagesData === undefined) {\n      return flatPagesTree;\n    }\n    const folders = new Map(\n      pagesData.folders.map((folder) => [folder.id, folder])\n    );\n    const pages = new Map(pagesData.pages.map((page) => [page.id, page]));\n    pages.set(pagesData.homePage.id, pagesData.homePage);\n    const traverse = (selector: string[], level = 0, isLastChild = false) => {\n      const [itemId] = selector;\n      let treeItem: undefined | PagesTreeItem;\n      let lastTreeItem: undefined | PagesTreeItem;\n      const folder = folders.get(itemId);\n      const page = pages.get(itemId);\n      if (page) {\n        treeItem = {\n          id: itemId,\n          selector,\n          level,\n          type: \"page\",\n          page,\n          isLastChild,\n        };\n        lastTreeItem = treeItem;\n        flatPagesTree.push(treeItem);\n      }\n      if (folder) {\n        let isExpanded: undefined | boolean;\n        if (level > 0 && folder.children.length > 0) {\n          isExpanded = expandedItems.has(folder.id);\n        }\n        // hide root folder\n        if (itemId !== ROOT_FOLDER_ID) {\n          treeItem = {\n            id: itemId,\n            selector,\n            level,\n            isExpanded,\n            type: \"folder\",\n            folder,\n            isLastChild,\n          };\n          lastTreeItem = treeItem;\n          flatPagesTree.push(treeItem);\n        }\n        if (level === 0 || isExpanded) {\n          for (let index = 0; index < folder.children.length; index += 1) {\n            const childId = folder.children[index];\n            const isLastChild = index === folder.children.length - 1;\n            lastTreeItem = traverse(\n              [childId, ...selector],\n              level + 1,\n              isLastChild\n            );\n          }\n        }\n      }\n\n      if (treeItem && dropTarget?.beforeId === itemId) {\n        treeItem.dropTarget = {\n          parentLevel: level - 1,\n          beforeLevel: level,\n        };\n      }\n      if (lastTreeItem && dropTarget?.afterId === itemId) {\n        lastTreeItem.dropTarget = {\n          parentLevel: level - 1,\n          afterLevel: level,\n        };\n      }\n      return lastTreeItem;\n    };\n    traverse([ROOT_FOLDER_ID]);\n    return flatPagesTree;\n  }\n);\n\nconst PagesTree = ({\n  onSelect,\n  selectedPageId,\n  onEdit,\n  editingItemId,\n}: {\n  onSelect: (pageId: string) => void;\n  selectedPageId: string;\n  onEdit: (pageId: string | undefined) => void;\n  editingItemId?: string;\n}) => {\n  const pages = useStore($pages);\n  const flatPagesTree = useStore($flatPagesTree);\n  const dropTarget = useStore($dropTarget);\n  useReparentOrphans();\n\n  if (pages === undefined) {\n    return null;\n  }\n\n  return (\n    <ScrollArea\n      direction=\"both\"\n      css={{\n        width: \"100%\",\n        overflow: \"hidden\",\n        flexBasis: 0,\n        flexGrow: 1,\n      }}\n    >\n      <TreeRoot>\n        {flatPagesTree.map((item, index) => {\n          const handleExpand = (isExpanded: boolean, all: boolean) => {\n            const expandedItems = new Set($expandedItems.get());\n            const items = all\n              ? getAllChildrenAndSelf(item.id, pages.folders, \"folder\")\n              : [item.id];\n            for (const itemId of items) {\n              if (isExpanded) {\n                expandedItems.add(itemId);\n              } else {\n                expandedItems.delete(itemId);\n              }\n            }\n            $expandedItems.set(expandedItems);\n          };\n\n          return (\n            <TreeSortableItem\n              key={item.id}\n              level={item.level}\n              isExpanded={item.isExpanded}\n              isLastChild={item.isLastChild}\n              data={item}\n              canDrag={() => {\n                if ($isContentMode.get()) {\n                  return false;\n                }\n\n                // forbid dragging home page\n                if (item.id === pages.homePage.id) {\n                  toast.error(\"Home page cannot be moved\");\n                  return false;\n                }\n                return true;\n              }}\n              onExpand={(isExpanded) => handleExpand(isExpanded, false)}\n              dropTarget={item.dropTarget}\n              onDropTargetChange={(dropTarget) => {\n                if (dropTarget) {\n                  const storedDropTarget = getStoredDropTarget(\n                    item.selector,\n                    dropTarget\n                  );\n                  if (\n                    storedDropTarget &&\n                    canDrop(storedDropTarget, pages.folders)\n                  ) {\n                    $dropTarget.set(storedDropTarget);\n                  }\n                } else {\n                  $dropTarget.set(undefined);\n                }\n              }}\n              onDrop={(item) => {\n                if (dropTarget === undefined) {\n                  return;\n                }\n                updateWebstudioData((data) => {\n                  reparentPageOrFolderMutable(\n                    data.pages.folders,\n                    item.id,\n                    dropTarget.parentId,\n                    dropTarget.indexWithinChildren\n                  );\n                });\n                $dropTarget.set(undefined);\n              }}\n            >\n              <TreeNode\n                level={item.level}\n                tabbable={index === 0}\n                isSelected={item.id === selectedPageId}\n                isHighlighted={dropTarget?.parentId === item.id}\n                isExpanded={item.isExpanded}\n                onExpand={handleExpand}\n                buttonProps={{\n                  onClick: (event) => {\n                    if (item.type === \"folder\") {\n                      handleExpand(item.isExpanded === false, event.altKey);\n                    }\n                    if (item.type === \"page\") {\n                      onSelect(item.id);\n                    }\n                  },\n                  ...(item.type === \"page\" &&\n                    item.id !== pages?.homePage.id && {\n                      \"data-page-id\": item.id,\n                    }),\n                  ...(item.type === \"folder\" &&\n                    !isRootFolder({ id: item.id }) && {\n                      \"data-folder-id\": item.id,\n                    }),\n                }}\n                action={\n                  <ItemSuffix\n                    type={item.type}\n                    isParentSelected={item.id === selectedPageId}\n                    itemId={item.id}\n                    editingItemId={editingItemId}\n                    onEdit={onEdit}\n                  />\n                }\n              >\n                {item.type === \"folder\" && (\n                  <TreeNodeLabel prefix={<FolderIcon />}>\n                    {item.folder.name}\n                  </TreeNodeLabel>\n                )}\n                {item.type === \"page\" && (\n                  <TreeNodeLabel\n                    prefix={\n                      item.id === pages?.homePage.id ? (\n                        <HomeIcon />\n                      ) : isPathnamePattern(item.page.path) ? (\n                        <DynamicPageIcon />\n                      ) : (\n                        <PageIcon />\n                      )\n                    }\n                  >\n                    {item.page.name}\n                  </TreeNodeLabel>\n                )}\n              </TreeNode>\n            </TreeSortableItem>\n          );\n        })}\n      </TreeRoot>\n    </ScrollArea>\n  );\n};\n\nconst newPageId = \"new-page\";\n\nconst PageEditor = ({\n  editingPageId,\n  onClose,\n}: {\n  editingPageId: string;\n  onClose: () => void;\n}) => {\n  const currentPage = useStore($selectedPage);\n  const pages = useStore($pages);\n  const [pageIdToDelete, setPageIdToDelete] = useState<string | undefined>();\n\n  if (editingPageId === newPageId) {\n    return (\n      <NewPageSettings\n        onClose={onClose}\n        onSuccess={(pageId) => {\n          onClose();\n          selectPage(pageId);\n        }}\n      />\n    );\n  }\n\n  const handleRequestDelete = () => {\n    if (pages) {\n      const page = findPageByIdOrPath(editingPageId, pages);\n      if (page) {\n        setPageIdToDelete(page.id);\n      }\n    }\n  };\n\n  const handleDelete = () => {\n    if (pageIdToDelete) {\n      updateWebstudioData((data) => {\n        deletePageMutable(pageIdToDelete, data);\n      });\n    }\n    onClose();\n    // switch to home page when deleted currently selected page\n    if (editingPageId === currentPage?.id) {\n      if (pages) {\n        selectPage(pages.homePage.id);\n      }\n    }\n  };\n\n  return (\n    <>\n      <PageSettings\n        onClose={onClose}\n        onDelete={handleRequestDelete}\n        onDuplicate={(newPageId) => {\n          onClose();\n          selectPage(newPageId);\n        }}\n        pageId={editingPageId}\n        key={editingPageId}\n      />\n      {pageIdToDelete && pages && (\n        <DeletePageConfirmationDialog\n          page={findPageByIdOrPath(pageIdToDelete, pages)!}\n          onClose={() => setPageIdToDelete(undefined)}\n          onConfirm={() => {\n            setPageIdToDelete(undefined);\n            handleDelete();\n          }}\n        />\n      )}\n    </>\n  );\n};\n\nconst FolderEditor = ({\n  editingFolderId,\n  onClose,\n}: {\n  editingFolderId: string;\n  onClose: () => void;\n}) => {\n  const pages = useStore($pages);\n  const [folderIdToDelete, setFolderIdToDelete] = useState<\n    string | undefined\n  >();\n\n  const handleRequestDelete = () => {\n    setFolderIdToDelete(editingFolderId);\n  };\n\n  const handleDuplicate = () => {\n    const newFolderId = duplicateFolder(editingFolderId);\n    if (newFolderId) {\n      $editingPageId.set(newFolderId);\n    }\n  };\n\n  if (editingFolderId === newFolderId) {\n    return (\n      <NewFolderSettings\n        key={newFolderId}\n        onClose={onClose}\n        onSuccess={onClose}\n      />\n    );\n  }\n\n  const handleDelete = () => {\n    if (folderIdToDelete) {\n      updateWebstudioData((data) => {\n        const { pageIds } = deleteFolderWithChildrenMutable(\n          folderIdToDelete,\n          data.pages.folders\n        );\n        pageIds.forEach((pageId) => {\n          deletePageMutable(pageId, data);\n        });\n      });\n    }\n    onClose();\n  };\n\n  const folder = pages?.folders.find(({ id }) => id === editingFolderId);\n\n  return (\n    <>\n      <FolderSettings\n        onClose={onClose}\n        onRequestDelete={handleRequestDelete}\n        onDuplicate={handleDuplicate}\n        folderId={editingFolderId}\n        key={editingFolderId}\n      />\n      {folderIdToDelete && folder && (\n        <DeleteFolderConfirmationDialog\n          folder={folder}\n          onClose={() => setFolderIdToDelete(undefined)}\n          onConfirm={() => {\n            setFolderIdToDelete(undefined);\n            handleDelete();\n          }}\n        />\n      )}\n    </>\n  );\n};\n\nexport const PagesPanel = ({ onClose }: { onClose: () => void }) => {\n  const currentPage = useStore($selectedPage);\n  const editingItemId = useStore($editingPageId);\n  const pages = useStore($pages);\n  const isDesignMode = useStore($isDesignMode);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [pageIdToDelete, setPageIdToDelete] = useState<string | undefined>();\n  const [folderIdToDelete, setFolderIdToDelete] = useState<\n    string | undefined\n  >();\n\n  if (currentPage === undefined || pages === undefined) {\n    return;\n  }\n\n  const handlePageDeleteConfirm = () => {\n    if (pageIdToDelete) {\n      updateWebstudioData((data) => {\n        deletePageMutable(pageIdToDelete, data);\n      });\n      // Close settings if this page was being edited\n      if (editingItemId === pageIdToDelete) {\n        $editingPageId.set(undefined);\n      }\n    }\n    setPageIdToDelete(undefined);\n  };\n\n  const handleDeleteFolderConfirm = () => {\n    if (folderIdToDelete) {\n      updateWebstudioData((data) => {\n        const { pageIds } = deleteFolderWithChildrenMutable(\n          folderIdToDelete,\n          data.pages.folders\n        );\n        pageIds.forEach((pageId) => {\n          deletePageMutable(pageId, data);\n        });\n      });\n      // Close settings if this folder was being edited\n      if (editingItemId === folderIdToDelete) {\n        $editingPageId.set(undefined);\n      }\n    }\n    setFolderIdToDelete(undefined);\n  };\n\n  return (\n    <div ref={containerRef} data-floating-panel-container>\n      <PanelTitle\n        suffix={\n          <>\n            {isDesignMode && (\n              <>\n                <Tooltip content=\"New folder\" side=\"bottom\">\n                  <Button\n                    onClick={() => {\n                      $editingPageId.set(\n                        editingItemId === newFolderId ? undefined : newFolderId\n                      );\n                    }}\n                    aria-label=\"New folder\"\n                    prefix={<NewFolderIcon />}\n                    color=\"ghost\"\n                  />\n                </Tooltip>\n                <Tooltip content=\"New page\" side=\"bottom\">\n                  <Button\n                    onClick={() => {\n                      $editingPageId.set(\n                        editingItemId === newPageId ? undefined : newPageId\n                      );\n                    }}\n                    aria-label=\"New page\"\n                    prefix={<NewPageIcon />}\n                    color=\"ghost\"\n                  />\n                </Tooltip>\n              </>\n            )}\n          </>\n        }\n      >\n        Pages\n      </PanelTitle>\n      <Separator />\n\n      <PageContextMenu\n        onRequestDeletePage={setPageIdToDelete}\n        onRequestDeleteFolder={setFolderIdToDelete}\n      >\n        <div>\n          <PagesTree\n            selectedPageId={currentPage.id}\n            onSelect={(itemId) => {\n              selectPage(itemId);\n              onClose();\n            }}\n            editingItemId={editingItemId}\n            onEdit={(itemId) => {\n              // always select page when edit its settings\n              if (itemId && isFolder(itemId, pages.folders) === false) {\n                selectPage(itemId);\n              }\n              $editingPageId.set(itemId);\n            }}\n          />\n        </div>\n      </PageContextMenu>\n      {editingItemId !== undefined && (\n        <FloatingPanel\n          content={\n            editingItemId === newFolderId ||\n            isFolder(editingItemId, pages.folders) ? (\n              <FolderEditor\n                editingFolderId={editingItemId}\n                onClose={() => $editingPageId.set(undefined)}\n              />\n            ) : (\n              <PageEditor\n                editingPageId={editingItemId}\n                onClose={() => $editingPageId.set(undefined)}\n              />\n            )\n          }\n          placement=\"right-start\"\n          width={Number.parseFloat(rawTheme.spacing[35])}\n          open={true}\n          onOpenChange={(isOpen) => {\n            if (!isOpen) {\n              $editingPageId.set(undefined);\n            }\n          }}\n        >\n          <span style={{ display: \"none\" }} />\n        </FloatingPanel>\n      )}\n      {pageIdToDelete && (\n        <DeletePageConfirmationDialog\n          page={findPageByIdOrPath(pageIdToDelete, pages)!}\n          onClose={() => setPageIdToDelete(undefined)}\n          onConfirm={handlePageDeleteConfirm}\n        />\n      )}\n      {folderIdToDelete && (\n        <DeleteFolderConfirmationDialog\n          folder={pages.folders.find(({ id }) => id === folderIdToDelete)!}\n          onClose={() => setFolderIdToDelete(undefined)}\n          onConfirm={handleDeleteFolderConfirm}\n        />\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/search-preview.stories.tsx",
    "content": "import { Box, StorySection } from \"@webstudio-is/design-system\";\nimport { SearchPreview as SearchPreviewComponent } from \"./search-preview\";\n\nexport default {\n  title: \"Pages/Search Preview\",\n  component: SearchPreviewComponent,\n};\n\nconst faviconDataUrl = `data:image/svg+xml,${encodeURIComponent(\n  `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\">\n    <rect width=\"16\" height=\"16\" rx=\"3\" fill=\"#4a6a8a\"/>\n    <text x=\"8\" y=\"8\" text-anchor=\"middle\" dominant-baseline=\"central\"\n      font-family=\"sans-serif\" font-size=\"10\" font-weight=\"bold\" fill=\"#fff\">W</text>\n  </svg>`\n)}`;\n\nexport const SearchPreview = () => (\n  <StorySection title=\"Search Preview\">\n    <Box css={{ width: 600, margin: 20 }}>\n      <SearchPreviewComponent\n        pageUrl=\"https://webstudio.is/blog-webstudios-architecture-an-overview\"\n        titleLink=\"Blog: Webstudio's Architecture - an overview\"\n        siteName=\"Webstudio\"\n        faviconUrl={faviconDataUrl}\n        snippet=\"This is an introduction for developers who want to contribute to Webstudio Core. However, it may also be an interesting read for ambitious designers who want to better understand the future of Webstudio and Visual Development.\"\n      />\n    </Box>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/search-preview.tsx",
    "content": "import { Box, Flex, Grid } from \"@webstudio-is/design-system\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport { formatUrl, truncateByWords, truncate } from \"./social-utils\";\n\n/**\n * Full description with links https://developers.google.com/search/docs/appearance/visual-elements-gallery\n */\ntype SearchPreviewProps = {\n  /**\n   *  https://developers.google.com/search/docs/appearance/site-names\n   * ```html\n   *   <script type=\"application/ld+json\">\n   *    {\n   *      \"@context\" : \"https://schema.org\",\n   *      \"@type\" : \"WebSite\",\n   *      \"name\" : \"Example\",\n   *      \"url\" : \"https://example.com/\"\n   *    }\n   *   </script>\n   * ```\n   */\n  siteName: string;\n\n  /**\n   * Domain + Visible Url, The URL of the page to preview in search results\n   * or https://developers.google.com/search/docs/appearance/structured-data/breadcrumb\n   */\n  pageUrl: string;\n\n  /**\n   * https://developers.google.com/search/docs/appearance/title-link\n   * ```html\n   *    <title>Blue title link example</title>\n   * ```\n   */\n  titleLink: string;\n\n  /**\n   * Snippets are automatically created from page content https://developers.google.com/search/docs/appearance/snippet\n   * sometimes meta description or structured data can be used\n   * ```html\n   *   <meta name=\"description\" content=\"This is the description of the content of the page\">\n   * ```\n   */\n  snippet: string;\n\n  /**\n   * https://developers.google.com/search/docs/appearance/favicon-in-search\n   */\n  faviconUrl: string;\n};\n\n/**\n * ... rotated 90 degrees\n */\nconst VerticalThreePointIcon = () => (\n  <svg\n    height=\"18\"\n    focusable=\"false\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 24 24\"\n  >\n    <path d=\"M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\"></path>\n  </svg>\n);\n\nexport const SearchPreview = (props: SearchPreviewProps) => {\n  return (\n    <Grid\n      gap={1}\n      css={{\n        fontFamily: \"arial,sans-serif\",\n        \"& *\": {\n          boxSizing: \"border-box\",\n        },\n      }}\n    >\n      <Flex\n        align={\"center\"}\n        css={{\n          gap: 12,\n        }}\n      >\n        <Flex\n          css={{\n            width: 28,\n            height: 28,\n            borderRadius: \"50%\",\n            backgroundColor: \"#f1f3f4\",\n            border: \"1px solid #ecedef\",\n          }}\n          justify={\"center\"}\n          align={\"center\"}\n        >\n          <Image\n            width={18}\n            height={18}\n            loader={wsImageLoader}\n            src={props.faviconUrl}\n          />\n        </Flex>\n        <Grid>\n          <Flex\n            css={{\n              color: \"#202124\",\n              fontSize: \"14px\",\n              lineHeight: \"20px\",\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {truncate(truncateByWords(props.siteName), 60)}\n          </Flex>\n          <Flex\n            css={{\n              fontSize: \"12px\",\n              lineHeight: \"18px\",\n              color: \"#4d5156\",\n              gap: 8,\n            }}\n            align={\"center\"}\n          >\n            {/*todo add > instead of / */ formatUrl(truncate(props.pageUrl))}\n            <VerticalThreePointIcon />\n          </Flex>\n        </Grid>\n      </Flex>\n      <div />\n      <Box\n        css={{\n          fontSize: \"20px\",\n          fontWeight: 400,\n          color: \"#1a0dab\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n          whiteSpace: \"nowrap\",\n          width: \"100%\",\n        }}\n      >\n        {truncateByWords(props.titleLink, 60)}\n      </Box>\n      <Box\n        css={{\n          lineHeight: 1.58,\n          fontSize: 14,\n          color: \"#4d5156\",\n          \"-webkit-line-clamp\": 2,\n          display: \"-webkit-box\",\n          \"-webkit-box-orient\": \"vertical\",\n          overflow: \"hidden\",\n        }}\n      >\n        {truncateByWords(props.snippet)}\n      </Box>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/social-preview.stories.tsx",
    "content": "import { Flex, StorySection } from \"@webstudio-is/design-system\";\nimport { SocialPreview as SocialPreviewComponent } from \"./social-preview\";\n\nconst placeholderImage = `data:image/svg+xml,${encodeURIComponent(\n  `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\">\n    <rect width=\"1200\" height=\"630\" fill=\"#c0d0e0\"/>\n    <text x=\"600\" y=\"315\" text-anchor=\"middle\" dominant-baseline=\"central\"\n      font-family=\"sans-serif\" font-size=\"48\" fill=\"#4a6a8a\">1200 × 630</text>\n  </svg>`\n)}`;\n\nexport default {\n  title: \"Pages/Social Preview\",\n  component: SocialPreviewComponent,\n};\n\nexport const SocialPreview = () => (\n  <StorySection title=\"Social Preview\">\n    <Flex direction=\"column\" gap=\"5\" css={{ width: 600, margin: 20 }}>\n      <SocialPreviewComponent\n        ogImageUrl={placeholderImage}\n        ogUrl=\"https://webstudio.is/blog/architecture-overview\"\n        ogTitle=\"Webstudio's Architecture - An Overview\"\n        ogDescription=\"This is an introduction for developers who want to contribute to Webstudio Core.\"\n      />\n      <SocialPreviewComponent\n        ogUrl=\"https://webstudio.is/about\"\n        ogTitle=\"About Webstudio\"\n        ogDescription=\"A visual development platform for building professional websites without code.\"\n      />\n      <SocialPreviewComponent\n        ogUrl=\"https://webstudio.is/blog/very-long-url-path/that-should-be-truncated/at-some-point-because-its-too-long\"\n        ogTitle=\"This Is a Very Long Title That Should Be Truncated After a Certain Number of Characters to Prevent Layout Issues\"\n        ogDescription=\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\"\n      />\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/social-preview.tsx",
    "content": "import { Box, Grid, Label, css, theme } from \"@webstudio-is/design-system\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport { truncateByWords, truncate } from \"./social-utils\";\n\ntype SocialPreviewProps = {\n  ogImageUrl?: string;\n  ogUrl: string;\n  ogTitle: string;\n  ogDescription: string;\n};\n\nconst imgStyle = css({\n  borderTopLeftRadius: theme.borderRadius[4],\n  borderTopRightRadius: theme.borderRadius[4],\n  width: \"100%\",\n  aspectRatio: \"1.91\",\n  background: \"#DFE3E6\",\n  borderBottom: `1px solid ${theme.colors.borderMain}`,\n  variants: {\n    hasImage: {\n      true: {\n        objectFit: \"cover\",\n      },\n    },\n  },\n});\n\nexport const SocialPreview = ({\n  ogImageUrl,\n  ogDescription,\n  ogTitle,\n  ogUrl,\n}: SocialPreviewProps) => {\n  return (\n    <Grid gap={1}>\n      <Label>Social sharing preview</Label>\n\n      <Grid\n        gap={1}\n        css={{\n          borderRadius: theme.borderRadius[4],\n          border: `1px solid ${theme.colors.borderMain}`,\n          backgroundColor: theme.colors.white,\n        }}\n      >\n        <Image\n          src={ogImageUrl}\n          loader={wsImageLoader}\n          className={imgStyle({\n            hasImage:\n              ogImageUrl === undefined || ogImageUrl === \"\" ? false : true,\n          })}\n        />\n\n        <Grid\n          gap={1}\n          css={{\n            margin: 12,\n          }}\n        >\n          <Box\n            css={{\n              color: \"#4D5156\",\n              fontFamily: \"Arial\",\n              fontSize: \"12px\",\n              lineHeight: \"16px\",\n            }}\n          >\n            {truncate(ogUrl)}\n          </Box>\n          <Box\n            css={{\n              color: \"#18283E\",\n              fontFamily: \"Arial\",\n              fontSize: \"14px\",\n              fontWeight: 700,\n              lineHeight: \"18px\",\n              overflow: \"hidden\",\n              textOverflow: \"ellipsis\",\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {truncateByWords(ogTitle, 60)}\n          </Box>\n          <Box\n            css={{\n              color: \"#4D5156\",\n              fontFamily: \"Arial\",\n              fontSize: \"12px\",\n              fontWeight: 400,\n              lineHeight: \"16px\",\n              \"-webkit-line-clamp\": 2,\n              display: \"-webkit-box\",\n              \"-webkit-box-orient\": \"vertical\",\n              overflow: \"hidden\",\n            }}\n          >\n            {truncateByWords(ogDescription)}\n          </Box>\n        </Grid>\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/social-utils.test.ts",
    "content": "/* eslint-disable no-irregular-whitespace */\nimport { expect, test } from \"vitest\";\nimport { truncateByWords, truncate } from \"./social-utils\";\n\ntest(\"Truncates description\", () => {\n  expect(\n    truncateByWords(\n      \"Accept everyone around you. Part of being a good person is not being judgmental. You accept everyone, no matter what race, age, sexual orientation, gender identity, or culture they are. Realize that everybody has feelings, every person is valid, and everyone should always be treated with respect.\\n\\nBe respectful of elderly people. \"\n    )\n  ).toMatchInlineSnapshot(\n    `\"Accept everyone around you. Part of being a good person is not being judgmental. You accept everyone, no matter what race, age, sexual orientation, gender ...\"`\n  );\n});\n\ntest(\"Truncates description\", () => {\n  expect(\n    truncateByWords(\n      \"The best way to determine the best balance for you is by learning to check in with your inner compass — and your results. With intentionality and a little creativity, you can recalibrate your expectations and reset your work-home balance.&nbsp\"\n    )\n  ).toMatchInlineSnapshot(\n    `\"The best way to determine the best balance for you is by learning to check in with your inner compass — and your results. With intentionality and a little ...\"`\n  );\n});\n\ntest(\"Truncates description\", () => {\n  expect(\n    truncateByWords(\n      \"The meta description summarizes a page&#8217;s content and presents that to users in the search results. It&#8217;s one of the first things people will likely see when searching for something, so optimizing it is crucial for SEO. It&#8217;s your chance to persuade users to click on \"\n    )\n  ).toMatchInlineSnapshot(\n    `\"The meta description summarizes a page&#8217;s content and presents that to users in the search results. It&#8217;s one of the first things people will ...\"`\n  );\n});\n\ntest(\"Truncates description\", () => {\n  expect(\n    truncateByWords(\n      \"A meta description (also known as a “description tag”) is an HTML attribute designed to describe the content of a webpage. Here’s what a meta description looks like in HTML form:\"\n    )\n  ).toMatchInlineSnapshot(\n    `\"A meta description (also known as a “description tag”) is an HTML attribute designed to describe the content of a webpage. Here’s what a meta description ...\"`\n  );\n});\n\ntest(\"Truncates url\", () => {\n  expect(\n    truncate(\"https://ahrefs.com/writing-tools/meta-description-generator\")\n  ).toMatchInlineSnapshot(\n    `\"https://ahrefs.com/writing-tools/meta-description-gen...\"`\n  );\n});\n\ntest(\"Truncates url\", () => {\n  expect(\n    truncate(\"https://university.webflow.com›lesson›seo-title-meta-description\")\n  ).toMatchInlineSnapshot(\n    `\"https://university.webflow.com›lesson›seo-title-meta-...\"`\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/pages/social-utils.ts",
    "content": "/**\n * Exact google truncation logic is not known, but this is a close approximation\n */\nexport const truncateByWords = (\n  description: string,\n  maxLength: number = 155\n) => {\n  if (description.length <= maxLength) {\n    return description;\n  }\n\n  const ellipsis = \"\\u00A0...\";\n  let truncated = description.substring(0, maxLength);\n  const lastSpaceIndex = truncated.lastIndexOf(\" \");\n\n  // If there's a space to truncate at, use it; otherwise, use the max length\n  truncated =\n    lastSpaceIndex > 0 ? truncated.substring(0, lastSpaceIndex) : truncated;\n\n  return `${truncated}${ellipsis}`;\n};\n\nexport const truncate = (pageUrl: string, maxLength = 53) => {\n  if (pageUrl.length <= maxLength) {\n    return pageUrl;\n  }\n\n  const ellipsis = \"...\";\n  const truncated = pageUrl.substring(0, maxLength);\n\n  return `${truncated}${ellipsis}`;\n};\n\nexport const formatUrl = (urlString: string) => {\n  try {\n    const url = new URL(urlString);\n\n    return `${url.origin}${url.pathname.split(\"/\").join(\" › \")}`;\n  } catch {\n    return urlString;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/add-domain.tsx",
    "content": "import {\n  Button,\n  Flex,\n  InputField,\n  Label,\n  Separator,\n  theme,\n  Text,\n  Grid,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport { validateDomain } from \"@webstudio-is/domain\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { useId, useOptimistic, useRef, useState } from \"react\";\nimport { TerminalIcon } from \"@webstudio-is/icons\";\nimport { nativeClient } from \"~/shared/trpc/trpc-client\";\nimport { extractCname } from \"./cname\";\n\ntype DomainsAddProps = {\n  projectId: Project[\"id\"];\n  onCreate: (domain: string) => void;\n  onExportClick: () => void;\n  refresh: () => Promise<void>;\n};\n\nexport const AddDomain = ({\n  projectId,\n  onCreate,\n  refresh,\n  onExportClick,\n}: DomainsAddProps) => {\n  const id = useId();\n  const [isOpen, setIsOpen] = useState(false);\n  const [error, setError] = useState<string>();\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const [isPending, setIsPendingOptimistic] = useOptimistic(false);\n\n  const handleCreateDomain = async (formData: FormData) => {\n    // Will be automatically reset on action end\n    setIsPendingOptimistic(true);\n\n    let domain = formData.get(\"domain\")?.toString() ?? \"\";\n    const validationResult = validateDomain(domain);\n\n    if (validationResult.success === false) {\n      setError(validationResult.error);\n      return;\n    }\n\n    // detect provider only when root domain is specified\n    if (extractCname(domain) === \"@\") {\n      const registrar = await nativeClient.domain.findDomainRegistrar.query({\n        domain,\n      });\n      // enforce www subdomain when no support for cname flattening\n      // and root cname can conflict with MX or NS\n      if (registrar.known && !registrar.cnameFlattening) {\n        domain = `www.${domain}`;\n      }\n    }\n\n    const result = await nativeClient.domain.create.mutate({\n      domain,\n      projectId,\n    });\n\n    if (result.success === false) {\n      toast.error(result.error);\n      setError(result.error);\n      return;\n    }\n\n    onCreate(domain);\n\n    await refresh();\n\n    setIsOpen(false);\n  };\n\n  return (\n    <>\n      <Flex\n        gap={2}\n        shrink={false}\n        direction={\"column\"}\n        onKeyDown={(event) => {\n          if (event.key === \"Escape\") {\n            setIsOpen(false);\n            event.preventDefault();\n          }\n        }}\n      >\n        {isOpen && (\n          <>\n            <Label htmlFor={id} text=\"title\">\n              New Domain\n            </Label>\n            <InputField\n              id={id}\n              name=\"domain\"\n              autoFocus\n              placeholder=\"your-domain.com\"\n              disabled={isPending}\n              onKeyDown={(event) => {\n                if (event.key === \"Enter\") {\n                  buttonRef.current\n                    ?.closest(\"form\")\n                    ?.requestSubmit(buttonRef.current);\n                }\n                if (event.key === \"Escape\") {\n                  setIsOpen(false);\n                  event.preventDefault();\n                }\n              }}\n              color={error !== undefined ? \"error\" : undefined}\n            />\n            {error !== undefined && (\n              <>\n                <Text color=\"destructive\">{error}</Text>\n              </>\n            )}\n          </>\n        )}\n\n        <Grid gap={2} columns={2}>\n          <Button\n            ref={buttonRef}\n            formAction={handleCreateDomain}\n            state={isPending ? \"pending\" : undefined}\n            color={isOpen ? \"primary\" : \"neutral\"}\n            onClick={(event) => {\n              if (isOpen === false) {\n                setIsOpen(true);\n                event.preventDefault();\n                return;\n              }\n            }}\n          >\n            {isOpen ? \"Add domain\" : \"Add a new domain\"}\n          </Button>\n\n          <Button\n            color={\"dark\"}\n            prefix={<TerminalIcon />}\n            type=\"button\"\n            onClick={onExportClick}\n          >\n            Export\n          </Button>\n        </Grid>\n      </Flex>\n      {isOpen && <Separator css={{ mb: theme.spacing[5] }} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/cname.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\n\nimport { extractCname } from \"./cname\";\n\ndescribe(\"extractCname from ordinary domains\", () => {\n  test('should return \"@\" for top level like domains', () => {\n    expect(extractCname(\"example\")).toBe(\"@\");\n  });\n\n  test('should return \"@\" for root level domains', () => {\n    expect(extractCname(\"example.com\")).toBe(\"@\");\n  });\n\n  test(\"should return the subdomain\", () => {\n    expect(extractCname(\"sub.example.com\")).toBe(\"sub\");\n  });\n\n  test(\"should return all subdomains\", () => {\n    expect(extractCname(\"sub.sub.example.com\")).toBe(\"sub.sub\");\n  });\n});\n\ndescribe(\"extractCname from public suffix domains\", () => {\n  test('should return \"@\"', () => {\n    expect(extractCname(\"co.za\")).toBe(\"@\");\n  });\n\n  test('should return \"@\"', () => {\n    expect(extractCname(\"example.co.za\")).toBe(\"@\");\n  });\n\n  test(\"should return all the subdomain\", () => {\n    expect(extractCname(\"sub.example.co.za\")).toBe(\"sub\");\n  });\n\n  test(\"should handle domains with multiple public suffixes correctly\", () => {\n    expect(extractCname(\"sub.sub.example.co.za\")).toBe(\"sub.sub\");\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/cname.ts",
    "content": "export const extractCname = (domain: string): string => {\n  // Split the domain into parts\n  const parts: string[] = domain.split(\".\");\n\n  for (let i = 0; i < parts.length; i++) {\n    if (publicSuffixList.has(parts.slice(i).join(\".\"))) {\n      const result = parts.slice(0, Math.max(i - 1, 0));\n      return result.length === 0 ? \"@\" : result.join(\".\");\n    }\n  }\n\n  // If not a public suffix, check the number of parts\n  if (parts.length <= 2) {\n    return \"@\"; // Root level or direct subdomain of a TLD\n  } else {\n    // Multiple subdomains, return all except the last two parts\n    return parts.slice(0, -2).join(\".\");\n  }\n};\n\n// https://publicsuffix.org/list/public_suffix_list.dat\nconst publicSuffixList = new Set([\n  \"com.ac\",\n  \"edu.ac\",\n  \"gov.ac\",\n  \"net.ac\",\n  \"mil.ac\",\n  \"org.ac\",\n  \"nom.ad\",\n  \"co.ae\",\n  \"net.ae\",\n  \"org.ae\",\n  \"sch.ae\",\n  \"ac.ae\",\n  \"gov.ae\",\n  \"mil.ae\",\n  \"accident-investigation.aero\",\n  \"accident-prevention.aero\",\n  \"aerobatic.aero\",\n  \"aeroclub.aero\",\n  \"aerodrome.aero\",\n  \"agents.aero\",\n  \"aircraft.aero\",\n  \"airline.aero\",\n  \"airport.aero\",\n  \"air-surveillance.aero\",\n  \"airtraffic.aero\",\n  \"air-traffic-control.aero\",\n  \"ambulance.aero\",\n  \"amusement.aero\",\n  \"association.aero\",\n  \"author.aero\",\n  \"ballooning.aero\",\n  \"broker.aero\",\n  \"caa.aero\",\n  \"cargo.aero\",\n  \"catering.aero\",\n  \"certification.aero\",\n  \"championship.aero\",\n  \"charter.aero\",\n  \"civilaviation.aero\",\n  \"club.aero\",\n  \"conference.aero\",\n  \"consultant.aero\",\n  \"consulting.aero\",\n  \"control.aero\",\n  \"council.aero\",\n  \"crew.aero\",\n  \"design.aero\",\n  \"dgca.aero\",\n  \"educator.aero\",\n  \"emergency.aero\",\n  \"engine.aero\",\n  \"engineer.aero\",\n  \"entertainment.aero\",\n  \"equipment.aero\",\n  \"exchange.aero\",\n  \"express.aero\",\n  \"federation.aero\",\n  \"flight.aero\",\n  \"fuel.aero\",\n  \"gliding.aero\",\n  \"government.aero\",\n  \"groundhandling.aero\",\n  \"group.aero\",\n  \"hanggliding.aero\",\n  \"homebuilt.aero\",\n  \"insurance.aero\",\n  \"journal.aero\",\n  \"journalist.aero\",\n  \"leasing.aero\",\n  \"logistics.aero\",\n  \"magazine.aero\",\n  \"maintenance.aero\",\n  \"media.aero\",\n  \"microlight.aero\",\n  \"modelling.aero\",\n  \"navigation.aero\",\n  \"parachuting.aero\",\n  \"paragliding.aero\",\n  \"passenger-association.aero\",\n  \"pilot.aero\",\n  \"press.aero\",\n  \"production.aero\",\n  \"recreation.aero\",\n  \"repbody.aero\",\n  \"res.aero\",\n  \"research.aero\",\n  \"rotorcraft.aero\",\n  \"safety.aero\",\n  \"scientist.aero\",\n  \"services.aero\",\n  \"show.aero\",\n  \"skydiving.aero\",\n  \"software.aero\",\n  \"student.aero\",\n  \"trader.aero\",\n  \"trading.aero\",\n  \"trainer.aero\",\n  \"union.aero\",\n  \"workinggroup.aero\",\n  \"works.aero\",\n  \"gov.af\",\n  \"com.af\",\n  \"org.af\",\n  \"net.af\",\n  \"edu.af\",\n  \"com.ag\",\n  \"org.ag\",\n  \"net.ag\",\n  \"co.ag\",\n  \"nom.ag\",\n  \"off.ai\",\n  \"com.ai\",\n  \"net.ai\",\n  \"org.ai\",\n  \"com.al\",\n  \"edu.al\",\n  \"gov.al\",\n  \"mil.al\",\n  \"net.al\",\n  \"org.al\",\n  \"co.am\",\n  \"com.am\",\n  \"commune.am\",\n  \"net.am\",\n  \"org.am\",\n  \"ed.ao\",\n  \"gv.ao\",\n  \"og.ao\",\n  \"co.ao\",\n  \"pb.ao\",\n  \"it.ao\",\n  \"bet.ar\",\n  \"com.ar\",\n  \"coop.ar\",\n  \"edu.ar\",\n  \"gob.ar\",\n  \"gov.ar\",\n  \"int.ar\",\n  \"mil.ar\",\n  \"musica.ar\",\n  \"mutual.ar\",\n  \"net.ar\",\n  \"org.ar\",\n  \"senasa.ar\",\n  \"tur.ar\",\n  \"e164.arpa\",\n  \"in-addr.arpa\",\n  \"ip6.arpa\",\n  \"iris.arpa\",\n  \"uri.arpa\",\n  \"urn.arpa\",\n  \"gov.as\",\n  \"ac.at\",\n  \"co.at\",\n  \"gv.at\",\n  \"or.at\",\n  \"com.au\",\n  \"net.au\",\n  \"org.au\",\n  \"edu.au\",\n  \"gov.au\",\n  \"asn.au\",\n  \"id.au\",\n  \"info.au\",\n  \"conf.au\",\n  \"oz.au\",\n  \"act.au\",\n  \"nsw.au\",\n  \"nt.au\",\n  \"qld.au\",\n  \"sa.au\",\n  \"tas.au\",\n  \"vic.au\",\n  \"wa.au\",\n  \"com.aw\",\n  \"com.az\",\n  \"net.az\",\n  \"int.az\",\n  \"gov.az\",\n  \"org.az\",\n  \"edu.az\",\n  \"info.az\",\n  \"pp.az\",\n  \"mil.az\",\n  \"name.az\",\n  \"pro.az\",\n  \"biz.az\",\n  \"com.ba\",\n  \"edu.ba\",\n  \"gov.ba\",\n  \"mil.ba\",\n  \"net.ba\",\n  \"org.ba\",\n  \"biz.bb\",\n  \"co.bb\",\n  \"com.bb\",\n  \"edu.bb\",\n  \"gov.bb\",\n  \"info.bb\",\n  \"net.bb\",\n  \"org.bb\",\n  \"store.bb\",\n  \"tv.bb\",\n  \"*.bd\",\n  \"ac.be\",\n  \"gov.bf\",\n  \"a.bg\",\n  \"b.bg\",\n  \"c.bg\",\n  \"d.bg\",\n  \"e.bg\",\n  \"f.bg\",\n  \"g.bg\",\n  \"h.bg\",\n  \"i.bg\",\n  \"j.bg\",\n  \"k.bg\",\n  \"l.bg\",\n  \"m.bg\",\n  \"n.bg\",\n  \"o.bg\",\n  \"p.bg\",\n  \"q.bg\",\n  \"r.bg\",\n  \"s.bg\",\n  \"t.bg\",\n  \"u.bg\",\n  \"v.bg\",\n  \"w.bg\",\n  \"x.bg\",\n  \"y.bg\",\n  \"z.bg\",\n  \"0.bg\",\n  \"1.bg\",\n  \"2.bg\",\n  \"3.bg\",\n  \"4.bg\",\n  \"5.bg\",\n  \"6.bg\",\n  \"7.bg\",\n  \"8.bg\",\n  \"9.bg\",\n  \"com.bh\",\n  \"edu.bh\",\n  \"net.bh\",\n  \"org.bh\",\n  \"gov.bh\",\n  \"co.bi\",\n  \"com.bi\",\n  \"edu.bi\",\n  \"or.bi\",\n  \"org.bi\",\n  \"africa.bj\",\n  \"agro.bj\",\n  \"architectes.bj\",\n  \"assur.bj\",\n  \"avocats.bj\",\n  \"co.bj\",\n  \"com.bj\",\n  \"eco.bj\",\n  \"econo.bj\",\n  \"edu.bj\",\n  \"info.bj\",\n  \"loisirs.bj\",\n  \"money.bj\",\n  \"net.bj\",\n  \"org.bj\",\n  \"ote.bj\",\n  \"resto.bj\",\n  \"restaurant.bj\",\n  \"tourism.bj\",\n  \"univ.bj\",\n  \"com.bm\",\n  \"edu.bm\",\n  \"gov.bm\",\n  \"net.bm\",\n  \"org.bm\",\n  \"com.bn\",\n  \"edu.bn\",\n  \"gov.bn\",\n  \"net.bn\",\n  \"org.bn\",\n  \"com.bo\",\n  \"edu.bo\",\n  \"gob.bo\",\n  \"int.bo\",\n  \"org.bo\",\n  \"net.bo\",\n  \"mil.bo\",\n  \"tv.bo\",\n  \"web.bo\",\n  \"academia.bo\",\n  \"agro.bo\",\n  \"arte.bo\",\n  \"blog.bo\",\n  \"bolivia.bo\",\n  \"ciencia.bo\",\n  \"cooperativa.bo\",\n  \"democracia.bo\",\n  \"deporte.bo\",\n  \"ecologia.bo\",\n  \"economia.bo\",\n  \"empresa.bo\",\n  \"indigena.bo\",\n  \"industria.bo\",\n  \"info.bo\",\n  \"medicina.bo\",\n  \"movimiento.bo\",\n  \"musica.bo\",\n  \"natural.bo\",\n  \"nombre.bo\",\n  \"noticias.bo\",\n  \"patria.bo\",\n  \"politica.bo\",\n  \"profesional.bo\",\n  \"plurinacional.bo\",\n  \"pueblo.bo\",\n  \"revista.bo\",\n  \"salud.bo\",\n  \"tecnologia.bo\",\n  \"tksat.bo\",\n  \"transporte.bo\",\n  \"wiki.bo\",\n  \"9guacu.br\",\n  \"abc.br\",\n  \"adm.br\",\n  \"adv.br\",\n  \"agr.br\",\n  \"aju.br\",\n  \"am.br\",\n  \"anani.br\",\n  \"aparecida.br\",\n  \"app.br\",\n  \"arq.br\",\n  \"art.br\",\n  \"ato.br\",\n  \"b.br\",\n  \"barueri.br\",\n  \"belem.br\",\n  \"bhz.br\",\n  \"bib.br\",\n  \"bio.br\",\n  \"blog.br\",\n  \"bmd.br\",\n  \"boavista.br\",\n  \"bsb.br\",\n  \"campinagrande.br\",\n  \"campinas.br\",\n  \"caxias.br\",\n  \"cim.br\",\n  \"cng.br\",\n  \"cnt.br\",\n  \"com.br\",\n  \"contagem.br\",\n  \"coop.br\",\n  \"coz.br\",\n  \"cri.br\",\n  \"cuiaba.br\",\n  \"curitiba.br\",\n  \"def.br\",\n  \"des.br\",\n  \"det.br\",\n  \"dev.br\",\n  \"ecn.br\",\n  \"eco.br\",\n  \"edu.br\",\n  \"emp.br\",\n  \"enf.br\",\n  \"eng.br\",\n  \"esp.br\",\n  \"etc.br\",\n  \"eti.br\",\n  \"far.br\",\n  \"feira.br\",\n  \"flog.br\",\n  \"floripa.br\",\n  \"fm.br\",\n  \"fnd.br\",\n  \"fortal.br\",\n  \"fot.br\",\n  \"foz.br\",\n  \"fst.br\",\n  \"g12.br\",\n  \"geo.br\",\n  \"ggf.br\",\n  \"goiania.br\",\n  \"gov.br\",\n  \"gru.br\",\n  \"imb.br\",\n  \"ind.br\",\n  \"inf.br\",\n  \"jab.br\",\n  \"jampa.br\",\n  \"jdf.br\",\n  \"joinville.br\",\n  \"jor.br\",\n  \"jus.br\",\n  \"leg.br\",\n  \"lel.br\",\n  \"log.br\",\n  \"londrina.br\",\n  \"macapa.br\",\n  \"maceio.br\",\n  \"manaus.br\",\n  \"maringa.br\",\n  \"mat.br\",\n  \"med.br\",\n  \"mil.br\",\n  \"morena.br\",\n  \"mp.br\",\n  \"mus.br\",\n  \"natal.br\",\n  \"net.br\",\n  \"niteroi.br\",\n  \"not.br\",\n  \"ntr.br\",\n  \"odo.br\",\n  \"ong.br\",\n  \"org.br\",\n  \"osasco.br\",\n  \"palmas.br\",\n  \"poa.br\",\n  \"ppg.br\",\n  \"pro.br\",\n  \"psc.br\",\n  \"psi.br\",\n  \"pvh.br\",\n  \"qsl.br\",\n  \"radio.br\",\n  \"rec.br\",\n  \"recife.br\",\n  \"rep.br\",\n  \"ribeirao.br\",\n  \"rio.br\",\n  \"riobranco.br\",\n  \"riopreto.br\",\n  \"salvador.br\",\n  \"sampa.br\",\n  \"santamaria.br\",\n  \"santoandre.br\",\n  \"saobernardo.br\",\n  \"saogonca.br\",\n  \"seg.br\",\n  \"sjc.br\",\n  \"slg.br\",\n  \"slz.br\",\n  \"sorocaba.br\",\n  \"srv.br\",\n  \"taxi.br\",\n  \"tc.br\",\n  \"tec.br\",\n  \"teo.br\",\n  \"the.br\",\n  \"tmp.br\",\n  \"trd.br\",\n  \"tur.br\",\n  \"tv.br\",\n  \"udi.br\",\n  \"vet.br\",\n  \"vix.br\",\n  \"vlog.br\",\n  \"wiki.br\",\n  \"zlg.br\",\n  \"com.bs\",\n  \"net.bs\",\n  \"org.bs\",\n  \"edu.bs\",\n  \"gov.bs\",\n  \"com.bt\",\n  \"edu.bt\",\n  \"gov.bt\",\n  \"net.bt\",\n  \"org.bt\",\n  \"co.bw\",\n  \"org.bw\",\n  \"gov.by\",\n  \"mil.by\",\n  \"com.by\",\n  \"of.by\",\n  \"com.bz\",\n  \"net.bz\",\n  \"org.bz\",\n  \"edu.bz\",\n  \"gov.bz\",\n  \"ab.ca\",\n  \"bc.ca\",\n  \"mb.ca\",\n  \"nb.ca\",\n  \"nf.ca\",\n  \"nl.ca\",\n  \"ns.ca\",\n  \"nt.ca\",\n  \"nu.ca\",\n  \"on.ca\",\n  \"pe.ca\",\n  \"qc.ca\",\n  \"sk.ca\",\n  \"yk.ca\",\n  \"gc.ca\",\n  \"gov.cd\",\n  \"org.ci\",\n  \"or.ci\",\n  \"com.ci\",\n  \"co.ci\",\n  \"edu.ci\",\n  \"ed.ci\",\n  \"ac.ci\",\n  \"net.ci\",\n  \"go.ci\",\n  \"asso.ci\",\n  \"aéroport.ci\",\n  \"int.ci\",\n  \"presse.ci\",\n  \"md.ci\",\n  \"gouv.ci\",\n  \"*.ck\",\n  \"!www.ck\",\n  \"co.cl\",\n  \"gob.cl\",\n  \"gov.cl\",\n  \"mil.cl\",\n  \"co.cm\",\n  \"com.cm\",\n  \"gov.cm\",\n  \"net.cm\",\n  \"ac.cn\",\n  \"com.cn\",\n  \"edu.cn\",\n  \"gov.cn\",\n  \"net.cn\",\n  \"org.cn\",\n  \"mil.cn\",\n  \"公司.cn\",\n  \"网络.cn\",\n  \"網絡.cn\",\n  \"ah.cn\",\n  \"bj.cn\",\n  \"cq.cn\",\n  \"fj.cn\",\n  \"gd.cn\",\n  \"gs.cn\",\n  \"gz.cn\",\n  \"gx.cn\",\n  \"ha.cn\",\n  \"hb.cn\",\n  \"he.cn\",\n  \"hi.cn\",\n  \"hl.cn\",\n  \"hn.cn\",\n  \"jl.cn\",\n  \"js.cn\",\n  \"jx.cn\",\n  \"ln.cn\",\n  \"nm.cn\",\n  \"nx.cn\",\n  \"qh.cn\",\n  \"sc.cn\",\n  \"sd.cn\",\n  \"sh.cn\",\n  \"sn.cn\",\n  \"sx.cn\",\n  \"tj.cn\",\n  \"xj.cn\",\n  \"xz.cn\",\n  \"yn.cn\",\n  \"zj.cn\",\n  \"hk.cn\",\n  \"mo.cn\",\n  \"tw.cn\",\n  \"arts.co\",\n  \"com.co\",\n  \"edu.co\",\n  \"firm.co\",\n  \"gov.co\",\n  \"info.co\",\n  \"int.co\",\n  \"mil.co\",\n  \"net.co\",\n  \"nom.co\",\n  \"org.co\",\n  \"rec.co\",\n  \"web.co\",\n  \"ac.cr\",\n  \"co.cr\",\n  \"ed.cr\",\n  \"fi.cr\",\n  \"go.cr\",\n  \"or.cr\",\n  \"sa.cr\",\n  \"com.cu\",\n  \"edu.cu\",\n  \"org.cu\",\n  \"net.cu\",\n  \"gov.cu\",\n  \"inf.cu\",\n  \"com.cv\",\n  \"edu.cv\",\n  \"int.cv\",\n  \"nome.cv\",\n  \"org.cv\",\n  \"com.cw\",\n  \"edu.cw\",\n  \"net.cw\",\n  \"org.cw\",\n  \"gov.cx\",\n  \"ac.cy\",\n  \"biz.cy\",\n  \"com.cy\",\n  \"ekloges.cy\",\n  \"gov.cy\",\n  \"ltd.cy\",\n  \"mil.cy\",\n  \"net.cy\",\n  \"org.cy\",\n  \"press.cy\",\n  \"pro.cy\",\n  \"tm.cy\",\n  \"com.dm\",\n  \"net.dm\",\n  \"org.dm\",\n  \"edu.dm\",\n  \"gov.dm\",\n  \"art.do\",\n  \"com.do\",\n  \"edu.do\",\n  \"gob.do\",\n  \"gov.do\",\n  \"mil.do\",\n  \"net.do\",\n  \"org.do\",\n  \"sld.do\",\n  \"web.do\",\n  \"art.dz\",\n  \"asso.dz\",\n  \"com.dz\",\n  \"edu.dz\",\n  \"gov.dz\",\n  \"org.dz\",\n  \"net.dz\",\n  \"pol.dz\",\n  \"soc.dz\",\n  \"tm.dz\",\n  \"com.ec\",\n  \"info.ec\",\n  \"net.ec\",\n  \"fin.ec\",\n  \"k12.ec\",\n  \"med.ec\",\n  \"pro.ec\",\n  \"org.ec\",\n  \"edu.ec\",\n  \"gov.ec\",\n  \"gob.ec\",\n  \"mil.ec\",\n  \"edu.ee\",\n  \"gov.ee\",\n  \"riik.ee\",\n  \"lib.ee\",\n  \"med.ee\",\n  \"com.ee\",\n  \"pri.ee\",\n  \"aip.ee\",\n  \"org.ee\",\n  \"fie.ee\",\n  \"com.eg\",\n  \"edu.eg\",\n  \"eun.eg\",\n  \"gov.eg\",\n  \"mil.eg\",\n  \"name.eg\",\n  \"net.eg\",\n  \"org.eg\",\n  \"sci.eg\",\n  \"*.er\",\n  \"com.es\",\n  \"nom.es\",\n  \"org.es\",\n  \"gob.es\",\n  \"edu.es\",\n  \"com.et\",\n  \"gov.et\",\n  \"org.et\",\n  \"edu.et\",\n  \"biz.et\",\n  \"name.et\",\n  \"info.et\",\n  \"net.et\",\n  \"aland.fi\",\n  \"ac.fj\",\n  \"biz.fj\",\n  \"com.fj\",\n  \"gov.fj\",\n  \"info.fj\",\n  \"mil.fj\",\n  \"name.fj\",\n  \"net.fj\",\n  \"org.fj\",\n  \"pro.fj\",\n  \"*.fk\",\n  \"com.fm\",\n  \"edu.fm\",\n  \"net.fm\",\n  \"org.fm\",\n  \"asso.fr\",\n  \"com.fr\",\n  \"gouv.fr\",\n  \"nom.fr\",\n  \"prd.fr\",\n  \"tm.fr\",\n  \"aeroport.fr\",\n  \"avocat.fr\",\n  \"avoues.fr\",\n  \"cci.fr\",\n  \"chambagri.fr\",\n  \"chirurgiens-dentistes.fr\",\n  \"experts-comptables.fr\",\n  \"geometre-expert.fr\",\n  \"greta.fr\",\n  \"huissier-justice.fr\",\n  \"medecin.fr\",\n  \"notaires.fr\",\n  \"pharmacien.fr\",\n  \"port.fr\",\n  \"veterinaire.fr\",\n  \"edu.gd\",\n  \"gov.gd\",\n  \"com.ge\",\n  \"edu.ge\",\n  \"gov.ge\",\n  \"org.ge\",\n  \"mil.ge\",\n  \"net.ge\",\n  \"pvt.ge\",\n  \"co.gg\",\n  \"net.gg\",\n  \"org.gg\",\n  \"com.gh\",\n  \"edu.gh\",\n  \"gov.gh\",\n  \"org.gh\",\n  \"mil.gh\",\n  \"com.gi\",\n  \"ltd.gi\",\n  \"gov.gi\",\n  \"mod.gi\",\n  \"edu.gi\",\n  \"org.gi\",\n  \"co.gl\",\n  \"com.gl\",\n  \"edu.gl\",\n  \"net.gl\",\n  \"org.gl\",\n  \"ac.gn\",\n  \"com.gn\",\n  \"edu.gn\",\n  \"gov.gn\",\n  \"org.gn\",\n  \"net.gn\",\n  \"com.gp\",\n  \"net.gp\",\n  \"mobi.gp\",\n  \"edu.gp\",\n  \"org.gp\",\n  \"asso.gp\",\n  \"com.gr\",\n  \"edu.gr\",\n  \"net.gr\",\n  \"org.gr\",\n  \"gov.gr\",\n  \"com.gt\",\n  \"edu.gt\",\n  \"gob.gt\",\n  \"ind.gt\",\n  \"mil.gt\",\n  \"net.gt\",\n  \"org.gt\",\n  \"com.gu\",\n  \"edu.gu\",\n  \"gov.gu\",\n  \"guam.gu\",\n  \"info.gu\",\n  \"net.gu\",\n  \"org.gu\",\n  \"web.gu\",\n  \"co.gy\",\n  \"com.gy\",\n  \"edu.gy\",\n  \"gov.gy\",\n  \"net.gy\",\n  \"org.gy\",\n  \"com.hk\",\n  \"edu.hk\",\n  \"gov.hk\",\n  \"idv.hk\",\n  \"net.hk\",\n  \"org.hk\",\n  \"公司.hk\",\n  \"教育.hk\",\n  \"敎育.hk\",\n  \"政府.hk\",\n  \"個人.hk\",\n  \"个人.hk\",\n  \"箇人.hk\",\n  \"網络.hk\",\n  \"网络.hk\",\n  \"组織.hk\",\n  \"網絡.hk\",\n  \"网絡.hk\",\n  \"组织.hk\",\n  \"組織.hk\",\n  \"組织.hk\",\n  \"com.hn\",\n  \"edu.hn\",\n  \"org.hn\",\n  \"net.hn\",\n  \"mil.hn\",\n  \"gob.hn\",\n  \"iz.hr\",\n  \"from.hr\",\n  \"name.hr\",\n  \"com.hr\",\n  \"com.ht\",\n  \"shop.ht\",\n  \"firm.ht\",\n  \"info.ht\",\n  \"adult.ht\",\n  \"net.ht\",\n  \"pro.ht\",\n  \"org.ht\",\n  \"med.ht\",\n  \"art.ht\",\n  \"coop.ht\",\n  \"pol.ht\",\n  \"asso.ht\",\n  \"edu.ht\",\n  \"rel.ht\",\n  \"gouv.ht\",\n  \"perso.ht\",\n  \"co.hu\",\n  \"info.hu\",\n  \"org.hu\",\n  \"priv.hu\",\n  \"sport.hu\",\n  \"tm.hu\",\n  \"2000.hu\",\n  \"agrar.hu\",\n  \"bolt.hu\",\n  \"casino.hu\",\n  \"city.hu\",\n  \"erotica.hu\",\n  \"erotika.hu\",\n  \"film.hu\",\n  \"forum.hu\",\n  \"games.hu\",\n  \"hotel.hu\",\n  \"ingatlan.hu\",\n  \"jogasz.hu\",\n  \"konyvelo.hu\",\n  \"lakas.hu\",\n  \"media.hu\",\n  \"news.hu\",\n  \"reklam.hu\",\n  \"sex.hu\",\n  \"shop.hu\",\n  \"suli.hu\",\n  \"szex.hu\",\n  \"tozsde.hu\",\n  \"utazas.hu\",\n  \"video.hu\",\n  \"ac.id\",\n  \"biz.id\",\n  \"co.id\",\n  \"desa.id\",\n  \"go.id\",\n  \"mil.id\",\n  \"my.id\",\n  \"net.id\",\n  \"or.id\",\n  \"ponpes.id\",\n  \"sch.id\",\n  \"web.id\",\n  \"gov.ie\",\n  \"ac.il\",\n  \"co.il\",\n  \"gov.il\",\n  \"idf.il\",\n  \"k12.il\",\n  \"muni.il\",\n  \"net.il\",\n  \"org.il\",\n  \"אקדמיה.ישראל\",\n  \"ישוב.ישראל\",\n  \"צהל.ישראל\",\n  \"ממשל.ישראל\",\n  \"ac.im\",\n  \"co.im\",\n  \"com.im\",\n  \"net.im\",\n  \"org.im\",\n  \"tt.im\",\n  \"tv.im\",\n  \"5g.in\",\n  \"6g.in\",\n  \"ac.in\",\n  \"ai.in\",\n  \"am.in\",\n  \"bihar.in\",\n  \"biz.in\",\n  \"business.in\",\n  \"ca.in\",\n  \"cn.in\",\n  \"co.in\",\n  \"com.in\",\n  \"coop.in\",\n  \"cs.in\",\n  \"delhi.in\",\n  \"dr.in\",\n  \"edu.in\",\n  \"er.in\",\n  \"firm.in\",\n  \"gen.in\",\n  \"gov.in\",\n  \"gujarat.in\",\n  \"ind.in\",\n  \"info.in\",\n  \"int.in\",\n  \"internet.in\",\n  \"io.in\",\n  \"me.in\",\n  \"mil.in\",\n  \"net.in\",\n  \"nic.in\",\n  \"org.in\",\n  \"pg.in\",\n  \"post.in\",\n  \"pro.in\",\n  \"res.in\",\n  \"travel.in\",\n  \"tv.in\",\n  \"uk.in\",\n  \"up.in\",\n  \"us.in\",\n  \"eu.int\",\n  \"com.io\",\n  \"gov.iq\",\n  \"edu.iq\",\n  \"mil.iq\",\n  \"com.iq\",\n  \"org.iq\",\n  \"net.iq\",\n  \"ac.ir\",\n  \"co.ir\",\n  \"gov.ir\",\n  \"id.ir\",\n  \"net.ir\",\n  \"org.ir\",\n  \"sch.ir\",\n  \"ایران.ir\",\n  \"ايران.ir\",\n  \"net.is\",\n  \"com.is\",\n  \"edu.is\",\n  \"gov.is\",\n  \"org.is\",\n  \"int.is\",\n  \"gov.it\",\n  \"edu.it\",\n  \"abr.it\",\n  \"abruzzo.it\",\n  \"aosta-valley.it\",\n  \"aostavalley.it\",\n  \"bas.it\",\n  \"basilicata.it\",\n  \"cal.it\",\n  \"calabria.it\",\n  \"cam.it\",\n  \"campania.it\",\n  \"emilia-romagna.it\",\n  \"emiliaromagna.it\",\n  \"emr.it\",\n  \"friuli-v-giulia.it\",\n  \"friuli-ve-giulia.it\",\n  \"friuli-vegiulia.it\",\n  \"friuli-venezia-giulia.it\",\n  \"friuli-veneziagiulia.it\",\n  \"friuli-vgiulia.it\",\n  \"friuliv-giulia.it\",\n  \"friulive-giulia.it\",\n  \"friulivegiulia.it\",\n  \"friulivenezia-giulia.it\",\n  \"friuliveneziagiulia.it\",\n  \"friulivgiulia.it\",\n  \"fvg.it\",\n  \"laz.it\",\n  \"lazio.it\",\n  \"lig.it\",\n  \"liguria.it\",\n  \"lom.it\",\n  \"lombardia.it\",\n  \"lombardy.it\",\n  \"lucania.it\",\n  \"mar.it\",\n  \"marche.it\",\n  \"mol.it\",\n  \"molise.it\",\n  \"piedmont.it\",\n  \"piemonte.it\",\n  \"pmn.it\",\n  \"pug.it\",\n  \"puglia.it\",\n  \"sar.it\",\n  \"sardegna.it\",\n  \"sardinia.it\",\n  \"sic.it\",\n  \"sicilia.it\",\n  \"sicily.it\",\n  \"taa.it\",\n  \"tos.it\",\n  \"toscana.it\",\n  \"trentin-sud-tirol.it\",\n  \"trentin-süd-tirol.it\",\n  \"trentin-sudtirol.it\",\n  \"trentin-südtirol.it\",\n  \"trentin-sued-tirol.it\",\n  \"trentin-suedtirol.it\",\n  \"trentino-a-adige.it\",\n  \"trentino-aadige.it\",\n  \"trentino-alto-adige.it\",\n  \"trentino-altoadige.it\",\n  \"trentino-s-tirol.it\",\n  \"trentino-stirol.it\",\n  \"trentino-sud-tirol.it\",\n  \"trentino-süd-tirol.it\",\n  \"trentino-sudtirol.it\",\n  \"trentino-südtirol.it\",\n  \"trentino-sued-tirol.it\",\n  \"trentino-suedtirol.it\",\n  \"trentino.it\",\n  \"trentinoa-adige.it\",\n  \"trentinoaadige.it\",\n  \"trentinoalto-adige.it\",\n  \"trentinoaltoadige.it\",\n  \"trentinos-tirol.it\",\n  \"trentinostirol.it\",\n  \"trentinosud-tirol.it\",\n  \"trentinosüd-tirol.it\",\n  \"trentinosudtirol.it\",\n  \"trentinosüdtirol.it\",\n  \"trentinosued-tirol.it\",\n  \"trentinosuedtirol.it\",\n  \"trentinsud-tirol.it\",\n  \"trentinsüd-tirol.it\",\n  \"trentinsudtirol.it\",\n  \"trentinsüdtirol.it\",\n  \"trentinsued-tirol.it\",\n  \"trentinsuedtirol.it\",\n  \"tuscany.it\",\n  \"umb.it\",\n  \"umbria.it\",\n  \"val-d-aosta.it\",\n  \"val-daosta.it\",\n  \"vald-aosta.it\",\n  \"valdaosta.it\",\n  \"valle-aosta.it\",\n  \"valle-d-aosta.it\",\n  \"valle-daosta.it\",\n  \"valleaosta.it\",\n  \"valled-aosta.it\",\n  \"valledaosta.it\",\n  \"vallee-aoste.it\",\n  \"vallée-aoste.it\",\n  \"vallee-d-aoste.it\",\n  \"vallée-d-aoste.it\",\n  \"valleeaoste.it\",\n  \"valléeaoste.it\",\n  \"valleedaoste.it\",\n  \"valléedaoste.it\",\n  \"vao.it\",\n  \"vda.it\",\n  \"ven.it\",\n  \"veneto.it\",\n  \"ag.it\",\n  \"agrigento.it\",\n  \"al.it\",\n  \"alessandria.it\",\n  \"alto-adige.it\",\n  \"altoadige.it\",\n  \"an.it\",\n  \"ancona.it\",\n  \"andria-barletta-trani.it\",\n  \"andria-trani-barletta.it\",\n  \"andriabarlettatrani.it\",\n  \"andriatranibarletta.it\",\n  \"ao.it\",\n  \"aosta.it\",\n  \"aoste.it\",\n  \"ap.it\",\n  \"aq.it\",\n  \"aquila.it\",\n  \"ar.it\",\n  \"arezzo.it\",\n  \"ascoli-piceno.it\",\n  \"ascolipiceno.it\",\n  \"asti.it\",\n  \"at.it\",\n  \"av.it\",\n  \"avellino.it\",\n  \"ba.it\",\n  \"balsan-sudtirol.it\",\n  \"balsan-südtirol.it\",\n  \"balsan-suedtirol.it\",\n  \"balsan.it\",\n  \"bari.it\",\n  \"barletta-trani-andria.it\",\n  \"barlettatraniandria.it\",\n  \"belluno.it\",\n  \"benevento.it\",\n  \"bergamo.it\",\n  \"bg.it\",\n  \"bi.it\",\n  \"biella.it\",\n  \"bl.it\",\n  \"bn.it\",\n  \"bo.it\",\n  \"bologna.it\",\n  \"bolzano-altoadige.it\",\n  \"bolzano.it\",\n  \"bozen-sudtirol.it\",\n  \"bozen-südtirol.it\",\n  \"bozen-suedtirol.it\",\n  \"bozen.it\",\n  \"br.it\",\n  \"brescia.it\",\n  \"brindisi.it\",\n  \"bs.it\",\n  \"bt.it\",\n  \"bulsan-sudtirol.it\",\n  \"bulsan-südtirol.it\",\n  \"bulsan-suedtirol.it\",\n  \"bulsan.it\",\n  \"bz.it\",\n  \"ca.it\",\n  \"cagliari.it\",\n  \"caltanissetta.it\",\n  \"campidano-medio.it\",\n  \"campidanomedio.it\",\n  \"campobasso.it\",\n  \"carbonia-iglesias.it\",\n  \"carboniaiglesias.it\",\n  \"carrara-massa.it\",\n  \"carraramassa.it\",\n  \"caserta.it\",\n  \"catania.it\",\n  \"catanzaro.it\",\n  \"cb.it\",\n  \"ce.it\",\n  \"cesena-forli.it\",\n  \"cesena-forlì.it\",\n  \"cesenaforli.it\",\n  \"cesenaforlì.it\",\n  \"ch.it\",\n  \"chieti.it\",\n  \"ci.it\",\n  \"cl.it\",\n  \"cn.it\",\n  \"co.it\",\n  \"como.it\",\n  \"cosenza.it\",\n  \"cr.it\",\n  \"cremona.it\",\n  \"crotone.it\",\n  \"cs.it\",\n  \"ct.it\",\n  \"cuneo.it\",\n  \"cz.it\",\n  \"dell-ogliastra.it\",\n  \"dellogliastra.it\",\n  \"en.it\",\n  \"enna.it\",\n  \"fc.it\",\n  \"fe.it\",\n  \"fermo.it\",\n  \"ferrara.it\",\n  \"fg.it\",\n  \"fi.it\",\n  \"firenze.it\",\n  \"florence.it\",\n  \"fm.it\",\n  \"foggia.it\",\n  \"forli-cesena.it\",\n  \"forlì-cesena.it\",\n  \"forlicesena.it\",\n  \"forlìcesena.it\",\n  \"fr.it\",\n  \"frosinone.it\",\n  \"ge.it\",\n  \"genoa.it\",\n  \"genova.it\",\n  \"go.it\",\n  \"gorizia.it\",\n  \"gr.it\",\n  \"grosseto.it\",\n  \"iglesias-carbonia.it\",\n  \"iglesiascarbonia.it\",\n  \"im.it\",\n  \"imperia.it\",\n  \"is.it\",\n  \"isernia.it\",\n  \"kr.it\",\n  \"la-spezia.it\",\n  \"laquila.it\",\n  \"laspezia.it\",\n  \"latina.it\",\n  \"lc.it\",\n  \"le.it\",\n  \"lecce.it\",\n  \"lecco.it\",\n  \"li.it\",\n  \"livorno.it\",\n  \"lo.it\",\n  \"lodi.it\",\n  \"lt.it\",\n  \"lu.it\",\n  \"lucca.it\",\n  \"macerata.it\",\n  \"mantova.it\",\n  \"massa-carrara.it\",\n  \"massacarrara.it\",\n  \"matera.it\",\n  \"mb.it\",\n  \"mc.it\",\n  \"me.it\",\n  \"medio-campidano.it\",\n  \"mediocampidano.it\",\n  \"messina.it\",\n  \"mi.it\",\n  \"milan.it\",\n  \"milano.it\",\n  \"mn.it\",\n  \"mo.it\",\n  \"modena.it\",\n  \"monza-brianza.it\",\n  \"monza-e-della-brianza.it\",\n  \"monza.it\",\n  \"monzabrianza.it\",\n  \"monzaebrianza.it\",\n  \"monzaedellabrianza.it\",\n  \"ms.it\",\n  \"mt.it\",\n  \"na.it\",\n  \"naples.it\",\n  \"napoli.it\",\n  \"no.it\",\n  \"novara.it\",\n  \"nu.it\",\n  \"nuoro.it\",\n  \"og.it\",\n  \"ogliastra.it\",\n  \"olbia-tempio.it\",\n  \"olbiatempio.it\",\n  \"or.it\",\n  \"oristano.it\",\n  \"ot.it\",\n  \"pa.it\",\n  \"padova.it\",\n  \"padua.it\",\n  \"palermo.it\",\n  \"parma.it\",\n  \"pavia.it\",\n  \"pc.it\",\n  \"pd.it\",\n  \"pe.it\",\n  \"perugia.it\",\n  \"pesaro-urbino.it\",\n  \"pesarourbino.it\",\n  \"pescara.it\",\n  \"pg.it\",\n  \"pi.it\",\n  \"piacenza.it\",\n  \"pisa.it\",\n  \"pistoia.it\",\n  \"pn.it\",\n  \"po.it\",\n  \"pordenone.it\",\n  \"potenza.it\",\n  \"pr.it\",\n  \"prato.it\",\n  \"pt.it\",\n  \"pu.it\",\n  \"pv.it\",\n  \"pz.it\",\n  \"ra.it\",\n  \"ragusa.it\",\n  \"ravenna.it\",\n  \"rc.it\",\n  \"re.it\",\n  \"reggio-calabria.it\",\n  \"reggio-emilia.it\",\n  \"reggiocalabria.it\",\n  \"reggioemilia.it\",\n  \"rg.it\",\n  \"ri.it\",\n  \"rieti.it\",\n  \"rimini.it\",\n  \"rm.it\",\n  \"rn.it\",\n  \"ro.it\",\n  \"roma.it\",\n  \"rome.it\",\n  \"rovigo.it\",\n  \"sa.it\",\n  \"salerno.it\",\n  \"sassari.it\",\n  \"savona.it\",\n  \"si.it\",\n  \"siena.it\",\n  \"siracusa.it\",\n  \"so.it\",\n  \"sondrio.it\",\n  \"sp.it\",\n  \"sr.it\",\n  \"ss.it\",\n  \"suedtirol.it\",\n  \"südtirol.it\",\n  \"sv.it\",\n  \"ta.it\",\n  \"taranto.it\",\n  \"te.it\",\n  \"tempio-olbia.it\",\n  \"tempioolbia.it\",\n  \"teramo.it\",\n  \"terni.it\",\n  \"tn.it\",\n  \"to.it\",\n  \"torino.it\",\n  \"tp.it\",\n  \"tr.it\",\n  \"trani-andria-barletta.it\",\n  \"trani-barletta-andria.it\",\n  \"traniandriabarletta.it\",\n  \"tranibarlettaandria.it\",\n  \"trapani.it\",\n  \"trento.it\",\n  \"treviso.it\",\n  \"trieste.it\",\n  \"ts.it\",\n  \"turin.it\",\n  \"tv.it\",\n  \"ud.it\",\n  \"udine.it\",\n  \"urbino-pesaro.it\",\n  \"urbinopesaro.it\",\n  \"va.it\",\n  \"varese.it\",\n  \"vb.it\",\n  \"vc.it\",\n  \"ve.it\",\n  \"venezia.it\",\n  \"venice.it\",\n  \"verbania.it\",\n  \"vercelli.it\",\n  \"verona.it\",\n  \"vi.it\",\n  \"vibo-valentia.it\",\n  \"vibovalentia.it\",\n  \"vicenza.it\",\n  \"viterbo.it\",\n  \"vr.it\",\n  \"vs.it\",\n  \"vt.it\",\n  \"vv.it\",\n  \"co.je\",\n  \"net.je\",\n  \"org.je\",\n  \"*.jm\",\n  \"com.jo\",\n  \"org.jo\",\n  \"net.jo\",\n  \"edu.jo\",\n  \"sch.jo\",\n  \"gov.jo\",\n  \"mil.jo\",\n  \"name.jo\",\n  \"ac.jp\",\n  \"ad.jp\",\n  \"co.jp\",\n  \"ed.jp\",\n  \"go.jp\",\n  \"gr.jp\",\n  \"lg.jp\",\n  \"ne.jp\",\n  \"or.jp\",\n  \"aichi.jp\",\n  \"akita.jp\",\n  \"aomori.jp\",\n  \"chiba.jp\",\n  \"ehime.jp\",\n  \"fukui.jp\",\n  \"fukuoka.jp\",\n  \"fukushima.jp\",\n  \"gifu.jp\",\n  \"gunma.jp\",\n  \"hiroshima.jp\",\n  \"hokkaido.jp\",\n  \"hyogo.jp\",\n  \"ibaraki.jp\",\n  \"ishikawa.jp\",\n  \"iwate.jp\",\n  \"kagawa.jp\",\n  \"kagoshima.jp\",\n  \"kanagawa.jp\",\n  \"kochi.jp\",\n  \"kumamoto.jp\",\n  \"kyoto.jp\",\n  \"mie.jp\",\n  \"miyagi.jp\",\n  \"miyazaki.jp\",\n  \"nagano.jp\",\n  \"nagasaki.jp\",\n  \"nara.jp\",\n  \"niigata.jp\",\n  \"oita.jp\",\n  \"okayama.jp\",\n  \"okinawa.jp\",\n  \"osaka.jp\",\n  \"saga.jp\",\n  \"saitama.jp\",\n  \"shiga.jp\",\n  \"shimane.jp\",\n  \"shizuoka.jp\",\n  \"tochigi.jp\",\n  \"tokushima.jp\",\n  \"tokyo.jp\",\n  \"tottori.jp\",\n  \"toyama.jp\",\n  \"wakayama.jp\",\n  \"yamagata.jp\",\n  \"yamaguchi.jp\",\n  \"yamanashi.jp\",\n  \"栃木.jp\",\n  \"愛知.jp\",\n  \"愛媛.jp\",\n  \"兵庫.jp\",\n  \"熊本.jp\",\n  \"茨城.jp\",\n  \"北海道.jp\",\n  \"千葉.jp\",\n  \"和歌山.jp\",\n  \"長崎.jp\",\n  \"長野.jp\",\n  \"新潟.jp\",\n  \"青森.jp\",\n  \"静岡.jp\",\n  \"東京.jp\",\n  \"石川.jp\",\n  \"埼玉.jp\",\n  \"三重.jp\",\n  \"京都.jp\",\n  \"佐賀.jp\",\n  \"大分.jp\",\n  \"大阪.jp\",\n  \"奈良.jp\",\n  \"宮城.jp\",\n  \"宮崎.jp\",\n  \"富山.jp\",\n  \"山口.jp\",\n  \"山形.jp\",\n  \"山梨.jp\",\n  \"岩手.jp\",\n  \"岐阜.jp\",\n  \"岡山.jp\",\n  \"島根.jp\",\n  \"広島.jp\",\n  \"徳島.jp\",\n  \"沖縄.jp\",\n  \"滋賀.jp\",\n  \"神奈川.jp\",\n  \"福井.jp\",\n  \"福岡.jp\",\n  \"福島.jp\",\n  \"秋田.jp\",\n  \"群馬.jp\",\n  \"香川.jp\",\n  \"高知.jp\",\n  \"鳥取.jp\",\n  \"鹿児島.jp\",\n  \"ac.ke\",\n  \"co.ke\",\n  \"go.ke\",\n  \"info.ke\",\n  \"me.ke\",\n  \"mobi.ke\",\n  \"ne.ke\",\n  \"or.ke\",\n  \"sc.ke\",\n  \"org.kg\",\n  \"net.kg\",\n  \"com.kg\",\n  \"edu.kg\",\n  \"gov.kg\",\n  \"mil.kg\",\n  \"*.kh\",\n  \"edu.ki\",\n  \"biz.ki\",\n  \"net.ki\",\n  \"org.ki\",\n  \"gov.ki\",\n  \"info.ki\",\n  \"com.ki\",\n  \"org.km\",\n  \"nom.km\",\n  \"gov.km\",\n  \"prd.km\",\n  \"tm.km\",\n  \"edu.km\",\n  \"mil.km\",\n  \"ass.km\",\n  \"com.km\",\n  \"coop.km\",\n  \"asso.km\",\n  \"presse.km\",\n  \"medecin.km\",\n  \"notaires.km\",\n  \"pharmaciens.km\",\n  \"veterinaire.km\",\n  \"gouv.km\",\n  \"net.kn\",\n  \"org.kn\",\n  \"edu.kn\",\n  \"gov.kn\",\n  \"com.kp\",\n  \"edu.kp\",\n  \"gov.kp\",\n  \"org.kp\",\n  \"rep.kp\",\n  \"tra.kp\",\n  \"ac.kr\",\n  \"co.kr\",\n  \"es.kr\",\n  \"go.kr\",\n  \"hs.kr\",\n  \"kg.kr\",\n  \"mil.kr\",\n  \"ms.kr\",\n  \"ne.kr\",\n  \"or.kr\",\n  \"pe.kr\",\n  \"re.kr\",\n  \"sc.kr\",\n  \"busan.kr\",\n  \"chungbuk.kr\",\n  \"chungnam.kr\",\n  \"daegu.kr\",\n  \"daejeon.kr\",\n  \"gangwon.kr\",\n  \"gwangju.kr\",\n  \"gyeongbuk.kr\",\n  \"gyeonggi.kr\",\n  \"gyeongnam.kr\",\n  \"incheon.kr\",\n  \"jeju.kr\",\n  \"jeonbuk.kr\",\n  \"jeonnam.kr\",\n  \"seoul.kr\",\n  \"ulsan.kr\",\n  \"com.kw\",\n  \"edu.kw\",\n  \"emb.kw\",\n  \"gov.kw\",\n  \"ind.kw\",\n  \"net.kw\",\n  \"org.kw\",\n  \"com.ky\",\n  \"edu.ky\",\n  \"net.ky\",\n  \"org.ky\",\n  \"org.kz\",\n  \"edu.kz\",\n  \"net.kz\",\n  \"gov.kz\",\n  \"mil.kz\",\n  \"com.kz\",\n  \"int.la\",\n  \"net.la\",\n  \"info.la\",\n  \"edu.la\",\n  \"gov.la\",\n  \"per.la\",\n  \"com.la\",\n  \"org.la\",\n  \"com.lb\",\n  \"edu.lb\",\n  \"gov.lb\",\n  \"net.lb\",\n  \"org.lb\",\n  \"com.lc\",\n  \"net.lc\",\n  \"co.lc\",\n  \"org.lc\",\n  \"edu.lc\",\n  \"gov.lc\",\n  \"gov.lk\",\n  \"sch.lk\",\n  \"net.lk\",\n  \"int.lk\",\n  \"com.lk\",\n  \"org.lk\",\n  \"edu.lk\",\n  \"ngo.lk\",\n  \"soc.lk\",\n  \"web.lk\",\n  \"ltd.lk\",\n  \"assn.lk\",\n  \"grp.lk\",\n  \"hotel.lk\",\n  \"ac.lk\",\n  \"com.lr\",\n  \"edu.lr\",\n  \"gov.lr\",\n  \"org.lr\",\n  \"net.lr\",\n  \"ac.ls\",\n  \"biz.ls\",\n  \"co.ls\",\n  \"edu.ls\",\n  \"gov.ls\",\n  \"info.ls\",\n  \"net.ls\",\n  \"org.ls\",\n  \"sc.ls\",\n  \"gov.lt\",\n  \"com.lv\",\n  \"edu.lv\",\n  \"gov.lv\",\n  \"org.lv\",\n  \"mil.lv\",\n  \"id.lv\",\n  \"net.lv\",\n  \"asn.lv\",\n  \"conf.lv\",\n  \"com.ly\",\n  \"net.ly\",\n  \"gov.ly\",\n  \"plc.ly\",\n  \"edu.ly\",\n  \"sch.ly\",\n  \"med.ly\",\n  \"org.ly\",\n  \"id.ly\",\n  \"co.ma\",\n  \"net.ma\",\n  \"gov.ma\",\n  \"org.ma\",\n  \"ac.ma\",\n  \"press.ma\",\n  \"tm.mc\",\n  \"asso.mc\",\n  \"co.me\",\n  \"net.me\",\n  \"org.me\",\n  \"edu.me\",\n  \"ac.me\",\n  \"gov.me\",\n  \"its.me\",\n  \"priv.me\",\n  \"org.mg\",\n  \"nom.mg\",\n  \"gov.mg\",\n  \"prd.mg\",\n  \"tm.mg\",\n  \"edu.mg\",\n  \"mil.mg\",\n  \"com.mg\",\n  \"co.mg\",\n  \"com.mk\",\n  \"org.mk\",\n  \"net.mk\",\n  \"edu.mk\",\n  \"gov.mk\",\n  \"inf.mk\",\n  \"name.mk\",\n  \"com.ml\",\n  \"edu.ml\",\n  \"gouv.ml\",\n  \"gov.ml\",\n  \"net.ml\",\n  \"org.ml\",\n  \"presse.ml\",\n  \"*.mm\",\n  \"gov.mn\",\n  \"edu.mn\",\n  \"org.mn\",\n  \"com.mo\",\n  \"net.mo\",\n  \"org.mo\",\n  \"edu.mo\",\n  \"gov.mo\",\n  \"gov.mr\",\n  \"com.ms\",\n  \"edu.ms\",\n  \"gov.ms\",\n  \"net.ms\",\n  \"org.ms\",\n  \"com.mt\",\n  \"edu.mt\",\n  \"net.mt\",\n  \"org.mt\",\n  \"com.mu\",\n  \"net.mu\",\n  \"org.mu\",\n  \"gov.mu\",\n  \"ac.mu\",\n  \"co.mu\",\n  \"or.mu\",\n  \"aero.mv\",\n  \"biz.mv\",\n  \"com.mv\",\n  \"coop.mv\",\n  \"edu.mv\",\n  \"gov.mv\",\n  \"info.mv\",\n  \"int.mv\",\n  \"mil.mv\",\n  \"museum.mv\",\n  \"name.mv\",\n  \"net.mv\",\n  \"org.mv\",\n  \"pro.mv\",\n  \"ac.mw\",\n  \"biz.mw\",\n  \"co.mw\",\n  \"com.mw\",\n  \"coop.mw\",\n  \"edu.mw\",\n  \"gov.mw\",\n  \"int.mw\",\n  \"museum.mw\",\n  \"net.mw\",\n  \"org.mw\",\n  \"com.mx\",\n  \"org.mx\",\n  \"gob.mx\",\n  \"edu.mx\",\n  \"net.mx\",\n  \"biz.my\",\n  \"com.my\",\n  \"edu.my\",\n  \"gov.my\",\n  \"mil.my\",\n  \"name.my\",\n  \"net.my\",\n  \"org.my\",\n  \"ac.mz\",\n  \"adv.mz\",\n  \"co.mz\",\n  \"edu.mz\",\n  \"gov.mz\",\n  \"mil.mz\",\n  \"net.mz\",\n  \"org.mz\",\n  \"info.na\",\n  \"pro.na\",\n  \"name.na\",\n  \"school.na\",\n  \"or.na\",\n  \"dr.na\",\n  \"us.na\",\n  \"mx.na\",\n  \"ca.na\",\n  \"in.na\",\n  \"cc.na\",\n  \"tv.na\",\n  \"ws.na\",\n  \"mobi.na\",\n  \"co.na\",\n  \"com.na\",\n  \"org.na\",\n  \"asso.nc\",\n  \"nom.nc\",\n  \"com.nf\",\n  \"net.nf\",\n  \"per.nf\",\n  \"rec.nf\",\n  \"web.nf\",\n  \"arts.nf\",\n  \"firm.nf\",\n  \"info.nf\",\n  \"other.nf\",\n  \"store.nf\",\n  \"com.ng\",\n  \"edu.ng\",\n  \"gov.ng\",\n  \"i.ng\",\n  \"mil.ng\",\n  \"mobi.ng\",\n  \"name.ng\",\n  \"net.ng\",\n  \"org.ng\",\n  \"sch.ng\",\n  \"ac.ni\",\n  \"biz.ni\",\n  \"co.ni\",\n  \"com.ni\",\n  \"edu.ni\",\n  \"gob.ni\",\n  \"in.ni\",\n  \"info.ni\",\n  \"int.ni\",\n  \"mil.ni\",\n  \"net.ni\",\n  \"nom.ni\",\n  \"org.ni\",\n  \"web.ni\",\n  \"fhs.no\",\n  \"vgs.no\",\n  \"fylkesbibl.no\",\n  \"folkebibl.no\",\n  \"museum.no\",\n  \"idrett.no\",\n  \"priv.no\",\n  \"mil.no\",\n  \"stat.no\",\n  \"dep.no\",\n  \"kommune.no\",\n  \"herad.no\",\n  \"aa.no\",\n  \"ah.no\",\n  \"bu.no\",\n  \"fm.no\",\n  \"hl.no\",\n  \"hm.no\",\n  \"jan-mayen.no\",\n  \"mr.no\",\n  \"nl.no\",\n  \"nt.no\",\n  \"of.no\",\n  \"ol.no\",\n  \"oslo.no\",\n  \"rl.no\",\n  \"sf.no\",\n  \"st.no\",\n  \"svalbard.no\",\n  \"tm.no\",\n  \"tr.no\",\n  \"va.no\",\n  \"vf.no\",\n  \"akrehamn.no\",\n  \"åkrehamn.no\",\n  \"algard.no\",\n  \"ålgård.no\",\n  \"arna.no\",\n  \"brumunddal.no\",\n  \"bryne.no\",\n  \"bronnoysund.no\",\n  \"brønnøysund.no\",\n  \"drobak.no\",\n  \"drøbak.no\",\n  \"egersund.no\",\n  \"fetsund.no\",\n  \"floro.no\",\n  \"florø.no\",\n  \"fredrikstad.no\",\n  \"hokksund.no\",\n  \"honefoss.no\",\n  \"hønefoss.no\",\n  \"jessheim.no\",\n  \"jorpeland.no\",\n  \"jørpeland.no\",\n  \"kirkenes.no\",\n  \"kopervik.no\",\n  \"krokstadelva.no\",\n  \"langevag.no\",\n  \"langevåg.no\",\n  \"leirvik.no\",\n  \"mjondalen.no\",\n  \"mjøndalen.no\",\n  \"mo-i-rana.no\",\n  \"mosjoen.no\",\n  \"mosjøen.no\",\n  \"nesoddtangen.no\",\n  \"orkanger.no\",\n  \"osoyro.no\",\n  \"osøyro.no\",\n  \"raholt.no\",\n  \"råholt.no\",\n  \"sandnessjoen.no\",\n  \"sandnessjøen.no\",\n  \"skedsmokorset.no\",\n  \"slattum.no\",\n  \"spjelkavik.no\",\n  \"stathelle.no\",\n  \"stavern.no\",\n  \"stjordalshalsen.no\",\n  \"stjørdalshalsen.no\",\n  \"tananger.no\",\n  \"tranby.no\",\n  \"vossevangen.no\",\n  \"afjord.no\",\n  \"åfjord.no\",\n  \"agdenes.no\",\n  \"al.no\",\n  \"ål.no\",\n  \"alesund.no\",\n  \"ålesund.no\",\n  \"alstahaug.no\",\n  \"alta.no\",\n  \"áltá.no\",\n  \"alaheadju.no\",\n  \"álaheadju.no\",\n  \"alvdal.no\",\n  \"amli.no\",\n  \"åmli.no\",\n  \"amot.no\",\n  \"åmot.no\",\n  \"andebu.no\",\n  \"andoy.no\",\n  \"andøy.no\",\n  \"andasuolo.no\",\n  \"ardal.no\",\n  \"årdal.no\",\n  \"aremark.no\",\n  \"arendal.no\",\n  \"ås.no\",\n  \"aseral.no\",\n  \"åseral.no\",\n  \"asker.no\",\n  \"askim.no\",\n  \"askvoll.no\",\n  \"askoy.no\",\n  \"askøy.no\",\n  \"asnes.no\",\n  \"åsnes.no\",\n  \"audnedaln.no\",\n  \"aukra.no\",\n  \"aure.no\",\n  \"aurland.no\",\n  \"aurskog-holand.no\",\n  \"aurskog-høland.no\",\n  \"austevoll.no\",\n  \"austrheim.no\",\n  \"averoy.no\",\n  \"averøy.no\",\n  \"balestrand.no\",\n  \"ballangen.no\",\n  \"balat.no\",\n  \"bálát.no\",\n  \"balsfjord.no\",\n  \"bahccavuotna.no\",\n  \"báhccavuotna.no\",\n  \"bamble.no\",\n  \"bardu.no\",\n  \"beardu.no\",\n  \"beiarn.no\",\n  \"bajddar.no\",\n  \"bájddar.no\",\n  \"baidar.no\",\n  \"báidár.no\",\n  \"berg.no\",\n  \"bergen.no\",\n  \"berlevag.no\",\n  \"berlevåg.no\",\n  \"bearalvahki.no\",\n  \"bearalváhki.no\",\n  \"bindal.no\",\n  \"birkenes.no\",\n  \"bjarkoy.no\",\n  \"bjarkøy.no\",\n  \"bjerkreim.no\",\n  \"bjugn.no\",\n  \"bodo.no\",\n  \"bodø.no\",\n  \"badaddja.no\",\n  \"bådåddjå.no\",\n  \"budejju.no\",\n  \"bokn.no\",\n  \"bremanger.no\",\n  \"bronnoy.no\",\n  \"brønnøy.no\",\n  \"bygland.no\",\n  \"bykle.no\",\n  \"barum.no\",\n  \"bærum.no\",\n  \"bievat.no\",\n  \"bievát.no\",\n  \"bomlo.no\",\n  \"bømlo.no\",\n  \"batsfjord.no\",\n  \"båtsfjord.no\",\n  \"bahcavuotna.no\",\n  \"báhcavuotna.no\",\n  \"dovre.no\",\n  \"drammen.no\",\n  \"drangedal.no\",\n  \"dyroy.no\",\n  \"dyrøy.no\",\n  \"donna.no\",\n  \"dønna.no\",\n  \"eid.no\",\n  \"eidfjord.no\",\n  \"eidsberg.no\",\n  \"eidskog.no\",\n  \"eidsvoll.no\",\n  \"eigersund.no\",\n  \"elverum.no\",\n  \"enebakk.no\",\n  \"engerdal.no\",\n  \"etne.no\",\n  \"etnedal.no\",\n  \"evenes.no\",\n  \"evenassi.no\",\n  \"evenášši.no\",\n  \"evje-og-hornnes.no\",\n  \"farsund.no\",\n  \"fauske.no\",\n  \"fuossko.no\",\n  \"fuoisku.no\",\n  \"fedje.no\",\n  \"fet.no\",\n  \"finnoy.no\",\n  \"finnøy.no\",\n  \"fitjar.no\",\n  \"fjaler.no\",\n  \"fjell.no\",\n  \"flakstad.no\",\n  \"flatanger.no\",\n  \"flekkefjord.no\",\n  \"flesberg.no\",\n  \"flora.no\",\n  \"fla.no\",\n  \"flå.no\",\n  \"folldal.no\",\n  \"forsand.no\",\n  \"fosnes.no\",\n  \"frei.no\",\n  \"frogn.no\",\n  \"froland.no\",\n  \"frosta.no\",\n  \"frana.no\",\n  \"fræna.no\",\n  \"froya.no\",\n  \"frøya.no\",\n  \"fusa.no\",\n  \"fyresdal.no\",\n  \"forde.no\",\n  \"førde.no\",\n  \"gamvik.no\",\n  \"gangaviika.no\",\n  \"gáŋgaviika.no\",\n  \"gaular.no\",\n  \"gausdal.no\",\n  \"gildeskal.no\",\n  \"gildeskål.no\",\n  \"giske.no\",\n  \"gjemnes.no\",\n  \"gjerdrum.no\",\n  \"gjerstad.no\",\n  \"gjesdal.no\",\n  \"gjovik.no\",\n  \"gjøvik.no\",\n  \"gloppen.no\",\n  \"gol.no\",\n  \"gran.no\",\n  \"grane.no\",\n  \"granvin.no\",\n  \"gratangen.no\",\n  \"grimstad.no\",\n  \"grong.no\",\n  \"kraanghke.no\",\n  \"kråanghke.no\",\n  \"grue.no\",\n  \"gulen.no\",\n  \"hadsel.no\",\n  \"halden.no\",\n  \"halsa.no\",\n  \"hamar.no\",\n  \"hamaroy.no\",\n  \"habmer.no\",\n  \"hábmer.no\",\n  \"hapmir.no\",\n  \"hápmir.no\",\n  \"hammerfest.no\",\n  \"hammarfeasta.no\",\n  \"hámmárfeasta.no\",\n  \"haram.no\",\n  \"hareid.no\",\n  \"harstad.no\",\n  \"hasvik.no\",\n  \"aknoluokta.no\",\n  \"ákŋoluokta.no\",\n  \"hattfjelldal.no\",\n  \"aarborte.no\",\n  \"haugesund.no\",\n  \"hemne.no\",\n  \"hemnes.no\",\n  \"hemsedal.no\",\n  \"hitra.no\",\n  \"hjartdal.no\",\n  \"hjelmeland.no\",\n  \"hobol.no\",\n  \"hobøl.no\",\n  \"hof.no\",\n  \"hol.no\",\n  \"hole.no\",\n  \"holmestrand.no\",\n  \"holtalen.no\",\n  \"holtålen.no\",\n  \"hornindal.no\",\n  \"horten.no\",\n  \"hurdal.no\",\n  \"hurum.no\",\n  \"hvaler.no\",\n  \"hyllestad.no\",\n  \"hagebostad.no\",\n  \"hægebostad.no\",\n  \"hoyanger.no\",\n  \"høyanger.no\",\n  \"hoylandet.no\",\n  \"høylandet.no\",\n  \"ha.no\",\n  \"hå.no\",\n  \"ibestad.no\",\n  \"inderoy.no\",\n  \"inderøy.no\",\n  \"iveland.no\",\n  \"jevnaker.no\",\n  \"jondal.no\",\n  \"jolster.no\",\n  \"jølster.no\",\n  \"karasjok.no\",\n  \"karasjohka.no\",\n  \"kárášjohka.no\",\n  \"karlsoy.no\",\n  \"galsa.no\",\n  \"gálsá.no\",\n  \"karmoy.no\",\n  \"karmøy.no\",\n  \"kautokeino.no\",\n  \"guovdageaidnu.no\",\n  \"klepp.no\",\n  \"klabu.no\",\n  \"klæbu.no\",\n  \"kongsberg.no\",\n  \"kongsvinger.no\",\n  \"kragero.no\",\n  \"kragerø.no\",\n  \"kristiansand.no\",\n  \"kristiansund.no\",\n  \"krodsherad.no\",\n  \"krødsherad.no\",\n  \"kvalsund.no\",\n  \"rahkkeravju.no\",\n  \"ráhkkerávju.no\",\n  \"kvam.no\",\n  \"kvinesdal.no\",\n  \"kvinnherad.no\",\n  \"kviteseid.no\",\n  \"kvitsoy.no\",\n  \"kvitsøy.no\",\n  \"kvafjord.no\",\n  \"kvæfjord.no\",\n  \"giehtavuoatna.no\",\n  \"kvanangen.no\",\n  \"kvænangen.no\",\n  \"navuotna.no\",\n  \"návuotna.no\",\n  \"kafjord.no\",\n  \"kåfjord.no\",\n  \"gaivuotna.no\",\n  \"gáivuotna.no\",\n  \"larvik.no\",\n  \"lavangen.no\",\n  \"lavagis.no\",\n  \"loabat.no\",\n  \"loabát.no\",\n  \"lebesby.no\",\n  \"davvesiida.no\",\n  \"leikanger.no\",\n  \"leirfjord.no\",\n  \"leka.no\",\n  \"leksvik.no\",\n  \"lenvik.no\",\n  \"leangaviika.no\",\n  \"leaŋgaviika.no\",\n  \"lesja.no\",\n  \"levanger.no\",\n  \"lier.no\",\n  \"lierne.no\",\n  \"lillehammer.no\",\n  \"lillesand.no\",\n  \"lindesnes.no\",\n  \"lindas.no\",\n  \"lindås.no\",\n  \"lom.no\",\n  \"loppa.no\",\n  \"lahppi.no\",\n  \"láhppi.no\",\n  \"lund.no\",\n  \"lunner.no\",\n  \"luroy.no\",\n  \"lurøy.no\",\n  \"luster.no\",\n  \"lyngdal.no\",\n  \"lyngen.no\",\n  \"ivgu.no\",\n  \"lardal.no\",\n  \"lerdal.no\",\n  \"lærdal.no\",\n  \"lodingen.no\",\n  \"lødingen.no\",\n  \"lorenskog.no\",\n  \"lørenskog.no\",\n  \"loten.no\",\n  \"løten.no\",\n  \"malvik.no\",\n  \"masoy.no\",\n  \"måsøy.no\",\n  \"muosat.no\",\n  \"muosát.no\",\n  \"mandal.no\",\n  \"marker.no\",\n  \"marnardal.no\",\n  \"masfjorden.no\",\n  \"meland.no\",\n  \"meldal.no\",\n  \"melhus.no\",\n  \"meloy.no\",\n  \"meløy.no\",\n  \"meraker.no\",\n  \"meråker.no\",\n  \"moareke.no\",\n  \"moåreke.no\",\n  \"midsund.no\",\n  \"midtre-gauldal.no\",\n  \"modalen.no\",\n  \"modum.no\",\n  \"molde.no\",\n  \"moskenes.no\",\n  \"moss.no\",\n  \"mosvik.no\",\n  \"malselv.no\",\n  \"målselv.no\",\n  \"malatvuopmi.no\",\n  \"málatvuopmi.no\",\n  \"namdalseid.no\",\n  \"aejrie.no\",\n  \"namsos.no\",\n  \"namsskogan.no\",\n  \"naamesjevuemie.no\",\n  \"nååmesjevuemie.no\",\n  \"laakesvuemie.no\",\n  \"nannestad.no\",\n  \"narvik.no\",\n  \"narviika.no\",\n  \"naustdal.no\",\n  \"nedre-eiker.no\",\n  \"nesna.no\",\n  \"nesodden.no\",\n  \"nesseby.no\",\n  \"unjarga.no\",\n  \"unjárga.no\",\n  \"nesset.no\",\n  \"nissedal.no\",\n  \"nittedal.no\",\n  \"nord-aurdal.no\",\n  \"nord-fron.no\",\n  \"nord-odal.no\",\n  \"norddal.no\",\n  \"nordkapp.no\",\n  \"davvenjarga.no\",\n  \"davvenjárga.no\",\n  \"nordre-land.no\",\n  \"nordreisa.no\",\n  \"raisa.no\",\n  \"ráisa.no\",\n  \"nore-og-uvdal.no\",\n  \"notodden.no\",\n  \"naroy.no\",\n  \"nærøy.no\",\n  \"notteroy.no\",\n  \"nøtterøy.no\",\n  \"odda.no\",\n  \"oksnes.no\",\n  \"øksnes.no\",\n  \"oppdal.no\",\n  \"oppegard.no\",\n  \"oppegård.no\",\n  \"orkdal.no\",\n  \"orland.no\",\n  \"ørland.no\",\n  \"orskog.no\",\n  \"ørskog.no\",\n  \"orsta.no\",\n  \"ørsta.no\",\n  \"osen.no\",\n  \"osteroy.no\",\n  \"osterøy.no\",\n  \"ostre-toten.no\",\n  \"østre-toten.no\",\n  \"overhalla.no\",\n  \"ovre-eiker.no\",\n  \"øvre-eiker.no\",\n  \"oyer.no\",\n  \"øyer.no\",\n  \"oygarden.no\",\n  \"øygarden.no\",\n  \"oystre-slidre.no\",\n  \"øystre-slidre.no\",\n  \"porsanger.no\",\n  \"porsangu.no\",\n  \"porsáŋgu.no\",\n  \"porsgrunn.no\",\n  \"radoy.no\",\n  \"radøy.no\",\n  \"rakkestad.no\",\n  \"rana.no\",\n  \"ruovat.no\",\n  \"randaberg.no\",\n  \"rauma.no\",\n  \"rendalen.no\",\n  \"rennebu.no\",\n  \"rennesoy.no\",\n  \"rennesøy.no\",\n  \"rindal.no\",\n  \"ringebu.no\",\n  \"ringerike.no\",\n  \"ringsaker.no\",\n  \"rissa.no\",\n  \"risor.no\",\n  \"risør.no\",\n  \"roan.no\",\n  \"rollag.no\",\n  \"rygge.no\",\n  \"ralingen.no\",\n  \"rælingen.no\",\n  \"rodoy.no\",\n  \"rødøy.no\",\n  \"romskog.no\",\n  \"rømskog.no\",\n  \"roros.no\",\n  \"røros.no\",\n  \"rost.no\",\n  \"røst.no\",\n  \"royken.no\",\n  \"røyken.no\",\n  \"royrvik.no\",\n  \"røyrvik.no\",\n  \"rade.no\",\n  \"råde.no\",\n  \"salangen.no\",\n  \"siellak.no\",\n  \"saltdal.no\",\n  \"salat.no\",\n  \"sálát.no\",\n  \"sálat.no\",\n  \"samnanger.no\",\n  \"sandefjord.no\",\n  \"sandnes.no\",\n  \"sandoy.no\",\n  \"sandøy.no\",\n  \"sarpsborg.no\",\n  \"sauda.no\",\n  \"sauherad.no\",\n  \"sel.no\",\n  \"selbu.no\",\n  \"selje.no\",\n  \"seljord.no\",\n  \"sigdal.no\",\n  \"siljan.no\",\n  \"sirdal.no\",\n  \"skaun.no\",\n  \"skedsmo.no\",\n  \"ski.no\",\n  \"skien.no\",\n  \"skiptvet.no\",\n  \"skjervoy.no\",\n  \"skjervøy.no\",\n  \"skierva.no\",\n  \"skiervá.no\",\n  \"skjak.no\",\n  \"skjåk.no\",\n  \"skodje.no\",\n  \"skanland.no\",\n  \"skånland.no\",\n  \"skanit.no\",\n  \"skánit.no\",\n  \"smola.no\",\n  \"smøla.no\",\n  \"snillfjord.no\",\n  \"snasa.no\",\n  \"snåsa.no\",\n  \"snoasa.no\",\n  \"snaase.no\",\n  \"snåase.no\",\n  \"sogndal.no\",\n  \"sokndal.no\",\n  \"sola.no\",\n  \"solund.no\",\n  \"songdalen.no\",\n  \"sortland.no\",\n  \"spydeberg.no\",\n  \"stange.no\",\n  \"stavanger.no\",\n  \"steigen.no\",\n  \"steinkjer.no\",\n  \"stjordal.no\",\n  \"stjørdal.no\",\n  \"stokke.no\",\n  \"stor-elvdal.no\",\n  \"stord.no\",\n  \"stordal.no\",\n  \"storfjord.no\",\n  \"omasvuotna.no\",\n  \"strand.no\",\n  \"stranda.no\",\n  \"stryn.no\",\n  \"sula.no\",\n  \"suldal.no\",\n  \"sund.no\",\n  \"sunndal.no\",\n  \"surnadal.no\",\n  \"sveio.no\",\n  \"svelvik.no\",\n  \"sykkylven.no\",\n  \"sogne.no\",\n  \"søgne.no\",\n  \"somna.no\",\n  \"sømna.no\",\n  \"sondre-land.no\",\n  \"søndre-land.no\",\n  \"sor-aurdal.no\",\n  \"sør-aurdal.no\",\n  \"sor-fron.no\",\n  \"sør-fron.no\",\n  \"sor-odal.no\",\n  \"sør-odal.no\",\n  \"sor-varanger.no\",\n  \"sør-varanger.no\",\n  \"matta-varjjat.no\",\n  \"mátta-várjjat.no\",\n  \"sorfold.no\",\n  \"sørfold.no\",\n  \"sorreisa.no\",\n  \"sørreisa.no\",\n  \"sorum.no\",\n  \"sørum.no\",\n  \"tana.no\",\n  \"deatnu.no\",\n  \"time.no\",\n  \"tingvoll.no\",\n  \"tinn.no\",\n  \"tjeldsund.no\",\n  \"dielddanuorri.no\",\n  \"tjome.no\",\n  \"tjøme.no\",\n  \"tokke.no\",\n  \"tolga.no\",\n  \"torsken.no\",\n  \"tranoy.no\",\n  \"tranøy.no\",\n  \"tromso.no\",\n  \"tromsø.no\",\n  \"tromsa.no\",\n  \"romsa.no\",\n  \"trondheim.no\",\n  \"troandin.no\",\n  \"trysil.no\",\n  \"trana.no\",\n  \"træna.no\",\n  \"trogstad.no\",\n  \"trøgstad.no\",\n  \"tvedestrand.no\",\n  \"tydal.no\",\n  \"tynset.no\",\n  \"tysfjord.no\",\n  \"divtasvuodna.no\",\n  \"divttasvuotna.no\",\n  \"tysnes.no\",\n  \"tysvar.no\",\n  \"tysvær.no\",\n  \"tonsberg.no\",\n  \"tønsberg.no\",\n  \"ullensaker.no\",\n  \"ullensvang.no\",\n  \"ulvik.no\",\n  \"utsira.no\",\n  \"vadso.no\",\n  \"vadsø.no\",\n  \"cahcesuolo.no\",\n  \"čáhcesuolo.no\",\n  \"vaksdal.no\",\n  \"valle.no\",\n  \"vang.no\",\n  \"vanylven.no\",\n  \"vardo.no\",\n  \"vardø.no\",\n  \"varggat.no\",\n  \"várggát.no\",\n  \"vefsn.no\",\n  \"vaapste.no\",\n  \"vega.no\",\n  \"vegarshei.no\",\n  \"vegårshei.no\",\n  \"vennesla.no\",\n  \"verdal.no\",\n  \"verran.no\",\n  \"vestby.no\",\n  \"vestnes.no\",\n  \"vestre-slidre.no\",\n  \"vestre-toten.no\",\n  \"vestvagoy.no\",\n  \"vestvågøy.no\",\n  \"vevelstad.no\",\n  \"vik.no\",\n  \"vikna.no\",\n  \"vindafjord.no\",\n  \"volda.no\",\n  \"voss.no\",\n  \"varoy.no\",\n  \"værøy.no\",\n  \"vagan.no\",\n  \"vågan.no\",\n  \"voagat.no\",\n  \"vagsoy.no\",\n  \"vågsøy.no\",\n  \"vaga.no\",\n  \"vågå.no\",\n  \"*.np\",\n  \"biz.nr\",\n  \"info.nr\",\n  \"gov.nr\",\n  \"edu.nr\",\n  \"org.nr\",\n  \"net.nr\",\n  \"com.nr\",\n  \"ac.nz\",\n  \"co.nz\",\n  \"cri.nz\",\n  \"geek.nz\",\n  \"gen.nz\",\n  \"govt.nz\",\n  \"health.nz\",\n  \"iwi.nz\",\n  \"kiwi.nz\",\n  \"maori.nz\",\n  \"mil.nz\",\n  \"māori.nz\",\n  \"net.nz\",\n  \"org.nz\",\n  \"parliament.nz\",\n  \"school.nz\",\n  \"co.om\",\n  \"com.om\",\n  \"edu.om\",\n  \"gov.om\",\n  \"med.om\",\n  \"museum.om\",\n  \"net.om\",\n  \"org.om\",\n  \"pro.om\",\n  \"ac.pa\",\n  \"gob.pa\",\n  \"com.pa\",\n  \"org.pa\",\n  \"sld.pa\",\n  \"edu.pa\",\n  \"net.pa\",\n  \"ing.pa\",\n  \"abo.pa\",\n  \"med.pa\",\n  \"nom.pa\",\n  \"edu.pe\",\n  \"gob.pe\",\n  \"nom.pe\",\n  \"mil.pe\",\n  \"org.pe\",\n  \"com.pe\",\n  \"net.pe\",\n  \"com.pf\",\n  \"org.pf\",\n  \"edu.pf\",\n  \"*.pg\",\n  \"com.ph\",\n  \"net.ph\",\n  \"org.ph\",\n  \"gov.ph\",\n  \"edu.ph\",\n  \"ngo.ph\",\n  \"mil.ph\",\n  \"i.ph\",\n  \"com.pk\",\n  \"net.pk\",\n  \"edu.pk\",\n  \"org.pk\",\n  \"fam.pk\",\n  \"biz.pk\",\n  \"web.pk\",\n  \"gov.pk\",\n  \"gob.pk\",\n  \"gok.pk\",\n  \"gon.pk\",\n  \"gop.pk\",\n  \"gos.pk\",\n  \"info.pk\",\n  \"com.pl\",\n  \"net.pl\",\n  \"org.pl\",\n  \"aid.pl\",\n  \"agro.pl\",\n  \"atm.pl\",\n  \"auto.pl\",\n  \"biz.pl\",\n  \"edu.pl\",\n  \"gmina.pl\",\n  \"gsm.pl\",\n  \"info.pl\",\n  \"mail.pl\",\n  \"miasta.pl\",\n  \"media.pl\",\n  \"mil.pl\",\n  \"nieruchomosci.pl\",\n  \"nom.pl\",\n  \"pc.pl\",\n  \"powiat.pl\",\n  \"priv.pl\",\n  \"realestate.pl\",\n  \"rel.pl\",\n  \"sex.pl\",\n  \"shop.pl\",\n  \"sklep.pl\",\n  \"sos.pl\",\n  \"szkola.pl\",\n  \"targi.pl\",\n  \"tm.pl\",\n  \"tourism.pl\",\n  \"travel.pl\",\n  \"turystyka.pl\",\n  \"gov.pl\",\n  \"augustow.pl\",\n  \"babia-gora.pl\",\n  \"bedzin.pl\",\n  \"beskidy.pl\",\n  \"bialowieza.pl\",\n  \"bialystok.pl\",\n  \"bielawa.pl\",\n  \"bieszczady.pl\",\n  \"boleslawiec.pl\",\n  \"bydgoszcz.pl\",\n  \"bytom.pl\",\n  \"cieszyn.pl\",\n  \"czeladz.pl\",\n  \"czest.pl\",\n  \"dlugoleka.pl\",\n  \"elblag.pl\",\n  \"elk.pl\",\n  \"glogow.pl\",\n  \"gniezno.pl\",\n  \"gorlice.pl\",\n  \"grajewo.pl\",\n  \"ilawa.pl\",\n  \"jaworzno.pl\",\n  \"jelenia-gora.pl\",\n  \"jgora.pl\",\n  \"kalisz.pl\",\n  \"kazimierz-dolny.pl\",\n  \"karpacz.pl\",\n  \"kartuzy.pl\",\n  \"kaszuby.pl\",\n  \"katowice.pl\",\n  \"kepno.pl\",\n  \"ketrzyn.pl\",\n  \"klodzko.pl\",\n  \"kobierzyce.pl\",\n  \"kolobrzeg.pl\",\n  \"konin.pl\",\n  \"konskowola.pl\",\n  \"kutno.pl\",\n  \"lapy.pl\",\n  \"lebork.pl\",\n  \"legnica.pl\",\n  \"lezajsk.pl\",\n  \"limanowa.pl\",\n  \"lomza.pl\",\n  \"lowicz.pl\",\n  \"lubin.pl\",\n  \"lukow.pl\",\n  \"malbork.pl\",\n  \"malopolska.pl\",\n  \"mazowsze.pl\",\n  \"mazury.pl\",\n  \"mielec.pl\",\n  \"mielno.pl\",\n  \"mragowo.pl\",\n  \"naklo.pl\",\n  \"nowaruda.pl\",\n  \"nysa.pl\",\n  \"olawa.pl\",\n  \"olecko.pl\",\n  \"olkusz.pl\",\n  \"olsztyn.pl\",\n  \"opoczno.pl\",\n  \"opole.pl\",\n  \"ostroda.pl\",\n  \"ostroleka.pl\",\n  \"ostrowiec.pl\",\n  \"ostrowwlkp.pl\",\n  \"pila.pl\",\n  \"pisz.pl\",\n  \"podhale.pl\",\n  \"podlasie.pl\",\n  \"polkowice.pl\",\n  \"pomorze.pl\",\n  \"pomorskie.pl\",\n  \"prochowice.pl\",\n  \"pruszkow.pl\",\n  \"przeworsk.pl\",\n  \"pulawy.pl\",\n  \"radom.pl\",\n  \"rawa-maz.pl\",\n  \"rybnik.pl\",\n  \"rzeszow.pl\",\n  \"sanok.pl\",\n  \"sejny.pl\",\n  \"slask.pl\",\n  \"slupsk.pl\",\n  \"sosnowiec.pl\",\n  \"stalowa-wola.pl\",\n  \"skoczow.pl\",\n  \"starachowice.pl\",\n  \"stargard.pl\",\n  \"suwalki.pl\",\n  \"swidnica.pl\",\n  \"swiebodzin.pl\",\n  \"swinoujscie.pl\",\n  \"szczecin.pl\",\n  \"szczytno.pl\",\n  \"tarnobrzeg.pl\",\n  \"tgory.pl\",\n  \"turek.pl\",\n  \"tychy.pl\",\n  \"ustka.pl\",\n  \"walbrzych.pl\",\n  \"warmia.pl\",\n  \"warszawa.pl\",\n  \"waw.pl\",\n  \"wegrow.pl\",\n  \"wielun.pl\",\n  \"wlocl.pl\",\n  \"wloclawek.pl\",\n  \"wodzislaw.pl\",\n  \"wolomin.pl\",\n  \"wroclaw.pl\",\n  \"zachpomor.pl\",\n  \"zagan.pl\",\n  \"zarow.pl\",\n  \"zgora.pl\",\n  \"zgorzelec.pl\",\n  \"gov.pn\",\n  \"co.pn\",\n  \"org.pn\",\n  \"edu.pn\",\n  \"net.pn\",\n  \"com.pr\",\n  \"net.pr\",\n  \"org.pr\",\n  \"gov.pr\",\n  \"edu.pr\",\n  \"isla.pr\",\n  \"pro.pr\",\n  \"biz.pr\",\n  \"info.pr\",\n  \"name.pr\",\n  \"est.pr\",\n  \"prof.pr\",\n  \"ac.pr\",\n  \"aaa.pro\",\n  \"aca.pro\",\n  \"acct.pro\",\n  \"avocat.pro\",\n  \"bar.pro\",\n  \"cpa.pro\",\n  \"eng.pro\",\n  \"jur.pro\",\n  \"law.pro\",\n  \"med.pro\",\n  \"recht.pro\",\n  \"edu.ps\",\n  \"gov.ps\",\n  \"sec.ps\",\n  \"plo.ps\",\n  \"com.ps\",\n  \"org.ps\",\n  \"net.ps\",\n  \"net.pt\",\n  \"gov.pt\",\n  \"org.pt\",\n  \"edu.pt\",\n  \"int.pt\",\n  \"publ.pt\",\n  \"com.pt\",\n  \"nome.pt\",\n  \"co.pw\",\n  \"ne.pw\",\n  \"or.pw\",\n  \"ed.pw\",\n  \"go.pw\",\n  \"belau.pw\",\n  \"com.py\",\n  \"coop.py\",\n  \"edu.py\",\n  \"gov.py\",\n  \"mil.py\",\n  \"net.py\",\n  \"org.py\",\n  \"com.qa\",\n  \"edu.qa\",\n  \"gov.qa\",\n  \"mil.qa\",\n  \"name.qa\",\n  \"net.qa\",\n  \"org.qa\",\n  \"sch.qa\",\n  \"asso.re\",\n  \"com.re\",\n  \"nom.re\",\n  \"arts.ro\",\n  \"com.ro\",\n  \"firm.ro\",\n  \"info.ro\",\n  \"nom.ro\",\n  \"nt.ro\",\n  \"org.ro\",\n  \"rec.ro\",\n  \"store.ro\",\n  \"tm.ro\",\n  \"www.ro\",\n  \"ac.rs\",\n  \"co.rs\",\n  \"edu.rs\",\n  \"gov.rs\",\n  \"in.rs\",\n  \"org.rs\",\n  \"ac.rw\",\n  \"co.rw\",\n  \"coop.rw\",\n  \"gov.rw\",\n  \"mil.rw\",\n  \"net.rw\",\n  \"org.rw\",\n  \"com.sa\",\n  \"net.sa\",\n  \"org.sa\",\n  \"gov.sa\",\n  \"med.sa\",\n  \"pub.sa\",\n  \"edu.sa\",\n  \"sch.sa\",\n  \"com.sb\",\n  \"edu.sb\",\n  \"gov.sb\",\n  \"net.sb\",\n  \"org.sb\",\n  \"com.sc\",\n  \"gov.sc\",\n  \"net.sc\",\n  \"org.sc\",\n  \"edu.sc\",\n  \"com.sd\",\n  \"net.sd\",\n  \"org.sd\",\n  \"edu.sd\",\n  \"med.sd\",\n  \"tv.sd\",\n  \"gov.sd\",\n  \"info.sd\",\n  \"a.se\",\n  \"ac.se\",\n  \"b.se\",\n  \"bd.se\",\n  \"brand.se\",\n  \"c.se\",\n  \"d.se\",\n  \"e.se\",\n  \"f.se\",\n  \"fh.se\",\n  \"fhsk.se\",\n  \"fhv.se\",\n  \"g.se\",\n  \"h.se\",\n  \"i.se\",\n  \"k.se\",\n  \"komforb.se\",\n  \"kommunalforbund.se\",\n  \"komvux.se\",\n  \"l.se\",\n  \"lanbib.se\",\n  \"m.se\",\n  \"n.se\",\n  \"naturbruksgymn.se\",\n  \"o.se\",\n  \"org.se\",\n  \"p.se\",\n  \"parti.se\",\n  \"pp.se\",\n  \"press.se\",\n  \"r.se\",\n  \"s.se\",\n  \"t.se\",\n  \"tm.se\",\n  \"u.se\",\n  \"w.se\",\n  \"x.se\",\n  \"y.se\",\n  \"z.se\",\n  \"com.sg\",\n  \"net.sg\",\n  \"org.sg\",\n  \"gov.sg\",\n  \"edu.sg\",\n  \"per.sg\",\n  \"com.sh\",\n  \"net.sh\",\n  \"gov.sh\",\n  \"org.sh\",\n  \"mil.sh\",\n  \"com.sl\",\n  \"net.sl\",\n  \"edu.sl\",\n  \"gov.sl\",\n  \"org.sl\",\n  \"art.sn\",\n  \"com.sn\",\n  \"edu.sn\",\n  \"gouv.sn\",\n  \"org.sn\",\n  \"perso.sn\",\n  \"univ.sn\",\n  \"com.so\",\n  \"edu.so\",\n  \"gov.so\",\n  \"me.so\",\n  \"net.so\",\n  \"org.so\",\n  \"biz.ss\",\n  \"com.ss\",\n  \"edu.ss\",\n  \"gov.ss\",\n  \"me.ss\",\n  \"net.ss\",\n  \"org.ss\",\n  \"sch.ss\",\n  \"co.st\",\n  \"com.st\",\n  \"consulado.st\",\n  \"edu.st\",\n  \"embaixada.st\",\n  \"mil.st\",\n  \"net.st\",\n  \"org.st\",\n  \"principe.st\",\n  \"saotome.st\",\n  \"store.st\",\n  \"com.sv\",\n  \"edu.sv\",\n  \"gob.sv\",\n  \"org.sv\",\n  \"red.sv\",\n  \"gov.sx\",\n  \"edu.sy\",\n  \"gov.sy\",\n  \"net.sy\",\n  \"mil.sy\",\n  \"com.sy\",\n  \"org.sy\",\n  \"co.sz\",\n  \"ac.sz\",\n  \"org.sz\",\n  \"ac.th\",\n  \"co.th\",\n  \"go.th\",\n  \"in.th\",\n  \"mi.th\",\n  \"net.th\",\n  \"or.th\",\n  \"ac.tj\",\n  \"biz.tj\",\n  \"co.tj\",\n  \"com.tj\",\n  \"edu.tj\",\n  \"go.tj\",\n  \"gov.tj\",\n  \"int.tj\",\n  \"mil.tj\",\n  \"name.tj\",\n  \"net.tj\",\n  \"nic.tj\",\n  \"org.tj\",\n  \"test.tj\",\n  \"web.tj\",\n  \"gov.tl\",\n  \"com.tm\",\n  \"co.tm\",\n  \"org.tm\",\n  \"net.tm\",\n  \"nom.tm\",\n  \"gov.tm\",\n  \"mil.tm\",\n  \"edu.tm\",\n  \"com.tn\",\n  \"ens.tn\",\n  \"fin.tn\",\n  \"gov.tn\",\n  \"ind.tn\",\n  \"info.tn\",\n  \"intl.tn\",\n  \"mincom.tn\",\n  \"nat.tn\",\n  \"net.tn\",\n  \"org.tn\",\n  \"perso.tn\",\n  \"tourism.tn\",\n  \"com.to\",\n  \"gov.to\",\n  \"net.to\",\n  \"org.to\",\n  \"edu.to\",\n  \"mil.to\",\n  \"av.tr\",\n  \"bbs.tr\",\n  \"bel.tr\",\n  \"biz.tr\",\n  \"com.tr\",\n  \"dr.tr\",\n  \"edu.tr\",\n  \"gen.tr\",\n  \"gov.tr\",\n  \"info.tr\",\n  \"mil.tr\",\n  \"k12.tr\",\n  \"kep.tr\",\n  \"name.tr\",\n  \"net.tr\",\n  \"org.tr\",\n  \"pol.tr\",\n  \"tel.tr\",\n  \"tsk.tr\",\n  \"tv.tr\",\n  \"web.tr\",\n  \"nc.tr\",\n  \"co.tt\",\n  \"com.tt\",\n  \"org.tt\",\n  \"net.tt\",\n  \"biz.tt\",\n  \"info.tt\",\n  \"pro.tt\",\n  \"int.tt\",\n  \"coop.tt\",\n  \"jobs.tt\",\n  \"mobi.tt\",\n  \"travel.tt\",\n  \"museum.tt\",\n  \"aero.tt\",\n  \"name.tt\",\n  \"gov.tt\",\n  \"edu.tt\",\n  \"edu.tw\",\n  \"gov.tw\",\n  \"mil.tw\",\n  \"com.tw\",\n  \"net.tw\",\n  \"org.tw\",\n  \"idv.tw\",\n  \"game.tw\",\n  \"ebiz.tw\",\n  \"club.tw\",\n  \"網路.tw\",\n  \"組織.tw\",\n  \"商業.tw\",\n  \"ac.tz\",\n  \"co.tz\",\n  \"go.tz\",\n  \"hotel.tz\",\n  \"info.tz\",\n  \"me.tz\",\n  \"mil.tz\",\n  \"mobi.tz\",\n  \"ne.tz\",\n  \"or.tz\",\n  \"sc.tz\",\n  \"tv.tz\",\n  \"com.ua\",\n  \"edu.ua\",\n  \"gov.ua\",\n  \"in.ua\",\n  \"net.ua\",\n  \"org.ua\",\n  \"cherkassy.ua\",\n  \"cherkasy.ua\",\n  \"chernigov.ua\",\n  \"chernihiv.ua\",\n  \"chernivtsi.ua\",\n  \"chernovtsy.ua\",\n  \"ck.ua\",\n  \"cn.ua\",\n  \"cr.ua\",\n  \"crimea.ua\",\n  \"cv.ua\",\n  \"dn.ua\",\n  \"dnepropetrovsk.ua\",\n  \"dnipropetrovsk.ua\",\n  \"donetsk.ua\",\n  \"dp.ua\",\n  \"if.ua\",\n  \"ivano-frankivsk.ua\",\n  \"kh.ua\",\n  \"kharkiv.ua\",\n  \"kharkov.ua\",\n  \"kherson.ua\",\n  \"khmelnitskiy.ua\",\n  \"khmelnytskyi.ua\",\n  \"kiev.ua\",\n  \"kirovograd.ua\",\n  \"km.ua\",\n  \"kr.ua\",\n  \"kropyvnytskyi.ua\",\n  \"krym.ua\",\n  \"ks.ua\",\n  \"kv.ua\",\n  \"kyiv.ua\",\n  \"lg.ua\",\n  \"lt.ua\",\n  \"lugansk.ua\",\n  \"lutsk.ua\",\n  \"lv.ua\",\n  \"lviv.ua\",\n  \"mk.ua\",\n  \"mykolaiv.ua\",\n  \"nikolaev.ua\",\n  \"od.ua\",\n  \"odesa.ua\",\n  \"odessa.ua\",\n  \"pl.ua\",\n  \"poltava.ua\",\n  \"rivne.ua\",\n  \"rovno.ua\",\n  \"rv.ua\",\n  \"sb.ua\",\n  \"sebastopol.ua\",\n  \"sevastopol.ua\",\n  \"sm.ua\",\n  \"sumy.ua\",\n  \"te.ua\",\n  \"ternopil.ua\",\n  \"uz.ua\",\n  \"uzhgorod.ua\",\n  \"vinnica.ua\",\n  \"vinnytsia.ua\",\n  \"vn.ua\",\n  \"volyn.ua\",\n  \"yalta.ua\",\n  \"zaporizhzhe.ua\",\n  \"zaporizhzhia.ua\",\n  \"zhitomir.ua\",\n  \"zhytomyr.ua\",\n  \"zp.ua\",\n  \"zt.ua\",\n  \"co.ug\",\n  \"or.ug\",\n  \"ac.ug\",\n  \"sc.ug\",\n  \"go.ug\",\n  \"ne.ug\",\n  \"com.ug\",\n  \"org.ug\",\n  \"ac.uk\",\n  \"co.uk\",\n  \"gov.uk\",\n  \"ltd.uk\",\n  \"me.uk\",\n  \"net.uk\",\n  \"nhs.uk\",\n  \"org.uk\",\n  \"plc.uk\",\n  \"police.uk\",\n  \"dni.us\",\n  \"fed.us\",\n  \"isa.us\",\n  \"kids.us\",\n  \"nsn.us\",\n  \"ak.us\",\n  \"al.us\",\n  \"ar.us\",\n  \"as.us\",\n  \"az.us\",\n  \"ca.us\",\n  \"co.us\",\n  \"ct.us\",\n  \"dc.us\",\n  \"de.us\",\n  \"fl.us\",\n  \"ga.us\",\n  \"gu.us\",\n  \"hi.us\",\n  \"ia.us\",\n  \"id.us\",\n  \"il.us\",\n  \"in.us\",\n  \"ks.us\",\n  \"ky.us\",\n  \"la.us\",\n  \"ma.us\",\n  \"md.us\",\n  \"me.us\",\n  \"mi.us\",\n  \"mn.us\",\n  \"mo.us\",\n  \"ms.us\",\n  \"mt.us\",\n  \"nc.us\",\n  \"nd.us\",\n  \"ne.us\",\n  \"nh.us\",\n  \"nj.us\",\n  \"nm.us\",\n  \"nv.us\",\n  \"ny.us\",\n  \"oh.us\",\n  \"ok.us\",\n  \"or.us\",\n  \"pa.us\",\n  \"pr.us\",\n  \"ri.us\",\n  \"sc.us\",\n  \"sd.us\",\n  \"tn.us\",\n  \"tx.us\",\n  \"ut.us\",\n  \"vi.us\",\n  \"vt.us\",\n  \"va.us\",\n  \"wa.us\",\n  \"wi.us\",\n  \"wv.us\",\n  \"wy.us\",\n  \"com.uy\",\n  \"edu.uy\",\n  \"gub.uy\",\n  \"mil.uy\",\n  \"net.uy\",\n  \"org.uy\",\n  \"co.uz\",\n  \"com.uz\",\n  \"net.uz\",\n  \"org.uz\",\n  \"com.vc\",\n  \"net.vc\",\n  \"org.vc\",\n  \"gov.vc\",\n  \"mil.vc\",\n  \"edu.vc\",\n  \"arts.ve\",\n  \"bib.ve\",\n  \"co.ve\",\n  \"com.ve\",\n  \"e12.ve\",\n  \"edu.ve\",\n  \"firm.ve\",\n  \"gob.ve\",\n  \"gov.ve\",\n  \"info.ve\",\n  \"int.ve\",\n  \"mil.ve\",\n  \"net.ve\",\n  \"nom.ve\",\n  \"org.ve\",\n  \"rar.ve\",\n  \"rec.ve\",\n  \"store.ve\",\n  \"tec.ve\",\n  \"web.ve\",\n  \"co.vi\",\n  \"com.vi\",\n  \"k12.vi\",\n  \"net.vi\",\n  \"org.vi\",\n  \"ac.vn\",\n  \"ai.vn\",\n  \"biz.vn\",\n  \"com.vn\",\n  \"edu.vn\",\n  \"gov.vn\",\n  \"health.vn\",\n  \"id.vn\",\n  \"info.vn\",\n  \"int.vn\",\n  \"io.vn\",\n  \"name.vn\",\n  \"net.vn\",\n  \"org.vn\",\n  \"pro.vn\",\n  \"angiang.vn\",\n  \"bacgiang.vn\",\n  \"backan.vn\",\n  \"baclieu.vn\",\n  \"bacninh.vn\",\n  \"baria-vungtau.vn\",\n  \"bentre.vn\",\n  \"binhdinh.vn\",\n  \"binhduong.vn\",\n  \"binhphuoc.vn\",\n  \"binhthuan.vn\",\n  \"camau.vn\",\n  \"cantho.vn\",\n  \"caobang.vn\",\n  \"daklak.vn\",\n  \"daknong.vn\",\n  \"danang.vn\",\n  \"dienbien.vn\",\n  \"dongnai.vn\",\n  \"dongthap.vn\",\n  \"gialai.vn\",\n  \"hagiang.vn\",\n  \"haiduong.vn\",\n  \"haiphong.vn\",\n  \"hanam.vn\",\n  \"hanoi.vn\",\n  \"hatinh.vn\",\n  \"haugiang.vn\",\n  \"hoabinh.vn\",\n  \"hungyen.vn\",\n  \"khanhhoa.vn\",\n  \"kiengiang.vn\",\n  \"kontum.vn\",\n  \"laichau.vn\",\n  \"lamdong.vn\",\n  \"langson.vn\",\n  \"laocai.vn\",\n  \"longan.vn\",\n  \"namdinh.vn\",\n  \"nghean.vn\",\n  \"ninhbinh.vn\",\n  \"ninhthuan.vn\",\n  \"phutho.vn\",\n  \"phuyen.vn\",\n  \"quangbinh.vn\",\n  \"quangnam.vn\",\n  \"quangngai.vn\",\n  \"quangninh.vn\",\n  \"quangtri.vn\",\n  \"soctrang.vn\",\n  \"sonla.vn\",\n  \"tayninh.vn\",\n  \"thaibinh.vn\",\n  \"thainguyen.vn\",\n  \"thanhhoa.vn\",\n  \"thanhphohochiminh.vn\",\n  \"thuathienhue.vn\",\n  \"tiengiang.vn\",\n  \"travinh.vn\",\n  \"tuyenquang.vn\",\n  \"vinhlong.vn\",\n  \"vinhphuc.vn\",\n  \"yenbai.vn\",\n  \"com.vu\",\n  \"edu.vu\",\n  \"net.vu\",\n  \"org.vu\",\n  \"com.ws\",\n  \"net.ws\",\n  \"org.ws\",\n  \"gov.ws\",\n  \"edu.ws\",\n  \"公司.香港\",\n  \"教育.香港\",\n  \"政府.香港\",\n  \"個人.香港\",\n  \"網絡.香港\",\n  \"組織.香港\",\n  \"пр.срб\",\n  \"орг.срб\",\n  \"обр.срб\",\n  \"од.срб\",\n  \"упр.срб\",\n  \"ак.срб\",\n  \"ศึกษา.ไทย\",\n  \"ธุรกิจ.ไทย\",\n  \"รัฐบาล.ไทย\",\n  \"ทหาร.ไทย\",\n  \"เน็ต.ไทย\",\n  \"องค์กร.ไทย\",\n  \"com.ye\",\n  \"edu.ye\",\n  \"gov.ye\",\n  \"net.ye\",\n  \"mil.ye\",\n  \"org.ye\",\n  \"ac.za\",\n  \"agric.za\",\n  \"alt.za\",\n  \"co.za\",\n  \"edu.za\",\n  \"gov.za\",\n  \"grondar.za\",\n  \"law.za\",\n  \"mil.za\",\n  \"net.za\",\n  \"ngo.za\",\n  \"nic.za\",\n  \"nis.za\",\n  \"nom.za\",\n  \"org.za\",\n  \"school.za\",\n  \"tm.za\",\n  \"web.za\",\n  \"ac.zm\",\n  \"biz.zm\",\n  \"co.zm\",\n  \"com.zm\",\n  \"edu.zm\",\n  \"gov.zm\",\n  \"info.zm\",\n  \"mil.zm\",\n  \"net.zm\",\n  \"org.zm\",\n  \"sch.zm\",\n  \"ac.zw\",\n  \"co.zw\",\n  \"gov.zw\",\n  \"mil.zw\",\n  \"org.zw\",\n  \"cc.ua\",\n  \"inf.ua\",\n  \"ltd.ua\",\n  \"611.to\",\n  \"graphox.us\",\n  \"activetrail.biz\",\n  \"adobeaemcloud.com\",\n  \"hlx.live\",\n  \"adobeaemcloud.net\",\n  \"hlx.page\",\n  \"hlx3.page\",\n  \"adobeio-static.net\",\n  \"adobeioruntime.net\",\n  \"beep.pl\",\n  \"airkitapps.com\",\n  \"airkitapps-au.com\",\n  \"airkitapps.eu\",\n  \"aivencloud.com\",\n  \"akadns.net\",\n  \"akamai.net\",\n  \"akamai-staging.net\",\n  \"akamaiedge.net\",\n  \"akamaiedge-staging.net\",\n  \"akamaihd.net\",\n  \"akamaihd-staging.net\",\n  \"akamaiorigin.net\",\n  \"akamaiorigin-staging.net\",\n  \"akamaized.net\",\n  \"akamaized-staging.net\",\n  \"edgekey.net\",\n  \"edgekey-staging.net\",\n  \"edgesuite.net\",\n  \"edgesuite-staging.net\",\n  \"barsy.ca\",\n  \"kasserver.com\",\n  \"altervista.org\",\n  \"alwaysdata.net\",\n  \"myamaze.net\",\n  \"cloudfront.net\",\n  \"elasticbeanstalk.com\",\n  \"awsglobalaccelerator.com\",\n  \"eero.online\",\n  \"eero-stage.online\",\n  \"t3l3p0rt.net\",\n  \"apigee.io\",\n  \"siiites.com\",\n  \"appspacehosted.com\",\n  \"appspaceusercontent.com\",\n  \"appudo.net\",\n  \"on-aptible.com\",\n  \"gv.vc\",\n  \"pimienta.org\",\n  \"poivron.org\",\n  \"potager.org\",\n  \"sweetpepper.org\",\n  \"myasustor.com\",\n  \"translated.page\",\n  \"autocode.dev\",\n  \"myfritz.net\",\n  \"onavstack.net\",\n  \"ecommerce-shop.pl\",\n  \"b-data.io\",\n  \"backplaneapp.io\",\n  \"balena-devices.com\",\n  \"rs.ba\",\n  \"base.ec\",\n  \"official.ec\",\n  \"buyshop.jp\",\n  \"fashionstore.jp\",\n  \"handcrafted.jp\",\n  \"kawaiishop.jp\",\n  \"supersale.jp\",\n  \"theshop.jp\",\n  \"shopselect.net\",\n  \"base.shop\",\n  \"beagleboard.io\",\n  \"betainabox.com\",\n  \"bnr.la\",\n  \"bitbucket.io\",\n  \"blackbaudcdn.net\",\n  \"of.je\",\n  \"bluebite.io\",\n  \"boomla.net\",\n  \"boutir.com\",\n  \"boxfuse.io\",\n  \"square7.ch\",\n  \"bplaced.com\",\n  \"bplaced.de\",\n  \"square7.de\",\n  \"bplaced.net\",\n  \"square7.net\",\n  \"browsersafetymark.io\",\n  \"cafjs.com\",\n  \"mycd.eu\",\n  \"canva-apps.cn\",\n  \"canva-apps.com\",\n  \"drr.ac\",\n  \"uwu.ai\",\n  \"carrd.co\",\n  \"crd.co\",\n  \"ju.mp\",\n  \"ae.org\",\n  \"br.com\",\n  \"cn.com\",\n  \"com.de\",\n  \"com.se\",\n  \"de.com\",\n  \"eu.com\",\n  \"gb.net\",\n  \"hu.net\",\n  \"jp.net\",\n  \"jpn.com\",\n  \"mex.com\",\n  \"ru.com\",\n  \"sa.com\",\n  \"se.net\",\n  \"uk.com\",\n  \"uk.net\",\n  \"us.com\",\n  \"za.bz\",\n  \"za.com\",\n  \"ar.com\",\n  \"hu.com\",\n  \"kr.com\",\n  \"no.com\",\n  \"qc.com\",\n  \"uy.com\",\n  \"africa.com\",\n  \"gr.com\",\n  \"in.net\",\n  \"web.in\",\n  \"us.org\",\n  \"co.com\",\n  \"aus.basketball\",\n  \"nz.basketball\",\n  \"radio.am\",\n  \"radio.fm\",\n  \"c.la\",\n  \"certmgr.org\",\n  \"cx.ua\",\n  \"discourse.group\",\n  \"discourse.team\",\n  \"cleverapps.io\",\n  \"clerk.app\",\n  \"clerkstage.app\",\n  \"clickrising.net\",\n  \"c66.me\",\n  \"cloud66.ws\",\n  \"cloud66.zone\",\n  \"jdevcloud.com\",\n  \"wpdevcloud.com\",\n  \"cloudaccess.host\",\n  \"freesite.host\",\n  \"cloudaccess.net\",\n  \"cloudcontrolled.com\",\n  \"cloudcontrolapp.com\",\n  \"cf-ipfs.com\",\n  \"cloudflare-ipfs.com\",\n  \"trycloudflare.com\",\n  \"pages.dev\",\n  \"r2.dev\",\n  \"workers.dev\",\n  \"wnext.app\",\n  \"co.ca\",\n  \"co.cz\",\n  \"cdn77-ssl.net\",\n  \"cloudns.asia\",\n  \"cloudns.biz\",\n  \"cloudns.club\",\n  \"cloudns.cc\",\n  \"cloudns.eu\",\n  \"cloudns.in\",\n  \"cloudns.info\",\n  \"cloudns.org\",\n  \"cloudns.pro\",\n  \"cloudns.pw\",\n  \"cloudns.us\",\n  \"cnpy.gdn\",\n  \"codeberg.page\",\n  \"co.nl\",\n  \"co.no\",\n  \"webhosting.be\",\n  \"hosting-cluster.nl\",\n  \"ac.ru\",\n  \"edu.ru\",\n  \"gov.ru\",\n  \"int.ru\",\n  \"mil.ru\",\n  \"test.ru\",\n  \"dynamisches-dns.de\",\n  \"dnsupdater.de\",\n  \"internet-dns.de\",\n  \"l-o-g-i-n.de\",\n  \"dynamic-dns.info\",\n  \"feste-ip.net\",\n  \"knx-server.net\",\n  \"static-access.net\",\n  \"realm.cz\",\n  \"cupcake.is\",\n  \"curv.dev\",\n  \"cyon.link\",\n  \"cyon.site\",\n  \"fnwk.site\",\n  \"folionetwork.site\",\n  \"platform0.app\",\n  \"daplie.me\",\n  \"dattolocal.com\",\n  \"dattorelay.com\",\n  \"dattoweb.com\",\n  \"mydatto.com\",\n  \"dattolocal.net\",\n  \"mydatto.net\",\n  \"biz.dk\",\n  \"co.dk\",\n  \"firm.dk\",\n  \"reg.dk\",\n  \"store.dk\",\n  \"builtwithdark.com\",\n  \"edgestack.me\",\n  \"ddns5.com\",\n  \"debian.net\",\n  \"deno.dev\",\n  \"deno-staging.dev\",\n  \"dedyn.io\",\n  \"deta.app\",\n  \"deta.dev\",\n  \"discordsays.com\",\n  \"discordsez.com\",\n  \"jozi.biz\",\n  \"dnshome.de\",\n  \"online.th\",\n  \"shop.th\",\n  \"drayddns.com\",\n  \"shoparena.pl\",\n  \"dreamhosters.com\",\n  \"mydrobo.com\",\n  \"drud.io\",\n  \"drud.us\",\n  \"duckdns.org\",\n  \"bip.sh\",\n  \"bitbridge.net\",\n  \"dy.fi\",\n  \"tunk.org\",\n  \"dyndns-at-home.com\",\n  \"dyndns-at-work.com\",\n  \"dyndns-blog.com\",\n  \"dyndns-free.com\",\n  \"dyndns-home.com\",\n  \"dyndns-ip.com\",\n  \"dyndns-mail.com\",\n  \"dyndns-office.com\",\n  \"dyndns-pics.com\",\n  \"dyndns-remote.com\",\n  \"dyndns-server.com\",\n  \"dyndns-web.com\",\n  \"dyndns-wiki.com\",\n  \"dyndns-work.com\",\n  \"dyndns.biz\",\n  \"dyndns.info\",\n  \"dyndns.org\",\n  \"dyndns.tv\",\n  \"at-band-camp.net\",\n  \"ath.cx\",\n  \"barrel-of-knowledge.info\",\n  \"barrell-of-knowledge.info\",\n  \"better-than.tv\",\n  \"blogdns.com\",\n  \"blogdns.net\",\n  \"blogdns.org\",\n  \"blogsite.org\",\n  \"boldlygoingnowhere.org\",\n  \"broke-it.net\",\n  \"buyshouses.net\",\n  \"cechire.com\",\n  \"dnsalias.com\",\n  \"dnsalias.net\",\n  \"dnsalias.org\",\n  \"dnsdojo.com\",\n  \"dnsdojo.net\",\n  \"dnsdojo.org\",\n  \"does-it.net\",\n  \"doesntexist.com\",\n  \"doesntexist.org\",\n  \"dontexist.com\",\n  \"dontexist.net\",\n  \"dontexist.org\",\n  \"doomdns.com\",\n  \"doomdns.org\",\n  \"dvrdns.org\",\n  \"dyn-o-saur.com\",\n  \"dynalias.com\",\n  \"dynalias.net\",\n  \"dynalias.org\",\n  \"dynathome.net\",\n  \"dyndns.ws\",\n  \"endofinternet.net\",\n  \"endofinternet.org\",\n  \"endoftheinternet.org\",\n  \"est-a-la-maison.com\",\n  \"est-a-la-masion.com\",\n  \"est-le-patron.com\",\n  \"est-mon-blogueur.com\",\n  \"for-better.biz\",\n  \"for-more.biz\",\n  \"for-our.info\",\n  \"for-some.biz\",\n  \"for-the.biz\",\n  \"from-ak.com\",\n  \"from-al.com\",\n  \"from-ar.com\",\n  \"from-az.net\",\n  \"from-ca.com\",\n  \"from-co.net\",\n  \"from-ct.com\",\n  \"from-dc.com\",\n  \"from-de.com\",\n  \"from-fl.com\",\n  \"from-ga.com\",\n  \"from-hi.com\",\n  \"from-ia.com\",\n  \"from-id.com\",\n  \"from-il.com\",\n  \"from-in.com\",\n  \"from-ks.com\",\n  \"from-ky.com\",\n  \"from-la.net\",\n  \"from-ma.com\",\n  \"from-md.com\",\n  \"from-me.org\",\n  \"from-mi.com\",\n  \"from-mn.com\",\n  \"from-mo.com\",\n  \"from-ms.com\",\n  \"from-mt.com\",\n  \"from-nc.com\",\n  \"from-nd.com\",\n  \"from-ne.com\",\n  \"from-nh.com\",\n  \"from-nj.com\",\n  \"from-nm.com\",\n  \"from-nv.com\",\n  \"from-ny.net\",\n  \"from-oh.com\",\n  \"from-ok.com\",\n  \"from-or.com\",\n  \"from-pa.com\",\n  \"from-pr.com\",\n  \"from-ri.com\",\n  \"from-sc.com\",\n  \"from-sd.com\",\n  \"from-tn.com\",\n  \"from-tx.com\",\n  \"from-ut.com\",\n  \"from-va.com\",\n  \"from-vt.com\",\n  \"from-wa.com\",\n  \"from-wi.com\",\n  \"from-wv.com\",\n  \"from-wy.com\",\n  \"ftpaccess.cc\",\n  \"fuettertdasnetz.de\",\n  \"game-host.org\",\n  \"game-server.cc\",\n  \"getmyip.com\",\n  \"gets-it.net\",\n  \"gotdns.com\",\n  \"gotdns.org\",\n  \"groks-the.info\",\n  \"groks-this.info\",\n  \"ham-radio-op.net\",\n  \"here-for-more.info\",\n  \"hobby-site.com\",\n  \"hobby-site.org\",\n  \"homedns.org\",\n  \"homeftp.net\",\n  \"homeftp.org\",\n  \"homeip.net\",\n  \"homelinux.com\",\n  \"homelinux.net\",\n  \"homelinux.org\",\n  \"homeunix.com\",\n  \"homeunix.net\",\n  \"homeunix.org\",\n  \"iamallama.com\",\n  \"in-the-band.net\",\n  \"is-a-anarchist.com\",\n  \"is-a-blogger.com\",\n  \"is-a-bookkeeper.com\",\n  \"is-a-bruinsfan.org\",\n  \"is-a-bulls-fan.com\",\n  \"is-a-candidate.org\",\n  \"is-a-caterer.com\",\n  \"is-a-celticsfan.org\",\n  \"is-a-chef.com\",\n  \"is-a-chef.net\",\n  \"is-a-chef.org\",\n  \"is-a-conservative.com\",\n  \"is-a-cpa.com\",\n  \"is-a-cubicle-slave.com\",\n  \"is-a-democrat.com\",\n  \"is-a-designer.com\",\n  \"is-a-doctor.com\",\n  \"is-a-financialadvisor.com\",\n  \"is-a-geek.com\",\n  \"is-a-geek.net\",\n  \"is-a-geek.org\",\n  \"is-a-green.com\",\n  \"is-a-guru.com\",\n  \"is-a-hard-worker.com\",\n  \"is-a-hunter.com\",\n  \"is-a-knight.org\",\n  \"is-a-landscaper.com\",\n  \"is-a-lawyer.com\",\n  \"is-a-liberal.com\",\n  \"is-a-libertarian.com\",\n  \"is-a-linux-user.org\",\n  \"is-a-llama.com\",\n  \"is-a-musician.com\",\n  \"is-a-nascarfan.com\",\n  \"is-a-nurse.com\",\n  \"is-a-painter.com\",\n  \"is-a-patsfan.org\",\n  \"is-a-personaltrainer.com\",\n  \"is-a-photographer.com\",\n  \"is-a-player.com\",\n  \"is-a-republican.com\",\n  \"is-a-rockstar.com\",\n  \"is-a-socialist.com\",\n  \"is-a-soxfan.org\",\n  \"is-a-student.com\",\n  \"is-a-teacher.com\",\n  \"is-a-techie.com\",\n  \"is-a-therapist.com\",\n  \"is-an-accountant.com\",\n  \"is-an-actor.com\",\n  \"is-an-actress.com\",\n  \"is-an-anarchist.com\",\n  \"is-an-artist.com\",\n  \"is-an-engineer.com\",\n  \"is-an-entertainer.com\",\n  \"is-by.us\",\n  \"is-certified.com\",\n  \"is-found.org\",\n  \"is-gone.com\",\n  \"is-into-anime.com\",\n  \"is-into-cars.com\",\n  \"is-into-cartoons.com\",\n  \"is-into-games.com\",\n  \"is-leet.com\",\n  \"is-lost.org\",\n  \"is-not-certified.com\",\n  \"is-saved.org\",\n  \"is-slick.com\",\n  \"is-uberleet.com\",\n  \"is-very-bad.org\",\n  \"is-very-evil.org\",\n  \"is-very-good.org\",\n  \"is-very-nice.org\",\n  \"is-very-sweet.org\",\n  \"is-with-theband.com\",\n  \"isa-geek.com\",\n  \"isa-geek.net\",\n  \"isa-geek.org\",\n  \"isa-hockeynut.com\",\n  \"issmarterthanyou.com\",\n  \"isteingeek.de\",\n  \"istmein.de\",\n  \"kicks-ass.net\",\n  \"kicks-ass.org\",\n  \"knowsitall.info\",\n  \"land-4-sale.us\",\n  \"lebtimnetz.de\",\n  \"leitungsen.de\",\n  \"likes-pie.com\",\n  \"likescandy.com\",\n  \"merseine.nu\",\n  \"mine.nu\",\n  \"misconfused.org\",\n  \"mypets.ws\",\n  \"myphotos.cc\",\n  \"neat-url.com\",\n  \"office-on-the.net\",\n  \"on-the-web.tv\",\n  \"podzone.net\",\n  \"podzone.org\",\n  \"readmyblog.org\",\n  \"saves-the-whales.com\",\n  \"scrapper-site.net\",\n  \"scrapping.cc\",\n  \"selfip.biz\",\n  \"selfip.com\",\n  \"selfip.info\",\n  \"selfip.net\",\n  \"selfip.org\",\n  \"sells-for-less.com\",\n  \"sells-for-u.com\",\n  \"sells-it.net\",\n  \"sellsyourhome.org\",\n  \"servebbs.com\",\n  \"servebbs.net\",\n  \"servebbs.org\",\n  \"serveftp.net\",\n  \"serveftp.org\",\n  \"servegame.org\",\n  \"shacknet.nu\",\n  \"simple-url.com\",\n  \"space-to-rent.com\",\n  \"stuff-4-sale.org\",\n  \"stuff-4-sale.us\",\n  \"teaches-yoga.com\",\n  \"thruhere.net\",\n  \"traeumtgerade.de\",\n  \"webhop.biz\",\n  \"webhop.info\",\n  \"webhop.net\",\n  \"webhop.org\",\n  \"worse-than.tv\",\n  \"writesthisblog.com\",\n  \"ddnss.de\",\n  \"dyndns1.de\",\n  \"dyn-ip24.de\",\n  \"home-webserver.de\",\n  \"myhome-server.de\",\n  \"ddnss.org\",\n  \"definima.net\",\n  \"definima.io\",\n  \"ondigitalocean.app\",\n  \"ddnsfree.com\",\n  \"ddnsgeek.com\",\n  \"giize.com\",\n  \"gleeze.com\",\n  \"kozow.com\",\n  \"loseyourip.com\",\n  \"ooguy.com\",\n  \"theworkpc.com\",\n  \"casacam.net\",\n  \"dynu.net\",\n  \"accesscam.org\",\n  \"camdvr.org\",\n  \"freeddns.org\",\n  \"mywire.org\",\n  \"webredirect.org\",\n  \"myddns.rocks\",\n  \"blogsite.xyz\",\n  \"dynv6.net\",\n  \"e4.cz\",\n  \"easypanel.app\",\n  \"easypanel.host\",\n  \"elementor.cloud\",\n  \"elementor.cool\",\n  \"en-root.fr\",\n  \"mytuleap.com\",\n  \"tuleap-partners.com\",\n  \"encr.app\",\n  \"encoreapi.com\",\n  \"onred.one\",\n  \"eu.org\",\n  \"eurodir.ru\",\n  \"twmail.cc\",\n  \"twmail.net\",\n  \"twmail.org\",\n  \"url.tw\",\n  \"onfabrica.com\",\n  \"ru.net\",\n  \"adygeya.ru\",\n  \"bashkiria.ru\",\n  \"bir.ru\",\n  \"cbg.ru\",\n  \"com.ru\",\n  \"dagestan.ru\",\n  \"grozny.ru\",\n  \"kalmykia.ru\",\n  \"kustanai.ru\",\n  \"marine.ru\",\n  \"mordovia.ru\",\n  \"msk.ru\",\n  \"mytis.ru\",\n  \"nalchik.ru\",\n  \"nov.ru\",\n  \"pyatigorsk.ru\",\n  \"spb.ru\",\n  \"vladikavkaz.ru\",\n  \"vladimir.ru\",\n  \"abkhazia.su\",\n  \"adygeya.su\",\n  \"aktyubinsk.su\",\n  \"arkhangelsk.su\",\n  \"armenia.su\",\n  \"ashgabad.su\",\n  \"azerbaijan.su\",\n  \"balashov.su\",\n  \"bashkiria.su\",\n  \"bryansk.su\",\n  \"bukhara.su\",\n  \"chimkent.su\",\n  \"dagestan.su\",\n  \"east-kazakhstan.su\",\n  \"exnet.su\",\n  \"georgia.su\",\n  \"grozny.su\",\n  \"ivanovo.su\",\n  \"jambyl.su\",\n  \"kalmykia.su\",\n  \"kaluga.su\",\n  \"karacol.su\",\n  \"karaganda.su\",\n  \"karelia.su\",\n  \"khakassia.su\",\n  \"krasnodar.su\",\n  \"kurgan.su\",\n  \"kustanai.su\",\n  \"lenug.su\",\n  \"mangyshlak.su\",\n  \"mordovia.su\",\n  \"msk.su\",\n  \"murmansk.su\",\n  \"nalchik.su\",\n  \"navoi.su\",\n  \"north-kazakhstan.su\",\n  \"nov.su\",\n  \"obninsk.su\",\n  \"penza.su\",\n  \"pokrovsk.su\",\n  \"sochi.su\",\n  \"spb.su\",\n  \"tashkent.su\",\n  \"termez.su\",\n  \"togliatti.su\",\n  \"troitsk.su\",\n  \"tselinograd.su\",\n  \"tula.su\",\n  \"tuva.su\",\n  \"vladikavkaz.su\",\n  \"vladimir.su\",\n  \"vologda.su\",\n  \"channelsdvr.net\",\n  \"edgecompute.app\",\n  \"fastly-edge.com\",\n  \"fastly-terrarium.com\",\n  \"fastlylb.net\",\n  \"fastvps-server.com\",\n  \"fastvps.host\",\n  \"myfast.host\",\n  \"fastvps.site\",\n  \"myfast.space\",\n  \"fedorainfracloud.org\",\n  \"fedorapeople.org\",\n  \"conn.uk\",\n  \"copro.uk\",\n  \"hosp.uk\",\n  \"mydobiss.com\",\n  \"fh-muenster.io\",\n  \"filegear.me\",\n  \"filegear-au.me\",\n  \"filegear-de.me\",\n  \"filegear-gb.me\",\n  \"filegear-ie.me\",\n  \"filegear-jp.me\",\n  \"filegear-sg.me\",\n  \"firebaseapp.com\",\n  \"fireweb.app\",\n  \"flap.id\",\n  \"onflashdrive.app\",\n  \"fldrv.com\",\n  \"fly.dev\",\n  \"edgeapp.net\",\n  \"shw.io\",\n  \"flynnhosting.net\",\n  \"forgeblocks.com\",\n  \"framer.app\",\n  \"framercanvas.com\",\n  \"framer.media\",\n  \"framer.photos\",\n  \"framer.website\",\n  \"framer.wiki\",\n  \"0e.vc\",\n  \"freebox-os.com\",\n  \"freeboxos.com\",\n  \"fbx-os.fr\",\n  \"fbxos.fr\",\n  \"freebox-os.fr\",\n  \"freeboxos.fr\",\n  \"freedesktop.org\",\n  \"freemyip.com\",\n  \"futurehosting.at\",\n  \"futuremailing.at\",\n  \"independent-commission.uk\",\n  \"independent-inquest.uk\",\n  \"independent-inquiry.uk\",\n  \"independent-panel.uk\",\n  \"independent-review.uk\",\n  \"public-inquiry.uk\",\n  \"royal-commission.uk\",\n  \"usercontent.jp\",\n  \"gentapps.com\",\n  \"gentlentapis.com\",\n  \"lab.ms\",\n  \"cdn-edges.net\",\n  \"ghost.io\",\n  \"gsj.bz\",\n  \"githubusercontent.com\",\n  \"githubpreview.dev\",\n  \"github.io\",\n  \"gitlab.io\",\n  \"gitapp.si\",\n  \"gitpage.si\",\n  \"glitch.me\",\n  \"nog.community\",\n  \"co.ro\",\n  \"shop.ro\",\n  \"lolipop.io\",\n  \"angry.jp\",\n  \"babyblue.jp\",\n  \"babymilk.jp\",\n  \"backdrop.jp\",\n  \"bambina.jp\",\n  \"bitter.jp\",\n  \"blush.jp\",\n  \"boo.jp\",\n  \"boy.jp\",\n  \"boyfriend.jp\",\n  \"but.jp\",\n  \"candypop.jp\",\n  \"capoo.jp\",\n  \"catfood.jp\",\n  \"cheap.jp\",\n  \"chicappa.jp\",\n  \"chillout.jp\",\n  \"chips.jp\",\n  \"chowder.jp\",\n  \"chu.jp\",\n  \"ciao.jp\",\n  \"cocotte.jp\",\n  \"coolblog.jp\",\n  \"cranky.jp\",\n  \"cutegirl.jp\",\n  \"daa.jp\",\n  \"deca.jp\",\n  \"deci.jp\",\n  \"digick.jp\",\n  \"egoism.jp\",\n  \"fakefur.jp\",\n  \"fem.jp\",\n  \"flier.jp\",\n  \"floppy.jp\",\n  \"fool.jp\",\n  \"frenchkiss.jp\",\n  \"girlfriend.jp\",\n  \"girly.jp\",\n  \"gloomy.jp\",\n  \"gonna.jp\",\n  \"greater.jp\",\n  \"hacca.jp\",\n  \"heavy.jp\",\n  \"her.jp\",\n  \"hiho.jp\",\n  \"hippy.jp\",\n  \"holy.jp\",\n  \"hungry.jp\",\n  \"icurus.jp\",\n  \"itigo.jp\",\n  \"jellybean.jp\",\n  \"kikirara.jp\",\n  \"kill.jp\",\n  \"kilo.jp\",\n  \"kuron.jp\",\n  \"littlestar.jp\",\n  \"lolipopmc.jp\",\n  \"lolitapunk.jp\",\n  \"lomo.jp\",\n  \"lovepop.jp\",\n  \"lovesick.jp\",\n  \"main.jp\",\n  \"mods.jp\",\n  \"mond.jp\",\n  \"mongolian.jp\",\n  \"moo.jp\",\n  \"namaste.jp\",\n  \"nikita.jp\",\n  \"nobushi.jp\",\n  \"noor.jp\",\n  \"oops.jp\",\n  \"parallel.jp\",\n  \"parasite.jp\",\n  \"pecori.jp\",\n  \"peewee.jp\",\n  \"penne.jp\",\n  \"pepper.jp\",\n  \"perma.jp\",\n  \"pigboat.jp\",\n  \"pinoko.jp\",\n  \"punyu.jp\",\n  \"pupu.jp\",\n  \"pussycat.jp\",\n  \"pya.jp\",\n  \"raindrop.jp\",\n  \"readymade.jp\",\n  \"sadist.jp\",\n  \"schoolbus.jp\",\n  \"secret.jp\",\n  \"staba.jp\",\n  \"stripper.jp\",\n  \"sub.jp\",\n  \"sunnyday.jp\",\n  \"thick.jp\",\n  \"tonkotsu.jp\",\n  \"under.jp\",\n  \"upper.jp\",\n  \"velvet.jp\",\n  \"verse.jp\",\n  \"versus.jp\",\n  \"vivian.jp\",\n  \"watson.jp\",\n  \"weblike.jp\",\n  \"whitesnow.jp\",\n  \"zombie.jp\",\n  \"heteml.net\",\n  \"cloudapps.digital\",\n  \"pymnt.uk\",\n  \"ro.im\",\n  \"goip.de\",\n  \"run.app\",\n  \"web.app\",\n  \"appspot.com\",\n  \"codespot.com\",\n  \"googleapis.com\",\n  \"googlecode.com\",\n  \"pagespeedmobilizer.com\",\n  \"publishproxy.com\",\n  \"withgoogle.com\",\n  \"withyoutube.com\",\n  \"cloud.goog\",\n  \"translate.goog\",\n  \"cloudfunctions.net\",\n  \"blogspot.ae\",\n  \"blogspot.al\",\n  \"blogspot.am\",\n  \"blogspot.ba\",\n  \"blogspot.be\",\n  \"blogspot.bg\",\n  \"blogspot.bj\",\n  \"blogspot.ca\",\n  \"blogspot.cf\",\n  \"blogspot.ch\",\n  \"blogspot.cl\",\n  \"blogspot.com\",\n  \"blogspot.cv\",\n  \"blogspot.cz\",\n  \"blogspot.de\",\n  \"blogspot.dk\",\n  \"blogspot.fi\",\n  \"blogspot.fr\",\n  \"blogspot.gr\",\n  \"blogspot.hk\",\n  \"blogspot.hr\",\n  \"blogspot.hu\",\n  \"blogspot.ie\",\n  \"blogspot.in\",\n  \"blogspot.is\",\n  \"blogspot.it\",\n  \"blogspot.jp\",\n  \"blogspot.kr\",\n  \"blogspot.li\",\n  \"blogspot.lt\",\n  \"blogspot.lu\",\n  \"blogspot.md\",\n  \"blogspot.mk\",\n  \"blogspot.mr\",\n  \"blogspot.mx\",\n  \"blogspot.my\",\n  \"blogspot.nl\",\n  \"blogspot.no\",\n  \"blogspot.pe\",\n  \"blogspot.pt\",\n  \"blogspot.qa\",\n  \"blogspot.re\",\n  \"blogspot.ro\",\n  \"blogspot.rs\",\n  \"blogspot.ru\",\n  \"blogspot.se\",\n  \"blogspot.sg\",\n  \"blogspot.si\",\n  \"blogspot.sk\",\n  \"blogspot.sn\",\n  \"blogspot.td\",\n  \"blogspot.tw\",\n  \"blogspot.ug\",\n  \"blogspot.vn\",\n  \"goupile.fr\",\n  \"gov.nl\",\n  \"awsmppl.com\",\n  \"günstigbestellen.de\",\n  \"günstigliefern.de\",\n  \"fin.ci\",\n  \"free.hr\",\n  \"caa.li\",\n  \"ua.rs\",\n  \"conf.se\",\n  \"hs.zone\",\n  \"hs.run\",\n  \"hashbang.sh\",\n  \"hasura.app\",\n  \"hasura-app.io\",\n  \"hepforge.org\",\n  \"herokuapp.com\",\n  \"herokussl.com\",\n  \"ravendb.cloud\",\n  \"ravendb.community\",\n  \"ravendb.me\",\n  \"development.run\",\n  \"ravendb.run\",\n  \"homesklep.pl\",\n  \"secaas.hk\",\n  \"hoplix.shop\",\n  \"orx.biz\",\n  \"biz.gl\",\n  \"col.ng\",\n  \"firm.ng\",\n  \"gen.ng\",\n  \"ltd.ng\",\n  \"ngo.ng\",\n  \"edu.scot\",\n  \"sch.so\",\n  \"ie.ua\",\n  \"hostyhosting.io\",\n  \"häkkinen.fi\",\n  \"moonscale.net\",\n  \"iki.fi\",\n  \"ibxos.it\",\n  \"iliadboxos.it\",\n  \"impertrixcdn.com\",\n  \"impertrix.com\",\n  \"smushcdn.com\",\n  \"wphostedmail.com\",\n  \"wpmucdn.com\",\n  \"tempurl.host\",\n  \"wpmudev.host\",\n  \"dyn-berlin.de\",\n  \"in-berlin.de\",\n  \"in-brb.de\",\n  \"in-butter.de\",\n  \"in-dsl.de\",\n  \"in-dsl.net\",\n  \"in-dsl.org\",\n  \"in-vpn.de\",\n  \"in-vpn.net\",\n  \"in-vpn.org\",\n  \"biz.at\",\n  \"info.at\",\n  \"info.cx\",\n  \"pixolino.com\",\n  \"na4u.ru\",\n  \"iopsys.se\",\n  \"ipifony.net\",\n  \"iservschule.de\",\n  \"mein-iserv.de\",\n  \"schulplattform.de\",\n  \"schulserver.de\",\n  \"test-iserv.de\",\n  \"iserv.dev\",\n  \"iobb.net\",\n  \"mycloud.by\",\n  \"diadem.cloud\",\n  \"jele.cloud\",\n  \"keliweb.cloud\",\n  \"oxa.cloud\",\n  \"primetel.cloud\",\n  \"jele.club\",\n  \"amscompute.com\",\n  \"dopaas.com\",\n  \"kilatiron.com\",\n  \"jele.host\",\n  \"mircloud.host\",\n  \"jele.io\",\n  \"jcloud.kz\",\n  \"cloudjiffy.net\",\n  \"faststacks.net\",\n  \"sdscloud.pl\",\n  \"unicloud.pl\",\n  \"mircloud.ru\",\n  \"enscaled.sg\",\n  \"jele.site\",\n  \"jelastic.team\",\n  \"orangecloud.tn\",\n  \"mircloud.us\",\n  \"myjino.ru\",\n  \"jotelulu.cloud\",\n  \"js.org\",\n  \"kaas.gg\",\n  \"khplay.nl\",\n  \"ktistory.com\",\n  \"kapsi.fi\",\n  \"keymachine.de\",\n  \"kinghost.net\",\n  \"uni5.net\",\n  \"knightpoint.systems\",\n  \"koobin.events\",\n  \"oya.to\",\n  \"kuleuven.cloud\",\n  \"co.krd\",\n  \"edu.krd\",\n  \"krellian.net\",\n  \"webthings.io\",\n  \"git-repos.de\",\n  \"lcube-server.de\",\n  \"svn-repos.de\",\n  \"leadpages.co\",\n  \"lpages.co\",\n  \"lpusercontent.com\",\n  \"lelux.site\",\n  \"co.business\",\n  \"co.education\",\n  \"co.events\",\n  \"co.financial\",\n  \"co.network\",\n  \"co.place\",\n  \"co.technology\",\n  \"linkyard.cloud\",\n  \"linkyard-cloud.ch\",\n  \"we.bs\",\n  \"localzone.xyz\",\n  \"loginline.app\",\n  \"loginline.dev\",\n  \"loginline.io\",\n  \"loginline.services\",\n  \"loginline.site\",\n  \"servers.run\",\n  \"lohmus.me\",\n  \"krasnik.pl\",\n  \"leczna.pl\",\n  \"lubartow.pl\",\n  \"lublin.pl\",\n  \"poniatowa.pl\",\n  \"swidnik.pl\",\n  \"barsy.bg\",\n  \"barsycenter.com\",\n  \"barsyonline.com\",\n  \"barsy.club\",\n  \"barsy.de\",\n  \"barsy.eu\",\n  \"barsy.in\",\n  \"barsy.info\",\n  \"barsy.io\",\n  \"barsy.me\",\n  \"barsy.menu\",\n  \"barsy.mobi\",\n  \"barsy.net\",\n  \"barsy.online\",\n  \"barsy.org\",\n  \"barsy.pro\",\n  \"barsy.pub\",\n  \"barsy.ro\",\n  \"barsy.shop\",\n  \"barsy.site\",\n  \"barsy.support\",\n  \"barsy.uk\",\n  \"mayfirst.info\",\n  \"mayfirst.org\",\n  \"cn.vu\",\n  \"mazeplay.com\",\n  \"mcpe.me\",\n  \"mcdir.me\",\n  \"mcdir.ru\",\n  \"mcpre.ru\",\n  \"mediatech.by\",\n  \"mediatech.dev\",\n  \"hra.health\",\n  \"miniserver.com\",\n  \"memset.net\",\n  \"messerli.app\",\n  \"meteorapp.com\",\n  \"co.pl\",\n  \"azurewebsites.net\",\n  \"azure-mobile.net\",\n  \"cloudapp.net\",\n  \"azurestaticapps.net\",\n  \"csx.cc\",\n  \"mintere.site\",\n  \"forte.id\",\n  \"mozilla-iot.org\",\n  \"bmoattachments.org\",\n  \"net.ru\",\n  \"org.ru\",\n  \"pp.ru\",\n  \"hostedpi.com\",\n  \"netlify.app\",\n  \"4u.com\",\n  \"ngrok.app\",\n  \"ngrok-free.app\",\n  \"ngrok.dev\",\n  \"ngrok-free.dev\",\n  \"ngrok.io\",\n  \"ngrok.pizza\",\n  \"nfshost.com\",\n  \"noop.app\",\n  \"noticeable.news\",\n  \"dnsking.ch\",\n  \"mypi.co\",\n  \"n4t.co\",\n  \"001www.com\",\n  \"ddnslive.com\",\n  \"myiphost.com\",\n  \"forumz.info\",\n  \"16-b.it\",\n  \"32-b.it\",\n  \"64-b.it\",\n  \"soundcast.me\",\n  \"tcp4.me\",\n  \"dnsup.net\",\n  \"hicam.net\",\n  \"now-dns.net\",\n  \"ownip.net\",\n  \"vpndns.net\",\n  \"dynserv.org\",\n  \"now-dns.org\",\n  \"x443.pw\",\n  \"now-dns.top\",\n  \"ntdll.top\",\n  \"freeddns.us\",\n  \"crafting.xyz\",\n  \"zapto.xyz\",\n  \"nsupdate.info\",\n  \"nerdpol.ovh\",\n  \"blogsyte.com\",\n  \"brasilia.me\",\n  \"cable-modem.org\",\n  \"ciscofreak.com\",\n  \"collegefan.org\",\n  \"couchpotatofries.org\",\n  \"damnserver.com\",\n  \"ddns.me\",\n  \"ditchyourip.com\",\n  \"dnsfor.me\",\n  \"dnsiskinky.com\",\n  \"dvrcam.info\",\n  \"dynns.com\",\n  \"eating-organic.net\",\n  \"fantasyleague.cc\",\n  \"geekgalaxy.com\",\n  \"golffan.us\",\n  \"health-carereform.com\",\n  \"homesecuritymac.com\",\n  \"homesecuritypc.com\",\n  \"hopto.me\",\n  \"ilovecollege.info\",\n  \"loginto.me\",\n  \"mlbfan.org\",\n  \"mmafan.biz\",\n  \"myactivedirectory.com\",\n  \"mydissent.net\",\n  \"myeffect.net\",\n  \"mymediapc.net\",\n  \"mypsx.net\",\n  \"mysecuritycamera.com\",\n  \"mysecuritycamera.net\",\n  \"mysecuritycamera.org\",\n  \"net-freaks.com\",\n  \"nflfan.org\",\n  \"nhlfan.net\",\n  \"no-ip.ca\",\n  \"no-ip.net\",\n  \"noip.us\",\n  \"onthewifi.com\",\n  \"pgafan.net\",\n  \"point2this.com\",\n  \"pointto.us\",\n  \"privatizehealthinsurance.net\",\n  \"quicksytes.com\",\n  \"read-books.org\",\n  \"securitytactics.com\",\n  \"serveexchange.com\",\n  \"servehumour.com\",\n  \"servep2p.com\",\n  \"servesarcasm.com\",\n  \"stufftoread.com\",\n  \"ufcfan.org\",\n  \"unusualperson.com\",\n  \"workisboring.com\",\n  \"3utilities.com\",\n  \"bounceme.net\",\n  \"ddns.net\",\n  \"ddnsking.com\",\n  \"gotdns.ch\",\n  \"hopto.org\",\n  \"myftp.biz\",\n  \"myftp.org\",\n  \"myvnc.com\",\n  \"no-ip.biz\",\n  \"no-ip.info\",\n  \"no-ip.org\",\n  \"noip.me\",\n  \"redirectme.net\",\n  \"servebeer.com\",\n  \"serveblog.net\",\n  \"servecounterstrike.com\",\n  \"serveftp.com\",\n  \"servegame.com\",\n  \"servehalflife.com\",\n  \"servehttp.com\",\n  \"serveirc.com\",\n  \"serveminecraft.net\",\n  \"servemp3.com\",\n  \"servepics.com\",\n  \"servequake.com\",\n  \"sytes.net\",\n  \"webhop.me\",\n  \"zapto.org\",\n  \"pcloud.host\",\n  \"nyc.mn\",\n  \"cya.gg\",\n  \"omg.lol\",\n  \"cloudycluster.net\",\n  \"omniwe.site\",\n  \"123hjemmeside.dk\",\n  \"123hjemmeside.no\",\n  \"123homepage.it\",\n  \"123kotisivu.fi\",\n  \"123minsida.se\",\n  \"123miweb.es\",\n  \"123paginaweb.pt\",\n  \"123sait.ru\",\n  \"123siteweb.fr\",\n  \"123webseite.at\",\n  \"123webseite.de\",\n  \"123website.be\",\n  \"123website.ch\",\n  \"123website.lu\",\n  \"123website.nl\",\n  \"service.one\",\n  \"simplesite.com\",\n  \"simplesite.gr\",\n  \"simplesite.pl\",\n  \"nid.io\",\n  \"opensocial.site\",\n  \"opencraft.hosting\",\n  \"orsites.com\",\n  \"operaunite.com\",\n  \"tech.orange\",\n  \"authgear-staging.com\",\n  \"authgearapps.com\",\n  \"skygearapp.com\",\n  \"outsystemscloud.com\",\n  \"ownprovider.com\",\n  \"own.pm\",\n  \"ox.rs\",\n  \"oy.lc\",\n  \"pgfog.com\",\n  \"pagefrontapp.com\",\n  \"pagexl.com\",\n  \"bar0.net\",\n  \"bar1.net\",\n  \"bar2.net\",\n  \"rdv.to\",\n  \"art.pl\",\n  \"gliwice.pl\",\n  \"krakow.pl\",\n  \"poznan.pl\",\n  \"wroc.pl\",\n  \"zakopane.pl\",\n  \"pantheonsite.io\",\n  \"gotpantheon.com\",\n  \"mypep.link\",\n  \"perspecta.cloud\",\n  \"lk3.ru\",\n  \"on-web.fr\",\n  \"platter-app.com\",\n  \"platter-app.dev\",\n  \"platterp.us\",\n  \"pdns.page\",\n  \"plesk.page\",\n  \"pleskns.com\",\n  \"dyn53.io\",\n  \"onporter.run\",\n  \"co.bn\",\n  \"postman-echo.com\",\n  \"pstmn.io\",\n  \"httpbin.org\",\n  \"prequalifyme.today\",\n  \"priv.at\",\n  \"prvcy.page\",\n  \"protonet.io\",\n  \"chirurgiens-dentistes-en-france.fr\",\n  \"byen.site\",\n  \"pubtls.org\",\n  \"pythonanywhere.com\",\n  \"qoto.io\",\n  \"qualifioapp.com\",\n  \"ladesk.com\",\n  \"qbuser.com\",\n  \"cloudsite.builders\",\n  \"instantcloud.cn\",\n  \"ras.ru\",\n  \"qa2.com\",\n  \"qcx.io\",\n  \"dev-myqnapcloud.com\",\n  \"alpha-myqnapcloud.com\",\n  \"myqnapcloud.com\",\n  \"vapor.cloud\",\n  \"vaporcloud.io\",\n  \"rackmaze.com\",\n  \"rackmaze.net\",\n  \"readthedocs.io\",\n  \"rhcloud.com\",\n  \"onrender.com\",\n  \"firewalledreplit.co\",\n  \"repl.co\",\n  \"repl.run\",\n  \"resindevice.io\",\n  \"hzc.io\",\n  \"wellbeingzone.eu\",\n  \"itcouldbewor.se\",\n  \"rocky.page\",\n  \"биз.рус\",\n  \"ком.рус\",\n  \"крым.рус\",\n  \"мир.рус\",\n  \"мск.рус\",\n  \"орг.рус\",\n  \"самара.рус\",\n  \"сочи.рус\",\n  \"спб.рус\",\n  \"я.рус\",\n  \"180r.com\",\n  \"dojin.com\",\n  \"sakuratan.com\",\n  \"sakuraweb.com\",\n  \"x0.com\",\n  \"2-d.jp\",\n  \"bona.jp\",\n  \"crap.jp\",\n  \"daynight.jp\",\n  \"eek.jp\",\n  \"flop.jp\",\n  \"halfmoon.jp\",\n  \"jeez.jp\",\n  \"matrix.jp\",\n  \"mimoza.jp\",\n  \"netgamers.jp\",\n  \"nyanta.jp\",\n  \"o0o0.jp\",\n  \"rdy.jp\",\n  \"rgr.jp\",\n  \"rulez.jp\",\n  \"saloon.jp\",\n  \"sblo.jp\",\n  \"skr.jp\",\n  \"tank.jp\",\n  \"uh-oh.jp\",\n  \"undo.jp\",\n  \"websozai.jp\",\n  \"xii.jp\",\n  \"squares.net\",\n  \"jpn.org\",\n  \"kirara.st\",\n  \"x0.to\",\n  \"from.tv\",\n  \"sakura.tv\",\n  \"sandcats.io\",\n  \"logoip.de\",\n  \"logoip.com\",\n  \"dedibox.fr\",\n  \"schokokeks.net\",\n  \"gov.scot\",\n  \"scrysec.com\",\n  \"firewall-gateway.com\",\n  \"firewall-gateway.de\",\n  \"my-gateway.de\",\n  \"my-router.de\",\n  \"spdns.de\",\n  \"spdns.eu\",\n  \"firewall-gateway.net\",\n  \"my-firewall.org\",\n  \"myfirewall.org\",\n  \"spdns.org\",\n  \"seidat.net\",\n  \"sellfy.store\",\n  \"senseering.net\",\n  \"minisite.ms\",\n  \"magnet.page\",\n  \"biz.ua\",\n  \"co.ua\",\n  \"pp.ua\",\n  \"shiftcrypto.dev\",\n  \"shiftcrypto.io\",\n  \"shiftedit.io\",\n  \"myshopblocks.com\",\n  \"myshopify.com\",\n  \"shopitsite.com\",\n  \"shopware.store\",\n  \"mo-siemens.io\",\n  \"1kapp.com\",\n  \"appchizi.com\",\n  \"applinzi.com\",\n  \"sinaapp.com\",\n  \"vipsinaapp.com\",\n  \"siteleaf.net\",\n  \"bounty-full.com\",\n  \"small-web.org\",\n  \"vp4.me\",\n  \"snowflake.app\",\n  \"streamlit.app\",\n  \"streamlitapp.com\",\n  \"try-snowplow.com\",\n  \"srht.site\",\n  \"stackhero-network.com\",\n  \"musician.io\",\n  \"novecore.site\",\n  \"static.land\",\n  \"storebase.store\",\n  \"vps-host.net\",\n  \"playstation-cloud.com\",\n  \"spacekit.io\",\n  \"myspreadshop.at\",\n  \"myspreadshop.be\",\n  \"myspreadshop.ca\",\n  \"myspreadshop.ch\",\n  \"myspreadshop.com\",\n  \"myspreadshop.de\",\n  \"myspreadshop.dk\",\n  \"myspreadshop.es\",\n  \"myspreadshop.fi\",\n  \"myspreadshop.fr\",\n  \"myspreadshop.ie\",\n  \"myspreadshop.it\",\n  \"myspreadshop.net\",\n  \"myspreadshop.nl\",\n  \"myspreadshop.no\",\n  \"myspreadshop.pl\",\n  \"myspreadshop.se\",\n  \"storipress.app\",\n  \"storj.farm\",\n  \"utwente.io\",\n  \"temp-dns.com\",\n  \"supabase.co\",\n  \"supabase.in\",\n  \"supabase.net\",\n  \"syncloud.it\",\n  \"dscloud.biz\",\n  \"dsmynas.com\",\n  \"familyds.com\",\n  \"diskstation.me\",\n  \"dscloud.me\",\n  \"i234.me\",\n  \"myds.me\",\n  \"synology.me\",\n  \"dscloud.mobi\",\n  \"dsmynas.net\",\n  \"familyds.net\",\n  \"dsmynas.org\",\n  \"familyds.org\",\n  \"vpnplus.to\",\n  \"mytabit.com\",\n  \"taifun-dns.de\",\n  \"ts.net\",\n  \"gda.pl\",\n  \"gdansk.pl\",\n  \"gdynia.pl\",\n  \"med.pl\",\n  \"sopot.pl\",\n  \"edugit.io\",\n  \"telebit.app\",\n  \"telebit.io\",\n  \"reservd.com\",\n  \"thingdustdata.com\",\n  \"tickets.io\",\n  \"arvo.network\",\n  \"azimuth.network\",\n  \"tlon.network\",\n  \"torproject.net\",\n  \"bloxcms.com\",\n  \"townnews-staging.com\",\n  \"12hp.at\",\n  \"2ix.at\",\n  \"4lima.at\",\n  \"lima-city.at\",\n  \"12hp.ch\",\n  \"2ix.ch\",\n  \"4lima.ch\",\n  \"lima-city.ch\",\n  \"trafficplex.cloud\",\n  \"de.cool\",\n  \"12hp.de\",\n  \"2ix.de\",\n  \"4lima.de\",\n  \"lima-city.de\",\n  \"1337.pictures\",\n  \"clan.rip\",\n  \"lima-city.rocks\",\n  \"webspace.rocks\",\n  \"lima.zone\",\n  \"tuxfamily.org\",\n  \"dd-dns.de\",\n  \"diskstation.eu\",\n  \"diskstation.org\",\n  \"dray-dns.de\",\n  \"draydns.de\",\n  \"dyn-vpn.de\",\n  \"dynvpn.de\",\n  \"mein-vigor.de\",\n  \"my-vigor.de\",\n  \"my-wan.de\",\n  \"syno-ds.de\",\n  \"synology-diskstation.de\",\n  \"synology-ds.de\",\n  \"typedream.app\",\n  \"uber.space\",\n  \"hk.com\",\n  \"hk.org\",\n  \"ltd.hk\",\n  \"inc.hk\",\n  \"it.com\",\n  \"name.pm\",\n  \"sch.tf\",\n  \"biz.wf\",\n  \"sch.wf\",\n  \"org.yt\",\n  \"virtualuser.de\",\n  \"virtual-user.de\",\n  \"upli.io\",\n  \"urown.cloud\",\n  \"dnsupdate.info\",\n  \"2038.io\",\n  \"vercel.app\",\n  \"vercel.dev\",\n  \"now.sh\",\n  \"router.management\",\n  \"v-info.info\",\n  \"voorloper.cloud\",\n  \"neko.am\",\n  \"nyaa.am\",\n  \"be.ax\",\n  \"cat.ax\",\n  \"es.ax\",\n  \"eu.ax\",\n  \"gg.ax\",\n  \"mc.ax\",\n  \"us.ax\",\n  \"xy.ax\",\n  \"nl.ci\",\n  \"xx.gl\",\n  \"app.gp\",\n  \"blog.gt\",\n  \"de.gt\",\n  \"to.gt\",\n  \"be.gy\",\n  \"cc.hn\",\n  \"blog.kg\",\n  \"io.kg\",\n  \"jp.kg\",\n  \"tv.kg\",\n  \"uk.kg\",\n  \"us.kg\",\n  \"de.ls\",\n  \"at.md\",\n  \"de.md\",\n  \"jp.md\",\n  \"to.md\",\n  \"indie.porn\",\n  \"vxl.sh\",\n  \"ch.tc\",\n  \"me.tc\",\n  \"we.tc\",\n  \"nyan.to\",\n  \"at.vg\",\n  \"blog.vu\",\n  \"dev.vu\",\n  \"me.vu\",\n  \"v.ua\",\n  \"wafflecell.com\",\n  \"reserve-online.net\",\n  \"reserve-online.com\",\n  \"bookonline.app\",\n  \"hotelwithflight.com\",\n  \"wedeploy.io\",\n  \"wedeploy.me\",\n  \"wedeploy.sh\",\n  \"remotewd.com\",\n  \"wmflabs.org\",\n  \"toolforge.org\",\n  \"wmcloud.org\",\n  \"panel.gg\",\n  \"messwithdns.com\",\n  \"woltlab-demo.com\",\n  \"myforum.community\",\n  \"community-pro.de\",\n  \"diskussionsbereich.de\",\n  \"community-pro.net\",\n  \"meinforum.net\",\n  \"wpenginepowered.com\",\n  \"wixsite.com\",\n  \"editorx.io\",\n  \"half.host\",\n  \"xnbay.com\",\n  \"cistron.nl\",\n  \"demon.nl\",\n  \"xs4all.space\",\n  \"yandexcloud.net\",\n  \"official.academy\",\n  \"yolasite.com\",\n  \"ybo.faith\",\n  \"yombo.me\",\n  \"homelink.one\",\n  \"ybo.party\",\n  \"ybo.review\",\n  \"ybo.science\",\n  \"ybo.trade\",\n  \"ynh.fr\",\n  \"nohost.me\",\n  \"noho.st\",\n  \"za.net\",\n  \"za.org\",\n  \"bss.design\",\n  \"basicserver.io\",\n  \"virtualserver.io\",\n  \"enterprisecloud.nu\",\n]);\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/collapsible-domain-section.stories.tsx",
    "content": "import {\n  Flex,\n  Grid,\n  Text,\n  Button,\n  InputField,\n  Label,\n  Separator,\n  SmallIconButton,\n  IconButton,\n  Tooltip,\n  ScrollArea,\n  PanelBanner,\n  Link,\n  Select,\n  TextArea,\n  Checkbox,\n  theme,\n  rawTheme,\n  buttonStyle,\n  Popover,\n  PopoverTitle,\n  PopoverTitleActions,\n  StorySection,\n} from \"@webstudio-is/design-system\";\nimport {\n  CheckCircleIcon,\n  AlertIcon,\n  CopyIcon,\n  GearIcon,\n  UpgradeIcon,\n  InfoCircleIcon,\n  HelpIcon,\n} from \"@webstudio-is/icons\";\nimport { CollapsibleDomainSection } from \"./collapsible-domain-section\";\n\nexport default {\n  title: \"Builder/Publish\",\n};\n\nconst StatusIcon = ({ status }: { status: \"ok\" | \"error\" | \"pending\" }) => (\n  <Flex\n    align=\"center\"\n    justify=\"center\"\n    css={{\n      width: theme.sizes.controlHeight,\n      height: theme.sizes.controlHeight,\n      color:\n        status === \"error\"\n          ? theme.colors.foregroundDestructive\n          : status === \"pending\"\n            ? theme.colors.foregroundSubtle\n            : theme.colors.foregroundSuccessText,\n    }}\n  >\n    {status === \"error\" ? <AlertIcon /> : <CheckCircleIcon />}\n  </Flex>\n);\n\nconst DomainSuffix = ({ status }: { status: \"ok\" | \"error\" | \"pending\" }) => (\n  <Grid flow=\"column\" align=\"center\">\n    <Tooltip content={<Text>Status</Text>}>\n      <StatusIcon status={status} />\n    </Tooltip>\n    <IconButton type=\"button\" tabIndex={-1}>\n      <CopyIcon />\n    </IconButton>\n  </Grid>\n);\n\nconst MockCheckbox = ({ checked = false }: { checked?: boolean }) => (\n  <Checkbox defaultChecked={checked} />\n);\n\n// Story: Staging domain section (the wstd.work domain)\nexport const StagingDomain = () => (\n  <StorySection title=\"Staging Domain\">\n    <Flex direction=\"column\" gap=\"3\" css={{ width: theme.spacing[33] }}>\n      <Text variant=\"labels\">Published</Text>\n      <CollapsibleDomainSection\n        title=\"my-project.wstd.work\"\n        prefix={<MockCheckbox checked />}\n        suffix={<DomainSuffix status=\"ok\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n              <Label>Domain:</Label>\n              <Tooltip\n                content=\"Domain can't be renamed once published\"\n                variant=\"wrapped\"\n              >\n                <InfoCircleIcon\n                  tabIndex={0}\n                  style={{ flexShrink: 0 }}\n                  color={rawTheme.colors.foregroundSubtle}\n                />\n              </Tooltip>\n            </Flex>\n            <InputField text=\"mono\" value=\"my-project\" disabled />\n          </Grid>\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Label css={{ width: theme.spacing[20] }}>Username:</Label>\n            <InputField text=\"mono\" value=\"staging\" readOnly />\n          </Grid>\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n              <Label>Password:</Label>\n              <Tooltip\n                content=\"Read-only password for staging\"\n                variant=\"wrapped\"\n              >\n                <InfoCircleIcon\n                  tabIndex={0}\n                  style={{ flexShrink: 0 }}\n                  color={rawTheme.colors.foregroundSubtle}\n                />\n              </Tooltip>\n            </Flex>\n            <InputField text=\"mono\" value=\"abc123\" readOnly />\n          </Grid>\n          <Tooltip content=\"Unpublish to enable domain renaming\">\n            <Button color=\"destructive\" css={{ width: \"100%\" }}>\n              Unpublish\n            </Button>\n          </Tooltip>\n        </Grid>\n      </CollapsibleDomainSection>\n\n      <Text variant=\"labels\">Not yet published</Text>\n      <CollapsibleDomainSection\n        title=\"my-project.wstd.work\"\n        prefix={<MockCheckbox />}\n        suffix={<DomainSuffix status=\"pending\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n              <Label>Domain:</Label>\n            </Flex>\n            <InputField text=\"mono\" value=\"my-project\" />\n          </Grid>\n        </Grid>\n      </CollapsibleDomainSection>\n\n      <Text variant=\"labels\">Domain rename error</Text>\n      <CollapsibleDomainSection\n        title=\"my-project.wstd.work\"\n        prefix={<MockCheckbox />}\n        suffix={<DomainSuffix status=\"error\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n              <Label>Domain:</Label>\n            </Flex>\n            <InputField text=\"mono\" value=\"bad domain!\" color=\"error\" />\n            <Text color=\"destructive\">\n              Domain must only contain letters, numbers, and hyphens\n            </Text>\n          </Grid>\n        </Grid>\n      </CollapsibleDomainSection>\n    </Flex>\n  </StorySection>\n);\n\n// Story: Custom domain states\nexport const CustomDomains = () => (\n  <StorySection title=\"Custom Domains\">\n    <Flex direction=\"column\" gap=\"3\" css={{ width: theme.spacing[33] }}>\n      <Text variant=\"labels\">Verified and active</Text>\n      <CollapsibleDomainSection\n        title=\"example.com\"\n        prefix={<MockCheckbox checked />}\n        suffix={<DomainSuffix status=\"ok\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Flex align=\"center\" gap={1}>\n            <CheckCircleIcon color={rawTheme.colors.foregroundSuccessText} />\n            <Text>Published 2 hours ago</Text>\n          </Flex>\n          <Grid gap={1}>\n            <Text variant=\"labels\">DNS records</Text>\n            <Grid\n              css={{\n                gridTemplateColumns: \"auto 1fr 1fr\",\n                gap: theme.spacing[2],\n              }}\n            >\n              <Text variant=\"mono\" color=\"subtle\">\n                CNAME\n              </Text>\n              <Text variant=\"mono\">www</Text>\n              <Text variant=\"mono\">cname.wstd.work</Text>\n              <Text variant=\"mono\" color=\"subtle\">\n                TXT\n              </Text>\n              <Text variant=\"mono\">@</Text>\n              <Text variant=\"mono\">webstudio-verify=abc123</Text>\n            </Grid>\n          </Grid>\n          <Grid flow=\"column\" gap={2}>\n            <Button color=\"neutral\">Verify DNS</Button>\n            <Button color=\"destructive\">Remove</Button>\n          </Grid>\n        </Grid>\n      </CollapsibleDomainSection>\n\n      <Text variant=\"labels\">Unverified (DNS not configured)</Text>\n      <CollapsibleDomainSection\n        title=\"new-domain.com\"\n        prefix={<MockCheckbox />}\n        suffix={<DomainSuffix status=\"error\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Flex align=\"center\" gap={1}>\n            <AlertIcon color={rawTheme.colors.foregroundDestructive} />\n            <Text color=\"destructive\">\n              DNS records not found. Add these records at your registrar:\n            </Text>\n          </Flex>\n          <Grid\n            css={{ gridTemplateColumns: \"auto 1fr 1fr\", gap: theme.spacing[2] }}\n          >\n            <Text variant=\"mono\" color=\"subtle\">\n              CNAME\n            </Text>\n            <Text variant=\"mono\">www</Text>\n            <Text variant=\"mono\">cname.wstd.work</Text>\n            <Text variant=\"mono\" color=\"subtle\">\n              TXT\n            </Text>\n            <Text variant=\"mono\">@</Text>\n            <Text variant=\"mono\">webstudio-verify=xyz789</Text>\n          </Grid>\n          <Grid flow=\"column\" gap={2}>\n            <Button color=\"neutral\">Check status</Button>\n            <Button color=\"destructive\">Remove</Button>\n          </Grid>\n        </Grid>\n      </CollapsibleDomainSection>\n\n      <Text variant=\"labels\">Pending (initializing)</Text>\n      <CollapsibleDomainSection\n        title=\"staging.example.com\"\n        prefix={<MockCheckbox />}\n        suffix={<DomainSuffix status=\"pending\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Flex align=\"center\" gap={1}>\n            <InfoCircleIcon color={rawTheme.colors.foregroundSubtle} />\n            <Text color=\"subtle\">Setting up SSL certificate...</Text>\n          </Flex>\n          <Button color=\"neutral\">Check status</Button>\n        </Grid>\n      </CollapsibleDomainSection>\n\n      <Text variant=\"labels\">Error state</Text>\n      <CollapsibleDomainSection\n        title=\"broken.example.com\"\n        prefix={<MockCheckbox />}\n        suffix={<DomainSuffix status=\"error\" />}\n        initiallyOpen\n      >\n        <Grid gap={2}>\n          <Flex align=\"center\" gap={1}>\n            <AlertIcon color={rawTheme.colors.foregroundDestructive} />\n            <Text color=\"destructive\">\n              SSL provisioning failed. Please verify your DNS records and try\n              again.\n            </Text>\n          </Flex>\n          <Grid flow=\"column\" gap={2}>\n            <Button color=\"neutral\">Verify DNS</Button>\n            <Button color=\"destructive\">Remove</Button>\n          </Grid>\n        </Grid>\n      </CollapsibleDomainSection>\n    </Flex>\n  </StorySection>\n);\n\n// Story: Publish button states\nexport const PublishButton = () => (\n  <StorySection title=\"Publish Button\">\n    <Flex direction=\"column\" gap=\"3\" css={{ width: theme.spacing[33] }}>\n      <Text variant=\"labels\">Ready to publish</Text>\n      <Button color=\"positive\" css={{ width: \"100%\" }}>\n        Publish\n      </Button>\n\n      <Text variant=\"labels\">Publishing with countdown</Text>\n      <Button color=\"positive\" css={{ width: \"100%\" }}>\n        Publishing (45s)\n      </Button>\n\n      <Text variant=\"labels\">Publishing (pending)</Text>\n      <Button color=\"positive\" state=\"pending\" css={{ width: \"100%\" }}>\n        Publish\n      </Button>\n\n      <Text variant=\"labels\">No domains selected</Text>\n      <Tooltip content=\"Select at least one domain to publish\">\n        <Button color=\"positive\" disabled css={{ width: \"100%\" }}>\n          Publish\n        </Button>\n      </Tooltip>\n\n      <Text variant=\"labels\">Disabled (restricted features)</Text>\n      <Button color=\"positive\" disabled css={{ width: \"100%\" }}>\n        Publish\n      </Button>\n\n      <Text variant=\"labels\">Publish error</Text>\n      <Flex gap={2} shrink={false} direction=\"column\">\n        <Text color=\"destructive\">\n          Build timed out after 3 minutes. Please try again.\n        </Text>\n        <Button color=\"positive\" css={{ width: \"100%\" }}>\n          Publish\n        </Button>\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\n// Story: Upgrade banners\nexport const UpgradeBanners = () => (\n  <StorySection title=\"Upgrade Banners\">\n    <Flex direction=\"column\" gap=\"5\" css={{ width: theme.spacing[33] }}>\n      <Text variant=\"labels\">Publish limit exceeded</Text>\n      <PanelBanner>\n        <Text variant=\"regularBold\">\n          Upgrade to publish more than 1 time per day:\n        </Text>\n        <Link\n          className={buttonStyle({ color: \"gradient\" })}\n          color=\"contrast\"\n          underline=\"none\"\n          href=\"#\"\n        >\n          Upgrade\n        </Link>\n      </PanelBanner>\n\n      <Text variant=\"labels\">Restricted pro features</Text>\n      <PanelBanner>\n        <Text variant=\"regularBold\">Following Pro features are used:</Text>\n        <Text as=\"ul\" color=\"destructive\" css={{ paddingLeft: \"1em\" }}>\n          <li>\n            <Flex align=\"center\" gap=\"1\">\n              <button\n                style={{\n                  all: \"unset\",\n                  cursor: \"pointer\",\n                  textDecoration: \"underline\",\n                }}\n                type=\"button\"\n              >\n                Dynamic path\n              </button>\n            </Flex>\n          </li>\n          <li>\n            <Flex align=\"center\" gap=\"1\">\n              Resource variable\n              <Tooltip\n                variant=\"wrapped\"\n                content=\"Used in Blog Post page for fetching CMS data\"\n              >\n                <SmallIconButton icon={<HelpIcon />} />\n              </Tooltip>\n            </Flex>\n          </li>\n          <li>Redirect</li>\n          <li>Custom contact email</li>\n        </Text>\n        <Text>You can delete these features or upgrade.</Text>\n        <Flex align=\"center\" gap={1}>\n          <UpgradeIcon />\n          <Link color=\"inherit\" href=\"#\">\n            Upgrade to Pro\n          </Link>\n        </Flex>\n      </PanelBanner>\n\n      <Text variant=\"labels\">Domain limit reached</Text>\n      <PanelBanner>\n        <Text variant=\"regular\">\n          <Text variant=\"regularBold\" inline>\n            Upgrade to a Pro account\n          </Text>{\" \"}\n          to add unlimited domains and publish to each domain individually.\n        </Text>\n        <Flex align=\"center\" gap={1}>\n          <UpgradeIcon />\n          <Link color=\"inherit\" href=\"#\">\n            Upgrade to Pro\n          </Link>\n        </Flex>\n      </PanelBanner>\n\n      <Text variant=\"labels\">Unpublished domain reminder</Text>\n      <PanelBanner>\n        <Flex align=\"center\" gap=\"1\">\n          <InfoCircleIcon color={rawTheme.colors.foregroundMain} />\n          <Text variant=\"regularBold\">Don&apos;t forget to publish</Text>\n        </Flex>\n        <Text>\n          You have a custom domain that hasn&apos;t been published yet. Hit\n          publish to make it live.\n        </Text>\n      </PanelBanner>\n    </Flex>\n  </StorySection>\n);\n\n// Story: Full publish dialog layout\nexport const PublishDialogLayout = () => (\n  <StorySection title=\"Publish Dialog Layout\">\n    <Flex\n      direction=\"column\"\n      css={{\n        width: theme.spacing[33],\n        border: `1px solid ${rawTheme.colors.borderMain}`,\n        borderRadius: theme.borderRadius[4],\n      }}\n    >\n      <PopoverTitle\n        suffix={\n          <PopoverTitleActions>\n            <IconButton type=\"button\">\n              <GearIcon />\n            </IconButton>\n          </PopoverTitleActions>\n        }\n      >\n        Publish\n      </PopoverTitle>\n      <ScrollArea css={{ maxHeight: 400 }}>\n        <CollapsibleDomainSection\n          title=\"my-project.wstd.work\"\n          prefix={<MockCheckbox checked />}\n          suffix={<DomainSuffix status=\"ok\" />}\n          initiallyOpen\n        >\n          <Grid gap={2}>\n            <Grid flow=\"column\" align=\"center\" gap={2}>\n              <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n                <Label>Domain:</Label>\n              </Flex>\n              <InputField text=\"mono\" value=\"my-project\" disabled />\n            </Grid>\n            <Tooltip content=\"Unpublish to enable domain renaming\">\n              <Button color=\"destructive\" css={{ width: \"100%\" }}>\n                Unpublish\n              </Button>\n            </Tooltip>\n          </Grid>\n        </CollapsibleDomainSection>\n\n        <CollapsibleDomainSection\n          title=\"example.com\"\n          prefix={<MockCheckbox checked />}\n          suffix={<DomainSuffix status=\"ok\" />}\n        >\n          <Text>Verified and active</Text>\n        </CollapsibleDomainSection>\n\n        <CollapsibleDomainSection\n          title=\"blog.example.com\"\n          prefix={<MockCheckbox />}\n          suffix={<DomainSuffix status=\"error\" />}\n        >\n          <Text color=\"destructive\">DNS not configured</Text>\n        </CollapsibleDomainSection>\n      </ScrollArea>\n\n      <Flex direction=\"column\" justify=\"end\" css={{ height: 0 }}>\n        <Separator />\n      </Flex>\n\n      <Flex direction=\"column\" gap=\"2\" css={{ padding: theme.panel.padding }}>\n        <Button color=\"positive\">Publish</Button>\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\n// Story: Export dialog layout\nexport const ExportDialogLayout = () => (\n  <StorySection title=\"Export Dialog Layout\">\n    <Popover>\n      <Flex\n        direction=\"column\"\n        css={{\n          width: theme.spacing[33],\n          border: `1px solid ${rawTheme.colors.borderMain}`,\n          borderRadius: theme.borderRadius[4],\n        }}\n      >\n        <PopoverTitle>Export</PopoverTitle>\n        <Grid columns={1} gap={3} css={{ padding: theme.panel.padding }}>\n          <Grid columns={2} gap={2} align=\"center\">\n            <Text color=\"main\" variant=\"labels\">\n              Destination\n            </Text>\n            <Select\n              fullWidth\n              value=\"docker\"\n              options={[\"docker\", \"static\", \"vercel\", \"netlify\"]}\n              getLabel={(v) => v.charAt(0).toUpperCase() + v.slice(1)}\n              onChange={() => {}}\n            />\n          </Grid>\n\n          <Grid columns={1} gap={2}>\n            <Grid\n              gap={2}\n              align=\"center\"\n              css={{ gridTemplateColumns: \"1fr auto 1fr\" }}\n            >\n              <Separator css={{ alignSelf: \"unset\" }} />\n              <Text color=\"main\">CLI</Text>\n              <Separator css={{ alignSelf: \"unset\" }} />\n            </Grid>\n\n            <Grid columns={1} gap={1}>\n              <Text color=\"main\" variant=\"labels\">\n                Step 1\n              </Text>\n              <Text color=\"subtle\">\n                Download and install Node v20+ from nodejs.org\n              </Text>\n            </Grid>\n\n            <Grid columns={1} gap={1}>\n              <Text color=\"main\" variant=\"labels\">\n                Step 2\n              </Text>\n              <Text color=\"subtle\">\n                Run this command to install Webstudio CLI and sync your project.\n              </Text>\n            </Grid>\n            <Flex gap={2}>\n              <InputField\n                css={{ flex: 1 }}\n                text=\"mono\"\n                readOnly\n                value=\"npx webstudio@latest\"\n              />\n              <Button type=\"button\" color=\"neutral\" prefix={<CopyIcon />}>\n                Copy\n              </Button>\n            </Flex>\n\n            <Grid columns={1} gap={1}>\n              <Text color=\"main\" variant=\"labels\">\n                Step 3\n              </Text>\n              <Text color=\"subtle\">Run this command to publish to Docker</Text>\n            </Grid>\n            <Flex gap={2} align=\"end\">\n              <TextArea\n                css={{ flex: 1 }}\n                variant=\"mono\"\n                readOnly\n                value={\"docker build -t my-image .\\ndocker run my-image\"}\n              />\n              <Button\n                type=\"button\"\n                css={{ flexShrink: 0 }}\n                color=\"neutral\"\n                prefix={<CopyIcon />}\n              >\n                Copy\n              </Button>\n            </Flex>\n          </Grid>\n        </Grid>\n      </Flex>\n    </Popover>\n  </StorySection>\n);\n\n// Story: Static export variant\nexport const ExportStaticLayout = () => (\n  <StorySection title=\"Export Static Layout\">\n    <Popover>\n      <Flex\n        direction=\"column\"\n        css={{\n          width: theme.spacing[33],\n          border: `1px solid ${rawTheme.colors.borderMain}`,\n          borderRadius: theme.borderRadius[4],\n        }}\n      >\n        <PopoverTitle>Export</PopoverTitle>\n        <Grid columns={1} gap={3} css={{ padding: theme.panel.padding }}>\n          <Grid columns={2} gap={2} align=\"center\">\n            <Text color=\"main\" variant=\"labels\">\n              Destination\n            </Text>\n            <Select\n              fullWidth\n              value=\"static\"\n              options={[\"docker\", \"static\", \"vercel\", \"netlify\"]}\n              getLabel={(v) => v.charAt(0).toUpperCase() + v.slice(1)}\n              onChange={() => {}}\n            />\n          </Grid>\n\n          <Button color=\"positive\">Build and download static site</Button>\n          <Text color=\"subtle\">\n            Learn about deploying static sites{\" \"}\n            <Link variant=\"inherit\" color=\"inherit\" href=\"#\">\n              here\n            </Link>\n          </Text>\n        </Grid>\n      </Flex>\n    </Popover>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/collapsible-domain-section.tsx",
    "content": "import {\n  Box,\n  Flex,\n  Label,\n  theme,\n  SectionTitle,\n  Grid,\n} from \"@webstudio-is/design-system\";\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { CollapsibleSectionRoot } from \"~/builder/shared/collapsible-section\";\n\nexport const CollapsibleDomainSection = ({\n  initiallyOpen = false,\n  children,\n  title,\n  suffix,\n  prefix,\n}: {\n  initiallyOpen?: boolean;\n  children: ReactNode;\n  prefix: ReactNode;\n  suffix: ReactNode;\n  title: string;\n}) => {\n  const [open, setOpen] = useState(initiallyOpen);\n  const titleRef = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    if (open) {\n      titleRef.current?.scrollIntoView({\n        behavior: \"smooth\",\n      });\n    }\n  }, [open]);\n\n  return (\n    <CollapsibleSectionRoot\n      label=\"\"\n      fullWidth\n      isOpen={open}\n      onOpenChange={setOpen}\n      trigger={\n        <SectionTitle\n          ref={titleRef}\n          suffix={\n            <Box\n              css={{ display: \"contents\" }}\n              onClick={(event) => {\n                if (event.defaultPrevented) {\n                  return;\n                }\n                setOpen(!open);\n              }}\n            >\n              {suffix}\n            </Box>\n          }\n        >\n          <Grid gap=\"1\" flow=\"column\" align=\"center\" justify=\"start\">\n            {prefix}\n            <Label truncate>{title}</Label>\n          </Grid>\n        </SectionTitle>\n      }\n    >\n      <Flex\n        css={{\n          paddingInline: theme.panel.paddingInline,\n          overflowWrap: \"anywhere\",\n        }}\n        gap={2}\n        direction={\"column\"}\n      >\n        {children}\n      </Flex>\n    </CollapsibleSectionRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/domain-checkbox.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  Flex,\n  Tooltip,\n  Text,\n  theme,\n  Link,\n  buttonStyle,\n  Checkbox,\n} from \"@webstudio-is/design-system\";\n\nimport { $project } from \"~/shared/sync/data-stores\";\nimport { $userPlanFeatures } from \"~/shared/nano-states\";\n\nexport const domainToPublishName = \"domainToPublish[]\";\n\ninterface DomainCheckboxProps {\n  defaultChecked?: boolean;\n  domain: string;\n  buildId: string | undefined;\n  disabled?: boolean;\n}\n\nexport const DomainCheckbox = (props: DomainCheckboxProps) => {\n  const { allowStagingPublish } = useStore($userPlanFeatures);\n  const project = useStore($project);\n\n  if (project === undefined) {\n    return;\n  }\n\n  const tooltipContentForFreeUsers = allowStagingPublish ? undefined : (\n    <Flex direction=\"column\" gap=\"2\" css={{ maxWidth: theme.spacing[28] }}>\n      <Text variant=\"titles\">Publish to staging</Text>\n      <Text>\n        <Flex direction=\"column\">\n          Staging allows you to preview a production version of your site\n          without potentially breaking what production site visitors will see.\n          <>\n            <br />\n            <br />\n            Upgrade to Pro account to publish to each domain individually.\n            <br /> <br />\n            <Link\n              className={buttonStyle({ color: \"gradient\" })}\n              color=\"contrast\"\n              underline=\"none\"\n              href=\"https://webstudio.is/pricing\"\n              target=\"_blank\"\n            >\n              Upgrade\n            </Link>\n          </>\n        </Flex>\n      </Text>\n    </Flex>\n  );\n\n  const defaultChecked = allowStagingPublish ? props.defaultChecked : true;\n  const disabled = allowStagingPublish ? props.disabled : true;\n\n  const hideDomainCheckbox =\n    project.domainsVirtual.filter(\n      (domain) => domain.status === \"ACTIVE\" && domain.verified\n    ).length === 0 && allowStagingPublish;\n\n  return (\n    <div style={{ display: hideDomainCheckbox ? \"none\" : \"contents\" }}>\n      <Tooltip content={tooltipContentForFreeUsers} variant=\"wrapped\">\n        <Checkbox\n          disabled={disabled}\n          key={props.buildId ?? \"-\"}\n          defaultChecked={hideDomainCheckbox || defaultChecked}\n          css={{ pointerEvents: \"all\" }}\n          name={domainToPublishName}\n          value={props.domain}\n        />\n      </Tooltip>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/domains.tsx",
    "content": "import {\n  Button,\n  theme,\n  Text,\n  Tooltip,\n  IconButton,\n  Grid,\n  InputField,\n  styled,\n  Flex,\n  Link,\n  SmallIconButton,\n  NestedInputButton,\n  Separator,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport type { Project } from \"@webstudio-is/project\";\nimport {\n  AlertIcon,\n  CheckCircleIcon,\n  CopyIcon,\n  HelpIcon,\n} from \"@webstudio-is/icons\";\nimport { CollapsibleDomainSection } from \"./collapsible-domain-section\";\nimport {\n  Fragment,\n  startTransition,\n  useEffect,\n  useOptimistic,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { Entri } from \"./entri\";\nimport { nativeClient } from \"~/shared/trpc/trpc-client\";\nimport { useStore } from \"@nanostores/react\";\nimport { $publisherHost } from \"~/shared/sync/data-stores\";\nimport { extractCname } from \"./cname\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport { DomainCheckbox } from \"./domain-checkbox\";\nimport { CopyToClipboard } from \"~/shared/copy-to-clipboard\";\nimport { RelativeTime } from \"~/builder/shared/relative-time\";\n\nexport type Domain = Project[\"domainsVirtual\"][number];\n\ntype DomainStatus = Domain[\"status\"];\n\nconst InputEllipsis = styled(InputField, {\n  \"&>input\": {\n    textOverflow: \"ellipsis\",\n  },\n});\n\nexport const getStatus = (projectDomain: Domain) =>\n  projectDomain.verified\n    ? (`VERIFIED_${projectDomain.status}` as const)\n    : `UNVERIFIED`;\n\nexport const PENDING_TIMEOUT =\n  process.env.NODE_ENV === \"production\" ? 60 * 3 * 1000 : 35000;\n\nexport const getPublishStatusAndText = ({\n  createdAt,\n  publishStatus,\n}: Pick<\n  NonNullable<Domain[\"latestBuildVirtual\"]>,\n  \"createdAt\" | \"publishStatus\"\n>) => {\n  let status = publishStatus;\n\n  const delta = Date.now() - new Date(createdAt).getTime();\n  // Assume build failed after 3 minutes\n\n  if (publishStatus === \"PENDING\" && delta > PENDING_TIMEOUT) {\n    status = \"FAILED\";\n  }\n\n  const textStart =\n    status === \"PUBLISHED\"\n      ? \"Published\"\n      : status === \"FAILED\"\n        ? \"Publish failed\"\n        : \"Publishing started\";\n\n  const statusText = (\n    <>\n      {textStart} <RelativeTime time={new Date(createdAt)} />\n    </>\n  );\n\n  return { statusText, status };\n};\n\nconst getStatusText = (props: {\n  projectDomain: Domain;\n  isLoading: boolean;\n}) => {\n  const status = getStatus(props.projectDomain);\n\n  let isVerifiedActive = false;\n  let text: ReactNode = \"Something went wrong\";\n\n  switch (status) {\n    case \"UNVERIFIED\":\n      text = \"Status: Not verified\";\n      break;\n\n    case \"VERIFIED_INITIALIZING\":\n      text = \"Status: Initializing CNAME\";\n      break;\n    case \"VERIFIED_PENDING\":\n      text = \"Status: Waiting for CNAME propagation\";\n      break;\n    case \"VERIFIED_ACTIVE\":\n      isVerifiedActive = true;\n      text = \"Status: Active, not published\";\n\n      if (props.projectDomain.latestBuildVirtual !== null) {\n        const publishText = getPublishStatusAndText(\n          props.projectDomain.latestBuildVirtual\n        );\n\n        text = publishText.statusText;\n        isVerifiedActive = publishText.status !== \"FAILED\";\n      }\n      break;\n    case \"VERIFIED_ERROR\":\n      text = props.projectDomain.error ?? text;\n      break;\n\n    default:\n      ((_value: never) => {\n        /* exhaustive check */\n      })(status);\n      break;\n  }\n\n  return {\n    isVerifiedActive,\n    text: props.isLoading ? \"Loading status...\" : text,\n  };\n};\n\nconst StatusIcon = (props: { projectDomain: Domain; isLoading: boolean }) => {\n  const { isVerifiedActive, text } = getStatusText(props);\n\n  const Icon = isVerifiedActive ? CheckCircleIcon : AlertIcon;\n\n  return (\n    <Tooltip content={text}>\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{\n          cursor: \"pointer\",\n          width: theme.sizes.controlHeight,\n          height: theme.sizes.controlHeight,\n          color: props.isLoading\n            ? theme.colors.foregroundDisabled\n            : isVerifiedActive\n              ? theme.colors.foregroundSuccessText\n              : theme.colors.foregroundDestructive,\n        }}\n      >\n        <Icon />\n      </Flex>\n    </Tooltip>\n  );\n};\n\nconst DomainItem = ({\n  initiallyOpen,\n  projectDomain,\n  project,\n  refresh,\n}: {\n  initiallyOpen: boolean;\n  projectDomain: Domain;\n  project: Project;\n  refresh: () => Promise<void>;\n}) => {\n  const timeSinceLastUpdateMs =\n    Date.now() - new Date(projectDomain.updatedAt).getTime();\n\n  const DAY_IN_MS = 24 * 60 * 60 * 1000;\n\n  const status = projectDomain.verified\n    ? (`VERIFIED_${projectDomain.status}` as `VERIFIED_${DomainStatus}`)\n    : `UNVERIFIED`;\n\n  const [isStatusLoading, setIsStatusLoading] = useState(\n    initiallyOpen ||\n      status === \"VERIFIED_ACTIVE\" ||\n      timeSinceLastUpdateMs > DAY_IN_MS\n      ? false\n      : true\n  );\n\n  const [isCheckStateInProgress, setIsCheckStateInProgress] =\n    useOptimistic(false);\n\n  const [isRemoveInProgress, setIsRemoveInProgress] = useOptimistic(false);\n\n  const [isUnpublishInProgress, setIsUnpublishInProgress] =\n    useOptimistic(false);\n\n  const handleUnpublish = async () => {\n    setIsUnpublishInProgress(true);\n    const result = await nativeClient.domain.unpublish.mutate({\n      projectId: projectDomain.projectId,\n      domain: projectDomain.domain,\n    });\n\n    if (result.success === false) {\n      toast.error(result.message);\n      return;\n    }\n\n    await refresh();\n    toast.success(result.message);\n  };\n\n  const handleRemoveDomain = async () => {\n    setIsRemoveInProgress(true);\n    const result = await nativeClient.domain.remove.mutate({\n      projectId: projectDomain.projectId,\n      domainId: projectDomain.domainId,\n    });\n\n    if (result.success === false) {\n      toast.error(result.error);\n      return;\n    }\n\n    await refresh();\n  };\n\n  const [verifyError, setVerifyError] = useState<string | undefined>(undefined);\n\n  const handleVerify = useEffectEvent(async () => {\n    setVerifyError(undefined);\n    setIsCheckStateInProgress(true);\n\n    const verifyResult = await nativeClient.domain.verify.mutate({\n      projectId: projectDomain.projectId,\n      domainId: projectDomain.domainId,\n    });\n\n    if (verifyResult.success === false) {\n      setVerifyError(verifyResult.error);\n      return;\n    }\n\n    await refresh();\n  });\n\n  const [updateStatusError, setUpdateStatusError] = useState<\n    string | undefined\n  >(undefined);\n\n  const handleUpdateStatus = useEffectEvent(async () => {\n    setUpdateStatusError(undefined);\n    setIsCheckStateInProgress(true);\n\n    const updateStatusResult = await nativeClient.domain.updateStatus.mutate({\n      projectId: projectDomain.projectId,\n      domain: projectDomain.domain,\n    });\n\n    setIsStatusLoading(false);\n\n    if (updateStatusResult.success === false) {\n      setUpdateStatusError(updateStatusResult.error);\n      return;\n    }\n\n    await refresh();\n  });\n\n  const onceRef = useRef(false);\n  useEffect(() => {\n    if (onceRef.current) {\n      return;\n    }\n    onceRef.current = true;\n\n    if (isStatusLoading === false) {\n      return;\n    }\n\n    if (status === \"UNVERIFIED\") {\n      startTransition(async () => {\n        await handleVerify();\n        await handleUpdateStatus();\n      });\n      return;\n    }\n    startTransition(async () => {\n      await handleUpdateStatus();\n    });\n  }, [status, isStatusLoading]);\n\n  const domainStatus = getStatus(projectDomain);\n\n  const { isVerifiedActive, text } = getStatusText({\n    projectDomain,\n    isLoading: false,\n  });\n\n  const publisherHost = useStore($publisherHost);\n  const cname = extractCname(projectDomain.domain);\n  const dnsRecords = [\n    {\n      type: \"CNAME\",\n      host: cname,\n      value: `${projectDomain.cname}.customers.${publisherHost}`,\n      ttl: 300,\n    } as const,\n    {\n      type: \"TXT\",\n      host: cname === \"@\" ? \"_webstudio_is\" : `_webstudio_is.${cname}`,\n      value: projectDomain.expectedTxtRecord,\n      ttl: 300,\n    } as const,\n  ];\n\n  return (\n    <CollapsibleDomainSection\n      prefix={\n        <DomainCheckbox\n          buildId={projectDomain.latestBuildVirtual?.buildId}\n          defaultChecked={\n            projectDomain.latestBuildVirtual == null ||\n            projectDomain.latestBuildVirtual.buildId ===\n              project.latestBuildVirtual?.buildId\n          }\n          domain={projectDomain.domain}\n          disabled={domainStatus !== \"VERIFIED_ACTIVE\"}\n        />\n      }\n      initiallyOpen={initiallyOpen}\n      title={projectDomain.domain}\n      suffix={\n        <Grid flow=\"column\">\n          <StatusIcon\n            isLoading={isStatusLoading}\n            projectDomain={projectDomain}\n          />\n\n          <CopyToClipboard\n            text={`https://${projectDomain.domain}`}\n            copyText={`Copy link: https://${projectDomain.domain}`}\n          >\n            <IconButton type=\"button\" tabIndex={-1}>\n              <CopyIcon />\n            </IconButton>\n          </CopyToClipboard>\n        </Grid>\n      }\n    >\n      {status === \"UNVERIFIED\" && (\n        <>\n          <Button\n            formAction={handleVerify}\n            state={isCheckStateInProgress ? \"pending\" : undefined}\n            color=\"neutral\"\n            css={{ width: \"100%\", flexShrink: 0, mt: theme.spacing[3] }}\n          >\n            Check status\n          </Button>\n        </>\n      )}\n\n      {status !== \"UNVERIFIED\" && (\n        <>\n          {updateStatusError && (\n            <Text color=\"destructive\">{updateStatusError}</Text>\n          )}\n          <Button\n            formAction={handleUpdateStatus}\n            state={isCheckStateInProgress ? \"pending\" : undefined}\n            color=\"neutral\"\n            css={{ width: \"100%\", flexShrink: 0, mt: theme.spacing[3] }}\n          >\n            Check status\n          </Button>\n        </>\n      )}\n\n      {projectDomain.latestBuildVirtual && (\n        <Button\n          formAction={handleUnpublish}\n          state={isUnpublishInProgress ? \"pending\" : undefined}\n          color=\"destructive\"\n          css={{ width: \"100%\", flexShrink: 0 }}\n        >\n          Unpublish\n        </Button>\n      )}\n\n      <Button\n        formAction={handleRemoveDomain}\n        state={isRemoveInProgress ? \"pending\" : undefined}\n        color=\"destructive\"\n        css={{ width: \"100%\", flexShrink: 0 }}\n      >\n        Remove domain\n      </Button>\n\n      <Grid gap={2} css={{ mt: theme.spacing[5] }}>\n        <Grid gap={1}>\n          {status === \"UNVERIFIED\" && (\n            <>\n              {verifyError ? (\n                <Text color=\"destructive\">\n                  Status: Failed to verify\n                  <br />\n                  {verifyError}\n                </Text>\n              ) : (\n                <>\n                  <Text color=\"destructive\">Status: Not verified</Text>\n                  <Text color=\"subtle\">\n                    Verification may take up to 24 hours but usually takes only\n                    a few minutes.\n                  </Text>\n                </>\n              )}\n            </>\n          )}\n\n          {status !== \"UNVERIFIED\" && (\n            <>\n              <Text color={isVerifiedActive ? \"success\" : \"destructive\"}>\n                {text}\n              </Text>\n            </>\n          )}\n        </Grid>\n\n        <Flex align=\"center\" gap=\"1\">\n          <Text color=\"subtle\">\n            <strong>Connect your domain</strong>\n          </Text>\n          <Tooltip\n            variant=\"wrapped\"\n            content={\n              <Text>\n                Visit the admin console of your domain registrar (the website\n                you purchased your domain from) and create one CNAME record and\n                one TXT record with the values shown below.{\" \"}\n                <Link\n                  color=\"inherit\"\n                  href=\"https://docs.webstudio.is/university/foundations/publishing-and-custom-domains\"\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                >\n                  Learn more.\n                </Link>\n              </Text>\n            }\n          >\n            <SmallIconButton icon={<HelpIcon />} />\n          </Tooltip>\n        </Flex>\n\n        <Grid\n          gap={2}\n          css={{ gridTemplateColumns: `${theme.spacing[18]} 1fr 1fr` }}\n        >\n          <Text color=\"subtle\" variant=\"titles\">\n            TYPE\n          </Text>\n          <Text color=\"subtle\" variant=\"titles\">\n            NAME\n          </Text>\n          <Text color=\"subtle\" variant=\"titles\">\n            VALUE\n          </Text>\n\n          {dnsRecords.map((record, index) => (\n            <Fragment key={index}>\n              <InputEllipsis readOnly value={record.type} />\n              <InputEllipsis\n                readOnly\n                value={record.host}\n                suffix={\n                  <CopyToClipboard text={record.host}>\n                    <NestedInputButton type=\"button\">\n                      <CopyIcon />\n                    </NestedInputButton>\n                  </CopyToClipboard>\n                }\n              />\n              <InputEllipsis\n                readOnly\n                value={record.value}\n                suffix={\n                  <CopyToClipboard text={record.value}>\n                    <NestedInputButton type=\"button\">\n                      <CopyIcon />\n                    </NestedInputButton>\n                  </CopyToClipboard>\n                }\n              />\n            </Fragment>\n          ))}\n        </Grid>\n\n        <Grid\n          gap={2}\n          align={\"center\"}\n          css={{\n            gridTemplateColumns: `1fr auto 1fr`,\n          }}\n        >\n          <Separator css={{ alignSelf: \"unset\" }} />\n          <Text color=\"main\">OR</Text>\n          <Separator css={{ alignSelf: \"unset\" }} />\n        </Grid>\n\n        <Entri\n          dnsRecords={dnsRecords}\n          domain={projectDomain.domain}\n          onClose={() => {\n            // Sometimes Entri modal dialog hangs even if it's successful,\n            // until they fix that, we'll just refresh the status here on every onClose event\n            if (status === \"UNVERIFIED\") {\n              startTransition(async () => {\n                await handleVerify();\n                await handleUpdateStatus();\n              });\n              return;\n            }\n            startTransition(async () => {\n              await handleUpdateStatus();\n            });\n          }}\n        />\n      </Grid>\n    </CollapsibleDomainSection>\n  );\n};\n\ntype DomainsProps = {\n  newDomains: Set<string>;\n  domains: Domain[];\n  refresh: () => Promise<void>;\n  project: Project;\n};\n\nexport const Domains = ({\n  newDomains,\n  domains,\n  refresh,\n  project,\n}: DomainsProps) => {\n  return (\n    <>\n      {domains.map((projectDomain) => (\n        <DomainItem\n          key={projectDomain.domain}\n          projectDomain={projectDomain}\n          initiallyOpen={newDomains.has(projectDomain.domain)}\n          refresh={refresh}\n          project={project}\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/entri.tsx",
    "content": "import * as entri from \"entrijs\";\nimport { useEffect, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  globalCss,\n  Button,\n  Text,\n  PanelBanner,\n  Flex,\n  Link,\n} from \"@webstudio-is/design-system\";\nimport { trpcClient } from \"~/shared/trpc/trpc-client\";\nimport { $userPlanFeatures } from \"~/shared/nano-states\";\nimport { extractCname } from \"./cname\";\nimport { UploadIcon } from \"@webstudio-is/icons\";\n\n// https://developers.entri.com/docs/install\ntype DnsRecord = {\n  type: \"CNAME\" | \"ALIAS\" | \"TXT\";\n  host: string;\n  value: string;\n  ttl: number;\n};\n\ntype EntriCloseEvent = CustomEvent<entri.EntriCloseEventDetail>;\n\ndeclare global {\n  // https://developers.entri.com/docs/integrate-with-dns-providers\n  interface WindowEventMap {\n    onEntriClose: EntriCloseEvent;\n  }\n}\n\n/**\n * Our FloatingPanelPopover adds pointerEvents: \"none\" to the body.\n * We open the entry dialog from the popover, so we need to allow pointer events on the entri dialog.\n */\nconst entriGlobalStyles = globalCss({\n  body: {\n    \"&>#entriApp\": {\n      pointerEvents: \"auto\",\n    },\n  },\n});\n\ntype EntriProps = {\n  domain: string;\n  dnsRecords: DnsRecord[];\n  onClose: (detail: entri.EntriCloseEventDetail) => void;\n};\n\nconst useEntri = ({ domain, dnsRecords, onClose }: EntriProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const {\n    load: entriTokenLoad,\n    data: entriTokenData,\n    error: entriTokenSystemError,\n  } = trpcClient.domain.getEntriToken.useQuery();\n\n  useEffect(() => {\n    const handleOnEntriClose = (event: EntriCloseEvent) => {\n      if (event.detail.domain === domain) {\n        onClose(event.detail);\n        setIsOpen(false);\n      }\n    };\n    window.addEventListener(\"onEntriClose\", handleOnEntriClose, false);\n    return () => {\n      window.removeEventListener(\"onEntriClose\", handleOnEntriClose, false);\n    };\n  }, [domain, onClose]);\n\n  const showDialog = () => {\n    setIsOpen(true);\n    entriTokenLoad(undefined, async (data) => {\n      if (data.success) {\n        await entri.showEntri({\n          applicationId: data.applicationId,\n          token: data.token,\n          dnsRecords,\n          prefilledDomain: domain,\n          // add redirect to www only when registered domain has www subdomain\n          wwwRedirect: extractCname(domain) === \"www\",\n        });\n      }\n    });\n  };\n\n  return {\n    isOpen,\n    showDialog,\n    error:\n      entriTokenSystemError ??\n      (entriTokenData?.success === false ? entriTokenData.error : undefined),\n  };\n};\n\nexport const Entri = ({ domain, dnsRecords, onClose }: EntriProps) => {\n  entriGlobalStyles();\n  const userPlanFeatures = useStore($userPlanFeatures);\n  const hasPaidPlan = userPlanFeatures.purchases.length > 0;\n  const { error, isOpen, showDialog } = useEntri({\n    domain,\n    dnsRecords,\n    onClose,\n  });\n  const [requestUpgrade, setRequestUpgrade] = useState(false);\n  return (\n    <>\n      {error !== undefined && <Text color=\"destructive\">{error}</Text>}\n      <Button\n        disabled={isOpen}\n        color=\"primary\"\n        type=\"button\"\n        onClick={() => {\n          if (hasPaidPlan) {\n            showDialog();\n          } else {\n            setRequestUpgrade(true);\n          }\n        }}\n      >\n        Setup automatically with Entri\n      </Button>\n      {requestUpgrade && (\n        <PanelBanner>\n          <Text>\n            Please upgrade to the Pro plan or higher to use automatic domain\n            configuration.\n          </Text>\n          <Flex align=\"center\" gap={1}>\n            <UploadIcon />\n            <Link\n              color=\"inherit\"\n              target=\"_blank\"\n              href=\"https://webstudio.is/pricing\"\n            >\n              Upgrade to Pro\n            </Link>\n          </Flex>\n        </PanelBanner>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/index.ts",
    "content": "export { PublishButton } from \"./publish\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/publish/publish.tsx",
    "content": "import stripIndent from \"strip-indent\";\nimport { computed } from \"nanostores\";\nimport {\n  useEffect,\n  useState,\n  useOptimistic,\n  useTransition,\n  startTransition,\n  useRef,\n  useId,\n  type ReactNode,\n} from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  Tooltip,\n  IconButton,\n  Grid,\n  Flex,\n  Label,\n  Text,\n  InputField,\n  Separator,\n  ScrollArea,\n  rawTheme,\n  Select,\n  theme,\n  TextArea,\n  Link,\n  PanelBanner,\n  buttonStyle,\n  toast,\n  RadioGroup,\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverTitle,\n  PopoverClose,\n  PopoverTitleActions,\n  css,\n  textVariants,\n  SmallIconButton,\n} from \"@webstudio-is/design-system\";\nimport { validateProjectDomain, type Project } from \"@webstudio-is/project\";\nimport {\n  $awareness,\n  $selectedPagePath,\n  findAwarenessByInstanceId,\n  type Awareness,\n} from \"~/shared/awareness\";\nimport {\n  $authTokenPermissions,\n  $dataSources,\n  $editingPageId,\n  $instances,\n  $pages,\n  $project,\n  $publishedOrigin,\n  $userPlanFeatures,\n  $stagingUsername,\n  $stagingPassword,\n  $publisherHost,\n} from \"~/shared/nano-states\";\nimport {\n  $publishDialog,\n  setActiveSidebarPanel,\n} from \"../../shared/nano-states\";\nimport { Domains, PENDING_TIMEOUT, getPublishStatusAndText } from \"./domains\";\nimport { CollapsibleDomainSection } from \"./collapsible-domain-section\";\nimport {\n  CheckCircleIcon,\n  AlertIcon,\n  CopyIcon,\n  GearIcon,\n  UpgradeIcon,\n  HelpIcon,\n  InfoCircleIcon,\n} from \"@webstudio-is/icons\";\nimport { AddDomain } from \"./add-domain\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { trpcClient, nativeClient } from \"~/shared/trpc/trpc-client\";\nimport { isPathnamePattern, type Templates } from \"@webstudio-is/sdk\";\nimport { DomainCheckbox, domainToPublishName } from \"./domain-checkbox\";\nimport { CopyToClipboard } from \"~/shared/copy-to-clipboard\";\nimport { $openProjectSettings } from \"~/shared/nano-states/project-settings\";\nimport { RelativeTime } from \"~/builder/shared/relative-time\";\nimport cmsUpgradeBanner from \"~/shared/cms-upgrade-banner.svg?url\";\n\ntype ChangeProjectDomainProps = {\n  project: Project;\n  projectState: \"idle\" | \"submitting\";\n  refresh: () => Promise<void>;\n};\n\nconst ChangeProjectDomain = ({\n  project,\n  refresh,\n}: ChangeProjectDomainProps) => {\n  const id = useId();\n  const publishedOrigin = useStore($publishedOrigin);\n  const selectedPagePath = useStore($selectedPagePath);\n  const stagingUsername = useStore($stagingUsername);\n  const stagingPassword = useStore($stagingPassword);\n  const publisherHost = useStore($publisherHost);\n\n  const [domain, setDomain] = useState(project.domain);\n  const [error, setError] = useState<string>();\n  const [isUpdateInProgress, setIsUpdateInProgress] = useOptimistic(false);\n  const [isUnpublishing, setIsUnpublishing] = useOptimistic(false);\n\n  const pageUrl = new URL(publishedOrigin);\n  pageUrl.pathname = selectedPagePath;\n\n  // Add username:password@ for staging domains\n  if (\n    stagingUsername &&\n    stagingPassword &&\n    pageUrl.hostname.endsWith(`.${publisherHost}`)\n  ) {\n    pageUrl.username = stagingUsername;\n    pageUrl.password = stagingPassword;\n  }\n\n  const updateProjectDomain = async () => {\n    setIsUpdateInProgress(true);\n    const validationResult = validateProjectDomain(domain);\n\n    if (validationResult.success === false) {\n      setError(validationResult.error);\n      return;\n    }\n\n    const updateResult = await nativeClient.domain.updateProjectDomain.mutate({\n      domain,\n      projectId: project.id,\n    });\n\n    if (updateResult.success === false) {\n      setError(updateResult.error);\n      return;\n    }\n\n    await refresh();\n  };\n\n  const handleUpdateProjectDomain = () => {\n    startTransition(async () => {\n      await updateProjectDomain();\n    });\n  };\n\n  const handleUnpublish = async () => {\n    setIsUnpublishing(true);\n    const result = await nativeClient.domain.unpublish.mutate({\n      projectId: project.id,\n      domain: `${project.domain}.${publisherHost}`,\n    });\n    if (result.success === false) {\n      toast.error(result.message);\n      return;\n    }\n    await refresh();\n    toast.success(result.message);\n  };\n\n  const { statusText, status } =\n    project.latestBuildVirtual != null\n      ? getPublishStatusAndText(project.latestBuildVirtual)\n      : {\n          statusText: \"Not published\",\n          status: \"PENDING\" as const,\n        };\n\n  // Check if the wstd domain specifically is published (not just any custom domain)\n  const isPublished = project.latestBuildVirtual?.domain === project.domain;\n\n  return (\n    <CollapsibleDomainSection\n      title={pageUrl.host}\n      prefix={\n        <DomainCheckbox\n          defaultChecked={project.latestBuildVirtual?.domain === domain}\n          buildId={project.latestBuildVirtual?.buildId}\n          domain={domain}\n        />\n      }\n      suffix={\n        <Grid flow=\"column\" align=\"center\">\n          <Tooltip\n            content={error !== undefined ? error : <Text>{statusText}</Text>}\n          >\n            <Flex\n              align=\"center\"\n              justify=\"center\"\n              css={{\n                cursor: \"pointer\",\n                width: theme.sizes.controlHeight,\n                height: theme.sizes.controlHeight,\n                color:\n                  error !== undefined || status === \"FAILED\"\n                    ? theme.colors.foregroundDestructive\n                    : theme.colors.foregroundSuccessText,\n              }}\n            >\n              {error !== undefined || status === \"FAILED\" ? (\n                <AlertIcon />\n              ) : (\n                <CheckCircleIcon />\n              )}\n            </Flex>\n          </Tooltip>\n\n          <CopyToClipboard\n            text={pageUrl.toString()}\n            copyText={`Copy link: ${pageUrl.toString()}`}\n          >\n            <IconButton type=\"button\" tabIndex={-1}>\n              <CopyIcon />\n            </IconButton>\n          </CopyToClipboard>\n        </Grid>\n      }\n    >\n      <Grid gap={2}>\n        <Grid flow=\"column\" align=\"center\" gap={2}>\n          <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n            <Label htmlFor={id}>Domain:</Label>\n            <Tooltip\n              content=\"Domain can't be renamed once published. Unpublish to enable renaming.\"\n              variant=\"wrapped\"\n            >\n              <InfoCircleIcon\n                tabIndex={0}\n                style={{ flexShrink: 0 }}\n                color={rawTheme.colors.foregroundSubtle}\n              />\n            </Tooltip>\n          </Flex>\n          <InputField\n            text=\"mono\"\n            id={id}\n            placeholder=\"Domain\"\n            value={domain}\n            disabled={isUpdateInProgress || isPublished}\n            onChange={(event) => {\n              setError(undefined);\n              setDomain(event.target.value);\n            }}\n            onBlur={handleUpdateProjectDomain}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\") {\n                handleUpdateProjectDomain();\n              }\n\n              if (event.key === \"Escape\") {\n                if (project.domain !== domain) {\n                  setDomain(project.domain);\n                  event.preventDefault();\n                }\n              }\n            }}\n            color={error !== undefined ? \"error\" : undefined}\n          />\n          {error !== undefined && <Text color=\"destructive\">{error}</Text>}\n        </Grid>\n        {stagingUsername && (\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Label\n              htmlFor={`${id}-username`}\n              css={{ width: theme.spacing[20] }}\n            >\n              Username:\n            </Label>\n            <InputField\n              text=\"mono\"\n              id={`${id}-username`}\n              type=\"text\"\n              value={stagingUsername}\n              readOnly\n            />\n          </Grid>\n        )}\n        {stagingPassword && (\n          <Grid flow=\"column\" align=\"center\" gap={2}>\n            <Flex align=\"center\" gap={1} css={{ width: theme.spacing[20] }}>\n              <Label htmlFor={`${id}-password`}>Password:</Label>\n              <Tooltip\n                content=\"This password is read-only and cannot be changed. It is the same for every user. This prevents phishing attacks.\"\n                variant=\"wrapped\"\n              >\n                <InfoCircleIcon\n                  tabIndex={0}\n                  style={{ flexShrink: 0 }}\n                  color={rawTheme.colors.foregroundSubtle}\n                />\n              </Tooltip>\n            </Flex>\n            <InputField\n              text=\"mono\"\n              id={`${id}-password`}\n              type=\"text\"\n              value={stagingPassword}\n              readOnly\n            />\n          </Grid>\n        )}\n        {isPublished && (\n          <Tooltip content=\"Unpublish to enable domain renaming\">\n            <Button\n              formAction={handleUnpublish}\n              color=\"destructive\"\n              state={isUnpublishing ? \"pending\" : undefined}\n              css={{ width: \"100%\" }}\n            >\n              Unpublish\n            </Button>\n          </Tooltip>\n        )}\n      </Grid>\n    </CollapsibleDomainSection>\n  );\n};\n\nconst $restrictedFeatures = computed(\n  [$pages, $dataSources, $instances, $userPlanFeatures],\n  (pages, dataSources, instances, userPlanFeatures) => {\n    const features = new Map<\n      string,\n      | undefined\n      | { awareness?: Awareness; view?: \"pageSettings\"; info?: ReactNode }\n    >();\n    if (pages === undefined) {\n      return features;\n    }\n    // specified emails for default webhook form\n    if (\n      userPlanFeatures.maxContactEmails === 0 &&\n      (pages?.meta?.contactEmail ?? \"\").trim()\n    ) {\n      features.set(\"Custom contact email\", undefined);\n    }\n    if (!userPlanFeatures.allowDynamicData) {\n      // pages with dynamic paths\n      for (const page of [pages.homePage, ...pages.pages]) {\n        const awareness = {\n          pageId: page.id,\n          instanceSelector: [page.rootInstanceId],\n        };\n        // allow catch all for 404 pages on free plan\n        if (isPathnamePattern(page.path) && page.path !== \"/*\") {\n          features.set(\"Dynamic path\", { awareness, view: \"pageSettings\" });\n        }\n        if (page.meta.redirect && page.meta.redirect !== `\"\"`) {\n          features.set(\"Redirect\", { awareness, view: \"pageSettings\" });\n        }\n      }\n      // has resource variables\n      for (const dataSource of dataSources.values()) {\n        if (dataSource.type === \"resource\") {\n          const instanceId = dataSource.scopeInstanceId ?? \"\";\n          features.set(\"Resource variable\", {\n            awareness: findAwarenessByInstanceId(pages, instances, instanceId),\n          });\n        }\n      }\n    }\n    return features;\n  }\n);\n\nconst usePublishCountdown = (isPublishing: boolean) => {\n  const [countdown, setCountdown] = useState<number | undefined>(undefined);\n\n  useEffect(() => {\n    if (isPublishing === false) {\n      setCountdown(undefined);\n      return;\n    }\n\n    setCountdown(60);\n\n    const interval = setInterval(() => {\n      setCountdown((prev) => {\n        if (prev === undefined || prev <= 0) {\n          return 0;\n        }\n        return prev - 1;\n      });\n    }, 1000);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }, [isPublishing]);\n\n  return countdown;\n};\n\nconst Publish = ({\n  project,\n  timesLeft,\n  disabled,\n  refresh,\n}: {\n  project: Project;\n  timesLeft: number;\n  disabled: boolean;\n  refresh: () => Promise<void>;\n}) => {\n  const { maxPublishesAllowedPerUser } = useStore($userPlanFeatures);\n  const [publishError, setPublishError] = useState<\n    undefined | JSX.Element | string\n  >();\n  const [isPublishing, setIsPublishing] = useOptimistic(false);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const [hasSelectedDomains, setHasSelectedDomains] = useState(false);\n  const userPlanFeatures = useStore($userPlanFeatures);\n  const hasPaidPlan = userPlanFeatures.purchases.length > 0;\n  const { allowStagingPublish } = userPlanFeatures;\n  const countdown = usePublishCountdown(isPublishing);\n\n  useEffect(() => {\n    if (allowStagingPublish === false) {\n      setHasSelectedDomains(true);\n      return;\n    }\n    const form = buttonRef.current?.closest(\"form\");\n\n    if (form == null) {\n      return;\n    }\n\n    const handleFormInput = () => {\n      const formData = new FormData(form);\n      const domainsSelected = formData.getAll(domainToPublishName).length;\n      setHasSelectedDomains(domainsSelected > 0);\n    };\n\n    const observer = new MutationObserver(() => {\n      handleFormInput();\n    });\n\n    observer.observe(form, {\n      attributes: true,\n      childList: true,\n      subtree: true,\n      attributeFilter: [\"value\", \"checked\"],\n    });\n\n    handleFormInput();\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [allowStagingPublish]);\n\n  const handlePublish = async (formData: FormData) => {\n    setPublishError(undefined);\n    setIsPublishing(true);\n\n    const domains = allowStagingPublish\n      ? formData\n          .getAll(domainToPublishName)\n          .map((domainEntry) => domainEntry.toString())\n      : [\n          project.domain,\n          ...project.domainsVirtual\n            .filter((domain) => domain.verified && domain.status === \"ACTIVE\")\n            .map((domain) => domain.domain),\n        ];\n\n    if (domains.length === 0) {\n      toast.error(\"Please select at least one domain to publish\");\n      return;\n    }\n\n    const publishResult = await nativeClient.domain.publish.mutate({\n      projectId: project.id,\n      domains,\n      destination: \"saas\",\n    });\n\n    if (publishResult.success === false) {\n      console.error(publishResult.error);\n\n      let error: JSX.Element | string = publishResult.error;\n      if (publishResult.error === \"NOT_IMPLEMENTED\") {\n        error = (\n          <>\n            <Tooltip\n              content={\n                <Text userSelect=\"text\">\n                  {project.latestBuildVirtual?.buildId}\n                </Text>\n              }\n            >\n              <span>Build data</span>\n            </Tooltip>{\" \"}\n            for publishing has been successfully created. Use{\" \"}\n            <Link href=\"https://docs.webstudio.is/university/self-hosting/cli\">\n              Webstudio&nbsp;CLI\n            </Link>{\" \"}\n            to generate the code.\n          </>\n        );\n      }\n      setPublishError(error);\n      if (publishResult.error === \"NOT_IMPLEMENTED\") {\n        toast.info(error);\n      } else {\n        toast.error(error);\n      }\n\n      if (process.env.NODE_ENV === \"development\") {\n        // Refresh locally as it's always an error\n        await refresh();\n      }\n\n      return;\n    }\n\n    let sleepTime = 15000;\n    const timeToFinish = Date.now() + PENDING_TIMEOUT + 2 * sleepTime;\n\n    // Wait until project is published or failed\n    while (Date.now() < timeToFinish) {\n      await refresh();\n\n      const project = $project.get();\n\n      if (project == null) {\n        throw new Error(\"Project not found\");\n      }\n\n      const { statusText, status } =\n        project.latestBuildVirtual != null\n          ? getPublishStatusAndText(project.latestBuildVirtual)\n          : {\n              statusText: \"Not published\",\n              status: \"PENDING\" as const,\n            };\n\n      if (status === \"PUBLISHED\") {\n        toast.success(\n          <>\n            The project has been successfully published.{\" \"}\n            {hasPaidPlan === false && (\n              <div>\n                On the free plan, you have {timesLeft} out of{\" \"}\n                {maxPublishesAllowedPerUser} daily publications remaining. The\n                counter resets tomorrow.\n              </div>\n            )}\n          </>,\n          { duration: 10000 }\n        );\n        break;\n      }\n\n      if (status === \"FAILED\") {\n        toast.error(statusText);\n        setPublishError(statusText);\n        break;\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, sleepTime));\n\n      sleepTime = Math.max(5000, sleepTime - 5000);\n    }\n  };\n\n  const hasPendingState = project.latestBuildVirtual\n    ? getPublishStatusAndText(project.latestBuildVirtual).status === \"PENDING\"\n    : false;\n\n  const isPublishInProgress = isPublishing || hasPendingState;\n  const showPendingState =\n    isPublishInProgress && (countdown === undefined || countdown === 0);\n\n  return (\n    <Flex gap={2} shrink={false} direction={\"column\"}>\n      {publishError && <Text color=\"destructive\">{publishError}</Text>}\n\n      <Tooltip\n        content={\n          isPublishInProgress\n            ? \"Publish process in progress\"\n            : hasSelectedDomains\n              ? undefined\n              : \"Select at least one domain to publish\"\n        }\n      >\n        <Button\n          ref={buttonRef}\n          formAction={handlePublish}\n          color=\"positive\"\n          state={showPendingState ? \"pending\" : undefined}\n          disabled={hasSelectedDomains === false || disabled}\n        >\n          {countdown !== undefined && countdown > 0\n            ? `Publishing (${countdown}s)`\n            : \"Publish\"}\n        </Button>\n      </Tooltip>\n    </Flex>\n  );\n};\n\nconst getStaticPublishStatusAndText = ({\n  updatedAt,\n  publishStatus,\n}: {\n  updatedAt: string;\n  publishStatus: \"PENDING\" | \"FAILED\" | \"PUBLISHED\";\n}) => {\n  let status = publishStatus;\n\n  const delta = Date.now() - new Date(updatedAt).getTime();\n  // Assume build failed after 3 minutes\n\n  if (publishStatus === \"PENDING\" && delta > PENDING_TIMEOUT) {\n    status = \"FAILED\";\n  }\n\n  const textStart =\n    status === \"PUBLISHED\"\n      ? \"Downloaded\"\n      : status === \"FAILED\"\n        ? \"Download failed\"\n        : \"Download started\";\n\n  const statusText = (\n    <>\n      {textStart} <RelativeTime time={new Date(updatedAt)} />\n    </>\n  );\n\n  return { statusText, status };\n};\n\nconst PublishStatic = ({\n  projectId,\n  templates,\n}: {\n  projectId: Project[\"id\"];\n  templates: readonly Templates[];\n}) => {\n  const project = useStore($project);\n  const [_, startTransition] = useTransition();\n\n  if (project == null) {\n    throw new Error(\"Project not found\");\n  }\n\n  const { status, statusText } =\n    project.latestStaticBuild == null\n      ? { status: \"LOADED\" as const, statusText: \"Not published\" }\n      : getStaticPublishStatusAndText(project.latestStaticBuild);\n\n  const [isPending, setIsPendingOptimistic] = useOptimistic(false);\n\n  const isPublishInProgress = status === \"PENDING\" || isPending;\n\n  return (\n    <Flex gap={2} shrink={false} direction={\"column\"}>\n      {status === \"FAILED\" && <Text color=\"destructive\">{statusText}</Text>}\n\n      <Tooltip\n        content={isPublishInProgress ? \"Preparing static site\" : undefined}\n      >\n        <Button\n          type=\"button\"\n          color=\"positive\"\n          state={isPublishInProgress ? \"pending\" : undefined}\n          onClick={() => {\n            startTransition(async () => {\n              try {\n                setIsPendingOptimistic(true);\n\n                const result = await nativeClient.domain.publish.mutate({\n                  projectId,\n                  destination: \"static\",\n                  templates: [...templates],\n                });\n\n                if (result.success === false) {\n                  toast.error(result.error);\n                  return;\n                }\n\n                const name = \"name\" in result ? result.name : undefined;\n\n                if (name == null) {\n                  toast.error('File name must be defined in \"result\"');\n                  return;\n                }\n\n                const timeout = 10000;\n\n                // Repeat few more times than timeout\n                const repeat = PENDING_TIMEOUT / timeout + 5;\n\n                for (let i = 0; i !== repeat; i++) {\n                  await new Promise((resolve) => setTimeout(resolve, timeout));\n\n                  await refreshProject();\n\n                  const latestStaticBuild = $project.get()?.latestStaticBuild;\n\n                  if (latestStaticBuild == null) {\n                    continue;\n                  }\n\n                  const { status } =\n                    getStaticPublishStatusAndText(latestStaticBuild);\n\n                  if (status !== \"PENDING\") {\n                    break;\n                  }\n                }\n\n                const latestStaticBuild = $project.get()?.latestStaticBuild;\n\n                if (latestStaticBuild == null) {\n                  throw new Error(\"Static build not found\");\n                }\n\n                const { status, statusText } =\n                  getStaticPublishStatusAndText(latestStaticBuild);\n\n                if (status === \"FAILED\") {\n                  // Report if Export failed\n                  toast.error(statusText);\n                }\n\n                if (status === \"PUBLISHED\") {\n                  window.location.href = `/cgi/static/ssg/${name}`;\n                }\n              } catch (error) {\n                toast.error(\n                  error instanceof Error ? error.message : \"Unknown error\"\n                );\n              }\n            });\n          }}\n        >\n          Build and download static site\n        </Button>\n      </Tooltip>\n    </Flex>\n  );\n};\n\nconst useCanAddDomain = () => {\n  const { load, data } = trpcClient.domain.countTotalDomains.useQuery();\n  const { maxDomainsAllowedPerUser } = useStore($userPlanFeatures);\n  const project = useStore($project);\n  const activeDomainsCount = project?.domainsVirtual.filter(\n    (domain) => domain.status === \"ACTIVE\" && domain.verified\n  ).length;\n  useEffect(() => {\n    load();\n  }, [load, activeDomainsCount]);\n  const canAddDomain = data\n    ? data.success && data.data < maxDomainsAllowedPerUser\n    : true;\n  return { canAddDomain, maxDomainsAllowedPerUser };\n};\n\nconst useUserPublishCount = () => {\n  const { load, data } = trpcClient.project.userPublishCount.useQuery();\n  const { maxPublishesAllowedPerUser } = useStore($userPlanFeatures);\n  useEffect(() => {\n    load();\n  }, [load]);\n  return {\n    userPublishCount: data?.success ? data.data : 0,\n    maxPublishesAllowedPerUser,\n  };\n};\n\nconst refreshProject = async () => {\n  const result = await nativeClient.domain.project.query(\n    {\n      projectId: $project.get()!.id,\n    }\n    // Pass abort signal\n    // { signal: undefined }\n  );\n\n  if (result.success) {\n    $project.set(result.project);\n    return;\n  }\n\n  toast.error(result.error);\n};\n\nconst buttonLinkClass = css({\n  all: \"unset\",\n  cursor: \"pointer\",\n  ...textVariants.link,\n}).toString();\n\nconst UpgradeBanner = () => {\n  const restrictedFeatures = useStore($restrictedFeatures);\n  const { canAddDomain } = useCanAddDomain();\n  const { userPublishCount, maxPublishesAllowedPerUser } =\n    useUserPublishCount();\n\n  if (userPublishCount >= maxPublishesAllowedPerUser) {\n    return (\n      <PanelBanner>\n        <Text variant=\"regularBold\">\n          Upgrade to publish more than {maxPublishesAllowedPerUser} times per\n          day:\n        </Text>\n        <Link\n          className={buttonStyle({ color: \"gradient\" })}\n          color=\"contrast\"\n          underline=\"none\"\n          href=\"https://webstudio.is/pricing\"\n          target=\"_blank\"\n        >\n          Upgrade\n        </Link>\n      </PanelBanner>\n    );\n  }\n\n  if (restrictedFeatures.size > 0) {\n    return (\n      <PanelBanner>\n        <img\n          src={cmsUpgradeBanner}\n          alt=\"Upgrade for CMS\"\n          width={rawTheme.spacing[28]}\n          style={{ aspectRatio: \"4.1\" }}\n        />\n        <Text variant=\"regularBold\">Following Pro features are used:</Text>\n        <Text as=\"ul\" color=\"destructive\" css={{ paddingLeft: \"1em\" }}>\n          {Array.from(restrictedFeatures).map(\n            ([message, { awareness, view, info } = {}], index) => (\n              <li key={index}>\n                <Flex align=\"center\" gap=\"1\">\n                  {awareness ? (\n                    <button\n                      className={buttonLinkClass}\n                      type=\"button\"\n                      onClick={() => {\n                        $awareness.set(awareness);\n                        if (view === \"pageSettings\") {\n                          setActiveSidebarPanel(\"pages\");\n                          $editingPageId.set(awareness.pageId);\n                        }\n                      }}\n                    >\n                      {message}\n                    </button>\n                  ) : (\n                    message\n                  )}\n                  {info && (\n                    <Tooltip variant=\"wrapped\" content={info}>\n                      <SmallIconButton icon={<HelpIcon />} />\n                    </Tooltip>\n                  )}\n                </Flex>\n              </li>\n            )\n          )}\n        </Text>\n        <Text>You can delete these features or upgrade.</Text>\n        <Flex align=\"center\" gap={1}>\n          <UpgradeIcon />\n          <Link\n            color=\"inherit\"\n            target=\"_blank\"\n            href=\"https://webstudio.is/pricing\"\n          >\n            Upgrade to Pro\n          </Link>\n        </Flex>\n      </PanelBanner>\n    );\n  }\n  if (canAddDomain === false) {\n    return (\n      <PanelBanner>\n        <Text variant=\"regular\">\n          <Text variant=\"regularBold\" inline>\n            Upgrade to a Pro account\n          </Text>{\" \"}\n          to add unlimited domains and publish to each domain individually.\n        </Text>\n        <Flex align=\"center\" gap={1}>\n          <UpgradeIcon />\n          <Link\n            color=\"inherit\"\n            target=\"_blank\"\n            href=\"https://webstudio.is/pricing\"\n          >\n            Upgrade to Pro\n          </Link>\n        </Flex>\n      </PanelBanner>\n    );\n  }\n};\n\nconst Content = (props: {\n  projectId: Project[\"id\"];\n  onExportClick: () => void;\n}) => {\n  const restrictedFeatures = useStore($restrictedFeatures);\n  const [newDomains, setNewDomains] = useState(new Set<string>());\n\n  const project = useStore($project);\n\n  if (project == null) {\n    throw new Error(\"Project not found\");\n  }\n  const projectState = \"idle\";\n\n  const { userPublishCount, maxPublishesAllowedPerUser } =\n    useUserPublishCount();\n\n  const hasUnpublishedDomains = project.domainsVirtual.some(\n    (domain) =>\n      domain.verified &&\n      domain.status === \"ACTIVE\" &&\n      domain.latestBuildVirtual == null\n  );\n\n  return (\n    <form>\n      <ScrollArea>\n        <RadioGroup name=\"publishDomain\">\n          <ChangeProjectDomain\n            refresh={refreshProject}\n            projectState={projectState}\n            project={project}\n          />\n\n          <Domains\n            newDomains={newDomains}\n            domains={project.domainsVirtual}\n            refresh={refreshProject}\n            project={project}\n          />\n        </RadioGroup>\n      </ScrollArea>\n      <Flex direction=\"column\" justify=\"end\" css={{ height: 0 }}>\n        <Separator />\n      </Flex>\n      <Flex direction=\"column\" gap=\"2\" css={{ padding: theme.panel.padding }}>\n        <AddDomain\n          projectId={props.projectId}\n          refresh={refreshProject}\n          onCreate={(domain) => {\n            setNewDomains((prev) => {\n              return new Set([...prev, domain]);\n            });\n          }}\n          onExportClick={props.onExportClick}\n        />\n        <UpgradeBanner />\n        {hasUnpublishedDomains && (\n          <PanelBanner>\n            <Flex align=\"center\" gap=\"1\">\n              <InfoCircleIcon color={rawTheme.colors.foregroundMain} />\n              <Text variant=\"regularBold\">Don't forget to publish</Text>\n            </Flex>\n            <Text>\n              You have a custom domain that hasn't been published yet. Hit\n              publish to make it live.\n            </Text>\n          </PanelBanner>\n        )}\n        <Publish\n          project={project}\n          refresh={refreshProject}\n          timesLeft={maxPublishesAllowedPerUser - userPublishCount}\n          disabled={\n            restrictedFeatures.size > 0 ||\n            userPublishCount >= maxPublishesAllowedPerUser\n          }\n        />\n      </Flex>\n    </form>\n  );\n};\n\ntype DeployTarget = {\n  docs?: string;\n  command?: string;\n  ssgTemplates?: Templates[];\n};\n\nconst deployTargets: Record<string, DeployTarget> = {\n  docker: {\n    docs: \"https://docs.docker.com\",\n    command: `\n      docker build -t my-image .\n      docker run my-image\n    `,\n  },\n  static: {\n    ssgTemplates: [\"ssg\"],\n  },\n  vercel: {\n    docs: \"https://vercel.com/docs/cli\",\n    command: \"npx vercel@latest\",\n    ssgTemplates: [\"ssg-vercel\"],\n  },\n  netlify: {\n    docs: \"https://docs.netlify.com/cli/get-started/\",\n    command: `\nnpx netlify-cli@latest login\nnpx netlify-cli sites:create\nnpx netlify-cli build\nnpx netlify-cli deploy`,\n    ssgTemplates: [\"ssg-netlify\"],\n  },\n};\n\ntype DeployTargets = keyof typeof deployTargets;\n\nconst isDeployTargets = (value: string): value is DeployTargets =>\n  Object.keys(deployTargets).includes(value);\n\nconst ExportContent = (props: { projectId: Project[\"id\"] }) => {\n  const npxCommand = \"npx webstudio@latest\";\n  const [deployTarget, setDeployTarget] = useState<DeployTargets>(\"docker\");\n\n  return (\n    <Grid columns={1} gap={3} css={{ padding: theme.panel.padding }}>\n      <Grid columns={1} gap={2}>\n        <div />\n        <Grid columns={2} gap={2} align={\"center\"}>\n          <Text color=\"main\" variant=\"labels\">\n            Destination\n          </Text>\n\n          <Select\n            fullWidth\n            value={deployTarget}\n            options={Object.keys(deployTargets)}\n            getLabel={(value) => humanizeString(value)}\n            onChange={(value) => {\n              if (isDeployTargets(value)) {\n                setDeployTarget(value);\n              }\n            }}\n          />\n        </Grid>\n      </Grid>\n\n      {deployTargets[deployTarget].ssgTemplates && (\n        <Grid columns={1} gap={1}>\n          <PublishStatic\n            projectId={props.projectId}\n            templates={deployTargets[deployTarget].ssgTemplates}\n          />\n          <div />\n          <Text color=\"subtle\">\n            Learn about deploying static sites{\" \"}\n            <Link\n              variant=\"inherit\"\n              color=\"inherit\"\n              href=\"https://wstd.us/ssg\"\n              target=\"_blank\"\n              rel=\"noreferrer\"\n            >\n              here\n            </Link>\n          </Text>\n        </Grid>\n      )}\n\n      {deployTargets[deployTarget].command && (\n        <Grid columns={1} gap={2}>\n          <Grid\n            gap={2}\n            align={\"center\"}\n            css={{\n              gridTemplateColumns: `1fr auto 1fr`,\n            }}\n          >\n            <Separator css={{ alignSelf: \"unset\" }} />\n            <Text color=\"main\">CLI</Text>\n            <Separator css={{ alignSelf: \"unset\" }} />\n          </Grid>\n          <Grid columns={1} gap={1}>\n            <Text color=\"main\" variant=\"labels\">\n              Step 1\n            </Text>\n            <Text color=\"subtle\">\n              Download and install Node v20+ from{\" \"}\n              <Link\n                variant=\"inherit\"\n                color=\"inherit\"\n                href=\"https://nodejs.org/\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                nodejs.org\n              </Link>{\" \"}\n              or with{\" \"}\n              <Link\n                variant=\"inherit\"\n                color=\"inherit\"\n                href=\"https://nodejs.org/en/download/package-manager\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                a package manager\n              </Link>\n              .\n            </Text>\n          </Grid>\n\n          <Grid columns={1} gap={2}>\n            <Grid columns={1} gap={1}>\n              <Text color=\"main\" variant=\"labels\">\n                Step 2\n              </Text>\n              <Text color=\"subtle\">\n                Run this command in your Terminal to install Webstudio CLI and\n                sync your project.\n              </Text>\n            </Grid>\n            <Flex gap={2}>\n              <InputField\n                css={{ flex: 1 }}\n                text=\"mono\"\n                readOnly\n                value={npxCommand}\n              />\n              <CopyToClipboard text={npxCommand}>\n                <Button type=\"button\" color=\"neutral\" prefix={<CopyIcon />}>\n                  Copy\n                </Button>\n              </CopyToClipboard>\n            </Flex>\n          </Grid>\n\n          <Grid columns={1} gap={2}>\n            <Grid columns={1} gap={1}>\n              <Text color=\"main\" variant=\"labels\">\n                Step 3\n              </Text>\n              <Text color=\"subtle\">\n                Run this command to publish to{\" \"}\n                <Link\n                  variant=\"inherit\"\n                  color=\"inherit\"\n                  href={deployTargets[deployTarget].docs}\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                >\n                  {humanizeString(deployTarget)}\n                </Link>{\" \"}\n              </Text>\n            </Grid>\n            <Flex gap={2} align=\"end\">\n              <TextArea\n                css={{ flex: 1 }}\n                variant=\"mono\"\n                readOnly\n                value={stripIndent(deployTargets[deployTarget].command)\n                  .trimStart()\n                  .replace(/ +$/, \"\")}\n              />\n              <CopyToClipboard text={deployTargets[deployTarget].command}>\n                <Button\n                  type=\"button\"\n                  css={{ flexShrink: 0 }}\n                  color=\"neutral\"\n                  prefix={<CopyIcon />}\n                >\n                  Copy\n                </Button>\n              </CopyToClipboard>\n            </Flex>\n          </Grid>\n\n          <Grid columns={1} gap={1}>\n            <Text color=\"subtle\">\n              Read the detailed documentation{\" \"}\n              <Link\n                variant=\"inherit\"\n                color=\"inherit\"\n                href=\"https://wstd.us/cli\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n              >\n                here\n              </Link>\n            </Text>\n          </Grid>\n        </Grid>\n      )}\n    </Grid>\n  );\n};\n\ntype PublishProps = {\n  projectId: Project[\"id\"];\n};\n\nexport const PublishButton = ({ projectId }: PublishProps) => {\n  const publishDialog = useStore($publishDialog);\n  const authTokenPermissions = useStore($authTokenPermissions);\n  const isPublishEnabled = authTokenPermissions.canPublish;\n\n  const tooltipContent = isPublishEnabled\n    ? undefined\n    : \"Only the owner, an admin, or content editors with publish permissions can publish projects\";\n\n  const handleExportClick = () => {\n    $publishDialog.set(\"export\");\n  };\n\n  const handleOpenChange = (isOpen: boolean) => {\n    $publishDialog.set(isOpen ? \"publish\" : \"none\");\n  };\n\n  return (\n    <Popover\n      modal\n      open={publishDialog !== \"none\"}\n      onOpenChange={handleOpenChange}\n    >\n      <Tooltip\n        side=\"bottom\"\n        content={tooltipContent ?? \"Publish to Webstudio Cloud\"}\n        sideOffset={Number.parseFloat(rawTheme.spacing[5])}\n      >\n        <PopoverTrigger asChild>\n          <Button\n            type=\"button\"\n            disabled={isPublishEnabled === false}\n            color=\"positive\"\n          >\n            Publish\n          </Button>\n        </PopoverTrigger>\n      </Tooltip>\n\n      <PopoverContent\n        sideOffset={Number.parseFloat(rawTheme.spacing[8])}\n        css={{\n          width: theme.spacing[33],\n          maxWidth: theme.spacing[33],\n          marginRight: theme.spacing[3],\n        }}\n      >\n        {publishDialog === \"export\" && (\n          <>\n            <PopoverTitle>Export</PopoverTitle>\n            <ExportContent projectId={projectId} />\n          </>\n        )}\n\n        {publishDialog === \"publish\" && (\n          <>\n            <PopoverTitle\n              suffix={\n                <PopoverTitleActions>\n                  <IconButton\n                    type=\"button\"\n                    onClick={() => {\n                      $openProjectSettings.set(\"publish\");\n                    }}\n                  >\n                    <GearIcon />\n                  </IconButton>\n                  <PopoverClose />\n                </PopoverTitleActions>\n              }\n            >\n              Publish\n            </PopoverTitle>\n            <Content projectId={projectId} onExportClick={handleExportClick} />\n          </>\n        )}\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/safe-mode.tsx",
    "content": "import { useState } from \"react\";\nimport { ShieldIcon } from \"@webstudio-is/icons\";\nimport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  theme,\n  ToolbarButton,\n  Button,\n  Text,\n  Flex,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport { builderApi } from \"~/shared/builder-api\";\n\nexport const SafeModeButton = () => {\n  const [open, setOpen] = useState(false);\n\n  if (!builderApi.isSafeMode()) {\n    return;\n  }\n\n  const handleExitSafeMode = () => {\n    const url = new URL(window.location.href);\n    url.searchParams.delete(\"safemode\");\n    window.location.href = url.href;\n  };\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <ToolbarButton variant=\"subtle\" tabIndex={0}>\n          <ShieldIcon stroke={rawTheme.colors.foregroundDestructive} />\n        </ToolbarButton>\n      </PopoverTrigger>\n      <PopoverContent>\n        <Flex\n          direction=\"column\"\n          gap=\"2\"\n          css={{\n            padding: theme.panel.padding,\n            width: theme.spacing[30],\n          }}\n        >\n          <Text variant=\"regularBold\">Safe mode active</Text>\n          <Text>\n            Safe mode prevents all external JavaScript from executing. HTML\n            embeds will not run scripts even if \"Run scripts on canvas\" is\n            enabled.\n          </Text>\n          <Button color=\"destructive\" onClick={handleExitSafeMode}>\n            Exit safe mode\n          </Button>\n        </Flex>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/boolean.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { Grid, Switch, theme } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  $selectedInstanceScope,\n  updateExpressionValue,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const BooleanControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"boolean\">) => {\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <Grid\n      css={{\n        gridTemplateColumns: `1fr max-content`,\n        minHeight: theme.spacing[13],\n        justifyItems: \"start\",\n      }}\n      align=\"center\"\n      gap=\"2\"\n    >\n      <PropertyLabel name={propName} readOnly={overwritable === false} />\n      <BindingControl>\n        <Switch\n          disabled={overwritable === false}\n          checked={Boolean(computedValue ?? false)}\n          onCheckedChange={(value) => {\n            if (prop?.type === \"expression\") {\n              updateExpressionValue(prop.value, value);\n            } else {\n              onChange({ type: \"boolean\", value });\n            }\n          }}\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => validatePrimitiveValue(value, label)}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({ type: \"boolean\", value: Boolean(evaluatedValue) })\n          }\n        />\n      </BindingControl>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/check.tsx",
    "content": "import { useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { Checkbox, CheckboxAndLabel } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  VerticalLayout,\n  Label,\n  $selectedInstanceScope,\n  updateExpressionValue,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nconst add = (array: string[], item: string) => {\n  if (array.includes(item)) {\n    return array;\n  }\n  return [...array, item];\n};\n\nconst remove = (array: string[], item: string) => {\n  if (array.includes(item) === false) {\n    return array;\n  }\n  return array.filter((arrayItem) => arrayItem !== item);\n};\n\nexport const CheckControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"check\" | \"inline-check\" | \"multi-select\">) => {\n  const value = Array.isArray(computedValue)\n    ? computedValue.map((item) => String(item))\n    : [];\n\n  // making sure that the current value is in the list of options\n  const options = Array.from(new Set([...meta.options, ...value]));\n\n  const id = useId();\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <VerticalLayout\n      label={\n        <PropertyLabel name={propName} readOnly={overwritable === false} />\n      }\n    >\n      <BindingControl>\n        {options.map((option) => (\n          <CheckboxAndLabel key={option}>\n            <Checkbox\n              disabled={overwritable === false}\n              checked={value.includes(option)}\n              onCheckedChange={(checked) => {\n                const newValue = checked\n                  ? add(value, option)\n                  : remove(value, option);\n                if (prop?.type === \"expression\") {\n                  updateExpressionValue(prop.value, newValue);\n                } else {\n                  onChange({ type: \"string[]\", value: newValue });\n                }\n              }}\n              id={`${id}:${option}`}\n            />\n            <Label htmlFor={`${id}:${option}`}>{option}</Label>\n          </CheckboxAndLabel>\n        ))}\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => {\n            const valid =\n              Array.isArray(value) &&\n              value.every((item) => typeof item === \"string\");\n            if (value !== undefined && valid === false) {\n              return `${label} expects an array of strings`;\n            }\n          }}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({\n              type: \"string[]\",\n              value: Array.isArray(evaluatedValue)\n                ? evaluatedValue.map((item) => String(item))\n                : [],\n            })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/code.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { useState } from \"react\";\nimport {\n  Button,\n  DialogClose,\n  DialogMaximize,\n  DialogTitle,\n  DialogTitleActions,\n  Flex,\n  SmallIconButton,\n  Text,\n  Tooltip,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport { CodeEditor } from \"~/shared/code-editor\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  useLocalValue,\n  type ControlProps,\n  VerticalLayout,\n  updateExpressionValue,\n  $selectedInstanceScope,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nconst ErrorInfo = ({\n  error,\n  onAutoFix,\n}: {\n  error?: Error;\n  onAutoFix: () => void;\n}) => {\n  if (error === undefined) {\n    return;\n  }\n  const errorContent = error.expected ? (\n    <Flex direction=\"column\" gap=\"2\" css={{ width: theme.spacing[28] }}>\n      <Text>{error.message} Do you want us to fix it?</Text>\n      <Button\n        color=\"neutral-destructive\"\n        onClick={() => {\n          onAutoFix();\n        }}\n      >\n        Fix automatically\n      </Button>\n    </Flex>\n  ) : (\n    <Flex direction=\"column\" gap=\"2\" css={{ width: theme.spacing[28] }}>\n      <Text>{error.message}</Text>\n    </Flex>\n  );\n\n  return (\n    <Tooltip content={errorContent} delayDuration={0}>\n      <SmallIconButton\n        icon={<InfoCircleIcon color={rawTheme.colors.foregroundDestructive} />}\n      />\n    </Tooltip>\n  );\n};\n\ntype Error = { message: string; value: string; expected?: string };\n\n/**\n * Use DOMParser in xml mode to parse potential svg\n */\nconst parseSvg = (value: string) => {\n  const doc = new DOMParser().parseFromString(value, \"application/xml\");\n  const errorNode = doc.querySelector(\"parsererror\");\n  if (errorNode) {\n    return \"\";\n  }\n  return doc.documentElement.outerHTML;\n};\n\nconst parseHtml = (value: string) => {\n  const div = document.createElement(\"div\");\n  div.innerHTML = value;\n  return div.innerHTML;\n};\n\n// The problem is to identify broken HTML and because browser is flexible and always tries to fix it we never\n// know if something is actually broken.\n// 1. Parse potential SVG with XML parser and serialize\n// 2. Compare the original SVG with resulting value\n// 3. Parse the HTML using DOM parser and serialize\n// 4. Compare the original HTML with resulting value\n// 5. We try to minimize the amount of false positives by removing\n//    - different amount of whitespace\n//    - unifying `boolean=\"\"` is the same as `boolean`\n//    - xmlns attirbute which is always reordered first\nconst validateHtml = (value: string): Error | undefined => {\n  const maxChars = 50_000;\n  if (value.length > maxChars) {\n    return {\n      message: `The HTML Embed code exceeds ${maxChars} character limit.`,\n      value,\n      expected: \"\",\n    };\n  }\n  const clean = (value: string) => {\n    return (\n      value\n        // Compare without whitespace to avoid false positives\n        .replaceAll(/\\s/g, \"\")\n        // normalize boolean attributes by turning `boolean=\"\"` into `boolean`\n        .replaceAll('=\"\"', \"\")\n        // namespace attribute is always reordered first\n        .replaceAll('xmlns=\"http://www.w3.org/2000/svg\"', \"\")\n    );\n  };\n  // in many cases svg is valid xml so serialize in xml mode first\n  // to avoid false positive of auto closing svg tags, for example\n  // <path /> -> <path></path>\n  const xml = parseSvg(value);\n  if (clean(xml) === clean(value)) {\n    return;\n  }\n  const html = parseHtml(value);\n  if (clean(html) === clean(value)) {\n    return;\n  }\n  return {\n    message: \"Entered HTML has a validation error.\",\n    value,\n    expected: html ?? \"\",\n  };\n};\n\nexport const CodeControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"code\"> | ControlProps<\"codetext\">) => {\n  const [error, setError] = useState<Error>();\n  const metaOverride = {\n    ...meta,\n    control: \"text\" as const,\n  };\n  const lang = meta.control === \"code\" ? meta.language : undefined;\n  const localValue = useLocalValue(String(computedValue ?? \"\"), (value) => {\n    if (lang === \"html\") {\n      const error = validateHtml(value);\n      setError(error);\n\n      if (error) {\n        return;\n      }\n    }\n\n    if (prop?.type === \"expression\") {\n      updateExpressionValue(prop.value, value);\n    } else {\n      onChange({ type: \"string\", value });\n    }\n  });\n  const label = humanizeAttribute(metaOverride.label || propName);\n\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  const errorInfo = (\n    <ErrorInfo\n      error={error}\n      onAutoFix={() => {\n        if (error?.expected) {\n          setError(undefined);\n          localValue.set(error.expected);\n        }\n      }}\n    />\n  );\n\n  return (\n    <VerticalLayout\n      label={\n        <Flex gap=\"1\" align=\"center\">\n          <PropertyLabel name={propName} readOnly={overwritable === false} />\n          {errorInfo}\n        </Flex>\n      }\n    >\n      <BindingControl>\n        <CodeEditor\n          lang={lang}\n          title={\n            <DialogTitle\n              suffix={\n                <DialogTitleActions>\n                  <DialogMaximize />\n                  <DialogClose />\n                </DialogTitleActions>\n              }\n            >\n              <Flex gap=\"1\" align=\"center\">\n                <Text variant=\"labels\">Code editor</Text>\n                {errorInfo}\n              </Flex>\n            </DialogTitle>\n          }\n          readOnly={overwritable === false}\n          invalid={error !== undefined}\n          value={localValue.value}\n          onChange={(value) => {\n            setError(undefined);\n            localValue.set(value);\n          }}\n          onChangeComplete={localValue.save}\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => validatePrimitiveValue(value, label)}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({ type: \"string\", value: String(evaluatedValue) })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/combined.tsx",
    "content": "import { TextControl } from \"./text\";\nimport { CodeControl } from \"./code\";\nimport { NumberControl } from \"./number\";\nimport { CheckControl } from \"./check\";\nimport { RadioControl } from \"./radio\";\nimport { SelectControl } from \"./select\";\nimport { BooleanControl } from \"./boolean\";\nimport { FileControl } from \"./file\";\nimport { UrlControl } from \"./url\";\nimport type { ControlProps } from \"../shared\";\nimport { JsonControl } from \"./json\";\nimport { TextContent } from \"./text-content\";\nimport { ResourceControl } from \"./resource-control\";\nimport { TagControl } from \"./tag-control\";\n\nexport const renderControl = ({\n  meta,\n  prop,\n  key,\n  ...rest\n}: ControlProps<string> & { key?: string }) => {\n  const computed = rest.computedValue;\n\n  if (meta.control === \"textContent\") {\n    return <TextContent key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"tag\") {\n    return <TagControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  // never render parameter props\n  if (prop?.type === \"parameter\") {\n    return;\n  }\n\n  // @todo remove once ui for action is implemented\n  if (prop?.type === \"action\") {\n    return;\n  }\n\n  if (meta.control === \"action\") {\n    return;\n  }\n\n  if (meta.control === \"json\") {\n    return <JsonControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"text\") {\n    return <TextControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"resource\") {\n    return <ResourceControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"code\") {\n    return <CodeControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"codetext\") {\n    return <CodeControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"color\") {\n    return (\n      <TextControl\n        key={key}\n        meta={{ ...meta, control: \"text\" }}\n        prop={prop}\n        {...rest}\n      />\n    );\n  }\n\n  if (meta.control === \"number\") {\n    return <NumberControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"boolean\") {\n    return <BooleanControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (\n    meta.control === \"check\" ||\n    meta.control === \"inline-check\" ||\n    meta.control === \"multi-select\"\n  ) {\n    return <CheckControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"radio\" || meta.control === \"inline-radio\") {\n    return <RadioControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"select\") {\n    return <SelectControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"file\") {\n    return <FileControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  if (meta.control === \"url\") {\n    return <UrlControl key={key} meta={meta} prop={prop} {...rest} />;\n  }\n\n  // Type in meta can be changed at some point without updating props in DB that are still using the old type\n  // In this case meta and prop will mismatch, but we try to guess a matching control based just on the prop type\n  if (prop) {\n    if (prop.type === \"asset\") {\n      return (\n        <FileControl\n          key={key}\n          meta={{\n            ...meta,\n            defaultValue: undefined,\n            control: \"file\",\n            type: \"string\",\n          }}\n          prop={prop}\n          {...rest}\n        />\n      );\n    }\n\n    if (prop.type === \"page\") {\n      return (\n        <UrlControl\n          key={key}\n          meta={{\n            ...meta,\n            defaultValue: undefined,\n            control: \"url\",\n            type: \"string\",\n          }}\n          prop={prop}\n          {...rest}\n        />\n      );\n    }\n\n    if (\n      prop.type === \"string\" ||\n      (prop.type === \"expression\" && typeof computed === \"string\")\n    ) {\n      return (\n        <TextControl\n          key={key}\n          meta={{\n            ...meta,\n            defaultValue: undefined,\n            control: \"text\",\n            type: \"string\",\n          }}\n          prop={prop}\n          {...rest}\n        />\n      );\n    }\n\n    if (\n      prop.type === \"number\" ||\n      (prop.type === \"expression\" && typeof computed === \"number\")\n    ) {\n      return (\n        <NumberControl\n          key={key}\n          meta={{\n            ...meta,\n            defaultValue: undefined,\n            control: \"number\",\n            type: \"number\",\n          }}\n          prop={prop}\n          {...rest}\n        />\n      );\n    }\n\n    if (\n      prop.type === \"boolean\" ||\n      (prop.type === \"expression\" && typeof computed === \"boolean\")\n    ) {\n      return (\n        <BooleanControl\n          key={key}\n          meta={{\n            ...meta,\n            defaultValue: undefined,\n            control: \"boolean\",\n            type: \"boolean\",\n          }}\n          prop={prop}\n          {...rest}\n        />\n      );\n    }\n\n    if (prop.type === \"json\" || prop.type === \"expression\") {\n      return (\n        <JsonControl\n          key={key}\n          meta={{\n            ...meta,\n            defaultValue: undefined,\n            control: \"json\",\n            type: \"json\",\n          }}\n          prop={prop}\n          {...rest}\n        />\n      );\n    }\n\n    if (prop.type === \"string[]\") {\n      throw new Error(\n        `Cannot render a fallback control for prop \"${rest.propName}\" with type string[], because we don't know the available options for a multiselect control`\n      );\n    }\n\n    if (prop.type === \"resource\") {\n      throw new Error(\n        `Cannot render a fallback control for prop \"${rest.propName}\" with type resource`\n      );\n    }\n\n    if (prop.type === \"animationAction\") {\n      throw new Error(\n        `Cannot render a fallback control for prop \"${rest.propName}\" with type animationAction`\n      );\n    }\n\n    prop satisfies never;\n  }\n\n  throw new Error(\n    `Unsupported control type \"${meta.control}\" for prop \"${rest.propName}\"`\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/controls.stories.tsx",
    "content": "import { Flex, StorySection, Text, theme } from \"@webstudio-is/design-system\";\nimport { BooleanControl } from \"./boolean\";\nimport { NumberControl } from \"./number\";\nimport { TextControl } from \"./text\";\nimport { SelectControl } from \"./select\";\nimport { CheckControl } from \"./check\";\nimport { RadioControl } from \"./radio\";\n\nexport default {\n  title: \"Builder/Settings panel/Controls\",\n};\n\nconst Section = ({\n  label,\n  children,\n}: {\n  label: string;\n  children: React.ReactNode;\n}) => (\n  <Flex direction=\"column\" gap=\"2\">\n    <Text\n      variant=\"labels\"\n      css={{\n        paddingBottom: theme.spacing[3],\n        borderBottom: `1px solid ${theme.colors.borderMain}`,\n      }}\n    >\n      {label}\n    </Text>\n    {children}\n  </Flex>\n);\n\nconst noop = () => {};\n\nexport const Controls = () => (\n  <StorySection title=\"Controls\">\n    <Flex\n      direction=\"column\"\n      gap=\"5\"\n      css={{ width: theme.sizes.sidebarWidth, padding: theme.spacing[5] }}\n    >\n      <Section label=\"Boolean\">\n        <BooleanControl\n          instanceId=\"inst-1\"\n          meta={{\n            type: \"boolean\",\n            control: \"boolean\",\n            required: false,\n            defaultValue: false,\n          }}\n          prop={undefined}\n          propName=\"disabled\"\n          computedValue={false}\n          onChange={noop}\n        />\n        <BooleanControl\n          instanceId=\"inst-1\"\n          meta={{\n            type: \"boolean\",\n            control: \"boolean\",\n            required: false,\n            defaultValue: true,\n          }}\n          prop={{\n            id: \"p-1\",\n            instanceId: \"inst-1\",\n            name: \"checked\",\n            type: \"boolean\",\n            value: true,\n          }}\n          propName=\"checked\"\n          computedValue={true}\n          onChange={noop}\n        />\n      </Section>\n\n      <Section label=\"Number\">\n        <NumberControl\n          instanceId=\"inst-1\"\n          meta={{ type: \"number\", control: \"number\", required: false }}\n          prop={undefined}\n          propName=\"tabIndex\"\n          computedValue={0}\n          onChange={noop}\n        />\n        <NumberControl\n          instanceId=\"inst-1\"\n          meta={{ type: \"number\", control: \"number\", required: false }}\n          prop={{\n            id: \"p-2\",\n            instanceId: \"inst-1\",\n            name: \"maxLength\",\n            type: \"number\",\n            value: 100,\n          }}\n          propName=\"maxLength\"\n          computedValue={100}\n          onChange={noop}\n        />\n      </Section>\n\n      <Section label=\"Text\">\n        <TextControl\n          instanceId=\"inst-1\"\n          meta={{ type: \"string\", control: \"text\", required: false, rows: 1 }}\n          prop={undefined}\n          propName=\"placeholder\"\n          computedValue=\"\"\n          onChange={noop}\n        />\n        <TextControl\n          instanceId=\"inst-1\"\n          meta={{ type: \"string\", control: \"text\", required: false, rows: 3 }}\n          prop={{\n            id: \"p-3\",\n            instanceId: \"inst-1\",\n            name: \"alt\",\n            type: \"string\",\n            value: \"A scenic mountain view at sunset\",\n          }}\n          propName=\"alt\"\n          computedValue=\"A scenic mountain view at sunset\"\n          onChange={noop}\n        />\n      </Section>\n\n      <Section label=\"Select\">\n        <SelectControl\n          instanceId=\"inst-1\"\n          meta={{\n            type: \"string\",\n            control: \"select\",\n            required: false,\n            options: [\"auto\", \"eager\", \"lazy\"],\n          }}\n          prop={undefined}\n          propName=\"loading\"\n          computedValue=\"lazy\"\n          onChange={noop}\n        />\n      </Section>\n\n      <Section label=\"Check (multi-select)\">\n        <CheckControl\n          instanceId=\"inst-1\"\n          meta={{\n            type: \"string[]\",\n            control: \"check\",\n            required: false,\n            options: [\"nofollow\", \"noopener\", \"noreferrer\"],\n          }}\n          prop={undefined}\n          propName=\"rel\"\n          computedValue={[\"noopener\", \"noreferrer\"]}\n          onChange={noop}\n        />\n      </Section>\n\n      <Section label=\"Radio\">\n        <RadioControl\n          instanceId=\"inst-1\"\n          meta={{\n            type: \"string\",\n            control: \"radio\",\n            required: false,\n            options: [\"_self\", \"_blank\", \"_parent\"],\n          }}\n          prop={undefined}\n          propName=\"target\"\n          computedValue=\"_self\"\n          onChange={noop}\n        />\n      </Section>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/file.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { useId } from \"react\";\nimport { Flex, InputField, theme } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  VerticalLayout,\n  useLocalValue,\n  updateExpressionValue,\n  $selectedInstanceScope,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { SelectAsset } from \"./select-asset\";\nimport { PropertyLabel } from \"../property-label\";\n\nconst UrlInput = ({\n  id,\n  readOnly,\n  localValue,\n}: {\n  id: string;\n  readOnly: boolean;\n  localValue: ReturnType<typeof useLocalValue<undefined | string>>;\n}) => (\n  <InputField\n    id={id}\n    disabled={readOnly}\n    value={localValue.value ?? \"\"}\n    placeholder=\"https://www.url.com\"\n    onChange={(event) => localValue.set(event.target.value)}\n    onBlur={localValue.save}\n    onKeyDown={(event) => {\n      if (event.key === \"Enter\") {\n        localValue.save();\n      }\n    }}\n    css={{ width: \"100%\" }}\n  />\n);\n\nexport const FileControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"file\">) => {\n  const id = useId();\n\n  const localStringValue = useLocalValue(\n    // use undefined for asset type to not delete\n    // when url is reset by asset selector\n    prop?.type === \"string\" || prop?.type === \"expression\"\n      ? String(computedValue)\n      : undefined,\n    (value) => {\n      if (value === undefined) {\n        return;\n      } else if (prop?.type === \"expression\") {\n        updateExpressionValue(prop.value, value);\n      } else {\n        onChange({ type: \"string\", value });\n      }\n    }\n  );\n\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <VerticalLayout label={<PropertyLabel name={propName} />}>\n      <Flex css={{ gap: theme.spacing[3] }} direction=\"column\" justify=\"center\">\n        <BindingControl>\n          <UrlInput\n            id={id}\n            readOnly={overwritable === false}\n            localValue={localStringValue}\n          />\n          <BindingPopover\n            scope={scope}\n            aliases={aliases}\n            validate={(value) => validatePrimitiveValue(value, label)}\n            variant={variant}\n            value={expression}\n            onChange={(newExpression) =>\n              onChange({ type: \"expression\", value: newExpression })\n            }\n            onRemove={(evaluatedValue) =>\n              onChange({ type: \"string\", value: String(evaluatedValue) })\n            }\n          />\n        </BindingControl>\n        <SelectAsset\n          prop={prop?.type === \"asset\" ? prop : undefined}\n          accept={meta.accept}\n          onChange={onChange}\n        />\n      </Flex>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/json.tsx",
    "content": "import { useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { isLiteralExpression } from \"@webstudio-is/sdk\";\nimport {\n  type ControlProps,\n  useLocalValue,\n  VerticalLayout,\n  updateExpressionValue,\n  $selectedInstanceScope,\n  useBindingState,\n} from \"../shared\";\nimport {\n  ExpressionEditor,\n  formatValue,\n} from \"~/builder/shared/expression-editor\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const JsonControl = ({\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"json\">) => {\n  const [error, setError] = useState<boolean>(false);\n  const valueString = formatValue(computedValue ?? \"\");\n  const localValue = useLocalValue(valueString, (value) => {\n    const isLiteral = isLiteralExpression(value);\n    setError(isLiteral ? false : true);\n    // prevent executing expressions which depends on global variables\n    if (isLiteral === false) {\n      return;\n    }\n    try {\n      // wrap into parens to treat object expression as value instead of block\n      const parsedValue = eval(`(${value})`);\n      if (prop?.type === \"expression\") {\n        updateExpressionValue(prop.value, parsedValue);\n      } else {\n        onChange({ type: \"json\", value: parsedValue });\n      }\n    } catch {\n      // empty block\n    }\n  });\n\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression = prop?.type === \"expression\" ? prop.value : valueString;\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <VerticalLayout\n      label={\n        <PropertyLabel name={propName} readOnly={overwritable === false} />\n      }\n    >\n      <BindingControl>\n        <ExpressionEditor\n          color={error ? \"error\" : undefined}\n          readOnly={overwritable === false}\n          value={localValue.value}\n          onChange={localValue.set}\n          onChangeComplete={localValue.save}\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({ type: \"json\", value: evaluatedValue })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/number.tsx",
    "content": "import { useId, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { InputField } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  useLocalValue,\n  ResponsiveLayout,\n  updateExpressionValue,\n  $selectedInstanceScope,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const NumberControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"number\">) => {\n  const id = useId();\n\n  const [isInvalid, setIsInvalid] = useState(false);\n  const number = Number(computedValue);\n  const localValue = useLocalValue(\n    Number.isNaN(number) ? \"\" : number,\n    (value) => {\n      if (typeof value === \"number\") {\n        if (prop?.type === \"expression\") {\n          updateExpressionValue(prop.value, value);\n        } else {\n          onChange({ type: \"number\", value });\n        }\n      }\n      if (value === \"\") {\n        setIsInvalid(true);\n      }\n    }\n  );\n\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <ResponsiveLayout\n      label={\n        <PropertyLabel name={propName} readOnly={overwritable === false} />\n      }\n    >\n      <BindingControl>\n        <InputField\n          id={id}\n          disabled={overwritable === false}\n          type=\"number\"\n          value={localValue.value}\n          color={isInvalid ? \"error\" : undefined}\n          onChange={({ target: { valueAsNumber, value } }) => {\n            localValue.set(Number.isNaN(valueAsNumber) ? value : valueAsNumber);\n            setIsInvalid(false);\n          }}\n          onBlur={localValue.save}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\") {\n              localValue.save();\n            }\n          }}\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => {\n            if (value !== undefined && typeof value !== \"number\") {\n              return `${label} expects a number value`;\n            }\n          }}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) => {\n            const number = Number(evaluatedValue);\n            onChange({\n              type: \"number\",\n              value: Number.isNaN(number) ? 0 : number,\n            });\n          }}\n        />\n      </BindingControl>\n    </ResponsiveLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/radio.tsx",
    "content": "import { useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { RadioGroup, Radio, RadioAndLabel } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  VerticalLayout,\n  Label,\n  $selectedInstanceScope,\n  updateExpressionValue,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const RadioControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"radio\" | \"inline-radio\">) => {\n  const value = computedValue === undefined ? undefined : String(computedValue);\n  // making sure that the current value is in the list of options\n  const options =\n    value === undefined || meta.options.includes(value)\n      ? meta.options\n      : [value, ...meta.options];\n\n  const id = useId();\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <VerticalLayout\n      label={\n        <PropertyLabel name={propName} readOnly={overwritable === false} />\n      }\n    >\n      <BindingControl>\n        <RadioGroup\n          disabled={overwritable === false}\n          name=\"value\"\n          value={value}\n          onValueChange={(value) => {\n            if (prop?.type === \"expression\") {\n              updateExpressionValue(prop.value, value);\n            } else {\n              onChange({ type: \"string\", value });\n            }\n          }}\n        >\n          {options.map((value) => (\n            <RadioAndLabel key={value}>\n              <Radio value={value} id={`${id}:${value}`} />\n              <Label htmlFor={`${id}:${value}`}>{value}</Label>\n            </RadioAndLabel>\n          ))}\n        </RadioGroup>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => {\n            if (\n              value !== undefined &&\n              meta.options.includes(String(value)) === false\n            ) {\n              const formatter = new Intl.ListFormat(undefined, {\n                type: \"disjunction\",\n              });\n              const options = formatter.format(meta.options);\n              return `${label} expects one of ${options}`;\n            }\n          }}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({ type: \"string\", value: String(evaluatedValue) })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/resource-control.tsx",
    "content": "import { nanoid } from \"nanoid\";\nimport { computed } from \"nanostores\";\nimport {\n  forwardRef,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n} from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { isFeatureEnabled } from \"@webstudio-is/feature-flags\";\nimport { GearIcon } from \"@webstudio-is/icons\";\nimport {\n  EnhancedTooltip,\n  Flex,\n  FloatingPanel,\n  InputField,\n  NestedInputButton,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { isLiteralExpression, Resource, type Prop } from \"@webstudio-is/sdk\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n  type BindingVariant,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  $dataSources,\n  $props,\n  $resources,\n  $variableValuesByInstanceSelector,\n} from \"~/shared/nano-states\";\nimport { computeExpression } from \"~/shared/data-variables\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport {\n  $selectedInstance,\n  $selectedInstanceKeyWithRoot,\n  $selectedPage,\n} from \"~/shared/awareness\";\nimport {\n  UrlField,\n  MethodField,\n  Headers,\n  parseResource,\n  getResourceScopeForInstance,\n} from \"../resource-panel\";\nimport { type ControlProps, useLocalValue, VerticalLayout } from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\n// dirty, dirty hack\nconst areAllFormErrorsVisible = (form: null | HTMLFormElement) => {\n  if (form === null) {\n    return false;\n  }\n  // check all errors in form fields are visible\n  for (const element of form.elements) {\n    if (\n      element instanceof HTMLInputElement ||\n      element instanceof HTMLTextAreaElement\n    ) {\n      // field is invalid and the error is not visible\n      if (\n        element.validity.valid === false &&\n        // rely on data-color=error convention in webstudio design system\n        element.getAttribute(\"data-color\") !== \"error\"\n      ) {\n        return false;\n      }\n    }\n  }\n  return true;\n};\n\nconst ResourceButton = forwardRef<\n  HTMLButtonElement,\n  ComponentProps<typeof NestedInputButton>\n>((props, ref) => {\n  return (\n    <EnhancedTooltip content=\"Edit Resource\">\n      <NestedInputButton {...props} ref={ref} aria-label=\"Edit Resource\">\n        <GearIcon />\n      </NestedInputButton>\n    </EnhancedTooltip>\n  );\n});\nResourceButton.displayName = \"ResourceButton\";\n\nconst $selectedInstanceResourceScope = computed(\n  [\n    $selectedPage,\n    $selectedInstanceKeyWithRoot,\n    $variableValuesByInstanceSelector,\n    $dataSources,\n  ],\n  (page, instanceKey, variableValuesByInstanceSelector, dataSources) => {\n    return getResourceScopeForInstance({\n      page,\n      instanceKey,\n      dataSources,\n      variableValuesByInstanceSelector,\n    });\n  }\n);\n\nconst ResourceForm = ({ resource }: { resource: Resource }) => {\n  const { scope, aliases } = useStore($selectedInstanceResourceScope);\n  const [url, setUrl] = useState(resource.url);\n  const [method, setMethod] = useState<Resource[\"method\"]>(resource.method);\n  const [headers, setHeaders] = useState<Resource[\"headers\"]>(resource.headers);\n  return (\n    <Flex\n      direction=\"column\"\n      css={{\n        width: theme.spacing[30],\n        overflow: \"hidden\",\n        gap: theme.spacing[9],\n        p: theme.spacing[9],\n      }}\n    >\n      <UrlField\n        scope={scope}\n        aliases={aliases}\n        value={url}\n        onChange={setUrl}\n        onCurlPaste={(curl) => {\n          // update all feilds when curl is paste into url field\n          setUrl(JSON.stringify(curl.url));\n          setMethod(curl.method);\n          setHeaders(\n            curl.headers.map((header) => ({\n              name: header.name,\n              value: JSON.stringify(header.value),\n            }))\n          );\n        }}\n      />\n      <MethodField value={method} onChange={setMethod} />\n      <Headers\n        scope={scope}\n        aliases={aliases}\n        headers={headers}\n        onChange={setHeaders}\n      />\n    </Flex>\n  );\n};\n\nconst ResourceControlPanel = ({\n  resource,\n  propName,\n  onChange,\n}: {\n  resource: Resource;\n  propName: string;\n  onChange: (resource: Resource) => void;\n}) => {\n  const [isResourceOpen, setIsResourceOpen] = useState(false);\n  const form = useRef<HTMLFormElement>(null);\n  return (\n    <FloatingPanel\n      title=\"Edit Resource\"\n      open={isResourceOpen}\n      onOpenChange={(isOpen) => {\n        if (isOpen) {\n          setIsResourceOpen(true);\n          return;\n        }\n        // attempt to save form on close\n        if (areAllFormErrorsVisible(form.current)) {\n          form.current?.requestSubmit();\n          setIsResourceOpen(false);\n        } else {\n          form.current?.checkValidity();\n          // prevent closing when not all errors are shown to user\n        }\n      }}\n      content={\n        <form\n          ref={form}\n          // ref={formRef}\n          noValidate={true}\n          // exclude from the flow\n          style={{ display: \"contents\" }}\n          onSubmit={(event) => {\n            event.preventDefault();\n            if (event.currentTarget.checkValidity()) {\n              const formData = new FormData(event.currentTarget);\n              const newResource = parseResource({\n                id: resource?.id ?? nanoid(),\n                name: resource?.name ?? propName,\n                formData,\n              });\n              onChange(newResource);\n            }\n          }}\n        >\n          {/* submit is not triggered when press enter on input without submit button */}\n          <button hidden></button>\n          <ResourceForm resource={resource} />\n        </form>\n      }\n    >\n      <ResourceButton />\n    </FloatingPanel>\n  );\n};\n\nconst $methodPropValue = computed(\n  [$selectedInstance, $props],\n  (instance, props): Resource[\"method\"] => {\n    for (const prop of props.values()) {\n      if (\n        prop.instanceId === instance?.id &&\n        prop.type === \"string\" &&\n        prop.name === \"method\"\n      ) {\n        const value = prop.value.toLowerCase();\n        if (\n          value === \"get\" ||\n          value === \"post\" ||\n          value === \"put\" ||\n          value === \"delete\"\n        ) {\n          return value;\n        }\n        break;\n      }\n    }\n    return \"post\";\n  }\n);\n\nexport const ResourceControl = ({\n  instanceId,\n  propName,\n  prop,\n}: ControlProps<\"resource\">) => {\n  const resources = useStore($resources);\n  const { variableValues, scope, aliases } = useStore(\n    $selectedInstanceResourceScope\n  );\n  const methodPropValue = useStore($methodPropValue);\n  let resource: undefined | Resource;\n  let urlExpression: string = JSON.stringify(\"\");\n  if (prop?.type === \"string\") {\n    urlExpression = JSON.stringify(prop.value);\n  }\n  if (prop?.type === \"expression\") {\n    urlExpression = prop.value;\n  }\n  if (prop?.type === \"resource\") {\n    resource = resources.get(prop.value);\n    if (resource) {\n      urlExpression = resource.url;\n    }\n  }\n  // create temporary resource\n  const resourceId = useMemo(() => resource?.id ?? nanoid(), [resource]);\n  resource ??= {\n    id: resourceId,\n    name: propName,\n    url: urlExpression,\n    method: methodPropValue,\n    headers: [{ name: \"Content-Type\", value: `\"application/json\"` }],\n  };\n\n  const updateResource = (newResource: Resource) => {\n    updateWebstudioData((data) => {\n      if (prop?.type === \"resource\") {\n        data.resources.set(newResource.id, newResource);\n      } else {\n        const newProp: Prop = {\n          id: prop?.id ?? nanoid(),\n          instanceId,\n          name: propName,\n          type: \"resource\",\n          value: newResource.id,\n        };\n        data.props.set(newProp.id, newProp);\n        data.resources.set(newResource.id, newResource);\n      }\n    });\n  };\n\n  const id = useId();\n  let variant: BindingVariant = \"bound\";\n  let readOnly = true;\n  if (isLiteralExpression(urlExpression)) {\n    variant = \"default\";\n    readOnly = false;\n  }\n  const localValue = useLocalValue(\n    String(computeExpression(resource.url, variableValues) ?? \"\"),\n    (value) => updateResource({ ...resource, url: JSON.stringify(value) })\n  );\n\n  return (\n    <VerticalLayout\n      label={<PropertyLabel name={propName} readOnly={readOnly} />}\n    >\n      <BindingControl>\n        <InputField\n          id={id}\n          disabled={readOnly}\n          value={localValue.value}\n          onChange={(event) => localValue.set(event.target.value)}\n          onBlur={localValue.save}\n          onSubmit={localValue.save}\n          suffix={\n            isFeatureEnabled(\"resourceProp\") && (\n              <ResourceControlPanel\n                resource={resource}\n                propName={propName}\n                onChange={updateResource}\n              />\n            )\n          }\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => validatePrimitiveValue(value, \"URL\")}\n          variant={variant}\n          value={urlExpression}\n          onChange={(newExpression) =>\n            updateResource({ ...resource, url: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            updateResource({\n              ...resource,\n              url: JSON.stringify(String(evaluatedValue)),\n            })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/select-asset.tsx",
    "content": "import { useMemo } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { Button, Flex, Text, FloatingPanel } from \"@webstudio-is/design-system\";\nimport type { Prop } from \"@webstudio-is/sdk\";\nimport { acceptToMimeCategories } from \"@webstudio-is/sdk\";\nimport { $assets } from \"~/shared/sync/data-stores\";\nimport { AssetManager } from \"~/builder/shared/asset-manager\";\nimport { type ControlProps } from \"../shared\";\nimport { formatAssetName } from \"~/builder/shared/assets/asset-utils\";\nimport { AssetUpload } from \"~/builder/shared/assets\";\n\n// tests whether we can use AssetManager for the given \"accept\" value\nconst isImageAccept = (accept?: string) => {\n  const acceptCategories = acceptToMimeCategories(accept || \"\");\n  return (\n    acceptCategories === \"*\" ||\n    (acceptCategories.size === 1 && acceptCategories.has(\"image\"))\n  );\n};\n\ntype AssetControlProps = ControlProps<unknown>;\n\ntype Props = {\n  accept?: string;\n  prop?: Extract<Prop, { type: \"asset\" }>;\n  onChange: AssetControlProps[\"onChange\"];\n};\n\nexport const SelectAsset = ({ prop, onChange, accept }: Props) => {\n  const $asset = useMemo(\n    () =>\n      computed($assets, (assets) =>\n        prop ? assets.get(prop.value) : undefined\n      ),\n    [prop]\n  );\n\n  const asset = useStore($asset);\n\n  if (isImageAccept(accept) === false) {\n    return <Text color=\"destructive\">Unsupported accept value: {accept}</Text>;\n  }\n\n  return (\n    <Flex gap={2} css={{ flex: 1 }} align=\"center\">\n      <FloatingPanel\n        title=\"Images\"\n        titleSuffix={<AssetUpload type=\"image\" accept={accept} />}\n        content={\n          <AssetManager\n            onChange={(assetId) => onChange({ type: \"asset\", value: assetId })}\n            accept={accept}\n          />\n        }\n      >\n        <Button color=\"neutral\" css={{ flex: 1 }}>\n          {asset ? formatAssetName(asset) : \"Choose source\"}\n        </Button>\n      </FloatingPanel>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/select.tsx",
    "content": "import { useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { Select } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  VerticalLayout,\n  $selectedInstanceScope,\n  updateExpressionValue,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const SelectControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"select\">) => {\n  const id = useId();\n\n  const value = computedValue === undefined ? undefined : String(computedValue);\n  // making sure that the current value is in the list of options\n  const options =\n    value === undefined || value.length === 0 || meta.options.includes(value)\n      ? meta.options\n      : [value, ...meta.options];\n\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <VerticalLayout\n      label={\n        <PropertyLabel name={propName} readOnly={overwritable === false} />\n      }\n    >\n      <BindingControl>\n        <Select\n          fullWidth\n          id={id}\n          disabled={overwritable === false}\n          value={value}\n          options={options}\n          onChange={(value) => {\n            if (prop?.type === \"expression\") {\n              updateExpressionValue(prop.value, value);\n            } else {\n              onChange({ type: \"string\", value });\n            }\n          }}\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => {\n            if (\n              value !== undefined &&\n              meta.options.includes(String(value)) === false\n            ) {\n              const formatter = new Intl.ListFormat(undefined, {\n                type: \"disjunction\",\n              });\n              const options = formatter.format(meta.options);\n              return `${label} expects one of ${options}`;\n            }\n          }}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({ type: \"string\", value: String(evaluatedValue) })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/tag-control.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { Box, Combobox, Select, theme } from \"@webstudio-is/design-system\";\nimport { elementsByTag } from \"@webstudio-is/html-data\";\nimport { tags } from \"@webstudio-is/sdk\";\nimport { $selectedInstance, $selectedInstancePath } from \"~/shared/awareness\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { isTreeSatisfyingContentModel } from \"~/shared/content-model\";\nimport {\n  $instances,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { type ControlProps, VerticalLayout } from \"../shared\";\nimport { FieldLabel } from \"../property-label\";\n\nconst $satisfyingTags = computed(\n  [$selectedInstancePath, $instances, $props, $registeredComponentMetas],\n  (instancePath, instances, props, metas) => {\n    const satisfyingTags: string[] = [];\n    if (instancePath === undefined) {\n      return satisfyingTags;\n    }\n    const [{ instance, instanceSelector }] = instancePath;\n    const newInstances = new Map(instances);\n    for (const tag of tags) {\n      newInstances.set(instance.id, { ...instance, tag });\n      const isSatisfying = isTreeSatisfyingContentModel({\n        instances: newInstances,\n        props,\n        metas,\n        instanceSelector,\n      });\n      if (isSatisfying) {\n        satisfyingTags.push(tag);\n      }\n    }\n    return satisfyingTags;\n  }\n);\n\nexport const TagControl = ({ meta, prop }: ControlProps<\"tag\">) => {\n  const instance = useStore($selectedInstance);\n  const propTag = prop?.type === \"string\" ? prop.value : undefined;\n  const instanceTag = instance?.tag;\n  const defaultTag = meta.options[0];\n  const computedTag = instanceTag ?? propTag ?? defaultTag;\n  let satisfyingTags = useStore($satisfyingTags);\n  // forbid changing tag on body element\n  if (computedTag === \"body\") {\n    satisfyingTags = [\"body\"];\n  }\n  const options = meta.options.filter((tag) => satisfyingTags.includes(tag));\n  const [value, setValue] = useState<undefined | string>();\n  const updateTag = (value: string) => {\n    if (instance === undefined) {\n      return;\n    }\n    const instanceId = instance.id;\n    updateWebstudioData((data) => {\n      // clean legacy <Box tag> and <Text tag>\n      if (prop) {\n        data.props.delete(prop.id);\n      }\n      const instance = data.instances.get(instanceId);\n      if (instance) {\n        instance.tag = value;\n      }\n    });\n  };\n  return (\n    <VerticalLayout\n      label={\n        <FieldLabel description=\"Use this property to change the HTML tag of this element to semantically structure and describe the content of a webpage. This can be important for accessibility tools and search engine optimization.\">\n          Tag\n        </FieldLabel>\n      }\n    >\n      {options.length > 10 ? (\n        <Combobox<string>\n          getItems={() => options}\n          itemToString={(item) => item ?? options[0]}\n          value={value ?? computedTag}\n          selectedItem={computedTag}\n          onChange={(value) => setValue(value ?? undefined)}\n          onItemSelect={(item) => {\n            updateTag(item);\n            setValue(undefined);\n          }}\n          getDescription={(item) => (\n            <Box css={{ width: theme.spacing[28] }}>\n              {elementsByTag[item ?? \"\"]?.description}\n            </Box>\n          )}\n        />\n      ) : (\n        <Select\n          fullWidth\n          value={computedTag}\n          options={options}\n          onChange={updateTag}\n          getDescription={(item) => (\n            <Box css={{ width: theme.spacing[28] }}>\n              {elementsByTag[item]?.description}\n            </Box>\n          )}\n        />\n      )}\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/text-content.tsx",
    "content": "import { useMemo } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { computed } from \"nanostores\";\nimport {\n  DialogClose,\n  DialogMaximize,\n  DialogTitle,\n  DialogTitleActions,\n  Flex,\n  rawTheme,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { AlertIcon } from \"@webstudio-is/icons\";\nimport { $instances } from \"~/shared/sync/data-stores\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n} from \"~/builder/shared/binding-popover\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { CodeEditor } from \"~/shared/code-editor\";\nimport {\n  type ControlProps,\n  useLocalValue,\n  VerticalLayout,\n  $selectedInstanceScope,\n  updateExpressionValue,\n  useBindingState,\n} from \"../shared\";\nimport { FieldLabel } from \"../property-label\";\n\nconst useInstance = (instanceId: Instance[\"id\"]) => {\n  const $store = useMemo(() => {\n    return computed($instances, (instances) => instances.get(instanceId));\n  }, [instanceId]);\n  return useStore($store);\n};\n\nconst updateChildren = (\n  instanceId: Instance[\"id\"],\n  type: \"text\" | \"expression\",\n  value: string\n) => {\n  updateWebstudioData((data) => {\n    const instance = data.instances.get(instanceId);\n    if (instance) {\n      instance.children = [{ type, value }];\n    }\n  });\n};\n\nexport const TextContent = ({\n  instanceId,\n  computedValue,\n}: ControlProps<\"textContent\">) => {\n  const instance = useInstance(instanceId);\n  const hasChildren = (instance?.children.length ?? 0) > 0;\n  // text content control is rendered only when empty or single child are present\n  const child = instance?.children?.[0] ?? { type: \"text\", value: \"\" };\n  const localValue = useLocalValue(String(computedValue ?? \"\"), (value) => {\n    if (child.type === \"expression\") {\n      updateExpressionValue(child.value, value);\n    } else {\n      updateChildren(instanceId, \"text\", value);\n    }\n  });\n\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  let expression: undefined | string;\n  if (child.type === \"text\") {\n    expression = JSON.stringify(child.value);\n  }\n  if (child.type === \"expression\") {\n    expression = child.value;\n  }\n\n  const { overwritable, variant } = useBindingState(\n    child.type === \"expression\" ? child.value : undefined\n  );\n\n  return (\n    <VerticalLayout\n      label={\n        <FieldLabel\n          description={\n            <>\n              Plain text content that can be bound to either a variable or a\n              resource value.\n              {overwritable === false && (\n                <Flex gap=\"1\">\n                  <AlertIcon\n                    color={rawTheme.colors.backgroundAlertMain}\n                    style={{ flexShrink: 0 }}\n                  />\n                  <Text>\n                    The value is controlled by an expression and cannot be\n                    changed.\n                  </Text>\n                </Flex>\n              )}\n            </>\n          }\n          resettable={hasChildren}\n          onReset={() => {\n            updateWebstudioData((data) => {\n              const instance = data.instances.get(instanceId);\n              if (instance) {\n                instance.children = [];\n              }\n            });\n          }}\n        >\n          Text Content\n        </FieldLabel>\n      }\n    >\n      <BindingControl>\n        <CodeEditor\n          title={\n            <DialogTitle\n              suffix={\n                <DialogTitleActions>\n                  <DialogMaximize />\n                  <DialogClose />\n                </DialogTitleActions>\n              }\n            >\n              <Text variant=\"labels\">Text content</Text>\n            </DialogTitle>\n          }\n          size=\"small\"\n          readOnly={overwritable === false}\n          value={localValue.value}\n          onChange={localValue.set}\n          onChangeComplete={localValue.save}\n        />\n        {expression !== undefined && (\n          <BindingPopover\n            scope={scope}\n            aliases={aliases}\n            validate={(value) => validatePrimitiveValue(value, \"Text Content\")}\n            variant={variant}\n            value={expression}\n            onChange={(newExpression) => {\n              updateChildren(instanceId, \"expression\", newExpression);\n            }}\n            onRemove={(evaluatedValue) =>\n              updateChildren(instanceId, \"text\", String(evaluatedValue))\n            }\n          />\n        )}\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/text.tsx",
    "content": "import { useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { TextArea } from \"@webstudio-is/design-system\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  useLocalValue,\n  ResponsiveLayout,\n  updateExpressionValue,\n  $selectedInstanceScope,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const TextControl = ({\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: ControlProps<\"text\">) => {\n  const localValue = useLocalValue(String(computedValue ?? \"\"), (value) => {\n    if (prop?.type === \"expression\") {\n      updateExpressionValue(prop.value, value);\n    } else {\n      onChange({ type: \"string\", value });\n    }\n  });\n  const id = useId();\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  const input = (\n    <BindingControl>\n      <TextArea\n        id={id}\n        disabled={overwritable === false}\n        autoGrow\n        value={localValue.value}\n        rows={meta.rows ?? 1}\n        // Set maxRows to 3 when meta.rows is undefined or equal to 1, otherwise set it to rows * 2\n        maxRows={Math.max(2 * (meta.rows ?? 1), 3)}\n        onChange={localValue.set}\n        onBlur={localValue.save}\n        onSubmit={localValue.save}\n      />\n\n      <BindingPopover\n        scope={scope}\n        aliases={aliases}\n        validate={(value) => validatePrimitiveValue(value, label)}\n        variant={variant}\n        value={expression}\n        onChange={(newExpression) =>\n          onChange({ type: \"expression\", value: newExpression })\n        }\n        onRemove={(evaluatedValue) =>\n          onChange({ type: \"string\", value: String(evaluatedValue) })\n        }\n      />\n    </BindingControl>\n  );\n\n  return (\n    <ResponsiveLayout\n      label={\n        <PropertyLabel name={propName} readOnly={overwritable === false} />\n      }\n    >\n      {input}\n    </ResponsiveLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/controls/url.tsx",
    "content": "import { type ReactNode, useEffect, useId, useMemo } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { computed } from \"nanostores\";\nimport {\n  theme,\n  InputField,\n  Flex,\n  ToggleGroup,\n  ToggleGroupButton,\n  Select,\n  Tooltip,\n  SelectGroup,\n  SelectLabel,\n  SelectItem,\n} from \"@webstudio-is/design-system\";\nimport {\n  AttachmentIcon,\n  EmailIcon,\n  LinkIcon,\n  PageIcon,\n  PhoneIcon,\n} from \"@webstudio-is/icons\";\nimport type { Folder, Instance, Page } from \"@webstudio-is/sdk\";\nimport {\n  findParentFolderByChildId,\n  findTreeInstanceIds,\n} from \"@webstudio-is/sdk\";\nimport { $instances, $pages, $props } from \"~/shared/sync/data-stores\";\nimport {\n  BindingControl,\n  BindingPopover,\n  validatePrimitiveValue,\n} from \"~/builder/shared/binding-popover\";\nimport {\n  type ControlProps,\n  useLocalValue,\n  VerticalLayout,\n  Label,\n  updateExpressionValue,\n  $selectedInstanceScope,\n  useBindingState,\n  humanizeAttribute,\n} from \"../shared\";\nimport { SelectAsset } from \"./select-asset\";\nimport { createRootFolder } from \"@webstudio-is/project-build\";\nimport { PropertyLabel } from \"../property-label\";\n\ntype UrlControlProps = ControlProps<\"url\">;\n\ntype BaseControlProps = {\n  id: string;\n  instanceId: string;\n  readOnly: boolean;\n  prop: UrlControlProps[\"prop\"];\n  value: string;\n  onChange: UrlControlProps[\"onChange\"];\n};\n\nconst Row = ({ children }: { children: ReactNode }) => (\n  <Flex css={{ height: theme.spacing[13] }} align=\"center\" justify=\"between\">\n    {children}\n  </Flex>\n);\n\nconst canParse = (value: string) => {\n  try {\n    return Boolean(new URL(value));\n  } catch {\n    return false;\n  }\n};\n\n/**\n * Add protocol to URL if it appears absolute and valid. Leave it unchanged otherwise.\n **/\nconst addHttpsIfMissing = (url: string) => {\n  if (url.startsWith(\"//\") && canParse(`https:${url}`)) {\n    return new URL(`https:${url}`).href;\n  }\n\n  if (url.startsWith(\"/\")) {\n    return url;\n  }\n\n  if (canParse(url)) {\n    return new URL(url).href;\n  }\n\n  if (canParse(`https://${url}`)) {\n    return new URL(`https://${url}`).href;\n  }\n\n  return url;\n};\n\nconst BaseUrl = ({ readOnly, prop, value, onChange, id }: BaseControlProps) => {\n  const localValue = useLocalValue(value, (value) => {\n    if (prop?.type === \"expression\") {\n      updateExpressionValue(prop.value, value);\n    } else {\n      onChange({ type: \"string\", value });\n    }\n  });\n\n  useEffect(() => {\n    return () => localValue.set(addHttpsIfMissing(localValue.value));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <Row>\n      <InputField\n        disabled={readOnly}\n        id={id}\n        value={localValue.value}\n        placeholder=\"https://www.url.com\"\n        onChange={(event) => localValue.set(event.target.value)}\n        onBlur={() => {\n          localValue.set(addHttpsIfMissing(localValue.value));\n          localValue.save();\n        }}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            localValue.set(addHttpsIfMissing(localValue.value));\n            localValue.save();\n          }\n        }}\n        css={{ width: \"100%\" }}\n      />\n    </Row>\n  );\n};\n\nconst BasePhone = ({\n  readOnly,\n  prop,\n  value,\n  onChange,\n  id,\n}: BaseControlProps) => {\n  const localValue = useLocalValue(\n    value.startsWith(\"tel:\") ? value.slice(4) : \"\",\n    (value) => {\n      if (prop?.type === \"expression\") {\n        updateExpressionValue(prop.value, `tel:${value}`);\n      } else {\n        onChange({ type: \"string\", value: `tel:${value}` });\n      }\n    }\n  );\n\n  return (\n    <Row>\n      <InputField\n        id={id}\n        disabled={readOnly}\n        value={localValue.value}\n        type=\"tel\"\n        placeholder=\"+15555555555\"\n        onChange={(event) => localValue.set(event.target.value)}\n        onBlur={localValue.save}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            localValue.save();\n          }\n        }}\n        css={{ width: \"100%\" }}\n      />\n    </Row>\n  );\n};\n\nconst propToEmail = (value: string) => {\n  let url;\n  try {\n    url = new URL(value);\n  } catch {\n    // empty block\n  }\n\n  if (url === undefined || url.protocol !== \"mailto:\") {\n    return { email: \"\", subject: \"\" };\n  }\n\n  return {\n    email: url.pathname,\n    subject: url.searchParams.get(\"subject\") ?? \"\",\n  };\n};\n\nconst BaseEmail = ({\n  readOnly,\n  prop,\n  value,\n  onChange,\n  id,\n}: BaseControlProps) => {\n  const localValue = useLocalValue(propToEmail(value), ({ email, subject }) => {\n    if (email === \"\") {\n      if (prop?.type === \"expression\") {\n        updateExpressionValue(prop.value, \"\");\n      } else {\n        onChange({ type: \"string\", value: \"\" });\n      }\n      return;\n    }\n    const url = new URL(`mailto:${email}`);\n    if (subject !== \"\") {\n      url.searchParams.set(\"subject\", subject);\n    }\n    const value = url.toString();\n    if (prop?.type === \"expression\") {\n      updateExpressionValue(prop.value, value);\n    } else {\n      onChange({ type: \"string\", value });\n    }\n  });\n\n  return (\n    <>\n      <Row>\n        <InputField\n          disabled={readOnly}\n          id={id}\n          value={localValue.value.email}\n          type=\"email\"\n          placeholder=\"email@address.com\"\n          onChange={(event) =>\n            localValue.set({ ...localValue.value, email: event.target.value })\n          }\n          onBlur={localValue.save}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\") {\n              localValue.save();\n            }\n          }}\n          css={{ width: \"100%\" }}\n        />\n      </Row>\n      <Row>\n        <Label htmlFor={`${id}-subject`}>Subject</Label>\n        <InputField\n          disabled={readOnly}\n          id={`${id}-subject`}\n          value={localValue.value.subject}\n          placeholder=\"You've got mail!\"\n          onChange={(event) =>\n            localValue.set({\n              ...localValue.value,\n              subject: event.target.value,\n            })\n          }\n          onBlur={localValue.save}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\") {\n              localValue.save();\n            }\n          }}\n          css={{ width: theme.spacing[24] }}\n        />\n      </Row>\n    </>\n  );\n};\n\nconst instancesPerPageStore = computed(\n  [$instances, $pages],\n  (instances, pages) =>\n    (pages ? [pages.homePage, ...pages.pages] : []).map((page) => ({\n      pageId: page.id,\n      instancesIds: findTreeInstanceIds(instances, page.rootInstanceId),\n    }))\n);\n\nconst $sections = computed(\n  [instancesPerPageStore, $props],\n  (instancesPerPage, props) => {\n    const sections: Array<{\n      pageId: Page[\"id\"];\n      instanceId: Instance[\"id\"];\n      hash: string;\n    }> = [];\n\n    for (const prop of props.values()) {\n      if (\n        prop.type === \"string\" &&\n        prop.name === \"id\" &&\n        prop.value.trim() !== \"\"\n      ) {\n        for (const { pageId, instancesIds } of instancesPerPage) {\n          if (instancesIds.has(prop.instanceId)) {\n            sections.push({\n              pageId,\n              instanceId: prop.instanceId,\n              hash: prop.value,\n            });\n          }\n        }\n      }\n    }\n\n    return sections.sort((a, b) => a.hash.localeCompare(b.hash));\n  }\n);\n\nconst getId = (data: { id: string }) => data.id;\nconst getHash = (data: { hash: string }) => data.hash;\nconst getInstanceId = (data: { instanceId: string }) => data.instanceId;\n\nconst BasePage = ({ prop, onChange }: BaseControlProps) => {\n  const pages = useStore($pages);\n  const { allPages, pageSelectOptions } = useMemo(() => {\n    const allPages = pages ? [pages.homePage, ...pages.pages] : [];\n    const rootFolder = createRootFolder();\n    const pageSelectOptions = new Map<\n      Folder[\"id\"],\n      { name: Folder[\"name\"]; pages: Array<Page> }\n    >();\n    for (const page of allPages) {\n      const folder =\n        findParentFolderByChildId(page.id, pages?.folders ?? []) ?? rootFolder;\n      let group = pageSelectOptions.get(folder.id);\n      if (group === undefined) {\n        group = { name: folder.name, pages: [] };\n        pageSelectOptions.set(folder.id, group);\n      }\n      group.pages.push(page);\n    }\n    return { pageSelectOptions, allPages };\n  }, [pages]);\n\n  const selectedPageId =\n    prop?.type === \"page\"\n      ? typeof prop.value === \"string\"\n        ? prop.value\n        : prop.value.pageId\n      : undefined;\n\n  const sections = useStore($sections);\n\n  const sectionSelectOptions = selectedPageId\n    ? sections.filter(({ pageId }) => pageId === selectedPageId)\n    : sections;\n\n  const sectionInstanceId =\n    prop?.type === \"page\" && typeof prop.value !== \"string\"\n      ? prop.value.instanceId\n      : undefined;\n\n  const sectionSelectValue =\n    sectionInstanceId === undefined\n      ? undefined\n      : sectionSelectOptions.find(\n          ({ instanceId }) => instanceId === sectionInstanceId\n        );\n\n  return (\n    <>\n      <Row>\n        <Select\n          value={selectedPageId}\n          options={allPages.map(getId)}\n          onChange={(id) => onChange({ type: \"page\", value: id })}\n          placeholder=\"Choose page\"\n          fullWidth\n        >\n          {Array.from(pageSelectOptions).map(([folderId, { name, pages }]) => {\n            return (\n              <SelectGroup key={folderId}>\n                <SelectLabel>{name}</SelectLabel>\n                {pages.map((page) => {\n                  return (\n                    <SelectItem key={page.id} value={page.id}>\n                      {page.name}\n                    </SelectItem>\n                  );\n                })}\n              </SelectGroup>\n            );\n          })}\n        </Select>\n      </Row>\n      <Row>\n        <Select\n          key={selectedPageId}\n          disabled={sectionSelectOptions.length === 0}\n          placeholder={\n            sectionSelectOptions.length === 0\n              ? selectedPageId\n                ? \"Selected page has no sections\"\n                : \"No sections available\"\n              : \"Choose section\"\n          }\n          value={sectionSelectValue}\n          options={sectionSelectOptions}\n          getLabel={getHash}\n          getValue={getInstanceId}\n          onChange={({ pageId, instanceId }) =>\n            onChange({ type: \"page\", value: { pageId, instanceId } })\n          }\n          fullWidth\n        />\n      </Row>\n    </>\n  );\n};\n\nconst BaseAttachment = ({ prop, onChange }: BaseControlProps) => (\n  <Row>\n    <SelectAsset\n      prop={prop?.type === \"asset\" ? prop : undefined}\n      onChange={onChange}\n    />\n  </Row>\n);\n\nconst modes = {\n  url: { icon: <LinkIcon />, control: BaseUrl, label: \"URL\" },\n  page: { icon: <PageIcon />, control: BasePage, label: \"Page\" },\n  email: { icon: <EmailIcon />, control: BaseEmail, label: \"Email\" },\n  phone: { icon: <PhoneIcon />, control: BasePhone, label: \"Phone\" },\n  attachment: {\n    icon: <AttachmentIcon />,\n    control: BaseAttachment,\n    label: \"Attachment\",\n  },\n} as const;\n\ntype Mode = keyof typeof modes;\n\nconst propToMode = (\n  prop: undefined | UrlControlProps[\"prop\"],\n  value: string\n): Mode => {\n  if (prop === undefined) {\n    return \"url\";\n  }\n\n  if (prop.type === \"page\") {\n    return \"page\";\n  }\n\n  if (prop.type === \"asset\") {\n    return \"attachment\";\n  }\n\n  if (value.startsWith(\"tel:\")) {\n    return \"phone\";\n  }\n\n  if (value.startsWith(\"mailto:\")) {\n    return \"email\";\n  }\n\n  return \"url\";\n};\n\nexport const UrlControl = ({\n  instanceId,\n  meta,\n  prop,\n  propName,\n  computedValue,\n  onChange,\n}: UrlControlProps) => {\n  const value = String(computedValue ?? \"\");\n  const { value: mode, set: setMode } = useLocalValue<Mode>(\n    propToMode(prop, value),\n    () => {}\n  );\n\n  const id = useId();\n\n  const BaseControl = modes[mode].control;\n\n  const label = humanizeAttribute(meta.label || propName);\n  const { scope, aliases } = useStore($selectedInstanceScope);\n  const expression =\n    prop?.type === \"expression\" ? prop.value : JSON.stringify(computedValue);\n  const { overwritable, variant } = useBindingState(\n    prop?.type === \"expression\" ? prop.value : undefined\n  );\n\n  return (\n    <VerticalLayout label={<PropertyLabel name={propName} />}>\n      <Flex\n        css={{\n          py: theme.spacing[2],\n\n          // temporary fix for ToggleGroup\n          // which borders protrude outside of the container\n          px: theme.spacing[1],\n        }}\n      >\n        <ToggleGroup\n          type=\"single\"\n          disabled={overwritable === false}\n          value={mode}\n          onValueChange={(value) => {\n            // too tricky to prove to TS that value is a Mode\n            // doesn't worth it given we map over modes below\n            setMode(value as Mode);\n          }}\n        >\n          {Object.entries(modes).map(([key, { icon, label }]) => (\n            <Tooltip key={key} content={label}>\n              <ToggleGroupButton value={key}>{icon}</ToggleGroupButton>\n            </Tooltip>\n          ))}\n        </ToggleGroup>\n      </Flex>\n\n      <BindingControl>\n        <BaseControl\n          id={id}\n          instanceId={instanceId}\n          readOnly={overwritable === false}\n          prop={prop}\n          value={value}\n          onChange={onChange}\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          validate={(value) => validatePrimitiveValue(value, label)}\n          variant={variant}\n          value={expression}\n          onChange={(newExpression) =>\n            onChange({ type: \"expression\", value: newExpression })\n          }\n          onRemove={(evaluatedValue) =>\n            onChange({ type: \"string\", value: String(evaluatedValue) })\n          }\n        />\n      </BindingControl>\n    </VerticalLayout>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/curl.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { generateCurl, parseCurl, type CurlRequest } from \"./curl\";\n\ntest(\"support url\", () => {\n  const result = {\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  };\n  expect(parseCurl(`curl https://my-url/hello-world`)).toEqual(result);\n  expect(parseCurl(`curl \"https://my-url/hello-world\"`)).toEqual(result);\n  expect(parseCurl(`curl 'https://my-url/hello-world'`)).toEqual(result);\n});\n\ntest(\"support multiline command with backslashes\", () => {\n  expect(\n    parseCurl(`curl \\\\\n      'https://my-url/hello-world'\n  `)\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  });\n});\n\ntest(\"forgive missing closed quotes\", () => {\n  expect(parseCurl(`curl \"https://my-url/hello-world`)).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  });\n});\n\ntest(\"skip when invalid\", () => {\n  expect(parseCurl(``)).toEqual(undefined);\n  expect(parseCurl(`  `)).toEqual(undefined);\n  expect(parseCurl(`something else`)).toEqual(undefined);\n  expect(parseCurl(`curl`)).toEqual(undefined);\n  expect(parseCurl(`curl `)).toEqual(undefined);\n});\n\ntest(\"support method with --request and -X flags\", () => {\n  const result = {\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"post\",\n    headers: [],\n  };\n  expect(parseCurl(`curl --request post https://my-url/hello-world`)).toEqual(\n    result\n  );\n  expect(parseCurl(`curl https://my-url/hello-world -X POST`)).toEqual(result);\n  expect(\n    parseCurl(`curl --request put https://my-url/hello-world -X post`)\n  ).toEqual(result);\n  expect(\n    parseCurl(`curl -X put https://my-url/hello-world --request post`)\n  ).toEqual(result);\n});\n\ntest(\"support --get and -G flags\", () => {\n  expect(\n    parseCurl(`curl --get https://my-url --data limit=3 --data first=0`)\n  ).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [\n      { name: \"limit\", value: \"3\" },\n      { name: \"first\", value: \"0\" },\n    ],\n    method: \"get\",\n    headers: [],\n  });\n  expect(parseCurl(`curl -G https://my-url -d limit=3 -d first=0`)).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [\n      { name: \"limit\", value: \"3\" },\n      { name: \"first\", value: \"0\" },\n    ],\n    method: \"get\",\n    headers: [],\n  });\n  expect(\n    parseCurl(`curl -G https://my-url?filter=1 -d limit=3 -d first=0`)\n  ).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [\n      { name: \"filter\", value: \"1\" },\n      { name: \"limit\", value: \"3\" },\n      { name: \"first\", value: \"0\" },\n    ],\n    method: \"get\",\n    headers: [],\n  });\n  expect(parseCurl(`curl -G https://my-url?filter -d limit`)).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [\n      { name: \"filter\", value: \"\" },\n      { name: \"limit\", value: \"\" },\n    ],\n    method: \"get\",\n    headers: [],\n  });\n});\n\ntest(\"support headers with --header and -H flags\", () => {\n  expect(\n    parseCurl(`curl https://my-url/hello-world --header \"name: value\"`)\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"get\",\n    headers: [{ name: \"name\", value: \"value\" }],\n  });\n  expect(parseCurl(`curl https://my-url/hello-world -H \"name: value\"`)).toEqual(\n    {\n      url: \"https://my-url/hello-world\",\n      searchParams: [],\n      method: \"get\",\n      headers: [{ name: \"name\", value: \"value\" }],\n    }\n  );\n  expect(\n    parseCurl(\n      `curl https://my-url/hello-world --header=\"name:value1\" -H \"name : value2\"`\n    )\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"get\",\n    headers: [\n      { name: \"name\", value: \"value1\" },\n      { name: \"name\", value: \"value2\" },\n    ],\n  });\n});\n\ntest(\"default to post method and urlencoded header when data is specified\", () => {\n  expect(\n    parseCurl(`\n      curl https://my-url \\\\\\\n        -d param=1 \\\\\\\n        --data param=2 \\\\\\\n        --data-ascii param=3 \\\\\\\n        --data-raw param=4\n    `)\n  ).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [],\n    method: \"post\",\n    headers: [\n      { name: \"content-type\", value: \"application/x-www-form-urlencoded\" },\n    ],\n    body: `param=1&param=2&param=3&param=4`,\n  });\n});\n\ntest(\"encode data for get request\", () => {\n  expect(\n    parseCurl(`curl -G https://my-url --data-urlencode param=привет`)\n  ).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [{ name: \"param\", value: \"привет\" }],\n    method: \"get\",\n    headers: [],\n  });\n});\n\ntest(\"encode data for post request\", () => {\n  expect(\n    parseCurl(`curl https://my-url --data-urlencode param=привет`)\n  ).toEqual({\n    url: \"https://my-url/\",\n    searchParams: [],\n    method: \"post\",\n    headers: [\n      { name: \"content-type\", value: \"application/x-www-form-urlencoded\" },\n    ],\n    body: `param=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82`,\n  });\n});\n\ntest(\"support text body\", () => {\n  expect(\n    parseCurl(`\n      curl https://my-url/hello-world \\\\\n        -H content-type:plain/text \\\\\n        --data '{\"param\":\"value\"}'\n    `)\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"post\",\n    headers: [{ name: \"content-type\", value: \"plain/text\" }],\n    body: `{\"param\":\"value\"}`,\n  });\n  expect(\n    parseCurl(\n      `curl https://my-url/hello-world -H content-type:plain/text -d '{\"param\":\"value\"}'`\n    )\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"post\",\n    headers: [{ name: \"content-type\", value: \"plain/text\" }],\n    body: `{\"param\":\"value\"}`,\n  });\n});\n\ntest(\"support text body with explicit method\", () => {\n  expect(\n    parseCurl(`\n      curl https://my-url/hello-world \\\\\n        -X put \\\\\n        -H content-type:plain/text \\\\\n        --data '{\"param\":\"value\"}'\n    `)\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"put\",\n    headers: [{ name: \"content-type\", value: \"plain/text\" }],\n    body: `{\"param\":\"value\"}`,\n  });\n});\n\ntest(\"support json body\", () => {\n  expect(\n    parseCurl(\n      `curl https://my-url/hello-world --header 'content-type: application/json' --data '{\"param\":\"value\"}'`\n    )\n  ).toEqual({\n    url: \"https://my-url/hello-world\",\n    searchParams: [],\n    method: \"post\",\n    headers: [{ name: \"content-type\", value: \"application/json\" }],\n    body: { param: \"value\" },\n  });\n});\n\ntest(\"avoid failing on syntax error\", () => {\n  expect(parseCurl(\"curl \\\\\")).toEqual(undefined);\n});\n\ntest(\"generate curl with json body\", () => {\n  expect(\n    generateCurl({\n      url: \"https://my-url.com\",\n      searchParams: [],\n      method: \"post\",\n      headers: [{ name: \"content-type\", value: \"application/json\" }],\n      body: { param: \"value\" },\n    })\n  ).toMatchInlineSnapshot(`\n\"curl \"https://my-url.com/\" \\\\\n  --request post \\\\\n  --header \"content-type: application/json\" \\\\\n  --data \"{\\\\\"param\\\\\":\\\\\"value\\\\\"}\"\"\n`);\n});\n\ntest(\"generate curl with text body\", () => {\n  expect(\n    generateCurl({\n      url: \"https://my-url.com\",\n      searchParams: [],\n      method: \"post\",\n      headers: [],\n      body: \"my data\",\n    })\n  ).toMatchInlineSnapshot(`\n\"curl \"https://my-url.com/\" \\\\\n  --request post \\\\\n  --data \"my data\"\"\n`);\n});\n\ntest(\"generate curl without body\", () => {\n  expect(\n    generateCurl({\n      url: \"https://my-url.com\",\n      searchParams: [],\n      method: \"post\",\n      headers: [],\n    })\n  ).toMatchInlineSnapshot(`\n\"curl \"https://my-url.com/\" \\\\\n  --request post\"\n`);\n});\n\ntest(\"generate curl with search params\", () => {\n  expect(\n    generateCurl({\n      url: \"https://my-url.com\",\n      searchParams: [\n        { name: \"search\", value: \"term1\" },\n        { name: \"search\", value: \"term2\" },\n        { name: \"filter\", value: \"привет\" },\n      ],\n      method: \"get\",\n      headers: [],\n    })\n  ).toMatchInlineSnapshot(`\n\"curl \"https://my-url.com/?search=term1&search=term2&filter=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82\" \\\\\n  --request get\"\n`);\n});\n\ntest(\"generate curl with JSON search params\", () => {\n  expect(\n    generateCurl({\n      url: \"https://my-url.com\",\n      searchParams: [\n        { name: \"filter\", value: { type: \"AND\", left: true, right: false } },\n      ],\n      method: \"get\",\n      headers: [],\n    })\n  ).toMatchInlineSnapshot(`\n\"curl \"https://my-url.com/?filter=%7B%22type%22%3A%22AND%22%2C%22left%22%3Atrue%2C%22right%22%3Afalse%7D\" \\\\\n  --request get\"\n`);\n});\n\ntest(\"generate curl with JSON headers\", () => {\n  expect(\n    generateCurl({\n      url: \"https://my-url.com\",\n      searchParams: [],\n      method: \"get\",\n      headers: [\n        { name: \"x-filter\", value: { type: \"AND\", left: true, right: false } },\n      ],\n    })\n  ).toMatchInlineSnapshot(`\n\"curl \"https://my-url.com/\" \\\\\n  --request get \\\\\n  --header \"x-filter: {\\\\\"type\\\\\":\\\\\"AND\\\\\",\\\\\"left\\\\\":true,\\\\\"right\\\\\":false}\"\"\n`);\n});\n\ntest(\"multiline graphql is idempotent\", () => {\n  const request: CurlRequest = {\n    url: \"https://eu-central-1-shared-euc1-02.cdn.hygraph.com/content/clorhpxi8qx7r01t6hfp1b5f6/master\",\n    searchParams: [],\n    method: \"post\",\n    headers: [{ name: \"Content-Type\", value: \"application/json\" }],\n    body: {\n      query: `\n        query Posts {\n          posts {\n            slug\n            title\n            updatedAt\n            excerpt\n          }\n        }\n      `,\n    },\n  };\n  expect(parseCurl(generateCurl(request))).toEqual(request);\n});\n\ntest(\"support basic http authentication\", () => {\n  expect(parseCurl(`curl https://my-url.com -u \"user:password\"`)).toEqual({\n    url: \"https://my-url.com/\",\n    searchParams: [],\n    method: \"get\",\n    headers: [\n      {\n        name: \"Authorization\",\n        value: `Basic ${btoa(\"user:password\")}`,\n      },\n    ],\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/curl.ts",
    "content": "import { tokenizeArgs } from \"args-tokenizer\";\nimport { parse as parseArgs } from \"@bomb.sh/args\";\nimport type { ResourceRequest } from \"@webstudio-is/sdk\";\nimport { serializeValue } from \"@webstudio-is/sdk/runtime\";\n\n/*\n\ncurl --request POST 'https://my-url/hello-world' \\\n  --header 'Authorization: Bearer ACCESS_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{ \"name\":\"Foo\" }'\n\n*/\n\nconst getMethod = (value: string): ResourceRequest[\"method\"] => {\n  switch (value.toLowerCase()) {\n    case \"post\":\n      return \"post\";\n    case \"put\":\n      return \"put\";\n    case \"delete\":\n      return \"delete\";\n    default:\n      return \"get\";\n  }\n};\n\nexport type CurlRequest = Pick<\n  ResourceRequest,\n  \"url\" | \"searchParams\" | \"method\" | \"headers\" | \"body\"\n>;\n\nconst encodeSearchParams = (data: string[]) => {\n  return data\n    .map((item) => {\n      if (item.includes(\"=\")) {\n        const [key, value] = item.split(\"=\");\n        return `${key}=${encodeURIComponent(value)}`;\n      }\n      return item;\n    })\n    .join(\"&\");\n};\n\nexport const parseCurl = (curl: string): undefined | CurlRequest => {\n  const argv = tokenizeArgs(curl, { loose: true });\n  if (argv.length === 0) {\n    return;\n  }\n  const args = parseArgs(argv, {\n    alias: {\n      X: [\"request\"],\n      G: [\"get\"],\n      H: [\"header\"],\n      d: [\"data\"],\n      \"data-ascii\": [\"data\"],\n      \"data-raw\": [\"data\"],\n      \"data-urlencode\": [\"data\"],\n      u: [\"user\"],\n    },\n    boolean: [\"get\", \"G\"],\n    string: [\n      \"request\",\n      \"X\",\n      \"data\",\n      \"d\",\n      \"data-ascii\",\n      \"data-raw\",\n      \"data-urlencode\",\n      \"user\",\n      \"u\",\n    ],\n    array: [\n      \"header\",\n      \"H\",\n      \"data\",\n      \"d\",\n      \"data-ascii\",\n      \"data-raw\",\n      \"data-urlencode\",\n    ],\n  });\n  // require at least 2 parameters and first is curl command\n  if (args._.length < 2 || args._[0] !== \"curl\") {\n    return;\n  }\n  // curl url\n  const url = new URL(args._[1].toString());\n  const defaultMethod = args.data ? \"post\" : \"get\";\n  const method: CurlRequest[\"method\"] = args.get\n    ? \"get\"\n    : getMethod(args.request ?? defaultMethod);\n  let contentType: undefined | string;\n  const searchParams: NonNullable<ResourceRequest[\"searchParams\"]> = [];\n  for (const [name, value] of url.searchParams) {\n    searchParams.push({ name, value });\n  }\n  // remove all search params from url\n  url.search = \"\";\n  const headers: ResourceRequest[\"headers\"] = (\n    (args.header as string[]) ?? []\n  ).map((header) => {\n    // in case of invalid header fallback value to empty string\n    const [name, value = \"\"] = header.trim().split(/\\s*:\\s*/);\n    if (name.toLowerCase() === \"content-type\") {\n      contentType = value;\n    }\n    return { name, value };\n  });\n  if (args.user) {\n    headers.push({ name: \"Authorization\", value: `Basic ${btoa(args.user)}` });\n  }\n  let body: undefined | unknown;\n  if (args.get && args.data) {\n    for (const pair of args.data) {\n      const [name, value = \"\"] = pair.split(\"=\");\n      searchParams.push({ name, value });\n    }\n  } else if (args.data) {\n    body = args.data[0];\n    if (contentType === \"application/json\") {\n      try {\n        body = JSON.parse(args.data[0]);\n      } catch {\n        // empty block\n      }\n    }\n    if (contentType === undefined) {\n      contentType = \"application/x-www-form-urlencoded\";\n      headers.push({ name: \"content-type\", value: contentType });\n    }\n    if (contentType === \"application/x-www-form-urlencoded\") {\n      body = encodeSearchParams(args.data);\n    }\n  }\n  return {\n    url: url.toString(),\n    searchParams,\n    method,\n    headers,\n    body,\n  };\n};\n\nexport const generateCurl = (request: CurlRequest) => {\n  const url = new URL(request.url);\n  for (const { name, value } of request.searchParams) {\n    url.searchParams.append(name, serializeValue(value));\n  }\n  const args = [`curl ${JSON.stringify(url)}`, `--request ${request.method}`];\n  for (const header of request.headers) {\n    args.push(\n      // escape json in headers\n      `--header \"${header.name}: ${serializeValue(header.value).replaceAll('\"', '\\\\\"')}\"`\n    );\n  }\n  if (request.body) {\n    let body = request.body;\n    // double serialize json\n    if (typeof request.body !== \"string\") {\n      body = JSON.stringify(body);\n    }\n    args.push(`--data ${JSON.stringify(body)}`);\n  }\n  return args.join(\" \\\\\\n  \");\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/index.ts",
    "content": "export * from \"./settings-panel\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/property-label.tsx",
    "content": "import { micromark } from \"micromark\";\nimport { useMemo, useState, type ReactNode } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  Flex,\n  Kbd,\n  Label,\n  Text,\n  Tooltip,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { AlertIcon } from \"@webstudio-is/icons\";\nimport type { Prop } from \"@webstudio-is/sdk\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { $selectedInstance } from \"~/shared/awareness\";\nimport { $props } from \"~/shared/sync/data-stores\";\nimport {\n  $selectedInstanceInitialPropNames,\n  $selectedInstancePropsMetas,\n  humanizeAttribute,\n} from \"./shared\";\n\nconst usePropMeta = (name: string) => {\n  const store = useMemo(() => {\n    return computed($selectedInstancePropsMetas, (propsMetas) =>\n      propsMetas.get(name)\n    );\n  }, [name]);\n  return useStore(store);\n};\n\nconst $selectedInstanceProps = computed(\n  [$selectedInstance, $props],\n  (instance, props) => {\n    const instanceProps = new Map<Prop[\"name\"], Prop>();\n    for (const prop of props.values()) {\n      if (prop.instanceId === instance?.id) {\n        instanceProps.set(prop.name, prop);\n      }\n    }\n    return instanceProps;\n  }\n);\n\nconst useProp = (name: string) => {\n  const store = useMemo(() => {\n    return computed([$selectedInstanceProps], (selectedInstanceProps) =>\n      selectedInstanceProps.get(name)\n    );\n  }, [name]);\n  return useStore(store);\n};\n\nconst deleteProp = (name: string) => {\n  const instance = $selectedInstance.get();\n  const instanceProps = $selectedInstanceProps.get();\n  updateWebstudioData((data) => {\n    const prop = instanceProps.get(name);\n    if (prop) {\n      data.props.delete(prop.id);\n    }\n    if (prop?.type === \"resource\") {\n      data.resources.delete(prop.value);\n    }\n    if (instance?.component === \"Image\" && name === \"src\") {\n      const widthProp = instanceProps.get(\"width\");\n      if (widthProp) {\n        data.props.delete(widthProp.id);\n      }\n      const heightProp = instanceProps.get(\"height\");\n      if (heightProp) {\n        data.props.delete(heightProp.id);\n      }\n    }\n  });\n};\n\nconst useIsResettable = (name: string) => {\n  const store = useMemo(() => {\n    return computed(\n      [$selectedInstanceInitialPropNames],\n      (initialPropNames) => name === showAttribute || initialPropNames.has(name)\n    );\n  }, [name]);\n  return useStore(store);\n};\n\nexport const PropertyLabel = ({\n  name,\n  readOnly,\n}: {\n  name: string;\n  readOnly?: boolean;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const propMeta = usePropMeta(name);\n  const prop = useProp(name);\n  const label = propMeta?.label ?? humanizeAttribute(name);\n  // not existing properties cannot be deleted\n  const isDeletable = prop !== undefined;\n  const isResettable = useIsResettable(name);\n  return (\n    <Flex align=\"center\" css={{ gap: theme.spacing[3] }}>\n      {/* prevent label growing */}\n      <div>\n        <Tooltip\n          open={isOpen}\n          onOpenChange={setIsOpen}\n          triggerProps={{\n            onClick: (event) => {\n              if (event.altKey) {\n                event.preventDefault();\n                if (isDeletable) {\n                  deleteProp(name);\n                }\n                return;\n              }\n              setIsOpen(true);\n            },\n          }}\n          content={\n            <Flex\n              direction=\"column\"\n              gap=\"2\"\n              css={{ maxWidth: theme.spacing[28] }}\n            >\n              <Text variant=\"titles\" css={{ textTransform: \"none\" }}>\n                {label}\n              </Text>\n              {propMeta?.description && <Text>{propMeta.description}</Text>}\n              {readOnly && (\n                <Flex gap=\"1\">\n                  <AlertIcon\n                    color={rawTheme.colors.backgroundAlertMain}\n                    style={{ flexShrink: 0 }}\n                  />\n                  <Text>\n                    The value is controlled by an expression and cannot be\n                    changed.\n                  </Text>\n                </Flex>\n              )}\n              {isDeletable && (\n                <Button\n                  color=\"dark\"\n                  // to align button text in the middle\n                  prefix={<div></div>}\n                  suffix={<Kbd value={[\"alt\", \"click\"]} color=\"moreSubtle\" />}\n                  css={{ gridTemplateColumns: \"1fr max-content 1fr\" }}\n                  onClick={() => {\n                    deleteProp(name);\n                    setIsOpen(false);\n                  }}\n                >\n                  {isResettable ? \"Reset value\" : \"Delete property\"}\n                </Button>\n              )}\n            </Flex>\n          }\n        >\n          <Label truncate color={prop ? \"local\" : \"default\"}>\n            {label}\n          </Label>\n        </Tooltip>\n      </div>\n    </Flex>\n  );\n};\n\nexport const FieldLabel = ({\n  description,\n  resettable = false,\n  onReset,\n  children,\n}: {\n  /**\n   * Markdown text to show in tooltip or react element\n   */\n  description?: string | ReactNode;\n  /**\n   * when true means field has value and colored true\n   */\n  resettable?: boolean;\n  onReset?: () => void;\n  children: string;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  if (typeof description === \"string\") {\n    description = (\n      <Text\n        css={{\n          \"> *\": { marginTop: 0 },\n        }}\n        dangerouslySetInnerHTML={{ __html: micromark(description) }}\n      ></Text>\n    );\n  } else if (description) {\n    description = <Text>{description}</Text>;\n  }\n  return (\n    <Flex align=\"center\" css={{ gap: theme.spacing[3] }}>\n      {/* prevent label growing */}\n      <div>\n        <Tooltip\n          open={isOpen}\n          onOpenChange={setIsOpen}\n          triggerProps={{\n            onClick: (event) => {\n              if (event.altKey) {\n                event.preventDefault();\n                if (resettable) {\n                  onReset?.();\n                }\n                return;\n              }\n              setIsOpen(true);\n            },\n          }}\n          content={\n            <Flex\n              direction=\"column\"\n              gap=\"2\"\n              css={{ maxWidth: theme.spacing[28] }}\n            >\n              <Text variant=\"titles\" css={{ textTransform: \"none\" }}>\n                {children}\n              </Text>\n              {description}\n              {resettable && (\n                <Button\n                  color=\"dark\"\n                  // to align button text in the middle\n                  prefix={<div></div>}\n                  suffix={<Kbd value={[\"alt\", \"click\"]} color=\"moreSubtle\" />}\n                  css={{ gridTemplateColumns: \"1fr max-content 1fr\" }}\n                  onClick={() => {\n                    onReset?.();\n                    setIsOpen(false);\n                  }}\n                >\n                  Reset value\n                </Button>\n              )}\n            </Flex>\n          }\n        >\n          <Label truncate color={resettable ? \"local\" : \"default\"}>\n            {children}\n          </Label>\n        </Tooltip>\n      </div>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/animation-keyframes.tsx",
    "content": "import { Fragment, useId, useMemo, useRef, useState } from \"react\";\nimport {\n  StyleValue,\n  toValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport {\n  Grid,\n  Label,\n  Tooltip,\n  theme,\n  SectionTitle,\n  SectionTitleButton,\n  SectionTitleLabel,\n  FloatingPanel,\n  CssValueListItem,\n  SmallIconButton,\n  CssValueListArrowFocus,\n} from \"@webstudio-is/design-system\";\nimport { MinusIcon, PlusIcon } from \"@webstudio-is/icons\";\nimport type { AnimationKeyframe } from \"@webstudio-is/sdk\";\nimport {\n  CssValueInput,\n  type IntermediateStyleValue,\n} from \"~/builder/features/style-panel/shared/css-value-input\";\nimport { CssEditor } from \"~/builder/shared/css-editor\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { calcOffsets, findInsertionIndex, moveItem } from \"./keyframe-helpers\";\nimport {\n  AnimationTransforms,\n  transformProperties,\n} from \"./animation-transforms\";\n\nconst unitOptions = [\n  {\n    id: \"%\" as const,\n    label: \"%\",\n    type: \"unit\" as const,\n  },\n];\n\nconst roundOffset = (value: number) => Math.round(value * 1000) / 10;\n\nconst OffsetInput = ({\n  id,\n  value,\n  placeholder,\n  onChange,\n}: {\n  id: string;\n  value: number | undefined;\n  placeholder: number;\n  onChange: (value: number | undefined) => void;\n}) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  return (\n    <CssValueInput\n      id={id}\n      placeholder={\n        value === undefined ? `auto (${roundOffset(placeholder)}%)` : \"auto\"\n      }\n      getOptions={() => []}\n      unitOptions={unitOptions}\n      intermediateValue={intermediateValue}\n      styleSource=\"default\"\n      /* same as offset has 0 - 100% */\n      property={\"opacity\"}\n      value={\n        value !== undefined\n          ? { type: \"unit\", value: roundOffset(value), unit: \"%\" }\n          : undefined\n      }\n      onChange={(styleValue) => {\n        if (styleValue === undefined) {\n          setIntermediateValue(styleValue);\n          return;\n        }\n\n        const clampedStyleValue = { ...styleValue };\n        if (\n          clampedStyleValue.type === \"unit\" &&\n          clampedStyleValue.unit === \"%\"\n        ) {\n          clampedStyleValue.value = Math.min(\n            100,\n            Math.max(0, clampedStyleValue.value)\n          );\n        }\n\n        setIntermediateValue(clampedStyleValue);\n      }}\n      onHighlight={(_styleValue) => {\n        /* @todo: think about preview */\n      }}\n      onChangeComplete={(event) => {\n        setIntermediateValue(undefined);\n\n        if (event.value.type === \"unit\" && event.value.unit === \"%\") {\n          onChange(Math.min(100, Math.max(0, event.value.value)) / 100);\n          return;\n        }\n\n        setIntermediateValue({\n          type: \"invalid\",\n          value: toValue(event.value),\n        });\n      }}\n      onAbort={() => {\n        /* @todo: allow to change some ephemeral property to see the result in action */\n      }}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        onChange(undefined);\n      }}\n    />\n  );\n};\n\nconst Keyframe = ({\n  value,\n  index,\n  offsetPlaceholder,\n  onChange,\n  onDelete,\n}: {\n  value: AnimationKeyframe;\n  index: number;\n  offsetPlaceholder: number;\n  onChange: (value: AnimationKeyframe, isEphemeral: boolean) => void;\n  onDelete: () => void;\n}) => {\n  const offsetId = useId();\n  const declarations = useMemo(\n    () =>\n      (Object.keys(value.styles) as CssProperty[])\n        // avoid duplicating transform properties in css editor\n        .filter((property) => !transformProperties.includes(property))\n        .map((property) => {\n          const styleValue = value.styles[property];\n          return {\n            property,\n            source: { name: \"local\" },\n            cascadedValue: styleValue,\n            computedValue: styleValue,\n            usedValue: styleValue,\n          } satisfies ComputedStyleDecl;\n        }),\n    [value.styles]\n  );\n\n  return (\n    <FloatingPanel\n      title=\"Keyframe\"\n      content={\n        <Grid css={{ paddingBlock: theme.panel.paddingBlock }}>\n          <Grid\n            gap={1}\n            align=\"center\"\n            css={{\n              gridTemplateColumns: `1fr ${theme.spacing[22]}`,\n              paddingInline: theme.panel.paddingInline,\n            }}\n          >\n            <Label htmlFor={offsetId}>Offset</Label>\n            <OffsetInput\n              id={offsetId}\n              value={value.offset}\n              placeholder={offsetPlaceholder}\n              onChange={(offset) => {\n                onChange({ ...value, offset }, false);\n              }}\n            />\n          </Grid>\n\n          <AnimationTransforms\n            styles={value.styles}\n            onUpdate={(property, newValue, options) => {\n              const styles = { ...value.styles, [property]: newValue };\n              onChange({ ...value, styles }, options?.isEphemeral ?? false);\n            }}\n            onDelete={(property, options) => {\n              if (options?.isEphemeral === true) {\n                return;\n              }\n              const styles = { ...value.styles };\n              delete styles[property];\n              onChange({ ...value, styles }, false);\n            }}\n          />\n\n          <CssEditor\n            showSearch={false}\n            showAddStyleInput\n            propertiesPosition=\"top\"\n            virtualize={false}\n            declarations={declarations}\n            onAddDeclarations={(addedStyleMap) => {\n              const styles = { ...value.styles };\n              for (const [property, value] of addedStyleMap) {\n                styles[property] = value;\n              }\n              onChange({ ...value, styles }, false);\n            }}\n            onDeleteProperty={(property, options = {}) => {\n              if (options.isEphemeral === true) {\n                return;\n              }\n              const styles = { ...value.styles };\n              delete styles[property];\n              onChange({ ...value, styles }, false);\n            }}\n            onSetProperty={(property) => {\n              return (newValue, options) => {\n                const styles = { ...value.styles, [property]: newValue };\n                onChange({ ...value, styles }, options?.isEphemeral ?? false);\n              };\n            }}\n            onDeleteAllDeclarations={() => {\n              onChange({ ...value, styles: {} }, false);\n            }}\n          />\n        </Grid>\n      }\n    >\n      <CssValueListItem\n        id={offsetPlaceholder.toString()}\n        index={index}\n        label={\n          <Label truncate>\n            {value.offset\n              ? `${roundOffset(value.offset)}%`\n              : `auto (${roundOffset(offsetPlaceholder)}%)`}\n          </Label>\n        }\n        buttons={\n          <Tooltip content=\"Remove keyframe\">\n            <SmallIconButton\n              variant=\"destructive\"\n              tabIndex={-1}\n              icon={<MinusIcon />}\n              onClick={onDelete}\n            />\n          </Tooltip>\n        }\n      ></CssValueListItem>\n    </FloatingPanel>\n  );\n};\n\nexport const Keyframes = ({\n  value: keyframes,\n  onChange,\n}: {\n  value: AnimationKeyframe[];\n  onChange: ((value: undefined, isEphemeral: true) => void) &\n    ((value: AnimationKeyframe[], isEphemeral: boolean) => void);\n}) => {\n  // To preserve focus on children swap\n  const keyRefs = useRef(\n    Array.from({ length: keyframes.length }, (_, index) => index)\n  );\n\n  if (keyframes.length !== keyRefs.current.length) {\n    keyRefs.current = Array.from(\n      { length: keyframes.length },\n      (_, index) => index\n    );\n  }\n\n  const offsets = calcOffsets(keyframes);\n\n  return (\n    <div>\n      <SectionTitle\n        collapsible={false}\n        suffix={\n          <SectionTitleButton\n            tabIndex={0}\n            prefix={<PlusIcon />}\n            onClick={() => {\n              onChange(\n                [...keyframes, { offset: undefined, styles: {} }],\n                false\n              );\n              keyRefs.current = [...keyRefs.current, keyframes.length];\n            }}\n          />\n        }\n      >\n        <SectionTitleLabel>Keyframes</SectionTitleLabel>\n      </SectionTitle>\n      <CssValueListArrowFocus>\n        {keyframes.map((value, index) => (\n          <Fragment key={keyRefs.current[index]}>\n            <Keyframe\n              value={value}\n              index={index}\n              offsetPlaceholder={offsets[index]}\n              onChange={(newValue, isEphemeral) => {\n                let newValues = [...keyframes];\n                newValues[index] = newValue;\n\n                const { offset } = newValue;\n                if (offset === undefined) {\n                  onChange(newValues, isEphemeral);\n                  return;\n                }\n\n                const insertionIndex = findInsertionIndex(newValues, index);\n                newValues = moveItem(newValues, index, insertionIndex);\n                keyRefs.current = moveItem(\n                  keyRefs.current,\n                  index,\n                  insertionIndex\n                );\n\n                onChange(newValues, isEphemeral);\n              }}\n              onDelete={() => {\n                const newValues = [...keyframes];\n                newValues.splice(index, 1);\n                onChange(newValues, false);\n              }}\n            />\n          </Fragment>\n        ))}\n      </CssValueListArrowFocus>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.stories.tsx",
    "content": "import { AnimationPanelContent } from \"./animation-panel-content\";\nimport { StorySection, theme } from \"@webstudio-is/design-system\";\nimport { useState } from \"react\";\nimport type { ScrollAnimation, ViewAnimation } from \"@webstudio-is/sdk\";\n\nexport default {\n  title: \"Settings panel/Animation panel content\",\n  component: AnimationPanelContent,\n  parameters: {\n    layout: \"centered\",\n  },\n  decorators: [\n    (Story: React.ComponentType) => (\n      <div\n        style={{\n          background: theme.colors.backgroundPanel,\n          padding: 16,\n          width: theme.sizes.sidebarWidth,\n        }}\n      >\n        <Story />\n      </div>\n    ),\n  ],\n};\n\nconst scrollInitialValue: ScrollAnimation = {\n  name: \"scroll-animation\",\n  timing: {\n    rangeStart: [\"start\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"end\", { type: \"unit\", value: 100, unit: \"%\" }],\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        opacity: { type: \"unit\", value: 0, unit: \"%\" },\n        color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n      },\n    },\n  ],\n};\n\nconst viewInitialValue: ViewAnimation = {\n  name: \"view-animation\",\n  timing: {\n    rangeStart: [\"entry\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"exit\", { type: \"unit\", value: 100, unit: \"%\" }],\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        opacity: { type: \"unit\", value: 0, unit: \"%\" },\n        color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n      },\n    },\n  ],\n};\n\nconst ScrollAnimationDemo = () => {\n  const [value, setValue] = useState<ScrollAnimation>(scrollInitialValue);\n  return (\n    <AnimationPanelContent\n      type=\"scroll\"\n      value={value}\n      onChange={(newValue) => {\n        setValue(newValue as ScrollAnimation);\n      }}\n    />\n  );\n};\n\nconst ViewAnimationDemo = () => {\n  const [value, setValue] = useState<ViewAnimation>(viewInitialValue);\n  return (\n    <AnimationPanelContent\n      type=\"view\"\n      value={value}\n      onChange={(newValue) => {\n        setValue(newValue as ViewAnimation);\n      }}\n    />\n  );\n};\n\nexport const AnimationPanelContentStory = () => (\n  <>\n    <StorySection title=\"Scroll animation\">\n      <ScrollAnimationDemo />\n    </StorySection>\n    <StorySection title=\"View animation\">\n      <ViewAnimationDemo />\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/animation-panel-content.tsx",
    "content": "import { useState, type ReactNode } from \"react\";\nimport {\n  Box,\n  Grid,\n  InputField,\n  ScrollArea,\n  Select,\n  theme,\n  toast,\n  ToggleGroup,\n  ToggleGroupButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { keywordValues } from \"@webstudio-is/css-data\";\n\nimport type {\n  DurationUnitValue,\n  RangeUnitValue,\n  IterationsUnitValue,\n  ScrollAnimation,\n  ViewAnimation,\n} from \"@webstudio-is/sdk\";\nimport {\n  durationUnitValueSchema,\n  rangeUnitValueSchema,\n  scrollAnimationSchema,\n  viewAnimationSchema,\n} from \"@webstudio-is/sdk\";\nimport {\n  CssValueInput,\n  type IntermediateStyleValue,\n} from \"~/builder/features/style-panel/shared/css-value-input/css-value-input\";\nimport {\n  cssWideKeywords,\n  toValue,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { Keyframes } from \"./animation-keyframes\";\nimport { humanizeString } from \"~/shared/string-utils\";\n\nimport {\n  EllipsesIcon,\n  RangeContain50Icon,\n  RangeContainIcon,\n  RangeCoverIcon,\n} from \"@webstudio-is/icons\";\nimport { $availableUnitVariables } from \"~/builder/features/style-panel/shared/model\";\nimport isEqual from \"fast-deep-equal\";\nimport { FieldLabel } from \"../../property-label\";\n\nconst RotateIcon180 = ({ children }: { children: React.ReactNode }) => {\n  return (\n    <Box\n      css={{\n        transform: \"rotate(180deg)\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nconst fillModeDescriptions: Record<\n  NonNullable<ViewAnimation[\"timing\"][\"fill\"]>,\n  string\n> = {\n  both: \"The animation state is applied before and after the active period. Set if unsure whether it's In or Out\",\n  backwards:\n    \"The animation state is applied before the active period. Prefered for In Animations\",\n  forwards:\n    \"The animation state is applied after the active period. Prefered for Out Animations\",\n  none: \"No animation is applied before or after the active period\",\n};\n\nconst fillModeNames = Object.keys(fillModeDescriptions) as NonNullable<\n  ViewAnimation[\"timing\"][\"fill\"]\n>[];\n\n/**\n * https://developer.mozilla.org/en-US/docs/Web/CSS/animation-range-start\n *\n * <timeline-range-name>\n **/\nconst viewTimelineRangeName = {\n  entry:\n    \"Animates during the subject element entry (starts entering → fully visible)\",\n  exit: \"Animates during the subject element exit (starts exiting → fully hidden)\",\n  contain:\n    \"Animates only while the subject element is fully in view (fullly visible after entering → starts exiting)\",\n  cover:\n    \"Animates entire time the subject element is visible (starts entering → ends after exiting)\",\n  \"entry-crossing\":\n    \"Animates as the subject element enters (leading edge → trailing edge enters view)\",\n  \"exit-crossing\":\n    \"Animates as the subject element exits (leading edge → trailing edge leaves view)\",\n};\n\n/**\n * Scroll does not support https://drafts.csswg.org/scroll-animations/#named-ranges\n * However, for simplicity and type unification with the view, we will use the names \"start\" and \"end,\"\n * which will be transformed as follows:\n * - \"start\" → `calc(0% + range)`\n * - \"end\" → `calc(100% - range)`\n */\nconst scrollTimelineRangeName = {\n  start: \"Distance from the top of the scroll container where animation begins\",\n  end: \"Distance from the bottom of the scroll container where animation ends\",\n};\n\ntype ValidatedCssValueInputProps<T> = Omit<\n  React.ComponentProps<typeof CssValueInput>,\n  | \"onChange\"\n  | \"onChangeComplete\"\n  | \"onHighlight\"\n  | \"onAbort\"\n  | \"onReset\"\n  | \"intermediateValue\"\n  | \"styleSource\"\n  | \"getOptions\"\n  | \"placeholder\"\n> & {\n  onValidate: (\n    styleValue: StyleValue,\n    rawValue: string\n  ) => { success: true; data: T | undefined } | { success: false };\n  onChange: (value: T | undefined, isEphemeral: boolean) => void;\n  onHighlight?: (value: T | undefined, isEphemeral: boolean) => void;\n  styleSource?: React.ComponentProps<typeof CssValueInput>[\"styleSource\"];\n  getOptions?: React.ComponentProps<typeof CssValueInput>[\"getOptions\"];\n  placeholder?: React.ComponentProps<typeof CssValueInput>[\"placeholder\"];\n};\n\n/**\n * Generic wrapper component for CssValueInput with validation\n */\nconst ValidatedCssValueInput = <T,>({\n  onValidate,\n  onChange,\n  onHighlight,\n  styleSource = \"default\",\n  getOptions = () => $availableUnitVariables.get(),\n  placeholder = \"auto\",\n  ...cssValueInputProps\n}: ValidatedCssValueInputProps<T>) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n  return (\n    <CssValueInput\n      {...cssValueInputProps}\n      styleSource={styleSource}\n      getOptions={getOptions}\n      placeholder={placeholder}\n      intermediateValue={intermediateValue}\n      onChange={(styleValue) => {\n        setIntermediateValue(styleValue);\n      }}\n      onHighlight={\n        onHighlight\n          ? (styleValue) => {\n              if (styleValue === undefined) {\n                onHighlight(undefined, true);\n                return;\n              }\n              const rawValue = toValue(styleValue);\n              const result = onValidate(styleValue, rawValue);\n              if (result.success) {\n                onHighlight(result.data, true);\n              }\n            }\n          : () => {}\n      }\n      onChangeComplete={(event) => {\n        onChange(undefined, true);\n        const rawValue = toValue(event.value);\n        const result = onValidate(event.value, rawValue);\n\n        if (result.success) {\n          onChange(result.data, false);\n          setIntermediateValue(undefined);\n          return;\n        }\n\n        setIntermediateValue({\n          type: \"invalid\",\n          value: rawValue,\n        });\n      }}\n      onAbort={() => {\n        onChange(undefined, true);\n      }}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        onChange(undefined, false);\n      }}\n    />\n  );\n};\n\nconst RangeValueInput = ({\n  value,\n  onChange,\n  disabled,\n}: {\n  value: RangeUnitValue;\n  disabled?: boolean;\n  onChange: (value: RangeUnitValue | undefined, isEphemeral: boolean) => void;\n}) => (\n  <ValidatedCssValueInput\n    value={value}\n    property=\"margin-left\" /* allows negative values */\n    disabled={disabled}\n    onValidate={(styleValue) => {\n      const parsedValue = rangeUnitValueSchema.safeParse(styleValue);\n      return parsedValue.success\n        ? { success: true, data: parsedValue.data }\n        : { success: false };\n    }}\n    onChange={onChange}\n  />\n);\n\nconst EasingInput = ({\n  value,\n  onChange,\n}: {\n  value: string | undefined;\n  onChange: (value: string | undefined, isEphemeral: boolean) => void;\n}) => (\n  <ValidatedCssValueInput\n    value={\n      value === undefined\n        ? { type: \"keyword\", value: \"ease\" }\n        : { type: \"unparsed\", value }\n    }\n    property=\"animation-timing-function\"\n    getOptions={() => [\n      ...keywordValues[\"animation-timing-function\"]\n        .filter((value) => !cssWideKeywords.has(value))\n        .map((value) => ({\n          type: \"keyword\" as const,\n          value,\n        })),\n      ...$availableUnitVariables.get(),\n    ]}\n    onValidate={(_, rawValue) => ({ success: true, data: rawValue })}\n    onChange={onChange}\n    onHighlight={onChange}\n  />\n);\nconst DurationInput = ({\n  value,\n  onChange,\n}: {\n  value: DurationUnitValue | undefined;\n  onChange: (\n    value: DurationUnitValue | undefined,\n    isEphemeral: boolean\n  ) => void;\n}) => (\n  <ValidatedCssValueInput\n    value={value}\n    property=\"animation-duration\"\n    onValidate={(styleValue, rawValue) => {\n      if (rawValue.toLowerCase() === \"auto\") {\n        return { success: true, data: undefined };\n      }\n      const parsedValue = durationUnitValueSchema.safeParse(styleValue);\n      return parsedValue.success\n        ? { success: true, data: parsedValue.data }\n        : { success: false };\n    }}\n    onChange={onChange}\n  />\n);\n\nconst IterationsInput = ({\n  value,\n  onChange,\n  disabled,\n}: {\n  value: IterationsUnitValue | undefined;\n  onChange: (\n    value: IterationsUnitValue | undefined,\n    isEphemeral: boolean\n  ) => void;\n  disabled?: boolean;\n}) => {\n  const styleValue: StyleValue | undefined =\n    value === undefined\n      ? { type: \"unit\", value: 1, unit: \"number\" }\n      : value === \"infinite\"\n        ? { type: \"keyword\", value: \"infinite\" }\n        : { type: \"unit\", value, unit: \"number\" };\n\n  return (\n    <ValidatedCssValueInput\n      value={styleValue}\n      placeholder=\"1\"\n      property=\"animation-iteration-count\"\n      disabled={disabled}\n      getOptions={() => [\n        { type: \"keyword\" as const, value: \"infinite\" },\n        ...$availableUnitVariables.get(),\n      ]}\n      onValidate={(styleValue, rawValue) => {\n        if (rawValue === \"auto\") {\n          return { success: true, data: undefined };\n        }\n        if (rawValue === \"infinite\") {\n          return { success: true, data: \"infinite\" as const };\n        }\n        if (styleValue.type === \"unit\" && styleValue.unit === \"number\") {\n          const numValue = styleValue.value;\n          if (typeof numValue === \"number\" && numValue > 0) {\n            return { success: true, data: numValue };\n          }\n        }\n        return { success: false };\n      }}\n      onChange={onChange}\n    />\n  );\n};\n\ntype AnimationPanelContentProps = {\n  type: \"scroll\" | \"view\";\n  value: ScrollAnimation | ViewAnimation;\n\n  onChange: ((\n    value: ScrollAnimation | ViewAnimation,\n    isEphemeral: boolean\n  ) => void) &\n    ((value: undefined, isEphemeral: true) => void);\n};\n\nconst defaultRangeStart = {\n  type: \"unit\",\n  value: 0,\n  unit: \"%\",\n};\n\nconst defaultRangeEnd = {\n  type: \"unit\",\n  value: 100,\n  unit: \"%\",\n};\n\nconst PanelContainer = ({ children }: { children: ReactNode }) => {\n  return (\n    <ScrollArea>\n      <Grid gap={2} css={{ paddingBlock: theme.panel.paddingBlock }}>\n        {children}\n      </Grid>\n    </ScrollArea>\n  );\n};\n\nconst simplifiedRanges = [\n  [\n    \"cover 0%\",\n    <RangeCoverIcon />,\n    [\"cover\", { type: \"unit\", unit: \"%\", value: 0 }],\n    \"the subject just begins to appear in view\",\n  ],\n  [\n    \"contain 0%\",\n    <RangeContainIcon />,\n    [\"contain\", { type: \"unit\", unit: \"%\", value: 0 }],\n    \"when the subject becomes fully visible\",\n  ],\n  [\n    \"contain 50%\",\n    <RangeContain50Icon />,\n    [\"contain\", { type: \"unit\", unit: \"%\", value: 50 }],\n    \"when the subject is centered in the view\",\n  ],\n\n  [\n    \"contain 100%\",\n    <RotateIcon180>\n      <RangeContainIcon />\n    </RotateIcon180>,\n    [\"contain\", { type: \"unit\", unit: \"%\", value: 100 }],\n    \"when the subject begins to leave the view but is still fully visible\",\n  ],\n\n  [\n    \"cover 100%\",\n    <RotateIcon180>\n      <RangeCoverIcon />\n    </RotateIcon180>,\n    [\"cover\", { type: \"unit\", unit: \"%\", value: 100 }],\n    \"when the subject is completely out of view\",\n  ],\n] as const;\n\nconst simplifiedStartRanges = simplifiedRanges.slice(0, -1);\nconst simplifiedEndRanges = simplifiedRanges.slice(1);\n\nconst isRangeEqual = (\n  rangeA:\n    | Readonly<ViewAnimation[\"timing\"][\"rangeStart\"]>\n    | Readonly<ScrollAnimation[\"timing\"][\"rangeStart\"]>,\n  rangeB:\n    | Readonly<ViewAnimation[\"timing\"][\"rangeStart\"]>\n    | Readonly<ScrollAnimation[\"timing\"][\"rangeStart\"]>\n): boolean => {\n  if (isEqual(rangeA, rangeB)) {\n    return true;\n  }\n\n  if (rangeA === undefined || rangeB === undefined) {\n    return false;\n  }\n\n  const rangeAValue = `${rangeA[0]} ${toValue(rangeA[1])}`;\n  const rangeBValue = `${rangeB[0]} ${toValue(rangeB[1])}`;\n\n  const rangesMap = {\n    \"entry 0%\": \"cover 0%\",\n    \"entry 100%\": \"contain 0%\",\n    \"exit 0%\": \"contain 100%\",\n    \"exit 100%\": \"cover 100%\",\n    \"cover 50%\": \"contain 50%\",\n  };\n\n  return (\n    (rangesMap[rangeAValue as keyof typeof rangesMap] ?? rangeAValue) ===\n    (rangesMap[rangeBValue as keyof typeof rangesMap] ?? rangeBValue)\n  );\n};\n\nexport const AnimationPanelContent = ({\n  onChange,\n  value,\n  type,\n}: AnimationPanelContentProps) => {\n  const startRangeIndex = simplifiedStartRanges.findIndex(([, , range]) =>\n    isRangeEqual(range, value.timing.rangeStart)\n  );\n\n  const [startRangeValue] = simplifiedStartRanges.find(([, , range]) =>\n    isRangeEqual(range, value.timing.rangeStart)\n  ) ?? [undefined, undefined, undefined];\n\n  const endRangeIndex = simplifiedEndRanges.findIndex(([, , range]) =>\n    isRangeEqual(range, value.timing.rangeEnd)\n  );\n\n  const [endRangeValue] = simplifiedEndRanges.find(([, , range]) =>\n    isRangeEqual(range, value.timing.rangeEnd)\n  ) ?? [undefined, undefined, undefined];\n\n  const [isAdvancedRangeStart, setIsAdvancedRangeStart] = useState(\n    () => startRangeValue === undefined\n  );\n\n  const [isAdvancedRangeEnd, setIsAdvancedRangeEnd] = useState(\n    () => endRangeValue === undefined\n  );\n\n  const isScrollAnimation = type === \"scroll\";\n\n  const timelineRangeDescriptions = isScrollAnimation\n    ? scrollTimelineRangeName\n    : viewTimelineRangeName;\n\n  const timelineRangeNames = Object.keys(timelineRangeDescriptions);\n\n  const isRangeEndEnabled = value.timing.duration === undefined;\n  const isRangeStartEnabled = value.timing.delay === undefined;\n  const isIterationsEnabled = value.timing.duration !== undefined;\n\n  const animationSchema = isScrollAnimation\n    ? scrollAnimationSchema\n    : viewAnimationSchema;\n\n  const handleChange = (rawValue: unknown, isEphemeral: boolean) => {\n    if (rawValue === undefined) {\n      onChange(undefined, true);\n      return;\n    }\n\n    const parsedValue = animationSchema.safeParse(rawValue);\n\n    if (parsedValue.success) {\n      onChange(parsedValue.data, isEphemeral);\n      return;\n    }\n\n    console.error(parsedValue.error.format());\n    toast.error(\"Animation schema is incompatible, try fix\");\n  };\n\n  // Flex is used to allow the Keyframes to overflow without setting\n  // gridTemplateRows: auto auto 1fr\n  return (\n    <PanelContainer>\n      <Grid\n        gap={1}\n        align=\"center\"\n        css={{ paddingInline: theme.panel.paddingInline }}\n      >\n        <FieldLabel description=\"A meaningful label to identify this animation\">\n          Name\n        </FieldLabel>\n        <InputField\n          css={{\n            width: \"100%\",\n            fontWeight: `inherit`,\n          }}\n          value={value.name}\n          placeholder=\"Enter animation name\"\n          onChange={(event) => {\n            const name = event.currentTarget.value;\n\n            const newValue = {\n              ...value,\n              name,\n            };\n\n            handleChange(newValue, false);\n          }}\n        />\n      </Grid>\n\n      <Grid\n        align={\"center\"}\n        css={{\n          columnGap: theme.spacing[6],\n          rowGap: theme.spacing[2],\n          gridTemplateColumns: \"1fr 1fr\",\n          paddingInline: theme.panel.paddingInline,\n          flexShrink: 0,\n        }}\n      >\n        <FieldLabel description=\"Controls how styles apply before and after the animation\">\n          Fill Mode\n        </FieldLabel>\n        <FieldLabel description=\"Controls how fast the animation moves at different times\">\n          Easing\n        </FieldLabel>\n\n        <Select\n          options={fillModeNames}\n          getLabel={humanizeString}\n          value={value.timing.fill ?? fillModeNames[0]}\n          onItemHighlight={(fillModeName) => {\n            if (fillModeName === undefined) {\n              handleChange(undefined, true);\n              return;\n            }\n\n            handleChange(\n              {\n                ...value,\n                timing: {\n                  ...value.timing,\n                  fill: fillModeName,\n                },\n              },\n              true\n            );\n          }}\n          getDescription={(fillModeName: string) => (\n            <Box\n              css={{\n                width: theme.spacing[28],\n              }}\n            >\n              {\n                fillModeDescriptions[\n                  fillModeName as keyof typeof fillModeDescriptions\n                ]\n              }\n            </Box>\n          )}\n          onChange={(fillModeName) => {\n            handleChange(\n              {\n                ...value,\n                timing: {\n                  ...value.timing,\n                  fill: fillModeName,\n                },\n              },\n              false\n            );\n          }}\n        />\n        <EasingInput\n          value={value.timing.easing}\n          onChange={(easing, isEphemeral) => {\n            if (easing === undefined && isEphemeral) {\n              handleChange(undefined, true);\n              return;\n            }\n\n            handleChange(\n              {\n                ...value,\n                timing: {\n                  ...value.timing,\n                  easing,\n                },\n              },\n              isEphemeral\n            );\n          }}\n        />\n      </Grid>\n\n      <Grid gap=\"2\" css={{ paddingInline: theme.panel.paddingInline }}>\n        <Grid\n          css={{\n            gridTemplateColumns: \"1.1fr 2fr\",\n          }}\n          gap={2}\n          align={\"center\"}\n        >\n          <FieldLabel description=\"When the animation ends, based on how much of the subject is visible\">\n            Range End\n          </FieldLabel>\n          {!isScrollAnimation && (\n            <ToggleGroup\n              value={\n                isAdvancedRangeEnd ? \"advanced\" : (endRangeValue ?? \"advanced\")\n              }\n              type=\"single\"\n            >\n              {simplifiedEndRanges.map(\n                ([toggleValue, icon, range, description], index) => (\n                  <Tooltip\n                    key={toggleValue}\n                    content={`The animation ends ${description}`}\n                    variant=\"wrapped\"\n                  >\n                    <ToggleGroupButton\n                      disabled={\n                        !isRangeEndEnabled ||\n                        (!isAdvancedRangeStart && index < startRangeIndex)\n                      }\n                      value={toggleValue}\n                      onClick={() => {\n                        setIsAdvancedRangeEnd(false);\n                        handleChange(\n                          {\n                            ...value,\n                            timing: {\n                              ...value.timing,\n                              rangeStart: value.timing.rangeStart,\n                              rangeEnd: range,\n                            },\n                          },\n                          false\n                        );\n                      }}\n                    >\n                      {icon}\n                    </ToggleGroupButton>\n                  </Tooltip>\n                )\n              )}\n\n              <Tooltip content=\"Set custom range\">\n                <ToggleGroupButton\n                  disabled={!isRangeEndEnabled}\n                  onClick={() => {\n                    setIsAdvancedRangeEnd(true);\n                  }}\n                  value=\"advanced\"\n                >\n                  <EllipsesIcon />\n                </ToggleGroupButton>\n              </Tooltip>\n            </ToggleGroup>\n          )}\n          {(isScrollAnimation || isAdvancedRangeEnd) && (\n            <Grid\n              css={{\n                gridColumn: \"2 / -1\",\n                gridTemplateColumns: \"1.5fr 1fr\",\n              }}\n              gap={2}\n            >\n              <Select\n                disabled={!isRangeEndEnabled}\n                options={timelineRangeNames}\n                getLabel={humanizeString}\n                value={value.timing.rangeEnd?.[0] ?? timelineRangeNames[0]!}\n                getDescription={(timelineRangeName: string) => (\n                  <Box\n                    css={{\n                      width: theme.spacing[28],\n                    }}\n                  >\n                    {\n                      timelineRangeDescriptions[\n                        timelineRangeName as keyof typeof timelineRangeDescriptions\n                      ]\n                    }\n                  </Box>\n                )}\n                onItemHighlight={(timelineRangeName) => {\n                  if (timelineRangeName === undefined) {\n                    handleChange(undefined, true);\n                    return;\n                  }\n                  handleChange(\n                    {\n                      ...value,\n                      timing: {\n                        ...value.timing,\n                        rangeEnd: [\n                          timelineRangeName,\n                          value.timing.rangeEnd?.[1] ?? defaultRangeEnd,\n                        ],\n                        rangeStart: value.timing.rangeStart,\n                      },\n                    },\n                    true\n                  );\n                }}\n                onChange={(timelineRangeName) => {\n                  handleChange(\n                    {\n                      ...value,\n                      timing: {\n                        ...value.timing,\n                        rangeEnd: [\n                          timelineRangeName,\n                          value.timing.rangeEnd?.[1] ?? defaultRangeEnd,\n                        ],\n                        rangeStart: value.timing.rangeStart,\n                      },\n                    },\n                    false\n                  );\n                }}\n              />\n\n              <RangeValueInput\n                disabled={!isRangeEndEnabled}\n                value={\n                  value.timing.rangeEnd?.[1] ?? {\n                    type: \"unit\",\n                    value: 0,\n                    unit: \"%\",\n                  }\n                }\n                onChange={(rangeEnd, isEphemeral) => {\n                  if (rangeEnd === undefined && isEphemeral) {\n                    handleChange(undefined, true);\n                    return;\n                  }\n\n                  const defaultTimelineRangeName = timelineRangeNames[0]!;\n\n                  handleChange(\n                    {\n                      ...value,\n                      timing: {\n                        ...value.timing,\n                        rangeEnd: [\n                          value.timing.rangeEnd?.[0] ??\n                            defaultTimelineRangeName,\n                          rangeEnd,\n                        ],\n                      },\n                    },\n                    isEphemeral\n                  );\n                }}\n              />\n            </Grid>\n          )}\n\n          <FieldLabel description=\"When the animation begins, based on how much of the subject is visible\">\n            Range Start\n          </FieldLabel>\n\n          {!isScrollAnimation && (\n            <ToggleGroup\n              value={\n                isAdvancedRangeStart\n                  ? \"advanced\"\n                  : (startRangeValue ?? \"advanced\")\n              }\n              type=\"single\"\n            >\n              {simplifiedStartRanges.map(\n                ([toggleValue, icon, range, description], index) => (\n                  <Tooltip\n                    key={toggleValue}\n                    content={`The animation starts ${description}`}\n                    variant=\"wrapped\"\n                  >\n                    <ToggleGroupButton\n                      key={toggleValue}\n                      disabled={!isRangeStartEnabled}\n                      value={toggleValue}\n                      onClick={() => {\n                        setIsAdvancedRangeStart(false);\n                        handleChange(\n                          {\n                            ...value,\n                            timing: {\n                              ...value.timing,\n                              rangeStart: range,\n                              rangeEnd:\n                                endRangeIndex < index\n                                  ? simplifiedEndRanges[index][2]\n                                  : value.timing.rangeEnd,\n                            },\n                          },\n                          false\n                        );\n                      }}\n                    >\n                      {icon}\n                    </ToggleGroupButton>\n                  </Tooltip>\n                )\n              )}\n\n              <Tooltip content=\"Set custom range\">\n                <ToggleGroupButton\n                  disabled={!isRangeStartEnabled}\n                  onClick={() => {\n                    setIsAdvancedRangeStart(true);\n                  }}\n                  value=\"advanced\"\n                >\n                  <EllipsesIcon />\n                </ToggleGroupButton>\n              </Tooltip>\n            </ToggleGroup>\n          )}\n\n          {(isScrollAnimation || isAdvancedRangeStart) && (\n            <Grid\n              css={{\n                gridColumn: \"2 / -1\",\n                gridTemplateColumns: \"1.5fr 1fr\",\n              }}\n              gap={2}\n            >\n              <Select\n                disabled={!isRangeStartEnabled}\n                options={timelineRangeNames}\n                getLabel={humanizeString}\n                value={value.timing.rangeStart?.[0] ?? timelineRangeNames[0]!}\n                getDescription={(timelineRangeName: string) => (\n                  <Box\n                    css={{\n                      width: theme.spacing[28],\n                    }}\n                  >\n                    {\n                      timelineRangeDescriptions[\n                        timelineRangeName as keyof typeof timelineRangeDescriptions\n                      ]\n                    }\n                  </Box>\n                )}\n                onItemHighlight={(timelineRangeName) => {\n                  if (timelineRangeName === undefined) {\n                    handleChange(undefined, true);\n                    return;\n                  }\n\n                  handleChange(\n                    {\n                      ...value,\n                      timing: {\n                        ...value.timing,\n                        rangeStart: [\n                          timelineRangeName,\n                          value.timing.rangeStart?.[1] ?? defaultRangeStart,\n                        ],\n                        rangeEnd: value.timing.rangeEnd,\n                      },\n                    },\n                    true\n                  );\n                }}\n                onChange={(timelineRangeName) => {\n                  handleChange(\n                    {\n                      ...value,\n                      timing: {\n                        ...value.timing,\n                        rangeStart: [\n                          timelineRangeName,\n                          value.timing.rangeStart?.[1] ?? defaultRangeStart,\n                        ],\n                        rangeEnd: value.timing.rangeEnd,\n                      },\n                    },\n                    false\n                  );\n                }}\n              />\n              <RangeValueInput\n                disabled={!isRangeStartEnabled}\n                value={\n                  value.timing.rangeStart?.[1] ?? {\n                    type: \"unit\",\n                    value: 0,\n                    unit: \"%\",\n                  }\n                }\n                onChange={(rangeStart, isEphemeral) => {\n                  if (rangeStart === undefined && isEphemeral) {\n                    handleChange(undefined, true);\n                    return;\n                  }\n\n                  const defaultTimelineRangeName = timelineRangeNames[0]!;\n\n                  handleChange(\n                    {\n                      ...value,\n                      timing: {\n                        ...value.timing,\n                        rangeStart: [\n                          value.timing.rangeStart?.[0] ??\n                            defaultTimelineRangeName,\n                          rangeStart,\n                        ],\n                      },\n                    },\n                    isEphemeral\n                  );\n                }}\n              />\n            </Grid>\n          )}\n        </Grid>\n        <Grid gap=\"2\" columns=\"3\">\n          <Box>\n            <FieldLabel description=\"Sets a fixed duration instead of using range end.\">\n              Duration\n            </FieldLabel>\n            <DurationInput\n              value={value.timing.duration}\n              onChange={(duration, isEphemeral) => {\n                if (duration === undefined && isEphemeral) {\n                  handleChange(undefined, true);\n                  return;\n                }\n\n                handleChange(\n                  {\n                    ...value,\n                    timing: {\n                      ...value.timing,\n                      duration,\n                    },\n                  },\n                  isEphemeral\n                );\n              }}\n            />\n          </Box>\n\n          <Box>\n            <FieldLabel description=\"Sets a fixed delay before the animation starts instead of using range start.\">\n              Delay\n            </FieldLabel>\n            <DurationInput\n              value={value.timing.delay}\n              onChange={(delay, isEphemeral) => {\n                if (delay === undefined && isEphemeral) {\n                  handleChange(undefined, true);\n                  return;\n                }\n\n                handleChange(\n                  {\n                    ...value,\n                    timing: {\n                      ...value.timing,\n                      delay,\n                    },\n                  },\n                  isEphemeral\n                );\n              }}\n            />\n          </Box>\n\n          <Box>\n            <FieldLabel description=\"Number of times the animation should repeat. Use 'infinite' for continuous loop. Requires duration to be set.\">\n              Iterations\n            </FieldLabel>\n            <IterationsInput\n              disabled={!isIterationsEnabled}\n              value={value.timing.iterations}\n              onChange={(iterations, isEphemeral) => {\n                if (iterations === undefined && isEphemeral) {\n                  handleChange(undefined, true);\n                  return;\n                }\n\n                handleChange(\n                  {\n                    ...value,\n                    timing: {\n                      ...value.timing,\n                      iterations,\n                    },\n                  },\n                  isEphemeral\n                );\n              }}\n            />\n          </Box>\n        </Grid>\n      </Grid>\n\n      <Keyframes\n        value={value.keyframes}\n        onChange={(keyframes, isEphemeral) => {\n          if (keyframes === undefined && isEphemeral) {\n            handleChange(undefined, true);\n            return;\n          }\n\n          handleChange({ ...value, keyframes }, isEphemeral);\n        }}\n      />\n    </PanelContainer>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/animation-section.tsx",
    "content": "import isEqual from \"fast-deep-equal\";\nimport { forwardRef, useState, type ComponentProps } from \"react\";\nimport {\n  Grid,\n  theme,\n  Select,\n  Separator,\n  Box,\n  toast,\n  ToggleGroup,\n  Tooltip,\n  ToggleGroupButton,\n  Text,\n  Switch,\n  FloatingPanel,\n  IconButton,\n} from \"@webstudio-is/design-system\";\nimport type {\n  AnimationAction,\n  AnimationActionScroll,\n  InsetUnitValue,\n} from \"@webstudio-is/sdk\";\nimport {\n  animationActionSchema,\n  insetUnitValueSchema,\n  RANGE_UNITS,\n} from \"@webstudio-is/sdk\";\nimport {\n  ArrowDownIcon,\n  ArrowRightIcon,\n  EllipsesIcon,\n} from \"@webstudio-is/icons\";\nimport { toValue, type StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  CssValueInput,\n  type IntermediateStyleValue,\n} from \"~/builder/features/style-panel/shared/css-value-input\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { FieldLabel } from \"../../property-label\";\nimport type { PropAndMeta } from \"../use-props-logic\";\nimport { AnimationsSelect } from \"./animations-select\";\nimport { SubjectSelect } from \"./subject-select\";\n\nconst animationTypeDescription: Record<AnimationAction[\"type\"], string> = {\n  scroll:\n    \"Scroll-based animations are triggered and controlled by the user’s scroll position.\",\n  view: \"View-based animations occur when an element enters or exits the viewport. They rely on the element’s visibility rather than the scroll position.\",\n};\n\nconst insetDescription =\n  \"Adjusts the animation’s start/end position relative to the scrollport. Positive values move it inward (delaying start or hastening end), while negative values move it outward (starting animation before visibility or continuing after disappearance).\";\n\nconst animationTypes = Object.keys(\n  animationTypeDescription\n) as AnimationAction[\"type\"][];\n\nconst defaultActionValue: AnimationAction = {\n  type: \"view\",\n  animations: [],\n};\n\nconst animationAxisDescription: Record<\n  Exclude<NonNullable<AnimationAction[\"axis\"]>, \"block\" | \"inline\">,\n  { icon: React.ReactNode; label: string; description: React.ReactNode }\n> = {\n  /*\n  // We decided to not support block and inline axis, as mostly not used\n  block: {\n    icon: <ArrowDownIcon />,\n    label: \"Block axis\",\n    description:\n      \"Uses the scroll progress along the block axis (depends on writing mode, usually vertical in English).\",\n  },\n  inline: {\n    icon: <ArrowRightIcon />,\n    label: \"Inline axis\",\n    description:\n      \"Uses the scroll progress along the inline axis (depends on writing mode, usually horizontal in English).\",\n  },\n  */\n\n  y: {\n    label: \"Y axis\",\n    icon: <ArrowDownIcon />,\n    description: \"The scrollbar on the vertical axis of the scroller element.\",\n  },\n  x: {\n    label: \"X axis\",\n    icon: <ArrowRightIcon />,\n    description:\n      \"The scrollbar on the horizontal axis of the scroller element.\",\n  },\n};\n\n/**\n * Support for block and inline axis is removed, as it is not widely used.\n */\nconst convertAxisToXY = (axis: NonNullable<AnimationAction[\"axis\"]>) => {\n  switch (axis) {\n    case \"block\":\n      return \"y\";\n    case \"inline\":\n      return \"x\";\n    default:\n      return axis;\n  }\n};\n\nconst animationSourceDescriptions: Record<\n  NonNullable<AnimationActionScroll[\"source\"]>,\n  string\n> = {\n  nearest: \"Selects the scrolling container that affects the current element.\",\n  root: \"Selects the scrolling element of the document.\",\n  closest: \"Selects the nearest ancestor element that is scrollable.\",\n};\n\nconst unitOptions = RANGE_UNITS.map((unit) => ({\n  id: unit,\n  label: unit,\n  type: \"unit\" as const,\n}));\n\nconst InsetValueInput = ({\n  value,\n  onChange,\n}: {\n  value: InsetUnitValue;\n  onChange: ((value: undefined, isEphemeral: true) => void) &\n    ((value: InsetUnitValue, isEphemeral: boolean) => void);\n}) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  const handleEphemeralChange = (styleValue: unknown | undefined) => {\n    if (styleValue === undefined) {\n      onChange(undefined, true);\n      return;\n    }\n\n    const parsedResult = insetUnitValueSchema.safeParse(styleValue);\n\n    if (parsedResult.success) {\n      onChange(parsedResult.data, true);\n      return;\n    }\n\n    onChange(undefined, true);\n  };\n\n  return (\n    <CssValueInput\n      styleSource=\"default\"\n      value={value}\n      /* marginLeft to allow negative values  */\n      property=\"margin-left\"\n      unitOptions={unitOptions}\n      intermediateValue={intermediateValue}\n      onChange={(styleValue) => {\n        setIntermediateValue(styleValue);\n\n        if (styleValue?.type !== \"intermediate\") {\n          handleEphemeralChange(styleValue);\n        }\n      }}\n      getOptions={() => [\n        {\n          value: \"auto\",\n          type: \"keyword\",\n          description:\n            \"Pick the child element’s viewTimelineInset property or use the scrolling element’s scroll-padding, depending on the selected axis.\",\n        },\n      ]}\n      onHighlight={(value) => {\n        handleEphemeralChange(value);\n      }}\n      onChangeComplete={(event) => {\n        const parsedValue = insetUnitValueSchema.safeParse(event.value);\n        if (parsedValue.success) {\n          onChange(parsedValue.data, false);\n          setIntermediateValue(undefined);\n          return;\n        }\n\n        setIntermediateValue({\n          type: \"invalid\",\n          value: toValue(event.value),\n        });\n      }}\n      onAbort={() => {\n        handleEphemeralChange(undefined);\n      }}\n      onReset={() => {\n        handleEphemeralChange(undefined);\n        setIntermediateValue(undefined);\n      }}\n    />\n  );\n};\n\nconst animationSources = Object.keys(\n  animationSourceDescriptions\n) as NonNullable<AnimationActionScroll[\"source\"]>[];\n\nconst AnimationConfig = ({\n  value,\n  onChange,\n}: {\n  value: AnimationAction;\n  onChange: ((value: AnimationAction, isEphemeral: boolean) => void) &\n    ((value: undefined, isEphemeral: true) => void);\n}) => {\n  return (\n    <Grid gap={2} css={{ padding: theme.panel.padding }}>\n      <Grid gap={1} align=\"center\" columns={2}>\n        <FieldLabel description=\"Type of the timeline defines how the animation is triggered.\">\n          Type\n        </FieldLabel>\n        <Select\n          options={animationTypes}\n          getLabel={humanizeString}\n          value={value.type}\n          getDescription={(animationType) => (\n            <Box css={{ width: theme.spacing[28] }}>\n              {animationTypeDescription[animationType]}\n            </Box>\n          )}\n          onChange={(typeValue) =>\n            onChange({ ...value, type: typeValue, animations: [] }, false)\n          }\n        />\n      </Grid>\n\n      <Grid gap={1} align=\"center\" columns={2}>\n        <FieldLabel description=\"Axis determines whether an animation progresses based on an element’s visibility along the horizontal or vertical direction.\">\n          Axis\n        </FieldLabel>\n        <ToggleGroup\n          css={{ justifySelf: \"end\" }}\n          type=\"single\"\n          value={convertAxisToXY(value.axis ?? (\"y\" as const))}\n          onValueChange={(axis: keyof typeof animationAxisDescription) =>\n            onChange({ ...value, axis: convertAxisToXY(axis) }, false)\n          }\n        >\n          {Object.entries(animationAxisDescription).map(\n            ([key, { icon, label, description }]) => (\n              <Tooltip\n                key={key}\n                variant=\"wrapped\"\n                content={\n                  <Grid gap={1}>\n                    <Text variant={\"titles\"}>{label}</Text>\n                    <Text>{description}</Text>\n                  </Grid>\n                }\n              >\n                <ToggleGroupButton value={key}>{icon}</ToggleGroupButton>\n              </Tooltip>\n            )\n          )}\n        </ToggleGroup>\n      </Grid>\n\n      {value.type === \"scroll\" && (\n        <Grid gap={1} align=\"center\" columns={2}>\n          <FieldLabel description=\"The scroll source is the element whose scrolling behavior drives the animation's progress.\">\n            Scroll Source\n          </FieldLabel>\n          <Select\n            options={animationSources}\n            getLabel={humanizeString}\n            value={value.source ?? \"nearest\"}\n            getDescription={(animationSource) => (\n              <Box css={{ width: theme.spacing[28] }}>\n                {animationSourceDescriptions[animationSource]}\n              </Box>\n            )}\n            onChange={(source) => onChange({ ...value, source }, false)}\n          />\n        </Grid>\n      )}\n\n      {value.type === \"view\" && (\n        <Grid gap={1} align=\"center\" columns={2}>\n          <FieldLabel description=\"The subject is the element whose visibility determines the animation’s progress.\">\n            Subject\n          </FieldLabel>\n          <SubjectSelect value={value} onChange={onChange} />\n        </Grid>\n      )}\n\n      {value.type === \"view\" && (\n        <Grid gap={1} align={\"center\"} css={{ gridTemplateColumns: \"1fr 1fr\" }}>\n          <FieldLabel description={insetDescription}>\n            {value.axis === \"inline\" || value.axis === \"x\"\n              ? \"Left Inset\"\n              : \"Top Inset\"}\n          </FieldLabel>\n          <FieldLabel description={insetDescription}>\n            {value.axis === \"inline\" || value.axis === \"x\"\n              ? \"Right Inset\"\n              : \"Bottom Inset\"}\n          </FieldLabel>\n          <InsetValueInput\n            value={value.insetStart ?? { type: \"keyword\", value: \"auto\" }}\n            onChange={(insetStart, isEphemeral) => {\n              if (insetStart === undefined) {\n                onChange(undefined, true);\n                return;\n              }\n              onChange({ ...value, insetStart }, isEphemeral);\n            }}\n          />\n          <InsetValueInput\n            value={value.insetEnd ?? { type: \"keyword\", value: \"auto\" }}\n            onChange={(insetEnd, isEphemeral) => {\n              if (insetEnd === undefined) {\n                onChange(undefined, true);\n                return;\n              }\n              onChange({ ...value, insetEnd }, isEphemeral);\n            }}\n          />\n        </Grid>\n      )}\n    </Grid>\n  );\n};\n\nconst AnimationConfigButton = forwardRef<\n  HTMLButtonElement,\n  Omit<ComponentProps<typeof IconButton>, \"value\" | \"onChange\"> & {\n    value: AnimationAction;\n    onChange: ((value: AnimationAction, isEphemeral: boolean) => void) &\n      ((value: undefined, isEphemeral: true) => void);\n  }\n>(({ value, onChange, ...props }, ref) => {\n  const { animations: defaultAnimations, ...defaultValue } = defaultActionValue;\n  const { animations, ...newValue } = value;\n  return (\n    <Tooltip content=\"Advanced transform options\">\n      <IconButton\n        {...props}\n        ref={ref}\n        variant={isEqual(defaultValue, newValue) ? \"default\" : \"local\"}\n        onClick={(event) => {\n          if (event.altKey) {\n            onChange(defaultActionValue, false);\n            return;\n          }\n          props.onClick?.(event);\n        }}\n      >\n        <EllipsesIcon />\n      </IconButton>\n    </Tooltip>\n  );\n});\n\nexport const AnimationSection = ({\n  animationAction,\n  onChange,\n  isAnimationEnabled,\n  selectedBreakpointId,\n}: {\n  animationAction: PropAndMeta;\n  onChange: ((value: undefined, isEphemeral: true) => void) &\n    ((value: AnimationAction, isEphemeral: boolean) => void);\n  isAnimationEnabled: (\n    enabled: [breakpointId: string, enabled: boolean][] | undefined\n  ) => boolean | undefined;\n  selectedBreakpointId: string;\n}) => {\n  const { prop } = animationAction;\n\n  const value: AnimationAction =\n    prop?.type === \"animationAction\" ? prop.value : defaultActionValue;\n\n  const handleChange = (value: unknown, isEphemeral: boolean) => {\n    if (value === undefined && isEphemeral) {\n      onChange(undefined, isEphemeral);\n      return;\n    }\n\n    const parsedValue = animationActionSchema.safeParse(value);\n    if (parsedValue.success) {\n      onChange(parsedValue.data, isEphemeral);\n      return;\n    }\n\n    toast.error(\"Invalid animation schema.\");\n  };\n\n  return (\n    <Grid css={{ paddingBottom: theme.panel.paddingBlock }}>\n      <Grid gap={2} css={{ padding: theme.panel.paddingInline }}>\n        <Grid gap={2} align=\"center\" css={{ gridTemplateColumns: \"1fr auto\" }}>\n          <FieldLabel description=\"Even if its off, you can preview the animation by selecting the item in the navigator.\">\n            Run on canvas\n          </FieldLabel>\n          <Tooltip content={value.isPinned ? \"Off\" : \"On\"}>\n            <Switch\n              checked={value.isPinned ?? false}\n              onCheckedChange={(isPinned) => {\n                handleChange({ ...value, isPinned }, false);\n              }}\n            />\n          </Tooltip>\n        </Grid>\n\n        <Grid gap={2} align=\"center\" css={{ gridTemplateColumns: \"1fr auto\" }}>\n          <FieldLabel description=\"Debug mode shows animation progress on canvas in design mode only.\">\n            Debug\n          </FieldLabel>\n          <Switch\n            css={{ justifySelf: \"end\" }}\n            checked={value.debug ?? false}\n            onCheckedChange={(debug) => {\n              handleChange({ ...value, debug }, false);\n            }}\n          />\n        </Grid>\n      </Grid>\n\n      <Separator />\n\n      <Grid gap={2}>\n        <AnimationsSelect\n          action={\n            <FloatingPanel\n              title=\"Advanced Animation\"\n              placement=\"bottom-within\"\n              content={\n                <AnimationConfig value={value} onChange={handleChange} />\n              }\n            >\n              <AnimationConfigButton value={value} onChange={handleChange} />\n            </FloatingPanel>\n          }\n          value={value}\n          onChange={handleChange}\n          isAnimationEnabled={isAnimationEnabled}\n          selectedBreakpointId={selectedBreakpointId}\n        />\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/animation-transforms.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  EnhancedTooltip,\n  Grid,\n  SmallToggleButton,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport {\n  toValue,\n  type CssProperty,\n  type StyleValue,\n  type TupleValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  Link2Icon,\n  Link2UnlinkedIcon,\n  XAxisIcon,\n  YAxisIcon,\n  ZAxisRotateIcon,\n} from \"@webstudio-is/icons\";\nimport { CssValueInputContainer } from \"~/builder/features/style-panel/shared/css-value-input\";\nimport { $availableUnitVariables } from \"~/builder/features/style-panel/shared/model\";\nimport type { StyleUpdateOptions } from \"~/builder/features/style-panel/shared/use-style-data\";\nimport { FieldLabel } from \"../../property-label\";\n\nconst isTupleItem = (value: StyleValue): value is TupleValue[\"value\"][number] =>\n  value.type === \"unit\" || value.type === \"var\" || value.type === \"unparsed\";\n\nexport const transformProperties: CssProperty[] = [\n  \"translate\",\n  \"scale\",\n  \"rotate\",\n  \"opacity\",\n];\n\n// gap linked gap\nconst CONTROLS_GAP = 4 + 16 + 4;\n\nexport const AnimationTransforms = ({\n  styles,\n  onUpdate,\n  onDelete,\n}: {\n  styles: Record<CssProperty, undefined | StyleValue>;\n  onUpdate: (\n    property: CssProperty,\n    value: StyleValue,\n    options?: StyleUpdateOptions\n  ) => void;\n  onDelete: (property: CssProperty, options?: StyleUpdateOptions) => void;\n}) => {\n  const [isScaleLinked, setIsScaleLinked] = useState(\n    styles.scale?.type !== \"tuple\" ||\n      toValue(styles.scale.value[0]) === toValue(styles.scale.value[1])\n  );\n  let translateX: StyleValue = { type: \"unit\", value: 0, unit: \"px\" };\n  let translateY: StyleValue = { type: \"unit\", value: 0, unit: \"px\" };\n  if (styles.translate?.type === \"tuple\") {\n    [translateX, translateY = translateY] = styles.translate.value;\n  }\n  let scaleX: StyleValue = { type: \"unit\", value: 100, unit: \"%\" };\n  let scaleY: StyleValue = { type: \"unit\", value: 100, unit: \"%\" };\n  if (styles.scale?.type === \"tuple\") {\n    [scaleX, scaleY = scaleY] = styles.scale.value;\n  }\n  let rotateZ: StyleValue = { type: \"unit\", value: 0, unit: \"deg\" };\n  if (styles.rotate?.type === \"tuple\") {\n    if (isTupleItem(styles.rotate.value[0])) {\n      rotateZ = styles.rotate.value[0];\n    }\n  }\n  return (\n    <Grid gap={2} css={{ padding: theme.panel.padding }}>\n      <Grid gap={1}>\n        <FieldLabel\n          resettable={styles.translate !== undefined}\n          onReset={() => onDelete(\"translate\")}\n        >\n          Translate\n        </FieldLabel>\n        <Grid columns={2} css={{ gap: CONTROLS_GAP }}>\n          <CssValueInputContainer\n            property=\"translate\"\n            styleSource={styles.translate ? \"local\" : \"default\"}\n            icon={<XAxisIcon />}\n            getOptions={() => $availableUnitVariables.get()}\n            value={translateX}\n            onUpdate={(value, options) => {\n              if (value.type === \"tuple\") {\n                onUpdate(\n                  \"translate\",\n                  { type: \"tuple\", value: [value.value[0], translateY] },\n                  options\n                );\n              }\n              // for scrabbing\n              if (value.type === \"unit\") {\n                onUpdate(\n                  \"translate\",\n                  { type: \"tuple\", value: [value, translateY] },\n                  options\n                );\n              }\n            }}\n            onDelete={(options) => {\n              const value = styles.translate ?? {\n                type: \"tuple\",\n                value: [translateX, translateY],\n              };\n              onUpdate(\"translate\", value, options);\n            }}\n          />\n          <CssValueInputContainer\n            property=\"translate\"\n            styleSource={styles.translate ? \"local\" : \"default\"}\n            icon={<YAxisIcon />}\n            getOptions={() => $availableUnitVariables.get()}\n            value={translateY}\n            onUpdate={(value, options) => {\n              if (value.type === \"tuple\") {\n                onUpdate(\n                  \"translate\",\n                  { type: \"tuple\", value: [translateX, value.value[0]] },\n                  options\n                );\n              }\n              // for scrabbing\n              if (value.type === \"unit\") {\n                onUpdate(\n                  \"translate\",\n                  { type: \"tuple\", value: [translateX, value] },\n                  options\n                );\n              }\n            }}\n            onDelete={(options) => {\n              const value = styles.translate ?? {\n                type: \"tuple\",\n                value: [translateX, translateY],\n              };\n              onUpdate(\"translate\", value, options);\n            }}\n          />\n        </Grid>\n      </Grid>\n\n      <Grid gap={1}>\n        <FieldLabel\n          resettable={styles.scale !== undefined}\n          onReset={() => onDelete(\"scale\")}\n        >\n          Scale\n        </FieldLabel>\n        <Grid\n          gap={1}\n          align=\"center\"\n          css={{ gridTemplateColumns: \"1fr 16px 1fr\" }}\n        >\n          <CssValueInputContainer\n            property=\"scale\"\n            styleSource={styles.scale ? \"local\" : \"default\"}\n            icon={<XAxisIcon />}\n            getOptions={() => $availableUnitVariables.get()}\n            value={scaleX}\n            onUpdate={(value, options) => {\n              if (value.type === \"tuple\") {\n                const newValue: StyleValue = {\n                  type: \"tuple\",\n                  value: isScaleLinked\n                    ? [value.value[0], value.value[0]]\n                    : [value.value[0], scaleY],\n                };\n                onUpdate(\"scale\", newValue, options);\n              }\n              // for scrabbing\n              if (value.type === \"unit\") {\n                const newValue: StyleValue = {\n                  type: \"tuple\",\n                  value: isScaleLinked ? [value, value] : [value, scaleY],\n                };\n                onUpdate(\"scale\", newValue, options);\n              }\n            }}\n            onDelete={(options) => {\n              if (isScaleLinked) {\n                onDelete(\"scale\", options);\n              } else {\n                const value = styles.scale ?? {\n                  type: \"tuple\",\n                  value: [scaleX, scaleY],\n                };\n                onUpdate(\"scale\", value, options);\n              }\n            }}\n          />\n          <EnhancedTooltip\n            content={isScaleLinked ? \"Unlink scale axes\" : \"Link scale axes\"}\n          >\n            <SmallToggleButton\n              variant=\"normal\"\n              icon={isScaleLinked ? <Link2Icon /> : <Link2UnlinkedIcon />}\n              pressed={isScaleLinked}\n              onPressedChange={(isPressed) => {\n                setIsScaleLinked(isPressed);\n                if (isPressed) {\n                  onUpdate(\"scale\", {\n                    type: \"tuple\",\n                    value: [scaleX, scaleX],\n                  });\n                }\n              }}\n            />\n          </EnhancedTooltip>\n          <CssValueInputContainer\n            property=\"scale\"\n            styleSource={styles.scale ? \"local\" : \"default\"}\n            icon={<YAxisIcon />}\n            getOptions={() => $availableUnitVariables.get()}\n            value={scaleY}\n            onUpdate={(value, options) => {\n              if (value.type === \"tuple\") {\n                const newValue: StyleValue = {\n                  type: \"tuple\",\n                  value: isScaleLinked\n                    ? [value.value[0], value.value[0]]\n                    : [scaleX, value.value[0]],\n                };\n                onUpdate(\"scale\", newValue, options);\n              }\n              // for scrabbing\n              if (value.type === \"unit\") {\n                const newValue: StyleValue = {\n                  type: \"tuple\",\n                  value: isScaleLinked ? [value, value] : [scaleX, value],\n                };\n                onUpdate(\"scale\", newValue, options);\n              }\n            }}\n            onDelete={(options) => {\n              if (isScaleLinked) {\n                onDelete(\"scale\", options);\n              } else {\n                const value = styles.scale ?? {\n                  type: \"tuple\",\n                  value: [scaleX, scaleY],\n                };\n                onUpdate(\"scale\", value, options);\n              }\n            }}\n          />\n        </Grid>\n      </Grid>\n\n      <Grid columns={2} css={{ gap: CONTROLS_GAP }}>\n        <Grid gap={1}>\n          <FieldLabel\n            resettable={styles.rotate !== undefined}\n            onReset={() => onDelete(\"rotate\")}\n          >\n            Rotate\n          </FieldLabel>\n          <CssValueInputContainer\n            property=\"rotate\"\n            styleSource={styles.rotate ? \"local\" : \"default\"}\n            icon={<ZAxisRotateIcon />}\n            getOptions={() => $availableUnitVariables.get()}\n            value={rotateZ}\n            onUpdate={(value, options) => {\n              if (value.type === \"tuple\" && isTupleItem(value.value[0])) {\n                const [rotateZ] = value.value;\n                onUpdate(\n                  \"rotate\",\n                  { type: \"tuple\", value: [rotateZ] },\n                  options\n                );\n              } else if (isTupleItem(value)) {\n                // for scrabbing\n                onUpdate(\"rotate\", { type: \"tuple\", value: [value] }, options);\n              } else {\n                onDelete(\"rotate\", options);\n              }\n            }}\n            onDelete={(options) => onDelete(\"rotate\", options)}\n          />\n        </Grid>\n        <Grid gap={1}>\n          <FieldLabel\n            resettable={styles.opacity !== undefined}\n            onReset={() => onDelete(\"opacity\")}\n          >\n            Opacity\n          </FieldLabel>\n          <CssValueInputContainer\n            property=\"opacity\"\n            styleSource={styles.opacity ? \"local\" : \"default\"}\n            getOptions={() => $availableUnitVariables.get()}\n            value={styles.opacity ?? { type: \"unit\", value: 100, unit: \"%\" }}\n            onUpdate={(value, options) => {\n              onUpdate(\"opacity\", value, options);\n            }}\n            onDelete={(options) => onDelete(\"opacity\", options)}\n          />\n        </Grid>\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/animations-select.tsx",
    "content": "import { useState, useMemo, type ReactNode, useRef } from \"react\";\nimport {\n  theme,\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  rawTheme,\n  CssValueListItem,\n  SmallToggleButton,\n  SmallIconButton,\n  Box,\n  Label,\n  Grid,\n  useSortable,\n  CssValueListArrowFocus,\n  FloatingPanel,\n  DialogTitle,\n  Tooltip,\n  SectionTitleButton,\n  SectionTitle,\n  SectionTitleLabel,\n  Flex,\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuItem,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport {\n  EyeClosedIcon,\n  EyeOpenIcon,\n  MinusIcon,\n  PlusIcon,\n} from \"@webstudio-is/icons\";\nimport {\n  scrollAnimationSchema,\n  viewAnimationSchema,\n  type AnimationAction,\n  type ScrollAnimation,\n  type ViewAnimation,\n} from \"@webstudio-is/sdk\";\nimport { newScrollAnimations } from \"./new-scroll-animations\";\nimport { newViewAnimations } from \"./new-view-animations\";\nimport { AnimationPanelContent } from \"./animation-panel-content\";\nimport { CollapsibleSectionRoot } from \"~/builder/shared/collapsible-section\";\nimport { z } from \"zod\";\n\nconst newAnimationsPerType: {\n  scroll: ScrollAnimation[];\n  view: ViewAnimation[];\n} = {\n  scroll: newScrollAnimations,\n  view: newViewAnimations,\n};\n\ntype AnimationsSelectProps = {\n  action?: ReactNode;\n  value: AnimationAction;\n  onChange: ((value: unknown, isEphemeral: boolean) => void) &\n    ((value: undefined, isEphemeral: true) => void);\n  isAnimationEnabled: (\n    enabled: [breakpointId: string, enabled: boolean][] | undefined\n  ) => boolean | undefined;\n  selectedBreakpointId: string;\n};\n\nconst floatingPanelOffset = { alignmentAxis: -100 };\n\nconst copyAttribute = \"data-animation-index\";\n\nconst clipboardNamespace = \"@webstudio/animation/v0.1\";\n\nconst serialize = (animations: (ScrollAnimation | ViewAnimation)[]) => {\n  return JSON.stringify({ [clipboardNamespace]: animations });\n};\n\nconst parseViewAnimations = (text: string): ViewAnimation[] => {\n  const data = JSON.parse(text);\n  const parsed = z\n    .object({ [clipboardNamespace]: z.array(viewAnimationSchema) })\n    .parse(data);\n  return parsed[clipboardNamespace];\n};\n\nconst parseScrollAnimations = (text: string): ScrollAnimation[] => {\n  const data = JSON.parse(text);\n  const parsed = z\n    .object({ [clipboardNamespace]: z.array(scrollAnimationSchema) })\n    .parse(data);\n  return parsed[clipboardNamespace];\n};\n\nconst AnimationContextMenu = ({\n  action,\n  onChange,\n  children,\n}: {\n  action: AnimationAction;\n  onChange: (newAction: AnimationAction) => void;\n  children: ReactNode;\n}) => {\n  const lastClickedAnimationIndex = useRef(-1);\n\n  const copyAnimation = () => {\n    const index = lastClickedAnimationIndex.current;\n    const animations =\n      index === -1 ? action.animations : [action.animations[index]];\n    navigator.clipboard.writeText(serialize(animations));\n  };\n\n  const copyAllAnimations = () => {\n    navigator.clipboard.writeText(serialize(action.animations));\n  };\n\n  const pasteAnimations = () => {\n    const index = lastClickedAnimationIndex.current;\n    navigator.clipboard\n      .readText()\n      .then((text) => {\n        if (action.type === \"scroll\") {\n          const animations = parseScrollAnimations(text);\n          const newAction = structuredClone(action);\n          newAction.animations.splice(index + 1, 0, ...animations);\n          onChange(newAction);\n        }\n        if (action.type === \"view\") {\n          const animations = parseViewAnimations(text);\n          const newAction = structuredClone(action);\n          newAction.animations.splice(index + 1, 0, ...animations);\n          onChange(newAction);\n        }\n      })\n      .catch((error) => {\n        toast.error(\"Pasted data is not valid animation\");\n        console.error(error);\n      });\n  };\n\n  const deleteAnimation = () => {\n    const index = lastClickedAnimationIndex.current;\n    if (index === -1) {\n      return;\n    }\n    const newAction = structuredClone(action);\n    newAction.animations.splice(index, 1);\n    onChange(newAction);\n  };\n\n  return (\n    <ContextMenu>\n      <ContextMenuTrigger\n        onPointerDown={(event) => {\n          if (event.target instanceof HTMLElement) {\n            const animationIndex = event.target\n              .closest(`[${copyAttribute}]`)\n              ?.getAttribute(copyAttribute);\n            lastClickedAnimationIndex.current = Number(animationIndex ?? -1);\n          }\n        }}\n      >\n        {children}\n      </ContextMenuTrigger>\n      <ContextMenuContent css={{ width: theme.spacing[25] }}>\n        <ContextMenuItem onSelect={copyAnimation}>\n          Copy animation\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={copyAllAnimations}>\n          Copy all animations\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={pasteAnimations}>\n          Paste animations\n        </ContextMenuItem>\n        <ContextMenuItem destructive onSelect={deleteAnimation}>\n          Delete animation\n        </ContextMenuItem>\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n};\n\nexport const AnimationsSelect = ({\n  action,\n  value,\n  onChange,\n  isAnimationEnabled,\n  selectedBreakpointId,\n}: AnimationsSelectProps) => {\n  const [newAnimationHint, setNewAnimationHint] = useState<string | undefined>(\n    undefined\n  );\n\n  const newAnimations = newAnimationsPerType[value.type];\n\n  const sortableItems = useMemo(\n    () => value.animations.map((_, index) => ({ id: `${index}`, index })),\n    [value.animations]\n  );\n\n  const { dragItemId, placementIndicator, sortableRefCallback } = useSortable({\n    items: sortableItems,\n    onSort: (newIndex, oldIndex) => {\n      const newAnimations = [...value.animations];\n      const [movedItem] = newAnimations.splice(oldIndex, 1);\n      newAnimations.splice(newIndex, 0, movedItem);\n      const newValue = { ...value, animations: newAnimations };\n      onChange(newValue, false);\n    },\n  });\n\n  const handleChange = (newValue: unknown, isEphemeral: boolean) => {\n    onChange(newValue, isEphemeral);\n  };\n\n  return (\n    <AnimationContextMenu\n      action={value}\n      onChange={(newAction) => handleChange(newAction, false)}\n    >\n      <CollapsibleSectionRoot\n        isOpen\n        fullWidth\n        trigger={\n          <SectionTitle\n            collapsible={false}\n            suffix={\n              <Flex gap=\"1\" align=\"center\">\n                {action}\n                <DropdownMenu>\n                  <DropdownMenuTrigger asChild>\n                    <SectionTitleButton prefix={<PlusIcon />} tabIndex={0} />\n                  </DropdownMenuTrigger>\n                  <DropdownMenuContent\n                    sideOffset={Number.parseFloat(rawTheme.spacing[5])}\n                    css={{ width: theme.spacing[25] }}\n                  >\n                    {newAnimations.map((animation, index) => (\n                      <DropdownMenuItem\n                        key={index}\n                        onSelect={() => {\n                          handleChange(\n                            {\n                              ...value,\n                              animations: value.animations.concat(animation),\n                            },\n                            false\n                          );\n                        }}\n                        onFocus={() =>\n                          setNewAnimationHint(animation.description)\n                        }\n                        onBlur={() => setNewAnimationHint(undefined)}\n                      >\n                        {animation.name}\n                      </DropdownMenuItem>\n                    ))}\n\n                    <DropdownMenuSeparator />\n\n                    <DropdownMenuItem css={{ display: \"grid\" }} hint>\n                      {newAnimations.map(({ description }, index) => (\n                        <Box\n                          css={{\n                            gridColumn: \"1\",\n                            gridRow: \"1\",\n                            visibility: \"hidden\",\n                          }}\n                          key={index}\n                        >\n                          {description}\n                        </Box>\n                      ))}\n                      <Box\n                        css={{\n                          gridColumn: \"1\",\n                          gridRow: \"1\",\n                        }}\n                      >\n                        {newAnimationHint ??\n                          \"Add new or select existing animation\"}\n                      </Box>\n                    </DropdownMenuItem>\n                  </DropdownMenuContent>\n                </DropdownMenu>\n              </Flex>\n            }\n          >\n            <SectionTitleLabel>Animations</SectionTitleLabel>\n          </SectionTitle>\n        }\n      >\n        <CssValueListArrowFocus dragItemId={dragItemId}>\n          <Grid ref={sortableRefCallback}>\n            {value.animations.map((animation, index) => {\n              const isEnabled = isAnimationEnabled(animation.enabled) ?? true;\n\n              return (\n                <FloatingPanel\n                  key={index}\n                  title={\n                    <DialogTitle css={{ paddingLeft: theme.spacing[6] }}>\n                      {animation.name}\n                    </DialogTitle>\n                  }\n                  content={\n                    <AnimationPanelContent\n                      type={value.type}\n                      value={animation}\n                      onChange={(animation, isEphemeral) => {\n                        if (animation === undefined) {\n                          // Reset ephemeral state\n                          handleChange(undefined, true);\n                          return;\n                        }\n\n                        const newAnimations = [...value.animations];\n                        newAnimations[index] = animation;\n                        const newValue = {\n                          ...value,\n                          animations: newAnimations,\n                        };\n                        handleChange(newValue, isEphemeral);\n                      }}\n                    />\n                  }\n                  offset={floatingPanelOffset}\n                >\n                  <CssValueListItem\n                    key={index}\n                    {...{ [copyAttribute]: index }}\n                    label={\n                      <Label disabled={false} truncate>\n                        {animation.name ?? \"Unnamed\"}\n                      </Label>\n                    }\n                    hidden={!isEnabled}\n                    draggable\n                    active={dragItemId === String(index)}\n                    state={undefined}\n                    index={index}\n                    id={String(index)}\n                    buttons={\n                      <>\n                        <Tooltip\n                          content={\n                            isEnabled\n                              ? \"Disable animation at breakpoint\"\n                              : \"Enable animation at breakpoint\"\n                          }\n                        >\n                          <SmallToggleButton\n                            pressed={!isEnabled}\n                            onPressedChange={() => {\n                              const enabledMap = new Map(animation.enabled);\n                              enabledMap.set(selectedBreakpointId, !isEnabled);\n\n                              const enabled = [...enabledMap];\n\n                              const newAnimations = [...value.animations];\n                              const newAnimation = {\n                                ...animation,\n                                enabled: enabled.every(\n                                  ([_, enabled]) => enabled\n                                )\n                                  ? undefined\n                                  : [...enabledMap],\n                              };\n\n                              newAnimations[index] = newAnimation;\n\n                              const newValue = {\n                                ...value,\n                                animations: newAnimations,\n                              };\n                              handleChange(newValue, false);\n                            }}\n                            variant=\"normal\"\n                            tabIndex={-1}\n                            icon={\n                              isEnabled ? <EyeOpenIcon /> : <EyeClosedIcon />\n                            }\n                          />\n                        </Tooltip>\n\n                        <SmallIconButton\n                          variant=\"destructive\"\n                          tabIndex={-1}\n                          icon={<MinusIcon />}\n                          onClick={() => {\n                            const newAnimations = [...value.animations];\n                            newAnimations.splice(index, 1);\n\n                            const newValue = {\n                              ...value,\n                              animations: newAnimations,\n                            };\n                            handleChange(newValue, false);\n                          }}\n                        />\n                      </>\n                    }\n                  />\n                </FloatingPanel>\n              );\n            })}\n            {placementIndicator}\n          </Grid>\n        </CssValueListArrowFocus>\n      </CollapsibleSectionRoot>\n    </AnimationContextMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/keyframe-helpers.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { calcOffsets, findInsertionIndex, moveItem } from \"./keyframe-helpers\";\n\ndescribe(\"calcOffsets\", () => {\n  test(\"returns empty array for empty input\", () => {\n    expect(calcOffsets([])).toEqual([]);\n  });\n\n  test(\"handles array with all offsets defined\", () => {\n    const keyframes = [{ offset: 0 }, { offset: 0.5 }, { offset: 1 }];\n    expect(calcOffsets(keyframes)).toEqual([0, 0.5, 1]);\n  });\n\n  test(\"adds implicit start and end offsets\", () => {\n    const keyframes = [\n      { offset: undefined },\n      { offset: 0.5 },\n      { offset: undefined },\n    ];\n    expect(calcOffsets(keyframes)).toEqual([0, 0.5, 1]);\n  });\n\n  test(\"interpolates missing middle offsets\", () => {\n    const keyframes = [{ offset: 0 }, { offset: undefined }, { offset: 1 }];\n    expect(calcOffsets(keyframes)).toEqual([0, 0.5, 1]);\n  });\n\n  test(\"handles multiple gaps in offsets\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: 1 },\n    ];\n    expect(calcOffsets(keyframes)).toEqual([0, 0.25, 0.5, 0.75, 1]);\n  });\n\n  test(\"handles multiple gaps in offsets\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: 0.51 },\n      { offset: undefined },\n      { offset: 1 },\n    ];\n\n    expect(calcOffsets(keyframes)).toEqual([0, 0.17, 0.34, 0.51, 0.755, 1]);\n  });\n\n  test(\"handles multiple gaps in offsets\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: 0.5 },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: 1 },\n    ];\n\n    expect(calcOffsets(keyframes)).toEqual([\n      0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1,\n    ]);\n  });\n});\n\ndescribe(\"findInsertionIndex\", () => {\n  test(\"should keep current index when it's in the correct position\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: 0.5, current: true },\n      { offset: 0.7 },\n      { offset: 1 },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual(keyframes);\n  });\n\n  test(\"should move index forward when current position is too early\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: 0.8, current: true },\n      { offset: 0.7 },\n      { offset: 1 },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual([\n      { offset: 0 },\n      { offset: 0.7 },\n      { offset: 0.8, current: true },\n      { offset: 1 },\n    ]);\n  });\n\n  test(\"should move index backward when current position is too late\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: 0.3 },\n      { offset: 0.7 },\n      { offset: 0.35, current: true },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual([\n      { offset: 0 },\n      { offset: 0.3 },\n      { offset: 0.35, current: true },\n      { offset: 0.7 },\n    ]);\n  });\n\n  test(\"should move index backward at 1 when current position is too late\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: 0.3 },\n      { offset: 0.7 },\n      { offset: 0, current: true },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual([\n      { offset: 0 },\n      { offset: 0, current: true },\n      { offset: 0.3 },\n      { offset: 0.7 },\n    ]);\n  });\n\n  test(\"should move index backward at 0 when current position is too late\", () => {\n    const keyframes = [\n      { offset: 0.1 },\n      { offset: 0.3 },\n      { offset: 0.7 },\n      { offset: 0, current: true },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual([\n      { offset: 0, current: true },\n      { offset: 0.1 },\n      { offset: 0.3 },\n      { offset: 0.7 },\n    ]);\n  });\n\n  test(\"should preserve position with undefined around\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: undefined },\n      { offset: 0.2, current: true },\n      { offset: undefined },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual([\n      { offset: 0 },\n      { offset: undefined },\n      { offset: 0.2, current: true },\n      { offset: undefined },\n    ]);\n  });\n\n  test(\"should move position with undefined around if needed\", () => {\n    const keyframes = [\n      { offset: 0 },\n      { offset: undefined },\n      { offset: 0.5, current: true },\n      { offset: undefined },\n      { offset: 0.3 },\n      { offset: undefined },\n      { offset: undefined },\n    ];\n    const currentIndex = keyframes.findIndex((k) => k.current)!;\n\n    expect(\n      moveItem(\n        keyframes,\n        currentIndex,\n        findInsertionIndex(keyframes, currentIndex)\n      )\n    ).toEqual([\n      { offset: 0 },\n      { offset: undefined },\n      { offset: undefined },\n      { offset: 0.3 },\n      { offset: 0.5, current: true },\n      { offset: undefined },\n      { offset: undefined },\n    ]);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/keyframe-helpers.ts",
    "content": "const multiplier = 10000;\n\nexport const calcOffsets = (\n  keyframes: { offset?: number | undefined }[]\n): number[] => {\n  if (keyframes.length === 0) {\n    return [];\n  }\n\n  const offsets = keyframes.map((k) =>\n    k.offset !== undefined ? k.offset * multiplier : undefined\n  );\n\n  if (offsets.length === 1 && offsets[0] === undefined) {\n    return [1];\n  }\n\n  if (offsets[0] === undefined) {\n    offsets[0] = 0;\n  }\n\n  if (offsets.at(-1) === undefined) {\n    offsets[offsets.length - 1] = 1 * multiplier;\n  }\n\n  let prev = 0;\n\n  for (let i = 1; i < offsets.length - 1; ) {\n    if (offsets[i] !== undefined) {\n      prev = i;\n      i++;\n      continue;\n    }\n\n    if (offsets[i] === undefined) {\n      const next = offsets.findIndex((v, vi) => vi >= i && v !== undefined);\n\n      const step = (offsets[next]! - offsets[prev]!) / (next - prev);\n\n      for (let j = prev + 1; j < next; j++) {\n        offsets[j] = offsets[j - 1]! + step; // offsets[prev]! + step * (j - prev);\n      }\n      prev = next;\n\n      i = next + 1;\n    }\n  }\n  return offsets.map((v) => v! / multiplier);\n};\n\n// [0, undefined, undefined, 0.5, undefined, 1]\nexport const findInsertionIndex = (\n  keyframes: { offset?: number | undefined }[],\n  currentIndex: number\n) => {\n  const offset = keyframes[currentIndex].offset;\n  if (offset === undefined) {\n    return currentIndex;\n  }\n\n  // There are 2 ways to approach this. Using calcOffsets to find the index would be technically more correct,\n  // as it would place the keyframe in the right position. However, visually placing something between \"auto\" keyframes\n  // is not very intuitive. Let's try the \"visually correct\" way first.\n\n  // Check ordering\n  const minLastIndex = keyframes.findLastIndex(\n    (keyframe, keyframeIndex) =>\n      keyframeIndex !== currentIndex &&\n      keyframe.offset !== undefined &&\n      keyframe.offset < offset\n  );\n\n  const maxFirstIndex = keyframes.findIndex(\n    (keyframe, keyframeIndex) =>\n      keyframeIndex !== currentIndex &&\n      keyframe.offset !== undefined &&\n      keyframe.offset > offset\n  );\n\n  if (currentIndex < minLastIndex) {\n    // [0, 0.5, 0.6, 1], currentIndex = 1, offset = 0.8\n    return minLastIndex;\n  }\n\n  if (currentIndex > maxFirstIndex && maxFirstIndex !== -1) {\n    // [0, 0.5, 0.6, 1], currentIndex = 2, offset = 0.4\n    return maxFirstIndex;\n  }\n\n  // Do nothing if the current index is already in the right place\n  return currentIndex;\n};\n\nexport const moveItem = <T>(\n  keyframes: T[],\n  currentIndex: number,\n  newIndex: number\n): T[] => {\n  const newKeyframes = [...keyframes];\n  const [keyframe] = newKeyframes.splice(currentIndex, 1);\n  newKeyframes.splice(newIndex, 0, keyframe);\n  return newKeyframes;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/new-scroll-animations.ts",
    "content": "import { parseCssValue } from \"@webstudio-is/css-data\";\nimport type { ScrollAnimation } from \"@webstudio-is/sdk\";\n\nconst newScrollAnimation: ScrollAnimation = {\n  name: \"New Animation\",\n  description: \"Create a new animation.\",\n\n  timing: {\n    rangeStart: [\"start\", { type: \"unit\", value: 0, unit: \"px\" }],\n    rangeEnd: [\"end\", { type: \"unit\", value: 0, unit: \"px\" }],\n    fill: \"both\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {},\n    },\n  ],\n};\n\n// @todo: visit https://github.com/argyleink/open-props/blob/main/src/props.animations.css\nconst newFadeInScrollAnimation: ScrollAnimation = {\n  name: \"Fade In\",\n  description: \"Fade in the element as it scrolls into view.\",\n\n  timing: {\n    rangeStart: [\"start\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"start\", { type: \"unit\", value: 50, unit: \"dvh\" }],\n    fill: \"backwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        opacity: parseCssValue(\"opacity\", \"0\"),\n      },\n    },\n  ],\n};\n\nconst newFadeOutScrollAnimation: ScrollAnimation = {\n  name: \"Fade Out\",\n  description: \"Fade out the element as it scrolls out of view.\",\n\n  timing: {\n    rangeStart: [\"end\", { type: \"unit\", value: 50, unit: \"dvh\" }],\n    rangeEnd: [\"end\", { type: \"unit\", value: 0, unit: \"%\" }],\n    fill: \"backwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 1,\n      styles: {\n        opacity: parseCssValue(\"opacity\", \"0\"),\n      },\n    },\n  ],\n};\n\nexport const newScrollAnimations = [\n  newScrollAnimation,\n  newFadeInScrollAnimation,\n  newFadeOutScrollAnimation,\n];\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/new-view-animations.ts",
    "content": "import { parseCssValue } from \"@webstudio-is/css-data\";\nimport type { ViewAnimation } from \"@webstudio-is/sdk\";\n\nconst newViewAnimation: ViewAnimation = {\n  name: \"New Animation\",\n  description: \"Create a new animation.\",\n\n  timing: {\n    rangeStart: [\"cover\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"cover\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"both\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {},\n    },\n  ],\n};\n\n// @todo: visit https://github.com/argyleink/open-props/blob/main/src/props.animations.css\nconst newFadeInViewAnimation: ViewAnimation = {\n  name: \"Fade In\",\n  description: \"Fade in the element as it scrolls into view.\",\n\n  timing: {\n    rangeStart: [\"entry\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"entry\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"backwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        opacity: parseCssValue(\"opacity\", \"0\"),\n      },\n    },\n  ],\n};\n\nconst newFadeOutViewAnimation: ViewAnimation = {\n  name: \"Fade Out\",\n  description: \"Fade out the element as it scrolls out of view.\",\n\n  timing: {\n    rangeStart: [\"exit\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"exit\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"forwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 1,\n      styles: {\n        opacity: parseCssValue(\"opacity\", \"0\"),\n      },\n    },\n  ],\n};\n\nconst newFlyInViewAnimation: ViewAnimation = {\n  name: \"Fly In\",\n  description: \"A fly in animation moves an element as it scrolls into view.\",\n\n  timing: {\n    rangeStart: [\"entry\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"entry\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"backwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        translate: parseCssValue(\"translate\", \"0 100px\"),\n      },\n    },\n  ],\n};\n\nconst newFlyOutViewAnimation: ViewAnimation = {\n  name: \"Fly Out\",\n  description:\n    \"A fly out animation moves an element as it scrolls out of view.\",\n\n  timing: {\n    rangeStart: [\"exit\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"exit\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"forwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 1,\n      styles: {\n        translate: parseCssValue(\"translate\", \"0 -100px\"),\n      },\n    },\n  ],\n};\n\nconst newWipeInViewAnimation: ViewAnimation = {\n  name: \"Wipe In\",\n  description:\n    \"A wipe in is an animation where one scene gradually replaces another as it scrolls into the view.\",\n\n  timing: {\n    rangeStart: [\"contain\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"contain\", { type: \"unit\", value: 50, unit: \"%\" }],\n    fill: \"backwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        \"clip-path\": parseCssValue(\"clip-path\", \"inset(0 100% 0 0)\"),\n      },\n    },\n    {\n      offset: 1,\n      styles: {\n        \"clip-path\": parseCssValue(\"clip-path\", \"inset(0 0 0 0)\"),\n      },\n    },\n  ],\n};\n\nconst newWipeOutViewAnimation: ViewAnimation = {\n  name: \"Wipe Out\",\n  description:\n    \"A wipe out is an animation where one scene gradually replaces another as it scrolls out of view.\",\n\n  timing: {\n    rangeStart: [\"contain\", { type: \"unit\", value: 50, unit: \"%\" }],\n    rangeEnd: [\"contain\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"forwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        \"clip-path\": parseCssValue(\"clip-path\", \"inset(0 0 0 0)\"),\n      },\n    },\n\n    {\n      offset: 1,\n      styles: {\n        \"clip-path\": parseCssValue(\"clip-path\", \"inset(0 0 0 100%)\"),\n      },\n    },\n  ],\n};\n\nconst newParallaxInAnimation: ViewAnimation = {\n  name: \"Parallax In\",\n  description: \"Parallax the element as it scrolls into the view.\",\n\n  timing: {\n    rangeStart: [\"cover\", { type: \"unit\", value: 0, unit: \"%\" }],\n    rangeEnd: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n    fill: \"backwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 0,\n      styles: {\n        translate: parseCssValue(\"translate\", \"0 100px\"),\n      },\n    },\n  ],\n};\n\nconst newParallaxOutAnimation: ViewAnimation = {\n  name: \"Parallax Out\",\n  description: \"Parallax the element as it scrolls out of view.\",\n\n  timing: {\n    rangeStart: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n    rangeEnd: [\"cover\", { type: \"unit\", value: 100, unit: \"%\" }],\n    fill: \"forwards\",\n    easing: \"linear\",\n  },\n  keyframes: [\n    {\n      offset: 1,\n      styles: {\n        translate: parseCssValue(\"translate\", \"0 -100px\"),\n      },\n    },\n  ],\n};\n\nexport const newViewAnimations = [\n  newViewAnimation,\n  newFadeInViewAnimation,\n  newFadeOutViewAnimation,\n  newFlyInViewAnimation,\n  newFlyOutViewAnimation,\n  newWipeInViewAnimation,\n  newWipeOutViewAnimation,\n  newParallaxInAnimation,\n  newParallaxOutAnimation,\n];\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/set-css-property.test.tsx",
    "content": "import { createRegularStyleSheet } from \"@webstudio-is/css-engine\";\nimport { ROOT_INSTANCE_ID, type WebstudioData } from \"@webstudio-is/sdk\";\nimport { $, ws, css, renderData } from \"@webstudio-is/template\";\nimport { expect, test } from \"vitest\";\nimport { setListedCssProperty } from \"./set-css-property\";\n\nconst toCss = (data: Omit<WebstudioData, \"pages\">) => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"base\");\n  for (const { instanceId, values } of data.styleSourceSelections.values()) {\n    for (const styleSourceId of values) {\n      const styleSource = data.styleSources.get(styleSourceId);\n      let name;\n      if (styleSource?.type === \"local\") {\n        name = `${instanceId}:local`;\n      }\n      if (styleSource?.type === \"token\") {\n        name = `${instanceId}:token(${styleSource.name})`;\n      }\n      if (name) {\n        const rule = sheet.addNestingRule(name);\n        for (const styleDecl of data.styles.values()) {\n          if (styleDecl.styleSourceId === styleSourceId) {\n            rule.setDeclaration({\n              breakpoint: styleDecl.breakpointId,\n              selector: styleDecl.state ?? \"\",\n              property: styleDecl.property,\n              value: styleDecl.value,\n            });\n          }\n        }\n      }\n    }\n  }\n  return sheet.cssText;\n};\n\ntest(\"Add Css Property styles\", () => {\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID}>\n      <$.Body>\n        <$.Box ws:id=\"boxId\">\n          <$.Box\n            ws:id=\"boxChildId\"\n            ws:style={css`\n              color: red;\n            `}\n          ></$.Box>\n        </$.Box>\n      </$.Body>\n    </ws.root>\n  );\n\n  setListedCssProperty(\n    data.breakpoints,\n    data.styleSources,\n    data.styleSourceSelections,\n    data.styles\n  )(\"boxChildId\", \"view-timeline-name\", {\n    type: \"unparsed\",\n    value: \"--view-timeline-name-child\",\n  });\n\n  setListedCssProperty(\n    data.breakpoints,\n    data.styleSources,\n    data.styleSourceSelections,\n    data.styles\n  )(\"boxId\", \"view-timeline-name\", {\n    type: \"unparsed\",\n    value: \"--view-timeline-name\",\n  });\n\n  expect(toCss(data)).toMatchInlineSnapshot(`\n    \"@media all {\n      boxChildId:local {\n        color: red;\n        --view-timeline-name: --view-timeline-name-child;\n        view-timeline-name: --view-timeline-name-child\n      }\n      boxId:local {\n        --view-timeline-name: --view-timeline-name;\n        view-timeline-name: --view-timeline-name\n      }\n    }\"\n  `);\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/set-css-property.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  getStyleDeclKey,\n  StyleDecl,\n  type Breakpoints,\n  type Instance,\n  type Styles,\n  type StyleSources,\n  type StyleSourceSelections,\n} from \"@webstudio-is/sdk\";\n\nimport { isBaseBreakpoint } from \"~/shared/nano-states\";\nimport { camelCaseProperty } from \"@webstudio-is/css-data\";\n\nexport const setListedCssProperty =\n  (\n    breakpoints: Breakpoints,\n    // Mutated\n    styleSources: StyleSources,\n    styleSourceSelections: StyleSourceSelections,\n    styles: Styles\n  ) =>\n  (instanceId: Instance[\"id\"], property: CssProperty, value: StyleValue) => {\n    if (!styleSourceSelections.has(instanceId)) {\n      const styleSourceId = nanoid();\n      styleSources.set(styleSourceId, { type: \"local\", id: styleSourceId });\n\n      styleSourceSelections.set(instanceId, {\n        instanceId,\n        values: [styleSourceId],\n      });\n    }\n\n    const styleSourceSelection = styleSourceSelections.get(instanceId)!;\n\n    const localStyleSorceId = styleSourceSelection.values.find(\n      (styleSourceId) => styleSources.get(styleSourceId)?.type === \"local\"\n    );\n\n    if (localStyleSorceId === undefined) {\n      throw new Error(\"Local style source not found\");\n    }\n\n    const baseBreakpoint = Array.from(breakpoints.values()).find(\n      isBaseBreakpoint\n    );\n\n    if (baseBreakpoint === undefined) {\n      throw new Error(\"Base breakpoint not found\");\n    }\n\n    const styleDecl: StyleDecl = {\n      breakpointId: baseBreakpoint.id,\n      property: camelCaseProperty(property),\n      styleSourceId: localStyleSorceId,\n      value,\n      listed: true,\n    };\n    styles.set(getStyleDeclKey(styleDecl), styleDecl);\n  };\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/animation/subject-select.tsx",
    "content": "import { Select, toast } from \"@webstudio-is/design-system\";\nimport { nanoid } from \"nanoid\";\nimport { useState } from \"react\";\nimport {\n  $hoveredInstanceSelector,\n  $instances,\n  $registeredComponentMetas,\n  $selectedInstanceSelector,\n} from \"~/shared/nano-states\";\nimport { getInstanceStyleDecl } from \"~/builder/features/style-panel/shared/model\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport type { AnimationAction } from \"@webstudio-is/sdk\";\nimport { setListedCssProperty } from \"./set-css-property\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\n\nconst initSubjects = () => {\n  const selectedInstanceSelector = $selectedInstanceSelector.get();\n  const instances = $instances.get();\n  const metas = $registeredComponentMetas.get();\n\n  if (\n    selectedInstanceSelector === undefined ||\n    selectedInstanceSelector.length === 0\n  ) {\n    return [];\n  }\n\n  const subjects = [\n    {\n      value: \"self\",\n      label: \"Self\",\n      isTimelineExists: true,\n      instanceId: selectedInstanceSelector.at(0)!,\n      selector: selectedInstanceSelector,\n    },\n  ];\n\n  for (\n    let selector = selectedInstanceSelector.slice(1);\n    selector.length !== 0;\n    selector = selector.slice(1)\n  ) {\n    const styleDecl = getInstanceStyleDecl(\"view-timeline-name\", selector);\n    const instanceId = selector.at(0)!;\n\n    const instance = instances.get(selector[0]);\n    if (instance === undefined) {\n      continue;\n    }\n    const meta = metas.get(instance.component);\n\n    if (meta === undefined) {\n      continue;\n    }\n\n    const viewTimelineName = toValue(styleDecl.computedValue);\n    const isTimelineExists = viewTimelineName.startsWith(\"--\");\n    const value = isTimelineExists\n      ? viewTimelineName\n      : `--generated-timeline-${nanoid()}`;\n\n    subjects.push({\n      value,\n      label: getInstanceLabel(instance),\n      isTimelineExists,\n      instanceId,\n      selector,\n    });\n  }\n\n  return subjects;\n};\n\nexport const SubjectSelect = ({\n  value,\n  onChange,\n}: {\n  value: AnimationAction;\n  onChange: ((value: AnimationAction, isEphemeral: boolean) => void) &\n    ((value: undefined, isEphemeral: true) => void);\n}) => {\n  const [subjects] = useState(() => initSubjects());\n\n  if (value.type !== \"view\") {\n    return;\n  }\n\n  return (\n    <Select\n      options={subjects.map((subject) => subject.value)}\n      value={value.subject ?? \"self\"}\n      getLabel={(subject) =>\n        subjects.find((s) => s.value === subject)?.label ?? \"-\"\n      }\n      onItemHighlight={(subject) => {\n        const selector =\n          subjects.find((s) => s.value === subject)?.selector ?? undefined;\n        $hoveredInstanceSelector.set(selector);\n\n        if (subject === undefined) {\n          onChange(undefined, true);\n          return;\n        }\n\n        const subjectItem = subjects.find((s) => s.value === subject);\n        if (subjectItem === undefined) {\n          onChange(undefined, true);\n          return;\n        }\n        const newValue = {\n          ...value,\n          subject: subject === \"self\" ? undefined : subject,\n        };\n\n        onChange(newValue, true);\n      }}\n      onChange={(subject) => {\n        const newValue = {\n          ...value,\n          subject: subject === \"self\" ? undefined : subject,\n        };\n\n        const subjectItem = subjects.find((s) => s.value === subject);\n\n        if (subjectItem === undefined) {\n          toast.error(`Subject \"${newValue.subject}\" not found`);\n          return;\n        }\n\n        if (\n          subjectItem.isTimelineExists === false &&\n          newValue.subject !== undefined\n        ) {\n          updateWebstudioData(\n            ({ breakpoints, styleSources, styleSourceSelections, styles }) => {\n              if (newValue.subject === undefined) {\n                return;\n              }\n\n              setListedCssProperty(\n                breakpoints,\n                styleSources,\n                styleSourceSelections,\n                styles\n              )(subjectItem.instanceId, \"view-timeline-name\", {\n                type: \"unparsed\",\n                value: newValue.subject,\n              });\n            }\n          );\n\n          // Wait styles to be applied\n          requestAnimationFrame(() => {\n            requestAnimationFrame(() => {\n              onChange(newValue, false);\n            });\n          });\n          return;\n        }\n\n        onChange(newValue, false);\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/match-media-breakpoints.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport { matchMediaBreakpoints } from \"./match-media-breakpoints\";\nimport type { IsEqual } from \"type-fest\";\n\ndescribe(\"matchMediaBreakpoints\", () => {\n  it(\"returns undefined when values array is undefined\", () => {\n    const matchingBreakpointIds = [\"mobile\", \"tablet\", \"desktop\"];\n    const matcher = matchMediaBreakpoints(matchingBreakpointIds);\n\n    expect(matcher(undefined)).toBeUndefined();\n  });\n\n  it(\"returns undefined when no matching breakpoints are found\", () => {\n    const matchingBreakpointIds = [\"mobile\", \"tablet\", \"desktop\"];\n    const values: Array<[string, number]> = [\n      [\"other-breakpoint\", 100],\n      [\"another-breakpoint\", 200],\n    ];\n\n    const matcher = matchMediaBreakpoints(matchingBreakpointIds);\n\n    expect(matcher(values)).toBeUndefined();\n  });\n\n  it(\"returns the value of the last matching breakpoint\", () => {\n    const matchingBreakpointIds = [\"mobile\", \"tablet\"];\n    const values: Array<[string, number]> = [\n      [\"mobile\", 320],\n      [\"tablet\", 768],\n      [\"desktop\", 1024],\n    ];\n\n    const matcher = matchMediaBreakpoints(matchingBreakpointIds);\n\n    expect(matcher(values)).toBe(768);\n  });\n\n  it(\"preserves the value type\", () => {\n    const matchingBreakpointIds = [\"mobile\", \"tablet\", \"desktop\"];\n\n    const stringValues: Array<[string, string]> = [[\"mobile\", \"small\"]];\n    const stringMatcher = matchMediaBreakpoints(matchingBreakpointIds);\n    const strResult = stringMatcher(stringValues);\n    expect(strResult).toBe(\"small\");\n    true satisfies IsEqual<string | undefined, typeof strResult>;\n\n    const booleanValues: Array<[string, boolean]> = [[\"tablet\", true]];\n    const booleanMatcher = matchMediaBreakpoints(matchingBreakpointIds);\n    const boolResult = booleanMatcher(booleanValues);\n    expect(boolResult).toBe(true);\n    true satisfies IsEqual<boolean | undefined, typeof boolResult>;\n\n    const objectValues: Array<[string, { width: number }]> = [\n      [\"desktop\", { width: 1024 }],\n    ];\n    const objectMatcher = matchMediaBreakpoints(matchingBreakpointIds);\n    const objResult = objectMatcher(objectValues);\n    expect(objResult).toEqual({ width: 1024 });\n    true satisfies IsEqual<{ width: number } | undefined, typeof objResult>;\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/match-media-breakpoints.ts",
    "content": "/**\n * Given an array of [breakpointId, value] tuples and an ordered list of breakpoint IDs,\n * returns the value associated with the last matching breakpoint found.\n * If none of the breakpoint IDs are present, returns undefined.\n * */\nexport const matchMediaBreakpoints =\n  (matchingBreakpointIds: string[]) =>\n  <T extends [breakpointId: string, value: unknown]>(\n    values: T[] | undefined\n  ): T[1] | undefined => {\n    let lastValue: T[1] | undefined = undefined;\n\n    if (values === undefined) {\n      return lastValue;\n    }\n\n    const valuesMap = new Map<string, T[1]>(values);\n\n    for (const matchingBreakpointId of matchingBreakpointIds) {\n      lastValue = valuesMap.get(matchingBreakpointId) ?? lastValue;\n    }\n\n    return lastValue;\n  };\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/props-section.stories.tsx",
    "content": "import { useState } from \"react\";\nimport type { PropMeta, Instance, Prop, Asset, Page } from \"@webstudio-is/sdk\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  textVariants,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { PropsSection as PropsSectionComponent } from \"./props-section\";\nimport { usePropsLogic } from \"./use-props-logic\";\nimport {\n  $assets,\n  $instances,\n  $pages,\n  $props,\n  registerComponentLibrary,\n} from \"~/shared/nano-states\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness } from \"~/shared/awareness\";\n\nlet id = 0;\nconst unique = () => `${++id}`;\n\nconst instanceId = unique();\nconst projectId = unique();\n\nconst page = (name: string, path: string): Page => ({\n  id: unique(),\n  name,\n  title: name,\n  path,\n  meta: {},\n  rootInstanceId: unique(),\n});\n\n$pages.set({\n  ...createDefaultPages({ rootInstanceId: unique() }),\n  homePage: page(\"Home\", \"\") as Page & { path: \"\" },\n  pages: [\n    page(\"About\", \"/about\"),\n    page(\"Pricing\", \"/pricing\"),\n    page(\"Contacts\", \"/contacts\"),\n  ],\n});\n\nconst getSectionInstanceId = (\n  name: string,\n  page: Page = $pages.get()?.homePage as Page\n) => (page === undefined ? \"\" : `${page.id}-${name}`);\n\nconst addLinkableSections = (\n  names: string[],\n  page: Page = $pages.get()?.homePage as Page\n) => {\n  if (page === undefined) {\n    return;\n  }\n\n  const instances = $instances.get();\n  const props = $props.get();\n\n  const rootInstance: Instance = {\n    id: page.rootInstanceId,\n    type: \"instance\",\n    component: \"body\",\n    children: [],\n  };\n  instances.set(rootInstance.id, rootInstance);\n\n  for (const name of names) {\n    const instance: Instance = {\n      id: getSectionInstanceId(name, page),\n      type: \"instance\",\n      component: \"box\",\n      children: [],\n    };\n    rootInstance.children.push({ type: \"id\", value: instance.id });\n    instances.set(instance.id, instance);\n\n    const prop: Prop = {\n      id: unique(),\n      instanceId: instance.id,\n      name: \"id\",\n      type: \"string\",\n      value: name,\n    };\n    props.set(prop.id, prop);\n  }\n  return rootInstance;\n};\n\naddLinkableSections([\"contacts\", \"about\"]);\nconst rootInstance = addLinkableSections(\n  [\"company\", \"employees\"],\n  $pages.get()?.pages[0]\n);\n$awareness.set({ pageId: $pages.get()?.homePage.id ?? \"\" });\n\nconst instance: Instance = {\n  id: instanceId,\n  type: \"instance\",\n  component: \"Box\",\n  children: [],\n};\nrootInstance?.children.push({ type: \"id\", value: instance.id });\n\nconst imageAsset = (name = \"cat\", format = \"jpg\"): Asset => ({\n  id: unique(),\n  projectId,\n  type: \"image\",\n  name: `${name}.${format}`,\n  format: format,\n  size: 100000,\n  createdAt: new Date().toISOString(),\n  meta: { width: 128, height: 180 },\n});\n\n$assets.set(\n  new Map(\n    [imageAsset(\"cat\"), imageAsset(\"car\", \"png\"), imageAsset(\"beach\")].map(\n      (asset) => [asset.id, asset]\n    )\n  )\n);\n\nconst textProp = (label?: string, defaultValue?: string): PropMeta => ({\n  type: \"string\",\n  control: \"text\",\n  required: false,\n  rows: 2,\n  label,\n  defaultValue,\n});\n\nconst shortTextProp = (label?: string): PropMeta => ({\n  type: \"string\",\n  control: \"text\",\n  required: false,\n  label,\n});\n\nconst numberProp = (label?: string): PropMeta => ({\n  type: \"number\",\n  control: \"number\",\n  required: false,\n  label,\n});\n\nconst booleanProp = (label?: string): PropMeta => ({\n  type: \"boolean\",\n  control: \"boolean\",\n  required: false,\n  label,\n});\n\nconst colorProp = (label?: string): PropMeta => ({\n  type: \"string\",\n  control: \"color\",\n  required: false,\n  label,\n});\n\nconst urlProp = (label?: string): PropMeta => ({\n  type: \"string\",\n  control: \"url\",\n  required: false,\n  label,\n});\n\nconst fileProp = (label?: string, accept?: string): PropMeta => ({\n  type: \"string\",\n  control: \"file\",\n  required: false,\n  label,\n  accept,\n});\n\nconst defaultOptions = [\"one\", \"two\", \"three-the-very-long-one-so-much-long\"];\n\nconst radioProp = (options = defaultOptions, label?: string): PropMeta => ({\n  type: \"string\",\n  control: \"radio\",\n  options,\n  required: false,\n  label,\n});\n\nconst selectProp = (options = defaultOptions, label?: string): PropMeta => ({\n  type: \"string\",\n  control: \"select\",\n  options,\n  required: false,\n  label,\n});\n\nconst checkProp = (options = defaultOptions, label?: string): PropMeta => ({\n  type: \"string[]\",\n  control: \"check\",\n  options,\n  required: false,\n  label,\n});\n\nregisterComponentLibrary({\n  components: {},\n  templates: {},\n  metas: {\n    Box: {\n      props: {\n        initialText: textProp(\"\", \"multi\\nline\"),\n        initialShortText: shortTextProp(),\n        initialNumber: numberProp(),\n        initialBoolean: booleanProp(),\n        initialColor: colorProp(),\n        initialRadio: radioProp(),\n        initialSelect: selectProp(),\n        initialCheck: checkProp(),\n        initialUrl: urlProp(),\n        initialFile: fileProp(\"Initial File (PNG only)\", \".png\"),\n        addedText: textProp(),\n        addedShortText: shortTextProp(),\n        addedNumber: numberProp(),\n        addedBoolean: booleanProp(),\n        addedColor: colorProp(),\n        addedRadio: radioProp(),\n        addedSelect: selectProp(),\n        addedCheck: checkProp(),\n        addedUrlUrl: urlProp(\"Added URL (URL)\"),\n        addedUrlPage: urlProp(\"Added URL (Page)\"),\n        addedUrlSection: urlProp(\"Added URL (Section)\"),\n        addedUrlEmail: urlProp(\"Added URL (Email)\"),\n        addedUrlPhone: urlProp(\"Added URL (Phone)\"),\n        addedUrlAttachment: urlProp(\"Added URL (Attachment)\"),\n        addedFile: fileProp(),\n        availableText: textProp(),\n        availableShortText: shortTextProp(),\n        availableNumber: numberProp(),\n        availableBoolean: booleanProp(),\n        availableColor: colorProp(),\n        availableRadio: radioProp(),\n        availableSelect: selectProp(),\n        availableCheck: checkProp(),\n        availableUrl: urlProp(),\n        availableFile: fileProp(),\n      },\n      initialProps: [\n        \"initialText\",\n        \"initialShortText\",\n        \"initialNumber\",\n        \"initialBoolean\",\n        \"initialColor\",\n        \"initialRadio\",\n        \"initialSelect\",\n        \"initialCheck\",\n        \"initialUrl\",\n        \"initialFile\",\n      ],\n    },\n  },\n});\n\nconst startingProps: Prop[] = [\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedText\",\n    type: \"string\",\n    value: \"some text\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedShortText\",\n    type: \"string\",\n    value: \"some short text\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedNumber\",\n    type: \"number\",\n    value: 10,\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedBoolean\",\n    type: \"boolean\",\n    value: true,\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedColor\",\n    type: \"string\",\n    value: \"#ff0000\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedRadio\",\n    type: \"string\",\n    value: \"two\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedSelect\",\n    type: \"string\",\n    value: \"two\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedCheck\",\n    type: \"string[]\",\n    value: [\"one\", \"two\"],\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedUrlUrl\",\n    type: \"string\",\n    value: \"https://example.com\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedUrlPage\",\n    type: \"page\",\n    value: $pages.get()?.pages[0].id ?? \"\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedUrlSection\",\n    type: \"page\",\n    value: {\n      pageId: $pages.get()?.homePage.id ?? \"\",\n      instanceId: getSectionInstanceId(\"about\"),\n    },\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedUrlEmail\",\n    type: \"string\",\n    value: \"mailto:hello@example.com?subject=Hello\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedUrlPhone\",\n    type: \"string\",\n    value: \"tel:+1234567890\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedUrlAttachment\",\n    type: \"asset\",\n    value: (Array.from($assets.get().keys())[0] as string) ?? \"\",\n  },\n  {\n    id: unique(),\n    instanceId,\n    name: \"addedFile\",\n    type: \"asset\",\n    value: (Array.from($assets.get().keys())[0] as string) ?? \"\",\n  },\n];\n\nexport const PropsSection = () => {\n  const [props, setProps] = useState(startingProps);\n\n  const handleUpdate = (prop: Prop) => {\n    setProps((current) => {\n      const exists = current.find((item) => item.id === prop.id) !== undefined;\n      return exists\n        ? current.map((item) => (item.id === prop.id ? prop : item))\n        : [...current, prop];\n    });\n  };\n\n  const logic = usePropsLogic({\n    instance,\n    props,\n    updateProp: handleUpdate,\n  });\n\n  return (\n    <StorySection title=\"Props Section\">\n      <Flex gap=\"3\">\n        <Box\n          css={{\n            width: theme.sizes.sidebarWidth,\n            border: \"dashed 3px #e3e3e3\",\n          }}\n        >\n          <PropsSectionComponent\n            instanceId={instanceId}\n            propsLogic={logic}\n            propValues={new Map()}\n            component=\"Button\"\n            selectedInstanceKey={instanceId}\n          />\n        </Box>\n        <pre style={textVariants.mono}>\n          {props\n            .map(\n              ({ name, value, type }) =>\n                `${name}: ${type} = ${JSON.stringify(value)}`\n            )\n            .join(\"\\n\")}\n        </pre>\n      </Flex>\n    </StorySection>\n  );\n};\n\nexport default {\n  title: \"Settings panel/Props Section\",\n  component: PropsSection,\n  parameters: {\n    lostpixel: {\n      // this is to fix cutting off the after scroll area in the screenshot\n      waitBeforeScreenshot: 5000,\n    },\n  },\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/props-section.tsx",
    "content": "import { nanoid } from \"nanoid\";\nimport { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { matchSorter } from \"match-sorter\";\nimport {\n  type Instance,\n  type Props,\n  descendantComponent,\n  rootComponent,\n} from \"@webstudio-is/sdk\";\nimport {\n  theme,\n  Combobox,\n  Separator,\n  Flex,\n  Box,\n  Grid,\n} from \"@webstudio-is/design-system\";\nimport {\n  isAttributeNameSafe,\n  reactPropsToStandardAttributes,\n  showAttribute,\n  standardAttributesToReactProps,\n} from \"@webstudio-is/react-sdk\";\nimport {\n  $propValuesByInstanceSelector,\n  $propsIndex,\n  $props,\n  $isDesignMode,\n  $isContentMode,\n  $memoryProps,\n  $selectedBreakpoint,\n} from \"~/shared/nano-states\";\nimport { CollapsibleSectionWithAddButton } from \"~/builder/shared/collapsible-section\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { $selectedInstance, $selectedInstanceKey } from \"~/shared/awareness\";\nimport { renderControl } from \"../controls/combined\";\nimport { usePropsLogic, type PropAndMeta } from \"./use-props-logic\";\nimport { AnimationSection } from \"./animation/animation-section\";\nimport { $matchingBreakpoints } from \"../../style-panel/shared/model\";\nimport { matchMediaBreakpoints } from \"./match-media-breakpoints\";\nimport {\n  $selectedInstanceInitialPropNames,\n  $selectedInstancePropsMetas,\n} from \"../shared\";\n\ntype Item = {\n  name: string;\n  label?: string;\n  description?: string;\n};\n\nconst itemToString = (item: Item | null) => item?.label || item?.name || \"\";\n\nconst matchOrSuggestToCreate = (\n  search: string,\n  items: Array<Item>,\n  itemToString: (item: Item) => string\n): Array<Item> => {\n  if (search.trim() === \"\") {\n    return items;\n  }\n  const matched = matchSorter(items, search, {\n    keys: [itemToString],\n  });\n  if (\n    itemToString(matched[0]).toLocaleLowerCase() !==\n    search.toLocaleLowerCase().trim()\n  ) {\n    matched.unshift({\n      name: search.trim(),\n      label: `Create attribute: \"${search.trim()}\"`,\n    });\n  }\n  return matched;\n};\n\nconst renderProperty = (\n  { propsLogic: logic, propValues, component, instanceId }: PropsSectionProps,\n  { prop, propName, meta }: PropAndMeta\n) =>\n  renderControl({\n    key: propName,\n    instanceId,\n    meta,\n    prop,\n    computedValue:\n      propValues.get(propName) ??\n      // support legacy html props with react names\n      propValues.get(standardAttributesToReactProps[propName]) ??\n      meta.defaultValue,\n    propName,\n    onChange: (propValue) => {\n      logic.handleChange({ prop, propName }, propValue);\n\n      if (\n        (component === \"Image\" || component === \"Video\") &&\n        propName === \"src\" &&\n        propValue.type === \"asset\"\n      ) {\n        logic.handleChangeByPropName(\"width\", propValue);\n        logic.handleChangeByPropName(\"height\", propValue);\n        logic.handleChangeByPropName(\"alt\", propValue);\n      }\n    },\n  });\n\nconst forbiddenProperties = new Set([\"style\"]);\n\nconst $availableProps = computed(\n  [\n    $selectedInstance,\n    $props,\n    $selectedInstancePropsMetas,\n    $selectedInstanceInitialPropNames,\n  ],\n  (instance, props, propsMetas, initialPropNames) => {\n    const availableProps = new Map<Item[\"name\"], Item>();\n    for (const [name, { label, description }] of propsMetas) {\n      if (name === showAttribute) {\n        continue;\n      }\n      availableProps.set(name, { name, label, description });\n    }\n    if (instance === undefined) {\n      return [];\n    }\n    // remove initial props\n    for (const name of initialPropNames) {\n      availableProps.delete(name);\n    }\n    // remove defined props\n    for (const prop of props.values()) {\n      if (prop.instanceId === instance.id) {\n        availableProps.delete(prop.name);\n        availableProps.delete(reactPropsToStandardAttributes[prop.name]);\n      }\n    }\n    return Array.from(availableProps.values());\n  }\n);\n\nconst AddPropertyOrAttribute = ({\n  onPropSelected,\n}: {\n  onPropSelected: (propName: string) => void;\n}) => {\n  const [value, setValue] = useState(\"\");\n  const [isValid, setIsValid] = useState(true);\n  return (\n    <Flex\n      css={{ height: theme.spacing[13] }}\n      direction=\"column\"\n      justify=\"center\"\n    >\n      <Combobox<Item>\n        autoFocus\n        color={isValid ? undefined : \"error\"}\n        placeholder=\"Select or create\"\n        // lazily load available props to not bloat component renders\n        getItems={() => $availableProps.get()}\n        itemToString={itemToString}\n        onItemSelect={(item) => {\n          if (\n            forbiddenProperties.has(item.name) ||\n            isAttributeNameSafe(item.name) === false\n          ) {\n            setIsValid(false);\n            return;\n          }\n          setIsValid(true);\n          onPropSelected(item.name);\n        }}\n        match={matchOrSuggestToCreate}\n        value={{ name: \"\", label: value }}\n        onChange={(value) => {\n          setValue(value ?? \"\");\n        }}\n        getDescription={(item) => {\n          return (\n            <Box css={{ width: theme.spacing[28] }}>\n              {item?.description ?? \"No description available\"}\n            </Box>\n          );\n        }}\n      />\n    </Flex>\n  );\n};\n\ntype PropsSectionProps = {\n  propsLogic: ReturnType<typeof usePropsLogic>;\n  propValues: Map<string, unknown>;\n  component: Instance[\"component\"];\n  instanceId: string;\n  selectedInstanceKey: string;\n};\n\n// A UI componet with minimum logic that can be demoed in Storybook etc.\nexport const PropsSection = (props: PropsSectionProps) => {\n  const { propsLogic: logic } = props;\n  const [addingProp, setAddingProp] = useState(false);\n  const isDesignMode = useStore($isDesignMode);\n  const isContentMode = useStore($isContentMode);\n  const matchingBreakpoints = useStore($matchingBreakpoints);\n  const selectedBreakpoint = useStore($selectedBreakpoint);\n\n  const matchMediaValue = matchMediaBreakpoints(matchingBreakpoints);\n\n  const hasItems =\n    logic.addedProps.length > 0 || addingProp || logic.initialProps.length > 0;\n\n  const animationAction = logic.initialProps.find(\n    (prop) => prop.meta.type === \"animationAction\"\n  );\n\n  const hasAnimation = animationAction !== undefined;\n\n  const showPropertiesSection =\n    isDesignMode || (isContentMode && logic.initialProps.length > 0);\n\n  return hasAnimation && selectedBreakpoint?.id !== undefined ? (\n    <>\n      <AnimationSection\n        animationAction={animationAction}\n        isAnimationEnabled={matchMediaValue}\n        selectedBreakpointId={selectedBreakpoint?.id}\n        onChange={(value, isEphemeral) => {\n          const memoryProps = new Map($memoryProps.get());\n          const memoryInstanceProp: Props = new Map(\n            memoryProps.get(props.selectedInstanceKey)\n          );\n\n          if (isEphemeral && value !== undefined) {\n            memoryInstanceProp.set(animationAction.propName, {\n              id: nanoid(),\n              instanceId: props.instanceId,\n              type: \"animationAction\",\n              name: animationAction.propName,\n              value,\n            });\n            memoryProps.set(props.selectedInstanceKey, memoryInstanceProp);\n            $memoryProps.set(memoryProps);\n            return;\n          }\n\n          if (memoryInstanceProp.has(animationAction.propName)) {\n            memoryInstanceProp.delete(animationAction.propName);\n            memoryProps.set(props.selectedInstanceKey, memoryInstanceProp);\n\n            $memoryProps.set(memoryProps);\n          }\n\n          if (isEphemeral || value === undefined) {\n            return;\n          }\n\n          isEphemeral satisfies false;\n\n          logic.handleChangeByPropName(animationAction.propName, {\n            type: \"animationAction\",\n            value,\n          });\n        }}\n      />\n    </>\n  ) : (\n    <>\n      <Grid\n        css={{\n          paddingBottom: theme.panel.paddingBlock,\n        }}\n      >\n        {logic.systemProps.map((item) => (\n          <Box\n            key={item.propName}\n            css={{ paddingInline: theme.panel.paddingInline }}\n          >\n            {renderProperty(props, item)}\n          </Box>\n        ))}\n      </Grid>\n\n      <Separator />\n      {showPropertiesSection && (\n        <CollapsibleSectionWithAddButton\n          label=\"Properties & Attributes\"\n          onAdd={isDesignMode ? () => setAddingProp(true) : undefined}\n          hasItems={hasItems}\n        >\n          <Flex gap=\"1\" direction=\"column\">\n            {addingProp && (\n              <AddPropertyOrAttribute\n                onPropSelected={(propName) => {\n                  setAddingProp(false);\n                  logic.handleAdd(propName);\n                }}\n              />\n            )}\n            {logic.addedProps.map((item) => renderProperty(props, item))}\n            {logic.initialProps.map((item) => renderProperty(props, item))}\n          </Flex>\n        </CollapsibleSectionWithAddButton>\n      )}\n    </>\n  );\n};\n\nconst $propValues = computed(\n  [$propValuesByInstanceSelector, $selectedInstanceKey],\n  (propValuesByInstanceSelector, instanceKey) =>\n    propValuesByInstanceSelector.get(instanceKey ?? \"\")\n);\n\nexport const PropsSectionContainer = ({\n  selectedInstance: instance,\n  selectedInstanceKey,\n}: {\n  selectedInstance: Instance;\n  selectedInstanceKey: string;\n}) => {\n  const { propsByInstanceId } = useStore($propsIndex);\n  const propValues = useStore($propValues);\n\n  const logic = usePropsLogic({\n    instance,\n    props: propsByInstanceId.get(instance.id) ?? [],\n\n    updateProp: (update) => {\n      const { propsByInstanceId } = $propsIndex.get();\n      const instanceProps = propsByInstanceId.get(instance.id) ?? [];\n      // Fixing a bug that caused some props to be duplicated on unmount by removing duplicates.\n      // see for details https://github.com/webstudio-is/webstudio/pull/2170\n      const duplicateProps = instanceProps\n        .filter((prop) => prop.id !== update.id)\n        .filter((prop) => prop.name === update.name);\n      serverSyncStore.createTransaction([$props], (props) => {\n        for (const prop of duplicateProps) {\n          props.delete(prop.id);\n        }\n        props.set(update.id, update);\n      });\n    },\n  });\n\n  const propsMetas = useStore($selectedInstancePropsMetas);\n  if (propsMetas.size === 0 || instance.component === rootComponent) {\n    return;\n  }\n\n  return (\n    <fieldset\n      style={{ display: \"contents\" }}\n      disabled={instance.component === descendantComponent}\n    >\n      <PropsSection\n        propsLogic={logic}\n        propValues={propValues ?? new Map()}\n        component={instance.component}\n        instanceId={instance.id}\n        selectedInstanceKey={selectedInstanceKey}\n      />\n    </fieldset>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/props-section/use-props-logic.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport type { PropMeta, Instance, Prop } from \"@webstudio-is/sdk\";\nimport { descendantComponent } from \"@webstudio-is/sdk\";\nimport {\n  reactPropsToStandardAttributes,\n  showAttribute,\n  standardAttributesToReactProps,\n  textContentAttribute,\n} from \"@webstudio-is/react-sdk\";\nimport {\n  $instances,\n  $isContentMode,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { isRichText } from \"~/shared/content-model\";\nimport { $selectedInstance, $selectedInstancePath } from \"~/shared/awareness\";\nimport {\n  $selectedInstanceInitialPropNames,\n  $selectedInstancePropsMetas,\n  showAttributeMeta,\n  type PropValue,\n} from \"../shared\";\nimport { $instanceTags } from \"../../style-panel/shared/model\";\n\ntype PropOrName = { prop?: Prop; propName: string };\n\nexport type PropAndMeta = {\n  prop?: Prop;\n  propName: string;\n  meta: PropMeta;\n};\n\n// The value we set prop to when it's added\n//\n// If undefined is returned,\n// we will not add a prop in storage until we get an onChange from control.\n//\n// User may have this experience:\n//   - they added a prop but didn't touch the control\n//   - they closed props panel\n//   - when they open props panel again, the prop is not there\n//\n// We want to avoid this if possible, but for some types like \"asset\" we can't\nconst getStartingValue = (\n  meta: PropMeta,\n  defaultBooleanValue: boolean\n): PropValue | undefined => {\n  if (meta.type === \"string\" && meta.control !== \"file\") {\n    return {\n      type: \"string\",\n      value: meta.defaultValue ?? \"\",\n    };\n  }\n\n  if (meta.type === \"number\") {\n    return {\n      type: \"number\",\n      value: meta.defaultValue ?? 0,\n    };\n  }\n\n  if (meta.type === \"boolean\") {\n    return {\n      type: \"boolean\",\n      value: meta.defaultValue ?? defaultBooleanValue,\n    };\n  }\n\n  if (meta.type === \"string[]\") {\n    return {\n      type: \"string[]\",\n      value: meta.defaultValue ?? [],\n    };\n  }\n\n  if (meta.type === \"action\") {\n    return {\n      type: \"action\",\n      value: [],\n    };\n  }\n};\n\nconst getDefaultMetaForType = (type: Prop[\"type\"]): PropMeta => {\n  switch (type) {\n    case \"action\":\n      return { type: \"action\", control: \"action\", required: false };\n    case \"string\":\n      return { type: \"string\", control: \"text\", required: false };\n    case \"number\":\n      return { type: \"number\", control: \"number\", required: false };\n    case \"boolean\":\n      return { type: \"boolean\", control: \"boolean\", required: false };\n    case \"asset\":\n      return { type: \"string\", control: \"file\", required: false };\n    case \"page\":\n      return { type: \"string\", control: \"url\", required: false };\n    case \"string[]\":\n      throw new Error(\n        \"A prop with type string[] must have a meta, we can't provide a default one because we need a list of options\"\n      );\n\n    case \"animationAction\":\n      return {\n        type: \"animationAction\",\n        control: \"animationAction\",\n        required: false,\n      };\n    case \"json\":\n      throw new Error(\n        \"A prop with type json must have a meta, we can't provide a default one because we need a list of options\"\n      );\n    case \"expression\":\n      throw new Error(\n        \"A prop with type expression must have a meta, we can't provide a default one because we need a list of options\"\n      );\n    case \"parameter\":\n      throw new Error(\n        \"A prop with type parameter must have a meta, we can't provide a default one because we need a list of options\"\n      );\n    case \"resource\":\n      throw new Error(\n        \"A prop with type resource must have a meta, we can't provide a default one because we need a list of options\"\n      );\n    default:\n      throw new Error(`Usupported data type: ${type satisfies never}`);\n  }\n};\n\ntype UsePropsLogicInput = {\n  instance: Instance;\n  props: Prop[];\n  updateProp: (update: Prop) => void;\n};\n\nconst getAndDelete = <Value>(map: Map<string, Value>, key: string) => {\n  const value = map.get(key);\n  map.delete(key);\n  return value;\n};\n\nconst $canHaveTextContent = computed(\n  [$instances, $props, $registeredComponentMetas, $selectedInstancePath],\n  (instances, props, metas, instancePath) => {\n    if (instancePath === undefined) {\n      return false;\n    }\n    const [{ instanceSelector }] = instancePath;\n    return isRichText({\n      instances,\n      props,\n      metas,\n      instanceSelector,\n    });\n  }\n);\n\nconst contentModePropertiesByTag: Partial<Record<string, string[]>> = {\n  img: [\"src\", \"width\", \"height\", \"alt\"],\n  a: [\"href\"],\n};\n\nconst $selectedInstanceTag = computed(\n  [$selectedInstance, $instanceTags],\n  (selectedInstance, instanceTags) => {\n    if (selectedInstance === undefined) {\n      return;\n    }\n    return instanceTags.get(selectedInstance.id);\n  }\n);\n\n/** usePropsLogic expects that key={instanceId} is used on the ancestor component */\nexport const usePropsLogic = ({\n  instance,\n  props,\n  updateProp,\n}: UsePropsLogicInput) => {\n  const isContentMode = useStore($isContentMode);\n  const selectedInstanceTag = useStore($selectedInstanceTag);\n\n  /**\n   * In content edit mode we show only Image and Link props\n   * In the future I hope the only thing we will show will be Components\n   */\n  const isPropVisible = (propName: string) => {\n    if (!isContentMode) {\n      return true;\n    }\n    const allowedProperties =\n      contentModePropertiesByTag[selectedInstanceTag ?? \"\"] ?? [];\n    return allowedProperties.includes(propName);\n  };\n\n  const savedProps = props;\n\n  // we will delete items from these maps as we categorize the props\n  const unprocessedSaved = new Map(savedProps.map((prop) => [prop.name, prop]));\n\n  const propsMetas = useStore($selectedInstancePropsMetas);\n\n  const initialPropNames = useStore($selectedInstanceInitialPropNames);\n\n  const systemProps: PropAndMeta[] = [];\n  // descendant component is not actually rendered\n  // but affects styling of nested elements\n  // hiding descendant does not hide nested elements and confuse users\n  if (instance.component !== descendantComponent) {\n    systemProps.push({\n      propName: showAttribute,\n      prop: getAndDelete(unprocessedSaved, showAttribute),\n      meta: showAttributeMeta,\n    });\n  }\n\n  const canHaveTextContent = useStore($canHaveTextContent);\n\n  const hasNoChildren = instance.children.length === 0;\n  const hasOnlyTextChild =\n    instance.children.length === 1 && instance.children[0].type === \"text\";\n  const hasOnlyExpressionChild =\n    instance.children.length === 1 &&\n    instance.children[0].type === \"expression\";\n  if (\n    canHaveTextContent &&\n    (hasNoChildren || hasOnlyTextChild || hasOnlyExpressionChild)\n  ) {\n    systemProps.push({\n      propName: textContentAttribute,\n      meta: {\n        required: false,\n        control: \"textContent\",\n        type: \"string\",\n      },\n    });\n  }\n\n  const initialProps: PropAndMeta[] = [];\n  for (const name of initialPropNames) {\n    const propMeta = propsMetas.get(name);\n    if (propMeta === undefined) {\n      continue;\n    }\n\n    let prop =\n      getAndDelete<Prop>(unprocessedSaved, name) ??\n      // support legacy html props stored with react names\n      getAndDelete<Prop>(\n        unprocessedSaved,\n        standardAttributesToReactProps[name]\n      );\n    if (prop) {\n      prop = { ...prop, name };\n    }\n\n    // For initial props, if prop is not saved, we want to show default value if available.\n    //\n    // Important to not use infer stating value if default is not available\n    // beacuse user may have this experience:\n    //   - they open props panel of an Image\n    //   - they see 0 in the control for \"width\"\n    //   - where 0 is a fallback when no default is available\n    //   - they think that width is set to 0, but it's actually not set at all\n    //\n    if (prop === undefined && propMeta.defaultValue !== undefined) {\n      // initial properties are not defined but suggested to default so default boolean is false\n      const value = getStartingValue(propMeta, false);\n      if (value) {\n        prop = { id: nanoid(), instanceId: instance.id, name, ...value };\n      }\n    }\n\n    initialProps.push({\n      prop,\n      propName: name,\n      meta: propMeta,\n    });\n  }\n\n  const addedProps: PropAndMeta[] = [];\n  for (let prop of Array.from(unprocessedSaved.values()).reverse()) {\n    // ignore parameter props\n    if (prop.type === \"parameter\") {\n      continue;\n    }\n    let name = prop.name;\n    let propMeta = propsMetas.get(name);\n    // support legacy html props stored with react names\n    if (propsMetas.has(reactPropsToStandardAttributes[name])) {\n      name = reactPropsToStandardAttributes[name];\n      propMeta = propsMetas.get(name);\n    }\n    prop = { ...prop, name };\n    propMeta ??= getDefaultMetaForType(\"string\");\n\n    addedProps.push({\n      prop,\n      propName: prop.name,\n      meta: propMeta,\n    });\n  }\n\n  const handleAdd = (propName: string) => {\n    // In case of custom property/attribute we get a string.\n    const propMeta =\n      propsMetas.get(propName) ?? getDefaultMetaForType(\"string\");\n    const value = getStartingValue(propMeta, true);\n    if (value) {\n      updateProp({\n        id: nanoid(),\n        instanceId: instance.id,\n        name: propName,\n        ...value,\n      });\n    }\n  };\n\n  const handleChange = ({ prop, propName }: PropOrName, value: PropValue) => {\n    updateProp(\n      prop === undefined\n        ? { id: nanoid(), instanceId: instance.id, name: propName, ...value }\n        : { ...prop, ...value }\n    );\n  };\n\n  const handleChangeByPropName = (propName: string, value: PropValue) => {\n    const prop = props.find((prop) => prop.name === propName);\n\n    updateProp(\n      prop === undefined\n        ? { id: nanoid(), instanceId: instance.id, name: propName, ...value }\n        : { ...prop, ...value }\n    );\n  };\n\n  return {\n    handleAdd,\n    handleChange,\n    handleChangeByPropName,\n    /** Similar to Initial, but displayed as a separate group in UI etc.\n     * Currentrly used only for the ID prop. */\n    systemProps: systemProps.filter(({ propName }) => isPropVisible(propName)),\n    /** Initial (not deletable) props */\n    initialProps: initialProps.filter(({ propName }) =>\n      isPropVisible(propName)\n    ),\n    /** Optional props that were added by user */\n    addedProps: addedProps.filter(({ propName }) => isPropVisible(propName)),\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/resource-panel.tsx",
    "content": "import { z } from \"zod\";\nimport { computed } from \"nanostores\";\nimport { nanoid } from \"nanoid\";\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  DataSources,\n  Resource,\n  type DataSource,\n  type Page,\n} from \"@webstudio-is/sdk\";\nimport {\n  encodeDataVariableId,\n  generateObjectExpression,\n  isLiteralExpression,\n  parseObjectExpression,\n  SYSTEM_VARIABLE_ID,\n  systemParameter,\n} from \"@webstudio-is/sdk\";\nimport {\n  serializeValue,\n  sitemapResourceUrl,\n  currentDateResourceUrl,\n  assetsResourceUrl,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  Box,\n  Flex,\n  Grid,\n  InputErrorsTooltip,\n  InputField,\n  Label,\n  Select,\n  SmallIconButton,\n  Text,\n  TextArea,\n  Tooltip,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { TrashIcon, InfoCircleIcon, PlusIcon } from \"@webstudio-is/icons\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport {\n  $dataSources,\n  $resources,\n  $variableValuesByInstanceSelector,\n} from \"~/shared/nano-states\";\nimport {\n  BindingControl,\n  BindingPopover,\n  evaluateExpressionWithinScope,\n} from \"~/builder/shared/binding-popover\";\nimport { ExpressionEditor } from \"~/builder/shared/expression-editor\";\nimport {\n  EditorDialog,\n  EditorDialogButton,\n  EditorDialogControl,\n} from \"~/shared/code-editor-base\";\nimport {\n  $selectedInstance,\n  $selectedInstancePathWithRoot,\n  $selectedPage,\n  getInstanceKey,\n  type InstancePath,\n} from \"~/shared/awareness\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { rebindTreeVariablesMutable } from \"~/shared/data-variables\";\nimport { parseCurl, type CurlRequest } from \"./curl\";\n\nexport const parseResource = ({\n  id,\n  control,\n  name,\n  formData,\n}: {\n  id: string;\n  control?: string;\n  name?: string;\n  formData: FormData;\n}) => {\n  const searchParamNames = formData.getAll(\"search-param-name\") as string[];\n  const searchParamValues = formData.getAll(\"search-param-value\") as string[];\n  const headerNames = formData.getAll(\"header-name\") as string[];\n  const headerValues = formData.getAll(\"header-value\") as string[];\n  return Resource.parse({\n    id,\n    control,\n    name: name ?? formData.get(\"name\"),\n    url: formData.get(\"url\"),\n    searchParams: searchParamNames\n      .map((name, index) => ({ name, value: searchParamValues[index] }))\n      .filter((item) => item.name.trim()),\n    method: formData.get(\"method\"),\n    headers: headerNames\n      .map((name, index) => ({ name, value: headerValues[index] }))\n      .filter((item) => item.name.trim()),\n    // use undefined instead of empty string\n    body: formData.get(\"body\") || undefined,\n  });\n};\n\nconst validateUrl = (value: string, scope: Record<string, unknown>) => {\n  const evaluatedValue = evaluateExpressionWithinScope(value, scope);\n  if (typeof evaluatedValue !== \"string\") {\n    return \"URL expects a string\";\n  }\n  if (evaluatedValue.length === 0) {\n    return \"URL is required\";\n  }\n  try {\n    new URL(evaluatedValue);\n  } catch {\n    return \"URL is invalid\";\n  }\n  return \"\";\n};\n\nexport const UrlField = ({\n  scope,\n  aliases,\n  value,\n  onChange,\n  onCurlPaste,\n}: {\n  aliases: Map<string, string>;\n  scope: Record<string, unknown>;\n  value: string;\n  onChange: (\n    urlExpression: string,\n    searchParams?: Resource[\"searchParams\"]\n  ) => void;\n  onCurlPaste: (curl: CurlRequest) => void;\n}) => {\n  const urlId = useId();\n  const ref = useRef<HTMLTextAreaElement>(null);\n  const [error, setError] = useState(\"\");\n  // revalidate and hide error message\n  // until validity is checks again\n  useEffect(() => {\n    ref.current?.setCustomValidity(validateUrl(value, scope));\n    setError(\"\");\n  }, [value, scope]);\n  return (\n    <Grid gap={1}>\n      <Label\n        htmlFor={urlId}\n        css={{ display: \"flex\", alignItems: \"center\", gap: theme.spacing[3] }}\n      >\n        URL\n        <Tooltip\n          content=\"You can paste a URL or cURL. cURL is a format that can be executed directly in your terminal because it contains the entire Resource configuration.\"\n          variant=\"wrapped\"\n          disableHoverableContent={true}\n        >\n          <InfoCircleIcon tabIndex={0} />\n        </Tooltip>\n      </Label>\n      <input hidden={true} readOnly={true} name=\"url\" value={value} />\n      <BindingControl>\n        <InputErrorsTooltip errors={error ? [error] : undefined}>\n          <TextArea\n            ref={ref}\n            name=\"url-validator\"\n            id={urlId}\n            rows={1}\n            grow={true}\n            // expressions with variables cannot be edited\n            disabled={isLiteralExpression(value) === false}\n            color={error ? \"error\" : undefined}\n            value={String(evaluateExpressionWithinScope(value, scope))}\n            onChange={(value) => {\n              const curl = parseCurl(value);\n              if (curl) {\n                onCurlPaste(curl);\n                return;\n              }\n              try {\n                const url = new URL(value);\n                if (url.searchParams.size > 0) {\n                  const searchParams: Resource[\"searchParams\"] = [];\n                  for (const [name, value] of url.searchParams) {\n                    searchParams.push({ name, value: JSON.stringify(value) });\n                  }\n                  // remove all search params from url\n                  url.search = \"\";\n                  // update text value as string literal\n                  onChange(JSON.stringify(url.href), searchParams);\n                  return;\n                }\n              } catch {\n                // serialize without changes when url is invalid\n              }\n              onChange(JSON.stringify(value));\n            }}\n            onBlur={(event) => event.currentTarget.checkValidity()}\n            onInvalid={(event) =>\n              setError(event.currentTarget.validationMessage)\n            }\n          />\n        </InputErrorsTooltip>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(value) ? \"default\" : \"bound\"}\n          value={value}\n          onChange={onChange}\n          onRemove={(evaluatedValue) =>\n            onChange(JSON.stringify(evaluatedValue))\n          }\n        />\n      </BindingControl>\n    </Grid>\n  );\n};\n\nexport const MethodField = ({\n  value,\n  onChange,\n}: {\n  value: Resource[\"method\"];\n  onChange: (value: Resource[\"method\"]) => void;\n}) => {\n  return (\n    <Grid gap={1}>\n      <Label>Method</Label>\n      <Select<Resource[\"method\"]>\n        options={[\"get\", \"post\", \"put\", \"delete\"]}\n        getLabel={humanizeString}\n        name=\"method\"\n        value={value}\n        onChange={onChange}\n      />\n    </Grid>\n  );\n};\n\nconst SearchParamPair = ({\n  aliases,\n  scope,\n  name,\n  value,\n  onChange,\n  onDelete,\n}: {\n  aliases: Map<string, string>;\n  scope: Record<string, unknown>;\n  name: string;\n  value: string;\n  onChange: (name: string, value: string) => void;\n  onDelete: () => void;\n}) => {\n  const evaluatedValue = evaluateExpressionWithinScope(value, scope);\n  // expressions with variables or objects cannot be edited from input\n  const isValueUnboundString =\n    isLiteralExpression(value) && typeof evaluatedValue === \"string\";\n  return (\n    <Grid\n      gap={2}\n      align=\"center\"\n      css={{ gridTemplateColumns: `120px 1fr min-content` }}\n    >\n      <InputField\n        // autofocus only new fields\n        autoFocus={name === \"\"}\n        placeholder=\"Name\"\n        name=\"search-param-name\"\n        value={name}\n        onChange={(event) => onChange(event.target.value, value)}\n      />\n      <input type=\"hidden\" name=\"search-param-value\" value={value} />\n      <BindingControl>\n        <InputField\n          placeholder=\"Value\"\n          name=\"search-param-value-literal\"\n          disabled={!isValueUnboundString}\n          value={serializeValue(evaluatedValue)}\n          // update text value as string literal\n          onChange={(event) =>\n            onChange(name, JSON.stringify(event.target.value))\n          }\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(value) ? \"default\" : \"bound\"}\n          value={value}\n          onChange={(newValue) => onChange(name, newValue)}\n          onRemove={(evaluatedValue) =>\n            onChange(name, JSON.stringify(evaluatedValue))\n          }\n        />\n      </BindingControl>\n      <SmallIconButton\n        aria-label=\"Delete search param\"\n        variant=\"destructive\"\n        icon={<TrashIcon />}\n        onClick={onDelete}\n      />\n    </Grid>\n  );\n};\n\nexport const SearchParams = ({\n  scope,\n  aliases,\n  searchParams,\n  onChange,\n}: {\n  scope: Record<string, unknown>;\n  aliases: Map<string, string>;\n  searchParams: NonNullable<Resource[\"searchParams\"]>;\n  onChange: (searchParams: NonNullable<Resource[\"searchParams\"]>) => void;\n}) => {\n  return (\n    <Grid gap={1}>\n      <Flex justify=\"between\" align=\"center\">\n        <Label>Search params</Label>\n        <SmallIconButton\n          aria-label=\"Add another search param\"\n          icon={<PlusIcon />}\n          onClick={() => {\n            // use empty string expression as default\n            const newSearchParams = [\n              ...searchParams,\n              { name: \"\", value: `\"\"` },\n            ];\n            onChange(newSearchParams);\n          }}\n        />\n      </Flex>\n      <Grid gap={2}>\n        {searchParams.map((searchParam, index) => (\n          <SearchParamPair\n            key={index}\n            scope={scope}\n            aliases={aliases}\n            name={searchParam.name}\n            value={searchParam.value}\n            onChange={(name, value) => {\n              const newSearchParams = [...searchParams];\n              newSearchParams[index] = { name, value };\n              onChange(newSearchParams);\n            }}\n            onDelete={() => {\n              const newSearchParams = [...searchParams];\n              newSearchParams.splice(index, 1);\n              onChange(newSearchParams);\n            }}\n          />\n        ))}\n        {searchParams.length === 0 && (\n          <Text color=\"subtle\" align=\"center\">\n            No search params\n          </Text>\n        )}\n      </Grid>\n    </Grid>\n  );\n};\n\nconst HeaderPair = ({\n  aliases,\n  scope,\n  name,\n  value,\n  onChange,\n  onDelete,\n}: {\n  aliases: Map<string, string>;\n  scope: Record<string, unknown>;\n  name: string;\n  value: string;\n  onChange: (name: string, value: string) => void;\n  onDelete: () => void;\n}) => {\n  const evaluatedValue = evaluateExpressionWithinScope(value, scope);\n  // expressions with variables or objects cannot be edited from input\n  const isValueUnboundString =\n    isLiteralExpression(value) && typeof evaluatedValue === \"string\";\n  return (\n    <Grid\n      gap={2}\n      align=\"center\"\n      css={{ gridTemplateColumns: `120px 1fr min-content` }}\n    >\n      <InputField\n        // autofocus only new fields\n        autoFocus={name === \"\"}\n        placeholder=\"Name\"\n        name=\"header-name\"\n        value={name}\n        onChange={(event) => onChange(event.target.value, value)}\n      />\n      <input hidden={true} readOnly={true} name=\"header-value\" value={value} />\n      <BindingControl>\n        <InputField\n          placeholder=\"Value\"\n          name=\"header-value-validator\"\n          disabled={!isValueUnboundString}\n          value={serializeValue(evaluatedValue)}\n          // update text value as string literal\n          onChange={(event) =>\n            onChange(name, JSON.stringify(event.target.value))\n          }\n        />\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isLiteralExpression(value) ? \"default\" : \"bound\"}\n          value={value}\n          onChange={(newValue) => onChange(name, newValue)}\n          onRemove={(evaluatedValue) =>\n            onChange(name, JSON.stringify(evaluatedValue))\n          }\n        />\n      </BindingControl>\n      <SmallIconButton\n        aria-label=\"Delete header\"\n        variant=\"destructive\"\n        icon={<TrashIcon />}\n        onClick={onDelete}\n      />\n    </Grid>\n  );\n};\n\nexport const Headers = ({\n  scope,\n  aliases,\n  headers,\n  onChange,\n}: {\n  scope: Record<string, unknown>;\n  aliases: Map<string, string>;\n  headers: Resource[\"headers\"];\n  onChange: (headers: Resource[\"headers\"]) => void;\n}) => {\n  return (\n    <Grid gap={1}>\n      <Flex justify=\"between\" align=\"center\">\n        <Label>Headers</Label>\n        <SmallIconButton\n          aria-label=\"Add another search param\"\n          icon={<PlusIcon />}\n          onClick={() => {\n            // use empty string expression as default\n            const newHeaders = [...headers, { name: \"\", value: `\"\"` }];\n            onChange(newHeaders);\n          }}\n        />\n      </Flex>\n      <Grid gap={2}>\n        {headers.map((header, index) => (\n          <HeaderPair\n            key={index}\n            scope={scope}\n            aliases={aliases}\n            name={header.name}\n            value={header.value}\n            onChange={(name, value) => {\n              const newHeaders = [...headers];\n              newHeaders[index] = { name, value };\n              onChange(newHeaders);\n            }}\n            onDelete={() => {\n              const newHeaders = [...headers];\n              newHeaders.splice(index, 1);\n              onChange(newHeaders);\n            }}\n          />\n        ))}\n        {headers.length === 0 && (\n          <Text color=\"subtle\" align=\"center\">\n            No headers\n          </Text>\n        )}\n      </Grid>\n    </Grid>\n  );\n};\n\nconst CacheMaxAge = ({\n  value,\n  onChange,\n}: {\n  value: undefined | string;\n  onChange: (newValue: string) => void;\n}) => {\n  return (\n    <Grid gap={1}>\n      <Label htmlFor=\"resource-panel-max-age\">Cache max age</Label>\n      <InputField\n        id=\"resource-panel-max-age\"\n        suffix={\n          <Text variant=\"small\" color=\"subtle\" css={{ paddingInline: \"2px\" }}>\n            S\n          </Text>\n        }\n        value={value ?? \"\"}\n        onChange={(event) => onChange(event.target.value)}\n      />\n      {value && (\n        <>\n          <input type=\"hidden\" name=\"header-name\" value=\"Cache-Control\" />\n          <input\n            type=\"hidden\"\n            name=\"header-value\"\n            value={`\"max-age=${value}\"`}\n          />\n        </>\n      )}\n    </Grid>\n  );\n};\n\nexport const getResourceScopeForInstance = ({\n  page,\n  instanceKey,\n  dataSources,\n  variableValuesByInstanceSelector,\n}: {\n  page: undefined | Page;\n  instanceKey: undefined | string;\n  dataSources: DataSources;\n  variableValuesByInstanceSelector: Map<string, Map<string, unknown>>;\n}) => {\n  const scope: Record<string, unknown> = {};\n  const aliases = new Map<string, string>();\n  const variableValues = new Map<DataSource[\"id\"], unknown>();\n  const hiddenDataSourceIds = new Set<DataSource[\"id\"]>();\n  for (const dataSource of dataSources.values()) {\n    // hide collection item and component parameters from resources\n    // to prevent waterfall and loop requests ans not complicate compiler\n    if (dataSource.type === \"parameter\") {\n      hiddenDataSourceIds.add(dataSource.id);\n    }\n    // prevent resources using data of other resources\n    if (dataSource.type === \"resource\") {\n      hiddenDataSourceIds.add(dataSource.id);\n    }\n  }\n  if (page?.systemDataSourceId) {\n    hiddenDataSourceIds.delete(page.systemDataSourceId);\n  }\n  const values = variableValuesByInstanceSelector.get(instanceKey ?? \"\");\n  if (values) {\n    for (const [dataSourceId, value] of values) {\n      if (hiddenDataSourceIds.has(dataSourceId)) {\n        continue;\n      }\n      let dataSource = dataSources.get(dataSourceId);\n      if (dataSourceId === SYSTEM_VARIABLE_ID) {\n        dataSource = systemParameter;\n      }\n      if (dataSource) {\n        const name = encodeDataVariableId(dataSourceId);\n        scope[name] = value;\n        aliases.set(name, dataSource.name);\n        variableValues.set(dataSourceId, value);\n      }\n    }\n  }\n  return { variableValues, scope, aliases };\n};\n\nconst getVariableInstanceKey = ({\n  variable,\n  instancePath,\n}: {\n  variable: undefined | DataSource;\n  instancePath: undefined | InstancePath;\n}) => {\n  if (instancePath === undefined) {\n    return;\n  }\n  // find instance key for variable instance\n  for (const { instance, instanceSelector } of instancePath) {\n    if (instance.id === variable?.scopeInstanceId) {\n      return getInstanceKey(instanceSelector);\n    }\n  }\n  // and fallback to currently selected instance\n  return getInstanceKey(instancePath[0].instanceSelector);\n};\n\nexport const useResourceScope = ({ variable }: { variable?: DataSource }) => {\n  return useStore(\n    useMemo(\n      () =>\n        computed(\n          [\n            $selectedPage,\n            $selectedInstancePathWithRoot,\n            $variableValuesByInstanceSelector,\n            $dataSources,\n          ],\n          (\n            page,\n            instancePath,\n            variableValuesByInstanceSelector,\n            dataSources\n          ) => {\n            const { scope, aliases, variableValues } =\n              getResourceScopeForInstance({\n                page,\n                instanceKey: getVariableInstanceKey({\n                  variable,\n                  instancePath,\n                }),\n                dataSources,\n                variableValuesByInstanceSelector,\n              });\n            // prevent showing currently edited variable in suggestions\n            // to avoid cirular dependeny\n            const newScope = { ...scope };\n            const newAliases = new Map(aliases);\n            const newVariableValues = new Map(variableValues);\n            if (variable) {\n              const key = encodeDataVariableId(variable.id);\n              delete newScope[key];\n              newAliases.delete(key);\n              newVariableValues.delete(variable.id);\n            }\n            return {\n              scope: newScope,\n              aliases: newAliases,\n              variableValues: newVariableValues,\n            };\n          }\n        ),\n      [variable]\n    )\n  );\n};\n\ntype PanelApi = {\n  save: (formData: FormData) => void;\n};\n\ntype BodyType = undefined | \"text\" | \"json\";\n\nconst validateBody = (\n  value: string,\n  bodyType: BodyType,\n  scope: Record<string, unknown>\n) => {\n  // skip empty expressions\n  if (value === \"\") {\n    return \"\";\n  }\n  const evaluatedValue = evaluateExpressionWithinScope(value, scope);\n  if (bodyType === \"json\") {\n    return typeof evaluatedValue === \"object\" && evaluatedValue !== null\n      ? \"\"\n      : \"Expected valid JSON object in body\";\n  } else {\n    return typeof evaluatedValue === \"string\" ? \"\" : \"Expected string in body\";\n  }\n};\n\nconst toMime = (bodyType: BodyType) => {\n  if (bodyType === \"json\") {\n    return \"application/json\";\n  }\n  if (bodyType === \"text\") {\n    return \"text/plain\";\n  }\n};\n\nconst BodyField = ({\n  scope,\n  aliases,\n  bodyType,\n  value,\n  onChange,\n}: {\n  aliases: Map<string, string>;\n  scope: Record<string, unknown>;\n  bodyType: BodyType;\n  value: string;\n  onChange: (value: string, bodyType: BodyType) => void;\n}) => {\n  const [isBodyLiteral, setIsBodyLiteral] = useState(\n    () => value === \"\" || isLiteralExpression(value)\n  );\n  const [bodyError, setBodyError] = useState(\"\");\n  const bodyRef = useRef<HTMLTextAreaElement>(null);\n  useEffect(() => {\n    bodyRef.current?.setCustomValidity(validateBody(value, bodyType, scope));\n    setBodyError(\"\");\n  }, [value, bodyType, scope]);\n  const updateBody = (newBody: string) => {\n    const evaluatedValue = evaluateExpressionWithinScope(newBody, scope);\n    // automatically add Content-Type: application/json header\n    // when value is object\n    const isBodyObject =\n      typeof evaluatedValue === \"object\" && evaluatedValue !== null;\n    onChange(newBody, isBodyObject ? \"json\" : bodyType);\n  };\n\n  return (\n    <Grid gap={1}>\n      <Label>Body</Label>\n      <Select<BodyType | \"\">\n        placeholder=\"Type\"\n        value={bodyType ?? \"\"}\n        options={[\"text\", \"json\"]}\n        onChange={(newBodyType) => {\n          if (newBodyType) {\n            onChange(value, newBodyType);\n          }\n        }}\n      />\n      {bodyType && (\n        <>\n          <input type=\"hidden\" name=\"header-name\" value=\"Content-Type\" />\n          <input\n            type=\"hidden\"\n            name=\"header-value\"\n            value={`\"${toMime(bodyType)}\"`}\n          />\n        </>\n      )}\n      <textarea\n        ref={bodyRef}\n        style={{ display: \"none\" }}\n        name=\"body\"\n        data-color={bodyError ? \"error\" : undefined}\n        value={value}\n        onChange={() => {}}\n        onInvalid={(event) =>\n          setBodyError(event.currentTarget.validationMessage)\n        }\n      />\n      <BindingControl>\n        <InputErrorsTooltip errors={bodyError ? [bodyError] : undefined}>\n          {bodyType === \"json\" ? (\n            // wrap with div to position error tooltip\n            <div>\n              <ExpressionEditor\n                color={bodyError ? \"error\" : undefined}\n                // expressions with variables cannot be edited\n                readOnly={isBodyLiteral === false}\n                value={\n                  isBodyLiteral\n                    ? value\n                    : (JSON.stringify(\n                        evaluateExpressionWithinScope(value, scope),\n                        null,\n                        2\n                      ) ?? \"\")\n                }\n                onChange={updateBody}\n                onChangeComplete={() => bodyRef.current?.checkValidity()}\n              />\n            </div>\n          ) : (\n            <TextArea\n              autoGrow={true}\n              maxRows={10}\n              // expressions with variables cannot be edited\n              disabled={isBodyLiteral === false}\n              color={bodyError ? \"error\" : undefined}\n              value={String(evaluateExpressionWithinScope(value, scope) ?? \"\")}\n              // update text value as string literal\n              onChange={(newValue) => updateBody(JSON.stringify(newValue))}\n              onBlur={() => bodyRef.current?.checkValidity()}\n            />\n          )}\n        </InputErrorsTooltip>\n        <BindingPopover\n          scope={scope}\n          aliases={aliases}\n          variant={isBodyLiteral ? \"default\" : \"bound\"}\n          value={value}\n          onChange={(value) => {\n            updateBody(value);\n            setIsBodyLiteral(isLiteralExpression(value));\n          }}\n          onRemove={(evaluatedValue) => {\n            updateBody(JSON.stringify(evaluatedValue));\n            setIsBodyLiteral(true);\n          }}\n        />\n      </BindingControl>\n    </Grid>\n  );\n};\n\nconst isCacheControl = (name: string) => name.toLowerCase() === \"cache-control\";\nconst isContentType = (name: string) => name.toLowerCase() === \"content-type\";\n\nconst parseHeaders = (headers: Resource[\"headers\"]) => {\n  let maxAge: undefined | string;\n  let bodyType: BodyType;\n  const newHeaders = headers.filter((header) => {\n    // cast raw expression result to string\n    const value = String(\n      evaluateExpressionWithinScope(header.value, {})\n    ).toLowerCase();\n    if (isCacheControl(header.name)) {\n      // move simple header like Cache-Control: max-age=10 to dedicated input\n      // preserve more complex cache-control\n      const matched = value.match(/^max-age=(\\d+)$/);\n      if (matched) {\n        [, maxAge] = matched;\n        return false;\n      }\n    }\n    // store json and text in dedicated input\n    // and preserve other types\n    if (isContentType(header.name)) {\n      if (value === \"application/json\") {\n        bodyType = \"json\";\n        return false;\n      }\n      if (value === \"text/plain\") {\n        bodyType = \"text\";\n        return false;\n      }\n    }\n    return true;\n  });\n  return { headers: newHeaders, maxAge, bodyType };\n};\n\nexport const ResourceForm = forwardRef<\n  undefined | PanelApi,\n  { variable?: DataSource }\n>(({ variable }, ref) => {\n  const { scope, aliases } = useResourceScope({ variable });\n\n  const resources = useStore($resources);\n  const resource =\n    variable?.type === \"resource\"\n      ? resources.get(variable.resourceId)\n      : undefined;\n  const parsedHeaders = parseHeaders(resource?.headers ?? []);\n\n  const [url, setUrl] = useState(resource?.url ?? `\"\"`);\n  const [method, setMethod] = useState<Resource[\"method\"]>(\n    resource?.method ?? \"get\"\n  );\n  const [searchParams, setSearchParams] = useState(\n    resource?.searchParams ?? []\n  );\n  const [headers, setHeaders] = useState<Resource[\"headers\"]>(\n    parsedHeaders.headers\n  );\n  const [maxAge, setMaxAge] = useState(parsedHeaders.maxAge);\n  const [bodyType, setBodyType] = useState(parsedHeaders.bodyType);\n  const [body, setBody] = useState(resource?.body);\n\n  useImperativeHandle(ref, () => ({\n    save: (formData) => {\n      // preserve existing instance scope when edit\n      const scopeInstanceId =\n        variable?.scopeInstanceId ?? $selectedInstance.get()?.id;\n      if (scopeInstanceId === undefined) {\n        return;\n      }\n      const newResource = parseResource({\n        id: resource?.id ?? nanoid(),\n        formData,\n      });\n      const newVariable: DataSource = {\n        id: variable?.id ?? nanoid(),\n        scopeInstanceId,\n        name: newResource.name,\n        type: \"resource\",\n        resourceId: newResource.id,\n      };\n      updateWebstudioData((data) => {\n        data.dataSources.set(newVariable.id, newVariable);\n        data.resources.set(newResource.id, newResource);\n        rebindTreeVariablesMutable({\n          startingInstanceId: scopeInstanceId,\n          ...data,\n        });\n      });\n    },\n  }));\n\n  return (\n    <>\n      <MethodField value={method} onChange={setMethod} />\n      <UrlField\n        scope={scope}\n        aliases={aliases}\n        value={url}\n        onChange={(urlExpression, searchParams) => {\n          setUrl(urlExpression);\n          if (searchParams) {\n            setSearchParams((prev) => [...prev, ...searchParams]);\n          }\n        }}\n        onCurlPaste={(curl) => {\n          // update all feilds when curl is paste into url field\n          setMethod(curl.method);\n          setUrl(JSON.stringify(curl.url));\n          setSearchParams(\n            (curl.searchParams ?? []).map((header) => ({\n              name: header.name,\n              value: JSON.stringify(header.value),\n            }))\n          );\n          const parsedHeaders = parseHeaders(\n            curl.headers.map((header) => ({\n              name: header.name,\n              value: JSON.stringify(header.value),\n            }))\n          );\n          setMaxAge(parsedHeaders.maxAge);\n          setHeaders(parsedHeaders.headers);\n          setBodyType(parsedHeaders.bodyType);\n          setBody(JSON.stringify(curl.body));\n        }}\n      />\n      <SearchParams\n        scope={scope}\n        aliases={aliases}\n        searchParams={searchParams}\n        onChange={setSearchParams}\n      />\n      <CacheMaxAge\n        value={maxAge}\n        onChange={(newMaxAge) => {\n          setMaxAge(newMaxAge);\n          // reset header\n          setHeaders((headers) =>\n            headers.filter(({ name }) => !isCacheControl(name))\n          );\n        }}\n      />\n      <Headers\n        scope={scope}\n        aliases={aliases}\n        headers={headers}\n        onChange={(newHeaders) => {\n          // reset dedicated fields\n          if (newHeaders.some(({ name }) => isCacheControl(name))) {\n            setMaxAge(undefined);\n          }\n          if (newHeaders.some(({ name }) => isContentType(name))) {\n            setBodyType(undefined);\n          }\n          setHeaders(newHeaders);\n        }}\n      />\n      {method !== \"get\" && (\n        <BodyField\n          scope={scope}\n          aliases={aliases}\n          value={body ?? \"\"}\n          bodyType={bodyType}\n          onChange={(newBody, newBodyType) => {\n            setBodyType(newBodyType);\n            // reset header\n            if (newBodyType) {\n              setHeaders((headers) =>\n                headers.filter(({ name }) => !isContentType(name))\n              );\n            }\n            setBody(newBody);\n          }}\n        />\n      )}\n    </>\n  );\n});\nResourceForm.displayName = \"ResourceForm\";\n\nexport const SystemResourceForm = forwardRef<\n  undefined | PanelApi,\n  { variable?: DataSource }\n>(({ variable }, ref) => {\n  const resources = useStore($resources);\n\n  const resource =\n    variable?.type === \"resource\"\n      ? resources.get(variable.resourceId)\n      : undefined;\n\n  const localResources = [\n    {\n      label: \"Sitemap\",\n      value: JSON.stringify(sitemapResourceUrl),\n      description: \"Resource that loads the sitemap data of the current site.\",\n    },\n    {\n      label: \"Current Date\",\n      value: JSON.stringify(currentDateResourceUrl),\n      description:\n        \"Provides current date information (year, month, day) normalized to midnight UTC. Time components are set to 00:00:00 to prevent React hydration errors.\",\n    },\n    {\n      label: \"Assets\",\n      value: JSON.stringify(assetsResourceUrl),\n      description:\n        \"Resource that loads the list of assets of the current project.\",\n    },\n  ];\n\n  const [localResource, setLocalResource] = useState(() => {\n    return (\n      localResources.find(\n        (localResource) => localResource.value === resource?.url\n      ) ?? localResources[0]\n    );\n  });\n\n  useImperativeHandle(ref, () => ({\n    save: (formData) => {\n      // preserve existing instance scope when edit\n      const scopeInstanceId =\n        variable?.scopeInstanceId ?? $selectedInstance.get()?.id;\n      if (scopeInstanceId === undefined) {\n        return;\n      }\n      const newResource: Resource = parseResource({\n        id: resource?.id ?? nanoid(),\n        control: \"system\",\n        formData,\n      });\n      const newVariable: DataSource = {\n        id: variable?.id ?? nanoid(),\n        scopeInstanceId,\n        name: newResource.name,\n        type: \"resource\",\n        resourceId: newResource.id,\n      };\n      updateWebstudioData((data) => {\n        data.dataSources.set(newVariable.id, newVariable);\n        data.resources.set(newResource.id, newResource);\n        rebindTreeVariablesMutable({\n          startingInstanceId: scopeInstanceId,\n          ...data,\n        });\n      });\n    },\n  }));\n\n  const resourceId = useId();\n\n  return (\n    <>\n      <input type=\"hidden\" name=\"method\" value=\"get\" />\n      <input type=\"hidden\" name=\"url\" value={localResource.value} />\n      <Flex direction=\"column\" css={{ gap: theme.spacing[3] }}>\n        <Label htmlFor={resourceId}>Resource</Label>\n        <Select\n          options={localResources}\n          getLabel={(option) => option.label}\n          getValue={(option) => option.value}\n          getDescription={(option) => {\n            return (\n              <Box css={{ width: theme.spacing[25] }}>\n                {option?.description}\n              </Box>\n            );\n          }}\n          value={localResource}\n          onChange={setLocalResource}\n        />\n      </Flex>\n    </>\n  );\n});\nSystemResourceForm.displayName = \"SystemResourceForm\";\n\nconst zGraphqlBody = z.object({\n  query: z.string(),\n  variables: z.optional(z.record(z.unknown())),\n});\n\nexport const GraphqlResourceForm = forwardRef<\n  undefined | PanelApi,\n  { variable?: DataSource }\n>(({ variable }, ref) => {\n  const { scope, aliases } = useResourceScope({ variable });\n\n  const resources = useStore($resources);\n  const resource =\n    variable?.type === \"resource\"\n      ? resources.get(variable.resourceId)\n      : undefined;\n\n  const [url, setUrl] = useState(resource?.url ?? `\"\"`);\n  const parsedHeaders = parseHeaders(resource?.headers ?? []);\n  const [maxAge, setMaxAge] = useState(parsedHeaders.maxAge);\n  const [headers, setHeaders] = useState(parsedHeaders.headers);\n\n  const [bodyExpressions] = useState(() =>\n    parseObjectExpression(resource?.body ?? \"\")\n  );\n  const queryId = useId();\n  const [query, setQuery] = useState(\n    () =>\n      evaluateExpressionWithinScope(bodyExpressions.get(\"query\") ?? \"\", {}) ??\n      \"\"\n  );\n  const [variables, setVariables] = useState(\n    () => bodyExpressions.get(\"variables\") ?? \"{}\"\n  );\n  const [isVariablesLiteral, setIsVariablesLiteral] = useState(() =>\n    isLiteralExpression(variables)\n  );\n  const [variablesError, setVariablesError] = useState(\"\");\n  const variablesRef = useRef<HTMLInputElement>(null);\n  useEffect(() => {\n    const evaluatedValue = evaluateExpressionWithinScope(variables, scope);\n    variablesRef.current?.setCustomValidity(\n      typeof evaluatedValue === \"object\" && evaluatedValue !== null\n        ? \"\"\n        : \"Expected valid JSON object in GraphQL variables\"\n    );\n    setVariablesError(\"\");\n  }, [variables, scope]);\n\n  useImperativeHandle(ref, () => ({\n    save: (formData) => {\n      // preserve existing instance scope when edit\n      const scopeInstanceId =\n        variable?.scopeInstanceId ?? $selectedInstance.get()?.id;\n      if (scopeInstanceId === undefined) {\n        return;\n      }\n      const newResource = parseResource({\n        id: resource?.id ?? nanoid(),\n        control: \"graphql\",\n        formData,\n      });\n      const newVariable: DataSource = {\n        id: variable?.id ?? nanoid(),\n        scopeInstanceId,\n        name: newResource.name,\n        type: \"resource\",\n        resourceId: newResource.id,\n      };\n      updateWebstudioData((data) => {\n        data.dataSources.set(newVariable.id, newVariable);\n        data.resources.set(newResource.id, newResource);\n        rebindTreeVariablesMutable({\n          startingInstanceId: scopeInstanceId,\n          ...data,\n        });\n      });\n    },\n  }));\n\n  return (\n    <>\n      <input type=\"hidden\" name=\"method\" value=\"post\" />\n      {!headers.some(({ name }) => isContentType(name)) && (\n        <>\n          <input type=\"hidden\" name=\"header-name\" value=\"Content-Type\" />\n          <input\n            type=\"hidden\"\n            name=\"header-value\"\n            value={`\"application/json\"`}\n          />\n        </>\n      )}\n      <input\n        type=\"hidden\"\n        name=\"body\"\n        value={generateObjectExpression(\n          new Map([\n            [\"query\", JSON.stringify(query)],\n            [\"variables\", variables],\n          ])\n        )}\n      />\n\n      <UrlField\n        scope={scope}\n        aliases={aliases}\n        value={url}\n        onChange={setUrl}\n        onCurlPaste={(curl) => {\n          // update all feilds when curl is paste into url field\n          setUrl(JSON.stringify(curl.url));\n          const parsedHeaders = parseHeaders(\n            curl.headers.map((header) => ({\n              name: header.name,\n              value: JSON.stringify(header.value),\n            }))\n          );\n          setMaxAge(parsedHeaders.maxAge);\n          setHeaders(parsedHeaders.headers);\n          const body = zGraphqlBody.safeParse(curl.body);\n          if (body.success) {\n            setQuery(body.data.query);\n            setVariables(JSON.stringify(body.data.variables, null, 2));\n          }\n        }}\n      />\n\n      <Grid gap={1}>\n        <Label htmlFor={queryId}>Query</Label>\n        <EditorDialogControl>\n          <TextArea\n            name=\"query\"\n            id={queryId}\n            rows={3}\n            maxRows={10}\n            autoGrow={true}\n            value={query}\n            onChange={setQuery}\n          />\n          <EditorDialog\n            title=\"GraphQL Query\"\n            content={<TextArea grow={true} value={query} onChange={setQuery} />}\n          >\n            <EditorDialogButton />\n          </EditorDialog>\n        </EditorDialogControl>\n      </Grid>\n\n      <Grid gap={1}>\n        <Label>GraphQL variables</Label>\n        {/* use invisible text input to reflect expression editor in form\n            type=hidden does not emit invalid event */}\n        <input\n          ref={variablesRef}\n          style={{ display: \"none\" }}\n          type=\"text\"\n          name=\"variables\"\n          data-color={variablesError ? \"error\" : undefined}\n          value={variables}\n          onChange={() => {}}\n          onInvalid={(event) =>\n            setVariablesError(event.currentTarget.validationMessage)\n          }\n        />\n        <BindingControl>\n          <InputErrorsTooltip\n            errors={variablesError ? [variablesError] : undefined}\n          >\n            {/* wrap with div to position error tooltip */}\n            <div>\n              <ExpressionEditor\n                color={variablesError ? \"error\" : undefined}\n                readOnly={isVariablesLiteral === false}\n                value={\n                  isVariablesLiteral\n                    ? variables\n                    : (JSON.stringify(\n                        evaluateExpressionWithinScope(variables, scope),\n                        null,\n                        2\n                      ) ?? \"\")\n                }\n                onChange={setVariables}\n                onChangeComplete={() => variablesRef.current?.checkValidity()}\n              />\n            </div>\n          </InputErrorsTooltip>\n          <BindingPopover\n            scope={scope}\n            aliases={aliases}\n            variant={isVariablesLiteral ? \"default\" : \"bound\"}\n            value={variables}\n            onChange={(value) => {\n              setVariables(value);\n              setIsVariablesLiteral(isLiteralExpression(value));\n            }}\n            onRemove={(evaluatedValue) => {\n              setVariables(JSON.stringify(evaluatedValue));\n              setIsVariablesLiteral(true);\n            }}\n          />\n        </BindingControl>\n      </Grid>\n\n      <CacheMaxAge\n        value={maxAge}\n        onChange={(newMaxAge) => {\n          setMaxAge(newMaxAge);\n          setHeaders((headers) =>\n            headers.filter(({ name }) => !isCacheControl(name))\n          );\n        }}\n      />\n\n      <Headers\n        scope={scope}\n        aliases={aliases}\n        headers={headers}\n        onChange={(newHeaders) => {\n          // reset dedicated fields\n          if (newHeaders.some(({ name }) => isCacheControl(name))) {\n            setMaxAge(undefined);\n          }\n          setHeaders(newHeaders);\n        }}\n      />\n    </>\n  );\n});\nGraphqlResourceForm.displayName = \"GraphqlResourceForm\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/settings-panel.tsx",
    "content": "import type { Instance } from \"@webstudio-is/sdk\";\nimport { SettingsSection } from \"./settings-section\";\nimport { PropsSectionContainer } from \"./props-section/props-section\";\nimport { VariablesSection } from \"./variables-section\";\nimport {\n  Box,\n  Flex,\n  Link,\n  PanelBanner,\n  Text,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { UpgradeIcon } from \"@webstudio-is/icons\";\nimport { useStore } from \"@nanostores/react\";\nimport cmsUpgradeBanner from \"~/shared/cms-upgrade-banner.svg?url\";\nimport { $isDesignMode, $userPlanFeatures } from \"~/shared/nano-states\";\n\nexport const SettingsPanel = ({\n  selectedInstance,\n  selectedInstanceKey,\n}: {\n  selectedInstance: Instance;\n  selectedInstanceKey: string;\n}) => {\n  const { allowDynamicData } = useStore($userPlanFeatures);\n  const isDesignMode = useStore($isDesignMode);\n\n  return (\n    <Box css={{ pt: theme.spacing[5] }}>\n      <SettingsSection />\n\n      <PropsSectionContainer\n        selectedInstance={selectedInstance}\n        selectedInstanceKey={selectedInstanceKey}\n      />\n\n      {isDesignMode && <VariablesSection />}\n\n      {allowDynamicData === false && (\n        <PanelBanner>\n          <img\n            src={cmsUpgradeBanner}\n            alt=\"Upgrade for CMS\"\n            width={rawTheme.spacing[28]}\n            style={{ aspectRatio: \"4.1\" }}\n          />\n          <Text variant=\"regularBold\">Upgrade for CMS</Text>\n          <Text>\n            Integrate content from other tools to create blogs, directories, and\n            any other structured content.\n          </Text>\n          <Flex align=\"center\" gap={1}>\n            <UpgradeIcon />\n            <Link\n              color=\"inherit\"\n              target=\"_blank\"\n              href=\"https://webstudio.is/pricing\"\n            >\n              Upgrade to Pro\n            </Link>\n          </Flex>\n        </PanelBanner>\n      )}\n    </Box>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/settings-section.tsx",
    "content": "import { useId } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { InputField } from \"@webstudio-is/design-system\";\nimport { $instances } from \"~/shared/sync/data-stores\";\nimport { HorizontalLayout, Label, Row, useLocalValue } from \"./shared\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { $selectedInstance } from \"~/shared/awareness\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\n\nconst saveLabel = (label: string, selectedInstance: Instance) => {\n  serverSyncStore.createTransaction([$instances], (instances) => {\n    const instance = instances.get(selectedInstance.id);\n    if (instance !== undefined) {\n      instance.label = label;\n    }\n  });\n};\n\nexport const SettingsSection = () => {\n  const selectedInstance = useStore($selectedInstance);\n  const id = useId();\n  const localValue = useLocalValue(selectedInstance?.label ?? \"\", (value) => {\n    if (selectedInstance) {\n      saveLabel(value, selectedInstance);\n    }\n  });\n\n  if (selectedInstance === undefined) {\n    return;\n  }\n\n  const placeholder = getInstanceLabel(selectedInstance);\n\n  return (\n    <Row>\n      <HorizontalLayout label={<Label htmlFor={id}>Name</Label>}>\n        <InputField\n          id={id}\n          /* Key is required, otherwise when label is undefined, previous value stayed */\n          key={selectedInstance.id}\n          placeholder={placeholder}\n          value={localValue.value}\n          onChange={(event) => localValue.set(event.target.value.trim())}\n          onBlur={localValue.save}\n        />\n      </HorizontalLayout>\n    </Row>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/shared.tsx",
    "content": "import { atom, computed, type ReadableAtom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport {\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  useRef,\n  useState,\n  useEffect,\n  useMemo,\n  type ComponentProps,\n} from \"react\";\nimport equal from \"fast-deep-equal\";\nimport {\n  ariaAttributes,\n  attributesByTag,\n  elementsByTag,\n} from \"@webstudio-is/html-data\";\nimport {\n  reactPropsToStandardAttributes,\n  showAttribute,\n  standardAttributesToReactProps,\n} from \"@webstudio-is/react-sdk\";\nimport {\n  decodeDataSourceVariable,\n  encodeDataSourceVariable,\n  SYSTEM_VARIABLE_ID,\n  systemParameter,\n} from \"@webstudio-is/sdk\";\nimport type { PropMeta, Prop, Asset } from \"@webstudio-is/sdk\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport {\n  Label as BaseLabel,\n  useIsTruncated,\n  Tooltip,\n  Box,\n  Flex,\n  Grid,\n  Text,\n  theme,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport {\n  $dataSourceVariables,\n  $dataSources,\n  $registeredComponentMetas,\n  $variableValuesByInstanceSelector,\n} from \"~/shared/nano-states\";\nimport type { BindingVariant } from \"~/builder/shared/binding-popover\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport {\n  $selectedInstance,\n  $selectedInstanceKeyWithRoot,\n} from \"~/shared/awareness\";\nimport { $instanceTags } from \"../style-panel/shared/model\";\n\nexport const showAttributeMeta: PropMeta = {\n  label: \"Show\",\n  required: false,\n  control: \"boolean\",\n  type: \"boolean\",\n  defaultValue: true,\n  // If you are changing it, change the other one too\n  description:\n    \"Removes the instance from the DOM. Breakpoints have no effect on this setting.\",\n};\n\nexport type PropValue =\n  | { type: \"number\"; value: number }\n  | { type: \"string\"; value: string }\n  | { type: \"boolean\"; value: boolean }\n  | { type: \"json\"; value: unknown }\n  | { type: \"string[]\"; value: string[] }\n  | { type: \"expression\"; value: string }\n  | { type: \"asset\"; value: Asset[\"id\"] }\n  | { type: \"page\"; value: Extract<Prop, { type: \"page\" }>[\"value\"] }\n  | { type: \"action\"; value: Extract<Prop, { type: \"action\" }>[\"value\"] }\n  | {\n      type: \"animationAction\";\n      value: Extract<Prop, { type: \"animationAction\" }>[\"value\"];\n    };\n\n// Weird code is to make type distributive\n// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types\ntype PropMetaByControl<Control> = Control extends string\n  ? Extract<PropMeta, { control: Control }>\n  : never;\nexport type ControlProps<Control> = {\n  instanceId: string;\n  meta: PropMetaByControl<Control>;\n  // prop is optional because we don't have it when an intial prop is not set\n  // and we don't want to show user something like a 0 for number when it's in fact not set to any value\n  prop: Prop | undefined;\n  propName: string;\n  computedValue: unknown;\n  onChange: (value: PropValue) => void;\n};\n\nconst SimpleLabel = ({\n  children,\n  ...rest\n}: ComponentPropsWithoutRef<typeof BaseLabel> & { children: string }) => {\n  const ref = useRef<HTMLLabelElement>(null);\n  const truncated = useIsTruncated(ref, children);\n\n  const label = (\n    <BaseLabel truncate {...rest} ref={ref}>\n      {children}\n    </BaseLabel>\n  );\n\n  return truncated ? <Tooltip content={children}>{label}</Tooltip> : label;\n};\n\ntype LabelProps = ComponentPropsWithoutRef<typeof BaseLabel> & {\n  htmlFor?: string;\n  children: string;\n  description?: string;\n  openOnClick?: boolean;\n  readOnly?: boolean;\n};\n\nexport const Label = ({\n  htmlFor,\n  children,\n  description,\n  openOnClick = false,\n  readOnly,\n  ...rest\n}: LabelProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  let label: ReactNode;\n\n  if (description == null) {\n    label = <SimpleLabel htmlFor={htmlFor}>{children}</SimpleLabel>;\n  } else {\n    label = (\n      <Tooltip\n        open={isOpen}\n        onOpenChange={setIsOpen}\n        content={\n          <Flex\n            direction=\"column\"\n            gap=\"2\"\n            css={{ maxWidth: theme.spacing[28] }}\n          >\n            <Text variant=\"titles\">{children}</Text>\n            <Text>{description}</Text>\n          </Flex>\n        }\n      >\n        <BaseLabel truncate htmlFor={htmlFor} {...rest}>\n          {children}\n        </BaseLabel>\n      </Tooltip>\n    );\n  }\n\n  return (\n    <Flex align=\"center\" css={{ gap: theme.spacing[3], width: \"100%\" }}>\n      <Box>{label}</Box>\n      {readOnly && (\n        <Tooltip\n          content={\n            \"The value is controlled by an expression and cannot be changed.\"\n          }\n          variant=\"wrapped\"\n        >\n          <InfoCircleIcon\n            color={rawTheme.colors.foregroundSubtle}\n            tabIndex={0}\n          />\n        </Tooltip>\n      )}\n    </Flex>\n  );\n};\n\nexport const useLocalValue = <Type,>(\n  savedValue: Type,\n  onSave: (value: Type) => void,\n  { autoSave = true } = {}\n) => {\n  const isEditingRef = useRef(false);\n  const localValueRef = useRef(savedValue);\n\n  const [_, setRefresh] = useState(0);\n\n  const onSaveRef = useRef(onSave);\n  onSaveRef.current = onSave;\n\n  const save = () => {\n    isEditingRef.current = false;\n    if (equal(localValueRef.current, savedValue) === false) {\n      // To synchronize with setState immediately followed by save\n      onSaveRef.current(localValueRef.current);\n    }\n  };\n\n  const saveDebounced = useDebouncedCallback(save, 500);\n\n  const setLocalValue = (value: Type) => {\n    isEditingRef.current = true;\n    localValueRef.current = value;\n    setRefresh((refresh) => refresh + 1);\n    if (autoSave) {\n      saveDebounced();\n    }\n  };\n\n  // onBlur will not trigger if control is unmounted when props panel is closed or similar.\n  // So we're saving at the unmount\n  // store save in ref to access latest saved value from render\n  // instead of stale one\n  const saveRef = useRef(save);\n  saveRef.current = save;\n  useEffect(() => {\n    // access ref in the moment of unmount\n    return () => saveRef.current();\n  }, []);\n\n  useEffect(() => {\n    // Update local value if saved value changes and control is not in edit mode.\n    if (\n      isEditingRef.current === false &&\n      localValueRef.current !== savedValue\n    ) {\n      localValueRef.current = savedValue;\n      setRefresh((refresh) => refresh + 1);\n    }\n  }, [savedValue]);\n\n  return {\n    /**\n     * Contains:\n     *  - either the latest `savedValue`\n     *  - or the latest value set via `set()`\n     * (whichever changed most recently)\n     */\n    value: localValueRef.current,\n    /**\n     * Should be called on onChange or similar event\n     */\n    set: setLocalValue,\n    /**\n     * Should be called on onBlur or similar event\n     */\n    save,\n  };\n};\n\ntype LayoutProps = {\n  label: ReturnType<typeof Label>;\n  children: ReactNode;\n};\n\nexport const VerticalLayout = ({ label, children }: LayoutProps) => (\n  <Box>\n    <Grid\n      css={{\n        gridTemplateColumns: `1fr`,\n        justifyItems: \"start\",\n      }}\n      align=\"center\"\n      gap=\"1\"\n      justify=\"between\"\n    >\n      {label}\n    </Grid>\n    <Box css={{ py: theme.spacing[2] }}>{children}</Box>\n  </Box>\n);\n\nexport const HorizontalLayout = ({ label, children }: LayoutProps) => (\n  <Grid\n    css={{\n      gridTemplateColumns: `${theme.spacing[19]} 1fr`,\n      minHeight: theme.spacing[12],\n    }}\n    align=\"center\"\n    gap=\"2\"\n  >\n    {label}\n    {children}\n  </Grid>\n);\n\nexport const ResponsiveLayout = ({ label, children }: LayoutProps) => {\n  return (\n    <Flex\n      align=\"center\"\n      wrap=\"wrap\"\n      css={{\n        columnGap: theme.spacing[5],\n        rowGap: theme.spacing[3],\n        paddingBlock: theme.spacing[2],\n      }}\n    >\n      <Box\n        css={{\n          // wrap label and input when label is more than ~9 characters\n          flexBasis: `calc(30% - ${theme.spacing[5]} / 2)`,\n          // allow content overflow flex basis\n          minWidth: \"auto\",\n        }}\n      >\n        {label}\n      </Box>\n      <Box\n        css={{ flexBasis: `calc(70% - ${theme.spacing[5]} / 2)`, flexGrow: 1 }}\n      >\n        {children}\n      </Box>\n    </Flex>\n  );\n};\n\nexport const Row = ({\n  children,\n  css,\n}: Pick<ComponentProps<typeof Flex>, \"css\" | \"children\">) => (\n  <Flex\n    css={{ paddingInline: theme.panel.paddingInline, ...css }}\n    direction=\"column\"\n  >\n    {children}\n  </Flex>\n);\n\nexport const $selectedInstanceScope = computed(\n  [\n    $selectedInstanceKeyWithRoot,\n    $variableValuesByInstanceSelector,\n    $dataSources,\n  ],\n  (instanceKey, variableValuesByInstanceSelector, dataSources) => {\n    const scope: Record<string, unknown> = {};\n    const aliases = new Map<string, string>();\n    if (instanceKey === undefined) {\n      return { scope, aliases };\n    }\n    const values = variableValuesByInstanceSelector.get(instanceKey);\n    if (values) {\n      for (const [dataSourceId, value] of values) {\n        let dataSource = dataSources.get(dataSourceId);\n        if (dataSourceId === SYSTEM_VARIABLE_ID) {\n          dataSource = systemParameter;\n        }\n        if (dataSource === undefined) {\n          continue;\n        }\n        const name = encodeDataSourceVariable(dataSourceId);\n        scope[name] = value;\n        aliases.set(name, dataSource.name);\n      }\n    }\n    return { scope, aliases };\n  }\n);\n\nexport const updateExpressionValue = (expression: string, value: unknown) => {\n  const dataSources = $dataSources.get();\n  // when expression contains only reference to variable update that variable\n  // extract id without parsing expression\n  const potentialVariableId = decodeDataSourceVariable(expression);\n  if (\n    potentialVariableId !== undefined &&\n    dataSources.has(potentialVariableId)\n  ) {\n    const dataSourceId = potentialVariableId;\n    const dataSourceVariables = new Map($dataSourceVariables.get());\n    dataSourceVariables.set(dataSourceId, value);\n    $dataSourceVariables.set(dataSourceVariables);\n  }\n};\n\ntype BindingState = {\n  overwritable: boolean;\n  variant: BindingVariant;\n};\n\nexport const useBindingState = (expression: undefined | string) => {\n  const $bindingState = useMemo((): ReadableAtom<BindingState> => {\n    if (expression === undefined) {\n      // value is not bound to expression and can be updated\n      return atom({ overwritable: true, variant: \"default\" });\n    }\n    // try to extract variable id from expression\n    const potentialVariableId = decodeDataSourceVariable(expression);\n    if (potentialVariableId === undefined) {\n      // expression is complex and cannot be updated\n      return atom({ overwritable: false, variant: \"bound\" });\n    }\n    return computed(\n      [$dataSources, $dataSourceVariables],\n      (dataSources, dataSourceVariables): BindingState => {\n        const dataSource = dataSources.get(potentialVariableId);\n        // resources and parameters cannot be updated\n        if (dataSource?.type !== \"variable\") {\n          return { overwritable: false, variant: \"bound\" };\n        }\n        const variableId = potentialVariableId;\n        return {\n          overwritable: true,\n          variant:\n            dataSourceVariables.get(variableId) === undefined\n              ? \"bound\"\n              : \"overwritten\",\n        };\n      }\n    );\n  }, [expression]);\n  return useStore($bindingState);\n};\n\nexport const humanizeAttribute = (string: string) => {\n  if (string.includes(\"-\")) {\n    return string;\n  }\n  if (string === \"class\" || string === \"className\") {\n    return \"Class\";\n  }\n  if (string === \"for\" || string === \"htmlFor\") {\n    return \"For\";\n  }\n  return humanizeString(standardAttributesToReactProps[string] ?? string);\n};\n\ntype Attribute = (typeof ariaAttributes)[number];\n\nconst attributeToMeta = (attribute: Attribute): PropMeta => {\n  const required = attribute.required ?? false;\n  const description = attribute.description;\n  if (attribute.type === \"select\") {\n    const options = attribute.options ?? [];\n    return {\n      type: \"string\",\n      control: options.length > 3 ? \"select\" : \"radio\",\n      required,\n      options,\n      description,\n    };\n  }\n  if (attribute.type === \"url\") {\n    return { type: \"string\", control: \"url\", required, description };\n  }\n  if (attribute.type === \"string\") {\n    return { type: \"string\", control: \"text\", required, description };\n  }\n  if (attribute.type === \"number\") {\n    return { type: \"number\", control: \"number\", required, description };\n  }\n  if (attribute.type === \"boolean\") {\n    return { type: \"boolean\", control: \"boolean\", required, description };\n  }\n  attribute.type satisfies never;\n  throw Error(\"impossible case\");\n};\n\nexport const $selectedInstancePropsMetas = computed(\n  [$selectedInstance, $registeredComponentMetas, $instanceTags],\n  (instance, metas, instanceTags): Map<string, PropMeta> => {\n    if (instance === undefined) {\n      return new Map();\n    }\n    const meta = metas.get(instance.component);\n    const tag = instanceTags.get(instance.id);\n    const propsMetas = new Map<Prop[\"name\"], PropMeta>();\n    // add html attributes only when instance has tag\n    if (tag) {\n      if (elementsByTag[tag].categories.includes(\"html-element\")) {\n        for (const attribute of [...ariaAttributes].reverse()) {\n          propsMetas.set(attribute.name, attributeToMeta(attribute));\n        }\n        // include global attributes only for html elements\n        if (attributesByTag[\"*\"]) {\n          for (const attribute of [...attributesByTag[\"*\"]].reverse()) {\n            propsMetas.set(attribute.name, attributeToMeta(attribute));\n          }\n        }\n      }\n      if (attributesByTag[tag]) {\n        for (const attribute of [...attributesByTag[tag]].reverse()) {\n          propsMetas.set(attribute.name, attributeToMeta(attribute));\n        }\n      }\n    }\n    for (const [name, propMeta] of Object.entries(\n      meta?.props ?? {}\n    ).reverse()) {\n      // when component property has the same name as html attribute in react\n      // override to deduplicate similar properties\n      // for example component can have own \"className\" and html has \"class\"\n      const htmlName = reactPropsToStandardAttributes[name];\n      if (htmlName) {\n        propsMetas.delete(htmlName);\n      }\n      propsMetas.set(name, propMeta);\n    }\n    propsMetas.set(showAttribute, showAttributeMeta);\n    // ui should render in the following order\n    // 1. system properties\n    // 2. component properties\n    // 3. specific tag attributes\n    // 4. global html attributes\n    // 5. aria attributes\n    return new Map(Array.from(propsMetas.entries()).reverse());\n  }\n);\n\nexport const $selectedInstanceInitialPropNames = computed(\n  [$selectedInstance, $registeredComponentMetas, $selectedInstancePropsMetas],\n  (selectedInstance, metas, instancePropsMetas) => {\n    const initialPropNames = new Set<string>();\n    if (selectedInstance) {\n      const initialProps =\n        metas.get(selectedInstance.component)?.initialProps ?? [];\n      for (const propName of initialProps) {\n        // className -> class\n        if (instancePropsMetas.has(reactPropsToStandardAttributes[propName])) {\n          initialPropNames.add(reactPropsToStandardAttributes[propName]);\n        } else {\n          initialPropNames.add(propName);\n        }\n      }\n    }\n    for (const [propName, propMeta] of instancePropsMetas) {\n      // skip show attribute which is added as system prop\n      if (propName === showAttribute) {\n        continue;\n      }\n      if (propMeta.required) {\n        initialPropNames.add(propName);\n      }\n    }\n    return initialPropNames;\n  }\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/variable-popover.tsx",
    "content": "import { z } from \"zod\";\nimport { nanoid } from \"nanoid\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { javascript } from \"@codemirror/lang-javascript\";\nimport {\n  type ReactNode,\n  type Ref,\n  type RefObject,\n  forwardRef,\n  useId,\n  useState,\n  useImperativeHandle,\n  useRef,\n  useEffect,\n  useCallback,\n  useMemo,\n} from \"react\";\nimport { CopyIcon, RefreshIcon, UpgradeIcon } from \"@webstudio-is/icons\";\nimport {\n  Box,\n  Button,\n  Combobox,\n  DialogClose,\n  DialogMaximize,\n  DialogTitle,\n  DialogTitleActions,\n  Flex,\n  FloatingPanel,\n  Grid,\n  InputErrorsTooltip,\n  InputField,\n  Label,\n  Link,\n  PanelBanner,\n  ProBadge,\n  ScrollArea,\n  Select,\n  Switch,\n  Text,\n  TextArea,\n  Tooltip,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport {\n  type DataSource,\n  transpileExpression,\n  lintExpression,\n  SYSTEM_VARIABLE_ID,\n  ResourceRequest,\n} from \"@webstudio-is/sdk\";\nimport {\n  ExpressionEditor,\n  formatValue,\n} from \"~/builder/shared/expression-editor\";\nimport {\n  $dataSources,\n  $resources,\n  $userPlanFeatures,\n  $instances,\n  $props,\n  $variableValuesByInstanceSelector,\n} from \"~/shared/nano-states\";\nimport {\n  $selectedInstance,\n  $selectedInstanceKeyWithRoot,\n} from \"~/shared/awareness\";\nimport {\n  EditorContent,\n  EditorDialog,\n  EditorDialogButton,\n  EditorDialogControl,\n  foldGutterExtension,\n} from \"~/shared/code-editor-base\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport {\n  findUnsetVariableNames,\n  rebindTreeVariablesMutable,\n} from \"~/shared/data-variables\";\nimport { validateDataVariableName } from \"~/builder/shared/data-variable-utils\";\nimport {\n  GraphqlResourceForm,\n  parseResource,\n  ResourceForm,\n  SystemResourceForm,\n  useResourceScope,\n} from \"./resource-panel\";\nimport { generateCurl } from \"./curl\";\nimport {\n  $hasPendingResources,\n  $resourcesCache,\n  computeResourceRequest,\n  getResourceKey,\n  invalidateResource,\n} from \"~/shared/resources\";\n\nconst NameField = ({\n  variable,\n  defaultValue,\n}: {\n  variable: undefined | DataSource;\n  defaultValue: string;\n}) => {\n  const ref = useRef<HTMLInputElement>(null);\n  const [error, setError] = useState(\"\");\n  const nameId = useId();\n  const scopeInstanceId =\n    variable?.scopeInstanceId ?? $selectedInstance.get()?.id;\n  const validateName = useCallback(\n    (value: string) => {\n      const error = validateDataVariableName(\n        value,\n        variable?.id,\n        scopeInstanceId\n      );\n      return error?.message ?? \"\";\n    },\n    [variable, scopeInstanceId]\n  );\n  const [value, setValue] = useState(defaultValue);\n  useEffect(() => {\n    ref.current?.setCustomValidity(validateName(value));\n  }, [value, validateName]);\n  return (\n    <Grid gap={1}>\n      <Label htmlFor={nameId}>Name</Label>\n      <InputErrorsTooltip errors={error ? [error] : undefined}>\n        <Combobox<string>\n          inputRef={ref}\n          name=\"name\"\n          id={nameId}\n          color={error ? \"error\" : undefined}\n          itemToString={(item) => item ?? \"\"}\n          getDescription={() => (\n            <>\n              Enter a new variable or select\n              <br />\n              a variable that has been used\n              <br />\n              in expressions but not yet created\n            </>\n          )}\n          getItems={() => {\n            // find unset variables for variable instance\n            // and fallback to selected instance for new variables\n            const scopeInstanceId =\n              variable?.scopeInstanceId ?? $selectedInstance.get()?.id;\n            if (scopeInstanceId === undefined) {\n              return [];\n            }\n            return findUnsetVariableNames({\n              startingInstanceId: scopeInstanceId,\n              instances: $instances.get(),\n              props: $props.get(),\n              dataSources: $dataSources.get(),\n              resources: $resources.get(),\n            });\n          }}\n          value={value}\n          onItemSelect={(newValue) => {\n            ref.current?.setCustomValidity(validateName(newValue));\n            setValue(newValue);\n            setError(\"\");\n          }}\n          onChange={(newValue = \"\") => {\n            ref.current?.setCustomValidity(validateName(newValue));\n            setValue(newValue);\n            setError(\"\");\n          }}\n          onBlur={() => ref.current?.checkValidity()}\n          onInvalid={(event) => setError(event.currentTarget.validationMessage)}\n        />\n      </InputErrorsTooltip>\n    </Grid>\n  );\n};\n\ntype VariableType =\n  | \"parameter\"\n  | \"string\"\n  | \"number\"\n  | \"boolean\"\n  | \"json\"\n  | \"resource\"\n  | \"graphql-resource\"\n  | \"system-resource\";\n\nconst TypeField = ({\n  value,\n  onChange,\n}: {\n  value: VariableType;\n  onChange: (value: VariableType) => void;\n}) => {\n  const { allowDynamicData } = useStore($userPlanFeatures);\n  const optionsList: Array<{\n    value: VariableType;\n    disabled?: boolean;\n    label: ReactNode;\n    description: string;\n  }> = [\n    {\n      value: \"string\",\n      label: \"String\",\n      description: \"Any alphanumeric text.\",\n    },\n    {\n      value: \"number\",\n      label: \"Number\",\n      description: \"Any number, can be used in math expressions.\",\n    },\n    {\n      value: \"boolean\",\n      label: \"Boolean\",\n      description: \"A boolean is a true/false switch.\",\n    },\n    {\n      value: \"json\",\n      label: \"JSON\",\n      description: \"Any JSON value\",\n    },\n    {\n      value: \"resource\",\n      label: (\n        <Flex direction=\"row\" gap=\"2\" align=\"center\">\n          Resource\n          {allowDynamicData === false && <ProBadge>Pro</ProBadge>}\n        </Flex>\n      ),\n      description:\n        \"A Resource is a configuration for secure data fetching. You can safely use secrets in any field.\",\n    },\n    {\n      value: \"graphql-resource\",\n      label: (\n        <Flex direction=\"row\" gap=\"2\" align=\"center\">\n          GraphQL\n          {allowDynamicData === false && <ProBadge>Pro</ProBadge>}\n        </Flex>\n      ),\n      description:\n        \"A Resource is a configuration for secure data fetching. You can safely use secrets in any field.\",\n    },\n    {\n      value: \"system-resource\",\n      label: (\n        <Flex direction=\"row\" gap=\"2\" align=\"center\">\n          System Resource\n          {allowDynamicData === false && <ProBadge>Pro</ProBadge>}\n        </Flex>\n      ),\n      description: \"A System Resource is a configuration for Webstudio data.\",\n    },\n  ];\n  const options = new Map(optionsList.map((option) => [option.value, option]));\n\n  return (\n    <Grid gap=\"1\">\n      <Label>Type</Label>\n      <Select\n        options={Array.from(options.keys())}\n        getLabel={(option: VariableType) => options.get(option)?.label}\n        getItemProps={(option) => ({\n          disabled: options.get(option)?.disabled,\n        })}\n        getDescription={(option) => (\n          <Box css={{ width: theme.spacing[27] }}>\n            {options.get(option)?.description}\n          </Box>\n        )}\n        value={value}\n        name=\"type\"\n        onChange={onChange}\n      />\n    </Grid>\n  );\n};\n\ntype PanelApi = {\n  save: (formData: FormData) => void;\n};\n\nconst ParameterForm = forwardRef<\n  undefined | PanelApi,\n  { variable?: DataSource }\n>(({ variable }, ref) => {\n  useImperativeHandle(ref, () => ({\n    save: (formData) => {\n      // only existing parameter variables can be renamed\n      if (variable?.scopeInstanceId === undefined) {\n        return;\n      }\n      const scopeInstanceId = variable.scopeInstanceId;\n      const name = z.string().parse(formData.get(\"name\"));\n      updateWebstudioData((data) => {\n        data.dataSources.set(variable.id, { ...variable, name });\n        rebindTreeVariablesMutable({\n          startingInstanceId: scopeInstanceId,\n          ...data,\n        });\n      });\n    },\n  }));\n  return <></>;\n});\nParameterForm.displayName = \"ParameterForm\";\n\nconst saveVariable = (variable: undefined | DataSource, formData: FormData) => {\n  const dataSourceId = variable?.id ?? nanoid();\n  // preserve existing instance scope when edit\n  const scopeInstanceId =\n    variable?.scopeInstanceId ?? $selectedInstance.get()?.id;\n  if (scopeInstanceId === undefined) {\n    return;\n  }\n  const type = z.string().parse(formData.get(\"type\"));\n  const name = z.string().parse(formData.get(\"name\"));\n  const value = z.string().nullable().parse(formData.get(\"value\"));\n  let variableValue: Extract<DataSource, { type: \"variable\" }>[\"value\"];\n  if (type === \"string\") {\n    variableValue = { type: \"string\", value: value ?? \"\" };\n  } else if (type === \"number\") {\n    variableValue = { type: \"number\", value: Number(value || 0) };\n  } else if (type === \"boolean\") {\n    variableValue = { type: \"boolean\", value: value != null };\n  } else {\n    variableValue = {\n      type: \"json\",\n      value: value ? parseJsonValue(value) : undefined,\n    };\n  }\n  updateWebstudioData((data) => {\n    // cleanup resource when value variable is set\n    if (variable?.type === \"resource\") {\n      data.resources.delete(variable.resourceId);\n    }\n    data.dataSources.set(dataSourceId, {\n      id: dataSourceId,\n      scopeInstanceId,\n      name,\n      type: \"variable\",\n      value: variableValue,\n    });\n    rebindTreeVariablesMutable({\n      startingInstanceId: scopeInstanceId,\n      ...data,\n    });\n  });\n};\n\nconst useValuePanelRef = ({\n  ref,\n  variable,\n}: {\n  ref: Ref<undefined | PanelApi>;\n  variable?: DataSource;\n}) => {\n  useImperativeHandle(ref, () => ({\n    save: (formData) => {\n      saveVariable(variable, formData);\n    },\n  }));\n};\n\nconst StringForm = forwardRef<\n  undefined | PanelApi,\n  {\n    variable?: DataSource;\n    value: unknown;\n    onChange: (value: unknown) => void;\n  }\n>(({ variable, value: unknownValue, onChange }, ref) => {\n  const value = typeof unknownValue === \"string\" ? unknownValue : \"\";\n  useValuePanelRef({ ref, variable });\n  const valueId = useId();\n  return (\n    <Flex direction=\"column\" css={{ gap: theme.spacing[3] }}>\n      <Label htmlFor={valueId}>Value</Label>\n      <EditorDialogControl>\n        <TextArea\n          name=\"value\"\n          rows={1}\n          maxRows={10}\n          autoGrow={true}\n          id={valueId}\n          value={value}\n          onChange={onChange}\n        />\n        <EditorDialog\n          title=\"Variable value\"\n          content={\n            <TextArea\n              grow={true}\n              id={valueId}\n              value={value}\n              onChange={onChange}\n            />\n          }\n        >\n          <EditorDialogButton />\n        </EditorDialog>\n      </EditorDialogControl>\n    </Flex>\n  );\n});\nStringForm.displayName = \"StringForm\";\n\nconst validateNumberValue = (value: string | number) => {\n  if (typeof value === \"string\" && value.length === 0) {\n    return \"Value expects a number\";\n  }\n  const number = Number(value);\n  return Number.isNaN(number) ? \"Invalid number\" : \"\";\n};\n\nconst NumberForm = forwardRef<\n  undefined | PanelApi,\n  {\n    variable?: DataSource;\n    value: unknown;\n    onChange: (value: unknown) => void;\n  }\n>(({ variable, value: unknownValue, onChange }, ref) => {\n  const value =\n    typeof unknownValue === \"number\" || typeof unknownValue === \"string\"\n      ? unknownValue\n      : \"\";\n  const [valueError, setValueError] = useState(\"\");\n  const valueRef = useRef<HTMLInputElement>(null);\n  useEffect(() => {\n    valueRef.current?.setCustomValidity(validateNumberValue(value));\n    setValueError(\"\");\n  }, [value]);\n  useValuePanelRef({ ref, variable });\n  const valueId = useId();\n  return (\n    <>\n      <Flex direction=\"column\" css={{ gap: theme.spacing[3] }}>\n        <Label htmlFor={valueId}>Value</Label>\n        <InputErrorsTooltip errors={valueError ? [valueError] : undefined}>\n          <InputField\n            inputRef={valueRef}\n            name=\"value\"\n            id={valueId}\n            inputMode=\"numeric\"\n            color={valueError ? \"error\" : undefined}\n            value={value}\n            onChange={(event) => onChange(event.target.value)}\n            onBlur={() => valueRef.current?.checkValidity()}\n            onInvalid={(event) =>\n              setValueError(event.currentTarget.validationMessage)\n            }\n          />\n        </InputErrorsTooltip>\n      </Flex>\n    </>\n  );\n});\nNumberForm.displayName = \"NumberForm\";\n\nconst BooleanForm = forwardRef<\n  undefined | PanelApi,\n  {\n    variable?: DataSource;\n    value: unknown;\n    onChange: (value: unknown) => void;\n  }\n>(({ variable, value: unknownValue, onChange }, ref) => {\n  const value = typeof unknownValue === \"boolean\" ? unknownValue : false;\n  useValuePanelRef({ ref, variable });\n  const valueId = useId();\n  return (\n    <>\n      <Flex direction=\"column\" css={{ gap: theme.spacing[3] }}>\n        <Label htmlFor={valueId}>Value</Label>\n        <Switch\n          name=\"value\"\n          value=\"on\"\n          id={valueId}\n          checked={value}\n          onCheckedChange={onChange}\n        />\n      </Flex>\n    </>\n  );\n});\nBooleanForm.displayName = \"BooleanForm\";\n\nconst validateJsonValue = (expression: string) => {\n  const diagnostics = lintExpression({ expression });\n  // prevent saving with any message including unset variable\n  return diagnostics.length > 0 ? \"error\" : \"\";\n};\n\nconst parseJsonValue = (expression: string) => {\n  try {\n    expression = transpileExpression({ expression, executable: true });\n    // wrap with parentheses to treat {} as object instead of block\n    return eval(`(${expression})`);\n  } catch {\n    // empty block\n  }\n};\n\nconst JsonForm = forwardRef<\n  undefined | PanelApi,\n  {\n    variable?: DataSource;\n    value: unknown;\n    onChange: (value: unknown) => void;\n  }\n>(({ variable, value: unknownValue, onChange }, ref) => {\n  const value = typeof unknownValue === \"string\" ? unknownValue : \"\";\n  const [valueError, setValueError] = useState(\"\");\n  const valueRef = useRef<HTMLInputElement>(null);\n  useEffect(() => {\n    valueRef.current?.setCustomValidity(validateJsonValue(value));\n    setValueError(\"\");\n  }, [value]);\n  useValuePanelRef({ ref, variable });\n  return (\n    <>\n      <input\n        ref={valueRef}\n        style={{ display: \"none\" }}\n        name=\"value\"\n        data-color={valueError ? \"error\" : undefined}\n        value={value}\n        onChange={() => {}}\n        onInvalid={(event) =>\n          setValueError(event.currentTarget.validationMessage)\n        }\n      />\n      <Flex direction=\"column\" css={{ gap: theme.spacing[3] }}>\n        <Label>Value</Label>\n        <ExpressionEditor\n          color={valueError ? \"error\" : undefined}\n          value={value}\n          onChange={onChange}\n          onChangeComplete={() => valueRef.current?.checkValidity()}\n        />\n      </Flex>\n    </>\n  );\n});\nJsonForm.displayName = \"JsonForm\";\n\nconst VariablePanelForm = forwardRef<\n  undefined | PanelApi,\n  {\n    variable?: DataSource;\n    variableType: VariableType;\n    onVariableTypeChange: (variableType: VariableType) => void;\n    value: unknown;\n    onValueChange: (value: unknown) => void;\n  }\n>(\n  (\n    { variable, variableType, onVariableTypeChange, value, onValueChange },\n    ref\n  ) => {\n    const { allowDynamicData } = useStore($userPlanFeatures);\n\n    const isResource =\n      variableType === \"resource\" ||\n      variableType === \"graphql-resource\" ||\n      variableType === \"system-resource\";\n    const requiresUpgrade = allowDynamicData === false && isResource;\n    return (\n      <>\n        {requiresUpgrade && (\n          <PanelBanner>\n            <Text>Resource fetching is part of the CMS functionality.</Text>\n            <Flex align=\"center\" gap={1}>\n              <UpgradeIcon />\n              <Link\n                color=\"inherit\"\n                target=\"_blank\"\n                href=\"https://webstudio.is/pricing\"\n              >\n                Upgrade to Pro\n              </Link>\n            </Flex>\n          </PanelBanner>\n        )}\n        <Flex\n          direction=\"column\"\n          css={{\n            overflow: \"hidden\",\n            padding: theme.panel.padding,\n            gap: theme.spacing[7],\n          }}\n        >\n          <NameField variable={variable} defaultValue={variable?.name ?? \"\"} />\n          {variableType !== \"parameter\" && (\n            <TypeField value={variableType} onChange={onVariableTypeChange} />\n          )}\n          {variableType === \"parameter\" && (\n            <ParameterForm ref={ref} variable={variable} />\n          )}\n          {variableType === \"string\" && (\n            <StringForm\n              ref={ref}\n              variable={variable}\n              value={value}\n              onChange={onValueChange}\n            />\n          )}\n          {variableType === \"number\" && (\n            <NumberForm\n              ref={ref}\n              variable={variable}\n              value={value}\n              onChange={onValueChange}\n            />\n          )}\n          {variableType === \"boolean\" && (\n            <BooleanForm\n              ref={ref}\n              variable={variable}\n              value={value}\n              onChange={onValueChange}\n            />\n          )}\n          {variableType === \"json\" && (\n            <JsonForm\n              ref={ref}\n              variable={variable}\n              value={value}\n              onChange={onValueChange}\n            />\n          )}\n          {variableType === \"resource\" && (\n            <ResourceForm ref={ref} variable={variable} />\n          )}\n          {variableType === \"graphql-resource\" && (\n            <GraphqlResourceForm ref={ref} variable={variable} />\n          )}\n          {variableType === \"system-resource\" && (\n            <SystemResourceForm ref={ref} variable={variable} />\n          )}\n        </Flex>\n      </>\n    );\n  }\n);\nVariablePanelForm.displayName = \"VariableForm\";\n\nconst $instanceVariableValues = computed(\n  [$selectedInstanceKeyWithRoot, $variableValuesByInstanceSelector],\n  (instanceKey, variableValuesByInstanceSelector) =>\n    variableValuesByInstanceSelector.get(instanceKey ?? \"\") ??\n    new Map<string, unknown>()\n);\n\nconst VariablePreview = ({\n  variable,\n  variableType,\n  variableValue,\n  onLoadData,\n}: {\n  variable?: DataSource;\n  variableType: VariableType;\n  variableValue: unknown;\n  onLoadData: () => void;\n}) => {\n  const isResource =\n    variableType === \"resource\" ||\n    variableType === \"graphql-resource\" ||\n    variableType === \"system-resource\";\n  const hasPendingResources = useStore($hasPendingResources);\n  const resources = useStore($resources);\n  const variableValues = useStore($instanceVariableValues);\n  const resourcesCache = useStore($resourcesCache);\n  const resourceScope = useResourceScope({ variable });\n  let computedValue: unknown;\n  if (variableType === \"string\" || variableType === \"boolean\") {\n    computedValue = variableValue;\n  } else if (variableType === \"json\") {\n    computedValue = parseJsonValue(String(variableValue));\n  } else if (variableType === \"number\") {\n    computedValue = Number(variableValue);\n    if (Number.isNaN(computedValue)) {\n      computedValue = variableValue;\n    }\n  } else if (variableType === \"parameter\") {\n    computedValue = variable ? variableValues.get(variable.id) : undefined;\n  } else {\n    // try to load current resource or saved one\n    let resourceRequest = ResourceRequest.safeParse(variableValue).data;\n    if (!resourceRequest && variable?.type === \"resource\") {\n      const resource = resources.get(variable.resourceId);\n      if (resource) {\n        resourceRequest = computeResourceRequest(\n          resource,\n          resourceScope.variableValues\n        );\n      }\n    }\n    if (resourceRequest) {\n      computedValue = resourcesCache.get(getResourceKey(resourceRequest));\n    }\n  }\n  const extensions = useMemo(() => [javascript({}), foldGutterExtension], []);\n  const editorProps = {\n    readOnly: true,\n    extensions,\n    // compute value as json lazily only when dialog is open\n    // by spliting into separate component which is invoked\n    // only when dialog content is rendered\n    value: formatValue(computedValue),\n    onChange: () => {},\n    onChangeComplete: () => {},\n  };\n  return (\n    <Grid\n      align=\"stretch\"\n      css={{\n        height: \"100%\",\n        overflow: \"hidden\",\n        boxSizing: \"content-box\",\n        position: \"relative\",\n      }}\n    >\n      <EditorContent {...editorProps} />\n      {isResource && !computedValue && (\n        <Flex\n          justify=\"center\"\n          align=\"center\"\n          css={{ position: \"absolute\", inset: 0 }}\n        >\n          <Button\n            color=\"neutral\"\n            disabled={hasPendingResources}\n            onClick={onLoadData}\n          >\n            {hasPendingResources ? \"Loading...\" : \"Load data\"}\n          </Button>\n        </Flex>\n      )}\n    </Grid>\n  );\n};\n\nconst VariablePopoverContent = ({\n  formRef,\n  variable,\n  onClose,\n}: {\n  formRef: RefObject<HTMLFormElement>;\n  variable?: DataSource;\n  onClose: () => void;\n}) => {\n  const hasPendingResources = useStore($hasPendingResources);\n  const panelRef = useRef<undefined | PanelApi>(undefined);\n  const isSystemVariable = variable?.id === SYSTEM_VARIABLE_ID;\n  const [value, setValue] = useState<unknown>(() => {\n    if (variable?.type === \"variable\") {\n      if (variable.value.type === \"json\") {\n        return formatValue(variable.value.value);\n      }\n      return variable.value.value;\n    }\n  });\n\n  const resources = useStore($resources);\n  const [variableType, setVariableType] = useState<VariableType>(() => {\n    if (variable?.type === \"resource\") {\n      const resource = resources.get(variable.resourceId);\n      if (resource?.control === \"system\") {\n        return \"system-resource\";\n      }\n      if (resource?.control === \"graphql\") {\n        return \"graphql-resource\";\n      }\n      return \"resource\";\n    }\n    if (variable?.type === \"parameter\") {\n      return variable.type;\n    }\n    if (variable?.type === \"variable\") {\n      const type = variable.value.type;\n      if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n        return type;\n      }\n      return \"json\";\n    }\n    return \"string\";\n  });\n\n  const updateVariableType = (variableType: VariableType) => {\n    setVariableType(variableType);\n    setValue((prev: unknown) => {\n      if (variableType === \"string\" && typeof prev !== \"string\") {\n        return \"\";\n      }\n      if (variableType === \"number\" && typeof prev !== \"number\") {\n        return \"\";\n      }\n      if (variableType === \"boolean\" && typeof prev !== \"boolean\") {\n        return false;\n      }\n      if (variableType === \"json\") {\n        // empty string gives an error\n        return prev || \"{}\";\n      }\n      return prev;\n    });\n  };\n\n  const resourceScope = useResourceScope({ variable });\n\n  const reloadData = () => {\n    const formData = new FormData(formRef.current ?? undefined);\n    const resource = parseResource({\n      id: variable?.id ?? \"new\",\n      formData,\n    });\n    const resourceRequest = computeResourceRequest(\n      resource,\n      resourceScope.variableValues\n    );\n    invalidateResource(resourceRequest);\n    setValue(resourceRequest);\n  };\n\n  const copyAsCurl = () => {\n    const formData = new FormData(formRef.current ?? undefined);\n    const resource = parseResource({\n      id: variable?.id ?? \"new\",\n      formData,\n    });\n    const resourceRequest = computeResourceRequest(\n      resource,\n      resourceScope.variableValues\n    );\n    navigator.clipboard.writeText(generateCurl(resourceRequest));\n  };\n\n  return (\n    <>\n      <Grid\n        css={{\n          height: \"100%\",\n          gridTemplateColumns: \"320px 1fr\",\n        }}\n      >\n        <ScrollArea\n          // flex fixes content overflowing artificial scroll area\n          css={{ display: \"flex\", flexDirection: \"column\" }}\n        >\n          <form\n            ref={formRef}\n            noValidate={true}\n            // exclude from the flow\n            style={{ display: \"contents\" }}\n            onSubmit={(event) => {\n              event.preventDefault();\n              if (isSystemVariable) {\n                return;\n              }\n              const nameElement =\n                event.currentTarget.elements.namedItem(\"name\");\n              // make sure only name is valid and allow to save everything else\n              // to avoid loosing complex configuration when closed accidentally\n              if (\n                nameElement instanceof HTMLInputElement &&\n                nameElement.checkValidity()\n              ) {\n                const formData = new FormData(event.currentTarget);\n                panelRef.current?.save(formData);\n                // close popover whenever new variable is created\n                // to prevent creating duplicated variable\n                if (variable === undefined) {\n                  onClose();\n                }\n              }\n            }}\n          >\n            {/* submit is not triggered when press enter on input without submit button */}\n            <button hidden></button>\n            <fieldset\n              style={{ display: \"contents\" }}\n              // forbid editing system variable\n              disabled={isSystemVariable}\n            >\n              <VariablePanelForm\n                ref={panelRef}\n                variable={variable}\n                variableType={variableType}\n                onVariableTypeChange={updateVariableType}\n                value={value}\n                onValueChange={setValue}\n              />\n            </fieldset>\n          </form>\n        </ScrollArea>\n        <VariablePreview\n          variable={variable}\n          variableType={variableType}\n          variableValue={value}\n          onLoadData={reloadData}\n        />\n      </Grid>\n\n      <DialogTitle\n        suffix={\n          <DialogTitleActions>\n            {(variableType === \"resource\" ||\n              variableType === \"graphql-resource\") && (\n              <Tooltip content=\"Copy resource as cURL command\" side=\"bottom\">\n                <Button\n                  aria-label=\"Copy resource as cURL command\"\n                  prefix={<CopyIcon />}\n                  color=\"ghost\"\n                  onClick={copyAsCurl}\n                />\n              </Tooltip>\n            )}\n            {(variableType === \"resource\" ||\n              variableType === \"graphql-resource\" ||\n              variableType === \"system-resource\") && (\n              <Tooltip content=\"Refresh resource data\" side=\"bottom\">\n                <Button\n                  aria-label=\"Refresh resource data\"\n                  prefix={<RefreshIcon />}\n                  color=\"ghost\"\n                  disabled={hasPendingResources}\n                  onClick={reloadData}\n                />\n              </Tooltip>\n            )}\n            <DialogMaximize />\n            <DialogClose />\n          </DialogTitleActions>\n        }\n      >\n        {variable ? \"Edit Variable\" : \"New Variable\"}\n      </DialogTitle>\n    </>\n  );\n};\n\nconst areAllFormErrorsVisible = (form: null | HTMLFormElement) => {\n  if (form === null) {\n    return true;\n  }\n  // check all errors in form fields are visible\n  for (const element of form.elements) {\n    if (\n      element instanceof HTMLInputElement ||\n      element instanceof HTMLTextAreaElement\n    ) {\n      // field is invalid and the error is not visible\n      if (\n        element.validity.valid === false &&\n        // rely on data-color=error convention in webstudio design system\n        element.getAttribute(\"data-color\") !== \"error\"\n      ) {\n        return false;\n      }\n    }\n  }\n  return true;\n};\n\nexport const VariablePopoverTrigger = ({\n  variable,\n  children,\n}: {\n  variable?: DataSource;\n  children: ReactNode;\n}) => {\n  const [isOpen, setOpen] = useState(false);\n  const formRef = useRef<HTMLFormElement>(null);\n\n  return (\n    <FloatingPanel\n      placement=\"center\"\n      width={740}\n      height={480}\n      open={isOpen}\n      onOpenChange={(newOpen) => {\n        if (newOpen) {\n          setOpen(true);\n          return;\n        }\n        // attempt to save form on close\n        if (areAllFormErrorsVisible(formRef.current)) {\n          formRef.current?.requestSubmit();\n          setOpen(false);\n        } else {\n          formRef.current?.checkValidity();\n          // prevent closing when not all errors are shown to user\n        }\n      }}\n      title={undefined}\n      content={\n        <VariablePopoverContent\n          formRef={formRef}\n          variable={variable}\n          onClose={() => setOpen(false)}\n        />\n      }\n    >\n      {children}\n    </FloatingPanel>\n  );\n};\n\nVariablePopoverTrigger.displayName = \"VariablePopoverTrigger\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/variables-section.stories.tsx",
    "content": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $pages, $instances } from \"~/shared/sync/data-stores\";\nimport { $userPlanFeatures } from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { VariablesSection as VariablesSectionComponent } from \"./variables-section\";\n\n$userPlanFeatures.set({\n  ...$userPlanFeatures.get(),\n  allowDynamicData: true,\n});\n\nexport default {\n  title: \"Variables Section\",\n  component: VariablesSectionComponent,\n} satisfies Meta;\n\nregisterContainers();\n$instances.set(\n  new Map([\n    [\"box\", { id: \"box\", type: \"instance\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(createDefaultPages({ rootInstanceId: \"box\" }));\n$awareness.set({ pageId: \"home\", instanceSelector: [\"box\"] });\n\nexport const VariablesSection: StoryObj = {\n  render: () => (\n    <StorySection title=\"Variables Section\">\n      <Box css={{ width: theme.sizes.sidebarWidth }}>\n        <VariablesSectionComponent />\n      </Box>\n    </StorySection>\n  ),\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/settings-panel/variables-section.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  css,\n  CssValueListArrowFocus,\n  CssValueListItem,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Flex,\n  Label,\n  SectionTitle,\n  SectionTitleButton,\n  SectionTitleLabel,\n  SmallIconButton,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { EllipsesIcon, PlusIcon } from \"@webstudio-is/icons\";\nimport type { DataSource } from \"@webstudio-is/sdk\";\nimport { findPageByIdOrPath } from \"@webstudio-is/sdk\";\nimport {\n  $dataSources,\n  $instances,\n  $pages,\n  $props,\n  $resources,\n  $variableValuesByInstanceSelector,\n} from \"~/shared/nano-states\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport { formatValuePreview } from \"~/builder/shared/expression-editor\";\nimport { VariablePopoverTrigger } from \"./variable-popover\";\nimport {\n  $selectedInstance,\n  $selectedInstanceKeyWithRoot,\n  $selectedPage,\n} from \"~/shared/awareness\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport {\n  deleteVariableMutable,\n  findAvailableVariables,\n  findUsedVariables,\n} from \"~/shared/data-variables\";\nimport { DeleteDataVariableDialog } from \"~/builder/shared/data-variable-utils\";\n\n/**\n * find variables defined specifically on this selected instance\n */\nconst $availableVariables = computed(\n  [$selectedInstance, $instances, $dataSources],\n  (selectedInstance, instances, dataSources) => {\n    if (selectedInstance === undefined) {\n      return [];\n    }\n    const availableVariables = findAvailableVariables({\n      startingInstanceId: selectedInstance.id,\n      instances,\n      dataSources,\n    });\n    // order local variables first\n    return Array.from(availableVariables.values()).sort((left, right) => {\n      const leftRank = left.scopeInstanceId === selectedInstance.id ? 0 : 1;\n      const rightRank = right.scopeInstanceId === selectedInstance.id ? 0 : 1;\n      return leftRank - rightRank;\n    });\n  }\n);\n\nconst $instanceVariableValues = computed(\n  [$selectedInstanceKeyWithRoot, $variableValuesByInstanceSelector],\n  (instanceKey, variableValuesByInstanceSelector) =>\n    variableValuesByInstanceSelector.get(instanceKey ?? \"\") ??\n    new Map<string, unknown>()\n);\n\nconst $usedVariables = computed(\n  [$selectedInstance, $pages, $instances, $props, $dataSources, $resources],\n  (selectedInstance, pages, instances, props, dataSources, resources) => {\n    if (selectedInstance === undefined) {\n      return new Map<DataSource[\"id\"], number>();\n    }\n    return findUsedVariables({\n      startingInstanceId: selectedInstance.id,\n      pages,\n      instances,\n      props,\n      dataSources,\n      resources,\n    });\n  }\n);\n\nconst EmptyVariables = () => {\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <Flex justify=\"center\" align=\"center\">\n        <Text variant=\"labels\" align=\"center\">\n          No data variables created\n          <br /> on this instance\n        </Text>\n      </Flex>\n      <Flex justify=\"center\" align=\"center\">\n        <VariablePopoverTrigger>\n          <Button prefix={<PlusIcon />}>Create data variable</Button>\n        </VariablePopoverTrigger>\n      </Flex>\n    </Flex>\n  );\n};\n\nconst variableLabelStyle = css({\n  whiteSpace: \"nowrap\",\n  overflow: \"hidden\",\n  textOverflow: \"ellipsis\",\n  maxWidth: \"100%\",\n});\n\nconst VariablesItem = ({\n  variable,\n  source,\n  index,\n  value,\n  usageCount,\n}: {\n  variable: DataSource;\n  source: \"local\" | \"remote\";\n  index: number;\n  value: unknown;\n  usageCount: number;\n}) => {\n  const selectedPage = useStore($selectedPage);\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n  const [variableToDelete, setVariableToDelete] = useState<{\n    id: string;\n    name: string;\n    usages: number;\n  }>();\n  return (\n    <VariablePopoverTrigger key={variable.id} variable={variable}>\n      <CssValueListItem\n        id={variable.id}\n        index={index}\n        label={\n          <Flex align=\"center\">\n            <Label tag=\"label\" color={source}>\n              {variable.name}\n            </Label>\n            {value !== undefined && (\n              <span className={variableLabelStyle.toString()}>\n                &nbsp;\n                {formatValuePreview(value)}\n              </span>\n            )}\n          </Flex>\n        }\n        data-state={isMenuOpen ? \"open\" : undefined}\n        buttons={\n          <>\n            {((source === \"local\" && variable.type !== \"parameter\") ||\n              (source === \"local\" &&\n                variable.id === selectedPage?.systemDataSourceId)) && (\n              <DropdownMenu modal onOpenChange={setIsMenuOpen}>\n                <DropdownMenuTrigger asChild>\n                  {/* a11y is completely broken here\n                      focus is not restored to button invoker\n                      @todo fix it eventually and consider restoring from closed value preview dialog\n                  */}\n                  <SmallIconButton\n                    tabIndex={-1}\n                    aria-label=\"Open variable menu\"\n                    icon={<EllipsesIcon />}\n                    onClick={() => {}}\n                  />\n                </DropdownMenuTrigger>\n                <DropdownMenuContent\n                  css={{ width: theme.spacing[28] }}\n                  onCloseAutoFocus={(event) => event.preventDefault()}\n                >\n                  {source === \"local\" && variable.type !== \"parameter\" && (\n                    <DropdownMenuItem\n                      onSelect={() => {\n                        setVariableToDelete({\n                          id: variable.id,\n                          name: variable.name,\n                          usages: usageCount,\n                        });\n                      }}\n                    >\n                      Delete {usageCount > 0 && `(${usageCount} bindings)`}\n                    </DropdownMenuItem>\n                  )}\n                  {source === \"local\" &&\n                    variable.id === selectedPage?.systemDataSourceId && (\n                      <DropdownMenuItem\n                        onSelect={() => {\n                          updateWebstudioData((data) => {\n                            const page = findPageByIdOrPath(\n                              selectedPage.id,\n                              data.pages\n                            );\n                            delete page?.systemDataSourceId;\n                            deleteVariableMutable(data, variable.id);\n                          });\n                        }}\n                      >\n                        Delete\n                      </DropdownMenuItem>\n                    )}\n                </DropdownMenuContent>\n              </DropdownMenu>\n            )}\n\n            <DeleteDataVariableDialog\n              variable={variableToDelete}\n              onClose={() => {\n                setVariableToDelete(undefined);\n              }}\n              onConfirm={(variableId) => {\n                updateWebstudioData((data) => {\n                  deleteVariableMutable(data, variableId);\n                });\n                setVariableToDelete(undefined);\n              }}\n            />\n          </>\n        }\n      />\n    </VariablePopoverTrigger>\n  );\n};\n\nconst VariablesList = () => {\n  const instance = useStore($selectedInstance);\n  const availableVariables = useStore($availableVariables);\n  const variableValues = useStore($instanceVariableValues);\n  const usedVariables = useStore($usedVariables);\n\n  if (availableVariables.length === 0) {\n    return <EmptyVariables />;\n  }\n\n  return (\n    <CssValueListArrowFocus>\n      {/* local variables should be ordered first to not block tab to first item */}\n      {availableVariables.map((variable, index) => (\n        <VariablesItem\n          key={variable.id}\n          source={\n            instance?.id === variable.scopeInstanceId ? \"local\" : \"remote\"\n          }\n          value={variableValues.get(variable.id)}\n          variable={variable}\n          index={index}\n          usageCount={usedVariables.get(variable.id) ?? 0}\n        />\n      ))}\n    </CssValueListArrowFocus>\n  );\n};\n\nconst label = \"Data variables\";\n\nexport const VariablesSection = () => {\n  const [isOpen, setIsOpen] = useOpenState(label);\n  return (\n    <CollapsibleSectionRoot\n      label={label}\n      fullWidth={true}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      trigger={\n        <SectionTitle\n          suffix={\n            <VariablePopoverTrigger>\n              <SectionTitleButton\n                prefix={<PlusIcon />}\n                // open panel when add new varable\n                onClick={() => {\n                  if (isOpen === false) {\n                    setIsOpen(true);\n                  }\n                }}\n              />\n            </VariablePopoverTrigger>\n          }\n        >\n          <SectionTitleLabel>Data variables</SectionTitleLabel>\n        </SectionTitle>\n      }\n    >\n      {/* prevent applyig gap to list items */}\n      <div>\n        <VariablesList />\n      </div>\n    </CollapsibleSectionRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/share.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  PopoverContent,\n  PopoverTitle,\n  PopoverTrigger,\n  theme,\n  Tooltip,\n  rawTheme,\n  Popover,\n} from \"@webstudio-is/design-system\";\nimport { ShareProjectContainer } from \"~/shared/share-project\";\nimport { $authPermit } from \"~/shared/nano-states\";\nimport { $isShareDialogOpen } from \"~/builder/shared/nano-states\";\n\nexport const ShareButton = ({ projectId }: { projectId: string }) => {\n  const isShareDialogOpen = useStore($isShareDialogOpen);\n  const authPermit = useStore($authPermit);\n\n  const isShareDisabled = authPermit !== \"own\";\n  const tooltipContent = isShareDisabled\n    ? \"Only owner can share projects\"\n    : undefined;\n\n  return (\n    <Popover\n      modal\n      open={isShareDialogOpen}\n      onOpenChange={(isOpen) => {\n        $isShareDialogOpen.set(isOpen);\n      }}\n    >\n      <Tooltip\n        content={tooltipContent ?? \"Share a project link\"}\n        sideOffset={Number.parseFloat(rawTheme.spacing[5])}\n      >\n        <PopoverTrigger asChild>\n          <Button disabled={isShareDisabled} color=\"gradient\">\n            Share\n          </Button>\n        </PopoverTrigger>\n      </Tooltip>\n      <PopoverContent\n        sideOffset={Number.parseFloat(rawTheme.spacing[8])}\n        css={{ marginRight: theme.spacing[3] }}\n      >\n        <ShareProjectContainer projectId={projectId} />\n        <PopoverTitle>Share</PopoverTitle>\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/color/color-control.tsx",
    "content": "import type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { keywordValues } from \"@webstudio-is/css-data\";\nimport { ColorPickerControl } from \"../../shared/color-picker\";\nimport {\n  $availableColorVariables,\n  useComputedStyleDecl,\n} from \"../../shared/model\";\nimport { deleteProperty, setProperty } from \"../../shared/use-style-data\";\n\nexport const ColorControl = ({ property }: { property: CssProperty }) => {\n  const computedStyleDecl = useComputedStyleDecl(property);\n  const value = computedStyleDecl.cascadedValue;\n  const currentColor = computedStyleDecl.usedValue;\n  const setValue = setProperty(property);\n  return (\n    <ColorPickerControl\n      property={property}\n      value={value}\n      currentColor={currentColor}\n      getOptions={() => [\n        ...(keywordValues[property] ?? []).map((item) => ({\n          type: \"keyword\" as const,\n          value: item,\n        })),\n        ...$availableColorVariables.get(),\n      ]}\n      onChange={(styleValue) => setValue(styleValue, { isEphemeral: true })}\n      onChangeComplete={setValue}\n      onAbort={() => deleteProperty(property, { isEphemeral: true })}\n      onReset={() => deleteProperty(property)}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/font-family/font-family-control.tsx",
    "content": "import { matchSorter } from \"match-sorter\";\nimport { forwardRef, useMemo, useState, type ComponentProps } from \"react\";\nimport {\n  Combobox,\n  EnhancedTooltip,\n  Flex,\n  NestedInputButton,\n  FloatingPanel,\n} from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { UploadIcon } from \"@webstudio-is/icons\";\nimport { keywordValues, parseCssValue } from \"@webstudio-is/css-data\";\nimport { FontsManager } from \"~/builder/shared/fonts-manager\";\nimport { useAssets, AssetUpload } from \"~/builder/shared/assets\";\nimport { toItems } from \"~/builder/shared/fonts-manager\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { setProperty } from \"../../shared/use-style-data\";\n\ntype Item = { value: string; label?: string };\n\nconst matchOrSuggestToCreate = (\n  search: string,\n  items: Array<Item>,\n  itemToString: (item: Item) => string\n): Array<Item> => {\n  const matched = matchSorter(items, search, {\n    keys: [itemToString],\n  });\n\n  if (\n    search.trim() !== \"\" &&\n    itemToString(matched[0]).toLocaleLowerCase() !==\n      search.toLocaleLowerCase().trim()\n  ) {\n    matched.unshift({\n      value: search.trim(),\n      label: `Custom Font: \"${search.trim()}\"`,\n    });\n  }\n  return matched;\n};\n\nexport const FontFamilyControl = () => {\n  const fontFamily = useComputedStyleDecl(\"font-family\");\n  const value = fontFamily.cascadedValue;\n  const setValue = setProperty(\"font-family\");\n  const [intermediateValue, setIntermediateValue] = useState<\n    string | undefined\n  >();\n  const { assetContainers } = useAssets(\"font\");\n  const items = useMemo(() => {\n    const fallbacks = keywordValues[\"font-family\"];\n    return [\n      ...toItems(assetContainers).map(({ label }) => ({ value: label })),\n      ...fallbacks.map((value) => ({ value })),\n    ];\n  }, [assetContainers]);\n  const [isFontManagerOpen, setIsFontMangerOpen] = useState(false);\n\n  const itemValue = useMemo(() => {\n    // Replacing the quotes just to make it look cleaner in the UI\n    return toValue(value, (value) => value).replace(/\"/g, \"\");\n  }, [value]);\n\n  return (\n    <Flex>\n      <Combobox<Item>\n        suffix={\n          <FloatingPanel\n            placement=\"left-start\"\n            title=\"Fonts\"\n            titleSuffix={<AssetUpload type=\"font\" />}\n            onOpenChange={setIsFontMangerOpen}\n            content={\n              <FontsManager\n                value={value.type === \"fontFamily\" ? value : undefined}\n                onChange={(newValue = itemValue) => {\n                  setValue({ type: \"fontFamily\", value: [newValue] });\n                }}\n              />\n            }\n          >\n            <FontsManagerButton />\n          </FloatingPanel>\n        }\n        getItems={() => items}\n        itemToString={(item) => item?.label ?? item?.value ?? \"\"}\n        onItemHighlight={(item) => {\n          if (item === null) {\n            setValue(parseCssValue(\"font-family\", itemValue), {\n              isEphemeral: true,\n            });\n            return;\n          }\n          setValue(\n            { type: \"fontFamily\", value: [item.value] },\n            { isEphemeral: true }\n          );\n        }}\n        onItemSelect={(item) => {\n          setValue(parseCssValue(\"font-family\", item.value));\n          setIntermediateValue(undefined);\n        }}\n        value={{ value: intermediateValue ?? itemValue }}\n        onChange={(value) => {\n          setIntermediateValue(value);\n        }}\n        onBlur={() => {\n          if (isFontManagerOpen) {\n            return;\n          }\n          setValue(parseCssValue(\"font-family\", itemValue));\n        }}\n        match={matchOrSuggestToCreate}\n      />\n    </Flex>\n  );\n};\n\nconst FontsManagerButton = forwardRef<\n  HTMLButtonElement,\n  ComponentProps<typeof NestedInputButton>\n>((props, ref) => {\n  return (\n    <EnhancedTooltip content=\"Open Font Manager\">\n      <NestedInputButton {...props} ref={ref} tabIndex={-1}>\n        <UploadIcon />\n      </NestedInputButton>\n    </EnhancedTooltip>\n  );\n});\nFontsManagerButton.displayName = \"FontsManagerButton\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/font-weight/font-weight-control.tsx",
    "content": "import { useEffect } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { Select, Text } from \"@webstudio-is/design-system\";\nimport {\n  type FontWeight,\n  fontWeightNames,\n  fontWeights,\n} from \"@webstudio-is/fonts\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { canvasApi } from \"~/shared/canvas-api\";\nimport { $detectedFontsWeights } from \"~/shared/nano-states\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { setProperty } from \"../../shared/use-style-data\";\n\nconst allFontWeights = Object.keys(fontWeights) as Array<FontWeight>;\n\nconst labels = new Map(\n  allFontWeights.map((weight) => [\n    weight,\n    `${weight} - ${fontWeights[weight as FontWeight].label}`,\n  ])\n);\n\nexport const FontWeightControl = () => {\n  // We need the font family to determine which font weights are available\n  const [fontWeight, fontFamily] = useComputedStyles([\n    \"font-weight\",\n    \"font-family\",\n  ]);\n  const detectedFontsWeights = useStore($detectedFontsWeights);\n  const fontFamilyCss = toValue(fontFamily.usedValue);\n  const fontWeightCss = toValue(fontWeight.cascadedValue);\n  const supportedFontWeights = detectedFontsWeights.get(fontFamilyCss) ?? [];\n\n  useEffect(() => {\n    canvasApi.detectSupportedFontWeights(fontFamilyCss);\n  }, [fontFamilyCss]);\n\n  const selectedWeight =\n    fontWeightNames.get(fontWeightCss) ?? (fontWeightCss as FontWeight);\n\n  const setValue = setProperty(\"font-weight\");\n\n  const setFontWeight = (\n    nextWeight: FontWeight,\n    options?: { isEphemeral: boolean }\n  ) => {\n    setValue({ type: \"keyword\", value: nextWeight }, options);\n  };\n\n  return (\n    <Select\n      // show empty field instead of radix placeholder\n      // like css value input does\n      placeholder=\"\"\n      options={allFontWeights}\n      getLabel={(weight: FontWeight) => {\n        return (\n          <Text\n            color={supportedFontWeights.includes(weight) ? \"main\" : \"subtle\"}\n          >\n            {labels.get(weight)}\n          </Text>\n        );\n      }}\n      // We use a weight as a value, because there are only 9 weights and they are unique.\n      value={selectedWeight}\n      onChange={setFontWeight}\n      onItemHighlight={(nextWeight) => {\n        // Remove preview when mouse leaves the item.\n        if (nextWeight === undefined) {\n          if (fontWeight !== undefined) {\n            setValue(fontWeight.cascadedValue, { isEphemeral: true });\n          }\n          return;\n        }\n        // Preview on mouse enter or focus.\n        setFontWeight(nextWeight, { isEphemeral: true });\n      }}\n      onOpenChange={(isOpen) => {\n        // Remove ephemeral changes when closing the menu.\n        if (isOpen === false && fontWeight !== undefined) {\n          setValue(fontWeight.cascadedValue, { isEphemeral: true });\n        }\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/image/image-control.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Button,\n  InputField,\n  Flex,\n  FloatingPanel,\n} from \"@webstudio-is/design-system\";\nimport type { CssProperty, InvalidValue } from \"@webstudio-is/css-engine\";\nimport { $assets } from \"~/shared/sync/data-stores\";\nimport { AssetManager } from \"~/builder/shared/asset-manager\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport {\n  getRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\nimport { formatAssetName } from \"~/builder/shared/assets/asset-utils\";\nimport { AssetUpload } from \"~/builder/shared/assets\";\n\nconst isValidURL = (value: string) => {\n  try {\n    return Boolean(new URL(value));\n  } catch {\n    return false;\n  }\n};\n\ntype IntermediateValue = {\n  type: \"intermediate\";\n  value: string;\n};\n\nexport const ImageControl = ({\n  property,\n  index,\n}: {\n  property: CssProperty;\n  index: number;\n}) => {\n  const assets = useStore($assets);\n  const styleDecl = useComputedStyleDecl(property);\n  const styleValue = getRepeatedStyleItem(styleDecl, index);\n  const [remoteImageURL, setRemoteImageURL] = useState<\n    IntermediateValue | InvalidValue | undefined\n  >(undefined);\n\n  useEffect(() => {\n    if (styleValue?.type === \"image\" && styleValue.value.type === \"url\") {\n      setRemoteImageURL({ type: \"intermediate\", value: styleValue.value.url });\n    }\n\n    if (styleValue?.type === \"image\" && styleValue.value.type === \"asset\") {\n      setRemoteImageURL(undefined);\n    }\n  }, [styleValue]);\n\n  if (styleValue === undefined) {\n    return;\n  }\n\n  const asset =\n    styleValue.type === \"image\" && styleValue.value.type === \"asset\"\n      ? assets.get(styleValue.value.value)\n      : undefined;\n\n  const handleImageURLInput = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const value = event.target.value;\n    if (isValidURL(value) === true) {\n      setRemoteImageURL({ type: \"intermediate\", value });\n    } else {\n      setRemoteImageURL({ type: \"invalid\", value });\n    }\n  };\n\n  const handleImageURLComplete = () => {\n    if (\n      remoteImageURL?.type === \"intermediate\" &&\n      isValidURL(remoteImageURL.value) === true\n    ) {\n      setRepeatedStyleItem(styleDecl, index, {\n        type: \"image\",\n        value: { type: \"url\", url: remoteImageURL.value },\n      });\n      setRemoteImageURL(undefined);\n    }\n  };\n\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <InputField\n        type=\"text\"\n        color={remoteImageURL?.type === \"invalid\" ? \"error\" : undefined}\n        placeholder=\"Enter image URL...\"\n        value={remoteImageURL?.value ?? \"\"}\n        onChange={handleImageURLInput}\n        onKeyDown={(event) => {\n          if (event.key === \"Enter\") {\n            handleImageURLComplete();\n          }\n        }}\n        onBlur={handleImageURLComplete}\n      />\n      <FloatingPanel\n        title=\"Images\"\n        titleSuffix={<AssetUpload type=\"image\" />}\n        content={\n          <AssetManager\n            accept=\"image/*\"\n            onChange={(assetId) => {\n              setRepeatedStyleItem(styleDecl, index, {\n                type: \"image\",\n                value: { type: \"asset\", value: assetId },\n              });\n            }}\n          />\n        }\n      >\n        <Button\n          color=\"neutral\"\n          css={{ maxWidth: \"100%\", justifySelf: \"right\" }}\n        >\n          {asset ? formatAssetName(asset) : \"Choose image...\"}\n        </Button>\n      </FloatingPanel>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/index.ts",
    "content": "export * from \"./text/text-control\";\nexport * from \"./color/color-control\";\nexport * from \"./select/select-control\";\nexport * from \"./menu/menu-control\";\nexport * from \"./font-family/font-family-control\";\nexport * from \"./font-weight/font-weight-control\";\nexport * from \"./image/image-control\";\nexport * from \"./position/position-control\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/menu/menu-control.tsx",
    "content": "import { useState } from \"react\";\nimport { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport type { IconComponent } from \"@webstudio-is/icons\";\nimport {\n  Box,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n  Flex,\n  IconButton,\n  MenuCheckedIcon,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport {\n  camelCaseProperty,\n  declarationDescriptions,\n} from \"@webstudio-is/css-data\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { setProperty } from \"../../shared/use-style-data\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { PropertyValueTooltip } from \"../../property-label\";\n\nexport const MenuControl = ({\n  property,\n  items,\n}: {\n  property: CssProperty;\n  items: Array<{\n    name: string;\n    label: string;\n    icon: IconComponent;\n  }>;\n}) => {\n  const computedStyleDecl = useComputedStyleDecl(property);\n  const [descriptionValue, setDescriptionValue] = useState<string>();\n\n  const setValue = setProperty(property);\n  const currentValue = toValue(computedStyleDecl.cascadedValue);\n  const currentItem = items.find((item) => item.name === currentValue);\n  const Icon = currentItem?.icon ?? items[0].icon;\n  const description =\n    declarationDescriptions[\n      `${camelCaseProperty(property)}:${\n        descriptionValue ?? currentValue\n      }` as keyof typeof declarationDescriptions\n    ];\n  // consider defined (not default) value as advanced\n  // when there is no matching item\n  const isAdvanced =\n    computedStyleDecl.source.name !== \"default\" && currentItem === undefined;\n\n  return (\n    <DropdownMenu modal={false}>\n      <PropertyValueTooltip\n        label={currentItem?.label ?? humanizeString(property)}\n        description={description}\n        properties={[property]}\n        isAdvanced={isAdvanced}\n      >\n        <DropdownMenuTrigger asChild>\n          <IconButton\n            aria-disabled={isAdvanced}\n            variant={computedStyleDecl.source.name}\n            onPointerDown={(event) => {\n              // tooltip reset property when click with altKey\n              if (event.altKey) {\n                event.preventDefault();\n              }\n            }}\n          >\n            <Icon />\n          </IconButton>\n        </DropdownMenuTrigger>\n      </PropertyValueTooltip>\n      <DropdownMenuContent sideOffset={4} collisionPadding={16} side=\"bottom\">\n        <DropdownMenuRadioGroup\n          value={currentValue}\n          onValueChange={(value) => setValue({ type: \"keyword\", value })}\n        >\n          {items.map(({ name, label, icon: Icon }) => {\n            return (\n              <DropdownMenuRadioItem\n                text=\"sentence\"\n                key={name}\n                value={name}\n                icon={<MenuCheckedIcon />}\n                onFocus={() => {\n                  setValue(\n                    { type: \"keyword\", value: name },\n                    { isEphemeral: true }\n                  );\n                  setDescriptionValue(name);\n                }}\n                onBlur={() => {\n                  setValue(\n                    { type: \"keyword\", value: currentValue },\n                    { isEphemeral: true }\n                  );\n                  setDescriptionValue(undefined);\n                }}\n              >\n                <Flex gap=\"1\">\n                  <Flex\n                    css={{\n                      width: theme.spacing[9],\n                      height: theme.spacing[9],\n                    }}\n                    align=\"center\"\n                    justify=\"center\"\n                  >\n                    <Icon />\n                  </Flex>\n                  {label}\n                </Flex>\n              </DropdownMenuRadioItem>\n            );\n          })}\n        </DropdownMenuRadioGroup>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem hint>\n          <Box css={{ width: theme.spacing[26] }}>\n            <Box css={{ fontFamily: theme.fonts.mono, mb: theme.spacing[5] }}>\n              {property}: {descriptionValue ?? currentValue}\n            </Box>\n            {description}\n          </Box>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/position/position-control.tsx",
    "content": "import {\n  camelCaseProperty,\n  keywordValues,\n  propertyDescriptions,\n} from \"@webstudio-is/css-data\";\nimport {\n  TupleValue,\n  TupleValueItem,\n  type StyleValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport { Flex, Grid, PositionGrid } from \"@webstudio-is/design-system\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport {\n  deleteProperty,\n  setProperty,\n  type SetValue,\n} from \"../../shared/use-style-data\";\nimport { PropertyInlineLabel } from \"../../property-label\";\n\nconst toPosition = (value: TupleValue) => {\n  // Should never actually happen, just for TS\n  if (value.value[0].type !== \"unit\" || value.value[1].type !== \"unit\") {\n    return { x: 0, y: 0 };\n  }\n\n  return {\n    x: value.value[0].value,\n    y: value.value[1].value,\n  };\n};\n\n// @todo SetValue has legacy string value support, that needs to be removed\nconst toTuple = (\n  valueX?: StyleValue | string,\n  valueY?: StyleValue | string\n) => {\n  const parsedValue = TupleValue.safeParse(valueX);\n  if (parsedValue.success) {\n    return parsedValue.data;\n  }\n\n  const parsedValueX = valueX\n    ? TupleValueItem.parse(valueX)\n    : ({ type: \"unit\", value: 0, unit: \"px\" } as const);\n  const parsedValueY = valueY ? TupleValueItem.parse(valueY) : parsedValueX;\n\n  return {\n    type: \"tuple\" as const,\n    value: [parsedValueX, parsedValueY],\n  };\n};\n\nexport const PositionControl = ({\n  property,\n  styleDecl,\n}: {\n  property: CssProperty;\n  styleDecl: ComputedStyleDecl;\n}) => {\n  const value = toTuple(styleDecl.cascadedValue);\n  const keywords = (keywordValues[property] ?? []).map((value) => ({\n    type: \"keyword\" as const,\n    value,\n  }));\n\n  const setValue = setProperty(property);\n\n  const setValueX: SetValue = (valueX, options) => {\n    const nextValue = toTuple(valueX, value.value[1]);\n    setValue(nextValue, options);\n  };\n\n  const setValueY: SetValue = (valueY, options) => {\n    const nextValue = toTuple(value.value[0], valueY);\n    setValue(nextValue, options);\n  };\n\n  return (\n    <Flex direction=\"column\" gap=\"1\">\n      <PropertyInlineLabel\n        label=\"Position\"\n        description={propertyDescriptions[camelCaseProperty(property)]}\n        properties={[property]}\n      />\n      <Flex gap=\"6\">\n        <PositionGrid\n          selectedPosition={toPosition(value)}\n          onSelect={({ x, y }) => {\n            setValue({\n              type: \"tuple\",\n              value: [\n                { type: \"unit\", value: x, unit: \"%\" },\n                { type: \"unit\", value: y, unit: \"%\" },\n              ],\n            });\n          }}\n        />\n        <Grid\n          css={{ gridTemplateColumns: \"max-content 1fr\" }}\n          align=\"center\"\n          gapX=\"2\"\n        >\n          <PropertyInlineLabel\n            label=\"Left\"\n            description=\"Left position offset\"\n            properties={[property]}\n          />\n          <CssValueInputContainer\n            property={property}\n            styleSource={styleDecl.source.name}\n            getOptions={() => keywords}\n            value={value.value[0]}\n            onUpdate={setValueX}\n            onDelete={(options) => deleteProperty(property, options)}\n          />\n          <PropertyInlineLabel\n            label=\"Top\"\n            description=\"Top position offset\"\n            properties={[property]}\n          />\n          <CssValueInputContainer\n            property={property}\n            styleSource={styleDecl.source.name}\n            getOptions={() => keywords}\n            value={value.value[1]}\n            onUpdate={setValueY}\n            onDelete={(options) => deleteProperty(property, options)}\n          />\n        </Grid>\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/select/select-control.tsx",
    "content": "import {\n  camelCaseProperty,\n  declarationDescriptions,\n  keywordValues,\n  parseCssValue,\n} from \"@webstudio-is/css-data\";\nimport {\n  toValue,\n  type StyleValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport { Box, Select, theme } from \"@webstudio-is/design-system\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport {\n  resetEphemeralStyles,\n  setProperty,\n  type StyleUpdateOptions,\n} from \"../../shared/use-style-data\";\nimport {\n  getRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\n\nexport const SelectControl = ({\n  property,\n  index,\n  items,\n}: {\n  property: CssProperty;\n  index?: number;\n  items?: Array<{ label: string; name: string }>;\n}) => {\n  const styleDecl = useComputedStyleDecl(property);\n  const value =\n    index === undefined\n      ? styleDecl.cascadedValue\n      : getRepeatedStyleItem(styleDecl, index);\n  const setValue = (value: StyleValue, options?: StyleUpdateOptions) => {\n    if (index === undefined) {\n      setProperty(property)(value, options);\n    } else {\n      setRepeatedStyleItem(styleDecl, index, value, options);\n    }\n  };\n  const options =\n    items?.map(({ name }) => name) ?? keywordValues[property] ?? [];\n\n  // We can't render an empty string as a value when display was added but without a value.\n  // One case is when advanced property is being added, but no value is set.\n  const valueString = toValue(value) || \"empty\";\n\n  // Append selected value when not present in the list of options\n  // because radix requires values to always be in the list.\n  if (options.includes(valueString) === false) {\n    options.push(valueString);\n  }\n\n  const hasDescription =\n    options.length > 0 &&\n    options.some(\n      (option) =>\n        declarationDescriptions[`${camelCaseProperty(property)}:${option}`] !==\n        undefined\n    );\n\n  return (\n    <Select\n      // Show empty field instead of radix placeholder like css value input does.\n      placeholder=\"\"\n      options={options}\n      value={valueString}\n      onChange={(name) => setValue({ type: \"keyword\", value: name })}\n      onItemHighlight={(name) => {\n        // Remove preview when mouse leaves the item.\n        if (name === undefined) {\n          if (value) {\n            setValue(value, { isEphemeral: true });\n          }\n          return;\n        }\n        // Preview on mouse enter or focus.\n        const nextValue = parseCssValue(property, name);\n        setValue(nextValue, { isEphemeral: true });\n      }}\n      onOpenChange={(isOpen) => {\n        // Remove ephemeral changes when closing the menu.\n        if (isOpen === false) {\n          resetEphemeralStyles();\n        }\n      }}\n      getDescription={(option) => {\n        if (hasDescription === false) {\n          return;\n        }\n\n        const description =\n          declarationDescriptions[`${camelCaseProperty(property)}:${option}`];\n        return (\n          <Box css={{ width: theme.spacing[26] }}>\n            {description ?? `The ${property} is ${option}`}\n          </Box>\n        );\n      }}\n      getItemProps={() => ({ text: \"sentence\" })}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/text/text-control.tsx",
    "content": "import { useState } from \"react\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { keywordValues } from \"@webstudio-is/css-data\";\nimport {\n  CssValueInput,\n  type IntermediateStyleValue,\n} from \"../../shared/css-value-input\";\nimport { deleteProperty, setProperty } from \"../../shared/use-style-data\";\nimport {\n  $availableUnitVariables,\n  useComputedStyleDecl,\n} from \"../../shared/model\";\n\nexport const TextControl = ({ property }: { property: CssProperty }) => {\n  const computedStyleDecl = useComputedStyleDecl(property);\n  const value = computedStyleDecl.cascadedValue;\n  const setValue = setProperty(property);\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n  return (\n    <CssValueInput\n      styleSource={computedStyleDecl.source.name}\n      property={property}\n      value={value}\n      intermediateValue={intermediateValue}\n      getOptions={() => [\n        ...(keywordValues[property] ?? []).map((value) => ({\n          type: \"keyword\" as const,\n          value,\n        })),\n        ...$availableUnitVariables.get(),\n      ]}\n      onChange={(styleValue) => {\n        setIntermediateValue(styleValue);\n        if (styleValue === undefined) {\n          deleteProperty(property, { isEphemeral: true });\n          return;\n        }\n        if (styleValue.type !== \"intermediate\") {\n          setValue(styleValue, { isEphemeral: true });\n        }\n      }}\n      onHighlight={(styleValue) => {\n        if (styleValue !== undefined) {\n          setValue(styleValue, { isEphemeral: true });\n        } else {\n          deleteProperty(property, { isEphemeral: true });\n        }\n      }}\n      onChangeComplete={({ value }) => {\n        setIntermediateValue(undefined);\n        setValue(value);\n      }}\n      onAbort={() => {\n        deleteProperty(property, { isEphemeral: true });\n      }}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        deleteProperty(property);\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/toggle/toggle-control.tsx",
    "content": "import { ToggleButton } from \"@webstudio-is/design-system\";\nimport {\n  camelCaseProperty,\n  declarationDescriptions,\n} from \"@webstudio-is/css-data\";\nimport { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport type { IconComponent } from \"@webstudio-is/icons\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { setProperty } from \"../../shared/use-style-data\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { PropertyValueTooltip } from \"../../property-label\";\n\nexport const ToggleControl = ({\n  property,\n  items,\n}: {\n  property: CssProperty;\n  items: Array<{\n    name: string;\n    label: string;\n    icon: IconComponent;\n  }>;\n}) => {\n  const computedStyleDecl = useComputedStyleDecl(property);\n  const currentValue = toValue(computedStyleDecl.cascadedValue);\n  const currentItem = items.find((item) => item.name === currentValue);\n  const setValue = setProperty(property);\n\n  // First item is the pressed state\n  const isPressed = items[0].name === currentValue ? true : false;\n  const Icon = currentItem?.icon ?? items[0].icon;\n  // consider defined (not default) value as advanced\n  // when there is no matching item\n  const isAdvanced =\n    computedStyleDecl.source.name !== \"default\" && currentItem === undefined;\n  const description =\n    declarationDescriptions[`${camelCaseProperty(property)}:${currentValue}`];\n\n  return (\n    <PropertyValueTooltip\n      label={currentItem?.label ?? humanizeString(property)}\n      description={description}\n      properties={[property]}\n      isAdvanced={isAdvanced}\n    >\n      <ToggleButton\n        aria-disabled={isAdvanced}\n        variant={computedStyleDecl.source.name}\n        pressed={isPressed}\n        onPressedChange={(isPressed) => {\n          setValue({\n            type: \"keyword\",\n            value: isPressed ? items[0].name : items[1].name,\n          });\n        }}\n        onPointerDown={(event) => {\n          // tooltip reset property when click with altKey\n          if (event.altKey) {\n            event.preventDefault();\n          }\n        }}\n      >\n        <Icon />\n      </ToggleButton>\n    </PropertyValueTooltip>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/controls/toggle-group/toggle-group-control.tsx",
    "content": "import { useState, type JSX, type ReactNode } from \"react\";\nimport {\n  camelCaseProperty,\n  declarationDescriptions,\n} from \"@webstudio-is/css-data\";\nimport { AlertIcon } from \"@webstudio-is/icons\";\nimport {\n  Flex,\n  rawTheme,\n  ToggleGroup,\n  ToggleGroupButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\nimport {\n  getPriorityStyleValueSource,\n  PropertyInfo,\n} from \"../../property-label\";\n\nexport const ToggleGroupTooltip = ({\n  isOpen,\n  onOpenChange,\n  isSelected,\n  label,\n  code,\n  description,\n  properties,\n  isAdvanced,\n  children,\n}: {\n  isOpen: boolean;\n  onOpenChange: (isOpen: boolean) => void;\n  isSelected: boolean;\n  label?: string;\n  code?: string;\n  description: string | undefined;\n  properties: CssProperty[];\n  isAdvanced?: boolean;\n  children: ReactNode;\n}) => {\n  const styles = useComputedStyles(properties);\n  const resetProperty = () => {\n    const batch = createBatchUpdate();\n    for (const property of properties) {\n      batch.deleteProperty(property);\n    }\n    batch.publish();\n  };\n  return (\n    <Tooltip\n      open={isOpen}\n      onOpenChange={onOpenChange}\n      // prevent closing tooltip on content click\n      onPointerDown={(event) => event.preventDefault()}\n      triggerProps={{\n        onClick: (event) => {\n          if (event.altKey) {\n            event.preventDefault();\n            resetProperty();\n            return;\n          }\n        },\n      }}\n      content={\n        <PropertyInfo\n          title={label ?? humanizeString(properties[0])}\n          code={code}\n          description={\n            <Flex gap=\"2\" direction=\"column\">\n              {description}\n              {isAdvanced && (\n                <Flex gap=\"1\">\n                  <AlertIcon color={rawTheme.colors.backgroundAlertMain} /> This\n                  value was defined in the Advanced section.\n                </Flex>\n              )}\n            </Flex>\n          }\n          styles={isSelected ? styles : []}\n          onReset={() => {\n            resetProperty();\n            onOpenChange(false);\n          }}\n        />\n      }\n    >\n      {children}\n    </Tooltip>\n  );\n};\n\nexport const ToggleGroupControl = ({\n  label,\n  properties,\n  items,\n}: {\n  label?: string;\n  properties: [CssProperty, ...CssProperty[]];\n  items: Array<{\n    child: JSX.Element;\n    value: string;\n    description?: string;\n  }>;\n}) => {\n  const styles = useComputedStyles(properties);\n  const styleValueSource = getPriorityStyleValueSource(styles);\n  const selectedValue = toValue(styles[0].cascadedValue);\n  const values = styles.map((styleDecl) => toValue(styleDecl.cascadedValue));\n  const isAdvanced =\n    // show value as advanced when value is not represent with buttons\n    items.some((item) => values.includes(item.value)) === false ||\n    // show value as advanced when longhands use inconsistent values\n    new Set(values).size > 1;\n  // Issue: The tooltip's grace area is too big and overlaps with nearby buttons,\n  // preventing the tooltip from changing when the buttons are hovered over in certain cases.\n  // To solve issue and allow tooltips to change on button hover,\n  // we close the button tooltip in the ToggleGroupButton.onMouseEnter handler.\n  // onMouseEnter used to preserve default hovering behavior on tooltip.\n  const [activeTooltip, setActiveTooltip] = useState<undefined | string>();\n  return (\n    <ToggleGroup\n      color={styleValueSource}\n      type=\"single\"\n      // trigger value change when value is advanced\n      // and need to change all at once\n      value={isAdvanced ? \"\" : selectedValue}\n      onValueChange={(value) => {\n        const batch = createBatchUpdate();\n        for (const property of properties) {\n          batch.setProperty(property)({ type: \"keyword\", value });\n        }\n        batch.publish();\n      }}\n    >\n      {items.map((item) => (\n        <ToggleGroupTooltip\n          key={item.value}\n          isOpen={item.value === activeTooltip}\n          onOpenChange={(isOpen) =>\n            setActiveTooltip(isOpen ? item.value : undefined)\n          }\n          isSelected={item.value === selectedValue}\n          isAdvanced={isAdvanced}\n          label={label}\n          code={properties\n            .map((property) => `${property}: ${item.value};`)\n            .join(\"\\n\")}\n          description={\n            item.description ??\n            declarationDescriptions[\n              `${camelCaseProperty(properties[0])}:${item.value}`\n            ]\n          }\n          properties={properties}\n        >\n          <ToggleGroupButton\n            aria-disabled={isAdvanced}\n            value={item.value}\n            onMouseEnter={() =>\n              // reset only when highlighted is not active\n              setActiveTooltip((prevValue) =>\n                prevValue === item.value ? prevValue : undefined\n              )\n            }\n          >\n            {item.child}\n          </ToggleGroupButton>\n        </ToggleGroupTooltip>\n      ))}\n    </ToggleGroup>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/index.ts",
    "content": "export * from \"./style-panel\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/property-label.tsx",
    "content": "import { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { useState, type ReactNode } from \"react\";\nimport { AlertIcon, ResetIcon } from \"@webstudio-is/icons\";\nimport { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport { propertiesData } from \"@webstudio-is/css-data\";\nimport {\n  Button,\n  Flex,\n  Kbd,\n  Label,\n  rawTheme,\n  SectionTitleLabel,\n  Text,\n  theme,\n  Tooltip,\n  IconLink,\n} from \"@webstudio-is/design-system\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport {\n  $breakpoints,\n  $instances,\n  $registeredComponentMetas,\n  $styleSources,\n} from \"~/shared/nano-states\";\nimport type {\n  ComputedStyleDecl,\n  StyleValueSourceColor,\n} from \"~/shared/style-object-model\";\nimport { useComputedStyles } from \"./shared/model\";\nimport { StyleSourceBadge } from \"./style-source\";\nimport { createBatchUpdate } from \"./shared/use-style-data\";\nimport { $virtualInstances } from \"~/shared/awareness\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\n\nconst $isAltPressed = atom(false);\nif (typeof window !== \"undefined\") {\n  addEventListener(\"keydown\", (event) => {\n    if (event.key === \"Alt\") {\n      $isAltPressed.set(true);\n    }\n  });\n  addEventListener(\"keyup\", (event) => {\n    if (event.key === \"Alt\") {\n      $isAltPressed.set(false);\n    }\n  });\n}\n\nconst renderCss = (styles: ComputedStyleDecl[], isComputed: boolean) => {\n  let css = \"\";\n  for (const styleDecl of styles) {\n    let value;\n    if (isComputed) {\n      value = toValue(styleDecl.usedValue);\n    } else {\n      value = toValue(styleDecl.cascadedValue);\n    }\n    css += `${styleDecl.property}: ${value};\\n`;\n  }\n  return css;\n};\n\nexport const PropertyInfo = ({\n  title,\n  code,\n  link,\n  description,\n  styles,\n  onReset,\n  resetType = \"reset\",\n}: {\n  title: string;\n  code?: string;\n  link?: string;\n  description: ReactNode;\n  styles: ComputedStyleDecl[];\n  onReset: () => void;\n  resetType?: \"reset\" | \"delete\";\n}) => {\n  const breakpoints = useStore($breakpoints);\n  const instances = useStore($instances);\n  const virtualInstances = useStore($virtualInstances);\n  const styleSources = useStore($styleSources);\n  const metas = useStore($registeredComponentMetas);\n  const isAltPressed = useStore($isAltPressed);\n\n  let resettable = false;\n  const breakpointSet = new Set<string>();\n  const styleSourceNameSet = new Set<string>();\n  const instanceSet = new Set<string>();\n\n  for (const { source } of styles) {\n    if (source.name === \"local\" || source.name === \"overwritten\") {\n      resettable = true;\n    }\n    const instance = source.instanceId\n      ? (instances.get(source.instanceId) ??\n        virtualInstances.get(source.instanceId))\n      : undefined;\n    if (instance === undefined) {\n      continue;\n    }\n    const meta = metas.get(instance.component);\n    const styleSource = source.styleSourceId\n      ? styleSources.get(source.styleSourceId)\n      : undefined;\n    const breakpoint = source.breakpointId\n      ? breakpoints.get(source.breakpointId)\n      : undefined;\n    if (styleSource) {\n      const styleSourceName =\n        styleSource.type === \"token\" ? styleSource.name : \"Local\";\n      if (source.state) {\n        const stateLabel =\n          meta?.states?.find((item) => item.selector === source.state)?.label ??\n          humanizeString(source.state);\n        styleSourceNameSet.add(`${styleSourceName} (${stateLabel})`);\n      } else {\n        styleSourceNameSet.add(styleSourceName);\n      }\n    }\n    if (breakpoint) {\n      breakpointSet.add(\n        breakpoint?.minWidth?.toString() ??\n          breakpoint?.maxWidth?.toString() ??\n          \"Base\"\n      );\n    }\n    if (instance && meta) {\n      instanceSet.add(getInstanceLabel(instance));\n    }\n  }\n\n  return (\n    <Flex direction=\"column\" gap=\"2\" css={{ maxWidth: theme.spacing[28] }}>\n      <Flex justify=\"between\">\n        <Text variant=\"titles\" truncate>\n          {title}\n        </Text>\n        {link && (\n          <IconLink\n            href={link}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            color=\"inherit\"\n            variant=\"inherit\"\n            size={13}\n          />\n        )}\n      </Flex>\n      <Text\n        variant=\"monoBold\"\n        color=\"moreSubtle\"\n        userSelect=\"text\"\n        css={{ whiteSpace: \"break-spaces\", cursor: \"text\" }}\n      >\n        {code ?? renderCss(styles, isAltPressed)}\n      </Text>\n      <Text>{description}</Text>\n      {(styleSourceNameSet.size > 0 || instanceSet.size > 0) && (\n        <Flex\n          direction=\"column\"\n          gap=\"1\"\n          css={{ paddingBottom: theme.spacing[5] }}\n        >\n          <Text color=\"moreSubtle\">Value comes from</Text>\n          <Flex gap=\"1\" wrap=\"wrap\">\n            {Array.from(breakpointSet).map((label) => (\n              <StyleSourceBadge key={label} source=\"breakpoint\" variant=\"small\">\n                {label}\n              </StyleSourceBadge>\n            ))}\n            {Array.from(styleSourceNameSet).map((label) => (\n              <StyleSourceBadge\n                key={label}\n                source={label === \"Local\" ? \"local\" : \"token\"}\n                variant=\"small\"\n              >\n                {label}\n              </StyleSourceBadge>\n            ))}\n            {Array.from(instanceSet).map((label) => (\n              <StyleSourceBadge key={label} source=\"instance\" variant=\"small\">\n                {label}\n              </StyleSourceBadge>\n            ))}\n          </Flex>\n        </Flex>\n      )}\n      {resettable && (\n        <Button\n          color=\"dark\"\n          prefix={\n            <Flex justify=\"end\">\n              <ResetIcon />\n            </Flex>\n          }\n          suffix={<Kbd value={[\"alt\", \"click\"]} color=\"moreSubtle\" />}\n          css={{ gridTemplateColumns: \"1fr max-content 1fr\" }}\n          onClick={onReset}\n        >\n          {resetType === \"delete\" ? \"Delete property\" : \"Reset value\"}\n        </Button>\n      )}\n    </Flex>\n  );\n};\n\nexport const getPriorityStyleValueSource = (\n  styles: ComputedStyleDecl[]\n): StyleValueSourceColor => {\n  const customOrder: StyleValueSourceColor[] = [\n    \"overwritten\",\n    \"local\",\n    \"remote\",\n    \"preset\",\n    \"default\",\n  ];\n  const styleSources = styles.map((styleDecl) => styleDecl.source.name);\n  for (const color of customOrder) {\n    if (styleSources.includes(color)) {\n      return color;\n    }\n  }\n  return \"default\";\n};\n\nexport const PropertyLabel = ({\n  label,\n  description,\n  properties,\n  disabled,\n}: {\n  label: string;\n  description?: string;\n  properties: [CssProperty, ...CssProperty[]];\n  disabled?: boolean;\n}) => {\n  const styles = useComputedStyles(properties);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  const [isOpen, setIsOpen] = useState(false);\n  const resetProperty = () => {\n    const batch = createBatchUpdate();\n    for (const property of properties) {\n      batch.deleteProperty(property);\n    }\n    batch.publish();\n  };\n\n  return (\n    <Flex align=\"center\">\n      <Tooltip\n        open={isOpen}\n        onOpenChange={setIsOpen}\n        triggerProps={{\n          onClick: (event) => {\n            if (event.altKey) {\n              event.preventDefault();\n              // If not, when mixed with ToogleGroupControl.\n              // The will trigger the reset of the toggle group.\n              // And resets all of the properties in the toggle group.\n              event.stopPropagation();\n              resetProperty();\n              return;\n            }\n            setIsOpen(true);\n          },\n        }}\n        content={\n          <PropertyInfo\n            title={label}\n            description={description}\n            styles={styles}\n            onReset={() => {\n              resetProperty();\n              setIsOpen(false);\n            }}\n            link={propertiesData[properties[0]]?.mdnUrl}\n          />\n        }\n      >\n        <Flex shrink gap={1} align=\"center\">\n          <Label color={styleValueSourceColor} truncate disabled={disabled}>\n            {label}\n          </Label>\n        </Flex>\n      </Tooltip>\n    </Flex>\n  );\n};\n\nexport const PropertySectionLabel = ({\n  label,\n  description,\n  properties,\n}: {\n  label: string;\n  description: string | undefined;\n  properties: [CssProperty, ...CssProperty[]];\n}) => {\n  const styles = useComputedStyles(properties);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  const [isOpen, setIsOpen] = useState(false);\n  const resetProperty = () => {\n    const batch = createBatchUpdate();\n    for (const property of properties) {\n      batch.deleteProperty(property);\n    }\n    batch.publish();\n  };\n\n  return (\n    <Flex align=\"center\">\n      <Tooltip\n        open={isOpen}\n        onOpenChange={setIsOpen}\n        // prevent closing tooltip on content click\n        onPointerDown={(event) => event.preventDefault()}\n        triggerProps={{\n          onClick: (event) => {\n            if (event.altKey) {\n              event.preventDefault();\n              resetProperty();\n              return;\n            }\n            setIsOpen(true);\n          },\n        }}\n        content={\n          <PropertyInfo\n            title={label}\n            description={description}\n            styles={styles}\n            onReset={() => {\n              resetProperty();\n              setIsOpen(false);\n            }}\n            link={propertiesData[properties[0]]?.mdnUrl}\n          />\n        }\n      >\n        <Flex shrink gap={1} align=\"center\">\n          <SectionTitleLabel color={styleValueSourceColor}>\n            {label}\n          </SectionTitleLabel>\n        </Flex>\n      </Tooltip>\n    </Flex>\n  );\n};\n\n/**\n * Some properties like layered background-image, background-size are non resetable.\n * UI of background would be unreadable, imagine you have\n * background-size inherited from one source, background-image from the other,\n * Every property have different amount of layers. The final result on the screen would be a mess.\n */\nexport const PropertyInlineLabel = ({\n  label,\n  title,\n  description,\n  properties,\n  disabled,\n}: {\n  label: string;\n  title?: string;\n  description?: string;\n  properties?: [CssProperty, ...CssProperty[]];\n  disabled?: boolean;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  return (\n    <Flex align=\"center\">\n      <Tooltip\n        open={isOpen}\n        onOpenChange={setIsOpen}\n        // prevent closing tooltip on content click\n        onPointerDown={(event) => event.preventDefault()}\n        triggerProps={{\n          onClick: () => setIsOpen(true),\n        }}\n        content={\n          <>\n            <Flex\n              direction=\"column\"\n              gap=\"2\"\n              css={{ maxWidth: theme.spacing[28] }}\n            >\n              <Text variant=\"titles\">{title ?? label}</Text>\n              {properties && (\n                <Text\n                  variant=\"monoBold\"\n                  color=\"moreSubtle\"\n                  userSelect=\"text\"\n                  css={{ whiteSpace: \"break-spaces\", cursor: \"text\" }}\n                >\n                  {properties.join(\"\\n\")}\n                </Text>\n              )}\n              <Text>{description}</Text>\n            </Flex>\n          </>\n        }\n      >\n        <Flex shrink gap={1} align=\"center\">\n          <Label color=\"default\" disabled={disabled} truncate>\n            {label}\n          </Label>\n        </Flex>\n      </Tooltip>\n    </Flex>\n  );\n};\n\nexport const PropertyValueTooltip = ({\n  label,\n  description,\n  properties,\n  isAdvanced,\n  children,\n}: {\n  label: string;\n  description: string | undefined;\n  properties: [CssProperty, ...CssProperty[]];\n  isAdvanced?: boolean;\n  children: ReactNode;\n}) => {\n  const styles = useComputedStyles(properties);\n  const [isOpen, setIsOpen] = useState(false);\n  const resetProperty = () => {\n    const batch = createBatchUpdate();\n    for (const property of properties) {\n      batch.deleteProperty(property);\n    }\n    batch.publish();\n  };\n\n  return (\n    <Tooltip\n      open={isOpen}\n      onOpenChange={setIsOpen}\n      // prevent closing tooltip on content click\n      onPointerDown={(event) => event.preventDefault()}\n      triggerProps={{\n        onClick: (event) => {\n          if (event.altKey) {\n            event.preventDefault();\n            resetProperty();\n            return;\n          }\n        },\n      }}\n      content={\n        <PropertyInfo\n          title={label}\n          description={\n            <Flex gap=\"2\" direction=\"column\">\n              {description}\n              {isAdvanced && (\n                <Flex gap=\"1\">\n                  <AlertIcon color={rawTheme.colors.backgroundAlertMain} /> This\n                  value was defined in the Advanced section.\n                </Flex>\n              )}\n            </Flex>\n          }\n          styles={styles}\n          onReset={() => {\n            resetProperty();\n            setIsOpen(false);\n          }}\n          link={propertiesData[properties[0]]?.mdnUrl}\n        />\n      }\n    >\n      {children}\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx",
    "content": "import { useState, type ReactNode } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { PlusIcon } from \"@webstudio-is/icons\";\nimport {\n  SectionTitle,\n  SectionTitleButton,\n  SectionTitleLabel,\n} from \"@webstudio-is/design-system\";\nimport { type CssProperty, type CssStyleMap } from \"@webstudio-is/css-engine\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport {\n  createBatchUpdate,\n  deleteProperty,\n  setProperty,\n  type DeleteProperty,\n} from \"../../shared/use-style-data\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { getDots } from \"../../shared/style-section\";\nimport { CssEditor } from \"../../../../shared/css-editor\";\nimport { $advancedStyleDeclarations } from \"./stores\";\nimport { $selectedInstanceKey } from \"~/shared/awareness\";\n\n// Only here to keep the same section module interface\nexport const properties = [];\n\nconst AdvancedStyleSection = (props: {\n  label: string;\n  properties: Array<CssProperty>;\n  onAdd: () => void;\n  children: ReactNode;\n}) => {\n  const { label, children, properties, onAdd } = props;\n  const [isOpen, setIsOpen] = useOpenState(label);\n  const styles = useComputedStyles(properties);\n  return (\n    <CollapsibleSectionRoot\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      fullWidth\n      trigger={\n        <SectionTitle\n          dots={getDots(styles)}\n          suffix={\n            <SectionTitleButton\n              prefix={<PlusIcon />}\n              onClick={() => {\n                setIsOpen(true);\n                onAdd();\n              }}\n            />\n          }\n        >\n          <SectionTitleLabel>{label}</SectionTitleLabel>\n        </SectionTitle>\n      }\n    >\n      {children}\n    </CollapsibleSectionRoot>\n  );\n};\n\nexport const Section = () => {\n  const advancedStyleDeclarations = useStore($advancedStyleDeclarations);\n  const properties = advancedStyleDeclarations.map(\n    (styleDecl) => styleDecl.property\n  );\n  const selectedInstanceKey = useStore($selectedInstanceKey);\n  // Memorizing recent properties by instance id, so that when user switches between instances and comes back\n  // they are still in-place\n  const [recentPropertiesMap, setRecentPropertiesMap] = useState<\n    Map<string, Array<CssProperty>>\n  >(new Map());\n  const [showAddStyleInput, setShowAddStyleInput] = useState<boolean>(false);\n\n  const recentProperties = selectedInstanceKey\n    ? (recentPropertiesMap.get(selectedInstanceKey) ?? [])\n    : [];\n\n  const updateRecentProperties = (properties: Array<CssProperty>) => {\n    if (selectedInstanceKey === undefined) {\n      return;\n    }\n    const newRecentPropertiesMap = new Map(recentPropertiesMap);\n    newRecentPropertiesMap.set(\n      selectedInstanceKey,\n      Array.from(new Set(properties))\n    );\n    setRecentPropertiesMap(newRecentPropertiesMap);\n  };\n\n  const handleAddDeclarations = (styleMap: CssStyleMap) => {\n    const batch = createBatchUpdate();\n    for (const [property, value] of styleMap) {\n      batch.setProperty(property)(value);\n    }\n    batch.publish({ listed: true });\n\n    const insertedProperties = Array.from(styleMap.keys());\n    updateRecentProperties([...recentProperties, ...insertedProperties]);\n  };\n\n  const handleDeleteProperty: DeleteProperty = (property, options = {}) => {\n    deleteProperty(property, options);\n\n    if (options.isEphemeral === true) {\n      return;\n    }\n    updateRecentProperties(\n      recentProperties.filter((recentProperty) => recentProperty !== property)\n    );\n  };\n\n  const handleDeleteAllDeclarations = (styleMap: CssStyleMap) => {\n    const batch = createBatchUpdate();\n    for (const [property] of styleMap) {\n      batch.deleteProperty(property);\n    }\n    batch.publish();\n    updateRecentProperties(\n      recentProperties.filter(\n        (recentProperty) => styleMap.has(recentProperty) === false\n      )\n    );\n  };\n\n  return (\n    <AdvancedStyleSection\n      label=\"Advanced\"\n      properties={properties}\n      onAdd={() => {\n        setShowAddStyleInput(true);\n      }}\n    >\n      <CssEditor\n        declarations={advancedStyleDeclarations}\n        onDeleteProperty={handleDeleteProperty}\n        onSetProperty={setProperty}\n        onAddDeclarations={handleAddDeclarations}\n        onDeleteAllDeclarations={handleDeleteAllDeclarations}\n        recentProperties={recentProperties}\n        showAddStyleInput={showAddStyleInput}\n        onToggleAddStyleInput={setShowAddStyleInput}\n      />\n    </AdvancedStyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/advanced/stores.ts",
    "content": "import { computed } from \"nanostores\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { ROOT_INSTANCE_ID } from \"@webstudio-is/sdk\";\nimport { $settings } from \"~/builder/shared/client-settings\";\nimport { $selectedInstance } from \"~/shared/awareness\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { $computedStyleDeclarations } from \"../../shared/model\";\nimport { sections } from \"../sections\";\n\nexport const $advancedStyleDeclarations = computed(\n  [$computedStyleDeclarations, $settings, $selectedInstance],\n  (computedStyleDeclarations, settings, selectedInstance) => {\n    const advancedStyles = new Map<\n      ComputedStyleDecl[\"property\"],\n      ComputedStyleDecl\n    >();\n    // All properties used by the panels except the advanced panel\n    const visualProperties = new Set<CssProperty>([]);\n    for (const { properties } of sections.values()) {\n      for (const property of properties) {\n        visualProperties.add(property);\n      }\n    }\n    for (const styleDecl of computedStyleDeclarations) {\n      // We don't want to show the massive amount of root variables on child instances.\n      // @todo add filters to the UI to allow user decide.\n      if (\n        styleDecl.source.name === \"remote\" &&\n        styleDecl.source.instanceId === ROOT_INSTANCE_ID &&\n        styleDecl.source.instanceId !== selectedInstance?.id\n      ) {\n        continue;\n      }\n      // ignore predefined styles in advanced mode\n      // @todo will be deleted https://github.com/webstudio-is/webstudio/issues/4871\n      if (\n        styleDecl.source.name === \"default\" &&\n        settings.stylePanelMode === \"advanced\"\n      ) {\n        continue;\n      }\n      const { property, listed } = styleDecl;\n      // When property is listed, it was added from advanced panel.\n      // If we are in advanced mode, we show them all.\n      if (\n        visualProperties.has(property) === false ||\n        listed ||\n        settings.stylePanelMode === \"advanced\"\n      ) {\n        advancedStyles.set(styleDecl.property, styleDecl);\n      }\n    }\n\n    return Array.from(advancedStyles.values());\n  }\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backdrop-filter/backdrop-filter.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $instances,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { Section } from \"./backdrop-filter\";\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(new Map([[\"local\", { id: \"local\", type: \"local\" }]]));\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nconst blurBackdropFilter: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"backdropFilter\",\n  value: {\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"blur\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n        },\n      },\n    ],\n  },\n};\n\nconst multipleBackdropFilters: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"backdropFilter\",\n  value: {\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"blur\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"brightness\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"%\", value: 150 }],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"contrast\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"%\", value: 200 }],\n        },\n      },\n    ],\n  },\n};\n\nconst WithBlurFilterVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([[getStyleDeclKey(blurBackdropFilter), blurBackdropFilter]])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nconst WithMultipleFiltersVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([\n        [getStyleDeclKey(multipleBackdropFilters), multipleBackdropFilters],\n      ])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nexport const BackdropFilters = () => (\n  <StorySection title=\"Backdrop filters\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <Section />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With blur filter</Text>\n        <WithBlurFilterVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With multiple filters</Text>\n        <WithMultipleFiltersVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Backdrop Filters\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backdrop-filter/backdrop-filter.tsx",
    "content": "import {\n  toValue,\n  type CssProperty,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { Tooltip, Flex, Text } from \"@webstudio-is/design-system\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { RepeatedStyleSection } from \"../../shared/style-section\";\nimport { FilterSectionContent } from \"../../shared/filter-content\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\nimport {\n  addRepeatedStyleItem,\n  editRepeatedStyleItem,\n  RepeatedStyle,\n} from \"../../shared/repeated-style\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\n\nexport const properties = [\"backdrop-filter\"] satisfies [\n  CssProperty,\n  ...CssProperty[],\n];\n\nconst label = \"Backdrop filters\";\nconst initialBackdropFilter = \"blur(0px)\";\n\nconst getItemProps = (_index: number, value: StyleValue) => {\n  const label =\n    value.type === \"function\"\n      ? `${humanizeString(value.name)}: ${toValue(value.args)}`\n      : \"Unknown filter\";\n  return { label };\n};\n\nexport const Section = () => {\n  const styleDecl = useComputedStyleDecl(\"backdrop-filter\");\n\n  return (\n    <RepeatedStyleSection\n      label={label}\n      description=\"Backdrop filters are similar to filters, but are applied to the area behind an element. This can be useful for creating frosted glass effects.\"\n      properties={properties}\n      onAdd={() => {\n        addRepeatedStyleItem(\n          [styleDecl],\n          parseCssFragment(initialBackdropFilter, [\"backdrop-filter\"])\n        );\n      }}\n    >\n      <RepeatedStyle\n        label={label}\n        styles={[styleDecl]}\n        getItemProps={getItemProps}\n        renderItemContent={(index, primaryValue) => (\n          <FilterSectionContent\n            index={index}\n            property=\"backdrop-filter\"\n            propertyValue={toValue(primaryValue)}\n            layer={primaryValue}\n            onEditLayer={(index, value, options) => {\n              editRepeatedStyleItem(\n                [styleDecl],\n                index,\n                new Map([[\"backdrop-filter\", value]]),\n                options\n              );\n            }}\n            tooltip={\n              <Tooltip\n                variant=\"wrapped\"\n                content={\n                  <Flex gap=\"2\" direction=\"column\">\n                    <Text variant=\"regularBold\">{label}</Text>\n                    <Text variant=\"monoBold\">backdrop-filter</Text>\n                    <Text>\n                      Applies graphical effects like blur or color shift to the\n                      area behind an element\n                      <br /> <br />\n                      <Text variant=\"mono\">{initialBackdropFilter}</Text>\n                    </Text>\n                  </Flex>\n                }\n              >\n                <InfoCircleIcon />\n              </Tooltip>\n            }\n          />\n        )}\n      />\n    </RepeatedStyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-code-editor.tsx",
    "content": "import type { InvalidValue, StyleValue } from \"@webstudio-is/css-engine\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  CssFragmentEditor,\n  CssFragmentEditorContent,\n  getCodeEditorCssVars,\n  parseCssFragment,\n} from \"../../shared/css-fragment\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { setProperty } from \"../../shared/use-style-data\";\nimport {\n  editRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { InputErrorsTooltip } from \"@webstudio-is/design-system\";\n\ntype IntermediateValue = {\n  type: \"intermediate\";\n  value: string;\n};\n\nconst isTransparent = (color: StyleValue) =>\n  color.type === \"keyword\" && color.value === \"transparent\";\n\ntype BackgroundCodeEditorProps = {\n  index: number;\n  /**\n   * Optional custom validation and error handling\n   */\n  onValidate?: (\n    value: string,\n    parsed: Map<string, StyleValue>\n  ) => string[] | undefined;\n};\n\nexport const BackgroundCodeEditor = ({\n  index,\n  onValidate,\n}: BackgroundCodeEditorProps) => {\n  const styleDecl = useComputedStyleDecl(\"background-image\");\n  let styleValue = styleDecl.cascadedValue;\n  if (styleValue.type === \"layers\") {\n    styleValue = styleValue.value[index];\n  }\n\n  const [intermediateValue, setIntermediateValue] = useState<\n    IntermediateValue | InvalidValue | undefined\n  >(undefined);\n\n  const [errors, setErrors] = useState<string[]>([]);\n\n  // Reset intermediate value when styleValue changes (e.g., from UI updates)\n  useEffect(() => {\n    setIntermediateValue(undefined);\n  }, [styleValue]);\n\n  const textAreaValue = intermediateValue?.value ?? toValue(styleValue);\n\n  const handleChange = useCallback(\n    (value: string) => {\n      setIntermediateValue({\n        type: \"intermediate\",\n        value,\n      });\n\n      const parsed = parseCssFragment(value, [\n        \"background-image\",\n        \"background\",\n      ]);\n      const newValue = parsed.get(\"background-image\");\n\n      if (newValue === undefined || newValue?.type === \"invalid\") {\n        setIntermediateValue({\n          type: \"invalid\",\n          value: value,\n        });\n        return;\n      }\n\n      // Run custom validation if provided\n      if (onValidate) {\n        const validationErrors = onValidate(value, parsed);\n        if (validationErrors && validationErrors.length > 0) {\n          setErrors(validationErrors);\n          setIntermediateValue({\n            type: \"invalid\",\n            value: value,\n          });\n          return;\n        }\n        setErrors([]);\n      }\n\n      setRepeatedStyleItem(styleDecl, index, newValue, { isEphemeral: true });\n    },\n    [index, onValidate, styleDecl]\n  );\n\n  const handleOnComplete = useCallback(() => {\n    if (intermediateValue === undefined) {\n      return;\n    }\n\n    const parsed = parseCssFragment(intermediateValue.value, [\n      \"background-image\",\n      \"background\",\n    ]);\n    const backgroundImage = parsed.get(\"background-image\");\n    const backgroundColor = parsed.get(\"background-color\");\n\n    if (backgroundColor?.type === \"invalid\" || backgroundImage === undefined) {\n      setIntermediateValue({ type: \"invalid\", value: intermediateValue.value });\n      if (styleValue) {\n        setRepeatedStyleItem(styleDecl, index, styleValue, {\n          isEphemeral: true,\n        });\n      }\n      return;\n    }\n    setIntermediateValue(undefined);\n    if (backgroundColor && isTransparent(backgroundColor) === false) {\n      setProperty(\"background-color\")(backgroundColor);\n    }\n    editRepeatedStyleItem(\n      [styleDecl],\n      index,\n      new Map([[\"background-image\", backgroundImage]])\n    );\n  }, [index, intermediateValue, styleDecl, styleValue]);\n\n  const handleOnCompleteRef = useRef(handleOnComplete);\n  useEffect(() => {\n    handleOnCompleteRef.current = handleOnComplete;\n  }, [handleOnComplete]);\n\n  useEffect(() => {\n    return () => {\n      handleOnCompleteRef.current();\n    };\n  }, []);\n\n  return (\n    <>\n      <PropertyInlineLabel\n        label=\"Code\"\n        description=\"Paste a CSS gradient or image, for example: linear-gradient(...) or url('image.jpg'). If pasting from Figma, remove the 'background' property name.\"\n      />\n      <InputErrorsTooltip errors={errors}>\n        <CssFragmentEditor\n          css={getCodeEditorCssVars({ minHeight: \"4lh\", maxHeight: \"4lh\" })}\n          content={\n            <CssFragmentEditorContent\n              invalid={intermediateValue?.type === \"invalid\"}\n              autoFocus={styleValue.type === \"var\"}\n              value={textAreaValue ?? \"\"}\n              onChange={handleChange}\n              onChangeComplete={handleOnComplete}\n            />\n          }\n        />\n      </InputErrorsTooltip>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-content.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport {\n  $breakpoints,\n  $instances,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { BackgroundContent as BackgroundContentPanel } from \"./background-content\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { setRepeatedStyleItem } from \"../../shared/repeated-style\";\nimport { type StyleValue } from \"@webstudio-is/css-engine\";\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n\nconst defaultBackgroundImage: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"backgroundImage\",\n  value: {\n    type: \"layers\",\n    value: [{ type: \"keyword\", value: \"none\" }],\n  },\n};\n\n$styles.set(\n  new Map([[getStyleDeclKey(defaultBackgroundImage), defaultBackgroundImage]])\n);\n\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nconst BackgroundStory = ({ styleValue }: { styleValue: StyleValue }) => {\n  const backgroundImage = useComputedStyleDecl(\"background-image\");\n\n  useEffect(() => {\n    setRepeatedStyleItem(backgroundImage, 0, styleValue);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <BackgroundContentPanel index={0} />\n    </Box>\n  );\n};\n\nexport const BackgroundContent = () => (\n  <StorySection title=\"Background content\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Image (none)</Text>\n        <BackgroundStory styleValue={{ type: \"keyword\", value: \"none\" }} />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Linear gradient</Text>\n        <BackgroundStory\n          styleValue={{\n            type: \"unparsed\",\n            value:\n              \"linear-gradient(135deg, rgba(255,126,95,1) 0%, rgba(254,180,123,1) 35%, rgba(134,168,231,1) 100%)\",\n          }}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Conic gradient</Text>\n        <BackgroundStory\n          styleValue={{\n            type: \"unparsed\",\n            value:\n              \"conic-gradient(from 0deg at 50% 50%, rgba(255,126,95,1) 0deg, rgba(254,180,123,1) 120deg, rgba(134,168,231,1) 240deg, rgba(255,126,95,1) 360deg)\",\n          }}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Radial gradient</Text>\n        <BackgroundStory\n          styleValue={{\n            type: \"unparsed\",\n            value:\n              \"radial-gradient(circle at 50% 50%, rgba(255,126,95,1) 0%, rgba(254,180,123,1) 50%, rgba(134,168,231,1) 100%)\",\n          }}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Solid</Text>\n        <BackgroundStory\n          styleValue={{\n            type: \"unparsed\",\n            value:\n              \"linear-gradient(0deg, rgba(56,189,248,1) 0%, rgba(56,189,248,1) 100%)\",\n          }}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Repeating linear gradient</Text>\n        <BackgroundStory\n          styleValue={{\n            type: \"unparsed\",\n            value:\n              \"repeating-linear-gradient(45deg, rgba(255,0,0,1) 0%, rgba(0,0,255,1) 10%, rgba(255,0,0,1) 20%)\",\n          }}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Multi-stop gradient</Text>\n        <BackgroundStory\n          styleValue={{\n            type: \"unparsed\",\n            value:\n              \"linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,165,0,1) 20%, rgba(255,255,0,1) 40%, rgba(0,128,0,1) 60%, rgba(0,0,255,1) 80%, rgba(128,0,128,1) 100%)\",\n          }}\n        />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Backgrounds\",\n  component: BackgroundContentPanel,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-content.tsx",
    "content": "/**\n * Will be fully rewritten in next iteration,\n * as of now just implement feature parity with old backgrounds section\n **/\n\nimport { type ReactNode, useCallback, useRef, useState } from \"react\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport {\n  RepeatGridIcon,\n  RepeatColumnIcon,\n  RepeatRowIcon,\n  XSmallIcon,\n  ImageIcon,\n  GradientLinearIcon,\n  GradientConicIcon,\n  GradientRadialIcon,\n} from \"@webstudio-is/icons\";\nimport { type StyleValue, toValue } from \"@webstudio-is/css-engine\";\nimport {\n  theme,\n  Flex,\n  Grid,\n  ToggleGroup,\n  ToggleGroupButton,\n  Separator,\n  styled,\n  Box,\n  EnhancedTooltip,\n  ScrollArea,\n} from \"@webstudio-is/design-system\";\nimport { SelectControl } from \"../../controls\";\nimport { ToggleGroupTooltip } from \"../../controls/toggle-group/toggle-group-control\";\nimport { BackgroundSize } from \"./background-size\";\nimport { BackgroundGradient } from \"./background-gradient\";\nimport { BackgroundImage } from \"./background-image\";\nimport { BackgroundPosition } from \"./background-position\";\nimport {\n  PropertyLabel,\n  PropertyValueTooltip,\n  PropertyInlineLabel,\n} from \"../../property-label\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport {\n  getRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport {\n  detectBackgroundType,\n  formatGradientForType,\n  getBackgroundStyleItem,\n  type BackgroundType,\n} from \"./gradient-utils\";\nimport { CollapsibleSectionRoot } from \"~/builder/shared/collapsible-section\";\n\nconst ColorSwatchIcon = styled(\"div\", {\n  width: theme.spacing[7],\n  height: theme.spacing[7],\n  borderRadius: theme.borderRadius[3],\n  backgroundColor: theme.colors.foregroundMain,\n  boxShadow: `inset 0 0 0 1px ${theme.colors.borderMain}`,\n});\n\ntype BackgroundTypeOption = {\n  value: BackgroundType;\n  label: string;\n  description: string;\n  code: string;\n  icon: ReactNode;\n  autoFocus?: boolean;\n};\n\n// looks like now when dialog is open first toggle group buttons need to have autoFocus\n// otherwise the following \"Choose image\" button is focused\n// https://github.com/radix-ui/primitives/pull/2027\n// https://github.com/radix-ui/primitives/issues/1910\nconst backgroundTypeOptions: BackgroundTypeOption[] = [\n  {\n    value: \"image\",\n    label: \"Image\",\n    description:\n      \"Use an image asset, remote URL, or data URI as the layer background.\",\n    code: \"background-image: url(...);\",\n    icon: <ImageIcon />,\n    autoFocus: true,\n  },\n  {\n    value: \"solid\",\n    label: \"Solid\",\n    description:\n      \"Use a single color layer while keeping control over stacking order.\",\n    code: \"background-image: linear-gradient(color, color);\",\n    icon: <ColorSwatchIcon />,\n  },\n  {\n    value: \"linearGradient\",\n    label: \"Linear gradient\",\n    description:\n      \"Blend multiple colors along a line to create smooth transitions.\",\n    code: \"background-image: linear-gradient(...);\",\n    icon: <GradientLinearIcon />,\n  },\n  {\n    value: \"radialGradient\",\n    label: \"Radial gradient\",\n    description:\n      \"Blend multiple colors in a circular pattern to create smooth transitions.\",\n    code: \"background-image: radial-gradient(...);\",\n    icon: <GradientRadialIcon />,\n  },\n  {\n    value: \"conicGradient\",\n    label: \"Conic gradient\",\n    description:\n      \"Spin colors around a center point for charts, dials, and spotlight effects.\",\n    code: \"background-image: conic-gradient(...);\",\n    icon: <GradientConicIcon />,\n  },\n];\n\ntype BackgroundTypeToggleProps = {\n  value: BackgroundType;\n  onChange: (value: BackgroundType) => void;\n  backgroundStyleItem: StyleValue | undefined;\n  styleDecl: ComputedStyleDecl;\n  index: number;\n  cachedValues: React.MutableRefObject<\n    Partial<Record<BackgroundType, StyleValue>>\n  >;\n};\n\nconst BackgroundTypeToggle = ({\n  value,\n  onChange,\n  backgroundStyleItem,\n  styleDecl,\n  index,\n  cachedValues,\n}: BackgroundTypeToggleProps) => {\n  const handleValueChange = useCallback(\n    (nextValue: BackgroundType) => {\n      if (nextValue === value) {\n        return;\n      }\n\n      // Cache current value before switching\n      if (backgroundStyleItem !== undefined) {\n        cachedValues.current[value] = backgroundStyleItem;\n      }\n\n      onChange(nextValue);\n\n      // Check if we have a cached value for the new type\n      const cachedValue = cachedValues.current[nextValue];\n\n      if (nextValue === \"image\") {\n        // For image, restore cached value or set to none\n        if (cachedValue !== undefined) {\n          setRepeatedStyleItem(styleDecl, index, cachedValue);\n        } else {\n          setRepeatedStyleItem(styleDecl, index, {\n            type: \"keyword\",\n            value: \"none\",\n          });\n        }\n      } else {\n        // For gradients and solid color, restore cached or generate new\n        const gradientValue = cachedValue\n          ? cachedValue.type === \"unparsed\"\n            ? cachedValue.value\n            : formatGradientForType(cachedValue, nextValue)\n          : formatGradientForType(backgroundStyleItem, nextValue);\n\n        setRepeatedStyleItem(styleDecl, index, {\n          type: \"unparsed\",\n          value: gradientValue,\n        });\n      }\n    },\n    [backgroundStyleItem, index, onChange, styleDecl, value, cachedValues]\n  );\n\n  return (\n    <ToggleGroup\n      type=\"single\"\n      value={value}\n      aria-label=\"Background type\"\n      onValueChange={handleValueChange}\n    >\n      {backgroundTypeOptions.map(\n        ({ value: optionValue, label, icon, autoFocus }) => (\n          <EnhancedTooltip key={optionValue} content={label}>\n            <ToggleGroupButton\n              value={optionValue}\n              aria-label={label}\n              autoFocus={autoFocus}\n            >\n              <Flex css={{ px: theme.spacing[3] }}>{icon}</Flex>\n            </ToggleGroupButton>\n          </EnhancedTooltip>\n        )\n      )}\n    </ToggleGroup>\n  );\n};\n\nconst BackgroundRepeat = ({ index }: { index: number }) => {\n  const styleDecl = useComputedStyleDecl(\"background-repeat\");\n  const value = getRepeatedStyleItem(styleDecl, index);\n  const items = [\n    {\n      child: <XSmallIcon />,\n      description:\n        \"This value indicates that the background image will not be repeated and will appear only once.\",\n      value: \"no-repeat\",\n    },\n    {\n      child: <RepeatGridIcon />,\n      description:\n        \"This value indicates that the background image will be repeated both horizontally and vertically to fill the entire background area.\",\n      value: \"repeat\",\n    },\n    {\n      child: <RepeatColumnIcon />,\n      description:\n        \"This value indicates that the background image will be repeated only vertically.\",\n      value: \"repeat-y\",\n    },\n    {\n      child: <RepeatRowIcon />,\n      description:\n        \"This value indicates that the background image will be repeated only horizontally.\",\n      value: \"repeat-x\",\n    },\n  ];\n  // Issue: The tooltip's grace area is too big and overlaps with nearby buttons,\n  // preventing the tooltip from changing when the buttons are hovered over in certain cases.\n  // To solve issue and allow tooltips to change on button hover,\n  // we close the button tooltip in the ToggleGroupButton.onMouseEnter handler.\n  // onMouseEnter used to preserve default hovering behavior on tooltip.\n  const [activeTooltip, setActiveTooltip] = useState<undefined | string>();\n  return (\n    <PropertyValueTooltip\n      label=\"Repeat\"\n      description={propertyDescriptions.backgroundRepeat}\n      properties={[\"background-repeat\"]}\n    >\n      <ToggleGroup\n        type=\"single\"\n        value={toValue(value)}\n        aria-label=\"Background repeat\"\n        onValueChange={(value) => {\n          setRepeatedStyleItem(styleDecl, index, { type: \"keyword\", value });\n        }}\n      >\n        {items.map((item) => (\n          <ToggleGroupTooltip\n            key={item.value}\n            isOpen={item.value === activeTooltip}\n            onOpenChange={(isOpen) =>\n              setActiveTooltip(isOpen ? item.value : undefined)\n            }\n            isSelected={false}\n            label=\"Background Repeat\"\n            code={`background-repeat: ${item.value};`}\n            description={item.description}\n            properties={[\"background-repeat\"]}\n          >\n            <ToggleGroupButton\n              value={item.value}\n              aria-label={\n                item.value === \"no-repeat\"\n                  ? \"Do not repeat background\"\n                  : item.value === \"repeat\"\n                    ? \"Repeat background\"\n                    : item.value === \"repeat-y\"\n                      ? \"Repeat background vertically\"\n                      : \"Repeat background horizontally\"\n              }\n              onMouseEnter={() =>\n                // reset only when highlighted is not active\n                setActiveTooltip((prevValue) =>\n                  prevValue === item.value ? prevValue : undefined\n                )\n              }\n            >\n              {item.child}\n            </ToggleGroupButton>\n          </ToggleGroupTooltip>\n        ))}\n      </ToggleGroup>\n    </PropertyValueTooltip>\n  );\n};\n\nconst BackgroundAttachment = ({ index }: { index: number }) => {\n  const styleDecl = useComputedStyleDecl(\"background-attachment\");\n  const value = getRepeatedStyleItem(styleDecl, index);\n  return (\n    <PropertyValueTooltip\n      label=\"Attachment\"\n      description={propertyDescriptions.backgroundAttachment}\n      properties={[\"background-attachment\"]}\n    >\n      <ToggleGroup\n        type=\"single\"\n        value={toValue(value)}\n        aria-label=\"Background attachment\"\n        onValueChange={(value) => {\n          setRepeatedStyleItem(styleDecl, index, { type: \"keyword\", value });\n        }}\n      >\n        <ToggleGroupButton value={\"scroll\"}>\n          <Flex css={{ px: theme.spacing[3] }}>Scroll</Flex>\n        </ToggleGroupButton>\n        <ToggleGroupButton value={\"fixed\"}>\n          <Flex css={{ px: theme.spacing[3] }}>Fixed</Flex>\n        </ToggleGroupButton>\n      </ToggleGroup>\n    </PropertyValueTooltip>\n  );\n};\n\nconst OtherLayerProperties = ({ index }: { index: number }) => {\n  return (\n    <CollapsibleSectionRoot label={\"More properties\"} fullWidth={true}>\n      <Flex\n        gap=\"2\"\n        direction=\"column\"\n        css={{ paddingInline: theme.panel.paddingInline }}\n      >\n        <Grid columns={2} gap={2}>\n          <PropertyLabel\n            label=\"Blend mode\"\n            description={propertyDescriptions.backgroundBlendMode}\n            properties={[\"background-blend-mode\"]}\n          />\n          <SelectControl property=\"background-blend-mode\" index={index} />\n        </Grid>\n        <BackgroundSize index={index} />\n        <BackgroundPosition index={index} />\n        <Grid columns={2} align=\"center\" gap={2}>\n          <PropertyLabel\n            label=\"Repeat\"\n            description={propertyDescriptions.backgroundRepeat}\n            properties={[\"background-repeat\"]}\n          />\n          <BackgroundRepeat index={index} />\n\n          <PropertyLabel\n            label=\"Attachment\"\n            description={propertyDescriptions.backgroundAttachment}\n            properties={[\"background-attachment\"]}\n          />\n          <BackgroundAttachment index={index} />\n        </Grid>\n        <Grid columns={2} align=\"center\" gap={2}>\n          <PropertyLabel\n            label=\"Clip\"\n            description={propertyDescriptions.backgroundClip}\n            properties={[\"background-clip\"]}\n          />\n          <SelectControl property=\"background-clip\" index={index} />\n\n          <PropertyLabel\n            label=\"Origin\"\n            description={propertyDescriptions.backgroundOrigin}\n            properties={[\"background-origin\"]}\n          />\n          <SelectControl property=\"background-origin\" index={index} />\n        </Grid>\n      </Flex>\n    </CollapsibleSectionRoot>\n  );\n};\n\nexport const BackgroundContent = ({ index }: { index: number }) => {\n  const backgroundImage = useComputedStyleDecl(\"background-image\");\n  const backgroundStyleItem = getBackgroundStyleItem(backgroundImage, index);\n\n  const [backgroundType, setBackgroundType] = useState<BackgroundType>(() =>\n    detectBackgroundType(backgroundStyleItem)\n  );\n\n  // Cache background values for each type to preserve user's intermediate changes\n  const cachedValuesRef = useRef<Partial<Record<BackgroundType, StyleValue>>>(\n    {}\n  );\n\n  return (\n    <>\n      <Flex\n        align=\"center\"\n        gap=\"2\"\n        justify=\"between\"\n        css={{ padding: theme.panel.padding }}\n        shrink={false}\n      >\n        <PropertyInlineLabel\n          label=\"Type\"\n          description={propertyDescriptions.backgroundImage}\n        />\n        <BackgroundTypeToggle\n          value={backgroundType}\n          onChange={setBackgroundType}\n          backgroundStyleItem={backgroundStyleItem}\n          styleDecl={backgroundImage}\n          index={index}\n          cachedValues={cachedValuesRef}\n        />\n      </Flex>\n\n      <Separator />\n\n      <ScrollArea>\n        <Box css={{ maxHeight: 500 }}>\n          {(backgroundType === \"linearGradient\" ||\n            backgroundType === \"conicGradient\" ||\n            backgroundType === \"radialGradient\" ||\n            backgroundType === \"solid\") && (\n            <BackgroundGradient\n              index={index}\n              type={\n                backgroundType === \"conicGradient\"\n                  ? \"conic\"\n                  : backgroundType === \"radialGradient\"\n                    ? \"radial\"\n                    : \"linear\"\n              }\n              variant={backgroundType === \"solid\" ? \"solid\" : \"default\"}\n            />\n          )}\n\n          {backgroundType === \"image\" && <BackgroundImage index={index} />}\n\n          <Separator />\n\n          <OtherLayerProperties index={index} />\n        </Box>\n      </ScrollArea>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-gradient.tsx",
    "content": "import {\n  toValue,\n  type StyleValue,\n  type UnitValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  type ParsedGradient,\n  type ParsedLinearGradient,\n  type ParsedConicGradient,\n  type ParsedRadialGradient,\n  type GradientStop,\n  formatLinearGradient,\n} from \"@webstudio-is/css-data\";\nimport {\n  Flex,\n  theme,\n  Tooltip,\n  GradientPicker,\n  Grid,\n  Box,\n  IconButton,\n  Select,\n  Separator,\n  ToggleGroup,\n  ToggleGroupButton,\n} from \"@webstudio-is/design-system\";\nimport { ColorPickerControl } from \"../../shared/color-picker\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n  type Dispatch,\n  type SetStateAction,\n} from \"react\";\nimport {\n  ArrowRightLeftIcon,\n  CircleIcon,\n  EllipseIcon,\n  MinusIcon,\n  PlusIcon,\n  RepeatGridIcon,\n  XSmallIcon,\n} from \"@webstudio-is/icons\";\nimport {\n  useComputedStyleDecl,\n  $availableUnitVariables,\n} from \"../../shared/model\";\nimport { setRepeatedStyleItem } from \"../../shared/repeated-style\";\nimport { useLocalValue } from \"../../../settings-panel/shared\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport {\n  angleUnitOptions,\n  clampStopIndex,\n  createDefaultGradient,\n  createSolidLinearGradient,\n  createPercentUnitValue,\n  ensureGradientHasStops,\n  fallbackStopColor,\n  formatGradientPositionValues,\n  formatGradientValue,\n  getDefaultAngle,\n  getStopPosition,\n  gradientPositionXOptions,\n  gradientPositionYOptions,\n  isConicGradient,\n  isLinearGradient,\n  isRadialGradient,\n  normalizeGradientInput,\n  parseAnyGradient,\n  parseGradientPositionValues,\n  percentUnitOptions,\n  pruneHintOverrides,\n  reindexHintOverrides,\n  removeHintOverride,\n  resolveAngleValue,\n  resolveGradientForPicker,\n  resolveReverseStops,\n  resolveStopHintUpdate,\n  resolveStopPositionUpdate,\n  setHintOverride,\n  sortGradientStops,\n  styleValueToColor,\n  updateGradientStop,\n} from \"./gradient-utils\";\nimport { BackgroundPositionControl } from \"./background-position\";\nimport { BackgroundCodeEditor } from \"./background-code-editor\";\nimport type {\n  GradientType,\n  IntermediateColorValue,\n  PercentUnitValue,\n} from \"./gradient-utils\";\n\nconst radialSizeOptions = [\n  \"closest-side\",\n  \"closest-corner\",\n  \"farthest-side\",\n  \"farthest-corner\",\n] as const;\n\ntype RadialSizeOption = (typeof radialSizeOptions)[number];\n\nconst radialSizeOptionsSet = new Set<RadialSizeOption>(radialSizeOptions);\n\nconst defaultRadialSize: RadialSizeOption = \"farthest-corner\";\nconst defaultRadialShape = \"ellipse\" as const;\n\nconst leftToRightAngle = {\n  type: \"unit\",\n  unit: \"deg\",\n  value: 90,\n} satisfies UnitValue;\n\nconst radialSizeDescriptions: Record<RadialSizeOption, string> = {\n  \"closest-side\": \"Extends to the nearest edge of the container\",\n  \"closest-corner\": \"Extends to the nearest corner of the container\",\n  \"farthest-side\": \"Extends to the farthest edge of the container\",\n  \"farthest-corner\": \"Extends to the farthest corner of the container\",\n};\n\nconst radialShapeDescriptions = {\n  ellipse: \"Use an ellipse ending shape (radial-gradient ellipse).\",\n  circle: \"Use a circle ending shape (radial-gradient circle).\",\n} as const;\n\ntype GradientEditorApplyFn = (\n  nextGradient: ParsedGradient,\n  options?: { isEphemeral?: boolean }\n) => void;\n\nconst getAvailableUnitVariables = () => $availableUnitVariables.get();\n\nexport const BackgroundGradient = ({\n  index,\n  type: gradientType = \"linear\",\n  variant = \"default\",\n}: {\n  index: number;\n  type?: GradientType;\n  variant?: \"default\" | \"solid\";\n}) => {\n  const styleDecl = useComputedStyleDecl(\"background-image\");\n  let styleValue = styleDecl.cascadedValue;\n  let computedStyleValue = styleDecl.computedValue;\n  if (styleValue.type === \"layers\") {\n    styleValue = styleValue.value[index];\n  }\n  if (computedStyleValue.type === \"layers\") {\n    computedStyleValue = computedStyleValue.value[index];\n  }\n\n  const gradientString = toValue(styleValue);\n  const { normalizedGradientString, initialIsRepeating } =\n    normalizeGradientInput(gradientString, gradientType);\n\n  const parsedGradient = useMemo(() => {\n    const parsed =\n      parseAnyGradient(normalizedGradientString) ??\n      createDefaultGradient(gradientType);\n    return ensureGradientHasStops(parsed);\n  }, [gradientType, normalizedGradientString]);\n\n  const computedGradientString = toValue(computedStyleValue);\n  const { normalizedGradientString: normalizedComputedGradientString } =\n    normalizeGradientInput(computedGradientString, gradientType);\n\n  const computedParsedGradient = useMemo(() => {\n    const parsed =\n      parseAnyGradient(normalizedComputedGradientString) ??\n      createDefaultGradient(gradientType);\n    return ensureGradientHasStops(parsed);\n  }, [gradientType, normalizedComputedGradientString]);\n\n  const handleGradientSave = useCallback(\n    (nextGradient: ParsedGradient) => {\n      const gradientValue = formatGradientValue(nextGradient);\n      const style: StyleValue = { type: \"unparsed\", value: gradientValue };\n      setRepeatedStyleItem(styleDecl, index, style);\n    },\n    [index, styleDecl]\n  );\n\n  const {\n    value: gradient,\n    set: setLocalGradient,\n    save: saveLocalGradient,\n  } = useLocalValue<ParsedGradient>(parsedGradient, handleGradientSave, {\n    autoSave: false,\n  });\n  const [selectedStopIndex, setSelectedStopIndex] = useState(0);\n  const [hintOverrides, setHintOverrides] = useState(\n    () => new Map<number, PercentUnitValue>()\n  );\n  const [isRepeating, setIsRepeating] = useState(initialIsRepeating);\n  const isSolidVariant = variant === \"solid\";\n\n  useEffect(() => {\n    setSelectedStopIndex((currentIndex) =>\n      clampStopIndex(currentIndex, gradient)\n    );\n  }, [gradient]);\n\n  useEffect(() => {\n    setHintOverrides((previous) => {\n      return pruneHintOverrides(previous, gradient.stops.length);\n    });\n  }, [gradient]);\n\n  useEffect(() => {\n    setIsRepeating(initialIsRepeating);\n  }, [initialIsRepeating]);\n\n  const applyGradient = useCallback(\n    (nextGradient: ParsedGradient, options?: { isEphemeral?: boolean }) => {\n      const isEphemeral = options?.isEphemeral === true;\n\n      // Only sort gradient stops on final (non-ephemeral) changes\n      // This allows stops to visually cross during ephemeral updates (e.g., typing in inputs)\n      let finalGradient: ParsedGradient;\n      let finalHints: Map<number, PercentUnitValue>;\n\n      if (isEphemeral) {\n        // During ephemeral changes, apply the gradient as-is without sorting\n        finalGradient = nextGradient;\n        finalHints = hintOverrides;\n      } else {\n        // On complete, sort stops and reindex hint overrides to keep them in sync\n        const { sortedGradient, reindexedHints } = sortGradientStops(\n          nextGradient,\n          hintOverrides\n        );\n        finalGradient = sortedGradient;\n        finalHints = reindexedHints;\n      }\n\n      setLocalGradient(finalGradient);\n      setHintOverrides(finalHints);\n\n      const gradientValue = formatGradientValue(finalGradient);\n      setRepeatedStyleItem(\n        styleDecl,\n        index,\n        { type: \"unparsed\", value: gradientValue },\n        { isEphemeral }\n      );\n      if (isEphemeral === false) {\n        saveLocalGradient();\n      }\n    },\n    [index, saveLocalGradient, setLocalGradient, styleDecl, hintOverrides]\n  );\n\n  return (\n    <Flex direction=\"column\" justify=\"center\" gap=\"2\" shrink={false}>\n      {isSolidVariant ? (\n        <Box css={{ padding: theme.panel.padding }}>\n          <SolidColorControls\n            gradient={gradient}\n            applyGradient={applyGradient}\n          />\n        </Box>\n      ) : (\n        <>\n          <GradientPickerSection\n            gradient={gradient}\n            computedGradient={computedParsedGradient}\n            gradientType={gradientType}\n            hintOverrides={hintOverrides}\n            setHintOverrides={setHintOverrides}\n            setSelectedStopIndex={setSelectedStopIndex}\n            applyGradient={applyGradient}\n            selectedStopIndex={selectedStopIndex}\n          />\n          <Separator />\n          <Box css={{ paddingInline: theme.panel.paddingInline }}>\n            <OtherGradientPropertiesSection\n              gradient={gradient}\n              applyGradient={applyGradient}\n              isRepeating={isRepeating}\n              setIsRepeating={setIsRepeating}\n            />\n          </Box>\n          <Box css={{ paddingInline: theme.panel.paddingInline }}>\n            <GradientPositionControls\n              gradient={gradient}\n              applyGradient={applyGradient}\n            />\n          </Box>\n        </>\n      )}\n      <Box\n        css={{\n          paddingInline: theme.panel.paddingInline,\n          paddingBottom: theme.panel.paddingBlock,\n        }}\n      >\n        <BackgroundCodeEditor index={index} />\n      </Box>\n    </Flex>\n  );\n};\n\ntype GradientPickerSectionProps = {\n  gradient: ParsedGradient;\n  computedGradient: ParsedGradient;\n  gradientType: GradientType;\n  hintOverrides: Map<number, PercentUnitValue>;\n  setHintOverrides: Dispatch<SetStateAction<Map<number, PercentUnitValue>>>;\n  setSelectedStopIndex: Dispatch<SetStateAction<number>>;\n  applyGradient: GradientEditorApplyFn;\n  selectedStopIndex: number;\n};\n\nconst GradientPickerSection = ({\n  gradient,\n  computedGradient,\n  gradientType,\n  hintOverrides,\n  setHintOverrides,\n  setSelectedStopIndex,\n  applyGradient,\n  selectedStopIndex,\n}: GradientPickerSectionProps) => {\n  // Use computed gradient for picker (resolves all CSS variables)\n  const computedGradientForPicker = useMemo(() => {\n    return resolveGradientForPicker(computedGradient, hintOverrides);\n  }, [computedGradient, hintOverrides]);\n\n  const handlePickerChange = useCallback(\n    (nextGradient: ParsedGradient) => {\n      applyGradient(nextGradient, { isEphemeral: true });\n    },\n    [applyGradient]\n  );\n\n  const handlePickerChangeComplete = useCallback(\n    (nextGradient: ParsedGradient) => {\n      applyGradient(nextGradient);\n    },\n    [applyGradient]\n  );\n\n  const handleThumbSelect = useCallback(\n    (index: number, _stop: GradientStop) => {\n      setSelectedStopIndex(index);\n    },\n    [setSelectedStopIndex]\n  );\n\n  const previewGradientForTrack = useMemo<ParsedLinearGradient>(() => {\n    const previewGradient: ParsedLinearGradient = {\n      type: \"linear\",\n      angle: leftToRightAngle,\n      stops: computedGradientForPicker.stops,\n    };\n    if (computedGradientForPicker.repeating) {\n      previewGradient.repeating = true;\n    }\n    return previewGradient;\n  }, [computedGradientForPicker]);\n\n  return (\n    <Flex\n      direction=\"column\"\n      shrink={false}\n      gap=\"2\"\n      css={{ padding: theme.panel.padding }}\n    >\n      <GradientPicker\n        gradient={computedGradientForPicker}\n        backgroundImage={formatLinearGradient(previewGradientForTrack)}\n        type={gradientType}\n        onChange={handlePickerChange}\n        onChangeComplete={handlePickerChangeComplete}\n        onThumbSelect={handleThumbSelect}\n        selectedStopIndex={selectedStopIndex}\n      />\n      <GradientStopControls\n        gradient={gradient}\n        computedGradient={computedGradient}\n        selectedStopIndex={selectedStopIndex}\n        setSelectedStopIndex={setSelectedStopIndex}\n        hintOverrides={hintOverrides}\n        setHintOverrides={setHintOverrides}\n        applyGradient={applyGradient}\n      />\n    </Flex>\n  );\n};\n\ntype OtherGradientPropertiesSectionProps = {\n  gradient: ParsedGradient;\n  applyGradient: GradientEditorApplyFn;\n  isRepeating: boolean;\n  setIsRepeating: Dispatch<SetStateAction<boolean>>;\n};\n\nconst OtherGradientPropertiesSection = ({\n  gradient,\n  applyGradient,\n  isRepeating,\n  setIsRepeating,\n}: OtherGradientPropertiesSectionProps) => {\n  const isLinear = isLinearGradient(gradient);\n  const isConic = isConicGradient(gradient);\n  const isRadial = isRadialGradient(gradient);\n  const supportsAngle = isLinear || isConic;\n  const angleValue = supportsAngle ? gradient.angle : undefined;\n  const defaultAngle = getDefaultAngle(gradient);\n\n  const gradientTypeName = isLinear\n    ? \"linear-gradient\"\n    : isConic\n      ? \"conic-gradient\"\n      : \"radial-gradient\";\n  const repeatingGradientTypeName = `repeating-${gradientTypeName}`;\n\n  // Radial gradient size and shape\n  useEffect(() => {\n    if (isRadial === false) {\n      return;\n    }\n    const updates: Partial<ParsedRadialGradient> = {};\n    if (gradient.size === undefined) {\n      updates.size = defaultRadialSize;\n    }\n    const normalizedShape = gradient.shape?.value.toLowerCase();\n    if (normalizedShape !== \"circle\" && normalizedShape !== \"ellipse\") {\n      updates.shape = { type: \"keyword\", value: defaultRadialShape };\n    }\n    if (Object.keys(updates).length > 0) {\n      applyGradient({ ...gradient, ...updates });\n    }\n  }, [applyGradient, gradient, isRadial]);\n\n  const currentRadialSize = isRadial ? gradient.size : undefined;\n  const isPresetRadialSize =\n    typeof currentRadialSize === \"string\" &&\n    radialSizeOptionsSet.has(currentRadialSize as RadialSizeOption);\n  const radialSizeValue = isRadial\n    ? isPresetRadialSize\n      ? (currentRadialSize as RadialSizeOption)\n      : defaultRadialSize\n    : undefined;\n  const radialShapeValue = (() => {\n    if (isRadial && gradient.shape) {\n      const normalized = gradient.shape.value.toLowerCase();\n      if (normalized === \"circle\" || normalized === \"ellipse\") {\n        return normalized;\n      }\n    }\n    return isRadial ? defaultRadialShape : undefined;\n  })();\n\n  const handleRepeatChange = useCallback(\n    (value: string) => {\n      if (value !== \"repeat\" && value !== \"no-repeat\") {\n        return;\n      }\n      const repeating = value === \"repeat\";\n      setIsRepeating(repeating);\n      applyGradient({ ...gradient, repeating });\n    },\n    [applyGradient, gradient, setIsRepeating]\n  );\n\n  const handleAngleUpdate = useCallback(\n    (styleValue: StyleValue, options?: { isEphemeral?: boolean }) => {\n      if (supportsAngle === false) {\n        return;\n      }\n      const angleValue = resolveAngleValue(styleValue);\n      if (angleValue === undefined) {\n        return;\n      }\n      if (isLinear) {\n        applyGradient(\n          {\n            ...(gradient as ParsedLinearGradient),\n            angle: angleValue,\n            sideOrCorner: undefined,\n          },\n          options\n        );\n        return;\n      }\n\n      applyGradient(\n        {\n          ...(gradient as ParsedConicGradient),\n          angle: angleValue,\n        },\n        options\n      );\n    },\n    [applyGradient, gradient, isLinear, supportsAngle]\n  );\n\n  const handleAngleDelete = useCallback(\n    (options?: { isEphemeral?: boolean }) => {\n      if (supportsAngle === false) {\n        return;\n      }\n      const nextGradient: ParsedGradient = {\n        ...gradient,\n        angle: undefined,\n      };\n      applyGradient(nextGradient, options);\n    },\n    [applyGradient, gradient, supportsAngle]\n  );\n\n  const handleRadialSizeChange = useCallback(\n    (nextSize?: RadialSizeOption) => {\n      if (isRadial === false) {\n        return;\n      }\n      const size = nextSize ?? defaultRadialSize;\n      applyGradient({ ...gradient, size });\n    },\n    [applyGradient, gradient, isRadial]\n  );\n\n  const handleEndingShapeChange = useCallback(\n    (nextShape?: string) => {\n      if (isRadial === false) {\n        return;\n      }\n      const shapeValue =\n        nextShape === \"circle\" || nextShape === \"ellipse\"\n          ? nextShape\n          : defaultRadialShape;\n      applyGradient({\n        ...gradient,\n        shape: { type: \"keyword\", value: shapeValue },\n      });\n    },\n    [applyGradient, gradient, isRadial]\n  );\n\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <Grid gap=\"2\" columns={isRadial ? 3 : supportsAngle ? 2 : 1}>\n        {supportsAngle && (\n          <Flex direction=\"column\" gap=\"1\">\n            <PropertyInlineLabel\n              label=\"Angle\"\n              description=\"Direction of the gradient line. 0deg is up, 90deg is right, 180deg is down, 270deg is left.\"\n            />\n            <CssValueInputContainer\n              property=\"rotate\"\n              styleSource=\"default\"\n              getOptions={getAvailableUnitVariables}\n              value={angleValue ?? defaultAngle}\n              unitOptions={angleUnitOptions}\n              onUpdate={handleAngleUpdate}\n              onDelete={handleAngleDelete}\n            />\n          </Flex>\n        )}\n        {isRadial && (\n          <Flex direction=\"column\" gap=\"1\">\n            <PropertyInlineLabel\n              label=\"Size\"\n              description=\"Radial gradient size determining how far the gradient extends from its center.\"\n            />\n            <Select\n              options={radialSizeOptions}\n              value={radialSizeValue}\n              fullWidth\n              onChange={(size) => handleRadialSizeChange(size)}\n              getDescription={(option) => radialSizeDescriptions[option]}\n            />\n          </Flex>\n        )}\n        {isRadial && (\n          <Flex direction=\"column\" gap=\"1\">\n            <PropertyInlineLabel\n              label=\"Shape\"\n              description=\"Radial gradient ending shape.\"\n            />\n            <ToggleGroup\n              type=\"single\"\n              value={radialShapeValue}\n              aria-label=\"Radial ending shape\"\n              onValueChange={handleEndingShapeChange}\n            >\n              <Tooltip\n                variant=\"wrapped\"\n                content={radialShapeDescriptions.ellipse}\n              >\n                <ToggleGroupButton value=\"ellipse\" aria-label=\"Ellipse\">\n                  <EllipseIcon />\n                </ToggleGroupButton>\n              </Tooltip>\n              <Tooltip\n                variant=\"wrapped\"\n                content={radialShapeDescriptions.circle}\n              >\n                <ToggleGroupButton value=\"circle\" aria-label=\"Circle\">\n                  <CircleIcon />\n                </ToggleGroupButton>\n              </Tooltip>\n            </ToggleGroup>\n          </Flex>\n        )}\n        <Flex direction=\"column\" gap=\"1\">\n          <PropertyInlineLabel\n            label=\"Repeat\"\n            description=\"Whether to repeat the gradient pattern.\"\n          />\n          <ToggleGroup\n            type=\"single\"\n            value={isRepeating ? \"repeat\" : \"no-repeat\"}\n            aria-label=\"Gradient repeat\"\n            onValueChange={handleRepeatChange}\n          >\n            <Tooltip\n              variant=\"wrapped\"\n              content={`Render the gradient once (${gradientTypeName}).`}\n            >\n              <ToggleGroupButton value=\"no-repeat\" aria-label=\"No repeat\">\n                <XSmallIcon />\n              </ToggleGroupButton>\n            </Tooltip>\n            <Tooltip\n              variant=\"wrapped\"\n              content={`Repeat the gradient pattern (${repeatingGradientTypeName}).`}\n            >\n              <ToggleGroupButton value=\"repeat\" aria-label=\"Repeat\">\n                <RepeatGridIcon />\n              </ToggleGroupButton>\n            </Tooltip>\n          </ToggleGroup>\n        </Flex>\n      </Grid>\n    </Flex>\n  );\n};\n\ntype SolidColorControlsProps = {\n  gradient: ParsedGradient;\n  applyGradient: GradientEditorApplyFn;\n};\n\nconst SolidColorControls = ({\n  gradient,\n  applyGradient,\n}: SolidColorControlsProps) => {\n  const solidColor: StyleValue = (gradient.stops[0]?.color ??\n    fallbackStopColor) as StyleValue;\n\n  const applySolidColorValue = useCallback(\n    (\n      styleValue: StyleValue | IntermediateColorValue | undefined,\n      options?: { isEphemeral?: boolean }\n    ) => {\n      const nextColor = styleValueToColor(styleValue);\n      if (nextColor === undefined) {\n        return;\n      }\n      const baseGradient = isLinearGradient(gradient) ? gradient : undefined;\n      const nextGradient = createSolidLinearGradient(nextColor, baseGradient);\n      applyGradient(nextGradient, options);\n    },\n    [applyGradient, gradient]\n  );\n\n  const handleColorChange = useCallback(\n    (styleValue: StyleValue | IntermediateColorValue | undefined) => {\n      applySolidColorValue(styleValue);\n    },\n    [applySolidColorValue]\n  );\n\n  const handleColorChangeComplete = useCallback(\n    (styleValue: StyleValue) => {\n      applySolidColorValue(styleValue, { isEphemeral: false });\n    },\n    [applySolidColorValue]\n  );\n\n  return (\n    <Grid gap=\"2\" columns=\"3\" align=\"end\">\n      <PropertyInlineLabel\n        label=\"Color\"\n        description=\"The solid color for this background layer. Renders as a linear gradient with the same color at 0% and 100%.\"\n      />\n      <Flex css={{ gridColumn: \"span 2\" }}>\n        <ColorPickerControl\n          property=\"color\"\n          value={solidColor}\n          currentColor={solidColor}\n          onChange={handleColorChange}\n          onChangeComplete={handleColorChangeComplete}\n          onAbort={() => {}}\n          onReset={() => {}}\n        />\n      </Flex>\n    </Grid>\n  );\n};\n\ntype GradientStopControlsProps = {\n  gradient: ParsedGradient;\n  computedGradient: ParsedGradient;\n  selectedStopIndex: number;\n  setSelectedStopIndex: Dispatch<SetStateAction<number>>;\n  hintOverrides: Map<number, PercentUnitValue>;\n  setHintOverrides: Dispatch<SetStateAction<Map<number, PercentUnitValue>>>;\n  applyGradient: GradientEditorApplyFn;\n};\n\nconst GradientStopControls = ({\n  gradient,\n  computedGradient,\n  selectedStopIndex,\n  setSelectedStopIndex,\n  hintOverrides,\n  setHintOverrides,\n  applyGradient,\n}: GradientStopControlsProps) => {\n  const reverseDisabled = gradient.stops.length <= 1;\n\n  const handleReverseStops = useCallback(() => {\n    const resolution = resolveReverseStops(gradient, selectedStopIndex);\n    if (resolution.type === \"none\") {\n      return;\n    }\n    setSelectedStopIndex(resolution.selectedStopIndex);\n    applyGradient(resolution.gradient);\n  }, [applyGradient, gradient, selectedStopIndex, setSelectedStopIndex]);\n\n  const updateStop = useCallback(\n    (\n      stopIndex: number,\n      updater: (stop: GradientStop) => GradientStop,\n      options?: { isEphemeral?: boolean }\n    ) => {\n      const nextGradient = updateGradientStop(gradient, stopIndex, updater);\n      applyGradient(nextGradient, options);\n    },\n    [applyGradient, gradient]\n  );\n\n  const handleAddStop = useCallback(() => {\n    // Calculate position for new stop between selected stop and adjacent stop\n    const clampedSelectedIndex = clampStopIndex(selectedStopIndex, gradient);\n    const currentStop = gradient.stops[clampedSelectedIndex];\n    const currentPosition = getStopPosition(currentStop);\n\n    // If last stop is selected, insert between it and the previous stop\n    const isLastStop = clampedSelectedIndex === gradient.stops.length - 1;\n\n    let newPosition: number;\n    if (isLastStop && gradient.stops.length > 1) {\n      const prevStop = gradient.stops[clampedSelectedIndex - 1];\n      const prevPosition = getStopPosition(prevStop);\n      newPosition = (prevPosition + currentPosition) / 2;\n    } else {\n      // Otherwise, insert between current and next stop\n      const nextStop = gradient.stops[clampedSelectedIndex + 1];\n      const nextPosition = nextStop ? getStopPosition(nextStop) : 100;\n      newPosition = (currentPosition + nextPosition) / 2;\n    }\n\n    applyGradient({\n      ...gradient,\n      stops: [\n        ...gradient.stops,\n        {\n          color: fallbackStopColor,\n          position: createPercentUnitValue(newPosition),\n        },\n      ],\n    });\n  }, [applyGradient, gradient, selectedStopIndex]);\n\n  const handleDeleteStop = useCallback(\n    (stopIndex: number) => {\n      if (gradient.stops.length <= 2) {\n        return;\n      }\n      applyGradient({\n        ...gradient,\n        stops: gradient.stops.filter((_, index) => index !== stopIndex),\n      });\n      setHintOverrides((previous) => reindexHintOverrides(previous, stopIndex));\n    },\n    [applyGradient, gradient, setHintOverrides]\n  );\n\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <Flex align=\"center\" justify=\"between\">\n        <PropertyInlineLabel\n          label=\"Stops\"\n          description=\"Gradient color stops and their positions along the gradient line.\"\n        />\n        <Flex gap=\"1\">\n          <Tooltip\n            variant=\"wrapped\"\n            content=\"Reverse the order of all gradient stops.\"\n          >\n            <IconButton\n              aria-label=\"Reverse gradient stops\"\n              onClick={handleReverseStops}\n              disabled={reverseDisabled}\n            >\n              <ArrowRightLeftIcon />\n            </IconButton>\n          </Tooltip>\n          <Tooltip content=\"Add gradient stop\" variant=\"wrapped\">\n            <IconButton aria-label=\"Add stop\" onClick={handleAddStop}>\n              <PlusIcon />\n            </IconButton>\n          </Tooltip>\n        </Flex>\n      </Flex>\n      {gradient.stops.map((stop, stopIndex) => {\n        const isSelected =\n          stopIndex === clampStopIndex(selectedStopIndex, gradient);\n        const stopPositionValue = stop.position;\n        const stopHintOverride = hintOverrides.get(stopIndex);\n        const stopHintValue = stop.hint ?? stopHintOverride;\n\n        const handleStopPositionUpdate = (\n          styleValue: StyleValue,\n          options?: { isEphemeral?: boolean }\n        ) => {\n          const resolution = resolveStopPositionUpdate(styleValue);\n          if (resolution.type === \"none\") {\n            return;\n          }\n\n          updateStop(\n            stopIndex,\n            (stop) => ({\n              ...stop,\n              position: resolution.position,\n            }),\n            options\n          );\n\n          if (!options?.isEphemeral && resolution.clearHintOverrides) {\n            setHintOverrides((previous) =>\n              removeHintOverride(previous, stopIndex)\n            );\n          }\n        };\n\n        const handleStopPositionDelete = (options?: {\n          isEphemeral?: boolean;\n        }) => {\n          updateStop(\n            stopIndex,\n            (stop) => {\n              const { position: _omit, ...rest } = stop;\n              return rest;\n            },\n            options\n          );\n        };\n\n        const handleStopHintUpdate = (\n          styleValue: StyleValue,\n          options?: { isEphemeral?: boolean }\n        ) => {\n          const resolution = resolveStopHintUpdate(styleValue);\n\n          if (resolution.type === \"none\") {\n            return;\n          }\n\n          updateStop(\n            stopIndex,\n            (stop) => ({\n              ...stop,\n              hint: resolution.hint,\n            }),\n            options\n          );\n\n          if (options?.isEphemeral) {\n            return;\n          }\n\n          if (resolution.clearOverride) {\n            setHintOverrides((previous) =>\n              removeHintOverride(previous, stopIndex)\n            );\n            return;\n          }\n\n          const override = resolution.override;\n          if (override !== undefined) {\n            setHintOverrides((previous) =>\n              setHintOverride(previous, stopIndex, override)\n            );\n          }\n        };\n\n        const handleStopHintDelete = (options?: { isEphemeral?: boolean }) => {\n          updateStop(\n            stopIndex,\n            (stop) => {\n              const { hint: _omit, ...rest } = stop;\n              return { ...rest };\n            },\n            options\n          );\n          if (!options?.isEphemeral) {\n            setHintOverrides((previous) =>\n              removeHintOverride(previous, stopIndex)\n            );\n          }\n        };\n\n        const handleStopColorChange = (\n          styleValue: StyleValue | IntermediateColorValue | undefined\n        ) => {\n          const nextColor = styleValueToColor(styleValue);\n          if (nextColor === undefined) {\n            return;\n          }\n          updateStop(\n            stopIndex,\n            (stop) => ({\n              ...stop,\n              color: nextColor,\n            }),\n            { isEphemeral: true }\n          );\n        };\n\n        const handleStopColorChangeComplete = (styleValue: StyleValue) => {\n          const nextColor = styleValueToColor(styleValue);\n          if (nextColor === undefined) {\n            return;\n          }\n          updateStop(\n            stopIndex,\n            (stop) => ({\n              ...stop,\n              color: nextColor,\n            }),\n            { isEphemeral: false }\n          );\n        };\n\n        const stopColor = (stop.color ?? fallbackStopColor) as StyleValue;\n        // Use computed color for display (resolves CSS variables)\n        const computedStop = computedGradient.stops[stopIndex];\n        const currentColor = (computedStop?.color ?? stopColor) as StyleValue;\n\n        return (\n          <Flex\n            key={stopIndex}\n            gap=\"1\"\n            css={{\n              opacity: isSelected ? 1 : 0.6,\n            }}\n            onFocus={() => {\n              if (!isSelected) {\n                setSelectedStopIndex(stopIndex);\n              }\n            }}\n          >\n            <Grid\n              align=\"end\"\n              gap=\"1\"\n              css={{ gridTemplateColumns: \"1fr 1fr 2fr\" }}\n            >\n              <Tooltip\n                content=\"Position of this gradient stop along the gradient line.\"\n                variant=\"wrapped\"\n              >\n                <Box>\n                  <CssValueInputContainer\n                    property={\"background-position-x\"}\n                    styleSource=\"default\"\n                    getOptions={getAvailableUnitVariables}\n                    value={stopPositionValue}\n                    unitOptions={percentUnitOptions}\n                    onUpdate={handleStopPositionUpdate}\n                    onDelete={handleStopPositionDelete}\n                  />\n                </Box>\n              </Tooltip>\n              <Tooltip\n                content=\"Midpoint position for color transition between this stop and the next.\"\n                variant=\"wrapped\"\n              >\n                <Box>\n                  <CssValueInputContainer\n                    property={\"background-position-x\"}\n                    styleSource=\"default\"\n                    getOptions={getAvailableUnitVariables}\n                    value={stopHintValue}\n                    unitOptions={percentUnitOptions}\n                    onUpdate={handleStopHintUpdate}\n                    onDelete={handleStopHintDelete}\n                  />\n                </Box>\n              </Tooltip>\n              <Tooltip content=\"Color of this gradient stop.\" variant=\"wrapped\">\n                <Box>\n                  <ColorPickerControl\n                    property=\"color\"\n                    value={stopColor}\n                    currentColor={currentColor}\n                    onChange={handleStopColorChange}\n                    onChangeComplete={handleStopColorChangeComplete}\n                    onAbort={() => {}}\n                    onReset={() => {}}\n                  />\n                </Box>\n              </Tooltip>\n            </Grid>\n            <Tooltip content=\"Delete stop\" variant=\"wrapped\">\n              <IconButton\n                aria-label=\"Delete stop\"\n                onClick={() => handleDeleteStop(stopIndex)}\n                disabled={gradient.stops.length <= 2}\n              >\n                <MinusIcon />\n              </IconButton>\n            </Tooltip>\n          </Flex>\n        );\n      })}\n    </Flex>\n  );\n};\n\ntype GradientPositionControlsProps = {\n  gradient: ParsedGradient;\n  applyGradient: GradientEditorApplyFn;\n};\n\nconst GradientPositionControls = ({\n  gradient,\n  applyGradient,\n}: GradientPositionControlsProps) => {\n  const supportsPosition =\n    isRadialGradient(gradient) || isConicGradient(gradient);\n\n  const gradientWithPosition = supportsPosition\n    ? (gradient as ParsedRadialGradient | ParsedConicGradient)\n    : undefined;\n\n  const positionValue = gradientWithPosition?.position;\n\n  // Parse position values directly without memoization to ensure the grid\n  // indicator updates immediately after position changes\n  const parsedValues = positionValue\n    ? parseGradientPositionValues(positionValue)\n    : { xValue: undefined, yValue: undefined };\n\n  // Extract single values from layers type if needed\n  const xValue =\n    parsedValues.xValue?.type === \"layers\"\n      ? parsedValues.xValue.value[0]\n      : parsedValues.xValue;\n  const yValue =\n    parsedValues.yValue?.type === \"layers\"\n      ? parsedValues.yValue.value[0]\n      : parsedValues.yValue;\n\n  const updatePosition = useCallback(\n    (\n      nextX: StyleValue | undefined,\n      nextY: StyleValue | undefined,\n      options?: { isEphemeral?: boolean }\n    ) => {\n      if (gradientWithPosition === undefined) {\n        return;\n      }\n      const position = formatGradientPositionValues(nextX, nextY);\n      applyGradient(\n        {\n          ...gradientWithPosition,\n          position,\n        },\n        options\n      );\n    },\n    [applyGradient, gradientWithPosition]\n  );\n\n  const handleAxisUpdate = useCallback(\n    (axis: \"x\" | \"y\") =>\n      (styleValue: StyleValue, options?: { isEphemeral?: boolean }) => {\n        updatePosition(\n          axis === \"x\" ? styleValue : xValue,\n          axis === \"y\" ? styleValue : yValue,\n          options\n        );\n      },\n    [updatePosition, xValue, yValue]\n  );\n\n  const handleAxisDelete = useCallback(\n    (axis: \"x\" | \"y\") => (options?: { isEphemeral?: boolean }) => {\n      updatePosition(\n        axis === \"x\" ? undefined : xValue,\n        axis === \"y\" ? undefined : yValue,\n        options\n      );\n    },\n    [updatePosition, xValue, yValue]\n  );\n\n  const handleGridSelect = useCallback(\n    ({ x, y }: { x: number; y: number }) => {\n      updatePosition(createPercentUnitValue(x), createPercentUnitValue(y));\n    },\n    [updatePosition]\n  );\n\n  if (supportsPosition === false || gradientWithPosition === undefined) {\n    return;\n  }\n\n  return (\n    <BackgroundPositionControl\n      label=\"Position\"\n      xAxis={{\n        label: \"Left\",\n        description: \"Left position offset\",\n        property: \"--gradient-position-x\",\n        value: xValue,\n        getOptions: () => gradientPositionXOptions,\n        unitOptions: percentUnitOptions,\n        onUpdate: handleAxisUpdate(\"x\"),\n        onDelete: handleAxisDelete(\"x\"),\n      }}\n      yAxis={{\n        label: \"Top\",\n        description: \"Top position offset\",\n        property: \"--gradient-position-y\",\n        value: yValue,\n        getOptions: () => gradientPositionYOptions,\n        unitOptions: percentUnitOptions,\n        onUpdate: handleAxisUpdate(\"y\"),\n        onDelete: handleAxisDelete(\"y\"),\n      }}\n      onSelect={handleGridSelect}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-image.tsx",
    "content": "import type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { Flex, theme, Grid, Box } from \"@webstudio-is/design-system\";\nimport { useRef, useCallback } from \"react\";\nimport { ImageControl } from \"../../controls\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { BackgroundCodeEditor } from \"./background-code-editor\";\nimport { $assets } from \"~/shared/sync/data-stores\";\nimport { isAbsoluteUrl } from \"@webstudio-is/sdk\";\n\nexport const BackgroundImage = ({ index }: { index: number }) => {\n  const elementRef = useRef<HTMLDivElement>(null);\n\n  const handleValidate = useCallback(\n    (_value: string, parsed: Map<string, StyleValue>) => {\n      const newValue = parsed.get(\"background-image\");\n\n      if (newValue === undefined || newValue?.type === \"invalid\") {\n        return;\n      }\n\n      const [layer] = newValue.type === \"layers\" ? newValue.value : [newValue];\n\n      // Only validate image URLs, not keywords or other types\n      if (layer?.type !== \"image\" || layer.value.type !== \"url\") {\n        return;\n      }\n\n      const url = layer.value.url;\n\n      // If it's an absolute URL, no validation needed\n      if (isAbsoluteUrl(url)) {\n        return;\n      }\n\n      // Check if the asset exists in the project\n      const usedAsset = Array.from($assets.get().values()).find(\n        (asset) => asset.type === \"image\" && asset.name === url\n      );\n\n      if (usedAsset === undefined) {\n        return [`Asset ${url} is not found in project`];\n      }\n\n      return;\n    },\n    []\n  );\n\n  return (\n    <Flex\n      direction=\"column\"\n      gap={1}\n      css={{ padding: theme.panel.padding }}\n      ref={elementRef}\n    >\n      <Grid gap=\"2\" columns=\"3\" align=\"start\">\n        <PropertyInlineLabel\n          label=\"Image\"\n          description={propertyDescriptions.backgroundImage}\n        />\n        <Box\n          css={{ gridColumn: \"span 2\" }}\n          ref={elementRef}\n          data-floating-panel-container\n        >\n          <ImageControl property=\"background-image\" index={index} />\n        </Box>\n      </Grid>\n      <BackgroundCodeEditor index={index} onValidate={handleValidate} />\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-position.tsx",
    "content": "import type {\n  CssProperty,\n  KeywordValue,\n  StyleValue,\n  VarValue,\n} from \"@webstudio-is/css-engine\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { Flex, Grid, PositionGrid } from \"@webstudio-is/design-system\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport { PropertyInlineLabel, PropertyLabel } from \"../../property-label\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport {\n  getRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\nimport type { UnitOption } from \"../../shared/css-value-input/unit-select\";\nimport type { SetValue, StyleUpdateOptions } from \"../../shared/use-style-data\";\n\nconst keyworkToValue: Record<string, number> = {\n  left: 0,\n  right: 100,\n  center: 50,\n  top: 0,\n  bottom: 100,\n};\n\nexport const calculateBackgroundPosition = (value: undefined | StyleValue) => {\n  if (value?.type === \"unit\") {\n    return value.value;\n  }\n  if (value?.type === \"keyword\") {\n    return keyworkToValue[value.value];\n  }\n  return 0;\n};\n\ntype AxisOption =\n  | KeywordValue\n  | VarValue\n  | (KeywordValue & { description?: string });\n\ntype AxisControlProps = {\n  label: string;\n  description: string;\n  property: CssProperty;\n  properties?: [CssProperty, ...CssProperty[]];\n  value: StyleValue | undefined;\n  getOptions: () => Array<AxisOption>;\n  unitOptions?: UnitOption[];\n  onUpdate: SetValue;\n  onDelete: (options?: StyleUpdateOptions) => void;\n};\n\nexport const BackgroundPositionControl = ({\n  label = \"Position\",\n  description = propertyDescriptions.backgroundPosition,\n  xAxis,\n  yAxis,\n  onSelect,\n}: {\n  label?: string;\n  description?: string;\n  xAxis: AxisControlProps;\n  yAxis: AxisControlProps;\n  onSelect: (position: { x: number; y: number }) => void;\n}) => {\n  const combinedProperties = (() => {\n    if (xAxis.properties && yAxis.properties) {\n      return [...xAxis.properties, ...yAxis.properties] as [\n        CssProperty,\n        ...CssProperty[],\n      ];\n    }\n    if (xAxis.properties) {\n      return xAxis.properties;\n    }\n    if (yAxis.properties) {\n      return yAxis.properties;\n    }\n  })();\n\n  return (\n    <Flex direction=\"column\" gap=\"1\">\n      {combinedProperties ? (\n        <PropertyLabel\n          label={label}\n          description={description}\n          properties={combinedProperties}\n        />\n      ) : (\n        <PropertyInlineLabel label={label} description={description} />\n      )}\n      <Grid gap=\"2\" columns={2}>\n        <PositionGrid\n          selectedPosition={{\n            x: calculateBackgroundPosition(xAxis.value),\n            y: calculateBackgroundPosition(yAxis.value),\n          }}\n          onSelect={onSelect}\n        />\n        <Grid\n          css={{ gridTemplateColumns: \"max-content 1fr\" }}\n          align=\"center\"\n          gapX=\"2\"\n        >\n          {xAxis.properties ? (\n            <PropertyLabel\n              label={xAxis.label}\n              description={xAxis.description}\n              properties={xAxis.properties}\n            />\n          ) : (\n            <PropertyInlineLabel\n              label={xAxis.label}\n              description={xAxis.description}\n            />\n          )}\n          <CssValueInputContainer\n            property={xAxis.property}\n            styleSource=\"default\"\n            getOptions={xAxis.getOptions}\n            unitOptions={xAxis.unitOptions}\n            value={xAxis.value}\n            onUpdate={xAxis.onUpdate}\n            onDelete={xAxis.onDelete}\n          />\n          {yAxis.properties ? (\n            <PropertyLabel\n              label={yAxis.label}\n              description={yAxis.description}\n              properties={yAxis.properties}\n            />\n          ) : (\n            <PropertyInlineLabel\n              label={yAxis.label}\n              description={yAxis.description}\n            />\n          )}\n          <CssValueInputContainer\n            property={yAxis.property}\n            styleSource=\"default\"\n            getOptions={yAxis.getOptions}\n            unitOptions={yAxis.unitOptions}\n            value={yAxis.value}\n            onUpdate={yAxis.onUpdate}\n            onDelete={yAxis.onDelete}\n          />\n        </Grid>\n      </Grid>\n    </Flex>\n  );\n};\n\nexport const BackgroundPosition = ({ index }: { index: number }) => {\n  const [backgroundPositionX, backgroundPositionY] = useComputedStyles([\n    \"background-position-x\",\n    \"background-position-y\",\n  ]);\n  const xValue = getRepeatedStyleItem(backgroundPositionX, index);\n  const yValue = getRepeatedStyleItem(backgroundPositionY, index);\n  const setValueX: SetValue = (value, options) => {\n    setRepeatedStyleItem(backgroundPositionX, index, value, options);\n  };\n  const setValueY: SetValue = (value, options) => {\n    setRepeatedStyleItem(backgroundPositionY, index, value, options);\n  };\n  const resetValue = (\n    axisValue: StyleValue | undefined,\n    setValue: SetValue,\n    options?: StyleUpdateOptions\n  ) => {\n    if (axisValue) {\n      setValue(axisValue, options);\n    }\n  };\n\n  return (\n    <BackgroundPositionControl\n      xAxis={{\n        label: \"Left\",\n        description: \"Left position offset\",\n        property: \"background-position-x\",\n        properties: [\"background-position-x\"],\n        value: xValue,\n        getOptions: () => [\n          { type: \"keyword\", value: \"center\" },\n          { type: \"keyword\", value: \"left\" },\n          { type: \"keyword\", value: \"right\" },\n        ],\n        onUpdate: setValueX,\n        onDelete: (options) => resetValue(xValue, setValueX, options),\n      }}\n      yAxis={{\n        label: \"Top\",\n        description: \"Top position offset\",\n        property: \"background-position-y\",\n        properties: [\"background-position-y\"],\n        value: yValue,\n        getOptions: () => [\n          { type: \"keyword\", value: \"center\" },\n          { type: \"keyword\", value: \"top\" },\n          { type: \"keyword\", value: \"bottom\" },\n        ],\n        onUpdate: setValueY,\n        onDelete: (options) => resetValue(yValue, setValueY, options),\n      }}\n      onSelect={({ x, y }) => {\n        setRepeatedStyleItem(backgroundPositionX, index, {\n          type: \"unit\",\n          value: x,\n          unit: \"%\",\n        });\n        setRepeatedStyleItem(backgroundPositionY, index, {\n          type: \"unit\",\n          value: y,\n          unit: \"%\",\n        });\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-size.test.ts",
    "content": "import { expect, test, describe } from \"vitest\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { __testing__ } from \"./background-size\";\n\nconst { getSelectValue } = __testing__;\n\ndescribe(\"background-size utilities\", () => {\n  describe(\"getSelectValue\", () => {\n    test(\"returns keyword value when styleValue is a keyword\", () => {\n      const styleValue: StyleValue = {\n        type: \"keyword\",\n        value: \"cover\",\n      };\n      expect(getSelectValue(styleValue)).toBe(\"cover\");\n    });\n\n    test(\"returns 'custom' when styleValue is a tuple\", () => {\n      const styleValue: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"keyword\", value: \"auto\" },\n          { type: \"keyword\", value: \"auto\" },\n        ],\n      };\n      expect(getSelectValue(styleValue)).toBe(\"custom\");\n    });\n\n    test(\"returns 'custom' when styleValue is a tuple with unit values\", () => {\n      const styleValue: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"unit\", value: 100, unit: \"px\" },\n          { type: \"unit\", value: 200, unit: \"px\" },\n        ],\n      };\n      expect(getSelectValue(styleValue)).toBe(\"custom\");\n    });\n\n    test(\"returns 'auto' when styleValue is undefined\", () => {\n      expect(getSelectValue(undefined)).toBe(\"auto\");\n    });\n\n    test(\"returns 'auto' for unsupported style value types\", () => {\n      const styleValue: StyleValue = {\n        type: \"invalid\",\n        value: \"something\",\n      } as StyleValue;\n      expect(getSelectValue(styleValue)).toBe(\"auto\");\n    });\n\n    test(\"tuple type always returns 'custom' to show width/height inputs\", () => {\n      // This is the critical test - tuple values must always return \"custom\"\n      // to ensure the width/height inputs are visible\n      const tupleValue: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"unit\", value: 50, unit: \"%\" },\n          { type: \"keyword\", value: \"auto\" },\n        ],\n      };\n\n      const selectValue = getSelectValue(tupleValue);\n\n      // This assertion prevents the bug where tuple values were treated as \"auto\"\n      expect(selectValue).toBe(\"custom\");\n      expect(selectValue).not.toBe(\"auto\");\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-size.tsx",
    "content": "import { Grid, Select, theme } from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { keywordValues, propertyDescriptions } from \"@webstudio-is/css-data\";\nimport {\n  type StyleValue,\n  TupleValue,\n  TupleValueItem,\n} from \"@webstudio-is/css-engine\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport type { SetValue } from \"../../shared/use-style-data\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport {\n  getRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\n\nconst autoKeyword = { type: \"keyword\" as const, value: \"auto\" };\n\n/**\n * Determines the select value based on style value type.\n * Returns \"custom\" for tuple types to show width/height inputs.\n */\nconst getSelectValue = (styleValue: StyleValue | undefined): string => {\n  if (styleValue?.type === \"keyword\") {\n    return toValue(styleValue);\n  }\n  if (styleValue?.type === \"tuple\") {\n    return \"custom\";\n  }\n  return \"auto\";\n};\n\nconst toTuple = (\n  valueX?: StyleValue | string,\n  valueY?: StyleValue | string\n) => {\n  const parsedValue = TupleValue.safeParse(valueX);\n  if (parsedValue.success) {\n    return parsedValue.data;\n  }\n\n  const parsedValueX = valueX ? TupleValueItem.parse(valueX) : autoKeyword;\n  const parsedValueY = valueY ? TupleValueItem.parse(valueY) : parsedValueX;\n\n  return {\n    type: \"tuple\" as const,\n    value: [parsedValueX, parsedValueY],\n  };\n};\n\nexport const BackgroundSize = ({ index }: { index: number }) => {\n  const property = \"background-size\";\n  const styleDecl = useComputedStyleDecl(property);\n  const styleValue = getRepeatedStyleItem(styleDecl, index);\n\n  const selectOptions = [...keywordValues[property], \"custom\"];\n  const selectValue = getSelectValue(styleValue);\n\n  const customSizeOptions = [autoKeyword];\n  const customSizeValue = toTuple(styleValue);\n\n  const setValue: SetValue = (value, options) => {\n    setRepeatedStyleItem(styleDecl, index, value, options);\n  };\n\n  const setValueX: SetValue = (value, options) => {\n    const [x] = value.type === \"layers\" ? value.value : [value];\n    const nextValue = toTuple(x, customSizeValue.value[1]);\n    setValue(nextValue, options);\n  };\n\n  const setValueY: SetValue = (value, options) => {\n    const [y] = value.type === \"layers\" ? value.value : [value];\n    const nextValue = toTuple(customSizeValue.value[0], y);\n    setValue(nextValue, options);\n  };\n\n  return (\n    <>\n      <Grid columns={2} align=\"center\" gap={2}>\n        <PropertyLabel\n          label=\"Size\"\n          description={propertyDescriptions.backgroundSize}\n          properties={[property]}\n        />\n\n        <Select\n          // show empty field instead of radix placeholder\n          // like css value input does\n          placeholder=\"\"\n          options={selectOptions}\n          value={selectValue}\n          onChange={(name: string) => {\n            if (name === \"custom\") {\n              setValue({ type: \"tuple\", value: [autoKeyword, autoKeyword] });\n            } else {\n              setValue({ type: \"keyword\", value: name });\n            }\n          }}\n          onItemHighlight={(name) => {\n            // No need to preview custom size as it needs additional user input.\n            if (name === \"custom\") {\n              return;\n            }\n            // Remove preview when mouse leaves the item.\n            if (name === undefined) {\n              if (styleValue !== undefined) {\n                setValue(styleValue, { isEphemeral: true });\n              }\n              return;\n            }\n            // Preview on mouse enter or focus.\n            setValue({ type: \"keyword\", value: name }, { isEphemeral: true });\n          }}\n          onOpenChange={(isOpen) => {\n            // Remove ephemeral changes when closing the menu.\n            if (isOpen === false && styleValue !== undefined) {\n              setValue(styleValue, { isEphemeral: true });\n            }\n          }}\n          getItemProps={() => ({ text: \"sentence\" })}\n        />\n      </Grid>\n\n      {selectValue === \"custom\" && (\n        <Grid\n          css={{ mt: theme.spacing[4] }}\n          align=\"center\"\n          columns={2}\n          gapX={2}\n          gapY={1}\n        >\n          <PropertyLabel\n            properties={[\"background-size\"]}\n            label=\"Width\"\n            description=\"The width of the background image.\"\n          />\n\n          <PropertyLabel\n            properties={[\"background-size\"]}\n            label=\"Height\"\n            description=\"The height of the background image.\"\n          />\n\n          <CssValueInputContainer\n            property={property}\n            styleSource=\"default\"\n            getOptions={() => customSizeOptions}\n            value={customSizeValue.value[0]}\n            onUpdate={setValueX}\n            onDelete={() => {}}\n          />\n\n          <CssValueInputContainer\n            property={property}\n            styleSource=\"default\"\n            getOptions={() => customSizeOptions}\n            value={customSizeValue.value[1]}\n            onUpdate={setValueY}\n            onDelete={() => {}}\n          />\n        </Grid>\n      )}\n    </>\n  );\n};\n\nexport const __testing__ = {\n  getSelectValue,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/background-thumbnail.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport type { Assets } from \"@webstudio-is/sdk\";\nimport { Image as WebstudioImage, wsImageLoader } from \"@webstudio-is/image\";\nimport { styled, theme } from \"@webstudio-is/design-system\";\nimport {\n  StyleValue,\n  toValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport { $assets } from \"~/shared/sync/data-stores\";\nimport brokenImage from \"~/shared/images/broken-image-placeholder.svg\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport {\n  getComputedRepeatedItem,\n  getRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\nimport { formatAssetName } from \"~/builder/shared/assets/asset-utils\";\nimport { parseAnyGradient, isSolidLinearGradient } from \"./gradient-utils\";\n\nexport const repeatedProperties = [\n  \"background-image\",\n  \"background-attachment\",\n  \"background-clip\",\n  \"background-origin\",\n  \"background-position-x\",\n  \"background-position-y\",\n  \"background-repeat\",\n  \"background-size\",\n  \"background-blend-mode\",\n] satisfies [CssProperty, ...CssProperty[]];\n\nconst thumbSize = theme.spacing[9];\n\nconst Thumbnail = styled(\"div\", {\n  borderRadius: 2,\n  borderWidth: 0,\n  width: thumbSize,\n  height: thumbSize,\n});\n\nconst NoneThumbnail = styled(Thumbnail, {\n  background:\n    \"repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%\",\n});\n\nconst StyledWebstudioImage = styled(WebstudioImage, {\n  position: \"relative\",\n  width: thumbSize,\n  height: thumbSize,\n  objectFit: \"contain\",\n\n  // This is shown only if an image was not loaded and broken\n  // From the spec:\n  // - The pseudo-elements generated by ::before and ::after are contained by the element's formatting box,\n  //   and thus don't apply to \"replaced\" elements such as <img>, or to <br> elements\n  // Not in spec but supported by all browsers:\n  // - broken image is not a \"replaced\" element so this style is applied\n  \"&::after\": {\n    content: \"' '\",\n    position: \"absolute\",\n    width: \"100%\",\n    height: \"100%\",\n    left: 0,\n    top: 0,\n    right: 0,\n    bottom: 0,\n    backgroundSize: \"contain\",\n    backgroundRepeat: \"no-repeat\",\n    backgroundPosition: \"center\",\n    backgroundImage: `url(${brokenImage})`,\n  },\n});\n\nconst gradientNames = [\n  \"conic-gradient\",\n  \"linear-gradient\",\n  \"radial-gradient\",\n  \"repeating-conic-gradient\",\n  \"repeating-linear-gradient\",\n  \"repeating-radial-gradient\",\n];\n\nexport const getBackgroundLabel = (\n  backgroundImageStyle: undefined | StyleValue,\n  assets: Assets\n) => {\n  if (backgroundImageStyle?.type === \"var\") {\n    return `--${backgroundImageStyle.value}`;\n  }\n  if (\n    backgroundImageStyle?.type === \"image\" &&\n    backgroundImageStyle.value.type === \"asset\"\n  ) {\n    const asset = assets.get(backgroundImageStyle.value.value);\n    if (asset) {\n      return formatAssetName(asset);\n    }\n  }\n\n  if (\n    backgroundImageStyle?.type === \"image\" &&\n    backgroundImageStyle.value.type === \"url\"\n  ) {\n    return backgroundImageStyle.value.url;\n  }\n\n  if (backgroundImageStyle?.type === \"unparsed\") {\n    const value = backgroundImageStyle.value;\n\n    // Check if it's a solid color gradient using cached parsing\n    const parsed = parseAnyGradient(value);\n    if (parsed?.type === \"linear\" && isSolidLinearGradient(parsed)) {\n      return \"Solid\";\n    }\n\n    const gradientName = gradientNames.find((name) => value.includes(name));\n    return gradientName ? humanizeString(gradientName) : \"Gradient\";\n  }\n\n  return \"None\";\n};\n\ntype RepeatedProperty = (typeof repeatedProperties)[number];\n\nexport const BackgroundThumbnail = ({ index }: { index: number }) => {\n  const assets = useStore($assets);\n  const styles = useComputedStyles(repeatedProperties);\n  const [backgroundImage] = styles;\n  // Use cascaded value to check for assets (before they're resolved to URLs)\n  const backgroundImageValue = getRepeatedStyleItem(backgroundImage, index);\n  // Use computed value for rendering gradients and other styles\n  const computedBackgroundImageValue = getComputedRepeatedItem(\n    backgroundImage,\n    index\n  );\n\n  if (\n    backgroundImageValue?.type === \"image\" &&\n    backgroundImageValue.value.type === \"asset\"\n  ) {\n    const asset = assets.get(backgroundImageValue.value.value);\n    if (asset === undefined) {\n      return;\n    }\n    return (\n      <StyledWebstudioImage\n        key={asset.id}\n        loader={wsImageLoader}\n        src={asset.name}\n        width={thumbSize}\n        optimize={true}\n      />\n    );\n  }\n\n  if (\n    backgroundImageValue?.type === \"image\" &&\n    backgroundImageValue.value.type === \"url\"\n  ) {\n    return (\n      <StyledWebstudioImage\n        key={backgroundImageValue.value.url}\n        loader={wsImageLoader}\n        src={backgroundImageValue.value.url}\n        width={thumbSize}\n        optimize={true}\n      />\n    );\n  }\n\n  if (computedBackgroundImageValue?.type === \"unparsed\") {\n    const cssStyle: { [property in RepeatedProperty]?: string } = {};\n    for (const styleDecl of styles) {\n      const itemValue = getComputedRepeatedItem(styleDecl, index);\n      cssStyle[styleDecl.property as RepeatedProperty] = toValue(itemValue);\n    }\n    return <Thumbnail css={cssStyle} />;\n  }\n\n  return <NoneThumbnail />;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/backgrounds.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport {\n  $breakpoints,\n  $instances,\n  $selectedBreakpointId,\n  $styles,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { Section as SectionComponent } from \"./backgrounds\";\nimport { $awareness } from \"~/shared/awareness\";\n\nconst backgroundImage: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"backgroundImage\",\n  value: {\n    type: \"layers\",\n    value: [\n      {\n        type: \"unparsed\",\n        value: \"linear-gradient(red, yellow)\",\n      },\n      {\n        type: \"unparsed\",\n        value: \"linear-gradient(blue, red)\",\n      },\n      {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    ],\n  },\n};\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styles.set(new Map([[getStyleDeclKey(backgroundImage), backgroundImage]]));\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$awareness.set({\n  pageId: \"\",\n  instanceSelector: [\"box\"],\n});\n\nconst SingleLayerVariant = () => {\n  useEffect(() => {\n    const singleBackground: StyleDecl = {\n      breakpointId: \"base\",\n      styleSourceId: \"local\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unparsed\",\n            value: \"linear-gradient(to right, red, orange)\",\n          },\n        ],\n      },\n    };\n    $styles.set(\n      new Map([[getStyleDeclKey(singleBackground), singleBackground]])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <SectionComponent />\n    </Box>\n  );\n};\n\nconst EmptyBackgroundsVariant = () => {\n  useEffect(() => {\n    $styles.set(new Map());\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <SectionComponent />\n    </Box>\n  );\n};\n\nexport const Backgrounds = () => (\n  <StorySection title=\"Backgrounds\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default (multiple layers)</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <SectionComponent />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Single layer</Text>\n        <SingleLayerVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Empty backgrounds</Text>\n        <EmptyBackgroundsVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Backgrounds/Section\",\n  component: SectionComponent,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/backgrounds.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { Flex, Grid, theme } from \"@webstudio-is/design-system\";\nimport { $assets } from \"~/shared/sync/data-stores\";\nimport { ColorControl } from \"../../controls/color/color-control\";\nimport { RepeatedStyleSection } from \"../../shared/style-section\";\nimport { PropertyLabel } from \"../../property-label\";\nimport {\n  addRepeatedStyleItem,\n  RepeatedStyle,\n} from \"../../shared/repeated-style\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\nimport { BackgroundContent } from \"./background-content\";\nimport {\n  getBackgroundLabel,\n  BackgroundThumbnail,\n  repeatedProperties,\n} from \"./background-thumbnail\";\n\nexport const properties = [\n  ...repeatedProperties,\n  \"background-color\",\n] satisfies [CssProperty, ...CssProperty[]];\n\nexport const Section = () => {\n  const styles = useComputedStyles(repeatedProperties);\n  const assets = useStore($assets);\n\n  return (\n    <RepeatedStyleSection\n      label=\"Backgrounds\"\n      description=\"Add one or more backgrounds to the instance such as a color, image, or gradient.\"\n      properties={properties}\n      onAdd={() => {\n        addRepeatedStyleItem(\n          styles,\n          parseCssFragment(\"none\", [\"background-image\"])\n        );\n      }}\n      collapsible\n    >\n      <Flex gap={1} direction=\"column\">\n        <RepeatedStyle\n          label=\"Background layer\"\n          styles={styles}\n          floatingPanelOffset={{ alignmentAxis: -100 }}\n          getItemProps={(_index, primaryValue) => ({\n            label: getBackgroundLabel(primaryValue, assets),\n          })}\n          renderThumbnail={(index) => <BackgroundThumbnail index={index} />}\n          renderItemContent={(index) => <BackgroundContent index={index} />}\n        />\n        <Grid\n          css={{\n            paddingInline: theme.panel.paddingInline,\n            gridTemplateColumns: `1fr ${theme.spacing[23]}`,\n          }}\n        >\n          <PropertyLabel\n            label=\"Color\"\n            description={propertyDescriptions.backgroundColor}\n            properties={[\"background-color\"]}\n          />\n          <ColorControl property=\"background-color\" />\n        </Grid>\n      </Flex>\n    </RepeatedStyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-utils.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport {\n  toValue,\n  type KeywordValue,\n  type RgbValue,\n  type StyleValue,\n  type UnitValue,\n  type VarValue,\n} from \"@webstudio-is/css-engine\";\nimport type {\n  GradientStop,\n  ParsedLinearGradient,\n  ParsedConicGradient,\n  ParsedRadialGradient,\n} from \"@webstudio-is/css-data\";\nimport {\n  clampStopIndex,\n  createSolidLinearGradient,\n  detectBackgroundType,\n  ensureGradientHasStops,\n  fillMissingStopPositions,\n  isSolidLinearGradient,\n  normalizeGradientInput,\n  type PercentUnitValue,\n  pruneHintOverrides,\n  reindexHintOverrides,\n  removeHintOverride,\n  resolveAngleValue,\n  resolveGradientForPicker,\n  resolveReverseStops,\n  resolveStopHintUpdate,\n  resolveStopPositionUpdate,\n  setHintOverride,\n  sideOrCornerToAngle,\n  sortGradientStops,\n  styleValueToColor,\n  formatGradientForType,\n  formatGradientValue,\n  convertGradientToTarget,\n  updateGradientStop,\n  type GradientType,\n} from \"./gradient-utils\";\n\nconst createLinearGradient = (\n  overrides: Partial<ParsedLinearGradient> = {}\n): ParsedLinearGradient => ({\n  type: \"linear\",\n  stops: [],\n  ...overrides,\n});\n\nconst createConicGradient = (\n  overrides: Partial<ParsedConicGradient> = {}\n): ParsedConicGradient => ({\n  type: \"conic\",\n  stops: [],\n  ...overrides,\n});\n\nconst createRadialGradient = (\n  overrides: Partial<ParsedRadialGradient> = {}\n): ParsedRadialGradient => ({\n  type: \"radial\",\n  stops: [],\n  ...overrides,\n});\n\ndescribe(\"formatGradientValue\", () => {\n  test(\"formats linear gradient\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 } },\n        { color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 } },\n      ],\n    });\n    expect(formatGradientValue(gradient)).toBe(\n      \"linear-gradient(rgb(0 0 0 / 1), rgb(255 255 255 / 1))\"\n    );\n  });\n\n  test(\"formats repeating linear gradient\", () => {\n    const gradient = createLinearGradient({\n      repeating: true,\n      stops: [\n        { color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 } },\n        { color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 } },\n      ],\n    });\n    expect(formatGradientValue(gradient)).toBe(\n      \"repeating-linear-gradient(rgb(0 0 0 / 1), rgb(255 255 255 / 1))\"\n    );\n  });\n\n  test(\"formats conic gradient\", () => {\n    const gradient = createConicGradient({\n      stops: [\n        { color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 } },\n        { color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 } },\n      ],\n    });\n    expect(formatGradientValue(gradient)).toBe(\n      \"conic-gradient(rgb(0 0 0 / 1), rgb(255 255 255 / 1))\"\n    );\n  });\n\n  test(\"formats repeating conic gradient\", () => {\n    const gradient = createConicGradient({\n      repeating: true,\n      stops: [\n        { color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 } },\n        { color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 } },\n      ],\n    });\n    expect(formatGradientValue(gradient)).toBe(\n      \"repeating-conic-gradient(rgb(0 0 0 / 1), rgb(255 255 255 / 1))\"\n    );\n  });\n\n  test(\"formats radial gradient\", () => {\n    const gradient = createRadialGradient({\n      shape: { type: \"keyword\", value: \"circle\" },\n      size: \"closest-side\",\n      position: \"center\",\n      stops: [\n        { color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 } },\n        { color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 } },\n      ],\n    });\n    expect(formatGradientValue(gradient)).toBe(\n      \"radial-gradient(circle closest-side at center, rgb(0 0 0 / 1), rgb(255 255 255 / 1))\"\n    );\n  });\n\n  test(\"formats repeating radial gradient\", () => {\n    const gradient = createRadialGradient({\n      repeating: true,\n      shape: { type: \"keyword\", value: \"circle\" },\n      position: \"center\",\n      stops: [\n        { color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 } },\n        { color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 } },\n      ],\n    });\n    expect(formatGradientValue(gradient)).toBe(\n      \"repeating-radial-gradient(circle at center, rgb(0 0 0 / 1), rgb(255 255 255 / 1))\"\n    );\n  });\n});\n\ndescribe(\"formatGradientForType\", () => {\n  const solidStyle: StyleValue = {\n    type: \"unparsed\",\n    value: \"linear-gradient(red, red)\",\n  };\n\n  test(\"formats solid color target\", () => {\n    expect(formatGradientForType(solidStyle, \"solid\")).toBe(\n      \"linear-gradient(rgb(255 0 0 / 1) 0%, rgb(255 0 0 / 1) 100%)\"\n    );\n  });\n\n  test(\"formats linear target\", () => {\n    expect(formatGradientForType(solidStyle, \"linearGradient\")).toBe(\n      \"linear-gradient(rgb(255 0 0 / 1), rgb(255 0 0 / 1))\"\n    );\n  });\n\n  test(\"formats conic target\", () => {\n    expect(formatGradientForType(undefined, \"conicGradient\")).toBe(\n      \"conic-gradient(rgb(0 0 0 / 1) 0%, rgb(0 0 0 / 0) 100%)\"\n    );\n  });\n\n  test(\"formats radial target\", () => {\n    const radialStyle: StyleValue = {\n      type: \"unparsed\",\n      value: \"radial-gradient(circle at center, red, blue)\",\n    };\n    expect(formatGradientForType(radialStyle, \"radialGradient\")).toBe(\n      \"radial-gradient(circle at center, rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"preserves repeating linear gradients\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"repeating-linear-gradient(red, blue)\",\n    };\n    expect(formatGradientForType(value, \"linearGradient\")).toBe(\n      \"repeating-linear-gradient(rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"preserves repeating conic gradients\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"repeating-conic-gradient(red, blue)\",\n    };\n    expect(formatGradientForType(value, \"conicGradient\")).toBe(\n      \"repeating-conic-gradient(rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"preserves repeating radial gradients\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"repeating-radial-gradient(circle, red, blue)\",\n    };\n    expect(formatGradientForType(value, \"radialGradient\")).toBe(\n      \"repeating-radial-gradient(circle, rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"converts conic gradient to radial target\", () => {\n    const conicStyle: StyleValue = {\n      type: \"unparsed\",\n      value: \"conic-gradient(red, blue)\",\n    };\n    expect(formatGradientForType(conicStyle, \"radialGradient\")).toBe(\n      \"radial-gradient(rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n});\n\ndescribe(\"convertGradientToTarget\", () => {\n  const gradientStrings: Record<GradientType, string> = {\n    linear: \"linear-gradient(red 0%, blue 100%)\",\n    conic: \"conic-gradient(red 0%, blue 100%)\",\n    radial: \"radial-gradient(circle, red 0%, blue 100%)\",\n  };\n  const gradientTypes: GradientType[] = [\"linear\", \"conic\", \"radial\"];\n\n  const getStopColors = (stops: GradientStop[]) =>\n    stops\n      .map((stop) => stop.color)\n      .filter(\n        (color): color is NonNullable<typeof color> => color !== undefined\n      )\n      .map((color) => toValue(color));\n\n  const expectedColorValues = [\"rgb(255 0 0 / 1)\", \"rgb(0 0 255 / 1)\"];\n\n  gradientTypes.forEach((sourceType) => {\n    gradientTypes.forEach((targetType) => {\n      test(`converts ${sourceType} gradient to ${targetType}`, () => {\n        const styleValue: StyleValue = {\n          type: \"unparsed\",\n          value: gradientStrings[sourceType],\n        };\n        const converted = convertGradientToTarget(styleValue, targetType);\n        expect(converted.type).toBe(targetType);\n        expect(converted.stops).toHaveLength(expectedColorValues.length);\n        expect(getStopColors(converted.stops)).toEqual(expectedColorValues);\n      });\n    });\n  });\n\n  test.each(gradientTypes)(\n    \"creates default gradient when value missing for %s target\",\n    (target) => {\n      const converted = convertGradientToTarget(undefined, target);\n      expect(converted.type).toBe(target);\n      expect(converted.stops.length).toBeGreaterThan(0);\n    }\n  );\n\n  test.each(gradientTypes)(\n    \"preserves repeating flag when converting to %s\",\n    (target) => {\n      const styleValue: StyleValue = {\n        type: \"unparsed\",\n        value: \"repeating-linear-gradient(red 0%, blue 100%)\",\n      };\n      const converted = convertGradientToTarget(styleValue, target);\n      expect(converted.repeating).toBe(true);\n    }\n  );\n\n  test.each(gradientTypes)(\n    \"omits repeating flag when source is not repeating for %s target\",\n    (target) => {\n      const styleValue: StyleValue = {\n        type: \"unparsed\",\n        value: \"linear-gradient(red 0%, blue 100%)\",\n      };\n      const converted = convertGradientToTarget(styleValue, target);\n      expect(converted.repeating).toBeUndefined();\n    }\n  );\n});\ndescribe(\"normalizeGradientInput\", () => {\n  test(\"returns string unchanged when not repeating\", () => {\n    const input = \"linear-gradient(red, blue)\";\n    expect(normalizeGradientInput(input, \"linear\")).toEqual({\n      normalizedGradientString: input,\n      initialIsRepeating: false,\n    });\n  });\n\n  test(\"normalizes repeating gradients\", () => {\n    const input = \"  repeating-linear-gradient(red, blue)\";\n    expect(normalizeGradientInput(input, \"linear\")).toEqual({\n      normalizedGradientString: \"  linear-gradient(red, blue)\",\n      initialIsRepeating: true,\n    });\n  });\n\n  test(\"handles uppercase repeating gradients while preserving leading whitespace\", () => {\n    const input = \"\\tRePeAtInG-Linear-GrAdIeNt(red, blue)\";\n    expect(normalizeGradientInput(input, \"linear\")).toEqual({\n      normalizedGradientString: \"\\tlinear-gradient(red, blue)\",\n      initialIsRepeating: true,\n    });\n  });\n\n  test(\"normalizes repeating conic gradients\", () => {\n    const input = \"repeating-conic-gradient(red, blue)\";\n    expect(normalizeGradientInput(input, \"conic\")).toEqual({\n      normalizedGradientString: \"conic-gradient(red, blue)\",\n      initialIsRepeating: true,\n    });\n  });\n\n  test(\"normalizes repeating radial gradients\", () => {\n    const input = \" repeating-radial-gradient(red, blue)\";\n    expect(normalizeGradientInput(input, \"radial\")).toEqual({\n      normalizedGradientString: \" radial-gradient(red, blue)\",\n      initialIsRepeating: true,\n    });\n  });\n});\n\ndescribe(\"sideOrCornerToAngle\", () => {\n  test(\"returns angle for single direction\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to top\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(0);\n  });\n\n  test(\"returns angle for right side\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to right\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(90);\n  });\n\n  test(\"returns angle for bottom side\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to bottom\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(180);\n  });\n\n  test(\"returns angle for left side\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to left\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(270);\n  });\n\n  test(\"returns angle for corner\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to bottom right\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(135);\n  });\n\n  test(\"returns angle for top right corner\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to top right\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(45);\n  });\n\n  test(\"returns angle for top left corner\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to top left\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(315);\n  });\n\n  test(\"returns angle for bottom left corner\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to bottom left\" };\n    expect(sideOrCornerToAngle(keyword)).toBe(225);\n  });\n\n  test(\"returns undefined for invalid input\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"center\" };\n    expect(sideOrCornerToAngle(keyword)).toBeUndefined();\n  });\n\n  test(\"returns undefined when side or corner missing\", () => {\n    expect(sideOrCornerToAngle(undefined)).toBeUndefined();\n  });\n\n  test(\"returns undefined when no direction tokens provided\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to    \" };\n    expect(sideOrCornerToAngle(keyword)).toBeUndefined();\n  });\n\n  test(\"returns undefined for unrecognized corner combination\", () => {\n    const keyword: KeywordValue = { type: \"keyword\", value: \"to top bottom\" };\n    expect(sideOrCornerToAngle(keyword)).toBeUndefined();\n  });\n});\n\ndescribe(\"resolveAngleValue\", () => {\n  test(\"returns unit angles\", () => {\n    const styleValue: StyleValue = { type: \"unit\", unit: \"deg\", value: 120 };\n    const result = resolveAngleValue(styleValue);\n    expect(result).toEqual({ type: \"unit\", unit: \"deg\", value: 120 });\n    expect(result).not.toBe(styleValue);\n  });\n\n  test(\"returns cloned var angles\", () => {\n    const fallback: VarValue[\"fallback\"] = {\n      type: \"unit\",\n      unit: \"deg\",\n      value: 45,\n    };\n    const styleValue: StyleValue = {\n      type: \"var\",\n      value: \"angle\",\n      fallback,\n    };\n    const result = resolveAngleValue(styleValue);\n    expect(result?.type).toBe(\"var\");\n    if (result?.type !== \"var\") {\n      throw new Error(\"Expected var angle\");\n    }\n    expect(result).not.toBe(styleValue);\n    expect(result.value).toBe(\"angle\");\n    expect(result.fallback).toEqual(fallback);\n    expect(result.fallback).not.toBe(styleValue.fallback);\n  });\n\n  test(\"returns undefined for non-angle values\", () => {\n    const styleValue: StyleValue = { type: \"keyword\", value: \"auto\" };\n    expect(resolveAngleValue(styleValue)).toBeUndefined();\n  });\n});\n\ndescribe(\"fillMissingStopPositions\", () => {\n  test(\"returns original gradient when no stops present\", () => {\n    const gradient = createLinearGradient();\n    expect(fillMissingStopPositions(gradient)).toBe(gradient);\n  });\n\n  test(\"returns original gradient when positions use non-percent units\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: undefined, position: { type: \"unit\", unit: \"px\", value: 10 } },\n        { color: undefined },\n      ],\n    });\n\n    expect(fillMissingStopPositions(gradient)).toBe(gradient);\n  });\n\n  test(\"assigns zero to a single stop without position\", () => {\n    const gradient = createLinearGradient({\n      stops: [{ color: undefined }],\n    });\n\n    const result = fillMissingStopPositions(gradient);\n    expect(result).not.toBe(gradient);\n    expect(result.stops[0]?.position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    });\n  });\n\n  test(\"distributes positions evenly when all positions missing\", () => {\n    const gradient = createLinearGradient({\n      stops: [{ color: undefined }, { color: undefined }, { color: undefined }],\n    });\n\n    const result = fillMissingStopPositions(gradient);\n    expect(result.stops.map((stop) => stop.position?.value)).toEqual([\n      0, 50, 100,\n    ]);\n  });\n\n  test(\"fills missing positions proportionally\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: undefined, position: { type: \"unit\", unit: \"%\", value: 0 } },\n        { color: undefined },\n        { color: undefined, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = fillMissingStopPositions(gradient);\n    expect(result.stops[1]?.position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 50,\n    });\n  });\n\n  test(\"returns original gradient when positions use css variables\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: {\n            type: \"var\",\n            value: \"start\",\n            fallback: { type: \"unparsed\", value: \"10%\" },\n          },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(fillMissingStopPositions(gradient)).toBe(gradient);\n  });\n\n  test(\"fills missing start and end positions when interior stops defined\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: undefined },\n        { color: undefined, position: { type: \"unit\", unit: \"%\", value: 20 } },\n        { color: undefined },\n        { color: undefined, position: { type: \"unit\", unit: \"%\", value: 80 } },\n        { color: undefined },\n      ],\n    });\n\n    const result = fillMissingStopPositions(gradient);\n    expect(result.stops.map((stop) => stop.position?.value)).toEqual([\n      0, 20, 50, 80, 100,\n    ]);\n  });\n\n  test(\"leaves gradients unchanged when positions are defined\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: undefined, position: { type: \"unit\", unit: \"%\", value: 0 } },\n        { color: undefined, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    expect(fillMissingStopPositions(gradient)).toBe(gradient);\n  });\n});\n\ndescribe(\"ensureGradientHasStops\", () => {\n  test(\"provides default stops when gradient is empty\", () => {\n    const gradient = createLinearGradient();\n    const result = ensureGradientHasStops(gradient);\n    expect(result.stops).toHaveLength(2);\n    expect(result.stops[0]?.color).toEqual({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [0, 0, 0],\n      alpha: 1,\n    });\n    expect(result.stops[1]?.color).toEqual({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [0, 0, 0],\n      alpha: 0,\n    });\n  });\n\n  test(\"fills missing colors with fallback\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n      ],\n    });\n\n    const result = ensureGradientHasStops(gradient);\n    expect(result.stops[0]?.color).toEqual({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [0, 0, 0],\n      alpha: 1,\n    });\n  });\n\n  test(\"preserves existing stop colors\", () => {\n    const stop: GradientStop = {\n      color: { type: \"rgb\", r: 10, g: 20, b: 30, alpha: 0.5 },\n      position: { type: \"unit\", unit: \"%\", value: 10 },\n    };\n    const gradient = createLinearGradient({\n      stops: [stop],\n    });\n\n    const result = ensureGradientHasStops(gradient);\n    expect(result).not.toBe(gradient);\n    expect(result.stops[0]).toBe(stop);\n  });\n});\n\ndescribe(\"clampStopIndex\", () => {\n  const gradient = createLinearGradient({\n    stops: [\n      { color: undefined, position: undefined },\n      { color: undefined, position: undefined },\n    ],\n  });\n\n  test(\"clamps to valid range\", () => {\n    expect(clampStopIndex(-1, gradient)).toBe(0);\n    expect(clampStopIndex(1, gradient)).toBe(1);\n    expect(clampStopIndex(5, gradient)).toBe(1);\n  });\n\n  test(\"returns zero when gradient has no stops\", () => {\n    const emptyGradient = createLinearGradient();\n    expect(clampStopIndex(3, emptyGradient)).toBe(0);\n  });\n});\n\ndescribe(\"styleValueToColor\", () => {\n  type IntermediateColorValue = { type: \"intermediate\"; value: string };\n\n  test(\"returns undefined when style value missing\", () => {\n    expect(styleValueToColor(undefined)).toBeUndefined();\n  });\n\n  test(\"returns rgb value when style already rgb\", () => {\n    const style: RgbValue = { type: \"rgb\", r: 1, g: 2, b: 3, alpha: 0.5 };\n    expect(styleValueToColor(style)).toBe(style);\n  });\n\n  test(\"parses keyword colors\", () => {\n    const style: StyleValue = { type: \"keyword\", value: \"red\" };\n    expect(styleValueToColor(style)).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"returns undefined when keyword is not recognized\", () => {\n    const style: StyleValue = { type: \"keyword\", value: \"not-a-color\" };\n    expect(styleValueToColor(style)).toBeUndefined();\n  });\n\n  test(\"parses intermediate string colors\", () => {\n    const style: IntermediateColorValue = {\n      type: \"intermediate\",\n      value: \"#0000ff\",\n    };\n    expect(styleValueToColor(style)).toEqual({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [0, 0, 1],\n      alpha: 1,\n    });\n  });\n\n  test(\"returns undefined when intermediate string is invalid\", () => {\n    const style: IntermediateColorValue = {\n      type: \"intermediate\",\n      value: \"#ggg\",\n    };\n    expect(styleValueToColor(style)).toBeUndefined();\n  });\n\n  test(\"parses invalid style when value is valid color string\", () => {\n    const style: StyleValue = { type: \"invalid\", value: \"rgb(10 20 30)\" };\n    expect(styleValueToColor(style)).toEqual({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [0.0392, 0.0784, 0.1176],\n      alpha: 1,\n    });\n  });\n\n  test(\"returns undefined for invalid style when value is not a color\", () => {\n    const style: StyleValue = { type: \"invalid\", value: \"oops\" };\n    expect(styleValueToColor(style)).toBeUndefined();\n  });\n\n  test(\"parses unparsed color strings\", () => {\n    const style: StyleValue = {\n      type: \"unparsed\",\n      value: \"hsl(180 100% 50%)\",\n    };\n    expect(styleValueToColor(style)).toEqual({\n      type: \"color\",\n      colorSpace: \"hsl\",\n      components: [180, 100, 50],\n      alpha: 1,\n    });\n  });\n\n  test(\"returns var values\", () => {\n    const variable: StyleValue = { type: \"var\", value: \"--color\" };\n    expect(styleValueToColor(variable)).toEqual(variable);\n  });\n\n  test(\"parses intermediate string var values\", () => {\n    const style: IntermediateColorValue = {\n      type: \"intermediate\",\n      value: \"var(--accent-color)\",\n    };\n    expect(styleValueToColor(style)).toEqual({\n      type: \"var\",\n      value: \"accent-color\",\n    });\n  });\n\n  test(\"returns undefined for unsupported values\", () => {\n    const style: StyleValue = { type: \"unit\", unit: \"%\", value: 25 };\n    expect(styleValueToColor(style)).toBeUndefined();\n  });\n});\n\ndescribe(\"resolveStopHintUpdate\", () => {\n  test(\"returns cloned var hint and requests override clear\", () => {\n    const fallback: StyleValue = { type: \"unparsed\", value: \"20%\" };\n    const styleValue: StyleValue = {\n      type: \"var\",\n      value: \"hint\",\n      fallback,\n    };\n\n    const result = resolveStopHintUpdate(styleValue);\n\n    expect(result.type).toBe(\"apply\");\n    if (result.type !== \"apply\") {\n      throw new Error(\"Expected apply result\");\n    }\n    expect(result).toEqual({\n      type: \"apply\",\n      hint: {\n        type: \"var\",\n        value: \"hint\",\n        fallback: { type: \"unparsed\", value: \"20%\" },\n      },\n      clearOverride: true,\n    });\n    expect(result.override).toBeUndefined();\n    if (result.hint?.type === \"var\") {\n      expect(result.hint).not.toBe(styleValue);\n      expect(result.hint.fallback).not.toBe(styleValue.fallback);\n    }\n  });\n\n  test(\"normalizes percent units and returns override\", () => {\n    const styleValue: StyleValue = { type: \"unit\", unit: \"%\", value: 45 };\n\n    const result = resolveStopHintUpdate(styleValue);\n\n    expect(result.type).toBe(\"apply\");\n    if (result.type !== \"apply\") {\n      throw new Error(\"Expected apply result\");\n    }\n    // Value is clamped between 0-100\n    expect(result).toEqual({\n      type: \"apply\",\n      hint: { type: \"unit\", unit: \"%\", value: 45 },\n      override: { type: \"unit\", unit: \"%\", value: 45 },\n      clearOverride: false,\n    });\n  });\n\n  test(\"returns none when value unsupported\", () => {\n    const styleValue: StyleValue = { type: \"keyword\", value: \"auto\" };\n\n    const result = resolveStopHintUpdate(styleValue);\n\n    expect(result).toEqual({ type: \"none\" });\n  });\n});\n\ndescribe(\"resolveStopPositionUpdate\", () => {\n  test(\"returns cloned var position and requests override clear\", () => {\n    const fallback: StyleValue = { type: \"unparsed\", value: \"30%\" };\n    const styleValue: StyleValue = {\n      type: \"var\",\n      value: \"pos\",\n      fallback,\n    };\n\n    const result = resolveStopPositionUpdate(styleValue);\n\n    expect(result.type).toBe(\"apply\");\n    if (result.type !== \"apply\") {\n      throw new Error(\"Expected apply result\");\n    }\n    expect(result.clearHintOverrides).toBe(true);\n    expect(result.position).toEqual({\n      type: \"var\",\n      value: \"pos\",\n      fallback: { type: \"unparsed\", value: \"30%\" },\n    });\n    if (result.position?.type === \"var\") {\n      expect(result.position).not.toBe(styleValue);\n      expect(result.position.fallback).not.toBe(styleValue.fallback);\n    }\n  });\n\n  test(\"normalizes percent unit and requests override clear\", () => {\n    const normalized: PercentUnitValue = {\n      type: \"unit\",\n      unit: \"%\",\n      value: 100,\n    };\n\n    const result = resolveStopPositionUpdate({\n      type: \"unit\",\n      unit: \"%\",\n      value: 120,\n    });\n\n    expect(result.type).toBe(\"apply\");\n    if (result.type !== \"apply\") {\n      throw new Error(\"Expected apply result\");\n    }\n    expect(result.position).toEqual(normalized);\n    expect(result.clearHintOverrides).toBe(true);\n  });\n\n  test(\"returns none when value unsupported\", () => {\n    const result = resolveStopPositionUpdate({\n      type: \"keyword\",\n      value: \"auto\",\n    });\n\n    expect(result).toEqual({ type: \"none\" });\n  });\n});\n\ndescribe(\"resolveReverseStops\", () => {\n  test(\"returns none when gradient has single stop\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n      ],\n    });\n\n    const result = resolveReverseStops(gradient, 0);\n    expect(result).toEqual({ type: \"none\" });\n  });\n\n  test(\"reverses stops and mirrors percent positions\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 255, b: 255, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 40 },\n        },\n        {\n          color: { type: \"rgb\", r: 100, g: 100, b: 100, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 90 },\n        },\n      ],\n    });\n\n    const result = resolveReverseStops(gradient, 1);\n    expect(result.type).toBe(\"apply\");\n    if (result.type !== \"apply\") {\n      throw new Error(\"Expected apply result\");\n    }\n    expect(result.selectedStopIndex).toBe(1);\n    const positions = result.gradient.stops.map((stop) => stop.position);\n    expect(positions).toEqual([\n      { type: \"unit\", unit: \"%\", value: 10 },\n      { type: \"unit\", unit: \"%\", value: 60 },\n      { type: \"unit\", unit: \"%\", value: 100 },\n    ]);\n  });\n\n  test(\"preserves non-percent positions when reversing\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"px\", value: 10 },\n        },\n        {\n          color: undefined,\n          position: { type: \"var\", value: \"progress\" },\n        },\n      ],\n    });\n\n    const result = resolveReverseStops(gradient, 0);\n    expect(result.type).toBe(\"apply\");\n    if (result.type !== \"apply\") {\n      throw new Error(\"Expected apply result\");\n    }\n    const [first, second] = result.gradient.stops;\n    expect(first.position).toEqual({ type: \"var\", value: \"progress\" });\n    expect(second.position).toEqual({ type: \"unit\", unit: \"px\", value: 10 });\n  });\n});\n\ndescribe(\"resolveGradientForPicker\", () => {\n  test(\"fills missing stop positions without overrides\", () => {\n    const gradient = createLinearGradient({\n      stops: [{ color: undefined }, { color: undefined }, { color: undefined }],\n    });\n\n    const result = resolveGradientForPicker(gradient, new Map());\n\n    expect(result.stops.map((stop) => stop.position?.value)).toEqual([\n      0, 50, 100,\n    ]);\n  });\n\n  test(\"applies overrides when hints missing\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 50 },\n        },\n      ],\n    });\n\n    const overrides = new Map<number, PercentUnitValue>([\n      [1, { type: \"unit\", unit: \"%\", value: 25 }],\n    ]);\n\n    const result = resolveGradientForPicker(gradient, overrides);\n\n    expect(result.stops[1]?.hint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 25,\n    });\n  });\n\n  test(\"leaves existing hints untouched\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n          hint: { type: \"unit\", unit: \"%\", value: 60 },\n        },\n      ],\n    });\n\n    const overrides = new Map<number, PercentUnitValue>([\n      [1, { type: \"unit\", unit: \"%\", value: 80 }],\n    ]);\n\n    const result = resolveGradientForPicker(gradient, overrides);\n\n    expect(result.stops[1]?.hint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 60,\n    });\n  });\n\n  test(\"resolves variable stop positions using fallback\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: {\n            type: \"var\",\n            value: \"--progress\",\n            fallback: { type: \"unit\", unit: \"%\", value: 30 },\n          },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    const result = resolveGradientForPicker(gradient, new Map());\n\n    expect(result.stops[0]?.position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 30,\n    });\n  });\n\n  test(\"falls back to inferred positions when variable has no fallback\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"var\", value: \"--start\" },\n        },\n        {\n          color: undefined,\n        },\n      ],\n    });\n\n    const result = resolveGradientForPicker(gradient, new Map());\n\n    expect(result.stops.map((stop) => stop.position?.value)).toEqual([0, 100]);\n  });\n\n  test(\"converts conic stop angles to percents\", () => {\n    const gradient = createConicGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"deg\", value: 0 },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"deg\", value: 120 },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"deg\", value: 240 },\n        },\n      ],\n    });\n\n    const result = resolveGradientForPicker(gradient, new Map());\n\n    const stopPositions = result.stops.map((stop) => stop.position);\n    expect(stopPositions[0]).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    });\n    expect(stopPositions[1]).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: (120 / 360) * 100,\n    });\n    expect(stopPositions[2]).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: (240 / 360) * 100,\n    });\n  });\n\n  test(\"converts conic hints expressed in angles\", () => {\n    const gradient = createConicGradient({\n      stops: [\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n          hint: { type: \"unit\", unit: \"turn\", value: 0.25 },\n        },\n        {\n          color: undefined,\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    const result = resolveGradientForPicker(gradient, new Map());\n    const firstHint = result.stops[0]?.hint;\n    expect(firstHint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 25,\n    });\n  });\n});\n\ndescribe(\"hint override helpers\", () => {\n  const makeOverride = (value: number): PercentUnitValue => ({\n    type: \"unit\",\n    unit: \"%\",\n    value,\n  });\n\n  test(\"removeHintOverride returns same map when index missing\", () => {\n    const overrides = new Map<number, PercentUnitValue>();\n    const result = removeHintOverride(overrides, 1);\n    expect(result).toBe(overrides);\n  });\n\n  test(\"removeHintOverride removes matching index\", () => {\n    const overrides = new Map<number, PercentUnitValue>([\n      [1, makeOverride(10)],\n    ]);\n    const result = removeHintOverride(overrides, 1);\n    expect(result).not.toBe(overrides);\n    expect(result.size).toBe(0);\n  });\n\n  test(\"setHintOverride adds new override without mutating original map\", () => {\n    const overrides = new Map<number, PercentUnitValue>();\n    const override = makeOverride(40);\n    const result = setHintOverride(overrides, 2, override);\n    expect(result).not.toBe(overrides);\n    expect(result.get(2)).toBe(override);\n    expect(overrides.size).toBe(0);\n  });\n\n  test(\"setHintOverride returns same map when override unchanged\", () => {\n    const override = makeOverride(60);\n    const overrides = new Map<number, PercentUnitValue>([[3, override]]);\n    const result = setHintOverride(overrides, 3, { ...override });\n    expect(result).toBe(overrides);\n  });\n\n  test(\"pruneHintOverrides removes indexes beyond stop count\", () => {\n    const overrides = new Map<number, PercentUnitValue>([\n      [0, makeOverride(10)],\n      [5, makeOverride(90)],\n    ]);\n    const result = pruneHintOverrides(overrides, 2);\n    expect(result).not.toBe(overrides);\n    expect([...result.keys()]).toEqual([0]);\n  });\n\n  test(\"pruneHintOverrides returns same map when nothing pruned\", () => {\n    const overrides = new Map<number, PercentUnitValue>([\n      [0, makeOverride(10)],\n      [1, makeOverride(20)],\n    ]);\n    const result = pruneHintOverrides(overrides, 3);\n    expect(result).toBe(overrides);\n  });\n});\n\ndescribe(\"createSolidLinearGradient\", () => {\n  test(\"duplicates color stops at 0% and 100% while preserving base geometry\", () => {\n    const color: GradientStop[\"color\"] = {\n      type: \"rgb\",\n      r: 120,\n      g: 80,\n      b: 200,\n      alpha: 0.7,\n    } satisfies RgbValue;\n    const baseAngle = {\n      type: \"unit\",\n      unit: \"deg\",\n      value: 45,\n    } satisfies UnitValue;\n    const baseSide: KeywordValue = { type: \"keyword\", value: \"to bottom\" };\n    const baseGradient = createLinearGradient({\n      angle: baseAngle,\n      sideOrCorner: baseSide,\n    });\n\n    const result = createSolidLinearGradient(color, baseGradient);\n\n    expect(result.type).toBe(\"linear\");\n    expect(result.angle).toBe(baseAngle);\n    expect(result.sideOrCorner).toBe(baseSide);\n    expect(result.stops).toHaveLength(2);\n\n    const [firstStop, secondStop] = result.stops;\n    expect(firstStop?.position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    });\n    expect(secondStop?.position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 100,\n    });\n    expect(firstStop?.color).toEqual(color);\n    expect(secondStop?.color).toEqual(color);\n    expect(firstStop?.color).not.toBe(color);\n    expect(secondStop?.color).not.toBe(color);\n  });\n\n  test(\"clones var color fallbacks for each stop\", () => {\n    const fallback: RgbValue = {\n      type: \"rgb\",\n      r: 10,\n      g: 40,\n      b: 90,\n      alpha: 1,\n    };\n    const color: VarValue = {\n      type: \"var\",\n      value: \"--accent\",\n      fallback,\n    };\n\n    const result = createSolidLinearGradient(color as GradientStop[\"color\"]);\n    const [firstStop, secondStop] = result.stops;\n    const firstColor = firstStop?.color;\n    const secondColor = secondStop?.color;\n\n    expect(firstColor).toEqual(color);\n    expect(secondColor).toEqual(color);\n    expect(firstColor).not.toBe(color);\n    expect(secondColor).not.toBe(color);\n\n    if (firstColor?.type !== \"var\" || secondColor?.type !== \"var\") {\n      throw new Error(\"Expected var colors to be cloned\");\n    }\n\n    expect(firstColor.fallback).toEqual(fallback);\n    expect(secondColor.fallback).toEqual(fallback);\n    expect(firstColor.fallback).not.toBe(fallback);\n    expect(secondColor.fallback).not.toBe(fallback);\n    expect(firstColor.fallback).not.toBe(secondColor.fallback);\n  });\n});\n\ndescribe(\"isSolidLinearGradient\", () => {\n  test(\"returns true for valid solid gradient (2 stops, same color, 0% and 100%)\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns false when gradient has only 1 stop\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns false when gradient has 3 stops\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 50 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns false when stops have different colors\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 0, g: 0, b: 255, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns false when first stop position is not 0%\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 10 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns false when second stop position is not 100%\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 90 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns true when first stop has no position (defaults to 0%)\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns true when second stop has no position (defaults to 100%)\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns false when positions use px unit instead of %\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"px\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"px\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns false when first stop color is undefined\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns false when second stop color is undefined\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns true when colors match with different alpha values\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 100, g: 150, b: 200, alpha: 0.5 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 100, g: 150, b: 200, alpha: 0.5 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns false when colors have same RGB but different alpha\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 100, g: 150, b: 200, alpha: 0.5 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 100, g: 150, b: 200, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns true for keyword colors that match\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"keyword\", value: \"red\" },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"keyword\", value: \"red\" },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns false when positions use var values\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: {\n            type: \"var\",\n            value: \"--start\",\n            fallback: { type: \"unit\", unit: \"%\", value: 0 },\n          },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(false);\n  });\n\n  test(\"returns true when both positions are undefined (auto-assigned 0% and 100%)\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns true when first position is undefined and second is 100%\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n\n  test(\"returns true when first position is 0% and second is undefined\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n        },\n        {\n          color: { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 },\n        },\n      ],\n    });\n\n    expect(isSolidLinearGradient(gradient)).toBe(true);\n  });\n});\n\ndescribe(\"detectBackgroundType\", () => {\n  test(\"returns image when style value undefined\", () => {\n    expect(detectBackgroundType(undefined)).toBe(\"image\");\n  });\n\n  test(\"returns image for keyword none\", () => {\n    const value: StyleValue = { type: \"keyword\", value: \"none\" };\n    expect(detectBackgroundType(value)).toBe(\"image\");\n  });\n\n  test(\"returns image for url image value\", () => {\n    const value: StyleValue = {\n      type: \"image\",\n      value: { type: \"url\", url: \"https://example.com/image.png\" },\n    };\n    expect(detectBackgroundType(value)).toBe(\"image\");\n  });\n\n  test(\"returns linearGradient for linear gradient\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"linear-gradient(red, blue)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"linearGradient\");\n  });\n\n  test(\"returns solid for uniform linear gradient with explicit positions\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"linear-gradient(red 0%, red 100%)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"solid\");\n  });\n\n  test(\"returns solid for uniform gradient without explicit positions\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"linear-gradient(red, red)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"solid\");\n  });\n\n  test(\"returns conicGradient for conic gradient\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"conic-gradient(red, blue)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"conicGradient\");\n  });\n\n  test(\"returns conicGradient for conic gradients with from/at syntax\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value:\n        \"conic-gradient(from 0deg at 50% 50%, rgba(255,126,95,1) 0deg, rgba(254,180,123,1) 120deg, rgba(134,168,231,1) 240deg, rgba(255,126,95,1) 360deg)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"conicGradient\");\n  });\n\n  test(\"returns radialGradient for radial gradients\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"radial-gradient(circle, red, blue)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"radialGradient\");\n  });\n\n  test(\"returns radialGradient for repeating radial gradients\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"repeating-radial-gradient(circle, red, blue)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"radialGradient\");\n  });\n\n  test(\"returns image for unsupported gradient types\", () => {\n    const value: StyleValue = {\n      type: \"unparsed\",\n      value: \"unsupported-gradient(circle, red, blue)\",\n    };\n    expect(detectBackgroundType(value)).toBe(\"image\");\n  });\n});\n\ndescribe(\"reindexHintOverrides\", () => {\n  test(\"reindexes hints after deleting a stop before them\", () => {\n    const overrides = new Map<number, PercentUnitValue>([\n      [1, { type: \"unit\", unit: \"%\", value: 25 }],\n      [3, { type: \"unit\", unit: \"%\", value: 75 }],\n    ]);\n    const result = reindexHintOverrides(overrides, 0);\n    expect(result.get(0)).toEqual({ type: \"unit\", unit: \"%\", value: 25 });\n    expect(result.get(2)).toEqual({ type: \"unit\", unit: \"%\", value: 75 });\n    expect(result.size).toBe(2);\n  });\n\n  test(\"removes hint for deleted stop\", () => {\n    const overrides = new Map<number, PercentUnitValue>([\n      [1, { type: \"unit\", unit: \"%\", value: 25 }],\n      [2, { type: \"unit\", unit: \"%\", value: 50 }],\n      [3, { type: \"unit\", unit: \"%\", value: 75 }],\n    ]);\n    const result = reindexHintOverrides(overrides, 2);\n    expect(result.get(1)).toEqual({ type: \"unit\", unit: \"%\", value: 25 });\n    expect(result.get(2)).toEqual({ type: \"unit\", unit: \"%\", value: 75 });\n    expect(result.has(3)).toBe(false);\n    expect(result.size).toBe(2);\n  });\n\n  test(\"keeps hints before deleted index unchanged\", () => {\n    const overrides = new Map<number, PercentUnitValue>([\n      [0, { type: \"unit\", unit: \"%\", value: 10 }],\n      [1, { type: \"unit\", unit: \"%\", value: 25 }],\n    ]);\n    const result = reindexHintOverrides(overrides, 3);\n    expect(result.get(0)).toEqual({ type: \"unit\", unit: \"%\", value: 10 });\n    expect(result.get(1)).toEqual({ type: \"unit\", unit: \"%\", value: 25 });\n    expect(result.size).toBe(2);\n  });\n\n  test(\"handles empty overrides map\", () => {\n    const overrides = new Map<number, PercentUnitValue>();\n    const result = reindexHintOverrides(overrides, 1);\n    expect(result.size).toBe(0);\n  });\n});\n\ndescribe(\"sortGradientStops\", () => {\n  const red: RgbValue = { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 };\n  const blue: RgbValue = { type: \"rgb\", r: 0, g: 0, b: 255, alpha: 1 };\n  const green: RgbValue = { type: \"rgb\", r: 0, g: 255, b: 0, alpha: 1 };\n\n  test(\"sorts stops by position ascending\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 100 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 0 } },\n        { color: green, position: { type: \"unit\", unit: \"%\", value: 50 } },\n      ],\n    });\n    const { sortedGradient } = sortGradientStops(gradient, new Map());\n\n    expect(sortedGradient.stops[0].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    });\n    expect(sortedGradient.stops[1].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 50,\n    });\n    expect(sortedGradient.stops[2].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 100,\n    });\n  });\n\n  test(\"reindexes hint overrides to match sorted positions\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 100 } }, // originalIndex: 0\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 0 } }, // originalIndex: 1\n        { color: green, position: { type: \"unit\", unit: \"%\", value: 50 } }, // originalIndex: 2\n      ],\n    });\n    const hintOverrides = new Map<number, PercentUnitValue>([\n      [0, { type: \"unit\", unit: \"%\", value: 90 }], // hint for stop at 100%\n      [2, { type: \"unit\", unit: \"%\", value: 40 }], // hint for stop at 50%\n    ]);\n    const { reindexedHints } = sortGradientStops(gradient, hintOverrides);\n\n    // After sorting: [0%, 50%, 100%]\n    // Hints should be at new indices: 1 (for 50%) and 2 (for 100%)\n    expect(reindexedHints.get(1)).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 40,\n    });\n    expect(reindexedHints.get(2)).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 90,\n    });\n    expect(reindexedHints.size).toBe(2);\n  });\n\n  test(\"handles stops without positions (defaults to 0)\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 50 } },\n        { color: blue }, // No position, should default to 0\n        { color: green, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n    const { sortedGradient } = sortGradientStops(gradient, new Map());\n\n    expect(sortedGradient.stops[0].color).toEqual(blue); // No position = 0\n    expect(sortedGradient.stops[1].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 50,\n    });\n    expect(sortedGradient.stops[2].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 100,\n    });\n  });\n\n  test(\"preserves gradient properties other than stops\", () => {\n    const gradient = createLinearGradient({\n      angle: { type: \"unit\", unit: \"deg\", value: 45 },\n      repeating: true,\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 100 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 0 } },\n      ],\n    });\n    const { sortedGradient } = sortGradientStops(gradient, new Map());\n\n    expect(sortedGradient.type).toBe(\"linear\");\n    if (sortedGradient.type === \"linear\") {\n      expect(sortedGradient.angle).toEqual({\n        type: \"unit\",\n        unit: \"deg\",\n        value: 45,\n      });\n    }\n    expect(sortedGradient.repeating).toBe(true);\n  });\n\n  test(\"handles empty hint overrides\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 50 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 0 } },\n      ],\n    });\n    const { reindexedHints } = sortGradientStops(gradient, new Map());\n\n    expect(reindexedHints.size).toBe(0);\n  });\n\n  test(\"maintains stable sort for stops at same position\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 50 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 50 } },\n        { color: green, position: { type: \"unit\", unit: \"%\", value: 50 } },\n      ],\n    });\n    const { sortedGradient } = sortGradientStops(gradient, new Map());\n\n    // Original order should be preserved for stops at the same position\n    expect(sortedGradient.stops[0].color).toEqual(red);\n    expect(sortedGradient.stops[1].color).toEqual(blue);\n    expect(sortedGradient.stops[2].color).toEqual(green);\n  });\n});\n\ndescribe(\"updateGradientStop\", () => {\n  const red: RgbValue = { type: \"rgb\", r: 255, g: 0, b: 0, alpha: 1 };\n  const blue: RgbValue = { type: \"rgb\", r: 0, g: 0, b: 255, alpha: 1 };\n\n  test(\"returns same gradient when stop index is invalid\", () => {\n    const gradient = createLinearGradient({\n      stops: [{ color: red, position: { type: \"unit\", unit: \"%\", value: 0 } }],\n    });\n\n    const result = updateGradientStop(gradient, -1, (stop) => stop);\n    expect(result).toBe(gradient);\n\n    const result2 = updateGradientStop(gradient, 5, (stop) => stop);\n    expect(result2).toBe(gradient);\n  });\n\n  test(\"updates stop without hint\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 0 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 25 },\n    }));\n\n    expect(result.stops[0].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 25,\n    });\n    expect(result.stops[0].hint).toBeUndefined();\n  });\n\n  test(\"maintains hint offset when position changes\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: red,\n          position: { type: \"unit\", unit: \"%\", value: 20 },\n          hint: { type: \"unit\", unit: \"%\", value: 30 }, // offset = 10\n        },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 50 },\n    }));\n\n    expect(result.stops[0].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 50,\n    });\n    // Hint should maintain offset of 10: 50 + 10 = 60\n    expect(result.stops[0].hint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 60,\n    });\n  });\n\n  test(\"clamps hint to 0-100 range when maintaining offset\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: red,\n          position: { type: \"unit\", unit: \"%\", value: 80 },\n          hint: { type: \"unit\", unit: \"%\", value: 95 }, // offset = 15\n        },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 90 },\n    }));\n\n    // Hint should be clamped: 90 + 15 = 105 -> 100\n    expect(result.stops[0].hint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 100,\n    });\n  });\n\n  test(\"clamps hint to minimum 0 when offset is negative\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: red,\n          position: { type: \"unit\", unit: \"%\", value: 50 },\n          hint: { type: \"unit\", unit: \"%\", value: 30 }, // offset = -20\n        },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 10 },\n    }));\n\n    // Hint should be clamped: 10 + (-20) = -10 -> 0\n    expect(result.stops[0].hint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    });\n  });\n\n  test(\"does not modify hint when position does not change\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: red,\n          position: { type: \"unit\", unit: \"%\", value: 20 },\n          hint: { type: \"unit\", unit: \"%\", value: 30 },\n        },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      color: blue, // Only change color, not position\n    }));\n\n    expect(result.stops[0].color).toEqual(blue);\n    expect(result.stops[0].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 20,\n    });\n    // Hint should remain unchanged\n    expect(result.stops[0].hint).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 30,\n    });\n  });\n\n  test(\"does not modify other stops\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: red,\n          position: { type: \"unit\", unit: \"%\", value: 0 },\n          hint: { type: \"unit\", unit: \"%\", value: 10 },\n        },\n        {\n          color: blue,\n          position: { type: \"unit\", unit: \"%\", value: 100 },\n          hint: { type: \"unit\", unit: \"%\", value: 90 },\n        },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 20 },\n    }));\n\n    // Second stop should be unchanged\n    expect(result.stops[1]).toEqual(gradient.stops[1]);\n  });\n\n  test(\"handles stop without hint gracefully\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 0 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 50 },\n    }));\n\n    expect(result.stops[0].position).toEqual({\n      type: \"unit\",\n      unit: \"%\",\n      value: 50,\n    });\n    expect(result.stops[0].hint).toBeUndefined();\n  });\n\n  test(\"handles non-percent hint units gracefully\", () => {\n    const gradient = createLinearGradient({\n      stops: [\n        {\n          color: red,\n          position: { type: \"unit\", unit: \"%\", value: 20 },\n          hint: { type: \"unit\", unit: \"px\", value: 50 } as UnitValue,\n        },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 50 },\n    }));\n\n    // Hint should not be modified for non-percent units\n    expect(result.stops[0].hint).toEqual({\n      type: \"unit\",\n      unit: \"px\",\n      value: 50,\n    });\n  });\n\n  test(\"preserves gradient properties other than stops\", () => {\n    const gradient = createLinearGradient({\n      angle: { type: \"unit\", unit: \"deg\", value: 45 },\n      repeating: true,\n      stops: [\n        { color: red, position: { type: \"unit\", unit: \"%\", value: 0 } },\n        { color: blue, position: { type: \"unit\", unit: \"%\", value: 100 } },\n      ],\n    });\n\n    const result = updateGradientStop(gradient, 0, (stop) => ({\n      ...stop,\n      position: { type: \"unit\", unit: \"%\", value: 25 },\n    }));\n\n    expect(result.type).toBe(\"linear\");\n    if (result.type === \"linear\") {\n      expect(result.angle).toEqual({ type: \"unit\", unit: \"deg\", value: 45 });\n    }\n    expect(result.repeating).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/backgrounds/gradient-utils.ts",
    "content": "import { clamp } from \"@react-aria/utils\";\nimport {\n  ColorValue,\n  toValue,\n  type CssProperty,\n  type KeywordValue,\n  type StyleValue,\n  type Unit,\n  type UnitValue,\n  type VarValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  parseCssValue,\n  parseLinearGradient,\n  parseConicGradient,\n  parseRadialGradient,\n  formatLinearGradient,\n  formatConicGradient,\n  formatRadialGradient,\n  expandShorthands,\n  type GradientStop,\n  type ParsedGradient,\n  type ParsedLinearGradient,\n  type ParsedConicGradient,\n  type ParsedRadialGradient,\n} from \"@webstudio-is/css-data\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { getRepeatedStyleItem } from \"../../shared/repeated-style\";\nimport type { UnitOption } from \"../../shared/css-value-input/unit-select\";\n\nconst backgroundPositionXLonghand: CssProperty = \"background-position-x\";\nconst backgroundPositionYLonghand: CssProperty = \"background-position-y\";\n\nexport type GradientType = \"linear\" | \"conic\" | \"radial\";\nexport type PercentUnitValue = UnitValue & { unit: \"%\" };\nexport type NormalizedGradient = {\n  normalizedGradientString: string;\n  initialIsRepeating: boolean;\n};\nexport type IntermediateColorValue = {\n  type: \"intermediate\";\n  value: string;\n};\n\nconst gradientFunctionNames: Record<\n  GradientType,\n  { base: string; repeating: string }\n> = {\n  linear: {\n    base: \"linear-gradient\",\n    repeating: \"repeating-linear-gradient\",\n  },\n  conic: {\n    base: \"conic-gradient\",\n    repeating: \"repeating-conic-gradient\",\n  },\n  radial: {\n    base: \"radial-gradient\",\n    repeating: \"repeating-radial-gradient\",\n  },\n};\n\n/**\n * Checks if a CSS value string starts with a gradient function of the specified type.\n * Handles both base and repeating gradient functions (e.g., \"linear-gradient\" and \"repeating-linear-gradient\").\n */\nconst startsWithGradientFunction = (value: string, type: GradientType) => {\n  const normalized = value.trim().toLowerCase();\n  const { base, repeating } = gradientFunctionNames[type];\n  return (\n    normalized.startsWith(`${base}(`) || normalized.startsWith(`${repeating}(`)\n  );\n};\n\nconst angleUnitTokens = [\"deg\", \"grad\", \"rad\", \"turn\"] as const;\ntype AngleUnit = (typeof angleUnitTokens)[number];\nconst angleUnitSet = new Set<AngleUnit>(angleUnitTokens);\nconst fullCircleDegrees = 360;\n\nexport const angleUnitOptions = angleUnitTokens.map((unit) => ({\n  id: unit,\n  label: unit,\n  type: \"unit\" as const,\n}));\n\nexport const percentUnitOptions: UnitOption[] = [\n  {\n    id: \"%\" as Unit,\n    label: \"%\",\n    type: \"unit\",\n  },\n];\n\nexport const gradientPositionXOptions: KeywordValue[] = [\n  { type: \"keyword\", value: \"center\" },\n  { type: \"keyword\", value: \"left\" },\n  { type: \"keyword\", value: \"right\" },\n];\n\nexport const gradientPositionYOptions: KeywordValue[] = [\n  { type: \"keyword\", value: \"center\" },\n  { type: \"keyword\", value: \"top\" },\n  { type: \"keyword\", value: \"bottom\" },\n];\n\nconst getAxisPositionValue = (\n  property: CssProperty,\n  value: string | undefined\n): StyleValue | undefined => {\n  if (value === undefined) {\n    return;\n  }\n  const parsed = parseCssValue(property, value);\n  if (parsed.type === \"invalid\") {\n    return;\n  }\n  return parsed;\n};\n\n/**\n * Parses a gradient position string into separate x and y StyleValues.\n * Defaults to \"center center\" if position is undefined or parsing fails.\n */\nexport const parseGradientPositionValues = (position?: string) => {\n  if (position === undefined) {\n    return {\n      xValue: { type: \"keyword\" as const, value: \"center\" },\n      yValue: { type: \"keyword\" as const, value: \"center\" },\n    } as const;\n  }\n  try {\n    const longhands = expandShorthands([[\"background-position\", position]]);\n    const [xLonghand, yLonghand] = longhands;\n    return {\n      // Use the real background-position longhand when parsing so we can reuse\n      // its CSS syntax rules, but assign the result to the gradient-specific\n      // custom property downstream.\n      xValue: getAxisPositionValue(\n        backgroundPositionXLonghand,\n        xLonghand?.[1]\n      ) ?? { type: \"keyword\" as const, value: \"center\" },\n      yValue: getAxisPositionValue(\n        backgroundPositionYLonghand,\n        yLonghand?.[1]\n      ) ?? { type: \"keyword\" as const, value: \"center\" },\n    } as const;\n  } catch {\n    return {\n      xValue: { type: \"keyword\" as const, value: \"center\" },\n      yValue: { type: \"keyword\" as const, value: \"center\" },\n    } as const;\n  }\n};\n\n/**\n * Formats x and y position values into a CSS position string.\n * Omits \"center center\" as it's the default. Returns just x if y is center.\n */\nexport const formatGradientPositionValues = (\n  xValue?: StyleValue,\n  yValue?: StyleValue\n) => {\n  const x = toValue(xValue ?? { type: \"keyword\" as const, value: \"center\" });\n  const y = toValue(yValue ?? { type: \"keyword\" as const, value: \"center\" });\n  if (x === \"center\" && y === \"center\") {\n    return;\n  }\n  if (y === \"center\") {\n    return x;\n  }\n  return `${x} ${y}`;\n};\n\nexport const createPercentUnitValue = (value: number): PercentUnitValue => ({\n  type: \"unit\",\n  unit: \"%\" as const,\n  value: clampPercentValue(value),\n});\n\nconst isAngleUnit = (unit: string): unit is AngleUnit =>\n  angleUnitSet.has(unit as AngleUnit);\n\nconst clampPercentValue = (value: number) => clamp(value, 0, 100);\n\nconst angleUnitToDegrees = (value: UnitValue): number | undefined => {\n  switch (value.unit) {\n    case \"deg\":\n      return value.value;\n    case \"grad\":\n      return (value.value * 360) / 400;\n    case \"rad\":\n      return (value.value * 180) / Math.PI;\n    case \"turn\":\n      return value.value * 360;\n    default:\n      return;\n  }\n};\n\n/**\n * Converts a UnitValue to a PercentUnitValue.\n * For percent units, clamps to 0-100. For angle units, converts to percent (0-100 representing 0-360deg).\n * Returns undefined for unsupported unit types.\n */\nconst toPercentUnitValue = (value: UnitValue): PercentUnitValue | undefined => {\n  if (value.unit === \"%\") {\n    return {\n      type: \"unit\" as const,\n      unit: \"%\" as const,\n      value: clampPercentValue(value.value),\n    } satisfies PercentUnitValue;\n  }\n\n  if (isAngleUnit(value.unit) === false) {\n    return;\n  }\n\n  const degrees = angleUnitToDegrees(value);\n  if (degrees === undefined || Number.isFinite(degrees) === false) {\n    return;\n  }\n\n  const normalizedDegrees =\n    ((degrees % fullCircleDegrees) + fullCircleDegrees) % fullCircleDegrees;\n  const percentValue = clampPercentValue(\n    (normalizedDegrees / fullCircleDegrees) * 100\n  );\n\n  return {\n    type: \"unit\" as const,\n    unit: \"%\" as const,\n    value: percentValue,\n  } satisfies PercentUnitValue;\n};\n\nexport const fallbackStopColor: ColorValue = {\n  type: \"color\",\n  colorSpace: \"srgb\",\n  components: [0, 0, 0],\n  alpha: 1,\n};\n\nconst transparentColor: ColorValue = {\n  type: \"color\",\n  colorSpace: \"srgb\",\n  components: [0, 0, 0],\n  alpha: 0,\n};\n\nexport const createDefaultStops = (): GradientStop[] => [\n  {\n    color: fallbackStopColor,\n    position: { type: \"unit\", unit: \"%\", value: 0 },\n  },\n  {\n    color: transparentColor,\n    position: { type: \"unit\", unit: \"%\", value: 100 },\n  },\n];\n\nconst createDefaultLinearGradient = (): ParsedLinearGradient => ({\n  type: \"linear\",\n  stops: createDefaultStops(),\n});\n\nconst createDefaultConicGradient = (): ParsedConicGradient => ({\n  type: \"conic\",\n  stops: createDefaultStops(),\n});\n\nconst createDefaultRadialGradient = (): ParsedRadialGradient => ({\n  type: \"radial\",\n  stops: createDefaultStops(),\n});\n\ntype CreateDefaultGradient = {\n  (type: \"linear\"): ParsedLinearGradient;\n  (type: \"conic\"): ParsedConicGradient;\n  (type: \"radial\"): ParsedRadialGradient;\n  (type: GradientType): ParsedGradient;\n};\n\nexport const createDefaultGradient = ((type: GradientType) => {\n  switch (type) {\n    case \"linear\":\n      return createDefaultLinearGradient();\n    case \"conic\":\n      return createDefaultConicGradient();\n    case \"radial\":\n      return createDefaultRadialGradient();\n  }\n}) as CreateDefaultGradient;\n\nexport const isLinearGradient = (\n  gradient: ParsedGradient\n): gradient is ParsedLinearGradient => gradient.type === \"linear\";\n\nexport const isConicGradient = (\n  gradient: ParsedGradient\n): gradient is ParsedConicGradient => gradient.type === \"conic\";\n\nexport const isRadialGradient = (\n  gradient: ParsedGradient\n): gradient is ParsedRadialGradient => gradient.type === \"radial\";\n\n/**\n * Returns the CSS spec default angle for each gradient type:\n * - linear-gradient: 180deg (to bottom)\n * - conic-gradient: 0deg (from top)\n * - radial-gradient: undefined (no angle)\n */\nexport const getDefaultAngle = (\n  gradient: ParsedGradient\n): UnitValue | undefined => {\n  if (isLinearGradient(gradient)) {\n    return { type: \"unit\", unit: \"deg\", value: 180 };\n  }\n  if (isConicGradient(gradient)) {\n    return { type: \"unit\", unit: \"deg\", value: 0 };\n  }\n  return;\n};\n\nexport const getPercentUnit = (\n  styleValue: StyleValue | undefined\n): PercentUnitValue | undefined => {\n  if (styleValue === undefined) {\n    return;\n  }\n\n  if (styleValue.type === \"unit\" && styleValue.unit === \"%\") {\n    return {\n      type: \"unit\" as const,\n      unit: \"%\" as const,\n      value: styleValue.value,\n    } satisfies PercentUnitValue;\n  }\n\n  if (styleValue.type === \"layers\") {\n    const firstLayer = styleValue.value[0];\n    if (firstLayer?.type === \"unit\" && firstLayer.unit === \"%\") {\n      return {\n        type: \"unit\" as const,\n        unit: \"%\" as const,\n        value: firstLayer.value,\n      } satisfies PercentUnitValue;\n    }\n  }\n};\n\n/**\n * Converts repeating-*-gradient to *-gradient while preserving leading whitespace.\n * Returns both the normalized string and whether it was originally repeating.\n */\nexport const normalizeGradientInput = (\n  gradientString: string,\n  gradientType: GradientType\n): NormalizedGradient => {\n  const leadingWhitespaceMatch = gradientString.match(/^\\s*/);\n  const leadingWhitespace = leadingWhitespaceMatch?.[0] ?? \"\";\n  const withoutLeading = gradientString.slice(leadingWhitespace.length);\n  const lowerCase = withoutLeading.toLowerCase();\n  const { base, repeating } = gradientFunctionNames[gradientType];\n\n  if (lowerCase.startsWith(repeating)) {\n    const suffix = withoutLeading.slice(repeating.length);\n    return {\n      normalizedGradientString: `${leadingWhitespace}${base}${suffix}`,\n      initialIsRepeating: true,\n    } satisfies NormalizedGradient;\n  }\n\n  return {\n    normalizedGradientString: gradientString,\n    initialIsRepeating: false,\n  } satisfies NormalizedGradient;\n};\n\nexport const sideOrCornerToAngle = (\n  sideOrCorner: KeywordValue | undefined\n): number | undefined => {\n  if (sideOrCorner === undefined) {\n    return;\n  }\n  const normalized = sideOrCorner.value.trim().toLowerCase();\n  if (normalized.startsWith(\"to \") === false) {\n    return;\n  }\n  const tokens = normalized.slice(3).split(/\\s+/).filter(Boolean);\n  if (tokens.length === 0) {\n    return;\n  }\n  const tokenSet = new Set(tokens);\n  const has = (token: string) => tokenSet.has(token);\n  if (tokenSet.size === 1) {\n    if (has(\"top\")) {\n      return 0;\n    }\n    if (has(\"right\")) {\n      return 90;\n    }\n    if (has(\"bottom\")) {\n      return 180;\n    }\n    if (has(\"left\")) {\n      return 270;\n    }\n    return;\n  }\n  if (tokenSet.size === 2) {\n    if (has(\"top\") && has(\"right\")) {\n      return 45;\n    }\n    if (has(\"bottom\") && has(\"right\")) {\n      return 135;\n    }\n    if (has(\"bottom\") && has(\"left\")) {\n      return 225;\n    }\n    if (has(\"top\") && has(\"left\")) {\n      return 315;\n    }\n  }\n};\n\n/**\n * Interpolates missing stop positions proportionally between defined positions.\n * Only works with percent units - returns original gradient for non-percent units.\n */\nexport const fillMissingStopPositions = <T extends ParsedGradient>(\n  gradient: T\n): T => {\n  const stops = gradient.stops;\n  if (stops.length === 0) {\n    return gradient;\n  }\n\n  const hasMissingPositions = stops.some((stop) => stop.position === undefined);\n  if (hasMissingPositions === false) {\n    return gradient;\n  }\n\n  const hasUnsupportedPosition = stops.some(\n    (stop) =>\n      stop.position !== undefined &&\n      (stop.position.type !== \"unit\" || stop.position.unit !== \"%\")\n  );\n  if (hasUnsupportedPosition) {\n    return gradient;\n  }\n\n  const totalStops = stops.length;\n  const values = stops.map((stop) =>\n    stop.position?.type === \"unit\" ? stop.position.value : undefined\n  );\n  const nextValues = [...values];\n  const definedCount = nextValues.filter(\n    (value): value is number => value !== undefined\n  ).length;\n\n  if (definedCount === 0) {\n    if (totalStops === 1) {\n      nextValues[0] = 0;\n    } else {\n      for (let index = 0; index < totalStops; index += 1) {\n        const value = (index / (totalStops - 1)) * 100;\n        nextValues[index] = clamp(value, 0, 100);\n      }\n    }\n  } else {\n    if (nextValues[0] === undefined) {\n      nextValues[0] = 0;\n    }\n    if (nextValues[totalStops - 1] === undefined) {\n      nextValues[totalStops - 1] = 100;\n    }\n\n    let start = 0;\n    while (start < totalStops) {\n      const startValue = nextValues[start];\n      if (startValue === undefined) {\n        start += 1;\n        continue;\n      }\n      let end = start + 1;\n      while (end < totalStops && nextValues[end] === undefined) {\n        end += 1;\n      }\n      if (end >= totalStops) {\n        break;\n      }\n      const endValue = nextValues[end];\n      if (endValue === undefined) {\n        break;\n      }\n      const span = end - start;\n      for (let offset = 1; offset < span; offset += 1) {\n        const interpolated =\n          startValue + ((endValue - startValue) * offset) / span;\n        nextValues[start + offset] = clamp(interpolated, 0, 100);\n      }\n      start = end;\n    }\n  }\n\n  const nextStops = stops.map((stop, index) => {\n    if (stop.position !== undefined) {\n      return stop;\n    }\n    const value = nextValues[index];\n    if (value === undefined) {\n      return stop;\n    }\n    return {\n      ...stop,\n      position: {\n        type: \"unit\" as const,\n        unit: \"%\" as const,\n        value,\n      },\n    };\n  });\n\n  return {\n    ...gradient,\n    stops: nextStops,\n  } as T;\n};\n\n/**\n * Clones stop values, deep cloning var fallbacks to avoid shared references.\n */\nconst cloneGradientStopValue = <\n  Value extends GradientStop[\"position\"] | GradientStop[\"hint\"],\n>(\n  value: Value\n): Value => {\n  if (value === undefined) {\n    return value;\n  }\n\n  if (value.type === \"var\") {\n    return {\n      ...value,\n      fallback: value.fallback && { ...value.fallback },\n    };\n  }\n\n  return { ...value };\n};\n\n/**\n * Clones colors, deep cloning var fallbacks to avoid shared references.\n */\nconst cloneGradientStopColor = (\n  color: GradientStop[\"color\"] | undefined\n): GradientStop[\"color\"] => {\n  if (color === undefined) {\n    return { ...fallbackStopColor };\n  }\n  if (color.type === \"var\") {\n    return {\n      ...color,\n      fallback: color.fallback && { ...color.fallback },\n    };\n  }\n  return { ...color };\n};\n\n/**\n * Creates a solid color gradient (two identical stops at 0% and 100%).\n */\nexport const createSolidLinearGradient = (\n  color: GradientStop[\"color\"],\n  base?: ParsedLinearGradient\n): ParsedLinearGradient => {\n  const firstColor = cloneGradientStopColor(color);\n  const secondColor = cloneGradientStopColor(color);\n  return {\n    type: \"linear\",\n    angle: base?.angle,\n    sideOrCorner: base?.sideOrCorner,\n    stops: [\n      {\n        color: firstColor,\n        position: { type: \"unit\", unit: \"%\", value: 0 },\n      },\n      {\n        color: secondColor,\n        position: { type: \"unit\", unit: \"%\", value: 100 },\n      },\n    ],\n  };\n};\n\ntype AngleUnitValue = UnitValue & { unit: AngleUnit };\ntype AngleValue = AngleUnitValue | VarValue;\n\nconst resolveAnglePrimitive = (\n  value: VarValue | UnitValue | undefined\n): AngleValue | undefined => {\n  if (value === undefined) {\n    return;\n  }\n\n  if (value.type === \"var\") {\n    return {\n      ...value,\n      fallback: value.fallback && { ...value.fallback },\n    };\n  }\n\n  if (value.type === \"unit\" && isAngleUnit(value.unit)) {\n    return {\n      ...value,\n      unit: value.unit,\n    };\n  }\n\n  return;\n};\n\nexport const resolveAngleValue = (\n  styleValue: StyleValue | undefined\n): AngleValue | undefined => {\n  if (styleValue === undefined) {\n    return;\n  }\n\n  if (styleValue.type === \"tuple\") {\n    const first = styleValue.value[0];\n    if (first?.type === \"var\" || first?.type === \"unit\") {\n      return resolveAnglePrimitive(first);\n    }\n    return;\n  }\n\n  if (styleValue.type === \"layers\") {\n    const firstLayer = styleValue.value[0];\n    if (firstLayer?.type === \"var\" || firstLayer?.type === \"unit\") {\n      return resolveAnglePrimitive(firstLayer);\n    }\n    return;\n  }\n\n  if (styleValue.type === \"var\" || styleValue.type === \"unit\") {\n    return resolveAnglePrimitive(styleValue);\n  }\n\n  return;\n};\n\nexport const formatGradientValue = (gradient: ParsedGradient) => {\n  if (isLinearGradient(gradient)) {\n    return formatLinearGradient(gradient);\n  }\n  if (isConicGradient(gradient)) {\n    return formatConicGradient(gradient);\n  }\n  return formatRadialGradient(gradient);\n};\n\nconst resolveVarPercentUnit = (\n  value: GradientStop[\"position\"] | GradientStop[\"hint\"]\n): PercentUnitValue | undefined => {\n  if (value?.type !== \"var\") {\n    return;\n  }\n  const fallback = value.fallback;\n  if (fallback?.type === \"unit\") {\n    const percentFallback = toPercentUnitValue(fallback);\n    if (percentFallback !== undefined) {\n      return percentFallback;\n    }\n  }\n};\n\nconst normalizeStopsForPicker = <T extends ParsedGradient>(gradient: T): T => {\n  let changed = false;\n  const stops = gradient.stops.map((stop) => {\n    let nextPosition = stop.position;\n    let nextHint = stop.hint;\n    let stopChanged = false;\n\n    if (stop.position?.type === \"var\") {\n      nextPosition = resolveVarPercentUnit(stop.position);\n      stopChanged = true;\n    }\n\n    if (stop.hint?.type === \"var\") {\n      nextHint = resolveVarPercentUnit(stop.hint);\n      stopChanged = true;\n    }\n\n    if (stopChanged) {\n      changed = true;\n      return {\n        ...stop,\n        position: nextPosition,\n        hint: nextHint,\n      } satisfies GradientStop;\n    }\n\n    return stop;\n  });\n\n  if (changed === false) {\n    return gradient;\n  }\n\n  return {\n    ...gradient,\n    stops,\n  } as T;\n};\n\n/**\n * Converts conic angle units (deg, turn, etc.) to percent units for picker UI.\n */\nconst convertConicStopsToPercent = <T extends ParsedGradient>(\n  gradient: T\n): T => {\n  if (isConicGradient(gradient) === false) {\n    return gradient;\n  }\n\n  let changed = false;\n  const stops = gradient.stops.map((stop) => {\n    let nextPosition = stop.position;\n    let nextHint = stop.hint;\n    let stopChanged = false;\n\n    if (stop.position?.type === \"unit\") {\n      const percentPosition = toPercentUnitValue(stop.position);\n      if (\n        percentPosition !== undefined &&\n        (stop.position.unit !== percentPosition.unit ||\n          stop.position.value !== percentPosition.value)\n      ) {\n        nextPosition = percentPosition;\n        stopChanged = true;\n      }\n    }\n\n    if (stop.hint?.type === \"unit\") {\n      const percentHint = toPercentUnitValue(stop.hint);\n      if (\n        percentHint !== undefined &&\n        (stop.hint.unit !== percentHint.unit ||\n          stop.hint.value !== percentHint.value)\n      ) {\n        nextHint = percentHint;\n        stopChanged = true;\n      }\n    }\n\n    if (stopChanged) {\n      changed = true;\n      return {\n        ...stop,\n        position: nextPosition,\n        hint: nextHint,\n      } satisfies GradientStop;\n    }\n\n    return stop;\n  });\n\n  if (changed === false) {\n    return gradient;\n  }\n\n  return {\n    ...gradient,\n    stops,\n  } as T;\n};\n\n/**\n * Prepares gradient for picker UI: converts angles to percent, resolves vars, fills positions, applies hint overrides.\n */\nexport const resolveGradientForPicker = <T extends ParsedGradient>(\n  gradient: T,\n  hintOverrides: ReadonlyMap<number, PercentUnitValue>\n): T => {\n  const withPercentualStops = convertConicStopsToPercent(gradient);\n  const normalized = normalizeStopsForPicker(withPercentualStops);\n  const filled = fillMissingStopPositions(normalized);\n  if (hintOverrides.size === 0) {\n    return filled;\n  }\n  let changed = false;\n  const stopsWithHints = filled.stops.map((stop, index) => {\n    if (stop.hint !== undefined) {\n      return stop;\n    }\n    const override = hintOverrides.get(index);\n    if (override === undefined) {\n      return stop;\n    }\n    changed = true;\n    return {\n      ...stop,\n      hint: override,\n    };\n  });\n  if (changed === false) {\n    return filled;\n  }\n  return {\n    ...filled,\n    stops: stopsWithHints,\n  } as T;\n};\n\n/**\n * Returns same map reference if unchanged for referential equality.\n */\nexport const removeHintOverride = (\n  overrides: Map<number, PercentUnitValue>,\n  stopIndex: number\n): Map<number, PercentUnitValue> => {\n  if (overrides.has(stopIndex) === false) {\n    return overrides;\n  }\n  const next = new Map(overrides);\n  next.delete(stopIndex);\n  return next;\n};\n\n/**\n * Returns same map reference if unchanged for referential equality.\n */\nexport const setHintOverride = (\n  overrides: Map<number, PercentUnitValue>,\n  stopIndex: number,\n  override: PercentUnitValue\n): Map<number, PercentUnitValue> => {\n  const existing = overrides.get(stopIndex);\n  if (existing !== undefined) {\n    if (existing === override) {\n      return overrides;\n    }\n    if (\n      existing.type === override.type &&\n      existing.unit === override.unit &&\n      existing.value === override.value\n    ) {\n      return overrides;\n    }\n  }\n  const next = new Map(overrides);\n  next.set(stopIndex, override);\n  return next;\n};\n\n/**\n * Returns same map reference if unchanged for referential equality.\n */\nexport const pruneHintOverrides = (\n  overrides: Map<number, PercentUnitValue>,\n  stopCount: number\n): Map<number, PercentUnitValue> => {\n  if (overrides.size === 0) {\n    return overrides;\n  }\n  let changed = false;\n  const next = new Map(overrides);\n  for (const [index] of overrides) {\n    if (index >= stopCount) {\n      next.delete(index);\n      changed = true;\n    }\n  }\n  return changed ? next : overrides;\n};\n\nexport const getStopPosition = (stop: GradientStop): number =>\n  stop.position?.type === \"unit\" && stop.position.unit === \"%\"\n    ? stop.position.value\n    : 0;\n\nexport const reindexHintOverrides = (\n  overrides: Map<number, PercentUnitValue>,\n  deletedIndex: number\n): Map<number, PercentUnitValue> => {\n  const reindexed = new Map<number, PercentUnitValue>();\n  overrides.forEach((value, key) => {\n    if (key < deletedIndex) {\n      reindexed.set(key, value);\n    } else if (key > deletedIndex) {\n      reindexed.set(key - 1, value);\n    }\n  });\n  return reindexed;\n};\n\nexport const sortGradientStops = (\n  gradient: ParsedGradient,\n  hintOverrides: Map<number, PercentUnitValue>\n): {\n  sortedGradient: ParsedGradient;\n  reindexedHints: Map<number, PercentUnitValue>;\n} => {\n  // Create array of stops with their original indices and hint overrides\n  const stopsWithData = gradient.stops.map((stop, originalIndex) => ({\n    stop,\n    originalIndex,\n    hint: hintOverrides.get(originalIndex),\n  }));\n\n  // Sort by position\n  stopsWithData.sort((a, b) => {\n    const posA = getStopPosition(a.stop);\n    const posB = getStopPosition(b.stop);\n    return posA - posB;\n  });\n\n  // Extract sorted stops and rebuild hint overrides with new indices\n  const sortedStops = stopsWithData.map(({ stop }) => stop);\n  const reindexedHints = new Map<number, PercentUnitValue>();\n  stopsWithData.forEach(({ hint }, newIndex) => {\n    if (hint !== undefined) {\n      reindexedHints.set(newIndex, hint);\n    }\n  });\n\n  return {\n    sortedGradient: {\n      ...gradient,\n      stops: sortedStops,\n    },\n    reindexedHints,\n  };\n};\n\nexport type ReverseStopsResolution<T extends ParsedGradient> =\n  | {\n      type: \"apply\";\n      gradient: T;\n      selectedStopIndex: number;\n    }\n  | { type: \"none\" };\n\n/**\n * Reverses stops and mirrors percent positions (0% becomes 100%, etc.).\n */\nexport const resolveReverseStops = <T extends ParsedGradient>(\n  gradient: T,\n  selectedStopIndex: number\n): ReverseStopsResolution<T> => {\n  if (gradient.stops.length <= 1) {\n    return { type: \"none\" } satisfies ReverseStopsResolution<T>;\n  }\n  const stopIndex = clampStopIndex(selectedStopIndex, gradient);\n  const reversedStops = [...gradient.stops].reverse();\n  const nextStops = reversedStops.map((stop) => {\n    let position = stop.position;\n    if (position?.type === \"unit\" && position.unit === \"%\") {\n      position = {\n        ...position,\n        value: clamp(100 - position.value, 0, 100),\n      } as PercentUnitValue;\n    }\n    return { ...stop, position } satisfies GradientStop;\n  });\n  const nextGradient = { ...gradient, stops: nextStops } as T;\n  const nextSelectedIndex = gradient.stops.length - 1 - stopIndex;\n  return {\n    type: \"apply\",\n    gradient: nextGradient,\n    selectedStopIndex: nextSelectedIndex,\n  } satisfies ReverseStopsResolution<T>;\n};\n\nexport type StopPositionUpdateResolution =\n  | {\n      type: \"apply\";\n      position: GradientStop[\"position\"];\n      clearHintOverrides: boolean;\n    }\n  | { type: \"none\" };\n\nexport const resolveStopPositionUpdate = (\n  styleValue: StyleValue\n): StopPositionUpdateResolution => {\n  if (styleValue.type === \"var\") {\n    return {\n      type: \"apply\",\n      position: cloneGradientStopValue(styleValue),\n      clearHintOverrides: true,\n    } satisfies StopPositionUpdateResolution;\n  }\n\n  const percentUnit = getPercentUnit(styleValue);\n  if (percentUnit === undefined) {\n    return { type: \"none\" } satisfies StopPositionUpdateResolution;\n  }\n\n  const normalized: PercentUnitValue = {\n    ...percentUnit,\n    value: clamp(percentUnit.value, 0, 100),\n  } satisfies PercentUnitValue;\n  return {\n    type: \"apply\",\n    position: normalized,\n    clearHintOverrides: true,\n  } satisfies StopPositionUpdateResolution;\n};\n\nexport type StopHintUpdateResolution =\n  | {\n      type: \"apply\";\n      hint: GradientStop[\"hint\"];\n      clearOverride: boolean;\n      override?: PercentUnitValue;\n    }\n  | { type: \"none\" };\n\nexport const resolveStopHintUpdate = (\n  styleValue: StyleValue\n): StopHintUpdateResolution => {\n  if (styleValue.type === \"var\") {\n    return {\n      type: \"apply\",\n      hint: cloneGradientStopValue(styleValue),\n      clearOverride: true,\n    };\n  }\n\n  const percentUnit = getPercentUnit(styleValue);\n  if (percentUnit === undefined) {\n    return { type: \"none\" };\n  }\n\n  // Clamp the value inline\n  const normalized: PercentUnitValue = {\n    ...percentUnit,\n    value: clamp(percentUnit.value, 0, 100),\n  };\n\n  return {\n    type: \"apply\",\n    hint: normalized,\n    override: normalized,\n    clearOverride: false,\n  };\n};\n\nexport const ensureGradientHasStops = <T extends ParsedGradient>(\n  gradient: T\n): T => {\n  const stops =\n    gradient.stops.length === 0\n      ? createDefaultStops()\n      : gradient.stops.map((stop) =>\n          stop.color === undefined\n            ? {\n                ...stop,\n                color: { ...fallbackStopColor },\n              }\n            : stop\n        );\n\n  return {\n    ...gradient,\n    stops,\n  } as T;\n};\n\nexport const clampStopIndex = <T extends ParsedGradient>(\n  index: number,\n  gradient: T\n) => clamp(index, 0, Math.max(gradient.stops.length - 1, 0));\n\n/**\n * Updates a gradient stop with automatic hint offset maintenance.\n * When a stop's position changes and it has a hint, the hint is adjusted\n * to maintain the same offset relative to the stop's new position.\n * This matches the behavior of the gradient picker during drag operations.\n */\nexport const updateGradientStop = <T extends ParsedGradient>(\n  gradient: T,\n  stopIndex: number,\n  updater: (stop: GradientStop) => GradientStop\n): T => {\n  const currentStop = gradient.stops[stopIndex];\n  if (currentStop === undefined) {\n    return gradient;\n  }\n\n  // Calculate hint offset before updating (like gradient picker does)\n  const currentPosition = getStopPosition(currentStop);\n  const currentHint =\n    currentStop.hint?.type === \"unit\" && currentStop.hint.unit === \"%\"\n      ? currentStop.hint.value\n      : undefined;\n  const hintOffset =\n    currentHint !== undefined ? currentHint - currentPosition : 0;\n\n  const stops = gradient.stops.map((stop, index) => {\n    if (index !== stopIndex) {\n      return stop;\n    }\n\n    const updatedStop = updater(stop);\n\n    // If position changed and stop has a hint, maintain the hint offset\n    if (\n      hintOffset !== 0 &&\n      stop.hint?.type === \"unit\" &&\n      stop.hint.unit === \"%\" &&\n      updatedStop.position !== stop.position\n    ) {\n      const newPosition = getStopPosition(updatedStop);\n      return {\n        ...updatedStop,\n        hint: createPercentUnitValue(newPosition + hintOffset),\n      };\n    }\n\n    return updatedStop;\n  });\n\n  return {\n    ...gradient,\n    stops,\n  };\n};\n\nconst parseColorString = (value: string): GradientStop[\"color\"] | undefined => {\n  const parsed = parseCssValue(\"color\", value);\n  if (\n    parsed.type === \"color\" ||\n    parsed.type === \"rgb\" ||\n    parsed.type === \"keyword\" ||\n    parsed.type === \"var\"\n  ) {\n    return parsed;\n  }\n};\n\nexport const styleValueToColor = (\n  styleValue: StyleValue | IntermediateColorValue | undefined\n): GradientStop[\"color\"] | undefined => {\n  if (styleValue === undefined) {\n    return;\n  }\n\n  if (styleValue.type === \"intermediate\") {\n    return parseColorString(styleValue.value);\n  }\n\n  if (styleValue.type === \"rgb\" || styleValue.type === \"color\") {\n    return styleValue;\n  }\n\n  if (styleValue.type === \"var\") {\n    return styleValue;\n  }\n\n  if (styleValue.type === \"keyword\") {\n    return parseColorString(styleValue.value);\n  }\n\n  if (styleValue.type === \"invalid\") {\n    return parseColorString(styleValue.value);\n  }\n\n  return parseColorString(toValue(styleValue));\n};\n\nexport type BackgroundType =\n  | \"image\"\n  | \"linearGradient\"\n  | \"conicGradient\"\n  | \"radialGradient\"\n  | \"solid\";\n\nexport const isSolidLinearGradient = (gradient: ParsedLinearGradient) => {\n  // Only consider it a solid gradient if there are exactly 2 stops\n  if (gradient.stops.length !== 2) {\n    return false;\n  }\n\n  // Fill in default positions (0% and 100%) for any missing positions\n  const normalized = fillMissingStopPositions(gradient);\n  const firstStop = normalized.stops[0];\n  const secondStop = normalized.stops[1];\n\n  // Check if both stops have the same color\n  const firstColor = firstStop?.color ? toValue(firstStop.color) : undefined;\n  const secondColor = secondStop?.color ? toValue(secondStop.color) : undefined;\n\n  if (\n    firstColor === undefined ||\n    secondColor === undefined ||\n    firstColor !== secondColor\n  ) {\n    return false;\n  }\n\n  // Check if first position is 0% and second position is 100%\n  const firstPosition = firstStop?.position;\n  const secondPosition = secondStop?.position;\n\n  const isFirstAt0 =\n    firstPosition?.type === \"unit\" &&\n    firstPosition.unit === \"%\" &&\n    firstPosition.value === 0;\n\n  const isSecondAt100 =\n    secondPosition?.type === \"unit\" &&\n    secondPosition.unit === \"%\" &&\n    secondPosition.value === 100;\n\n  return isFirstAt0 && isSecondAt100;\n};\n\nconst formatSolidColorGradient = (styleValue: StyleValue | undefined) => {\n  const cssValue = styleValue === undefined ? \"\" : toValue(styleValue);\n  const gradientString = typeof cssValue === \"string\" ? cssValue : \"\";\n  const parsed =\n    gradientString.length > 0 ? parseAnyGradient(gradientString) : undefined;\n  const parsedLinear =\n    parsed?.type === \"linear\" ? parsed : createDefaultLinearGradient();\n  const source = parsedLinear;\n  const baseColor = source.stops[0]?.color ?? createDefaultStops()[0].color;\n  const solidStops: GradientStop[] = [\n    {\n      color: baseColor,\n      position: { type: \"unit\", unit: \"%\", value: 0 },\n    },\n    {\n      color: baseColor,\n      position: { type: \"unit\", unit: \"%\", value: 100 },\n    },\n  ];\n  return formatLinearGradient({\n    type: \"linear\",\n    angle: source.angle,\n    sideOrCorner: source.sideOrCorner,\n    stops: solidStops,\n  });\n};\n\ntype GradientByType<T extends GradientType> = Extract<\n  ParsedGradient,\n  { type: T }\n>;\n\n/**\n * Cached to avoid re-parsing the same string.\n */\nconst parsedGradientCache = new Map<string, ParsedGradient | undefined>();\n\nexport const parseAnyGradient = (value: string): ParsedGradient | undefined => {\n  // Check cache first\n  if (parsedGradientCache.has(value)) {\n    return parsedGradientCache.get(value);\n  }\n\n  // Parse gradient\n  const parsed =\n    parseLinearGradient(value) ??\n    parseConicGradient(value) ??\n    parseRadialGradient(value);\n\n  // Cache the result\n  parsedGradientCache.set(value, parsed);\n\n  return parsed;\n};\n\nexport const convertGradientToTarget = <Target extends GradientType>(\n  styleValue: StyleValue | undefined,\n  target: Target\n): GradientByType<Target> => {\n  const cssValue = styleValue === undefined ? \"\" : toValue(styleValue);\n  const gradientString = typeof cssValue === \"string\" ? cssValue : \"\";\n  const parsed =\n    gradientString.length > 0 ? parseAnyGradient(gradientString) : undefined;\n  const source = ensureGradientHasStops(\n    parsed ?? createDefaultGradient(target)\n  );\n  if (source.type === target) {\n    return source as GradientByType<Target>;\n  }\n  const template = createDefaultGradient(target);\n  const converted = {\n    ...template,\n    repeating: source.repeating,\n    stops: source.stops,\n  } satisfies ParsedGradient;\n  return converted as GradientByType<Target>;\n};\n\n/**\n * Formats a gradient for a specific background type.\n * Handles solid color conversion and gradient type conversions.\n * Returns the formatted CSS gradient string.\n */\nexport const formatGradientForType = (\n  styleValue: StyleValue | undefined,\n  target: Exclude<BackgroundType, \"image\">\n) => {\n  if (target === \"solid\") {\n    return formatSolidColorGradient(styleValue);\n  }\n  if (target === \"linearGradient\") {\n    const parsed = convertGradientToTarget(styleValue, \"linear\");\n    return formatLinearGradient(parsed);\n  }\n\n  if (target === \"conicGradient\") {\n    const parsed = convertGradientToTarget(styleValue, \"conic\");\n    return formatConicGradient(parsed);\n  }\n\n  const parsed = convertGradientToTarget(styleValue, \"radial\");\n  return formatRadialGradient(parsed);\n};\n\n/**\n * Detects the background type from a StyleValue.\n * Returns \"solid\" for uniform linear gradients, specific gradient types for gradients,\n * and \"image\" for non-gradient values or unparseable gradients.\n */\nexport const detectBackgroundType = (\n  styleValue?: StyleValue\n): BackgroundType => {\n  if (styleValue === undefined) {\n    return \"image\";\n  }\n\n  if (styleValue.type === \"image\") {\n    return \"image\";\n  }\n\n  if (styleValue.type === \"keyword\") {\n    // The only allowed keyword for backgroundImage is none\n    return \"image\";\n  }\n\n  const cssValue = toValue(styleValue);\n  if (typeof cssValue === \"string\") {\n    // Use parseAnyGradient (which is cached) for all gradient detection\n    const parsed = parseAnyGradient(cssValue);\n\n    if (parsed !== undefined) {\n      if (parsed.type === \"linear\") {\n        return isSolidLinearGradient(parsed) ? \"solid\" : \"linearGradient\";\n      }\n      if (parsed.type === \"conic\") {\n        return \"conicGradient\";\n      }\n      if (parsed.type === \"radial\") {\n        return \"radialGradient\";\n      }\n    }\n\n    // Fallback checks for unparseable gradients\n    if (startsWithGradientFunction(cssValue, \"conic\")) {\n      return \"conicGradient\";\n    }\n\n    if (startsWithGradientFunction(cssValue, \"radial\")) {\n      return \"radialGradient\";\n    }\n\n    if (startsWithGradientFunction(cssValue, \"linear\")) {\n      return \"linearGradient\";\n    }\n  }\n\n  return \"image\";\n};\n\n/**\n * Gets a specific background layer item from a ComputedStyleDecl.\n * For index 0, returns the first layer or the cascaded value itself.\n * For index > 0, delegates to getRepeatedStyleItem.\n */\nexport const getBackgroundStyleItem = (\n  styleDecl: ComputedStyleDecl,\n  index: number\n) => {\n  const repeatedItem = getRepeatedStyleItem(styleDecl, index);\n  if (repeatedItem !== undefined) {\n    return repeatedItem;\n  }\n\n  if (index > 0) {\n    return;\n  }\n\n  const cascaded = styleDecl.cascadedValue;\n  if (cascaded.type === \"layers\" || cascaded.type === \"tuple\") {\n    return cascaded.value[0];\n  }\n\n  return cascaded;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/border-color.tsx",
    "content": "import { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport { Box, Grid } from \"@webstudio-is/design-system\";\nimport { rowCss } from \"./utils\";\nimport { PropertyLabel, PropertyValueTooltip } from \"../../property-label\";\nimport { ColorPickerControl } from \"../../shared/color-picker\";\nimport {\n  $availableColorVariables,\n  useComputedStyles,\n} from \"../../shared/model\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\nimport { keywordValues } from \"@webstudio-is/css-data\";\n\nexport const properties = [\n  \"border-top-color\",\n  \"border-right-color\",\n  \"border-bottom-color\",\n  \"border-left-color\",\n] satisfies [CssProperty, ...CssProperty[]];\n\nexport const BorderColor = () => {\n  const styles = useComputedStyles(properties);\n  const serialized = styles.map((styleDecl) =>\n    toValue(styleDecl.cascadedValue)\n  );\n  const isAdvanced = new Set(serialized).size > 1;\n  // display first set value and reference it in tooltip\n  const local =\n    styles.find(\n      (styleDecl) =>\n        styleDecl.source.name === \"local\" ||\n        styleDecl.source.name === \"overwritten\"\n    ) ?? styles[0];\n\n  const value = local.cascadedValue;\n  const currentColor = local.usedValue;\n\n  return (\n    <Grid css={rowCss}>\n      <PropertyLabel\n        label=\"Color\"\n        description=\"Sets the color of the border\"\n        properties={properties}\n      />\n      <Box css={{ gridColumn: `span 2` }}>\n        <PropertyValueTooltip\n          label=\"Color\"\n          description=\"Sets the color of the border\"\n          properties={properties}\n          isAdvanced={isAdvanced}\n        >\n          <div>\n            <ColorPickerControl\n              disabled={isAdvanced}\n              currentColor={currentColor}\n              property={local.property}\n              value={value}\n              getOptions={() => [\n                ...keywordValues[\"border-top-color\"].map((value) => ({\n                  type: \"keyword\" as const,\n                  value,\n                })),\n                ...$availableColorVariables.get(),\n              ]}\n              onChange={(styleValue) => {\n                const batch = createBatchUpdate();\n                for (const property of properties) {\n                  batch.setProperty(property)(styleValue);\n                }\n                batch.publish({ isEphemeral: true });\n              }}\n              onChangeComplete={(styleValue) => {\n                const batch = createBatchUpdate();\n                for (const property of properties) {\n                  batch.setProperty(property)(styleValue);\n                }\n                batch.publish();\n              }}\n              onAbort={() => {\n                const batch = createBatchUpdate();\n                for (const property of properties) {\n                  batch.deleteProperty(property);\n                }\n                batch.publish({ isEphemeral: true });\n              }}\n              onReset={() => {\n                const batch = createBatchUpdate();\n                for (const property of properties) {\n                  batch.deleteProperty(property);\n                }\n                batch.publish();\n              }}\n            />\n          </div>\n        </PropertyValueTooltip>\n      </Box>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/border-property.tsx",
    "content": "import { type ReactNode } from \"react\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { Box, Grid, ToggleButton } from \"@webstudio-is/design-system\";\nimport { keywordValues } from \"@webstudio-is/css-data\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport { rowCss } from \"./utils\";\nimport { useSelectedInstanceKv } from \"../../shared/instances-kv\";\nimport {\n  getPriorityStyleValueSource,\n  PropertyLabel,\n} from \"../../property-label\";\nimport {\n  createBatchUpdate,\n  deleteProperty,\n  setProperty,\n} from \"../../shared/use-style-data\";\nimport { $availableUnitVariables, useComputedStyles } from \"../../shared/model\";\n\nexport const BorderProperty = ({\n  individualModeIcon,\n  borderPropertyOptions,\n  label,\n  description,\n}: {\n  individualModeIcon?: ReactNode;\n  borderPropertyOptions: Partial<{\n    [property in CssProperty]: { icon?: ReactNode };\n  }>;\n  label: string;\n  description: string;\n}) => {\n  const borderProperties = Object.keys(borderPropertyOptions) as [\n    CssProperty,\n    ...CssProperty[],\n  ];\n  const styles = useComputedStyles(borderProperties);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  const allPropertyValuesAreEqual =\n    new Set(styles.map((styleDecl) => toValue(styleDecl.cascadedValue)))\n      .size === 1;\n\n  /**\n   * We do not use shorthand properties such as borderWidth or borderRadius in our code.\n   * However, in the UI, we can display a single field, and in that case, we can use any property\n   * from the shorthand property set and pass it instead.\n   **/\n  const firstPropertyName = borderProperties[0];\n\n  const [showIndividualMode, setShowIndividualMode] = useSelectedInstanceKv(\n    `${firstPropertyName}-showIndividualMode`,\n    allPropertyValuesAreEqual === false && individualModeIcon !== undefined\n  );\n\n  /**\n   * If the property is displayed in a non-individual mode, we need to provide a value for it.\n   * In Webflow, an empty value is shown. In Figma, the \"Mixed\" keyword is shown.\n   * We have decided to show the first defined value, as it is difficult to determine a maximum value\n   * when there are keywords (such as \"thin\" or \"thick\") and different units involved.\n   **/\n  const value = styles[0].cascadedValue;\n\n  return (\n    <Grid gap={1}>\n      <Grid css={rowCss}>\n        <PropertyLabel\n          label={label}\n          description={description}\n          properties={borderProperties}\n        />\n\n        <Box\n          css={{\n            visibility: showIndividualMode ? \"hidden\" : \"visible\",\n            gridColumn: individualModeIcon ? `span 1` : `span 2`,\n          }}\n        >\n          <CssValueInputContainer\n            property={firstPropertyName}\n            styleSource={styleValueSourceColor}\n            getOptions={() => [\n              ...(keywordValues[firstPropertyName] ?? []).map((value) => ({\n                type: \"keyword\" as const,\n                value,\n              })),\n              ...$availableUnitVariables.get(),\n            ]}\n            value={value}\n            onUpdate={(newValue, options) => {\n              const batch = createBatchUpdate();\n              for (const property of borderProperties) {\n                batch.setProperty(property)(newValue);\n              }\n              batch.publish(options);\n            }}\n            onDelete={(options) => {\n              const batch = createBatchUpdate();\n              for (const property of borderProperties) {\n                batch.deleteProperty(property);\n              }\n              batch.publish(options);\n            }}\n          />\n        </Box>\n\n        {individualModeIcon && (\n          <ToggleButton\n            pressed={showIndividualMode}\n            onPressedChange={setShowIndividualMode}\n          >\n            {individualModeIcon}\n          </ToggleButton>\n        )}\n      </Grid>\n      {showIndividualMode && (\n        <Grid columns={2} gap={1}>\n          {styles.map((styleDecl) => (\n            <CssValueInputContainer\n              key={styleDecl.property}\n              icon={borderPropertyOptions[styleDecl.property]?.icon}\n              property={styleDecl.property}\n              styleSource={styleDecl.source.name}\n              getOptions={() =>\n                (keywordValues[firstPropertyName] ?? []).map((value) => ({\n                  type: \"keyword\" as const,\n                  value,\n                }))\n              }\n              value={styleDecl.cascadedValue}\n              onUpdate={setProperty(styleDecl.property)}\n              onDelete={(options) =>\n                deleteProperty(styleDecl.property, options)\n              }\n            />\n          ))}\n        </Grid>\n      )}\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/border-radius.tsx",
    "content": "import type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  BorderRadiusIndividualIcon,\n  BorderRadiusBottomRightIcon,\n  BorderRadiusTopLeftIcon,\n  BorderRadiusTopRightIcon,\n  BorderRadiusBottomLeftIcon,\n} from \"@webstudio-is/icons\";\nimport { BorderProperty } from \"./border-property\";\n\nexport const properties = [\n  \"border-top-left-radius\",\n  \"border-top-right-radius\",\n  \"border-bottom-left-radius\",\n  \"border-bottom-right-radius\",\n] satisfies Array<CssProperty>;\n\nconst borderPropertyOptions = {\n  \"border-top-left-radius\": {\n    icon: <BorderRadiusTopLeftIcon />,\n  },\n  \"border-top-right-radius\": {\n    icon: <BorderRadiusTopRightIcon />,\n  },\n  \"border-bottom-left-radius\": {\n    icon: <BorderRadiusBottomLeftIcon />,\n  },\n  \"border-bottom-right-radius\": {\n    icon: <BorderRadiusBottomRightIcon />,\n  },\n} as const satisfies Partial<{ [property in CssProperty]: unknown }>;\n\nexport const BorderRadius = () => {\n  return (\n    <BorderProperty\n      label=\"Radius\"\n      description=\"Sets the radius of border\"\n      borderPropertyOptions={borderPropertyOptions}\n      individualModeIcon={<BorderRadiusIndividualIcon />}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/border-style.tsx",
    "content": "import type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { Box, Grid } from \"@webstudio-is/design-system\";\nimport {\n  MinusIcon,\n  DashedBorderIcon,\n  DottedBorderIcon,\n  XSmallIcon,\n} from \"@webstudio-is/icons\";\nimport { ToggleGroupControl } from \"../../controls/toggle-group/toggle-group-control\";\nimport {\n  declarationDescriptions,\n  propertyDescriptions,\n} from \"@webstudio-is/css-data\";\nimport { rowCss } from \"./utils\";\nimport { PropertyLabel } from \"../../property-label\";\n\nexport const properties: [CssProperty, ...CssProperty[]] = [\n  \"border-top-style\",\n  \"border-right-style\",\n  \"border-left-style\",\n  \"border-bottom-style\",\n];\n\nexport const BorderStyle = () => {\n  return (\n    <Grid css={rowCss}>\n      <PropertyLabel\n        label=\"Style\"\n        description={propertyDescriptions.borderBlockStyle}\n        properties={properties}\n      />\n      <Box css={{ gridColumn: `span 2` }}>\n        <ToggleGroupControl\n          label=\"Style\"\n          properties={properties}\n          items={[\n            {\n              child: <XSmallIcon />,\n              description: declarationDescriptions[\"borderBlockStyle:none\"],\n              value: \"none\",\n            },\n            {\n              child: <MinusIcon />,\n              description: declarationDescriptions[\"borderBlockStyle:solid\"],\n              value: \"solid\",\n            },\n            {\n              child: <DashedBorderIcon />,\n              description: declarationDescriptions[\"borderBlockStyle:dashed\"],\n              value: \"dashed\",\n            },\n            {\n              child: <DottedBorderIcon />,\n              description: declarationDescriptions[\"borderBlockStyle:dotted\"],\n              value: \"dotted\",\n            },\n          ]}\n        />\n      </Box>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/border-width.tsx",
    "content": "import type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  BorderWidthIndividualIcon,\n  BorderWidthTopIcon,\n  BorderWidthRightIcon,\n  BorderWidthBottomIcon,\n  BorderWidthLeftIcon,\n} from \"@webstudio-is/icons\";\nimport { BorderProperty } from \"./border-property\";\n\nexport const properties = [\n  \"border-top-width\",\n  \"border-right-width\",\n  \"border-bottom-width\",\n  \"border-left-width\",\n] satisfies CssProperty[];\n\nconst borderPropertyOptions = {\n  \"border-top-width\": {\n    icon: <BorderWidthTopIcon />,\n  },\n  \"border-right-width\": {\n    icon: <BorderWidthRightIcon />,\n  },\n  \"border-left-width\": {\n    icon: <BorderWidthLeftIcon />,\n  },\n  \"border-bottom-width\": {\n    icon: <BorderWidthBottomIcon />,\n  },\n} as const satisfies Partial<{ [property in CssProperty]: unknown }>;\n\nexport const BorderWidth = () => {\n  return (\n    <BorderProperty\n      label=\"Width\"\n      description=\"Sets the width of the border\"\n      borderPropertyOptions={borderPropertyOptions}\n      individualModeIcon={<BorderWidthIndividualIcon />}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/borders.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $instances,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { Section } from \"./borders\";\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(new Map([[\"local\", { id: \"local\", type: \"local\" }]]));\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nconst solidBorderTop: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderTopStyle\",\n  value: { type: \"keyword\", value: \"solid\" },\n};\n\nconst solidBorderRight: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderRightStyle\",\n  value: { type: \"keyword\", value: \"solid\" },\n};\n\nconst solidBorderBottom: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderBottomStyle\",\n  value: { type: \"keyword\", value: \"solid\" },\n};\n\nconst solidBorderLeft: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderLeftStyle\",\n  value: { type: \"keyword\", value: \"solid\" },\n};\n\nconst borderWidthTop: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderTopWidth\",\n  value: { type: \"unit\", value: 2, unit: \"px\" },\n};\n\nconst dashedBorderTop: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderTopStyle\",\n  value: { type: \"keyword\", value: \"dashed\" },\n};\n\nconst dottedBorderBottom: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderBottomStyle\",\n  value: { type: \"keyword\", value: \"dotted\" },\n};\n\nconst borderRadius: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"borderTopLeftRadius\",\n  value: { type: \"unit\", value: 8, unit: \"px\" },\n};\n\nconst WithSolidBorderVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([\n        [getStyleDeclKey(solidBorderTop), solidBorderTop],\n        [getStyleDeclKey(solidBorderRight), solidBorderRight],\n        [getStyleDeclKey(solidBorderBottom), solidBorderBottom],\n        [getStyleDeclKey(solidBorderLeft), solidBorderLeft],\n        [getStyleDeclKey(borderWidthTop), borderWidthTop],\n      ])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nconst WithMixedBordersVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([\n        [getStyleDeclKey(dashedBorderTop), dashedBorderTop],\n        [getStyleDeclKey(dottedBorderBottom), dottedBorderBottom],\n        [getStyleDeclKey(borderRadius), borderRadius],\n      ])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nexport const Borders = () => (\n  <StorySection title=\"Borders\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <Section />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With solid border</Text>\n        <WithSolidBorderVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With mixed borders</Text>\n        <WithMixedBordersVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Borders\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/borders.tsx",
    "content": "import type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport {\n  BorderRadius,\n  properties as borderRadiusProperties,\n} from \"./border-radius\";\nimport {\n  BorderStyle,\n  properties as borderStyleProperties,\n} from \"./border-style\";\nimport {\n  BorderWidth,\n  properties as borderWidthProperties,\n} from \"./border-width\";\nimport {\n  BorderColor,\n  properties as borderColorProperties,\n} from \"./border-color\";\n\nexport const properties = [\n  ...borderColorProperties,\n  ...borderRadiusProperties,\n  ...borderStyleProperties,\n  ...borderWidthProperties,\n] satisfies CssProperty[];\n\nexport const Section = () => {\n  return (\n    <StyleSection label=\"Borders\" properties={properties}>\n      <BorderStyle />\n      <BorderColor />\n      <BorderWidth />\n      <BorderRadius />\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/borders/utils.ts",
    "content": "import { theme, type CSS } from \"@webstudio-is/design-system\";\n\nexport const rowCss: CSS = {\n  // Our aim is to maintain consistent styling throughout the property and align\n  // the input fields on the left-hand side\n  // See ./border-property.tsx for more details\n  gridTemplateColumns: `1fr ${theme.spacing[21]} ${theme.spacing[10]}`,\n  gap: theme.spacing[3],\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/box-shadows/box-shadows.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $instances,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { Section } from \"./box-shadows\";\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(new Map([[\"local\", { id: \"local\", type: \"local\" }]]));\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nconst outerShadow: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"boxShadow\",\n  value: {\n    type: \"layers\",\n    value: [\n      {\n        type: \"shadow\",\n        position: \"outset\",\n        offsetX: { type: \"unit\", unit: \"px\", value: 0 },\n        offsetY: { type: \"unit\", unit: \"px\", value: 2 },\n        blur: { type: \"unit\", unit: \"px\", value: 5 },\n        spread: { type: \"unit\", unit: \"px\", value: 0 },\n        color: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 0.2,\n          components: [0, 0, 0],\n        },\n      },\n    ],\n  },\n};\n\nconst insetShadow: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"boxShadow\",\n  value: {\n    type: \"layers\",\n    value: [\n      {\n        type: \"shadow\",\n        position: \"inset\",\n        offsetX: { type: \"unit\", unit: \"px\", value: 0 },\n        offsetY: { type: \"unit\", unit: \"px\", value: 0 },\n        blur: { type: \"unit\", unit: \"px\", value: 10 },\n        color: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 0.3,\n          components: [0, 0, 0],\n        },\n      },\n    ],\n  },\n};\n\nconst multipleShadows: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"boxShadow\",\n  value: {\n    type: \"layers\",\n    value: [\n      {\n        type: \"shadow\",\n        position: \"outset\",\n        offsetX: { type: \"unit\", unit: \"px\", value: 0 },\n        offsetY: { type: \"unit\", unit: \"px\", value: 2 },\n        blur: { type: \"unit\", unit: \"px\", value: 5 },\n        color: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 0.2,\n          components: [0, 0, 0],\n        },\n      },\n      {\n        type: \"shadow\",\n        position: \"inset\",\n        offsetX: { type: \"unit\", unit: \"px\", value: 0 },\n        offsetY: { type: \"unit\", unit: \"px\", value: 0 },\n        blur: { type: \"unit\", unit: \"px\", value: 10 },\n        color: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 0.3,\n          components: [0, 0, 0],\n        },\n      },\n    ],\n  },\n};\n\nconst WithOuterShadowVariant = () => {\n  useEffect(() => {\n    $styles.set(new Map([[getStyleDeclKey(outerShadow), outerShadow]]));\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nconst WithInsetShadowVariant = () => {\n  useEffect(() => {\n    $styles.set(new Map([[getStyleDeclKey(insetShadow), insetShadow]]));\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nconst WithMultipleShadowsVariant = () => {\n  useEffect(() => {\n    $styles.set(new Map([[getStyleDeclKey(multipleShadows), multipleShadows]]));\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nexport const BoxShadows = () => (\n  <StorySection title=\"Box Shadows\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <Section />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With outer shadow</Text>\n        <WithOuterShadowVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With inset shadow</Text>\n        <WithInsetShadowVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With multiple shadows</Text>\n        <WithMultipleShadowsVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Box Shadows\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/box-shadows/box-shadows.tsx",
    "content": "import {\n  toValue,\n  type CssProperty,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { RepeatedStyleSection } from \"../../shared/style-section\";\nimport { ShadowContent } from \"../../shared/shadow-content\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport {\n  addRepeatedStyleItem,\n  editRepeatedStyleItem,\n  getComputedRepeatedItem,\n  RepeatedStyle,\n} from \"../../shared/repeated-style\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\n\nexport const properties = [\"box-shadow\"] satisfies [\n  CssProperty,\n  ...CssProperty[],\n];\n\nconst label = \"Box shadows\";\nconst initialBoxShadow = \"0px 2px 5px 0px rgba(0, 0, 0, 0.2)\";\n\nconst getItemProps = (layer: StyleValue, computedLayer?: StyleValue) => {\n  const shadowValue =\n    computedLayer?.type === \"shadow\" ? computedLayer : undefined;\n  const labels = [];\n  if (shadowValue?.position === \"inset\") {\n    labels.push(\"Inner:\");\n  } else {\n    labels.push(\"Outer:\");\n  }\n  if (layer.type === \"var\") {\n    labels.push(`--${layer.value}`);\n  } else if (shadowValue) {\n    labels.push(toValue(shadowValue.offsetX));\n    labels.push(toValue(shadowValue.offsetY));\n    labels.push(toValue(shadowValue.blur));\n    labels.push(toValue(shadowValue.spread));\n  } else {\n    labels.push(toValue(shadowValue));\n  }\n  const color = shadowValue?.color ? toValue(shadowValue.color) : undefined;\n  return { label: labels.join(\" \"), color };\n};\n\nexport const Section = () => {\n  const styleDecl = useComputedStyleDecl(\"box-shadow\");\n\n  return (\n    <RepeatedStyleSection\n      label={label}\n      description=\"Adds shadow effects around an element's frame.\"\n      properties={properties}\n      onAdd={() => {\n        addRepeatedStyleItem(\n          [styleDecl],\n          parseCssFragment(initialBoxShadow, [\"box-shadow\"])\n        );\n      }}\n    >\n      <RepeatedStyle\n        label={label}\n        styles={[styleDecl]}\n        getItemProps={(index, layer) =>\n          getItemProps(layer, getComputedRepeatedItem(styleDecl, index))\n        }\n        renderItemContent={(index, value) => (\n          <ShadowContent\n            index={index}\n            layer={value}\n            computedLayer={getComputedRepeatedItem(styleDecl, index)}\n            property=\"box-shadow\"\n            propertyValue={toValue(value)}\n            onEditLayer={(index, value, options) => {\n              editRepeatedStyleItem(\n                [styleDecl],\n                index,\n                new Map([[\"box-shadow\", value]]),\n                options\n              );\n            }}\n          />\n        )}\n      />\n    </RepeatedStyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/filter/filter.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $instances,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { Section } from \"./filter\";\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(new Map([[\"local\", { id: \"local\", type: \"local\" }]]));\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nconst blurFilterStyle: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"filter\",\n  value: {\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"blur\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n        },\n      },\n    ],\n  },\n};\n\nconst multipleFilterStyles: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"filter\",\n  value: {\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"blur\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"brightness\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"%\", value: 150 }],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"grayscale\",\n        args: {\n          type: \"tuple\",\n          value: [{ type: \"unit\", unit: \"%\", value: 50 }],\n        },\n      },\n    ],\n  },\n};\n\nconst WithBlurFilterVariant = () => {\n  useEffect(() => {\n    $styles.set(new Map([[getStyleDeclKey(blurFilterStyle), blurFilterStyle]]));\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nconst WithMultipleFiltersVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([[getStyleDeclKey(multipleFilterStyles), multipleFilterStyles]])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nexport const Filters = () => (\n  <StorySection title=\"Filters\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <Section />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With blur filter</Text>\n        <WithBlurFilterVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With multiple filters</Text>\n        <WithMultipleFiltersVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Filters\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/filter/filter.tsx",
    "content": "import { Flex, Tooltip, Text } from \"@webstudio-is/design-system\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport {\n  toValue,\n  type CssProperty,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { RepeatedStyleSection } from \"../../shared/style-section\";\nimport { FilterSectionContent } from \"../../shared/filter-content\";\nimport {\n  addRepeatedStyleItem,\n  editRepeatedStyleItem,\n  RepeatedStyle,\n} from \"../../shared/repeated-style\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { humanizeString } from \"~/shared/string-utils\";\n\nexport const properties = [\"filter\"] satisfies [CssProperty, ...CssProperty[]];\n\nconst label = \"Filters\";\nconst initialFilter = \"blur(0px)\";\n\nconst getItemProps = (_index: number, value: StyleValue) => {\n  const label =\n    value.type === \"function\"\n      ? `${humanizeString(value.name)}: ${toValue(value.args)}`\n      : \"Unknown filter\";\n  return { label };\n};\n\nexport const Section = () => {\n  const styleDecl = useComputedStyleDecl(\"filter\");\n\n  return (\n    <RepeatedStyleSection\n      label={label}\n      description=\"Filter effects allow you to apply graphical effects like blurring, color shifting, and more to elements.\"\n      properties={properties}\n      onAdd={() => {\n        addRepeatedStyleItem(\n          [styleDecl],\n          parseCssFragment(initialFilter, [\"filter\"])\n        );\n      }}\n    >\n      <RepeatedStyle\n        label={label}\n        styles={[styleDecl]}\n        getItemProps={getItemProps}\n        renderItemContent={(index, primaryValue) => (\n          <FilterSectionContent\n            index={index}\n            property=\"filter\"\n            propertyValue={toValue(primaryValue)}\n            layer={primaryValue}\n            onEditLayer={(index, value, options) => {\n              editRepeatedStyleItem(\n                [styleDecl],\n                index,\n                new Map([[\"filter\", value]]),\n                options\n              );\n            }}\n            tooltip={\n              <Tooltip\n                variant=\"wrapped\"\n                content={\n                  <Flex gap=\"2\" direction=\"column\">\n                    <Text variant=\"regularBold\">{label}</Text>\n                    <Text variant=\"monoBold\">filter</Text>\n                    <Text>\n                      Applies graphical effects like blur or color shift to an\n                      element, for example:\n                      <br /> <br />\n                      <Text variant=\"mono\">{initialFilter}</Text>\n                    </Text>\n                  </Flex>\n                }\n              >\n                <InfoCircleIcon />\n              </Tooltip>\n            }\n          />\n        )}\n      />\n    </RepeatedStyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/flex-child/flex-child.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./flex-child\";\n\nexport const FlexChild = () => (\n  <StorySection title=\"Flex child\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Flex Child\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/flex-child/flex-child.tsx",
    "content": "import {\n  Flex,\n  Grid,\n  theme,\n  ToggleGroup,\n  ToggleGroupButton,\n  FloatingPanel,\n  SectionTitleButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { ToggleGroupTooltip } from \"../../controls/toggle-group/toggle-group-control\";\nimport { TextControl } from \"../../controls\";\nimport {\n  XSmallIcon,\n  ShrinkIcon,\n  GrowIcon,\n  EllipsesIcon,\n  ExternalLinkIcon,\n} from \"@webstudio-is/icons\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport {\n  getPriorityStyleValueSource,\n  PropertyLabel,\n} from \"../../property-label\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\nimport { AlignSelfControl } from \"../shared/align-self\";\nimport { OrderControl } from \"../shared/order\";\nimport { $selectedInstancePath, selectInstance } from \"~/shared/awareness\";\n\nexport const properties = [\n  \"flex-shrink\",\n  \"flex-grow\",\n  \"flex-basis\",\n  \"align-self\",\n  \"order\",\n] satisfies [CssProperty, ...CssProperty[]];\n\nexport const Section = () => {\n  const instancePath = useStore($selectedInstancePath);\n  // Get the parent instance (second item in the path, index 1)\n  const parentInstance = instancePath?.[1];\n\n  return (\n    <StyleSection\n      label=\"Flex child\"\n      properties={properties}\n      suffix={\n        parentInstance && (\n          <Tooltip content=\"Select flex container\">\n            <SectionTitleButton\n              prefix={<ExternalLinkIcon />}\n              onClick={() => selectInstance(parentInstance.instanceSelector)}\n            />\n          </Tooltip>\n        )\n      }\n    >\n      <Flex css={{ flexDirection: \"column\", gap: theme.spacing[5] }}>\n        <FlexChildSectionAlign />\n        <FlexChildSectionSizing />\n        <OrderControl />\n      </Flex>\n    </StyleSection>\n  );\n};\n\nconst FlexChildSectionAlign = () => {\n  return <AlignSelfControl variant=\"flex\" />;\n};\n\nconst getSizingValue = (flexGrow: string, flexShrink: string) => {\n  if (flexGrow === \"0\" && flexShrink === \"0\") {\n    return \"none\";\n  }\n  if (flexGrow === \"1\" && flexShrink === \"0\") {\n    return \"grow\";\n  }\n  if (flexGrow === \"0\" && flexShrink === \"1\") {\n    return \"shrink\";\n  }\n  return \"\";\n};\n\nconst FlexChildSectionSizing = () => {\n  const styles = useComputedStyles([\"flex-grow\", \"flex-shrink\", \"flex-basis\"]);\n  const [flexGrow, flexShrink, flexBasis] = styles;\n  const styleValueSource = getPriorityStyleValueSource(styles);\n  const selectedValue = getSizingValue(\n    toValue(flexGrow.cascadedValue),\n    toValue(flexShrink.cascadedValue)\n  );\n  const items = [\n    {\n      child: <XSmallIcon />,\n      description: \"Don't grow or shrink\",\n      value: \"none\",\n      codeLines: [\"flex-grow: 0;\", \"flex-shrink: 0;\"],\n    },\n    {\n      child: <GrowIcon />,\n      title: \"Flex\",\n      description:\n        \"Item will expand to take up available space within a flex container if needed, but it will not shrink if there is limited space.\",\n      value: \"grow\",\n      codeLines: [\"flex-grow: 1;\", \"flex-shrink: 0;\"],\n    },\n    {\n      child: <ShrinkIcon />,\n      title: \"Flex\",\n      description:\n        \"Item will not grow to take up available space within a flex container, but it will shrink if there is limited space\",\n      value: \"shrink\",\n      codeLines: [\"flex-grow: 0;\", \"flex-shrink: 1;\"],\n    },\n    {\n      child: <FlexChildSectionSizingPopover />,\n      title: \"Flex\",\n      description:\n        \"More sizing options, set flex-basis, flex-grow, flex-shrink individually\",\n      value: \"\",\n      codeLines: [\n        `flex-basis: ${toValue(flexBasis.cascadedValue)};`,\n        `flex-grow: ${toValue(flexGrow.cascadedValue)};`,\n        `flex-shrink: ${toValue(flexShrink.cascadedValue)};`,\n      ],\n    },\n  ];\n  // Issue: The tooltip's grace area is too big and overlaps with nearby buttons,\n  // preventing the tooltip from changing when the buttons are hovered over in certain cases.\n  // To solve issue and allow tooltips to change on button hover,\n  // we close the button tooltip in the ToggleGroupButton.onMouseEnter handler.\n  // onMouseEnter used to preserve default hovering behavior on tooltip.\n  const [activeTooltip, setActiveTooltip] = useState<undefined | string>();\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <PropertyLabel\n        label=\"Sizing\"\n        description=\"Specifies the ability of a flex item to grow, shrink, or set its initial size within a flex container.\"\n        properties={[\"flex-grow\", \"flex-shrink\", \"flex-basis\"]}\n      />\n\n      {/* We don't support \"flex\" shorthand and\n        this control is manipulating 3 properties at once */}\n      <ToggleGroup\n        color={styleValueSource}\n        type=\"single\"\n        value={selectedValue}\n        onValueChange={(value) => {\n          const batch = createBatchUpdate();\n          let flexGrow: undefined | number;\n          let flexShrink: undefined | number;\n          if (value === \"none\") {\n            flexGrow = 0;\n            flexShrink = 0;\n          }\n          if (value === \"grow\") {\n            flexGrow = 1;\n            flexShrink = 0;\n          }\n          if (value === \"shrink\") {\n            flexGrow = 0;\n            flexShrink = 1;\n          }\n          if (flexGrow !== undefined && flexShrink !== undefined) {\n            batch.setProperty(\"flex-grow\")({\n              type: \"unit\",\n              value: flexGrow,\n              unit: \"number\",\n            });\n            batch.setProperty(\"flex-shrink\")({\n              type: \"unit\",\n              value: flexShrink,\n              unit: \"number\",\n            });\n          }\n          batch.publish();\n        }}\n      >\n        {items.map((item) => (\n          <ToggleGroupTooltip\n            key={item.value}\n            isOpen={item.value === activeTooltip}\n            onOpenChange={(isOpen) =>\n              setActiveTooltip(isOpen ? item.value : undefined)\n            }\n            isSelected={item.value === selectedValue}\n            label=\"Sizing\"\n            code={item.codeLines.join(\"\\n\")}\n            description={item.description}\n            properties={[\"flex-grow\", \"flex-shrink\", \"flex-basis\"]}\n          >\n            <ToggleGroupButton\n              aria-checked={item.value === selectedValue}\n              value={item.value}\n              onMouseEnter={() =>\n                // reset only when highlighted is not active\n                setActiveTooltip((prevValue) =>\n                  prevValue === item.value ? prevValue : undefined\n                )\n              }\n            >\n              {item.child}\n            </ToggleGroupButton>\n          </ToggleGroupTooltip>\n        ))}\n      </ToggleGroup>\n    </Grid>\n  );\n};\n\nconst FlexChildSectionSizingPopover = () => {\n  return (\n    <FloatingPanel\n      title=\"Sizing\"\n      placement=\"bottom-within\"\n      content={\n        <Grid\n          gap=\"3\"\n          css={{\n            gridTemplateColumns: \"1fr 1fr 1.5fr\",\n            padding: theme.panel.padding,\n          }}\n        >\n          <Grid css={{ gridTemplateColumns: \"auto\", gap: theme.spacing[3] }}>\n            <PropertyLabel\n              properties={[\"flex-grow\"]}\n              description={propertyDescriptions.flexGrow}\n              label=\"Grow\"\n            />\n            <TextControl property=\"flex-grow\" />\n          </Grid>\n          <Grid css={{ gridTemplateColumns: \"auto\", gap: theme.spacing[3] }}>\n            <PropertyLabel\n              label=\"Shrink\"\n              description={propertyDescriptions.flexShrink}\n              properties={[\"flex-shrink\"]}\n            />\n            <TextControl property=\"flex-shrink\" />\n          </Grid>\n          <Grid css={{ gridTemplateColumns: \"auto\", gap: theme.spacing[3] }}>\n            <PropertyLabel\n              label=\"Basis\"\n              description={propertyDescriptions.flexBasis}\n              properties={[\"flex-basis\"]}\n            />\n            <TextControl property=\"flex-basis\" />\n          </Grid>\n        </Grid>\n      }\n    >\n      <Flex>\n        <EllipsesIcon />\n      </Flex>\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/grid-child/grid-child.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./grid-child\";\n\nexport const GridChild = () => (\n  <StorySection title=\"Grid child\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Grid Child\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/grid-child/grid-child.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { __testing__ } from \"./grid-child\";\n\nconst { derivePositionMode } = __testing__;\n\ndescribe(\"derivePositionMode\", () => {\n  const auto: StyleValue = { type: \"keyword\", value: \"auto\" };\n\n  describe(\"returns 'auto' mode\", () => {\n    test(\"when all values are 'auto'\", () => {\n      expect(derivePositionMode(auto, auto, auto, auto)).toBe(\"auto\");\n    });\n\n    test(\"when values are 'auto' with span tuples\", () => {\n      const spanValue: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"keyword\", value: \"span\" },\n          { type: \"unit\", value: 2, unit: \"number\" },\n        ],\n      };\n      expect(derivePositionMode(auto, spanValue, auto, spanValue)).toBe(\"auto\");\n    });\n\n    test(\"when all values are span tuples\", () => {\n      const span2: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"keyword\", value: \"span\" },\n          { type: \"unit\", value: 2, unit: \"number\" },\n        ],\n      };\n      const span3: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"keyword\", value: \"span\" },\n          { type: \"unit\", value: 3, unit: \"number\" },\n        ],\n      };\n      expect(derivePositionMode(auto, span2, auto, span3)).toBe(\"auto\");\n    });\n  });\n\n  describe(\"returns 'manual' mode\", () => {\n    test(\"when any value is numeric\", () => {\n      const numeric: StyleValue = { type: \"unit\", value: 1, unit: \"number\" };\n      expect(derivePositionMode(numeric, auto, auto, auto)).toBe(\"manual\");\n    });\n\n    test(\"when all values are numeric\", () => {\n      const val1: StyleValue = { type: \"unit\", value: 1, unit: \"number\" };\n      const val2: StyleValue = { type: \"unit\", value: 2, unit: \"number\" };\n      const val3: StyleValue = { type: \"unit\", value: 1, unit: \"number\" };\n      const val4: StyleValue = { type: \"unit\", value: 3, unit: \"number\" };\n      expect(derivePositionMode(val1, val2, val3, val4)).toBe(\"manual\");\n    });\n\n    test(\"when some values are numeric and some auto\", () => {\n      const numeric: StyleValue = { type: \"unit\", value: 2, unit: \"number\" };\n      expect(derivePositionMode(auto, numeric, auto, auto)).toBe(\"manual\");\n    });\n  });\n\n  describe(\"returns 'area' mode\", () => {\n    test(\"when values contain named area keywords\", () => {\n      const areaName: StyleValue = { type: \"keyword\", value: \"header\" };\n      expect(derivePositionMode(areaName, areaName, areaName, areaName)).toBe(\n        \"area\"\n      );\n    });\n\n    test(\"when some values are named areas and some auto\", () => {\n      const areaName: StyleValue = { type: \"keyword\", value: \"sidebar\" };\n      // In practice, when using grid-area shorthand, all 4 values get the same area name\n      expect(derivePositionMode(areaName, areaName, areaName, areaName)).toBe(\n        \"area\"\n      );\n    });\n\n    test(\"when start values are named areas\", () => {\n      const areaName: StyleValue = { type: \"keyword\", value: \"main\" };\n      expect(derivePositionMode(areaName, auto, areaName, auto)).toBe(\"area\");\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"prioritizes 'manual' over 'area' when mixed\", () => {\n      // If there's any numeric value, it's manual mode\n      const numeric: StyleValue = { type: \"unit\", value: 1, unit: \"number\" };\n      const areaName: StyleValue = { type: \"keyword\", value: \"header\" };\n      expect(derivePositionMode(numeric, areaName, auto, auto)).toBe(\"manual\");\n    });\n\n    test(\"handles guaranteedInvalid value by defaulting to auto\", () => {\n      const invalid: StyleValue = { type: \"guaranteedInvalid\" };\n      expect(derivePositionMode(invalid, invalid, invalid, invalid)).toBe(\n        \"auto\"\n      );\n    });\n\n    test(\"handles unparsed values by defaulting to auto\", () => {\n      const unparsed: StyleValue = { type: \"unparsed\", value: \"something\" };\n      expect(derivePositionMode(unparsed, unparsed, unparsed, unparsed)).toBe(\n        \"auto\"\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/grid-child/grid-child.tsx",
    "content": "import { useState, useMemo, useEffect } from \"react\";\nimport {\n  Box,\n  Flex,\n  Grid,\n  Select,\n  Text,\n  theme,\n  ToggleGroup,\n  ToggleGroupButton,\n  SectionTitleButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { parseGridAreas, type AreaInfo } from \"@webstudio-is/css-data\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { ToggleGroupTooltip } from \"../../controls/toggle-group/toggle-group-control\";\nimport {\n  CssValueInput,\n  type IntermediateStyleValue,\n} from \"../../shared/css-value-input\";\nimport {\n  useComputedStyleDecl,\n  useComputedStyles,\n  useParentComputedStyleDecl,\n} from \"../../shared/model\";\nimport {\n  createBatchUpdate,\n  deleteProperty,\n  resetEphemeralStyles,\n} from \"../../shared/use-style-data\";\nimport { getGridDimensions } from \"../layout/shared/grid-areas\";\nimport { GridAreaPicker } from \"../layout/shared/grid-area-picker\";\nimport {\n  GridPositionInputs,\n  type GridPosition,\n} from \"../layout/shared/grid-position-inputs\";\nimport { useLocalValue } from \"../../../settings-panel/shared\";\nimport { AlignSelfControl, JustifySelfControl } from \"../shared/align-self\";\nimport { OrderControl } from \"../shared/order\";\nimport { useStore } from \"@nanostores/react\";\nimport { $selectedInstancePath, selectInstance } from \"~/shared/awareness\";\nimport { $isStylePanelGridVisible } from \"~/builder/shared/nano-states\";\nimport { ExternalLinkIcon } from \"@webstudio-is/icons\";\n\nexport const properties = [\n  \"grid-column-start\",\n  \"grid-column-end\",\n  \"grid-row-start\",\n  \"grid-row-end\",\n  \"align-self\",\n  \"justify-self\",\n  \"order\",\n] satisfies [CssProperty, ...CssProperty[]];\n\ntype PositionMode = \"auto\" | \"area\" | \"manual\";\n\n/**\n * Derives the position mode from the current CSS values.\n * - \"auto\": All values are \"auto\" or span values (auto placement)\n * - \"area\": Values contain named grid areas (keywords that are not \"auto\")\n * - \"manual\": Values are numeric line positions\n */\nconst derivePositionMode = (\n  columnStart: StyleValue,\n  columnEnd: StyleValue,\n  rowStart: StyleValue,\n  rowEnd: StyleValue\n): PositionMode => {\n  const values = [columnStart, columnEnd, rowStart, rowEnd];\n\n  // Check if any value is numeric (manual mode)\n  const hasNumericValues = values.some(\n    (value) => value.type === \"unit\" && value.unit === \"number\"\n  );\n\n  if (hasNumericValues) {\n    return \"manual\";\n  }\n\n  // Check if all values are \"auto\" or span tuples (auto mode)\n  const isAutoOrSpan = (value: StyleValue): boolean => {\n    if (value.type === \"keyword\" && value.value === \"auto\") {\n      return true;\n    }\n    // Span values like \"span 2\" are tuples\n    if (value.type === \"tuple\" && value.value.length === 2) {\n      const [first] = value.value;\n      if (first.type === \"keyword\" && first.value === \"span\") {\n        return true;\n      }\n    }\n    return false;\n  };\n\n  const allAutoOrSpan = values.every(isAutoOrSpan);\n\n  if (allAutoOrSpan) {\n    return \"auto\";\n  }\n\n  // If we have non-auto keyword values, it's likely a named area\n  const hasNamedArea = values.some(\n    (value) => value.type === \"keyword\" && value.value !== \"auto\"\n  );\n\n  if (hasNamedArea) {\n    return \"area\";\n  }\n\n  // Default to auto\n  return \"auto\";\n};\n\n/**\n * Shows grid guides on the canvas while this component is mounted.\n */\nconst GridChildGuides = () => {\n  useEffect(() => {\n    $isStylePanelGridVisible.set(true);\n    return () => {\n      $isStylePanelGridVisible.set(false);\n    };\n  }, []);\n  return null;\n};\n\nexport const Section = () => {\n  const [columnStart, columnEnd, rowStart, rowEnd] = useComputedStyles([\n    \"grid-column-start\",\n    \"grid-column-end\",\n    \"grid-row-start\",\n    \"grid-row-end\",\n  ]);\n\n  // Derive position mode from CSS values - this ensures the correct tab is shown\n  // when selecting different instances with different grid positioning\n  const derivedPositionMode = useMemo(\n    () =>\n      derivePositionMode(\n        columnStart.cascadedValue,\n        columnEnd.cascadedValue,\n        rowStart.cascadedValue,\n        rowEnd.cascadedValue\n      ),\n    [columnStart, columnEnd, rowStart, rowEnd]\n  );\n\n  // Track user override - when user explicitly changes mode, use that until CSS values match\n  const [userOverrideMode, setUserOverrideMode] = useState<PositionMode | null>(\n    null\n  );\n\n  // Use user override if set, otherwise use derived mode\n  const positionMode = userOverrideMode ?? derivedPositionMode;\n\n  const handleModeChange = (newMode: PositionMode) => {\n    const hasKeywordValues =\n      columnStart.cascadedValue.type === \"keyword\" &&\n      columnEnd.cascadedValue.type === \"keyword\" &&\n      rowStart.cascadedValue.type === \"keyword\" &&\n      rowEnd.cascadedValue.type === \"keyword\";\n\n    const batch = createBatchUpdate();\n\n    if (newMode === \"auto\") {\n      // Reset to span 1 on all axes for auto placement\n      const span1: StyleValue = {\n        type: \"tuple\",\n        value: [\n          { type: \"keyword\", value: \"span\" },\n          { type: \"unit\", value: 1, unit: \"number\" },\n        ],\n      };\n      batch.setProperty(\"grid-column-start\")(span1);\n      batch.setProperty(\"grid-column-end\")(span1);\n      batch.setProperty(\"grid-row-start\")(span1);\n      batch.setProperty(\"grid-row-end\")(span1);\n      batch.publish();\n    } else if (newMode === \"manual\" && hasKeywordValues) {\n      // Convert keywords to numeric positions when switching to manual\n      batch.setProperty(\"grid-column-start\")({\n        type: \"unit\",\n        value: 1,\n        unit: \"number\",\n      });\n      batch.setProperty(\"grid-column-end\")({\n        type: \"unit\",\n        value: 2,\n        unit: \"number\",\n      });\n      batch.setProperty(\"grid-row-start\")({\n        type: \"unit\",\n        value: 1,\n        unit: \"number\",\n      });\n      batch.setProperty(\"grid-row-end\")({\n        type: \"unit\",\n        value: 2,\n        unit: \"number\",\n      });\n      batch.publish();\n    }\n\n    // Set user override - this will be cleared when CSS values change\n    // to match the new mode (since derivePositionMode will return newMode)\n    setUserOverrideMode(newMode);\n  };\n\n  // Clear user override when derived mode matches (CSS was updated to match the mode)\n  // This allows the component to correctly derive mode for new instances\n  useEffect(() => {\n    if (userOverrideMode !== null && derivedPositionMode === userOverrideMode) {\n      setUserOverrideMode(null);\n    }\n  }, [derivedPositionMode, userOverrideMode]);\n\n  const instancePath = useStore($selectedInstancePath);\n  // Get the parent instance (second item in the path, index 1)\n  const parentInstance = instancePath?.[1];\n\n  return (\n    <StyleSection\n      label=\"Grid child\"\n      properties={properties}\n      suffix={\n        parentInstance && (\n          <Tooltip content=\"Select grid container\">\n            <SectionTitleButton\n              prefix={<ExternalLinkIcon />}\n              onClick={() => selectInstance(parentInstance.instanceSelector)}\n            />\n          </Tooltip>\n        )\n      }\n    >\n      <GridChildGuides />\n      <Flex css={{ flexDirection: \"column\", gap: theme.spacing[5] }}>\n        <GridChildPositionMode\n          value={positionMode}\n          onChange={handleModeChange}\n        />\n        {positionMode === \"auto\" && <GridChildPositionAuto />}\n        {positionMode === \"area\" && <GridChildPositionArea />}\n        {positionMode === \"manual\" && <GridChildPositionManual />}\n        <GridChildAlign />\n      </Flex>\n    </StyleSection>\n  );\n};\n\nconst GridChildPositionMode = ({\n  value,\n  onChange,\n}: {\n  value: PositionMode;\n  onChange: (value: PositionMode) => void;\n}) => {\n  const [activeTooltip, setActiveTooltip] = useState<string | undefined>();\n\n  const items = [\n    {\n      value: \"auto\" as const,\n      label: \"Auto\",\n      description: \"Let the grid automatically place this item.\",\n      code: \"grid-column: auto;\\ngrid-row: auto;\",\n    },\n    {\n      value: \"area\" as const,\n      label: \"Area\",\n      description: \"Place the item in a named grid area.\",\n      code: \"grid-area: <area-name>;\",\n    },\n    {\n      value: \"manual\" as const,\n      label: \"Manual\",\n      description: \"Manually specify the item's position using grid lines.\",\n      code: \"grid-column: <start> / <end>;\\ngrid-row: <start> / <end>;\",\n    },\n  ];\n\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <PropertyLabel\n        label=\"Position\"\n        description=\"How the grid item is positioned within the grid\"\n        properties={[\n          \"grid-column-start\",\n          \"grid-column-end\",\n          \"grid-row-start\",\n          \"grid-row-end\",\n        ]}\n      />\n      <ToggleGroup\n        type=\"single\"\n        value={value}\n        onValueChange={(newValue) => {\n          if (newValue) {\n            onChange(newValue as PositionMode);\n          }\n        }}\n      >\n        {items.map((item) => (\n          <ToggleGroupTooltip\n            key={item.value}\n            isOpen={item.value === activeTooltip}\n            onOpenChange={(isOpen) =>\n              setActiveTooltip(isOpen ? item.value : undefined)\n            }\n            isSelected={item.value === value}\n            label={item.label}\n            code={item.code}\n            description={item.description}\n            properties={[\"grid-column-start\", \"grid-row-start\"]}\n          >\n            <ToggleGroupButton\n              value={item.value}\n              onMouseEnter={() =>\n                setActiveTooltip((prevValue) =>\n                  prevValue === item.value ? prevValue : undefined\n                )\n              }\n            >\n              <Box css={{ paddingInline: theme.spacing[4] }}>{item.label}</Box>\n            </ToggleGroupButton>\n          </ToggleGroupTooltip>\n        ))}\n      </ToggleGroup>\n    </Grid>\n  );\n};\n\nconst GridChildPositionAuto = () => {\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <div />\n      <Grid css={{ gridTemplateColumns: \"1fr 1fr\", gap: theme.spacing[5] }}>\n        <Grid css={{ gap: theme.spacing[3] }}>\n          <SpanInput\n            property=\"grid-column-end\"\n            startProperty=\"grid-column-start\"\n          />\n          <Text variant=\"small\" color=\"subtle\">\n            Column span\n          </Text>\n        </Grid>\n        <Grid css={{ gap: theme.spacing[3] }}>\n          <SpanInput property=\"grid-row-end\" startProperty=\"grid-row-start\" />\n          <Text variant=\"small\" color=\"subtle\">\n            Row span\n          </Text>\n        </Grid>\n      </Grid>\n    </Grid>\n  );\n};\n\n/**\n * A specialized input for grid span values.\n * Handles conversion between span values and the number displayed.\n * When span is 1, grid uses \"auto\". For span > 1, uses \"span N\".\n */\nconst SpanInput = ({\n  property,\n  startProperty,\n}: {\n  property: \"grid-column-end\" | \"grid-row-end\";\n  startProperty: \"grid-column-start\" | \"grid-row-start\";\n}) => {\n  const computedStyleDecl = useComputedStyleDecl(property);\n  const value = computedStyleDecl.cascadedValue;\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  // Extract span number from value\n  // \"auto\" = 1, \"span 2\" = 2, etc.\n  const getSpanNumber = (styleValue: StyleValue): number => {\n    if (styleValue.type === \"keyword\" && styleValue.value === \"auto\") {\n      return 1;\n    }\n    if (styleValue.type === \"tuple\" && styleValue.value.length === 2) {\n      const spanKeyword = styleValue.value[0];\n      const spanNumber = styleValue.value[1];\n      if (\n        spanKeyword.type === \"keyword\" &&\n        spanKeyword.value === \"span\" &&\n        spanNumber.type === \"unit\" &&\n        spanNumber.unit === \"number\"\n      ) {\n        return spanNumber.value;\n      }\n    }\n    if (styleValue.type === \"unit\" && styleValue.unit === \"number\") {\n      return styleValue.value;\n    }\n    return 1;\n  };\n\n  // Create span StyleValue from number\n  const createSpanValue = (num: number): StyleValue => ({\n    type: \"tuple\",\n    value: [\n      { type: \"keyword\", value: \"span\" },\n      { type: \"unit\", value: num, unit: \"number\" },\n    ],\n  });\n\n  const currentSpan = getSpanNumber(value);\n\n  // Display the span number as a simple unit value for the input\n  const displayValue: StyleValue = {\n    type: \"unit\",\n    value: currentSpan,\n    unit: \"number\",\n  };\n\n  const handleChange = (styleValue: StyleValue | undefined) => {\n    if (styleValue === undefined) {\n      deleteProperty(property, { isEphemeral: true });\n      // Also ensure start is auto for auto placement\n      const batch = createBatchUpdate();\n      batch.deleteProperty(startProperty);\n      batch.publish({ isEphemeral: true });\n      return;\n    }\n\n    if (styleValue.type === \"unit\" && styleValue.unit === \"number\") {\n      const spanValue = createSpanValue(\n        Math.max(1, Math.round(styleValue.value))\n      );\n      const batch = createBatchUpdate();\n      batch.setProperty(startProperty)(spanValue);\n      batch.setProperty(property)(spanValue);\n      batch.publish({ isEphemeral: true });\n    }\n  };\n\n  const handleChangeComplete = (styleValue: StyleValue) => {\n    setIntermediateValue(undefined);\n    if (styleValue.type === \"unit\" && styleValue.unit === \"number\") {\n      const spanValue = createSpanValue(\n        Math.max(1, Math.round(styleValue.value))\n      );\n      const batch = createBatchUpdate();\n      batch.setProperty(startProperty)(spanValue);\n      batch.setProperty(property)(spanValue);\n      batch.publish();\n    }\n  };\n\n  return (\n    <CssValueInput\n      styleSource={computedStyleDecl.source.name}\n      property={property}\n      value={displayValue}\n      intermediateValue={intermediateValue}\n      getOptions={() => []}\n      onChange={(styleValue) => {\n        setIntermediateValue(styleValue);\n        if (styleValue === undefined) {\n          handleChange(undefined);\n          return;\n        }\n        if (styleValue.type !== \"intermediate\") {\n          handleChange(styleValue);\n        }\n      }}\n      onHighlight={(styleValue) => {\n        if (styleValue !== undefined) {\n          handleChange(styleValue);\n        } else {\n          deleteProperty(property, { isEphemeral: true });\n        }\n      }}\n      onChangeComplete={({ value }) => {\n        handleChangeComplete(value);\n        resetEphemeralStyles();\n      }}\n      onAbort={() => {\n        resetEphemeralStyles();\n      }}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        const batch = createBatchUpdate();\n        batch.deleteProperty(startProperty);\n        batch.deleteProperty(property);\n        batch.publish();\n      }}\n    />\n  );\n};\n\nconst GridChildPositionArea = () => {\n  const parentGridTemplateAreas = useParentComputedStyleDecl(\n    \"grid-template-areas\"\n  );\n  const gridRowStart = useComputedStyleDecl(\"grid-row-start\");\n\n  // Parse area names from parent's grid-template-areas\n  const areasValue = toValue(parentGridTemplateAreas.computedValue);\n  const areas = parseGridAreas(areasValue);\n  const areaNames = areas.map((area) => area.name);\n\n  // Get current area name from grid-row-start\n  // When using named areas, grid-row-start contains the area name\n  const currentValue = toValue(gridRowStart.cascadedValue);\n  const selectedArea = areaNames.includes(currentValue) ? currentValue : \"\";\n\n  const handleAreaChange = (areaName: string) => {\n    const batch = createBatchUpdate();\n    // Setting all four properties to the same area name places the item in that area\n    const areaValue: StyleValue = { type: \"keyword\", value: areaName };\n    batch.setProperty(\"grid-row-start\")(areaValue);\n    batch.setProperty(\"grid-column-start\")(areaValue);\n    batch.setProperty(\"grid-row-end\")(areaValue);\n    batch.setProperty(\"grid-column-end\")(areaValue);\n    batch.publish();\n  };\n\n  if (areaNames.length === 0) {\n    return (\n      <Text color=\"moreSubtle\">\n        No named areas defined. Add areas in the parent grid's template.\n      </Text>\n    );\n  }\n\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <div />\n      <Select\n        options={areaNames}\n        value={selectedArea}\n        onChange={handleAreaChange}\n        placeholder=\"Select area\"\n      />\n    </Grid>\n  );\n};\n\nconst GridChildPositionManual = () => {\n  const parentGridTemplateColumns = useParentComputedStyleDecl(\n    \"grid-template-columns\"\n  );\n  const parentGridTemplateRows =\n    useParentComputedStyleDecl(\"grid-template-rows\");\n  const [columnStart, columnEnd, rowStart, rowEnd] = useComputedStyles([\n    \"grid-column-start\",\n    \"grid-column-end\",\n    \"grid-row-start\",\n    \"grid-row-end\",\n  ]);\n\n  // Get grid dimensions from parent\n  const { columns: gridColumns, rows: gridRows } = getGridDimensions(\n    toValue(parentGridTemplateColumns.computedValue),\n    toValue(parentGridTemplateRows.computedValue)\n  );\n\n  // Extract numeric values from CSS values\n  // For \"auto\" or non-numeric values, use sensible defaults based on position type\n  const getNumericValue = (\n    styleValue: StyleValue,\n    defaultValue: number\n  ): number => {\n    if (styleValue.type === \"unit\" && styleValue.unit === \"number\") {\n      return styleValue.value;\n    }\n    return defaultValue;\n  };\n\n  // Get values with defaults, ensuring they form a valid position\n  const colStart = getNumericValue(columnStart.cascadedValue, 1);\n  const colEnd = getNumericValue(columnEnd.cascadedValue, 2);\n  const rStart = getNumericValue(rowStart.cascadedValue, 1);\n  const rEnd = getNumericValue(rowEnd.cascadedValue, 2);\n\n  // Validate and fix invalid positions (end must be greater than start)\n  // Use useMemo to stabilize the position object so it doesn't change on every render\n  const position: GridPosition = useMemo(\n    () => ({\n      columnStart: colStart,\n      columnEnd: colEnd > colStart ? colEnd : colStart + 1,\n      rowStart: rStart,\n      rowEnd: rEnd > rStart ? rEnd : rStart + 1,\n    }),\n    [colStart, colEnd, rStart, rEnd]\n  );\n\n  const localValue = useLocalValue(\n    position,\n    (newPosition) => {\n      // Validate and fix the position before saving\n      const validPosition = {\n        columnStart: Math.max(1, Math.round(newPosition.columnStart)),\n        columnEnd: Math.max(1, Math.round(newPosition.columnEnd)),\n        rowStart: Math.max(1, Math.round(newPosition.rowStart)),\n        rowEnd: Math.max(1, Math.round(newPosition.rowEnd)),\n      };\n\n      // Ensure end is greater than start\n      if (validPosition.columnEnd <= validPosition.columnStart) {\n        validPosition.columnEnd = validPosition.columnStart + 1;\n      }\n      if (validPosition.rowEnd <= validPosition.rowStart) {\n        validPosition.rowEnd = validPosition.rowStart + 1;\n      }\n\n      const batch = createBatchUpdate();\n      batch.setProperty(\"grid-column-start\")({\n        type: \"unit\",\n        value: validPosition.columnStart,\n        unit: \"number\",\n      });\n      batch.setProperty(\"grid-column-end\")({\n        type: \"unit\",\n        value: validPosition.columnEnd,\n        unit: \"number\",\n      });\n      batch.setProperty(\"grid-row-start\")({\n        type: \"unit\",\n        value: validPosition.rowStart,\n        unit: \"number\",\n      });\n      batch.setProperty(\"grid-row-end\")({\n        type: \"unit\",\n        value: validPosition.rowEnd,\n        unit: \"number\",\n      });\n      batch.publish();\n    },\n    { autoSave: false }\n  );\n\n  const handleChange = (newPosition: GridPosition) => {\n    localValue.set(newPosition);\n    const batch = createBatchUpdate();\n    batch.setProperty(\"grid-column-start\")({\n      type: \"unit\",\n      value: newPosition.columnStart,\n      unit: \"number\",\n    });\n    batch.setProperty(\"grid-column-end\")({\n      type: \"unit\",\n      value: newPosition.columnEnd,\n      unit: \"number\",\n    });\n    batch.setProperty(\"grid-row-start\")({\n      type: \"unit\",\n      value: newPosition.rowStart,\n      unit: \"number\",\n    });\n    batch.setProperty(\"grid-row-end\")({\n      type: \"unit\",\n      value: newPosition.rowEnd,\n      unit: \"number\",\n    });\n    batch.publish({ isEphemeral: true });\n  };\n\n  const handlePickerChange = (area: AreaInfo) => {\n    const newPosition: GridPosition = {\n      columnStart: area.columnStart,\n      columnEnd: area.columnEnd,\n      rowStart: area.rowStart,\n      rowEnd: area.rowEnd,\n    };\n    localValue.set(newPosition);\n    handleChange(newPosition);\n    localValue.save();\n    resetEphemeralStyles();\n  };\n\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <Grid\n        css={{\n          gridTemplateColumns: \"3fr 8fr\",\n          alignItems: \"start\",\n        }}\n      >\n        <div />\n        <GridPositionInputs\n          value={localValue.value}\n          onChange={handleChange}\n          onBlur={() => {\n            localValue.save();\n            resetEphemeralStyles();\n          }}\n          gridColumns={gridColumns}\n          gridRows={gridRows}\n        />\n      </Grid>\n      <GridAreaPicker\n        value={{\n          name: \"\",\n          ...localValue.value,\n        }}\n        onChange={handlePickerChange}\n        gridColumns={gridColumns}\n        gridRows={gridRows}\n        otherAreas={[]}\n      />\n    </Flex>\n  );\n};\n\nconst GridChildAlign = () => {\n  return (\n    <Flex direction=\"column\" gap=\"1\">\n      <AlignSelfControl variant=\"grid\" />\n      <JustifySelfControl />\n      <OrderControl />\n    </Flex>\n  );\n};\n\nexport const __testing__ = {\n  derivePositionMode,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/index.ts",
    "content": "export * from \"./sections\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/layout.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./layout\";\n\nexport const Layout = () => (\n  <StorySection title=\"Layout\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Layout\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/layout.tsx",
    "content": "import { useEffect, useRef, useState, type JSX, type ReactNode } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { $selectedInstanceKey } from \"~/shared/awareness\";\nimport {\n  theme,\n  Box,\n  EnhancedTooltip,\n  Flex,\n  Grid,\n  SmallToggleButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { keywordValues, propertyDescriptions } from \"@webstudio-is/css-data\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport {\n  Link2Icon,\n  Link2UnlinkedIcon,\n  GapHorizontalIcon,\n  GapVerticalIcon,\n  WrapIcon,\n  NoWrapIcon,\n  ArrowRightIcon,\n  ArrowLeftIcon,\n  ArrowDownIcon,\n  ArrowUpIcon,\n  AlignCenterHorizontalIcon,\n  AlignHorizontalJustifyCenterIcon,\n  AlignContentCenterIcon,\n  AlignStartHorizontalIcon,\n  AlignEndHorizontalIcon,\n  AlignBaselineIcon,\n  StretchVerticalIcon,\n  AlignHorizontalJustifyStartIcon,\n  AlignHorizontalJustifyEndIcon,\n  AlignHorizontalSpaceBetweenIcon,\n  AlignHorizontalSpaceAroundIcon,\n  AlignContentStartIcon,\n  AlignContentEndIcon,\n  AlignContentSpaceAroundIcon,\n  AlignContentSpaceBetweenIcon,\n  AlignContentStretchIcon,\n  RepeatGridIcon,\n} from \"@webstudio-is/icons\";\nimport { MenuControl, SelectControl } from \"../../controls\";\nimport { createBatchUpdate, deleteProperty } from \"../../shared/use-style-data\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport {\n  type IntermediateStyleValue,\n  CssValueInput,\n} from \"../../shared/css-value-input\";\nimport { ToggleControl } from \"../../controls/toggle/toggle-control\";\nimport { PropertyInfo, PropertyLabel } from \"../../property-label\";\nimport {\n  useComputedStyles,\n  useComputedStyleDecl,\n  $availableUnitVariables,\n} from \"../../shared/model\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { FlexAlignment } from \"./shared/flex-alignment\";\nimport { GridGenerator } from \"./shared/grid-generator\";\nimport { GridSettings } from \"./shared/grid-settings\";\nimport { GridAlignment } from \"./shared/grid-alignment\";\nimport { $isStylePanelGridVisible } from \"~/builder/shared/nano-states\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { DEFAULT_GRID_TRACK_COUNT, DEFAULT_GRID_GAP } from \"./shared/constants\";\n\nconst GapLinked = ({\n  isLinked,\n  onChange,\n}: {\n  isLinked: boolean;\n  onChange: (isLinked: boolean) => void;\n}) => (\n  <EnhancedTooltip content={isLinked ? \"Unlink gap values\" : \"Link gap values\"}>\n    <SmallToggleButton\n      pressed={isLinked}\n      onPressedChange={onChange}\n      variant=\"normal\"\n      icon={isLinked ? <Link2Icon /> : <Link2UnlinkedIcon />}\n    />\n  </EnhancedTooltip>\n);\n\nconst GapTooltip = ({\n  label,\n  styleDecl,\n  onReset,\n  children,\n}: {\n  label: string;\n  styleDecl: ComputedStyleDecl;\n  onReset: () => void;\n  children: ReactNode;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const description = propertyDescriptions[styleDecl.property];\n  return (\n    <Tooltip\n      open={isOpen}\n      onOpenChange={setIsOpen}\n      // prevent closing tooltip on content click\n      onPointerDown={(event) => event.preventDefault()}\n      triggerProps={{\n        onClick(event) {\n          if (event.altKey) {\n            event.preventDefault();\n            onReset();\n            return;\n          }\n          setIsOpen(true);\n        },\n      }}\n      content={\n        <PropertyInfo\n          title={label}\n          description={description}\n          styles={[styleDecl]}\n          onReset={() => {\n            onReset();\n            setIsOpen(false);\n          }}\n        />\n      }\n    >\n      {children}\n    </Tooltip>\n  );\n};\n\nconst GapInput = ({\n  icon,\n  property,\n  styleDecl,\n  intermediateValue,\n  onIntermediateChange,\n  onPreviewChange,\n  onChange,\n  onReset,\n}: {\n  icon: JSX.Element;\n  property: CssProperty;\n  styleDecl: ComputedStyleDecl;\n  intermediateValue?: StyleValue | IntermediateStyleValue;\n  onIntermediateChange: (value?: StyleValue | IntermediateStyleValue) => void;\n  onPreviewChange: (value?: StyleValue) => void;\n  onChange: (value: StyleValue) => void;\n  onReset: () => void;\n}) => {\n  return (\n    <Box>\n      <CssValueInput\n        styleSource={styleDecl.source.name}\n        icon={\n          <GapTooltip\n            label={humanizeString(property)}\n            styleDecl={styleDecl}\n            onReset={onReset}\n          >\n            {icon}\n          </GapTooltip>\n        }\n        property={property}\n        value={styleDecl.cascadedValue}\n        intermediateValue={intermediateValue}\n        getOptions={() => [\n          ...(keywordValues[property] ?? []).map((value) => ({\n            type: \"keyword\" as const,\n            value,\n          })),\n          ...$availableUnitVariables.get(),\n        ]}\n        onChange={(styleValue) => {\n          onIntermediateChange(styleValue);\n          if (styleValue === undefined) {\n            onPreviewChange();\n            return;\n          }\n          if (styleValue.type !== \"intermediate\") {\n            onPreviewChange(styleValue);\n          }\n        }}\n        onHighlight={(styleValue) => {\n          if (styleValue !== undefined) {\n            onPreviewChange(styleValue);\n          } else {\n            onPreviewChange();\n          }\n        }}\n        onChangeComplete={({ value }) => {\n          onChange(value);\n          onIntermediateChange(undefined);\n        }}\n        onAbort={() => {\n          onPreviewChange();\n        }}\n        onReset={() => {\n          onIntermediateChange(undefined);\n          onReset();\n        }}\n      />\n    </Box>\n  );\n};\n\nconst Gap = () => {\n  const [columnGap, rowGap] = useComputedStyles([\"column-gap\", \"row-gap\"]);\n  const [isLinked, setIsLinked] = useState(\n    () => toValue(columnGap.cascadedValue) === toValue(rowGap.cascadedValue)\n  );\n\n  const [intermediateColumnGap, setIntermediateColumnGap] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n  const [intermediateRowGap, setIntermediateRowGap] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  return (\n    <Grid\n      css={{\n        gridTemplateColumns: \"4fr 1fr 4fr\",\n        gridTemplateRows: \"auto\",\n        gridTemplateAreas: `\n          \"column-gap linked row-gap\"\n        `,\n        alignItems: \"center\",\n      }}\n    >\n      <Box css={{ gridArea: \"column-gap\" }}>\n        <GapInput\n          icon={\n            <GapHorizontalIcon\n              onClick={(event) => {\n                if (event.altKey) {\n                  event.preventDefault();\n                  deleteProperty(\"column-gap\");\n                  if (isLinked) {\n                    deleteProperty(\"row-gap\");\n                  }\n                }\n              }}\n            />\n          }\n          property=\"column-gap\"\n          styleDecl={columnGap}\n          intermediateValue={intermediateColumnGap}\n          onIntermediateChange={(value) => {\n            setIntermediateColumnGap(value);\n            if (isLinked) {\n              setIntermediateRowGap(value);\n            }\n          }}\n          onReset={() => {\n            const batch = createBatchUpdate();\n            batch.deleteProperty(\"column-gap\");\n            if (isLinked) {\n              batch.deleteProperty(\"row-gap\");\n            }\n            batch.publish();\n          }}\n          onPreviewChange={(value) => {\n            const batch = createBatchUpdate();\n            if (value === undefined) {\n              batch.deleteProperty(\"column-gap\");\n              if (isLinked) {\n                batch.deleteProperty(\"row-gap\");\n              }\n            } else {\n              batch.setProperty(\"column-gap\")(value);\n              if (isLinked) {\n                batch.setProperty(\"row-gap\")(value);\n              }\n            }\n            batch.publish({ isEphemeral: true });\n          }}\n          onChange={(value) => {\n            const batch = createBatchUpdate();\n            batch.setProperty(\"column-gap\")(value);\n            if (isLinked) {\n              batch.setProperty(\"row-gap\")(value);\n            }\n            batch.publish();\n          }}\n        />\n      </Box>\n\n      <Flex css={{ gridArea: \"linked\", px: theme.spacing[3] }} justify=\"center\">\n        <GapLinked\n          isLinked={isLinked}\n          onChange={(isLinked) => {\n            setIsLinked(isLinked);\n            if (isLinked === false) {\n              return;\n            }\n            const isColumnGapDefined =\n              columnGap.source.name === \"local\" ||\n              columnGap.source.name === \"overwritten\";\n            const isRowGapDefined =\n              rowGap.source.name === \"local\" ||\n              rowGap.source.name === \"overwritten\";\n            if (isColumnGapDefined) {\n              const batch = createBatchUpdate();\n              batch.setProperty(\"row-gap\")(columnGap.cascadedValue);\n              batch.publish();\n            } else if (isRowGapDefined) {\n              const batch = createBatchUpdate();\n              batch.setProperty(\"column-gap\")(rowGap.cascadedValue);\n              batch.publish();\n            }\n          }}\n        />\n      </Flex>\n\n      <Box css={{ gridArea: \"row-gap\" }}>\n        <GapInput\n          icon={\n            <GapVerticalIcon\n              onClick={(event) => {\n                if (event.altKey) {\n                  event.preventDefault();\n                  deleteProperty(\"row-gap\");\n                  if (isLinked) {\n                    deleteProperty(\"column-gap\");\n                  }\n                }\n              }}\n            />\n          }\n          property=\"row-gap\"\n          styleDecl={rowGap}\n          intermediateValue={intermediateRowGap}\n          onIntermediateChange={(value) => {\n            setIntermediateRowGap(value);\n            if (isLinked) {\n              setIntermediateColumnGap(value);\n            }\n          }}\n          onReset={() => {\n            const batch = createBatchUpdate();\n            batch.deleteProperty(\"row-gap\");\n            if (isLinked) {\n              batch.deleteProperty(\"column-gap\");\n            }\n            batch.publish();\n          }}\n          onPreviewChange={(value) => {\n            const batch = createBatchUpdate();\n            if (value === undefined) {\n              batch.deleteProperty(\"row-gap\");\n              if (isLinked) {\n                batch.deleteProperty(\"column-gap\");\n              }\n            } else {\n              batch.setProperty(\"row-gap\")(value);\n              if (isLinked) {\n                batch.setProperty(\"column-gap\")(value);\n              }\n            }\n            batch.publish({ isEphemeral: true });\n          }}\n          onChange={(value) => {\n            const batch = createBatchUpdate();\n            batch.setProperty(\"row-gap\")(value);\n            if (isLinked) {\n              batch.setProperty(\"column-gap\")(value);\n            }\n            batch.publish();\n          }}\n        />\n      </Box>\n    </Grid>\n  );\n};\n\n// Shared alignment controls for both Flex and Grid\nconst AlignmentControls = ({\n  showAlignContent = true,\n}: {\n  showAlignContent?: boolean;\n}) => {\n  return (\n    <Grid\n      css={{\n        gridTemplateColumns: \"1fr 1fr 1fr\",\n        gap: theme.spacing[7],\n      }}\n    >\n      <MenuControl\n        property=\"align-items\"\n        items={[\n          {\n            name: \"start\",\n            label: \"Start\",\n            icon: AlignStartHorizontalIcon,\n          },\n          {\n            name: \"center\",\n            label: \"Center\",\n            icon: AlignCenterHorizontalIcon,\n          },\n          { name: \"end\", label: \"End\", icon: AlignEndHorizontalIcon },\n          {\n            name: \"stretch\",\n            label: \"Stretch\",\n            icon: StretchVerticalIcon,\n          },\n          {\n            name: \"baseline\",\n            label: \"Baseline\",\n            icon: AlignBaselineIcon,\n          },\n        ]}\n      />\n      <MenuControl\n        property=\"justify-content\"\n        items={[\n          {\n            name: \"start\",\n            label: \"Start\",\n            icon: AlignHorizontalJustifyStartIcon,\n          },\n          {\n            name: \"center\",\n            label: \"Center\",\n            icon: AlignHorizontalJustifyCenterIcon,\n          },\n          {\n            name: \"end\",\n            label: \"End\",\n            icon: AlignHorizontalJustifyEndIcon,\n          },\n          {\n            name: \"space-between\",\n            label: \"Space Between\",\n            icon: AlignHorizontalSpaceBetweenIcon,\n          },\n          {\n            name: \"space-around\",\n            label: \"Space Around\",\n            icon: AlignHorizontalSpaceAroundIcon,\n          },\n        ]}\n      />\n      {showAlignContent && (\n        <MenuControl\n          property=\"align-content\"\n          items={[\n            {\n              name: \"start\",\n              label: \"Start\",\n              icon: AlignContentStartIcon,\n            },\n            {\n              name: \"center\",\n              label: \"Center\",\n              icon: AlignContentCenterIcon,\n            },\n            { name: \"end\", label: \"End\", icon: AlignContentEndIcon },\n            {\n              name: \"stretch\",\n              label: \"Stretch\",\n              icon: AlignContentStretchIcon,\n            },\n            {\n              name: \"space-between\",\n              label: \"Space Between\",\n              icon: AlignContentSpaceBetweenIcon,\n            },\n            {\n              name: \"space-around\",\n              label: \"Space Around\",\n              icon: AlignContentSpaceAroundIcon,\n            },\n          ]}\n        />\n      )}\n    </Grid>\n  );\n};\n\nconst LayoutSectionFlex = () => {\n  const flexWrap = useComputedStyleDecl(\"flex-wrap\");\n  const flexWrapValue = toValue(flexWrap.cascadedValue);\n\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <Grid\n        css={{\n          gridTemplateColumns: `1fr 1fr 1fr`,\n          gap: theme.spacing[7],\n          alignItems: \"start\",\n        }}\n      >\n        <FlexAlignment />\n        <Grid\n          css={{\n            gridTemplateColumns: \"1fr 1fr\",\n            gap: theme.spacing[7],\n          }}\n        >\n          <MenuControl\n            property=\"flex-direction\"\n            items={[\n              { name: \"row\", label: \"Row\", icon: ArrowRightIcon },\n              {\n                name: \"row-reverse\",\n                label: \"Row Reverse\",\n                icon: ArrowLeftIcon,\n              },\n              { name: \"column\", label: \"Column\", icon: ArrowDownIcon },\n              {\n                name: \"column-reverse\",\n                label: \"Column Reverse\",\n                icon: ArrowUpIcon,\n              },\n            ]}\n          />\n          <ToggleControl\n            property=\"flex-wrap\"\n            items={[\n              { name: \"nowrap\", label: \"No Wrap\", icon: NoWrapIcon },\n              { name: \"wrap\", label: \"Wrap\", icon: WrapIcon },\n            ]}\n          />\n          <Box css={{ gridColumn: \"1 / -1\" }}>\n            <AlignmentControls\n              showAlignContent={\n                flexWrapValue === \"wrap\" || flexWrapValue === \"wrap-reverse\"\n              }\n            />\n          </Box>\n        </Grid>\n      </Grid>\n      <Gap />\n    </Flex>\n  );\n};\n\nconst applyDefaultGridStyles = (columnsValue: string, rowsValue: string) => {\n  const isColumnsEmpty = !columnsValue || columnsValue === \"none\";\n  const isRowsEmpty = !rowsValue || rowsValue === \"none\";\n  if (isColumnsEmpty === false || isRowsEmpty === false) {\n    return;\n  }\n  const defaultColumns = Array(DEFAULT_GRID_TRACK_COUNT).fill(\"1fr\").join(\" \");\n  const defaultRows = Array(DEFAULT_GRID_TRACK_COUNT).fill(\"auto\").join(\" \");\n  const batch = createBatchUpdate();\n  batch.setProperty(\"grid-template-columns\")({\n    type: \"unparsed\",\n    value: defaultColumns,\n  });\n  batch.setProperty(\"grid-template-rows\")({\n    type: \"unparsed\",\n    value: defaultRows,\n  });\n  batch.setProperty(\"grid-auto-columns\")({\n    type: \"unit\",\n    value: 1,\n    unit: \"fr\",\n  });\n  batch.setProperty(\"column-gap\")({\n    type: \"unit\",\n    value: DEFAULT_GRID_GAP,\n    unit: \"px\",\n  });\n  batch.setProperty(\"row-gap\")({\n    type: \"unit\",\n    value: DEFAULT_GRID_GAP,\n    unit: \"px\",\n  });\n  batch.publish();\n};\n\nconst LayoutSectionGrid = () => {\n  const [openPanel, setOpenPanel] = useState({\n    generator: false,\n    settings: false,\n  });\n\n  useEffect(() => {\n    $isStylePanelGridVisible.set(true);\n    return () => {\n      $isStylePanelGridVisible.set(false);\n    };\n  }, []);\n\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <GridGenerator\n        open={openPanel.generator}\n        onOpenChange={(open) => setOpenPanel({ ...openPanel, generator: open })}\n      />\n      <GridSettings\n        open={openPanel.settings}\n        onOpenChange={(open) => setOpenPanel({ ...openPanel, settings: open })}\n      />\n      <Grid\n        css={{\n          gridTemplateColumns: \"1fr 1fr 1fr\",\n          gap: theme.spacing[7],\n          alignItems: \"stretch\",\n        }}\n      >\n        <GridAlignment />\n        <Flex direction=\"column\" justify=\"between\">\n          <Flex css={{ gap: theme.spacing[7] }}>\n            <MenuControl\n              property=\"grid-auto-flow\"\n              items={[\n                { name: \"row\", label: \"Row\", icon: ArrowRightIcon },\n                { name: \"column\", label: \"Column\", icon: ArrowDownIcon },\n                { name: \"row dense\", label: \"Row Dense\", icon: RepeatGridIcon },\n                {\n                  name: \"column dense\",\n                  label: \"Column Dense\",\n                  icon: RepeatGridIcon,\n                },\n              ]}\n            />\n            <MenuControl\n              property=\"align-content\"\n              items={[\n                {\n                  name: \"start\",\n                  label: \"Start\",\n                  icon: AlignContentStartIcon,\n                },\n                {\n                  name: \"center\",\n                  label: \"Center\",\n                  icon: AlignContentCenterIcon,\n                },\n                { name: \"end\", label: \"End\", icon: AlignContentEndIcon },\n                {\n                  name: \"stretch\",\n                  label: \"Stretch\",\n                  icon: AlignContentStretchIcon,\n                },\n                {\n                  name: \"space-between\",\n                  label: \"Space Between\",\n                  icon: AlignContentSpaceBetweenIcon,\n                },\n                {\n                  name: \"space-around\",\n                  label: \"Space Around\",\n                  icon: AlignContentSpaceAroundIcon,\n                },\n              ]}\n            />\n          </Flex>\n          <Flex css={{ gap: theme.spacing[7] }}>\n            <MenuControl\n              property=\"align-items\"\n              items={[\n                {\n                  name: \"start\",\n                  label: \"Start\",\n                  icon: AlignStartHorizontalIcon,\n                },\n                {\n                  name: \"center\",\n                  label: \"Center\",\n                  icon: AlignCenterHorizontalIcon,\n                },\n                { name: \"end\", label: \"End\", icon: AlignEndHorizontalIcon },\n                {\n                  name: \"stretch\",\n                  label: \"Stretch\",\n                  icon: StretchVerticalIcon,\n                },\n                {\n                  name: \"baseline\",\n                  label: \"Baseline\",\n                  icon: AlignBaselineIcon,\n                },\n              ]}\n            />\n            <MenuControl\n              property=\"justify-items\"\n              items={[\n                {\n                  name: \"start\",\n                  label: \"Start\",\n                  icon: AlignStartHorizontalIcon,\n                },\n                {\n                  name: \"center\",\n                  label: \"Center\",\n                  icon: AlignCenterHorizontalIcon,\n                },\n                { name: \"end\", label: \"End\", icon: AlignEndHorizontalIcon },\n                {\n                  name: \"stretch\",\n                  label: \"Stretch\",\n                  icon: StretchVerticalIcon,\n                },\n                {\n                  name: \"baseline\",\n                  label: \"Baseline\",\n                  icon: AlignBaselineIcon,\n                },\n              ]}\n            />\n            <MenuControl\n              property=\"justify-content\"\n              items={[\n                {\n                  name: \"start\",\n                  label: \"Start\",\n                  icon: AlignHorizontalJustifyStartIcon,\n                },\n                {\n                  name: \"center\",\n                  label: \"Center\",\n                  icon: AlignHorizontalJustifyCenterIcon,\n                },\n                {\n                  name: \"end\",\n                  label: \"End\",\n                  icon: AlignHorizontalJustifyEndIcon,\n                },\n                {\n                  name: \"space-between\",\n                  label: \"Space Between\",\n                  icon: AlignHorizontalSpaceBetweenIcon,\n                },\n                {\n                  name: \"space-around\",\n                  label: \"Space Around\",\n                  icon: AlignHorizontalSpaceAroundIcon,\n                },\n              ]}\n            />\n          </Flex>\n        </Flex>\n      </Grid>\n\n      <Gap />\n    </Flex>\n  );\n};\n\nconst orderedDisplayValues = [\n  \"block\",\n  \"flex\",\n  \"grid\",\n  \"inline-block\",\n  \"inline-flex\",\n  \"inline-grid\",\n  \"inline\",\n  \"none\",\n];\n\nexport const properties = [\n  \"display\",\n  \"flex-direction\",\n  \"flex-wrap\",\n  \"align-items\",\n  \"justify-items\",\n  \"justify-content\",\n  \"align-content\",\n  \"row-gap\",\n  \"column-gap\",\n  \"grid-auto-flow\",\n  \"grid-auto-columns\",\n  \"grid-auto-rows\",\n  \"grid-template-columns\",\n  \"grid-template-rows\",\n] satisfies Array<CssProperty>;\n\nconst isGridDisplay = (value: string) =>\n  value === \"grid\" || value === \"inline-grid\";\n\nexport const Section = () => {\n  const display = useComputedStyleDecl(\"display\");\n  const displayValue = toValue(display.cascadedValue);\n  const instanceKey = useStore($selectedInstanceKey);\n  const gridTemplateColumns = useComputedStyleDecl(\"grid-template-columns\");\n  const gridTemplateRows = useComputedStyleDecl(\"grid-template-rows\");\n  const columnsValue = toValue(gridTemplateColumns.cascadedValue);\n  const rowsValue = toValue(gridTemplateRows.cascadedValue);\n\n  const prevRef = useRef<{\n    displayValue: string;\n    instanceKey: string | undefined;\n  }>();\n\n  useEffect(() => {\n    const prev = prevRef.current;\n    prevRef.current = { displayValue, instanceKey };\n    // Apply defaults only when display transitions from non-grid to grid\n    // on the same instance (not on initial render or instance switch)\n    if (\n      prev === undefined ||\n      prev.instanceKey !== instanceKey ||\n      isGridDisplay(prev.displayValue) ||\n      isGridDisplay(displayValue) === false\n    ) {\n      return;\n    }\n    applyDefaultGridStyles(columnsValue, rowsValue);\n  }, [displayValue, instanceKey, columnsValue, rowsValue]);\n\n  return (\n    <StyleSection label=\"Layout\" properties={properties}>\n      <Flex direction=\"column\" gap=\"2\">\n        <Grid\n          css={{\n            gridTemplateColumns: `1fr 2fr`,\n            gap: theme.spacing[4],\n          }}\n        >\n          <PropertyLabel\n            label=\"Display\"\n            description={propertyDescriptions.display}\n            properties={[\"display\"]}\n          />\n          <SelectControl\n            property=\"display\"\n            items={orderedDisplayValues.map((name) => ({ name, label: name }))}\n          />\n        </Grid>\n        {(displayValue === \"flex\" || displayValue === \"inline-flex\") && (\n          <LayoutSectionFlex />\n        )}\n        {(displayValue === \"grid\" || displayValue === \"inline-grid\") && (\n          <LayoutSectionGrid />\n        )}\n      </Flex>\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/alignment-ui.stories.tsx",
    "content": "import {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { AlignmentUi } from \"./alignment-ui\";\n\nexport default {\n  title: \"Style panel/Layout/Alignment UI\",\n  component: AlignmentUi,\n};\n\nconst alignItemsValues = [\n  \"normal\",\n  \"start\",\n  \"center\",\n  \"end\",\n  \"stretch\",\n  \"baseline\",\n];\nconst justifyContentValues = [\n  \"normal\",\n  \"start\",\n  \"center\",\n  \"end\",\n  \"space-between\",\n  \"space-around\",\n];\n\nconst labelStyle = { fontSize: 10, fontFamily: \"sans-serif\" };\n\nconst CombinationGrid = ({\n  title,\n  isColumnDirection,\n  itemStretchWidth,\n  itemStretchHeight,\n}: {\n  title: string;\n  isColumnDirection: boolean;\n  itemStretchWidth: (ai: string) => boolean;\n  itemStretchHeight: (ai: string) => boolean;\n}) => (\n  <Flex direction=\"column\" gap=\"1\">\n    <Text css={{ ...labelStyle, fontWeight: \"bold\", fontSize: 12 }}>\n      {title}\n    </Text>\n    <Flex direction=\"column\" gap=\"2\">\n      <Flex gap=\"2\" align=\"center\">\n        <Box css={{ width: 70 }} />\n        {justifyContentValues.map((jc) => (\n          <Box\n            key={jc}\n            css={{\n              width: 64,\n              ...labelStyle,\n              textAlign: \"center\",\n              fontWeight: \"bold\",\n            }}\n          >\n            {jc}\n          </Box>\n        ))}\n      </Flex>\n      {alignItemsValues.map((ai) => (\n        <Flex key={ai} gap=\"2\" align=\"center\">\n          <Box css={{ ...labelStyle, width: 70, textAlign: \"right\" }}>{ai}</Box>\n          {justifyContentValues.map((jc) => (\n            <Box\n              key={jc}\n              css={{\n                width: 64,\n                height: 62,\n                color: theme.colors.foregroundFlexUiMain,\n              }}\n            >\n              <AlignmentUi\n                justifyContent={jc}\n                alignItems={ai}\n                isColumnDirection={isColumnDirection}\n                color=\"currentColor\"\n                itemStretchWidth={itemStretchWidth(ai)}\n                itemStretchHeight={itemStretchHeight(ai)}\n                onSelect={() => {}}\n              />\n            </Box>\n          ))}\n        </Flex>\n      ))}\n    </Flex>\n  </Flex>\n);\n\nconst isStretchOrNormal = (ai: string) => ai === \"stretch\" || ai === \"normal\";\n\nexport const AlignmentUI = () => (\n  <>\n    <StorySection title=\"Flex\">\n      <Flex direction=\"column\" gap=\"6\">\n        <CombinationGrid\n          title=\"flex-direction: row\"\n          isColumnDirection={false}\n          itemStretchWidth={() => false}\n          itemStretchHeight={() => false}\n        />\n        <CombinationGrid\n          title=\"flex-direction: column\"\n          isColumnDirection={true}\n          itemStretchWidth={() => false}\n          itemStretchHeight={() => false}\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Grid\">\n      <Flex direction=\"column\" gap=\"6\">\n        <CombinationGrid\n          title=\"grid-auto-flow: row, justify-items: stretch\"\n          isColumnDirection={false}\n          itemStretchWidth={isStretchOrNormal}\n          itemStretchHeight={() => false}\n        />\n        <CombinationGrid\n          title=\"grid-auto-flow: row, justify-items: start\"\n          isColumnDirection={false}\n          itemStretchWidth={() => false}\n          itemStretchHeight={() => false}\n        />\n        <CombinationGrid\n          title=\"grid-auto-flow: column, justify-items: stretch\"\n          isColumnDirection={true}\n          itemStretchWidth={() => false}\n          itemStretchHeight={isStretchOrNormal}\n        />\n        <CombinationGrid\n          title=\"grid-auto-flow: column, justify-items: start\"\n          isColumnDirection={true}\n          itemStretchWidth={() => false}\n          itemStretchHeight={() => false}\n        />\n      </Flex>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/alignment-ui.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { shouldHideDot } from \"./alignment-ui\";\n\nconst call = (overrides: Partial<Parameters<typeof shouldHideDot>[0]> = {}) =>\n  shouldHideDot({\n    x: 0,\n    y: 0,\n    justifyContent: \"start\",\n    alignItems: \"start\",\n    isColumnDirection: false,\n    itemStretchWidth: false,\n    itemStretchHeight: false,\n    ...overrides,\n  });\n\ndescribe(\"shouldHideDot\", () => {\n  describe(\"row direction, non-stretched align-items\", () => {\n    test(\"align-items: start hides dots only at y=0\", () => {\n      expect(\n        call({ alignItems: \"start\", justifyContent: \"start\", x: 0, y: 0 })\n      ).toBe(true);\n      expect(\n        call({ alignItems: \"start\", justifyContent: \"start\", x: 0, y: 1 })\n      ).toBe(false);\n      expect(\n        call({ alignItems: \"start\", justifyContent: \"start\", x: 0, y: 2 })\n      ).toBe(false);\n    });\n\n    test(\"align-items: center hides dots only at y=1\", () => {\n      expect(\n        call({ alignItems: \"center\", justifyContent: \"start\", x: 0, y: 0 })\n      ).toBe(false);\n      expect(\n        call({ alignItems: \"center\", justifyContent: \"start\", x: 0, y: 1 })\n      ).toBe(true);\n      expect(\n        call({ alignItems: \"center\", justifyContent: \"start\", x: 0, y: 2 })\n      ).toBe(false);\n    });\n\n    test(\"align-items: end hides dots only at y=2\", () => {\n      expect(\n        call({ alignItems: \"end\", justifyContent: \"start\", x: 0, y: 0 })\n      ).toBe(false);\n      expect(\n        call({ alignItems: \"end\", justifyContent: \"start\", x: 0, y: 2 })\n      ).toBe(true);\n    });\n\n    test(\"align-items: baseline hides dots only at y=0\", () => {\n      expect(\n        call({ alignItems: \"baseline\", justifyContent: \"start\", x: 0, y: 0 })\n      ).toBe(true);\n      expect(\n        call({ alignItems: \"baseline\", justifyContent: \"start\", x: 0, y: 1 })\n      ).toBe(false);\n    });\n  });\n\n  describe(\"row direction, stretched cross-axis\", () => {\n    // CSS align-items: stretch/normal stretches bars to full height\n    test.each([\"stretch\", \"normal\"])(\n      \"align-items: %s hides at all y positions\",\n      (ai) => {\n        expect(\n          call({ alignItems: ai, justifyContent: \"start\", x: 0, y: 0 })\n        ).toBe(true);\n        expect(\n          call({ alignItems: ai, justifyContent: \"start\", x: 0, y: 1 })\n        ).toBe(true);\n        expect(\n          call({ alignItems: ai, justifyContent: \"start\", x: 0, y: 2 })\n        ).toBe(true);\n        // but only at matching main-axis position\n        expect(\n          call({ alignItems: ai, justifyContent: \"start\", x: 1, y: 0 })\n        ).toBe(false);\n      }\n    );\n\n    // Explicit itemStretchHeight has same effect\n    test(\"itemStretchHeight=true hides at all y positions\", () => {\n      expect(\n        call({\n          alignItems: \"start\",\n          itemStretchHeight: true,\n          justifyContent: \"center\",\n          x: 1,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          alignItems: \"start\",\n          itemStretchHeight: true,\n          justifyContent: \"center\",\n          x: 1,\n          y: 2,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          alignItems: \"start\",\n          itemStretchHeight: true,\n          justifyContent: \"center\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(false);\n    });\n  });\n\n  describe(\"row direction, main-axis positions\", () => {\n    const stretched = { alignItems: \"stretch\" };\n\n    test(\"justify-content: normal/start hides at x=0\", () => {\n      expect(call({ ...stretched, justifyContent: \"normal\", x: 0 })).toBe(true);\n      expect(call({ ...stretched, justifyContent: \"start\", x: 0 })).toBe(true);\n      expect(call({ ...stretched, justifyContent: \"start\", x: 1 })).toBe(false);\n      expect(call({ ...stretched, justifyContent: \"start\", x: 2 })).toBe(false);\n    });\n\n    test(\"justify-content: center hides at x=1\", () => {\n      expect(call({ ...stretched, justifyContent: \"center\", x: 1 })).toBe(true);\n      expect(call({ ...stretched, justifyContent: \"center\", x: 0 })).toBe(\n        false\n      );\n    });\n\n    test(\"justify-content: end hides at x=2\", () => {\n      expect(call({ ...stretched, justifyContent: \"end\", x: 2 })).toBe(true);\n      expect(call({ ...stretched, justifyContent: \"end\", x: 1 })).toBe(false);\n    });\n\n    test(\"justify-content: space-between hides at all x positions\", () => {\n      expect(\n        call({ ...stretched, justifyContent: \"space-between\", x: 0 })\n      ).toBe(true);\n      expect(\n        call({ ...stretched, justifyContent: \"space-between\", x: 1 })\n      ).toBe(true);\n      expect(\n        call({ ...stretched, justifyContent: \"space-between\", x: 2 })\n      ).toBe(true);\n    });\n\n    test(\"justify-content: space-around hides no dots\", () => {\n      expect(call({ ...stretched, justifyContent: \"space-around\", x: 0 })).toBe(\n        false\n      );\n      expect(call({ ...stretched, justifyContent: \"space-around\", x: 1 })).toBe(\n        false\n      );\n      expect(call({ ...stretched, justifyContent: \"space-around\", x: 2 })).toBe(\n        false\n      );\n    });\n  });\n\n  describe(\"row direction, intersection check\", () => {\n    test(\"align-items: start + justify-content: space-between hides row 0 only\", () => {\n      expect(\n        call({\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 1,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 2,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 0,\n          y: 1,\n        })\n      ).toBe(false);\n      expect(\n        call({\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 2,\n          y: 2,\n        })\n      ).toBe(false);\n    });\n\n    test(\"align-items: end + justify-content: center hides only (1,2)\", () => {\n      expect(\n        call({ alignItems: \"end\", justifyContent: \"center\", x: 1, y: 2 })\n      ).toBe(true);\n      expect(\n        call({ alignItems: \"end\", justifyContent: \"center\", x: 0, y: 2 })\n      ).toBe(false);\n      expect(\n        call({ alignItems: \"end\", justifyContent: \"center\", x: 1, y: 0 })\n      ).toBe(false);\n    });\n  });\n\n  describe(\"column direction\", () => {\n    const col = { isColumnDirection: true };\n\n    test(\"x maps to justify-content (main), y maps to align-items (cross) — same as row\", () => {\n      // align-items: start → cross match at y=0\n      // justify-content: start → main match at x=0\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 1,\n          y: 0,\n        })\n      ).toBe(false);\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 1,\n        })\n      ).toBe(false);\n    });\n\n    test(\"align-items: center matches y=1\", () => {\n      expect(\n        call({\n          ...col,\n          alignItems: \"center\",\n          justifyContent: \"end\",\n          x: 2,\n          y: 1,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"center\",\n          justifyContent: \"end\",\n          x: 2,\n          y: 0,\n        })\n      ).toBe(false);\n    });\n\n    test(\"align-items: stretch covers all y\", () => {\n      expect(\n        call({\n          ...col,\n          alignItems: \"stretch\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"stretch\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 1,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"stretch\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 2,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"stretch\",\n          justifyContent: \"start\",\n          x: 1,\n          y: 0,\n        })\n      ).toBe(false);\n    });\n\n    test(\"itemStretchWidth covers all y in column direction\", () => {\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          itemStretchWidth: true,\n          justifyContent: \"center\",\n          x: 1,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          itemStretchWidth: true,\n          justifyContent: \"center\",\n          x: 1,\n          y: 2,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          itemStretchWidth: true,\n          justifyContent: \"center\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(false);\n    });\n\n    test(\"space-between hides all x when cross matches\", () => {\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 1,\n          y: 0,\n        })\n      ).toBe(true);\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 2,\n          y: 0,\n        })\n      ).toBe(true);\n      // cross doesn't match\n      expect(\n        call({\n          ...col,\n          alignItems: \"start\",\n          justifyContent: \"space-between\",\n          x: 0,\n          y: 1,\n        })\n      ).toBe(false);\n    });\n  });\n\n  describe(\"stretch direction matters for itemStretch*\", () => {\n    test(\"row: itemStretchWidth has no effect on cross-axis (y)\", () => {\n      expect(\n        call({\n          isColumnDirection: false,\n          itemStretchWidth: true,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(true); // matches anyway via alignItems\n      expect(\n        call({\n          isColumnDirection: false,\n          itemStretchWidth: true,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 1,\n        })\n      ).toBe(false); // itemStretchWidth doesn't affect row cross-axis\n    });\n\n    test(\"column: itemStretchHeight has no effect on cross-axis (y)\", () => {\n      expect(\n        call({\n          isColumnDirection: true,\n          itemStretchHeight: true,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 0,\n        })\n      ).toBe(true); // matches anyway via alignItems\n      expect(\n        call({\n          isColumnDirection: true,\n          itemStretchHeight: true,\n          alignItems: \"start\",\n          justifyContent: \"start\",\n          x: 0,\n          y: 1,\n        })\n      ).toBe(false); // itemStretchHeight doesn't affect column cross-axis\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/alignment-ui.tsx",
    "content": "import { Box, Flex, Grid, IconButton } from \"@webstudio-is/design-system\";\nimport { DotIcon } from \"@webstudio-is/icons\";\nimport { theme } from \"@webstudio-is/design-system\";\n\n// Hide dots where indicator bars overlap to avoid sub-pixel artifacts.\n// Bars have a cross-axis position (from alignItems) and main-axis position\n// (from justifyContent). A dot is hidden when both axes match.\n//\n// x always maps to justify-content (main-axis position: 0=start, 1=center, 2=end)\n// y always maps to align-items (cross-axis position: 0=start, 1=center, 2=end)\n// The visual transposition for column direction is handled by the CSS grid swap.\n//\n// Cross-axis (y): CSS align-items controls bar position in the cross direction.\n//   stretch/normal → bars span full cross-axis (CSS stretches flex items)\n//   itemStretchWidth (column) / itemStretchHeight (row) → same visual effect\n//   start/baseline/center/end → bars sit at one specific position\n//\n// Main-axis (x): CSS justify-content controls bar distribution.\n//   normal/start → bars clustered at position 0\n//   center → bars at position 1\n//   end → bars at position 2\n//   space-between → bars at positions 0, 1, 2\n//   space-around → bars between positions, no dot overlap\nexport const shouldHideDot = ({\n  x,\n  y,\n  justifyContent,\n  alignItems,\n  isColumnDirection,\n  itemStretchWidth,\n  itemStretchHeight,\n}: {\n  x: number;\n  y: number;\n  justifyContent: string;\n  alignItems: string;\n  isColumnDirection: boolean;\n  itemStretchWidth: boolean;\n  itemStretchHeight: boolean;\n}) => {\n  // Check if the dot's cross-axis (y) position overlaps with bars.\n  // CSS align-items: stretch/normal makes bars span full cross-axis.\n  // Explicit itemStretch* does the same via min-width/height: 100%.\n  const isStretched =\n    alignItems === \"stretch\" ||\n    alignItems === \"normal\" ||\n    (isColumnDirection ? itemStretchWidth : itemStretchHeight);\n\n  let crossMatch = false;\n  if (isStretched) {\n    crossMatch = true;\n  } else {\n    switch (alignItems) {\n      case \"start\":\n      case \"baseline\":\n        crossMatch = y === 0;\n        break;\n      case \"center\":\n        crossMatch = y === 1;\n        break;\n      case \"end\":\n        crossMatch = y === 2;\n        break;\n    }\n  }\n\n  if (!crossMatch) {\n    return false;\n  }\n\n  // Check if the dot's main-axis (x) position overlaps with bars.\n  switch (justifyContent) {\n    case \"normal\":\n    case \"start\":\n      return x === 0;\n    case \"center\":\n      return x === 1;\n    case \"end\":\n      return x === 2;\n    case \"space-between\":\n      return true;\n    // space-around: bars sit between dot positions, no overlap\n  }\n\n  return false;\n};\n\ntype AlignmentVisualProps = {\n  // Visual state\n  justifyContent: string;\n  alignItems: string;\n  isColumnDirection: boolean;\n  color: string;\n  // Item sizing for stretch behavior\n  itemStretchWidth: boolean;\n  itemStretchHeight: boolean;\n  // Click handler for grid buttons\n  onSelect: (position: { x: number; y: number }) => void;\n};\n\nexport const AlignmentUi = ({\n  justifyContent,\n  alignItems,\n  isColumnDirection,\n  color,\n  itemStretchWidth,\n  itemStretchHeight,\n  onSelect,\n}: AlignmentVisualProps) => {\n  const alignment = [\"start\", \"center\", \"end\"];\n  const gridSize = alignment.length;\n\n  return (\n    <Grid\n      tabIndex={0}\n      css={{\n        padding: theme.spacing[3],\n        borderRadius: theme.borderRadius[4],\n        background: theme.colors.backgroundControls,\n        alignItems: \"center\",\n        gap: 0,\n        gridTemplateColumns: \"repeat(3, 1fr)\",\n        gridTemplateRows: \"repeat(3, 1fr)\",\n        color,\n        \"&:focus-within\": {\n          outline: `1px solid ${theme.colors.borderLocalFlexUi}`,\n        },\n      }}\n    >\n      {Array.from(Array(gridSize * gridSize), (_, index) => {\n        const x = index % gridSize;\n        const y = Math.floor(index / gridSize);\n        let gridColumn = `${x + 1} / ${x + 2}`;\n        let gridRow = `${y + 1} / ${y + 2}`;\n        if (isColumnDirection) {\n          [gridColumn, gridRow] = [gridRow, gridColumn];\n        }\n        return (\n          <Flex\n            key={index}\n            justify=\"center\"\n            align=\"center\"\n            css={{\n              width: \"100%\",\n              height: \"100%\",\n              gridColumn,\n              gridRow,\n            }}\n          >\n            <IconButton\n              tabIndex={-1}\n              css={{\n                width: \"90%\",\n                height: \"90%\",\n                minWidth: \"auto\",\n                color: theme.colors.foregroundFlexUiMain,\n                \"&:hover\": {\n                  background: theme.colors.foregroundFlexUiHover,\n                },\n                \"&:focus\": {\n                  background: \"none\",\n                  boxShadow: \"none\",\n                  outline: \"none\",\n                },\n              }}\n              onClick={() => onSelect({ x, y })}\n            >\n              <Box\n                css={{\n                  size: 16,\n                  visibility: shouldHideDot({\n                    x,\n                    y,\n                    justifyContent,\n                    alignItems,\n                    isColumnDirection,\n                    itemStretchWidth,\n                    itemStretchHeight,\n                  })\n                    ? \"hidden\"\n                    : \"visible\",\n                }}\n              >\n                <DotIcon size={8} />\n              </Box>\n            </IconButton>\n          </Flex>\n        );\n      })}\n\n      <Flex\n        css={{\n          width: \"100%\",\n          height: \"100%\",\n          gridArea: \"-1 / -1 / 1 / 1\", // fill whole grid\n          p: 2,\n          gap: 2.5,\n          pointerEvents: \"none\",\n        }}\n        style={{\n          flexDirection: isColumnDirection ? \"column\" : \"row\",\n          justifyContent,\n          alignItems,\n          ...(justifyContent === \"space-between\"\n            ? isColumnDirection\n              ? { paddingTop: 8, paddingBottom: 8 }\n              : { paddingLeft: 8, paddingRight: 8 }\n            : justifyContent === \"space-around\"\n              ? isColumnDirection\n                ? { paddingTop: 14.5, paddingBottom: 14.5 }\n                : { paddingLeft: 14.5, paddingRight: 14.5 }\n              : {}),\n        }}\n      >\n        {[9, 14, 7].map((size) => (\n          <Box\n            key={size}\n            css={{\n              borderRadius: theme.borderRadius[1],\n              backgroundColor: \"currentColor\",\n              flexShrink: 0,\n            }}\n            style={\n              isColumnDirection\n                ? {\n                    minWidth: itemStretchWidth ? \"100%\" : size,\n                    minHeight: 3,\n                  }\n                : {\n                    minWidth: 3,\n                    minHeight: itemStretchHeight ? \"100%\" : size,\n                  }\n            }\n          />\n        ))}\n      </Flex>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/constants.ts",
    "content": "/**\n * Default number of tracks (columns and rows) when a grid is first created.\n */\nexport const DEFAULT_GRID_TRACK_COUNT = 2;\n\n/**\n * Default gap (in px) applied to both column-gap and row-gap when a grid is first created.\n */\nexport const DEFAULT_GRID_GAP = 16;\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/flex-alignment.tsx",
    "content": "import { toValue } from \"@webstudio-is/css-engine\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport { useComputedStyles } from \"../../../shared/model\";\nimport { getPriorityStyleValueSource } from \"../../../property-label\";\nimport { createBatchUpdate } from \"../../../shared/use-style-data\";\nimport { AlignmentUi } from \"./alignment-ui\";\n\nexport const FlexAlignment = () => {\n  const styles = useComputedStyles([\n    \"flex-direction\",\n    \"justify-content\",\n    \"align-items\",\n  ]);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  const [flexDirection, justifyContent, alignItems] = styles;\n\n  const flexDirectionValue = toValue(flexDirection.cascadedValue);\n  const justifyContentValue = toValue(justifyContent.cascadedValue);\n  const alignItemsValue = toValue(alignItems.cascadedValue);\n\n  const isColumnDirection =\n    flexDirectionValue === \"column\" || flexDirectionValue === \"column-reverse\";\n\n  let color = theme.colors.foregroundFlexUiMain;\n  if (styleValueSourceColor === \"local\") {\n    color = theme.colors.foregroundLocalFlexUi;\n  }\n  if (styleValueSourceColor === \"overwritten\") {\n    color = theme.colors.foregroundOverwrittenFlexUi;\n  }\n  if (styleValueSourceColor === \"remote\") {\n    color = theme.colors.foregroundRemoteFlexUi;\n  }\n\n  const alignment = [\"start\", \"center\", \"end\"];\n\n  return (\n    <AlignmentUi\n      justifyContent={justifyContentValue}\n      alignItems={alignItemsValue}\n      isColumnDirection={isColumnDirection}\n      color={color}\n      itemStretchWidth={false}\n      itemStretchHeight={false}\n      onSelect={({ x, y }) => {\n        const justifyContent = alignment[x];\n        const alignItems = alignment[y];\n        const batch = createBatchUpdate();\n        batch.setProperty(\"align-items\")({\n          type: \"keyword\",\n          value: alignItems,\n        });\n        batch.setProperty(\"justify-content\")({\n          type: \"keyword\",\n          value: justifyContent,\n        });\n        batch.publish();\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-alignment.tsx",
    "content": "import { theme } from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { useComputedStyles } from \"../../../shared/model\";\nimport { getPriorityStyleValueSource } from \"../../../property-label\";\nimport { createBatchUpdate } from \"../../../shared/use-style-data\";\nimport { AlignmentUi } from \"./alignment-ui\";\n\nexport const GridAlignment = () => {\n  const styles = useComputedStyles([\n    \"grid-auto-flow\",\n    \"justify-content\",\n    \"justify-items\",\n    \"align-items\",\n  ]);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  const [gridAutoFlow, justifyContent, justifyItems, alignItems] = styles;\n\n  const gridAutoFlowValue = toValue(gridAutoFlow.cascadedValue);\n  const justifyContentValue = toValue(justifyContent.cascadedValue);\n  const justifyItemsValue = toValue(justifyItems.cascadedValue);\n  const alignItemsValue = toValue(alignItems.cascadedValue);\n\n  const isColumnDirection = gridAutoFlowValue.includes(\"column\");\n\n  // For grid, justify-items controls item alignment within cells\n  // When stretch, items fill the track width (row) or height (column)\n  const itemStretchWidth =\n    !isColumnDirection && justifyItemsValue === \"stretch\";\n  const itemStretchHeight =\n    isColumnDirection && justifyItemsValue === \"stretch\";\n\n  let color = theme.colors.foregroundFlexUiMain;\n  if (styleValueSourceColor === \"local\") {\n    color = theme.colors.foregroundLocalFlexUi;\n  }\n  if (styleValueSourceColor === \"overwritten\") {\n    color = theme.colors.foregroundOverwrittenFlexUi;\n  }\n  if (styleValueSourceColor === \"remote\") {\n    color = theme.colors.foregroundRemoteFlexUi;\n  }\n\n  const alignment = [\"start\", \"center\", \"end\"];\n\n  return (\n    <AlignmentUi\n      justifyContent={justifyContentValue}\n      alignItems={alignItemsValue}\n      isColumnDirection={isColumnDirection}\n      color={color}\n      itemStretchWidth={itemStretchWidth}\n      itemStretchHeight={itemStretchHeight}\n      onSelect={({ x, y }) => {\n        const justifyContent = alignment[x];\n        const alignItems = alignment[y];\n        const batch = createBatchUpdate();\n        batch.setProperty(\"align-items\")({\n          type: \"keyword\",\n          value: alignItems,\n        });\n        batch.setProperty(\"justify-content\")({\n          type: \"keyword\",\n          value: justifyContent,\n        });\n        batch.publish();\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-area-picker.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { __testing__ } from \"./grid-area-picker\";\nimport type { AreaInfo } from \"@webstudio-is/css-data\";\n\nconst { buildOccupiedCellMap, clampRectangle, computeCellClick } = __testing__;\n\ndescribe(\"buildOccupiedCellMap\", () => {\n  test(\"returns empty map when no areas\", () => {\n    const map = buildOccupiedCellMap([], 3, 3);\n    expect(map.size).toBe(0);\n  });\n\n  test(\"maps single 1×1 area\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"a\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    const map = buildOccupiedCellMap(areas, 3, 3);\n    expect(map.get(\"1,1\")).toBe(\"a\");\n    expect(map.has(\"2,1\")).toBe(false);\n  });\n\n  test(\"maps multi-cell area\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n    ];\n    const map = buildOccupiedCellMap(areas, 3, 3);\n    expect(map.get(\"1,1\")).toBe(\"header\");\n    expect(map.get(\"2,1\")).toBe(\"header\");\n    expect(map.has(\"3,1\")).toBe(false);\n  });\n\n  test(\"maps multiple areas\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"a\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n      { name: \"b\", columnStart: 2, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n    ];\n    const map = buildOccupiedCellMap(areas, 3, 3);\n    expect(map.get(\"1,1\")).toBe(\"a\");\n    expect(map.get(\"2,1\")).toBe(\"b\");\n  });\n\n  test(\"clamps to grid bounds\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"big\", columnStart: 1, columnEnd: 10, rowStart: 1, rowEnd: 10 },\n    ];\n    const map = buildOccupiedCellMap(areas, 2, 2);\n    expect(map.get(\"1,1\")).toBe(\"big\");\n    expect(map.get(\"2,2\")).toBe(\"big\");\n    expect(map.has(\"3,1\")).toBe(false);\n  });\n});\n\ndescribe(\"clampRectangle\", () => {\n  test(\"returns single cell when no obstacles\", () => {\n    const result = clampRectangle(\n      { col: 1, row: 1 },\n      { col: 1, row: 1 },\n      new Map()\n    );\n    expect(result).toEqual({\n      colStart: 1,\n      colEnd: 1,\n      rowStart: 1,\n      rowEnd: 1,\n    });\n  });\n\n  test(\"returns full rectangle when no obstacles\", () => {\n    const result = clampRectangle(\n      { col: 1, row: 1 },\n      { col: 3, row: 2 },\n      new Map()\n    );\n    expect(result).toEqual({\n      colStart: 1,\n      colEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"shrinks from target side to avoid occupied cell\", () => {\n    const occupied = new Map([[\"3,1\", \"other\"]]);\n    const result = clampRectangle(\n      { col: 1, row: 1 },\n      { col: 3, row: 1 },\n      occupied\n    );\n    expect(result).toEqual({\n      colStart: 1,\n      colEnd: 2,\n      rowStart: 1,\n      rowEnd: 1,\n    });\n  });\n\n  test(\"works when target is before anchor\", () => {\n    const result = clampRectangle(\n      { col: 3, row: 3 },\n      { col: 1, row: 1 },\n      new Map()\n    );\n    expect(result).toEqual({\n      colStart: 1,\n      colEnd: 3,\n      rowStart: 1,\n      rowEnd: 3,\n    });\n  });\n\n  test(\"shrinks columns first, then rows to avoid occupied cell\", () => {\n    // Occupied cell at col 1, row 3 — columns shrink first (colEnd 2→1),\n    // then rows shrink (rowEnd 3→2) to clear \"1,3\"\n    const occupied = new Map([[\"1,3\", \"other\"]]);\n    const result = clampRectangle(\n      { col: 1, row: 1 },\n      { col: 2, row: 3 },\n      occupied\n    );\n    expect(result).toEqual({\n      colStart: 1,\n      colEnd: 1,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n});\n\ndescribe(\"computeCellClick\", () => {\n  const baseArea: AreaInfo = {\n    name: \"test\",\n    columnStart: 1,\n    columnEnd: 2,\n    rowStart: 1,\n    rowEnd: 2,\n  };\n\n  test(\"ignores click on occupied cell\", () => {\n    const occupied = new Map([[\"2,1\", \"other\"]]);\n    const result = computeCellClick(\n      2,\n      1,\n      baseArea,\n      { col: 1, row: 1 },\n      occupied\n    );\n    expect(result).toBeUndefined();\n  });\n\n  test(\"clicking selected cell resets to 1×1 at that cell\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 4,\n      rowStart: 1,\n      rowEnd: 3,\n    };\n    const result = computeCellClick(2, 2, area, { col: 1, row: 1 }, new Map());\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 2,\n        columnEnd: 3,\n        rowStart: 2,\n        rowEnd: 3,\n      },\n      anchor: { col: 2, row: 2 },\n    });\n  });\n\n  test(\"clicking unselected cell extends from anchor\", () => {\n    const result = computeCellClick(\n      3,\n      3,\n      baseArea,\n      { col: 1, row: 1 },\n      new Map()\n    );\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 1,\n        columnEnd: 4,\n        rowStart: 1,\n        rowEnd: 4,\n      },\n      anchor: { col: 1, row: 1 },\n    });\n  });\n\n  test(\"extending to diagonal cell forms full rectangle\", () => {\n    // click (1,1) then (6,6) — should select 1-6 × 1-6\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const result = computeCellClick(6, 6, area, { col: 1, row: 1 }, new Map());\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 1,\n        columnEnd: 7,\n        rowStart: 1,\n        rowEnd: 7,\n      },\n      anchor: { col: 1, row: 1 },\n    });\n  });\n\n  test(\"extending after reset forms new rectangle from new anchor\", () => {\n    // After resetting to (2,2), clicking (7,7) should select 2-7 × 2-7\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    const result = computeCellClick(7, 7, area, { col: 2, row: 2 }, new Map());\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 2,\n        columnEnd: 8,\n        rowStart: 2,\n        rowEnd: 8,\n      },\n      anchor: { col: 2, row: 2 },\n    });\n  });\n\n  test(\"extending clamps around occupied cells\", () => {\n    const occupied = new Map([[\"3,1\", \"other\"]]);\n    const result = computeCellClick(\n      4,\n      1,\n      baseArea,\n      { col: 1, row: 1 },\n      occupied\n    );\n    // Can't include col 3, so clamps to col 1-2\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 1,\n        columnEnd: 3,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      anchor: { col: 1, row: 1 },\n    });\n  });\n\n  test(\"clicking the only selected cell (1×1) still resets anchor\", () => {\n    const result = computeCellClick(\n      1,\n      1,\n      baseArea,\n      { col: 1, row: 1 },\n      new Map()\n    );\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      anchor: { col: 1, row: 1 },\n    });\n  });\n\n  test(\"clicking selected cell in square selection resets to 1×1\", () => {\n    const squareArea: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 4,\n      rowStart: 1,\n      rowEnd: 4,\n    };\n    const result = computeCellClick(\n      2,\n      2,\n      squareArea,\n      { col: 1, row: 1 },\n      new Map()\n    );\n    expect(result).toEqual({\n      area: {\n        name: \"test\",\n        columnStart: 2,\n        columnEnd: 3,\n        rowStart: 2,\n        rowEnd: 3,\n      },\n      anchor: { col: 2, row: 2 },\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-area-picker.tsx",
    "content": "import { useRef, useMemo } from \"react\";\nimport { theme, Grid, Tooltip, css } from \"@webstudio-is/design-system\";\nimport type { AreaInfo } from \"@webstudio-is/css-data\";\n\n/**\n * Build a map from \"col,row\" keys to area names for cells occupied by other areas.\n */\nconst buildOccupiedCellMap = (\n  otherAreas: AreaInfo[],\n  gridColumns: number,\n  gridRows: number\n): Map<string, string> => {\n  const map = new Map<string, string>();\n  for (const area of otherAreas) {\n    for (let r = area.rowStart; r < area.rowEnd; r++) {\n      for (let c = area.columnStart; c < area.columnEnd; c++) {\n        if (c >= 1 && c <= gridColumns && r >= 1 && r <= gridRows) {\n          map.set(`${c},${r}`, area.name);\n        }\n      }\n    }\n  }\n  return map;\n};\n\n/**\n * Clamp a rectangle so it does not include any occupied cells.\n * Shrinks the rectangle edge-by-edge from the extension side.\n */\nconst clampRectangle = (\n  anchor: { col: number; row: number },\n  target: { col: number; row: number },\n  occupied: Map<string, string>\n): { colStart: number; colEnd: number; rowStart: number; rowEnd: number } => {\n  let colStart = Math.min(anchor.col, target.col);\n  let colEnd = Math.max(anchor.col, target.col);\n  let rowStart = Math.min(anchor.row, target.row);\n  let rowEnd = Math.max(anchor.row, target.row);\n\n  const hasOccupied = (\n    cs: number,\n    ce: number,\n    rs: number,\n    re: number\n  ): boolean => {\n    for (let r = rs; r <= re; r++) {\n      for (let c = cs; c <= ce; c++) {\n        if (occupied.has(`${c},${r}`)) {\n          return true;\n        }\n      }\n    }\n    return false;\n  };\n\n  // Shrink from the side opposite to anchor until no occupied cells remain\n  while (hasOccupied(colStart, colEnd, rowStart, rowEnd)) {\n    const shrunk =\n      // Try shrinking column range from the side away from anchor\n      (colEnd > anchor.col && colEnd > colStart && ((colEnd -= 1), true)) ||\n      (colStart < anchor.col && colStart < colEnd && ((colStart += 1), true)) ||\n      // Try shrinking row range from the side away from anchor\n      (rowEnd > anchor.row && rowEnd > rowStart && ((rowEnd -= 1), true)) ||\n      (rowStart < anchor.row && rowStart < rowEnd && ((rowStart += 1), true));\n\n    if (!shrunk) {\n      break;\n    }\n  }\n\n  return { colStart, colEnd, rowStart, rowEnd };\n};\n\n/**\n * Compute the result of clicking a cell in the area picker.\n *\n * Returns `undefined` when the click should be ignored (occupied cell).\n * Otherwise returns `{ area, anchor }` with the new area bounds and anchor point.\n *\n * - Clicking an occupied cell → ignored\n * - Clicking a selected cell → resets to 1×1 at that cell (new anchor)\n * - Clicking an unselected cell → extends from anchor, clamped around obstacles\n */\nconst computeCellClick = (\n  col: number,\n  row: number,\n  value: AreaInfo,\n  anchor: { col: number; row: number },\n  occupied: Map<string, string>\n): { area: AreaInfo; anchor: { col: number; row: number } } | undefined => {\n  if (occupied.has(`${col},${row}`)) {\n    return undefined;\n  }\n\n  const isSelected =\n    col >= value.columnStart &&\n    col < value.columnEnd &&\n    row >= value.rowStart &&\n    row < value.rowEnd;\n\n  // Click a selected cell → deselect all, set this cell as the new anchor\n  if (isSelected) {\n    return {\n      area: {\n        ...value,\n        columnStart: col,\n        columnEnd: col + 1,\n        rowStart: row,\n        rowEnd: row + 1,\n      },\n      anchor: { col, row },\n    };\n  }\n\n  // Click an unselected cell → extend from anchor to form a rectangle\n  const rect = clampRectangle(anchor, { col, row }, occupied);\n  return {\n    area: {\n      ...value,\n      columnStart: rect.colStart,\n      columnEnd: rect.colEnd + 1,\n      rowStart: rect.rowStart,\n      rowEnd: rect.rowEnd + 1,\n    },\n    anchor,\n  };\n};\n\n// Export for testing only\nexport const __testing__ = {\n  buildOccupiedCellMap,\n  clampRectangle,\n  computeCellClick,\n};\n\nconst freeCellStyle = css({\n  minHeight: 20,\n  border: \"none\",\n  padding: 0,\n  backgroundColor: theme.colors.backgroundControls,\n  transition: \"background-color 0.1s ease\",\n  cursor: \"pointer\",\n  \"&:hover\": {\n    backgroundColor: theme.colors.backgroundPrimary,\n  },\n});\n\nconst selectedCellStyle = css({\n  minHeight: 20,\n  border: \"none\",\n  padding: 0,\n  backgroundColor: \"transparent\",\n  cursor: \"pointer\",\n  \"&:hover\": {\n    backgroundColor: \"rgba(255, 255, 255, 0.15)\",\n  },\n});\n\nconst occupiedAreaStyle = css({\n  minHeight: 20,\n  border: \"none\",\n  padding: 0,\n  borderRadius: theme.borderRadius[3],\n  backgroundColor: theme.colors.backgroundDestructiveMain,\n  cursor: \"not-allowed\",\n  opacity: 0.6,\n});\n\nconst selectedAreaBgStyle = css({\n  border: \"none\",\n  borderRadius: theme.borderRadius[3],\n  backgroundColor: theme.colors.backgroundPrimary,\n  pointerEvents: \"none\",\n});\n\ntype GridAreaPickerProps = {\n  value: AreaInfo;\n  onChange: (value: AreaInfo) => void;\n  onHoverChange?: (area: AreaInfo | undefined) => void;\n  gridColumns: number;\n  gridRows: number;\n  otherAreas: AreaInfo[];\n};\n\nexport const GridAreaPicker = ({\n  value,\n  onChange,\n  onHoverChange,\n  gridColumns,\n  gridRows,\n  otherAreas,\n}: GridAreaPickerProps) => {\n  const anchorRef = useRef<{ col: number; row: number }>({\n    col: value.columnStart,\n    row: value.rowStart,\n  });\n\n  const occupied = useMemo(\n    () => buildOccupiedCellMap(otherAreas, gridColumns, gridRows),\n    [otherAreas, gridColumns, gridRows]\n  );\n\n  const handleCellClick = (col: number, row: number) => {\n    const result = computeCellClick(\n      col,\n      row,\n      value,\n      anchorRef.current,\n      occupied\n    );\n    if (result === undefined) {\n      return;\n    }\n    anchorRef.current = result.anchor;\n    onChange(result.area);\n  };\n\n  // Deduplicate areas for rendering as single spanning elements\n  const visibleAreas = useMemo(() => {\n    const seen = new Set<string>();\n    return otherAreas.filter((area) => {\n      if (seen.has(area.name)) {\n        return false;\n      }\n      if (area.columnStart > gridColumns || area.rowStart > gridRows) {\n        return false;\n      }\n      if (area.columnEnd <= 1 || area.rowEnd <= 1) {\n        return false;\n      }\n      seen.add(area.name);\n      return true;\n    });\n  }, [otherAreas, gridColumns, gridRows]);\n\n  const hasSelection =\n    value.columnEnd > value.columnStart && value.rowEnd > value.rowStart;\n\n  // Individual buttons for free and selected cells\n  const cells = useMemo(() => {\n    const result: React.ReactNode[] = [];\n    for (let row = 1; row <= gridRows; row++) {\n      for (let col = 1; col <= gridColumns; col++) {\n        if (occupied.has(`${col},${row}`)) {\n          continue;\n        }\n        const isSelected =\n          col >= value.columnStart &&\n          col < value.columnEnd &&\n          row >= value.rowStart &&\n          row < value.rowEnd;\n        result.push(\n          <button\n            key={`${col},${row}`}\n            className={isSelected ? selectedCellStyle() : freeCellStyle()}\n            style={{\n              gridColumn: `${col} / ${col + 1}`,\n              gridRow: `${row} / ${row + 1}`,\n            }}\n            onClick={() => handleCellClick(col, row)}\n            onMouseEnter={() => {\n              onHoverChange?.({\n                name: \"\",\n                columnStart: col,\n                columnEnd: col + 1,\n                rowStart: row,\n                rowEnd: row + 1,\n              });\n            }}\n            onMouseLeave={() => onHoverChange?.(undefined)}\n            aria-label={`Cell ${col}, ${row}`}\n          />\n        );\n      }\n    }\n    return result;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [\n    gridColumns,\n    gridRows,\n    occupied,\n    value.columnStart,\n    value.columnEnd,\n    value.rowStart,\n    value.rowEnd,\n  ]);\n\n  return (\n    <Grid\n      onMouseLeave={() => onHoverChange?.(undefined)}\n      css={{\n        width: \"100%\",\n        gridTemplateColumns: `repeat(${gridColumns}, minmax(20px, 1fr))`,\n        gridTemplateRows: `repeat(${gridRows}, 20px)`,\n        gap: 1,\n        backgroundColor: theme.colors.borderMain,\n        borderRadius: theme.borderRadius[4],\n        overflow: \"hidden\",\n      }}\n    >\n      {/* Selected area background: single element spanning the whole selection */}\n      {hasSelection && (\n        <div\n          className={selectedAreaBgStyle()}\n          style={{\n            gridColumn: `${value.columnStart} / ${value.columnEnd}`,\n            gridRow: `${value.rowStart} / ${value.rowEnd}`,\n          }}\n        />\n      )}\n\n      {/* Occupied areas: one spanning element per area */}\n      {visibleAreas.map((area) => {\n        const colStart = Math.max(1, area.columnStart);\n        const colEnd = Math.min(gridColumns + 1, area.columnEnd);\n        const rowStart = Math.max(1, area.rowStart);\n        const rowEnd = Math.min(gridRows + 1, area.rowEnd);\n        return (\n          <Tooltip key={area.name} content={area.name}>\n            <button\n              disabled\n              className={occupiedAreaStyle()}\n              style={{\n                gridColumn: `${colStart} / ${colEnd}`,\n                gridRow: `${rowStart} / ${rowEnd}`,\n              }}\n              onMouseEnter={() => onHoverChange?.(area)}\n              onMouseLeave={() => onHoverChange?.(undefined)}\n              aria-label={`Area ${area.name}`}\n            />\n          </Tooltip>\n        );\n      })}\n\n      {cells}\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-areas.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  parseGridAreas,\n  getGridDimensions,\n  __testing__,\n  type AreaInfo,\n} from \"./grid-areas\";\n\nconst {\n  generateGridTemplate,\n  checkOverlap,\n  generateUniqueAreaName,\n  findNonOverlappingPosition,\n  isAreaWithinBounds,\n  filterAreasWithinBounds,\n  toAreaName,\n} = __testing__;\n\ndescribe(\"parseGridAreas\", () => {\n  test(\"parses empty or none value\", () => {\n    expect(parseGridAreas(\"\")).toEqual([]);\n    expect(parseGridAreas(\"none\")).toEqual([]);\n  });\n\n  test(\"parses single area\", () => {\n    const result = parseGridAreas('\"header\"');\n    expect(result).toEqual([\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ]);\n  });\n\n  test(\"parses area spanning multiple columns\", () => {\n    const result = parseGridAreas('\"header header header\"');\n    expect(result).toEqual([\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 4,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ]);\n  });\n\n  test(\"parses multiple areas in one row\", () => {\n    const result = parseGridAreas('\"sidebar main\"');\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"parses area spanning multiple rows\", () => {\n    const result = parseGridAreas('\"sidebar main\" \"sidebar footer\"');\n    expect(result).toHaveLength(3);\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 3,\n    });\n  });\n\n  test(\"parses complex layout with multiple rows and columns\", () => {\n    const result = parseGridAreas(\n      '\"header header\" \"sidebar main\" \"footer footer\"'\n    );\n    expect(result).toHaveLength(4);\n    expect(result).toContainEqual({\n      name: \"header\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"footer\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 3,\n      rowEnd: 4,\n    });\n  });\n\n  test(\"ignores dots (empty cells)\", () => {\n    const result = parseGridAreas('\"header .\" \". main\"');\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      name: \"header\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n  });\n});\n\ndescribe(\"generateGridTemplate\", () => {\n  test(\"generates template for single area\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe('\"header .\" \". .\"');\n  });\n\n  test(\"generates template for area spanning columns\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe('\"header header\" \". .\"');\n  });\n\n  test(\"generates template for complex layout\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n      { name: \"sidebar\", columnStart: 1, columnEnd: 2, rowStart: 2, rowEnd: 3 },\n      { name: \"main\", columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe(\n      '\"header header\" \"sidebar main\"'\n    );\n  });\n\n  test(\"fills empty cells with dots\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n      { name: \"footer\", columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe('\"header .\" \". footer\"');\n  });\n\n  test(\"handles area out of bounds gracefully\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 10,\n        rowStart: 1,\n        rowEnd: 10,\n      },\n    ];\n    // Should only fill within the grid bounds\n    expect(generateGridTemplate(areas, 2, 2)).toBe(\n      '\"header header\" \"header header\"'\n    );\n  });\n});\n\ndescribe(\"getGridDimensions\", () => {\n  test(\"parses column and row counts\", () => {\n    expect(getGridDimensions(\"1fr 2fr\", \"100px 200px\")).toEqual({\n      columns: 2,\n      rows: 2,\n    });\n  });\n\n  test(\"handles complex track definitions\", () => {\n    expect(\n      getGridDimensions(\"repeat(3, 1fr)\", \"minmax(100px, 1fr) auto 200px\")\n    ).toEqual({\n      columns: 3, // repeat(3, 1fr) = 3 columns\n      rows: 3, // minmax(100px, 1fr), auto, 200px = 3 rows\n    });\n  });\n\n  test(\"defaults to 2 when none\", () => {\n    expect(getGridDimensions(\"none\", \"none\")).toEqual({\n      columns: 2,\n      rows: 2,\n    });\n  });\n\n  test(\"defaults to 2 when empty\", () => {\n    expect(getGridDimensions(\"\", \"\")).toEqual({\n      columns: 2,\n      rows: 2,\n    });\n  });\n});\n\ndescribe(\"checkOverlap\", () => {\n  test(\"detects overlapping areas\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 3,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 4,\n      rowStart: 2,\n      rowEnd: 4,\n    };\n    expect(checkOverlap(area1, area2)).toBe(true);\n  });\n\n  test(\"detects non-overlapping areas horizontally\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(checkOverlap(area1, area2)).toBe(false);\n  });\n\n  test(\"detects non-overlapping areas vertically\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    expect(checkOverlap(area1, area2)).toBe(false);\n  });\n\n  test(\"detects complete containment as overlap\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 5,\n      rowStart: 1,\n      rowEnd: 5,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 4,\n      rowStart: 2,\n      rowEnd: 4,\n    };\n    expect(checkOverlap(area1, area2)).toBe(true);\n  });\n\n  test(\"detects edge adjacency as non-overlapping\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    expect(checkOverlap(area1, area2)).toBe(false);\n  });\n});\n\ndescribe(\"generateUniqueAreaName\", () => {\n  test(\"returns 'Area' when no existing names\", () => {\n    expect(generateUniqueAreaName([])).toBe(\"Area\");\n  });\n\n  test(\"returns 'Area' when it doesn't exist\", () => {\n    expect(generateUniqueAreaName([\"Header\", \"Footer\"])).toBe(\"Area\");\n  });\n\n  test(\"returns 'Area-1' when 'Area' exists\", () => {\n    expect(generateUniqueAreaName([\"Area\"])).toBe(\"Area-1\");\n  });\n\n  test(\"returns next available number\", () => {\n    expect(generateUniqueAreaName([\"Area\", \"Area-1\", \"Area-2\"])).toBe(\"Area-3\");\n  });\n\n  test(\"handles gaps in numbering\", () => {\n    expect(generateUniqueAreaName([\"Area\", \"Area-2\", \"Area-5\"])).toBe(\"Area-1\");\n  });\n});\n\ndescribe(\"findNonOverlappingPosition\", () => {\n  test(\"finds first cell when grid is empty\", () => {\n    const result = findNonOverlappingPosition([], 2, 2);\n    expect(result.needsNewRow).toBe(false);\n    expect(result.area).toMatchObject({\n      name: \"Area\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"finds next available cell\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"Area\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.needsNewRow).toBe(false);\n    expect(result.area).toMatchObject({\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"generates unique name for new area\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"Area\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.area.name).toBe(\"Area-1\");\n  });\n\n  test(\"adds new row when grid is full\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"Area\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n      { name: \"Area-1\", columnStart: 2, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n      { name: \"Area-2\", columnStart: 1, columnEnd: 2, rowStart: 2, rowEnd: 3 },\n      { name: \"Area-3\", columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.needsNewRow).toBe(true);\n    expect(result.area).toMatchObject({\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 3,\n      rowEnd: 4,\n    });\n  });\n\n  test(\"finds cell when area spans multiple cells\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.needsNewRow).toBe(false);\n    expect(result.area).toMatchObject({\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n  });\n});\n\ndescribe(\"isAreaWithinBounds\", () => {\n  test(\"validates area within bounds\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(true);\n  });\n\n  test(\"rejects area with invalid column start\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 0,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with column start >= column end\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 2,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with column end beyond grid\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 4,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with invalid row start\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 0,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with row start >= row end\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with row end beyond grid\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 4,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"allows area at grid boundary\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(true);\n  });\n});\n\ndescribe(\"filterAreasWithinBounds\", () => {\n  test(\"returns all areas when all are valid\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"area1\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"area2\",\n        columnStart: 2,\n        columnEnd: 3,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 3, 2);\n    expect(result).toEqual(areas);\n    expect(result).toHaveLength(2);\n  });\n\n  test(\"filters out single area that exceeds column bounds\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"valid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid\",\n        columnStart: 1,\n        columnEnd: 4,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe(\"valid\");\n  });\n\n  test(\"filters out single area that exceeds row bounds\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"valid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 4,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe(\"valid\");\n  });\n\n  test(\"filters out multiple invalid areas\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"valid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid1\",\n        columnStart: 1,\n        columnEnd: 5,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid2\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 5,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe(\"valid\");\n  });\n\n  test(\"returns empty array when all areas are invalid\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"invalid1\",\n        columnStart: 1,\n        columnEnd: 5,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid2\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 5,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toEqual([]);\n  });\n\n  test(\"returns empty array for empty input\", () => {\n    const result = filterAreasWithinBounds([], 3, 3);\n    expect(result).toEqual([]);\n  });\n\n  test(\"preserves order of valid areas\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"first\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid\",\n        columnStart: 1,\n        columnEnd: 5,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"second\",\n        columnStart: 2,\n        columnEnd: 3,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 3, 2);\n    expect(result).toHaveLength(2);\n    expect(result[0].name).toBe(\"first\");\n    expect(result[1].name).toBe(\"second\");\n  });\n});\n\ndescribe(\"toAreaName\", () => {\n  test(\"replaces spaces with dashes\", () => {\n    expect(toAreaName(\"Area 1\")).toBe(\"Area-1\");\n  });\n\n  test(\"replaces multiple consecutive spaces with a single dash\", () => {\n    expect(toAreaName(\"my   area\")).toBe(\"my-area\");\n  });\n\n  test(\"replaces leading and trailing spaces\", () => {\n    expect(toAreaName(\" header \")).toBe(\"-header-\");\n  });\n\n  test(\"returns single word unchanged\", () => {\n    expect(toAreaName(\"sidebar\")).toBe(\"sidebar\");\n  });\n\n  test(\"handles empty string\", () => {\n    expect(toAreaName(\"\")).toBe(\"\");\n  });\n\n  test(\"handles tabs and mixed whitespace\", () => {\n    expect(toAreaName(\"a\\tb\")).toBe(\"a-b\");\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-areas.tsx",
    "content": "import { useState, useCallback, useEffect } from \"react\";\nimport {\n  theme,\n  Flex,\n  Text,\n  IconButton,\n  FloatingPanel,\n  Label,\n  InputField,\n  Grid,\n  CssValueListItem,\n  CssValueListArrowFocus,\n  SmallIconButton,\n} from \"@webstudio-is/design-system\";\nimport { PlusIcon, MinusIcon } from \"@webstudio-is/icons\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { lexer } from \"css-tree\";\nimport {\n  parseGridTemplateTrackList,\n  parseGridAreas,\n  type AreaInfo,\n} from \"@webstudio-is/css-data\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport { useComputedStyleDecl } from \"../../../shared/model\";\nimport { createBatchUpdate } from \"../../../shared/use-style-data\";\nimport { GridPositionInputs } from \"./grid-position-inputs\";\nimport { GridAreaPicker } from \"./grid-area-picker\";\nimport { $gridEditingArea } from \"~/builder/shared/nano-states\";\n\nexport { parseGridAreas, type AreaInfo } from \"@webstudio-is/css-data\";\n\n/**\n * Convert a user-typed area name into a valid CSS <custom-ident>.\n * Replaces whitespace runs with a single dash.\n */\nconst toAreaName = (input: string): string => input.replace(/\\s+/g, \"-\");\n\n/**\n * Generate grid-template-areas CSS string from area information\n * @example\n * generateGridTemplate([\n *   { name: 'header', columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 }\n * ], 2, 2)\n * // Returns: '\"header header\" \". .\"'\n */\nconst generateGridTemplate = (\n  areas: AreaInfo[],\n  columns: number,\n  rows: number\n): string => {\n  // Create a 2D grid filled with dots\n  const grid: string[][] = Array.from({ length: rows }, () =>\n    Array(columns).fill(\".\")\n  );\n\n  // Fill in each area\n  areas.forEach((area) => {\n    for (let r = area.rowStart - 1; r < area.rowEnd - 1; r++) {\n      for (let c = area.columnStart - 1; c < area.columnEnd - 1; c++) {\n        if (r >= 0 && r < rows && c >= 0 && c < columns) {\n          grid[r][c] = area.name;\n        }\n      }\n    }\n  });\n\n  // Convert to CSS string format\n  return grid.map((row) => `\"${row.join(\" \")}\"`).join(\" \");\n};\n\n/**\n * Get grid dimensions from template columns/rows CSS values\n * @example\n * getGridDimensions('1fr 2fr', '100px 200px 300px')\n * // Returns: { columns: 2, rows: 3 }\n */\nexport const getGridDimensions = (\n  columnsValue: string,\n  rowsValue: string\n): { columns: number; rows: number } => {\n  const columnTracks = parseGridTemplateTrackList(columnsValue);\n  const rowTracks = parseGridTemplateTrackList(rowsValue);\n  // Default to 2 if no tracks (for \"none\" or empty values)\n  const columns = columnTracks.length || 2;\n  const rows = rowTracks.length || 2;\n  return { columns, rows };\n};\n\n/**\n * Check if two grid areas overlap\n * @example\n * checkOverlap(\n *   { name: 'a', columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n *   { name: 'b', columnStart: 2, columnEnd: 4, rowStart: 1, rowEnd: 2 }\n * )\n * // Returns: true (they overlap in columns 2)\n */\nconst checkOverlap = (area1: AreaInfo, area2: AreaInfo): boolean => {\n  return (\n    area1.columnStart < area2.columnEnd &&\n    area1.columnEnd > area2.columnStart &&\n    area1.rowStart < area2.rowEnd &&\n    area1.rowEnd > area2.rowStart\n  );\n};\n\n/**\n * Generate a unique area name based on existing names\n * @example\n * generateUniqueAreaName(['Area', 'Area-1'])\n * // Returns: 'Area-2'\n */\nconst generateUniqueAreaName = (existingNames: string[]): string => {\n  const baseName = \"Area\";\n  const existingNamesSet = new Set(existingNames);\n\n  if (!existingNamesSet.has(baseName)) {\n    return baseName;\n  }\n\n  let counter = 1;\n  while (existingNamesSet.has(`${baseName}-${counter}`)) {\n    counter++;\n  }\n  return `${baseName}-${counter}`;\n};\n\n/**\n * Find a non-overlapping position for a new area in the grid\n * Returns the area with a unique name and whether a new row is needed\n * @example\n * findNonOverlappingPosition([], 2, 2)\n * // Returns: { area: { name: 'Area', columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 }, needsNewRow: false }\n */\nconst findNonOverlappingPosition = (\n  existingAreas: AreaInfo[],\n  gridColumns: number,\n  gridRows: number\n): { area: AreaInfo; needsNewRow: boolean } => {\n  const uniqueName = generateUniqueAreaName(existingAreas.map((a) => a.name));\n\n  // Try to find an empty cell in the existing grid\n  for (let row = 1; row <= gridRows; row++) {\n    for (let col = 1; col <= gridColumns; col++) {\n      const testArea: AreaInfo = {\n        name: uniqueName,\n        columnStart: col,\n        columnEnd: col + 1,\n        rowStart: row,\n        rowEnd: row + 1,\n      };\n\n      const hasOverlap = existingAreas.some((existing) =>\n        checkOverlap(testArea, existing)\n      );\n\n      if (!hasOverlap) {\n        return { area: testArea, needsNewRow: false };\n      }\n    }\n  }\n\n  // No empty cell found, need to add a new row\n  return {\n    area: {\n      name: uniqueName,\n      columnStart: 1,\n      columnEnd: gridColumns + 1,\n      rowStart: gridRows + 1,\n      rowEnd: gridRows + 2,\n    },\n    needsNewRow: true,\n  };\n};\n\n/**\n * Validate area bounds are within grid dimensions\n */\nconst isAreaWithinBounds = (\n  area: AreaInfo,\n  gridColumns: number,\n  gridRows: number\n): boolean => {\n  return (\n    area.columnStart >= 1 &&\n    area.columnStart < area.columnEnd &&\n    area.columnEnd <= gridColumns + 1 &&\n    area.rowStart >= 1 &&\n    area.rowStart < area.rowEnd &&\n    area.rowEnd <= gridRows + 1\n  );\n};\n\n/**\n * Filter out areas that don't fit within the grid dimensions\n * Used when grid dimensions change (rows/columns deleted)\n */\nconst filterAreasWithinBounds = (\n  areas: AreaInfo[],\n  gridColumns: number,\n  gridRows: number\n): AreaInfo[] => {\n  return areas.filter((area) =>\n    isAreaWithinBounds(area, gridColumns, gridRows)\n  );\n};\n\n// Export for testing only\nexport const __testing__ = {\n  generateGridTemplate,\n  checkOverlap,\n  generateUniqueAreaName,\n  findNonOverlappingPosition,\n  isAreaWithinBounds,\n  filterAreasWithinBounds,\n  toAreaName,\n};\n\ntype AreaEditorProps = {\n  area: AreaInfo | undefined;\n  editingIndex: number | undefined;\n  gridColumns: number;\n  gridRows: number;\n  existingAreas: AreaInfo[];\n  onSave: (area: AreaInfo, oldName?: string) => void;\n  onClose: () => void;\n};\n\nconst AreaEditor = ({\n  area,\n  editingIndex,\n  gridColumns,\n  gridRows,\n  existingAreas,\n  onSave,\n  onClose,\n}: AreaEditorProps) => {\n  const [value, setValue] = useState<AreaInfo>(\n    area || {\n      name: \"Area\",\n      columnStart: 1,\n      columnEnd: gridColumns + 1,\n      rowStart: 1,\n      rowEnd: gridRows + 1,\n    }\n  );\n\n  const handleSave = useCallback(\n    (overrideValue?: AreaInfo) => {\n      const current = overrideValue ?? value;\n      const trimmedName = toAreaName(current.name.trim()) || \"Area\";\n\n      // Validate CSS identifier using css-tree lexer\n      if (!lexer.match(\"<custom-ident>\", trimmedName).matched) {\n        return;\n      }\n\n      // Filter out the area being edited from validation using index\n      const otherAreas =\n        editingIndex !== undefined\n          ? existingAreas.filter((_, index) => index !== editingIndex)\n          : existingAreas;\n\n      // Check for duplicate names\n      const hasDuplicateName = otherAreas.some(\n        (existingArea) => existingArea.name === trimmedName\n      );\n\n      if (hasDuplicateName) {\n        return;\n      }\n\n      // Validate bounds\n      if (!isAreaWithinBounds(current, gridColumns, gridRows)) {\n        return;\n      }\n\n      // Check for overlaps with other areas\n      const hasOverlap = otherAreas.some((existingArea) =>\n        checkOverlap(current, existingArea)\n      );\n\n      if (hasOverlap) {\n        return;\n      }\n\n      onSave(\n        {\n          ...current,\n          name: trimmedName,\n        },\n        area?.name\n      );\n    },\n    [value, area, existingAreas, onSave, editingIndex, gridColumns, gridRows]\n  );\n\n  // Validation is now handled by GridPositionInputs component\n\n  // Check for duplicate names\n  const trimmedName = toAreaName(value.name.trim());\n  // Filter out the area being edited from the comparison using index\n  const otherAreas =\n    editingIndex !== undefined\n      ? existingAreas.filter((_, index) => index !== editingIndex)\n      : existingAreas;\n\n  const hasDuplicateName =\n    trimmedName !== \"\" &&\n    trimmedName !== area?.name && // Don't show error if name hasn't changed\n    otherAreas.some((existingArea) => existingArea.name === trimmedName);\n\n  // Check for overlaps with other areas\n  const hasOverlap = otherAreas.some((existingArea) =>\n    checkOverlap(value, existingArea)\n  );\n\n  return (\n    <Flex direction=\"column\" gap=\"2\" css={{ padding: theme.panel.padding }}>\n      <Grid\n        css={{\n          gridTemplateColumns: \"60px 1fr 1fr\",\n          gap: theme.spacing[3],\n          alignItems: \"center\",\n        }}\n      >\n        <Label>Name</Label>\n        <InputField\n          css={{ gridColumn: \"span 2\" }}\n          value={value.name}\n          onChange={(event) => setValue({ ...value, name: event.target.value })}\n          onBlur={() => handleSave()}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\") {\n              onClose();\n            } else if (event.key === \"Enter\") {\n              handleSave();\n            }\n          }}\n          color={hasDuplicateName ? \"error\" : undefined}\n          placeholder=\"Area\"\n          autoFocus\n        />\n      </Grid>\n\n      <Grid\n        css={{\n          gridTemplateColumns: \"60px 1fr\",\n          gap: theme.spacing[3],\n          alignItems: \"start\",\n        }}\n      >\n        <Label css={{ paddingTop: theme.spacing[3] }}>Position</Label>\n        <GridPositionInputs\n          value={{\n            ...value,\n            columnEnd: value.columnEnd - 1,\n            rowEnd: value.rowEnd - 1,\n          }}\n          onChange={(position) =>\n            setValue({\n              ...value,\n              ...position,\n              columnEnd: position.columnEnd + 1,\n              rowEnd: position.rowEnd + 1,\n            })\n          }\n          onBlur={() => handleSave()}\n          onKeyDown={(event) => {\n            if (event.key === \"Escape\") {\n              onClose();\n            } else if (event.key === \"Enter\") {\n              handleSave();\n            }\n          }}\n          gridColumns={gridColumns}\n          gridRows={gridRows}\n          checkBounds\n          inclusiveEnd\n        />\n      </Grid>\n\n      <GridAreaPicker\n        value={value}\n        onChange={(picked) => {\n          setValue(picked);\n          handleSave(picked);\n        }}\n        onHoverChange={(hovered) => {\n          if (hovered) {\n            $gridEditingArea.set({\n              columnStart: hovered.columnStart,\n              columnEnd: hovered.columnEnd,\n              rowStart: hovered.rowStart,\n              rowEnd: hovered.rowEnd,\n            });\n          } else {\n            // Restore current value highlight\n            $gridEditingArea.set({\n              columnStart: value.columnStart,\n              columnEnd: value.columnEnd,\n              rowStart: value.rowStart,\n              rowEnd: value.rowEnd,\n            });\n          }\n        }}\n        gridColumns={gridColumns}\n        gridRows={gridRows}\n        otherAreas={otherAreas}\n      />\n\n      {hasDuplicateName && (\n        <Text variant=\"labels\" color=\"destructive\">\n          Area name already exists\n        </Text>\n      )}\n      {hasOverlap && (\n        <Text variant=\"labels\" color=\"destructive\">\n          Area overlaps with another area\n        </Text>\n      )}\n    </Flex>\n  );\n};\n\nexport const GridAreas = () => {\n  const [isOpen, setIsOpen] = useOpenState(\"Areas\");\n  const [editingAreaIndex, setEditingAreaIndex] = useState<number | undefined>(\n    undefined\n  );\n  const [hoveredAreaIndex, setHoveredAreaIndex] = useState<number | undefined>(\n    undefined\n  );\n\n  const gridTemplateAreas = useComputedStyleDecl(\"grid-template-areas\");\n  const gridTemplateColumns = useComputedStyleDecl(\"grid-template-columns\");\n  const gridTemplateRows = useComputedStyleDecl(\"grid-template-rows\");\n\n  const areasValue = toValue(gridTemplateAreas.cascadedValue);\n  const columnsValue = toValue(gridTemplateColumns.cascadedValue);\n  const rowsValue = toValue(gridTemplateRows.cascadedValue);\n\n  const areas = parseGridAreas(areasValue);\n  const { columns, rows } = getGridDimensions(columnsValue, rowsValue);\n\n  // Update grid editing area highlight when editing or hovering an area\n  // Editing takes priority over hovering\n  const highlightIndex = editingAreaIndex ?? hoveredAreaIndex;\n  useEffect(() => {\n    if (highlightIndex !== undefined && areas[highlightIndex]) {\n      const area = areas[highlightIndex];\n      $gridEditingArea.set({\n        columnStart: area.columnStart,\n        columnEnd: area.columnEnd,\n        rowStart: area.rowStart,\n        rowEnd: area.rowEnd,\n      });\n    } else {\n      $gridEditingArea.set(undefined);\n    }\n    return () => {\n      $gridEditingArea.set(undefined);\n    };\n  }, [highlightIndex, areas]);\n\n  const saveArea = useCallback(\n    (newArea: AreaInfo, oldName?: string) => {\n      const batch = createBatchUpdate();\n\n      let updatedAreas = [...areas];\n\n      // Check if oldName exists in current areas\n      const isUpdate = oldName && areas.some((a) => a.name === oldName);\n\n      if (isUpdate) {\n        // Update existing area\n        updatedAreas = updatedAreas.map((a) =>\n          a.name === oldName ? newArea : a\n        );\n      } else {\n        // Add new area\n        updatedAreas.push(newArea);\n      }\n\n      // Filter out areas that are out of bounds\n      const validAreas = filterAreasWithinBounds(updatedAreas, columns, rows);\n\n      const template = generateGridTemplate(validAreas, columns, rows);\n      batch.setProperty(\"grid-template-areas\")({\n        type: \"unparsed\",\n        value: template,\n      });\n      batch.publish();\n    },\n    [areas, columns, rows]\n  );\n\n  const removeArea = useCallback(\n    (areaName: string) => {\n      const batch = createBatchUpdate();\n      const remainingAreas = areas.filter((a) => a.name !== areaName);\n\n      if (remainingAreas.length === 0) {\n        batch.setProperty(\"grid-template-areas\")({\n          type: \"keyword\",\n          value: \"none\",\n        });\n      } else {\n        const template = generateGridTemplate(remainingAreas, columns, rows);\n        batch.setProperty(\"grid-template-areas\")({\n          type: \"unparsed\",\n          value: template,\n        });\n      }\n      batch.publish();\n    },\n    [areas, columns, rows]\n  );\n\n  // Clean up areas that are out of bounds when grid dimensions change\n  useEffect(() => {\n    // Only run if there are areas and dimensions are valid\n    if (areas.length === 0 || columns === 0 || rows === 0) {\n      return;\n    }\n\n    const validAreas = filterAreasWithinBounds(areas, columns, rows);\n\n    // If some areas are invalid, update the grid-template-areas\n    if (validAreas.length < areas.length) {\n      const batch = createBatchUpdate();\n\n      if (validAreas.length === 0) {\n        batch.setProperty(\"grid-template-areas\")({\n          type: \"keyword\",\n          value: \"none\",\n        });\n      } else {\n        const template = generateGridTemplate(validAreas, columns, rows);\n        batch.setProperty(\"grid-template-areas\")({\n          type: \"unparsed\",\n          value: template,\n        });\n      }\n\n      batch.publish();\n    }\n  }, [columns, rows, areas]);\n\n  const addArea = useCallback(() => {\n    const { area: newArea, needsNewRow } = findNonOverlappingPosition(\n      areas,\n      columns,\n      rows\n    );\n\n    const batch = createBatchUpdate();\n\n    // Calculate actual dimensions after potential row addition\n    let actualRows = rows;\n\n    // If we need a new row, add it to the grid\n    if (needsNewRow) {\n      const currentRowTracks = parseGridTemplateTrackList(rowsValue);\n      const lastRowSize =\n        currentRowTracks[currentRowTracks.length - 1]?.value || \"1fr\";\n      const updatedRows = [\n        ...currentRowTracks.map((t) => t.value),\n        lastRowSize,\n      ].join(\" \");\n      batch.setProperty(\"grid-template-rows\")({\n        type: \"unparsed\",\n        value: updatedRows,\n      });\n      actualRows = rows + 1; // Use the new row count\n    }\n\n    // Add the new area to the grid\n    const updatedAreas = [...areas, newArea];\n    const template = generateGridTemplate(\n      updatedAreas,\n      columns,\n      actualRows // Use actualRows instead of stale rows\n    );\n    batch.setProperty(\"grid-template-areas\")({\n      type: \"unparsed\",\n      value: template,\n    });\n    batch.publish();\n  }, [areas, columns, rows, rowsValue]);\n\n  return (\n    <CollapsibleSectionRoot\n      label={`Areas (${areas.length})`}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      fullWidth\n      trigger={\n        <Flex\n          align=\"center\"\n          justify=\"between\"\n          css={{ padding: theme.spacing[5] }}\n        >\n          <Text variant=\"labels\" color=\"subtle\">\n            Areas ({areas.length})\n          </Text>\n          <IconButton\n            onClick={(e) => {\n              e.stopPropagation();\n              addArea();\n            }}\n          >\n            <PlusIcon />\n          </IconButton>\n        </Flex>\n      }\n    >\n      <CssValueListArrowFocus>\n        <Flex direction=\"column\">\n          {areas.length === 0 && (\n            <Text\n              color=\"subtle\"\n              align=\"center\"\n              css={{ padding: theme.panel.padding }}\n            >\n              No Areas\n            </Text>\n          )}\n          {areas.map((area, index) => (\n            <FloatingPanel\n              key={area.name}\n              placement=\"left-start\"\n              title=\"Edit area\"\n              content={\n                <AreaEditor\n                  area={area}\n                  editingIndex={index}\n                  gridColumns={columns}\n                  gridRows={rows}\n                  existingAreas={areas}\n                  onSave={saveArea}\n                  onClose={() => setEditingAreaIndex(undefined)}\n                />\n              }\n              open={editingAreaIndex === index}\n              onOpenChange={(open) => {\n                if (open) {\n                  setEditingAreaIndex(index);\n                } else {\n                  setEditingAreaIndex(undefined);\n                }\n              }}\n            >\n              <CssValueListItem\n                id={String(index)}\n                index={index}\n                onMouseEnter={() => setHoveredAreaIndex(index)}\n                onMouseLeave={() => setHoveredAreaIndex(undefined)}\n                label={\n                  <Label truncate>\n                    {area.name} ({area.columnStart}-{area.columnEnd - 1} /{\" \"}\n                    {area.rowStart}-{area.rowEnd - 1})\n                  </Label>\n                }\n                buttons={\n                  <SmallIconButton\n                    variant=\"destructive\"\n                    tabIndex={-1}\n                    icon={<MinusIcon />}\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      removeArea(area.name);\n                    }}\n                  />\n                }\n              />\n            </FloatingPanel>\n          ))}\n        </Flex>\n      </CssValueListArrowFocus>\n    </CollapsibleSectionRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-areas.utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  parseGridAreas,\n  getGridDimensions,\n  __testing__,\n  type AreaInfo,\n} from \"./grid-areas\";\n\nconst {\n  generateGridTemplate,\n  checkOverlap,\n  generateUniqueAreaName,\n  findNonOverlappingPosition,\n  isAreaWithinBounds,\n  filterAreasWithinBounds,\n} = __testing__;\n\ndescribe(\"parseGridAreas\", () => {\n  test(\"parses empty or none value\", () => {\n    expect(parseGridAreas(\"\")).toEqual([]);\n    expect(parseGridAreas(\"none\")).toEqual([]);\n  });\n\n  test(\"parses single area\", () => {\n    const result = parseGridAreas('\"header\"');\n    expect(result).toEqual([\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ]);\n  });\n\n  test(\"parses area spanning multiple columns\", () => {\n    const result = parseGridAreas('\"header header header\"');\n    expect(result).toEqual([\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 4,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ]);\n  });\n\n  test(\"parses multiple areas in one row\", () => {\n    const result = parseGridAreas('\"sidebar main\"');\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"parses area spanning multiple rows\", () => {\n    const result = parseGridAreas('\"sidebar main\" \"sidebar footer\"');\n    expect(result).toHaveLength(3);\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 3,\n    });\n  });\n\n  test(\"parses complex layout with multiple rows and columns\", () => {\n    const result = parseGridAreas(\n      '\"header header\" \"sidebar main\" \"footer footer\"'\n    );\n    expect(result).toHaveLength(4);\n    expect(result).toContainEqual({\n      name: \"header\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"footer\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 3,\n      rowEnd: 4,\n    });\n  });\n\n  test(\"ignores dots (empty cells)\", () => {\n    const result = parseGridAreas('\"header .\" \". main\"');\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      name: \"header\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n  });\n});\n\ndescribe(\"generateGridTemplate\", () => {\n  test(\"generates template for single area\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe('\"header .\" \". .\"');\n  });\n\n  test(\"generates template for area spanning columns\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe('\"header header\" \". .\"');\n  });\n\n  test(\"generates template for complex layout\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n      { name: \"sidebar\", columnStart: 1, columnEnd: 2, rowStart: 2, rowEnd: 3 },\n      { name: \"main\", columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe(\n      '\"header header\" \"sidebar main\"'\n    );\n  });\n\n  test(\"fills empty cells with dots\", () => {\n    const areas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n      { name: \"footer\", columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 },\n    ];\n    expect(generateGridTemplate(areas, 2, 2)).toBe('\"header .\" \". footer\"');\n  });\n\n  test(\"handles area out of bounds gracefully\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 10,\n        rowStart: 1,\n        rowEnd: 10,\n      },\n    ];\n    // Should only fill within the grid bounds\n    expect(generateGridTemplate(areas, 2, 2)).toBe(\n      '\"header header\" \"header header\"'\n    );\n  });\n});\n\ndescribe(\"getGridDimensions\", () => {\n  test(\"parses column and row counts\", () => {\n    expect(getGridDimensions(\"1fr 2fr\", \"100px 200px\")).toEqual({\n      columns: 2,\n      rows: 2,\n    });\n  });\n\n  test(\"handles complex track definitions\", () => {\n    expect(\n      getGridDimensions(\"repeat(3, 1fr)\", \"minmax(100px, 1fr) auto 200px\")\n    ).toEqual({\n      columns: 3, // repeat(3, 1fr) expands to 3 tracks\n      rows: 3, // minmax(100px, 1fr), auto, 200px = 3 tracks\n    });\n  });\n\n  test(\"defaults to 2 when none\", () => {\n    expect(getGridDimensions(\"none\", \"none\")).toEqual({\n      columns: 2,\n      rows: 2,\n    });\n  });\n\n  test(\"defaults to 2 when empty\", () => {\n    expect(getGridDimensions(\"\", \"\")).toEqual({\n      columns: 2,\n      rows: 2,\n    });\n  });\n});\n\ndescribe(\"checkOverlap\", () => {\n  test(\"detects overlapping areas\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 3,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 4,\n      rowStart: 2,\n      rowEnd: 4,\n    };\n    expect(checkOverlap(area1, area2)).toBe(true);\n  });\n\n  test(\"detects non-overlapping areas horizontally\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(checkOverlap(area1, area2)).toBe(false);\n  });\n\n  test(\"detects non-overlapping areas vertically\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    expect(checkOverlap(area1, area2)).toBe(false);\n  });\n\n  test(\"detects complete containment as overlap\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 5,\n      rowStart: 1,\n      rowEnd: 5,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 4,\n      rowStart: 2,\n      rowEnd: 4,\n    };\n    expect(checkOverlap(area1, area2)).toBe(true);\n  });\n\n  test(\"detects edge adjacency as non-overlapping\", () => {\n    const area1: AreaInfo = {\n      name: \"a\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    const area2: AreaInfo = {\n      name: \"b\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    expect(checkOverlap(area1, area2)).toBe(false);\n  });\n});\n\ndescribe(\"generateUniqueAreaName\", () => {\n  test(\"returns 'Area' when no existing names\", () => {\n    expect(generateUniqueAreaName([])).toBe(\"Area\");\n  });\n\n  test(\"returns 'Area' when it doesn't exist\", () => {\n    expect(generateUniqueAreaName([\"Header\", \"Footer\"])).toBe(\"Area\");\n  });\n\n  test(\"returns 'Area-1' when 'Area' exists\", () => {\n    expect(generateUniqueAreaName([\"Area\"])).toBe(\"Area-1\");\n  });\n\n  test(\"returns next available number\", () => {\n    expect(generateUniqueAreaName([\"Area\", \"Area-1\", \"Area-2\"])).toBe(\"Area-3\");\n  });\n\n  test(\"handles gaps in numbering\", () => {\n    expect(generateUniqueAreaName([\"Area\", \"Area-2\", \"Area-5\"])).toBe(\"Area-1\");\n  });\n});\n\ndescribe(\"findNonOverlappingPosition\", () => {\n  test(\"finds first cell when grid is empty\", () => {\n    const result = findNonOverlappingPosition([], 2, 2);\n    expect(result.needsNewRow).toBe(false);\n    expect(result.area).toMatchObject({\n      name: \"Area\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"finds next available cell\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"Area\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.needsNewRow).toBe(false);\n    expect(result.area).toMatchObject({\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"generates unique name for new area\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"Area\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.area.name).toBe(\"Area-1\");\n  });\n\n  test(\"adds new row when grid is full\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"Area\", columnStart: 1, columnEnd: 2, rowStart: 1, rowEnd: 2 },\n      { name: \"Area-1\", columnStart: 2, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n      { name: \"Area-2\", columnStart: 1, columnEnd: 2, rowStart: 2, rowEnd: 3 },\n      { name: \"Area-3\", columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.needsNewRow).toBe(true);\n    expect(result.area).toMatchObject({\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 3,\n      rowEnd: 4,\n    });\n  });\n\n  test(\"finds cell when area spans multiple cells\", () => {\n    const existingAreas: AreaInfo[] = [\n      { name: \"header\", columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n    ];\n    const result = findNonOverlappingPosition(existingAreas, 2, 2);\n    expect(result.needsNewRow).toBe(false);\n    expect(result.area).toMatchObject({\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n  });\n});\n\ndescribe(\"isAreaWithinBounds\", () => {\n  test(\"validates area within bounds\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(true);\n  });\n\n  test(\"rejects area with invalid column start\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 0,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with column start >= column end\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 2,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with column end beyond grid\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 4,\n      rowStart: 1,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with invalid row start\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 0,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with row start >= row end\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 2,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"rejects area with row end beyond grid\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 4,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(false);\n  });\n\n  test(\"allows area at grid boundary\", () => {\n    const area: AreaInfo = {\n      name: \"test\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    };\n    expect(isAreaWithinBounds(area, 2, 2)).toBe(true);\n  });\n});\n\ndescribe(\"filterAreasWithinBounds\", () => {\n  test(\"returns all areas when all are valid\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"area1\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"area2\",\n        columnStart: 2,\n        columnEnd: 3,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 3, 2);\n    expect(result).toEqual(areas);\n    expect(result).toHaveLength(2);\n  });\n\n  test(\"filters out single area that exceeds column bounds\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"valid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid\",\n        columnStart: 1,\n        columnEnd: 4,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe(\"valid\");\n  });\n\n  test(\"filters out single area that exceeds row bounds\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"valid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 4,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe(\"valid\");\n  });\n\n  test(\"filters out multiple invalid areas\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"valid\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid1\",\n        columnStart: 1,\n        columnEnd: 5,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid2\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 5,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toHaveLength(1);\n    expect(result[0].name).toBe(\"valid\");\n  });\n\n  test(\"returns empty array when all areas are invalid\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"invalid1\",\n        columnStart: 1,\n        columnEnd: 5,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid2\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 5,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 2, 2);\n    expect(result).toEqual([]);\n  });\n\n  test(\"returns empty array for empty input\", () => {\n    const result = filterAreasWithinBounds([], 3, 3);\n    expect(result).toEqual([]);\n  });\n\n  test(\"preserves order of valid areas\", () => {\n    const areas: AreaInfo[] = [\n      {\n        name: \"first\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"invalid\",\n        columnStart: 1,\n        columnEnd: 5,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n      {\n        name: \"second\",\n        columnStart: 2,\n        columnEnd: 3,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ];\n    const result = filterAreasWithinBounds(areas, 3, 2);\n    expect(result).toHaveLength(2);\n    expect(result[0].name).toBe(\"first\");\n    expect(result[1].name).toBe(\"second\");\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-generator.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { WebstudioData } from \"@webstudio-is/sdk\";\nimport { __testing__ } from \"./grid-generator\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\n\nconst { gridPresets, computeFillGridData, applyFillGridItems } = __testing__;\n\nconst makeIdGenerator = () => {\n  let counter = 0;\n  return () => `id-${++counter}`;\n};\n\nconst createEmptyData = (parentId: string): WebstudioData => ({\n  pages: createDefaultPages({ rootInstanceId: parentId }),\n  assets: new Map(),\n  dataSources: new Map(),\n  resources: new Map(),\n  props: new Map(),\n  breakpoints: new Map(),\n  instances: new Map([\n    [\n      parentId,\n      {\n        type: \"instance\" as const,\n        id: parentId,\n        component: \"Box\",\n        children: [] as Array<{ type: \"id\"; value: string }>,\n      },\n    ],\n  ]),\n  styleSources: new Map(),\n  styleSourceSelections: new Map(),\n  styles: new Map(),\n});\n\ndescribe(\"gridPresets\", () => {\n  test(\"each preset has required fields\", () => {\n    for (const preset of gridPresets) {\n      expect(preset.label).toBeTruthy();\n      expect(preset.columns).toBeTruthy();\n      expect(preset.rows).toBeTruthy();\n      expect(preset.previewColumns).toBeTruthy();\n      expect(preset.previewRows).toBeTruthy();\n    }\n  });\n\n  test(\"no preset duplicates what the NxM picker does\", () => {\n    const equalFrPattern = /^(1fr\\s*)+$/;\n    for (const preset of gridPresets) {\n      const trivialCols = equalFrPattern.test(preset.columns);\n      const trivialRows = equalFrPattern.test(preset.rows);\n      // At least one axis must go beyond equal 1fr tracks\n      expect(trivialCols && trivialRows).toBe(false);\n    }\n  });\n\n  test(\"fluid sidebar uses fit-content\", () => {\n    const sidebar = gridPresets.find((p) => p.label === \"Fluid sidebar\");\n    expect(sidebar).toBeDefined();\n    expect(sidebar?.columns).toContain(\"fit-content\");\n  });\n\n  test(\"page stack has auto-sized header and footer rows\", () => {\n    const stack = gridPresets.find((p) => p.label === \"Page stack\");\n    expect(stack).toBeDefined();\n    expect(stack?.rows).toBe(\"auto 1fr auto\");\n  });\n\n  test(\"holy grail preset has named areas\", () => {\n    const holyGrail = gridPresets.find((p) => p.label === \"Holy grail\");\n    expect(holyGrail).toBeDefined();\n    expect(holyGrail?.areas).toBeDefined();\n    expect(holyGrail?.areas).toContain(\"header\");\n    expect(holyGrail?.areas).toContain(\"sidebar\");\n    expect(holyGrail?.areas).toContain(\"main\");\n    expect(holyGrail?.areas).toContain(\"footer\");\n  });\n\n  test(\"responsive cards uses auto-fit with 250px minimum\", () => {\n    const cards = gridPresets.find((p) => p.label === \"Responsive cards\");\n    expect(cards).toBeDefined();\n    expect(cards?.columns).toContain(\"auto-fit\");\n    expect(cards?.columns).toContain(\"250px\");\n  });\n\n  test(\"feature section uses auto-fit with 350px minimum\", () => {\n    const feature = gridPresets.find((p) => p.label === \"Feature section\");\n    expect(feature).toBeDefined();\n    expect(feature?.columns).toContain(\"auto-fit\");\n    expect(feature?.columns).toContain(\"350px\");\n  });\n\n  test(\"footer columns uses auto-fit with 150px minimum\", () => {\n    const footer = gridPresets.find((p) => p.label === \"Footer columns\");\n    expect(footer).toBeDefined();\n    expect(footer?.columns).toContain(\"auto-fit\");\n    expect(footer?.columns).toContain(\"150px\");\n  });\n\n  test(\"all auto-fit presets have distinct minmax thresholds\", () => {\n    const autoFitPresets = gridPresets.filter((p) =>\n      p.columns.includes(\"auto-fit\")\n    );\n    const thresholds = autoFitPresets.map((p) => {\n      const match = p.columns.match(/minmax\\((\\d+)px/);\n      return match?.[1];\n    });\n    expect(new Set(thresholds).size).toBe(thresholds.length);\n  });\n\n  test(\"preview thumbnails visually distinguish each preset\", () => {\n    const previews = gridPresets.map(\n      (p) => `${p.previewColumns}|${p.previewRows}`\n    );\n    expect(new Set(previews).size).toBe(previews.length);\n  });\n});\n\ndescribe(\"computeFillGridData\", () => {\n  test(\"returns items for each empty cell\", () => {\n    const items = computeFillGridData({\n      totalCells: 6,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    expect(items.length).toBe(6);\n  });\n\n  test(\"subtracts existing children\", () => {\n    const items = computeFillGridData({\n      totalCells: 6,\n      existingChildCount: 4,\n      generateId: makeIdGenerator(),\n    });\n    expect(items.length).toBe(2);\n  });\n\n  test(\"returns empty array when grid is already full\", () => {\n    const items = computeFillGridData({\n      totalCells: 4,\n      existingChildCount: 4,\n      generateId: makeIdGenerator(),\n    });\n    expect(items.length).toBe(0);\n  });\n\n  test(\"returns empty array when grid has more children than cells\", () => {\n    const items = computeFillGridData({\n      totalCells: 4,\n      existingChildCount: 7,\n      generateId: makeIdGenerator(),\n    });\n    expect(items.length).toBe(0);\n  });\n\n  test(\"each item has unique instanceId and styleSourceId\", () => {\n    const items = computeFillGridData({\n      totalCells: 9,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    const instanceIds = items.map((item) => item.instanceId);\n    const styleSourceIds = items.map((item) => item.styleSourceId);\n    const allIds = [...instanceIds, ...styleSourceIds];\n    expect(new Set(allIds).size).toBe(allIds.length);\n  });\n\n  test(\"single cell grid with no children returns one item\", () => {\n    const items = computeFillGridData({\n      totalCells: 1,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    expect(items.length).toBe(1);\n  });\n});\n\ndescribe(\"applyFillGridItems\", () => {\n  test(\"creates Box instances in the data\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const items = computeFillGridData({\n      totalCells: 3,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"grid-1\");\n\n    expect(data.instances.size).toBe(4); // parent + 3 children\n    for (const item of items) {\n      const instance = data.instances.get(item.instanceId);\n      expect(instance).toBeDefined();\n      expect(instance?.component).toBe(\"Box\");\n      expect(instance?.children).toEqual([]);\n    }\n  });\n\n  test(\"appends children to parent instance\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const items = computeFillGridData({\n      totalCells: 2,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"grid-1\");\n\n    const parent = data.instances.get(\"grid-1\");\n    expect(parent?.children.length).toBe(2);\n    expect(parent?.children[0]).toEqual({\n      type: \"id\",\n      value: items[0].instanceId,\n    });\n    expect(parent?.children[1]).toEqual({\n      type: \"id\",\n      value: items[1].instanceId,\n    });\n  });\n\n  test(\"preserves existing children when appending\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const parent = data.instances.get(\"grid-1\")!;\n    parent.children.push({ type: \"id\", value: \"existing-child\" });\n\n    const items = computeFillGridData({\n      totalCells: 3,\n      existingChildCount: 1,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"grid-1\");\n\n    expect(parent.children.length).toBe(3);\n    expect(parent.children[0]).toEqual({\n      type: \"id\",\n      value: \"existing-child\",\n    });\n  });\n\n  test(\"creates local style sources for each item\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const items = computeFillGridData({\n      totalCells: 2,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"grid-1\");\n\n    expect(data.styleSources.size).toBe(2);\n    for (const item of items) {\n      const source = data.styleSources.get(item.styleSourceId);\n      expect(source).toEqual({ type: \"local\", id: item.styleSourceId });\n    }\n  });\n\n  test(\"links style sources to instances via selections\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const items = computeFillGridData({\n      totalCells: 2,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"grid-1\");\n\n    expect(data.styleSourceSelections.size).toBe(2);\n    for (const item of items) {\n      const selection = data.styleSourceSelections.get(item.instanceId);\n      expect(selection).toEqual({\n        instanceId: item.instanceId,\n        values: [item.styleSourceId],\n      });\n    }\n  });\n\n  test(\"sets display flex and flex-direction column on each item\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const items = computeFillGridData({\n      totalCells: 2,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"grid-1\");\n\n    expect(data.styles.size).toBe(4);\n    const styleValues = Array.from(data.styles.values()) as Array<{\n      breakpointId: string;\n      property: string;\n      value: { type: string; value: string };\n    }>;\n    for (const style of styleValues) {\n      expect(style.breakpointId).toBe(\"bp-1\");\n    }\n    const displayStyles = styleValues.filter((s) => s.property === \"display\");\n    const directionStyles = styleValues.filter(\n      (s) => s.property === \"flexDirection\"\n    );\n    expect(displayStyles).toHaveLength(2);\n    expect(directionStyles).toHaveLength(2);\n    for (const style of displayStyles) {\n      expect(style.value).toEqual({ type: \"keyword\", value: \"flex\" });\n    }\n    for (const style of directionStyles) {\n      expect(style.value).toEqual({ type: \"keyword\", value: \"column\" });\n    }\n  });\n\n  test(\"does nothing when parent instance is missing\", () => {\n    const data = createEmptyData(\"grid-1\");\n    const items = computeFillGridData({\n      totalCells: 2,\n      existingChildCount: 0,\n      generateId: makeIdGenerator(),\n    });\n    applyFillGridItems(data, items, \"bp-1\", \"nonexistent\");\n\n    // No new instances or styles should be created\n    expect(data.instances.size).toBe(1); // only original parent\n    expect(data.styleSources.size).toBe(0);\n    expect(data.styles.size).toBe(0);\n  });\n\n  test(\"handles empty items array\", () => {\n    const data = createEmptyData(\"grid-1\");\n    applyFillGridItems(data, [], \"bp-1\", \"grid-1\");\n\n    const parent = data.instances.get(\"grid-1\");\n    expect(parent?.children.length).toBe(0);\n    expect(data.styleSources.size).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-generator.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { nanoid } from \"nanoid\";\nimport {\n  Box,\n  Button,\n  Flex,\n  Grid,\n  theme,\n  Text,\n  FloatingPanel,\n  Tooltip,\n  css,\n  Separator,\n} from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  parseGridTemplateTrackList,\n  parseGridAreas,\n  getGridAxisMode,\n  getGridAxisLabel,\n  isImplicitGridMode,\n  type GridAxisMode,\n} from \"@webstudio-is/css-data\";\nimport { useComputedStyleDecl } from \"../../../shared/model\";\nimport { createBatchUpdate } from \"../../../shared/use-style-data\";\nimport {\n  $breakpoints,\n  $gridCellData,\n  $instances,\n  $selectedInstanceSelector,\n} from \"~/shared/nano-states\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { DEFAULT_GRID_TRACK_COUNT } from \"./constants\";\n\n/**\n * Parse track count from a computed CSS value.\n * Returns DEFAULT_GRID_TRACK_COUNT for empty/none values.\n */\nconst parseTrackCount = (value: string): number => {\n  if (!value || value === \"none\") {\n    return DEFAULT_GRID_TRACK_COUNT;\n  }\n  const tracks = parseGridTemplateTrackList(value);\n  return tracks.length || DEFAULT_GRID_TRACK_COUNT;\n};\n\n/**\n * Calculate the actual track count for an axis.\n * Uses DOM-probed count for implicit modes, parsed count for explicit.\n */\nconst getAxisTrackCount = (\n  mode: GridAxisMode,\n  computedValue: string,\n  domCount: number | undefined\n): number => {\n  const parsedCount = parseTrackCount(computedValue);\n  if (isImplicitGridMode(mode)) {\n    return domCount ?? parsedCount;\n  }\n  return parsedCount;\n};\n\nconst selectorCellStyle = css({\n  width: 16,\n  height: 16,\n  borderRadius: theme.borderRadius[3],\n  backgroundColor: theme.colors.backgroundControls,\n  border: `1px solid ${theme.colors.borderMain}`,\n  transition: \"all 0.1s ease\",\n  \"&:hover\": {\n    borderColor: theme.colors.borderFocus,\n  },\n  variants: {\n    highlighted: {\n      true: {\n        backgroundColor: theme.colors.backgroundHover,\n        borderColor: theme.colors.borderFocus,\n      },\n    },\n  },\n});\n\ntype GridGeneratorSelectorProps = {\n  onSelect: (columns: number, rows: number) => void;\n  initialColumns: number;\n  initialRows: number;\n};\n\nconst GridGeneratorSelector = ({\n  onSelect,\n  initialColumns,\n  initialRows,\n}: GridGeneratorSelectorProps) => {\n  const [hoveredCell, setHoveredCell] = useState<{\n    col: number;\n    row: number;\n  } | null>(null);\n\n  const maxCols = 12;\n  const maxRows = 8;\n\n  const cells = useMemo(() => {\n    return Array.from({ length: maxRows * maxCols }).map((_, i) => {\n      const col = i % maxCols;\n      const row = Math.floor(i / maxCols);\n      const isSelected =\n        col < initialColumns && row < initialRows && !hoveredCell;\n      const isHighlighted =\n        hoveredCell && col <= hoveredCell.col && row <= hoveredCell.row;\n\n      return (\n        <Tooltip\n          key={i}\n          content={\n            hoveredCell && col === hoveredCell.col && row === hoveredCell.row\n              ? `${hoveredCell.col + 1}×${hoveredCell.row + 1}`\n              : \"\"\n          }\n          open={\n            hoveredCell\n              ? col === hoveredCell.col && row === hoveredCell.row\n              : false\n          }\n        >\n          <button\n            onMouseEnter={() => setHoveredCell({ col, row })}\n            onClick={() => onSelect(col + 1, row + 1)}\n            className={selectorCellStyle({\n              highlighted: isHighlighted || isSelected || undefined,\n            })}\n          />\n        </Tooltip>\n      );\n    });\n  }, [hoveredCell, onSelect, maxCols, maxRows, initialColumns, initialRows]);\n\n  return (\n    <Grid\n      onMouseLeave={() => setHoveredCell(null)}\n      css={{\n        gridTemplateColumns: `repeat(${maxCols}, 1fr)`,\n        gridTemplateRows: `repeat(${maxRows}, 1fr)`,\n        gap: 2,\n      }}\n    >\n      {cells}\n    </Grid>\n  );\n};\n\ntype GridPreset = {\n  label: string;\n  columns: string;\n  rows: string;\n  areas?: string;\n  // Mini-preview dimensions for the button thumbnail\n  previewColumns: string;\n  previewRows: string;\n};\n\nconst gridPresets: GridPreset[] = [\n  {\n    label: \"Fluid sidebar\",\n    columns: \"fit-content(300px) 1fr\",\n    rows: \"1fr\",\n    previewColumns: \"1fr 3fr\",\n    previewRows: \"1fr\",\n  },\n  {\n    label: \"Page stack\",\n    columns: \"1fr\",\n    rows: \"auto 1fr auto\",\n    previewColumns: \"1fr\",\n    previewRows: \"1fr 4fr 1fr\",\n  },\n  {\n    label: \"Holy grail\",\n    columns: \"1fr 3fr 1fr\",\n    rows: \"auto 1fr auto\",\n    areas: `\"header header header\" \"sidebar main aside\" \"footer footer footer\"`,\n    previewColumns: \"1fr 3fr 1fr\",\n    previewRows: \"1fr 3fr 1fr\",\n  },\n  {\n    label: \"Responsive cards\",\n    columns: \"repeat(auto-fit, minmax(250px, 1fr))\",\n    rows: \"auto\",\n    previewColumns: \"1fr 1fr 1fr\",\n    previewRows: \"1fr 1fr\",\n  },\n  {\n    label: \"Feature section\",\n    columns: \"repeat(auto-fit, minmax(350px, 1fr))\",\n    rows: \"auto\",\n    previewColumns: \"1fr 1fr\",\n    previewRows: \"1fr\",\n  },\n  {\n    label: \"Footer columns\",\n    columns: \"repeat(auto-fit, minmax(150px, 1fr))\",\n    rows: \"auto\",\n    previewColumns: \"1fr 1fr 1fr 1fr\",\n    previewRows: \"1fr\",\n  },\n];\n\nconst presetButtonStyle = css({\n  all: \"unset\",\n  display: \"flex\",\n  flexDirection: \"column\",\n  alignItems: \"center\",\n  gap: theme.spacing[2],\n  cursor: \"pointer\",\n  borderRadius: theme.borderRadius[3],\n  padding: theme.spacing[3],\n  boxSizing: \"border-box\",\n  \"&:hover\": {\n    backgroundColor: theme.colors.backgroundHover,\n  },\n  \"&:focus-visible\": {\n    outline: `2px solid ${theme.colors.borderFocus}`,\n  },\n});\n\nconst presetPreviewStyle = css({\n  width: \"100%\",\n  aspectRatio: \"3 / 2\",\n  borderRadius: theme.borderRadius[2],\n  border: `1px solid ${theme.colors.borderMain}`,\n  overflow: \"hidden\",\n});\n\ntype GridPresetsPickerProps = {\n  onSelect: (preset: GridPreset) => void;\n};\n\nconst GridPresetsPicker = ({ onSelect }: GridPresetsPickerProps) => {\n  return (\n    <Grid\n      css={{\n        gridTemplateColumns: \"repeat(3, 1fr)\",\n        gap: theme.spacing[3],\n      }}\n    >\n      {gridPresets.map((preset) => (\n        <Tooltip key={preset.label} content={preset.label}>\n          <button\n            className={presetButtonStyle()}\n            onClick={() => onSelect(preset)}\n          >\n            <Grid\n              className={presetPreviewStyle()}\n              css={{\n                gridTemplateColumns: preset.previewColumns,\n                gridTemplateRows: preset.previewRows,\n                gap: 1,\n                padding: 2,\n              }}\n            >\n              {Array.from({\n                length:\n                  preset.previewColumns.split(\" \").length *\n                  preset.previewRows.split(\" \").length,\n              }).map((_, i) => (\n                <Box\n                  key={i}\n                  css={{\n                    backgroundColor: theme.colors.backgroundControls,\n                    borderRadius: theme.borderRadius[1],\n                    border: `1px solid ${theme.colors.borderMain}`,\n                  }}\n                />\n              ))}\n            </Grid>\n            <Text variant=\"small\" align=\"center\">\n              {preset.label}\n            </Text>\n          </button>\n        </Tooltip>\n      ))}\n    </Grid>\n  );\n};\n\ntype FillGridInput = {\n  totalCells: number;\n  existingChildCount: number;\n  generateId: () => string;\n};\n\ntype FillGridItem = {\n  instanceId: string;\n  styleSourceId: string;\n};\n\nconst computeFillGridData = ({\n  totalCells,\n  existingChildCount,\n  generateId,\n}: FillGridInput): FillGridItem[] => {\n  const cellsToAdd = totalCells - existingChildCount;\n  if (cellsToAdd <= 0) {\n    return [];\n  }\n  return Array.from({ length: cellsToAdd }, () => ({\n    instanceId: generateId(),\n    styleSourceId: generateId(),\n  }));\n};\n\nconst applyFillGridItems = (\n  data: Parameters<Parameters<typeof updateWebstudioData>[0]>[0],\n  items: FillGridItem[],\n  breakpointId: string,\n  parentInstanceId: string\n) => {\n  const parentInstance = data.instances.get(parentInstanceId);\n  if (parentInstance === undefined) {\n    return;\n  }\n  for (const { instanceId, styleSourceId } of items) {\n    data.instances.set(instanceId, {\n      type: \"instance\",\n      id: instanceId,\n      component: \"Box\",\n      children: [],\n    });\n    data.styleSources.set(styleSourceId, {\n      type: \"local\",\n      id: styleSourceId,\n    });\n    data.styleSourceSelections.set(instanceId, {\n      instanceId,\n      values: [styleSourceId],\n    });\n    const displayStyleDecl: StyleDecl = {\n      breakpointId,\n      styleSourceId,\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    };\n    data.styles.set(getStyleDeclKey(displayStyleDecl), displayStyleDecl);\n    const directionStyleDecl: StyleDecl = {\n      breakpointId,\n      styleSourceId,\n      property: \"flexDirection\",\n      value: { type: \"keyword\", value: \"column\" },\n    };\n    data.styles.set(getStyleDeclKey(directionStyleDecl), directionStyleDecl);\n    parentInstance.children.push({ type: \"id\", value: instanceId });\n  }\n};\n\nconst gridGeneratorButtonStyle = css({\n  all: \"unset\",\n  position: \"relative\",\n  display: \"grid\",\n  width: \"100%\",\n  height: 60,\n  outline: `1px solid ${theme.colors.borderMain}`,\n  borderRadius: theme.borderRadius[3],\n  overflow: \"hidden\",\n  cursor: \"pointer\",\n  \"&:focus-within, &[data-state=open]\": {\n    outline: `1px solid ${theme.colors.borderLocalFlexUi}`,\n  },\n  \"&:hover\": {\n    backgroundColor: theme.colors.backgroundHover,\n  },\n});\n\ntype GridGeneratorProps = {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n};\n\nexport const GridGenerator = ({ open, onOpenChange }: GridGeneratorProps) => {\n  const gridTemplateColumns = useComputedStyleDecl(\"grid-template-columns\");\n  const gridTemplateRows = useComputedStyleDecl(\"grid-template-rows\");\n  const gridTemplateAreas = useComputedStyleDecl(\"grid-template-areas\");\n  const gridCellData = useStore($gridCellData);\n\n  // Analyze both axes using pure functions\n  const columnsValueCascaded = toValue(gridTemplateColumns.cascadedValue);\n  const rowsValueCascaded = toValue(gridTemplateRows.cascadedValue);\n  const columnsMode = getGridAxisMode(columnsValueCascaded);\n  const rowsMode = getGridAxisMode(rowsValueCascaded);\n\n  // Calculate track counts\n  // For unknown modes (subgrid, masonry, line-names), we use defaults\n  const columnsValueComputed = toValue(gridTemplateColumns.computedValue);\n  const rowsValueComputed = toValue(gridTemplateRows.computedValue);\n  const columnCount = getAxisTrackCount(\n    columnsMode,\n    columnsValueComputed,\n    gridCellData?.columnCount\n  );\n  const rowCount = getAxisTrackCount(\n    rowsMode,\n    rowsValueComputed,\n    gridCellData?.rowCount\n  );\n\n  const displayColumnCount = Math.min(columnCount, 8);\n  const displayRowCount = Math.min(rowCount, 8);\n\n  const handleChange = (columns: number, rows: number) => {\n    const batch = createBatchUpdate();\n\n    batch.setProperty(\"grid-template-columns\")({\n      type: \"unparsed\",\n      value: Array(columns).fill(\"1fr\").join(\" \"),\n    });\n\n    batch.setProperty(\"grid-template-rows\")({\n      type: \"unparsed\",\n      value: Array(rows).fill(\"1fr\").join(\" \"),\n    });\n\n    batch.publish();\n  };\n\n  const handleSelectorSelect = (columns: number, rows: number) => {\n    handleChange(columns, rows);\n  };\n\n  const handlePresetSelect = (preset: GridPreset) => {\n    const batch = createBatchUpdate();\n    batch.setProperty(\"grid-template-columns\")({\n      type: \"unparsed\",\n      value: preset.columns,\n    });\n    batch.setProperty(\"grid-template-rows\")({\n      type: \"unparsed\",\n      value: preset.rows,\n    });\n    if (preset.areas) {\n      batch.setProperty(\"grid-template-areas\")({\n        type: \"unparsed\",\n        value: preset.areas,\n      });\n    } else {\n      batch.deleteProperty(\"grid-template-areas\");\n    }\n    batch.publish();\n  };\n\n  const handleFillGrid = () => {\n    const instanceSelector = $selectedInstanceSelector.get();\n    if (instanceSelector === undefined) {\n      return;\n    }\n    const instances = $instances.get();\n    const parentInstance = instances.get(instanceSelector[0]);\n    if (parentInstance === undefined) {\n      return;\n    }\n    const existingChildCount = parentInstance.children.filter(\n      (child) => child.type === \"id\"\n    ).length;\n    const baseBreakpoint = Array.from($breakpoints.get().values()).find(\n      (bp) => bp.minWidth === undefined && bp.maxWidth === undefined\n    );\n    if (baseBreakpoint === undefined) {\n      return;\n    }\n    const items = computeFillGridData({\n      totalCells: columnCount * rowCount,\n      existingChildCount,\n      generateId: nanoid,\n    });\n    if (items.length === 0) {\n      return;\n    }\n    updateWebstudioData((data) => {\n      applyFillGridItems(data, items, baseBreakpoint.id, instanceSelector[0]);\n    });\n  };\n\n  // Build a map from \"col,row\" (0-based) to area name for the preview\n  const areasValue = toValue(gridTemplateAreas.cascadedValue);\n  const areas = useMemo(() => parseGridAreas(areasValue), [areasValue]);\n\n  const areaMap = useMemo(() => {\n    const map = new Map<string, string>();\n    for (const area of areas) {\n      for (let r = area.rowStart; r < area.rowEnd; r++) {\n        for (let c = area.columnStart; c < area.columnEnd; c++) {\n          const displayCol = c - 1;\n          const displayRow = r - 1;\n          if (displayCol < displayColumnCount && displayRow < displayRowCount) {\n            map.set(`${displayCol},${displayRow}`, area.name);\n          }\n        }\n      }\n    }\n    return map;\n  }, [areas, displayColumnCount, displayRowCount]);\n\n  // Memoize grid cells to avoid recreating on each render\n  const gridCells = useMemo(() => {\n    return Array.from({ length: displayColumnCount * displayRowCount }).map(\n      (_, i) => {\n        const col = i % displayColumnCount;\n        const row = Math.floor(i / displayColumnCount);\n        const areaName = areaMap.get(`${col},${row}`);\n        const rightNeighbor = areaMap.get(`${col + 1},${row}`);\n        const bottomNeighbor = areaMap.get(`${col},${row + 1}`);\n        // Hide border when both sides belong to the same named area\n        const mergedRight =\n          areaName !== undefined && areaName === rightNeighbor;\n        const mergedBottom =\n          areaName !== undefined && areaName === bottomNeighbor;\n\n        return (\n          <Box\n            key={i}\n            css={{\n              width: \"100%\",\n              height: \"100%\",\n              borderRight:\n                col < displayColumnCount - 1 && !mergedRight\n                  ? `1px solid ${theme.colors.borderMain}`\n                  : undefined,\n              borderBottom:\n                row < displayRowCount - 1 && !mergedBottom\n                  ? `1px solid ${theme.colors.borderMain}`\n                  : undefined,\n            }}\n          />\n        );\n      }\n    );\n  }, [displayColumnCount, displayRowCount, areaMap]);\n\n  return (\n    <FloatingPanel\n      title=\"Grid generator\"\n      placement=\"left-start\"\n      content={\n        <Flex direction=\"column\">\n          <Flex\n            direction=\"column\"\n            gap=\"3\"\n            css={{ padding: theme.panel.padding }}\n          >\n            <GridGeneratorSelector\n              onSelect={handleSelectorSelect}\n              initialColumns={columnCount}\n              initialRows={rowCount}\n            />\n          </Flex>\n          <Separator />\n          <Flex\n            direction=\"column\"\n            gap=\"3\"\n            css={{ padding: theme.panel.padding }}\n          >\n            <GridPresetsPicker onSelect={handlePresetSelect} />\n          </Flex>\n          <Separator />\n          <Flex css={{ padding: theme.panel.padding }}>\n            <Button\n              color=\"neutral\"\n              css={{ width: \"100%\" }}\n              onClick={handleFillGrid}\n            >\n              Fill grid\n            </Button>\n          </Flex>\n        </Flex>\n      }\n      open={open}\n      onOpenChange={onOpenChange}\n      closeOnInteractOutside={false}\n    >\n      {/* Visual grid preview - similar to Figma's style */}\n      <button\n        aria-label={`Grid layout: ${columnCount} columns by ${rowCount} rows`}\n        className={gridGeneratorButtonStyle()}\n        style={{\n          gridTemplateColumns: `repeat(${displayColumnCount}, 1fr)`,\n          gridTemplateRows: `repeat(${displayRowCount}, 1fr)`,\n        }}\n      >\n        {gridCells}\n        <Text\n          variant=\"mono\"\n          css={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            pointerEvents: \"none\",\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {getGridAxisLabel(columnsMode, columnCount) +\n            \"×\" +\n            getGridAxisLabel(rowsMode, rowCount)}\n        </Text>\n      </button>\n    </FloatingPanel>\n  );\n};\n\nexport const __testing__ = {\n  gridPresets,\n  computeFillGridData,\n  applyFillGridItems,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-position-inputs.tsx",
    "content": "import {\n  Flex,\n  Grid,\n  InputField,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\n\nexport type GridPosition = {\n  columnStart: number;\n  columnEnd: number;\n  rowStart: number;\n  rowEnd: number;\n};\n\nexport type GridPositionValidation = {\n  isColumnStartValid: boolean;\n  isColumnEndValid: boolean;\n  isRowStartValid: boolean;\n  isRowEndValid: boolean;\n};\n\nexport const validateGridPosition = (\n  position: GridPosition,\n  gridColumns: number,\n  gridRows: number,\n  options: { checkBounds?: boolean; inclusiveEnd?: boolean } = {}\n): GridPositionValidation => {\n  const { checkBounds = false, inclusiveEnd = false } = options;\n  // With inclusiveEnd, end is a track index (end >= start is valid).\n  // Without, end is a grid-line number (end > start is required).\n  const endOp = inclusiveEnd\n    ? (end: number, start: number) => end >= start\n    : (end: number, start: number) => end > start;\n  const maxCol = inclusiveEnd ? gridColumns : gridColumns + 1;\n  const maxRow = inclusiveEnd ? gridRows : gridRows + 1;\n  const result = {\n    isColumnStartValid:\n      position.columnStart >= 1 &&\n      endOp(position.columnEnd, position.columnStart),\n    isColumnEndValid:\n      endOp(position.columnEnd, position.columnStart) &&\n      (!checkBounds || position.columnEnd <= maxCol),\n    isRowStartValid:\n      position.rowStart >= 1 && endOp(position.rowEnd, position.rowStart),\n    isRowEndValid:\n      endOp(position.rowEnd, position.rowStart) &&\n      (!checkBounds || position.rowEnd <= maxRow),\n  };\n  return result;\n};\n\ntype GridPositionInputsProps = {\n  value: GridPosition;\n  onChange: (value: GridPosition) => void;\n  onBlur?: () => void;\n  onKeyDown?: (event: React.KeyboardEvent) => void;\n  gridColumns: number;\n  gridRows: number;\n  checkBounds?: boolean;\n  /**\n   * When true, \"end\" values are inclusive track indices (1-based)\n   * instead of exclusive grid-line numbers.\n   * Validation uses end >= start instead of end > start.\n   */\n  inclusiveEnd?: boolean;\n};\n\nconst PositionInputGroup = ({\n  startValue,\n  endValue,\n  onStartChange,\n  onEndChange,\n  onBlur,\n  onKeyDown,\n  isStartValid,\n  isEndValid,\n  minStart,\n  maxStart,\n  minEnd,\n  maxEnd,\n  label,\n}: {\n  startValue: number;\n  endValue: number;\n  onStartChange: (value: number) => void;\n  onEndChange: (value: number) => void;\n  onBlur?: () => void;\n  isStartValid: boolean;\n  isEndValid: boolean;\n  minStart: number;\n  maxStart: number;\n  minEnd: number;\n  maxEnd: number;\n  label: string;\n  onKeyDown?: (event: React.KeyboardEvent) => void;\n}) => (\n  <Flex direction=\"column\" gap=\"1\" css={{ flex: 1 }}>\n    <Grid css={{ gridTemplateColumns: \"1fr 1fr\", gap: theme.spacing[3] }}>\n      <InputField\n        type=\"number\"\n        value={String(startValue)}\n        onChange={(event) => {\n          const num = Number(event.target.value);\n          if (!isNaN(num)) {\n            onStartChange(num);\n          }\n        }}\n        onBlur={onBlur}\n        onKeyDown={onKeyDown}\n        color={isStartValid ? undefined : \"error\"}\n        min={minStart}\n        max={maxStart}\n      />\n      <InputField\n        type=\"number\"\n        value={String(endValue)}\n        onChange={(event) => {\n          const num = Number(event.target.value);\n          if (!isNaN(num)) {\n            onEndChange(num);\n          }\n        }}\n        onBlur={onBlur}\n        onKeyDown={onKeyDown}\n        color={isEndValid ? undefined : \"error\"}\n        min={minEnd}\n        max={maxEnd}\n      />\n    </Grid>\n    <Text variant=\"small\" color=\"subtle\">\n      {label}\n    </Text>\n  </Flex>\n);\n\nexport const GridPositionInputs = ({\n  value,\n  onChange,\n  onBlur,\n  onKeyDown,\n  gridColumns,\n  gridRows,\n  checkBounds = false,\n  inclusiveEnd = false,\n}: GridPositionInputsProps) => {\n  const validation = validateGridPosition(value, gridColumns, gridRows, {\n    checkBounds,\n    inclusiveEnd,\n  });\n\n  const colMaxEnd = inclusiveEnd ? gridColumns : gridColumns + 1;\n  const rowMaxEnd = inclusiveEnd ? gridRows : gridRows + 1;\n  const minEnd = inclusiveEnd ? 1 : 2;\n\n  return (\n    <Flex gap=\"2\">\n      <PositionInputGroup\n        startValue={value.columnStart}\n        endValue={value.columnEnd}\n        onStartChange={(columnStart) => onChange({ ...value, columnStart })}\n        onEndChange={(columnEnd) => onChange({ ...value, columnEnd })}\n        onBlur={onBlur}\n        onKeyDown={onKeyDown}\n        isStartValid={validation.isColumnStartValid}\n        isEndValid={validation.isColumnEndValid}\n        minStart={1}\n        maxStart={gridColumns}\n        minEnd={minEnd}\n        maxEnd={colMaxEnd}\n        label=\"Column: start/end\"\n      />\n      <PositionInputGroup\n        startValue={value.rowStart}\n        endValue={value.rowEnd}\n        onStartChange={(rowStart) => onChange({ ...value, rowStart })}\n        onEndChange={(rowEnd) => onChange({ ...value, rowEnd })}\n        onBlur={onBlur}\n        onKeyDown={onKeyDown}\n        isStartValid={validation.isRowStartValid}\n        isEndValid={validation.isRowEndValid}\n        minStart={1}\n        maxStart={gridRows}\n        minEnd={minEnd}\n        maxEnd={rowMaxEnd}\n        label=\"Row: start/end\"\n      />\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-settings.tsx",
    "content": "import { useMemo, useState, useCallback, useEffect } from \"react\";\nimport {\n  theme,\n  Flex,\n  Text,\n  FloatingPanel,\n  IconButton,\n  Label,\n  CssValueListItem,\n  CssValueListArrowFocus,\n  SmallIconButton,\n  useSortable,\n  Button,\n  Checkbox,\n  Grid,\n} from \"@webstudio-is/design-system\";\nimport { toValue, type StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  parseGridTemplateTrackList,\n  serializeGridTemplateTrackList,\n  parseMinmax,\n  serializeMinmax,\n  isEditableGridMode,\n  getGridAxisMode,\n  type GridTrack,\n} from \"@webstudio-is/css-data\";\nimport { PlusIcon, MinusIcon } from \"@webstudio-is/icons\";\nimport { useStore } from \"@nanostores/react\";\nimport { $selectedInstance } from \"~/shared/awareness\";\nimport { useComputedStyleDecl } from \"../../../shared/model\";\nimport { CssValueInputContainer } from \"../../../shared/css-value-input\";\nimport { createBatchUpdate } from \"../../../shared/use-style-data\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport { GridAreas } from \"./grid-areas\";\nimport { $gridEditingTrack } from \"~/builder/shared/nano-states\";\n\n/**\n * Compute how many auto tracks are created when children exceed\n * the defined template tracks along the flow axis.\n * e.g. a 2×2 grid with 5 children in row flow → ceil(5/2) - 2 = 1 auto row.\n */\nconst computeAutoTrackCount = (\n  childCount: number,\n  crossAxisTrackCount: number,\n  flowAxisTrackCount: number\n) =>\n  Math.max(\n    0,\n    Math.ceil(childCount / Math.max(1, crossAxisTrackCount)) -\n      flowAxisTrackCount\n  );\n\nconst trackTypeLabels = {\n  column: { singular: \"Column\", plural: \"Columns\" },\n  row: { singular: \"Row\", plural: \"Rows\" },\n} as const;\n\nconst serializeTrackList = (tracks: GridTrack[]): StyleValue => {\n  const value = serializeGridTemplateTrackList(tracks);\n  if (value === \"none\") {\n    return { type: \"keyword\", value: \"none\" };\n  }\n  return { type: \"unparsed\", value };\n};\n\ntype GridTrackProperty =\n  | \"grid-template-columns\"\n  | \"grid-template-rows\"\n  | \"grid-auto-columns\"\n  | \"grid-auto-rows\";\n\ntype TrackItemProps = {\n  property: GridTrackProperty;\n  trackType: \"column\" | \"row\";\n  track: string;\n  index: number;\n  id: string;\n  dragItemId: string | undefined;\n  isEditing: boolean;\n  isAuto?: boolean;\n  canRemove: boolean;\n  onEditingChange: (open: boolean) => void;\n  onUpdate: (index: number, newValue: string) => void;\n  onRemove: (index: number) => void;\n  onHoverStart: () => void;\n  onHoverEnd: () => void;\n};\n\nconst TrackItem = ({\n  property,\n  trackType,\n  track,\n  index,\n  id,\n  dragItemId,\n  isEditing,\n  isAuto,\n  canRemove,\n  onEditingChange,\n  onUpdate,\n  onRemove,\n  onHoverStart,\n  onHoverEnd,\n}: TrackItemProps) => {\n  const minmaxParts = parseMinmax(track);\n  const [isMinmax, setIsMinmax] = useState(minmaxParts !== undefined);\n  const [minValue, setMinValue] = useState(minmaxParts?.min ?? \"0\");\n  const [maxValue, setMaxValue] = useState(minmaxParts?.max ?? \"1fr\");\n\n  // Sync state when track prop changes (e.g., external edits, undo/redo)\n  useEffect(() => {\n    const parsed = parseMinmax(track);\n    setIsMinmax(parsed !== undefined);\n    if (parsed) {\n      setMinValue(parsed.min);\n      setMaxValue(parsed.max);\n    }\n  }, [track]);\n\n  const handleMinmaxToggle = (checked: boolean) => {\n    setIsMinmax(checked);\n    if (checked) {\n      // Convert single value to minmax with reasonable defaults\n      const newMin = \"0\";\n      const newMax = track === \"auto\" ? \"1fr\" : track;\n      setMinValue(newMin);\n      setMaxValue(newMax);\n      onUpdate(index, serializeMinmax({ min: newMin, max: newMax }));\n    } else {\n      // Convert minmax to single value (use max value)\n      onUpdate(index, maxValue);\n    }\n  };\n\n  const handleMinUpdate = (value: string) => {\n    setMinValue(value);\n    onUpdate(index, serializeMinmax({ min: value, max: maxValue }));\n  };\n\n  const handleMaxUpdate = (value: string) => {\n    setMaxValue(value);\n    onUpdate(index, serializeMinmax({ min: minValue, max: value }));\n  };\n\n  return (\n    <FloatingPanel\n      placement=\"bottom-within\"\n      title={`Edit ${trackType}`}\n      content={\n        <Flex direction=\"column\" gap=\"2\" css={{ padding: theme.panel.padding }}>\n          {isMinmax ? (\n            <Grid columns={2} gap=\"2\">\n              <Flex direction=\"column\" gap=\"1\">\n                <Label>Min</Label>\n                <CssValueInputContainer\n                  styleSource=\"local\"\n                  property={property}\n                  value={{\n                    type: \"unparsed\",\n                    value: minValue,\n                  }}\n                  onUpdate={(styleValue) => {\n                    const stringValue =\n                      styleValue.type === \"unparsed\"\n                        ? styleValue.value\n                        : toValue(styleValue);\n                    handleMinUpdate(stringValue);\n                  }}\n                  onDelete={() => {}}\n                />\n              </Flex>\n              <Flex direction=\"column\" gap=\"1\">\n                <Label>Max</Label>\n                <CssValueInputContainer\n                  styleSource=\"local\"\n                  property={property}\n                  value={{\n                    type: \"unparsed\",\n                    value: maxValue,\n                  }}\n                  onUpdate={(styleValue) => {\n                    const stringValue =\n                      styleValue.type === \"unparsed\"\n                        ? styleValue.value\n                        : toValue(styleValue);\n                    handleMaxUpdate(stringValue);\n                  }}\n                  onDelete={() => {}}\n                />\n              </Flex>\n            </Grid>\n          ) : (\n            <Flex direction=\"column\" gap=\"1\">\n              <Label>Value</Label>\n              <CssValueInputContainer\n                styleSource=\"local\"\n                property={property}\n                value={{\n                  type: \"unparsed\",\n                  value: track,\n                }}\n                onUpdate={(styleValue) => {\n                  const stringValue =\n                    styleValue.type === \"unparsed\"\n                      ? styleValue.value\n                      : toValue(styleValue);\n                  onUpdate(index, stringValue);\n                }}\n                onDelete={() => {}}\n              />\n            </Flex>\n          )}\n          <Flex align=\"center\" gap=\"2\">\n            <Checkbox\n              id={`minmax-${id}`}\n              checked={isMinmax}\n              onCheckedChange={handleMinmaxToggle}\n            />\n            <Label htmlFor={`minmax-${id}`}>Use min/max</Label>\n          </Flex>\n        </Flex>\n      }\n      open={isEditing}\n      onOpenChange={onEditingChange}\n    >\n      <CssValueListItem\n        id={id}\n        draggable={!isAuto}\n        active={dragItemId === id}\n        index={index}\n        onMouseEnter={onHoverStart}\n        onMouseLeave={onHoverEnd}\n        label={\n          <Label truncate title={track}>\n            {track}\n          </Label>\n        }\n        buttons={\n          isAuto ? undefined : (\n            <SmallIconButton\n              variant=\"destructive\"\n              tabIndex={-1}\n              disabled={canRemove === false}\n              icon={<MinusIcon />}\n              onClick={(e) => {\n                e.stopPropagation();\n                onRemove(index);\n              }}\n            />\n          )\n        }\n      />\n    </FloatingPanel>\n  );\n};\n\ntype TrackEditorProps = {\n  property: GridTrackProperty;\n  trackType: keyof typeof trackTypeLabels;\n  label?: string;\n  defaultTrackValue?: string;\n  autoTrackCount?: number;\n  disabled?: boolean;\n};\n\nconst TrackEditor = ({\n  property,\n  trackType,\n  label: labelOverride,\n  defaultTrackValue = \"1fr\",\n  autoTrackCount,\n  disabled = false,\n}: TrackEditorProps) => {\n  const { plural } = trackTypeLabels[trackType];\n  const isAuto =\n    property === \"grid-auto-columns\" || property === \"grid-auto-rows\";\n  const label = labelOverride ?? plural;\n  const displayCount = isAuto ? (autoTrackCount ?? 0) : undefined;\n  const openStateKey = isAuto ? `auto-${trackType}` : trackType;\n  const styleDecl = useComputedStyleDecl(property);\n  const value = toValue(styleDecl.cascadedValue);\n  const tracks = parseGridTemplateTrackList(value);\n  const [isOpen, setIsOpen] = useOpenState(openStateKey);\n  const [editingIndex, setEditingIndex] = useState<number | undefined>(\n    undefined\n  );\n  const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(\n    undefined\n  );\n\n  // Update grid editing track highlight when editing or hovering a track\n  // Only for template tracks since auto tracks aren't shown in the grid guides\n  const highlightIndex = editingIndex ?? hoveredIndex;\n  useEffect(() => {\n    if (isAuto) {\n      return;\n    }\n    if (highlightIndex !== undefined) {\n      $gridEditingTrack.set({ type: trackType, index: highlightIndex });\n    } else {\n      // Only clear if this track type was being edited\n      const current = $gridEditingTrack.get();\n      if (current?.type === trackType) {\n        $gridEditingTrack.set(undefined);\n      }\n    }\n    return () => {\n      const current = $gridEditingTrack.get();\n      if (current?.type === trackType) {\n        $gridEditingTrack.set(undefined);\n      }\n    };\n  }, [isAuto, highlightIndex, trackType]);\n\n  const updateTracks = useCallback(\n    (newTracks: GridTrack[]) => {\n      const batch = createBatchUpdate();\n      batch.setProperty(property)(serializeTrackList(newTracks));\n      batch.publish();\n    },\n    [property]\n  );\n\n  const addTrack = useCallback(() => {\n    const newTracks = [...tracks, { value: defaultTrackValue }];\n    updateTracks(newTracks);\n  }, [tracks, updateTracks, defaultTrackValue]);\n\n  const removeTrack = useCallback(\n    (index: number) => {\n      if (tracks.length > 1) {\n        updateTracks(tracks.filter((_, i) => i !== index));\n        if (editingIndex === index) {\n          setEditingIndex(undefined);\n        }\n      }\n    },\n    [tracks, updateTracks, editingIndex]\n  );\n\n  const updateTrack = useCallback(\n    (index: number, newValue: string) => {\n      const newTracks = [...tracks];\n      newTracks[index] = { value: newValue };\n      updateTracks(newTracks);\n    },\n    [tracks, updateTracks]\n  );\n\n  const sortableItems = useMemo(\n    () => tracks.map((_, index) => ({ id: String(index) })),\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [tracks.length]\n  );\n\n  const { dragItemId, placementIndicator, sortableRefCallback } = useSortable({\n    items: sortableItems,\n    onSort: (newIndex, oldIndex) => {\n      if (oldIndex === newIndex) {\n        return;\n      }\n      const newTracks = [...tracks];\n      const [removed] = newTracks.splice(oldIndex, 1);\n      newTracks.splice(newIndex, 0, removed);\n      updateTracks(newTracks);\n    },\n  });\n\n  return (\n    <CollapsibleSectionRoot\n      label={`${label} (${displayCount ?? tracks.length})`}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      fullWidth\n      trigger={\n        <Flex\n          align=\"center\"\n          justify=\"between\"\n          css={{ padding: theme.spacing[5] }}\n        >\n          <Text variant=\"labels\" color=\"subtle\">\n            {label} ({displayCount ?? tracks.length})\n          </Text>\n          {!isAuto && (\n            <IconButton\n              disabled={disabled}\n              onClick={(e) => {\n                e.stopPropagation();\n                addTrack();\n              }}\n            >\n              <PlusIcon />\n            </IconButton>\n          )}\n        </Flex>\n      }\n    >\n      <CssValueListArrowFocus dragItemId={dragItemId}>\n        <Flex direction=\"column\" ref={sortableRefCallback}>\n          {tracks.length === 0 && (\n            <Text\n              color=\"subtle\"\n              align=\"center\"\n              css={{ padding: theme.panel.padding }}\n            >\n              No {trackType}\n            </Text>\n          )}\n          {tracks.map((track, index) => {\n            const id = String(index);\n            return (\n              <TrackItem\n                key={id}\n                property={property}\n                trackType={trackType}\n                track={track.value}\n                index={index}\n                id={id}\n                dragItemId={dragItemId}\n                isEditing={editingIndex === index}\n                isAuto={isAuto}\n                canRemove={tracks.length > 1}\n                onEditingChange={(open) => {\n                  if (open) {\n                    setEditingIndex(index);\n                  } else {\n                    setEditingIndex(undefined);\n                  }\n                }}\n                onUpdate={updateTrack}\n                onRemove={removeTrack}\n                onHoverStart={() => setHoveredIndex(index)}\n                onHoverEnd={() => setHoveredIndex(undefined)}\n              />\n            );\n          })}\n          {placementIndicator}\n        </Flex>\n      </CssValueListArrowFocus>\n    </CollapsibleSectionRoot>\n  );\n};\n\ntype GridSettingsProps = {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n};\n\nexport const GridSettings = ({ open, onOpenChange }: GridSettingsProps) => {\n  const selectedInstance = useStore($selectedInstance);\n  const gridTemplateColumns = useComputedStyleDecl(\"grid-template-columns\");\n  const gridTemplateRows = useComputedStyleDecl(\"grid-template-rows\");\n  const gridAutoFlow = useComputedStyleDecl(\"grid-auto-flow\");\n\n  const columnsValue = toValue(gridTemplateColumns.cascadedValue);\n  const rowsValue = toValue(gridTemplateRows.cascadedValue);\n  const autoFlowValue = toValue(gridAutoFlow.cascadedValue);\n\n  const childCount =\n    selectedInstance?.children.filter((child) => child.type === \"id\").length ??\n    0;\n  const templateColumnCount = parseGridTemplateTrackList(columnsValue).length;\n  const templateRowCount = parseGridTemplateTrackList(rowsValue).length;\n  const isColumnFlow = autoFlowValue.startsWith(\"column\");\n\n  const autoRowCount = isColumnFlow\n    ? 0\n    : computeAutoTrackCount(childCount, templateColumnCount, templateRowCount);\n  const autoColumnCount = isColumnFlow\n    ? computeAutoTrackCount(childCount, templateRowCount, templateColumnCount)\n    : 0;\n\n  // Check if each axis can be edited visually\n  const columnsMode = getGridAxisMode(columnsValue);\n  const rowsMode = getGridAxisMode(rowsValue);\n  const isColumnsEditable = isEditableGridMode(columnsMode);\n  const isRowsEditable = isEditableGridMode(rowsMode);\n\n  const editGridButton = (\n    <Button color=\"neutral\" css={{ width: \"100%\" }}>\n      Configure grid\n    </Button>\n  );\n\n  return (\n    <FloatingPanel\n      title=\"Grid settings\"\n      placement=\"bottom-within\"\n      content={\n        <Flex\n          direction=\"column\"\n          css={{ width: theme.spacing[30], overflow: \"auto\" }}\n          data-floating-panel-container\n        >\n          <TrackEditor\n            property=\"grid-template-columns\"\n            trackType=\"column\"\n            disabled={!isColumnsEditable}\n          />\n          <TrackEditor\n            property=\"grid-auto-columns\"\n            trackType=\"column\"\n            label=\"Auto columns\"\n            autoTrackCount={autoColumnCount}\n          />\n          <TrackEditor\n            property=\"grid-template-rows\"\n            trackType=\"row\"\n            disabled={!isRowsEditable}\n          />\n          <TrackEditor\n            property=\"grid-auto-rows\"\n            trackType=\"row\"\n            label=\"Auto rows\"\n            defaultTrackValue=\"auto\"\n            autoTrackCount={autoRowCount}\n          />\n          <GridAreas />\n        </Flex>\n      }\n      open={open}\n      onOpenChange={onOpenChange}\n      closeOnInteractOutside={false}\n    >\n      {editGridButton}\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type {\n  StyleDecl,\n  StyleDeclKey,\n  StyleSource,\n  StyleSourceSelection,\n} from \"@webstudio-is/sdk\";\nimport type { StyleProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { isAutoGridPlacement, resetGridChildPlacement } from \"./grid-utils\";\n\nconst createStyleDecl = (\n  styleSourceId: string,\n  breakpointId: string,\n  property: StyleProperty,\n  value: StyleValue\n): StyleDecl => ({\n  styleSourceId,\n  breakpointId,\n  property,\n  value,\n});\n\nconst createStyleDeclEntry = (\n  styleDecl: StyleDecl\n): [StyleDeclKey, StyleDecl] => [\n  `${styleDecl.styleSourceId}:${styleDecl.breakpointId}:${styleDecl.property}:`,\n  styleDecl,\n];\n\nconst createData = ({\n  styleSourcesList = [],\n  selectionsList = [],\n  styleDeclsList = [],\n}: {\n  styleSourcesList?: StyleSource[];\n  selectionsList?: StyleSourceSelection[];\n  styleDeclsList?: StyleDecl[];\n}) => ({\n  styleSources: new Map(styleSourcesList.map((s) => [s.id, s])),\n  styleSourceSelections: new Map(selectionsList.map((s) => [s.instanceId, s])),\n  styles: new Map(styleDeclsList.map((d) => createStyleDeclEntry(d))),\n});\n\nconst auto: StyleValue = { type: \"keyword\", value: \"auto\" };\nconst numeric = (n: number): StyleValue => ({\n  type: \"unit\",\n  value: n,\n  unit: \"number\",\n});\nconst keyword = (v: string): StyleValue => ({ type: \"keyword\", value: v });\n\ndescribe(\"isAutoGridPlacement\", () => {\n  test(\"returns true when instance has no style source selection\", () => {\n    const data = createData({});\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(true);\n  });\n\n  test(\"returns true when instance has no local style sources\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"token1\", type: \"token\", name: \"Token\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"token1\"] }],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(true);\n  });\n\n  test(\"returns true when no grid placement styles exist\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"alignSelf\", keyword(\"center\")),\n      ],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(true);\n  });\n\n  test(\"returns true when all grid placement values are auto\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", auto),\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnEnd\", auto),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowStart\", auto),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowEnd\", auto),\n      ],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(true);\n  });\n\n  test(\"returns false when grid placement has numeric values\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", numeric(2)),\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnEnd\", numeric(3)),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowStart\", numeric(1)),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowEnd\", numeric(2)),\n      ],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(false);\n  });\n\n  test(\"returns false when any single property is numeric\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", numeric(2)),\n      ],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(false);\n  });\n\n  test(\"returns false when grid placement uses named areas\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", keyword(\"main\")),\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnEnd\", keyword(\"main\")),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowStart\", keyword(\"main\")),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowEnd\", keyword(\"main\")),\n      ],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(false);\n  });\n\n  test(\"ignores grid placement styles from token style sources\", () => {\n    const data = createData({\n      styleSourcesList: [\n        { id: \"token1\", type: \"token\", name: \"Token\" },\n        { id: \"local1\", type: \"local\" },\n      ],\n      selectionsList: [{ instanceId: \"child\", values: [\"token1\", \"local1\"] }],\n      styleDeclsList: [\n        // Token has grid placement — should be ignored\n        createStyleDecl(\"token1\", \"bp1\", \"gridColumnStart\", numeric(2)),\n        // Local has no grid placement\n        createStyleDecl(\"local1\", \"bp1\", \"alignSelf\", keyword(\"center\")),\n      ],\n    });\n    expect(isAutoGridPlacement({ ...data, instanceId: \"child\" })).toBe(true);\n  });\n});\n\ndescribe(\"resetGridChildPlacement\", () => {\n  test(\"removes grid placement styles from local style source\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", numeric(2)),\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnEnd\", numeric(3)),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowStart\", numeric(1)),\n        createStyleDecl(\"local1\", \"bp1\", \"gridRowEnd\", numeric(2)),\n        createStyleDecl(\"local1\", \"bp1\", \"alignSelf\", keyword(\"center\")),\n      ],\n    });\n\n    resetGridChildPlacement({ ...data, instanceId: \"child\" });\n\n    // Grid placement styles should be removed\n    expect(data.styles.size).toBe(1);\n    const remaining = [...data.styles.values()][0];\n    expect(remaining.property).toBe(\"alignSelf\");\n  });\n\n  test(\"keeps non-grid styles intact\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"alignSelf\", keyword(\"center\")),\n        createStyleDecl(\"local1\", \"bp1\", \"justifySelf\", keyword(\"start\")),\n        createStyleDecl(\"local1\", \"bp1\", \"order\", numeric(1)),\n      ],\n    });\n\n    resetGridChildPlacement({ ...data, instanceId: \"child\" });\n\n    expect(data.styles.size).toBe(3);\n  });\n\n  test(\"does nothing when instance has no style source selection\", () => {\n    const data = createData({});\n    resetGridChildPlacement({ ...data, instanceId: \"child\" });\n    expect(data.styles.size).toBe(0);\n  });\n\n  test(\"does nothing when instance has no local style sources\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"token1\", type: \"token\", name: \"Token\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"token1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"token1\", \"bp1\", \"gridColumnStart\", numeric(2)),\n      ],\n    });\n\n    resetGridChildPlacement({ ...data, instanceId: \"child\" });\n\n    // Token styles should not be touched\n    expect(data.styles.size).toBe(1);\n  });\n\n  test(\"removes auto-valued grid placement styles\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", auto),\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnEnd\", auto),\n      ],\n    });\n\n    resetGridChildPlacement({ ...data, instanceId: \"child\" });\n\n    expect(data.styles.size).toBe(0);\n  });\n\n  test(\"removes grid placement across multiple breakpoints\", () => {\n    const data = createData({\n      styleSourcesList: [{ id: \"local1\", type: \"local\" }],\n      selectionsList: [{ instanceId: \"child\", values: [\"local1\"] }],\n      styleDeclsList: [\n        createStyleDecl(\"local1\", \"bp1\", \"gridColumnStart\", numeric(1)),\n        createStyleDecl(\"local1\", \"bp2\", \"gridColumnStart\", numeric(3)),\n        createStyleDecl(\"local1\", \"bp1\", \"alignSelf\", keyword(\"center\")),\n      ],\n    });\n\n    resetGridChildPlacement({ ...data, instanceId: \"child\" });\n\n    expect(data.styles.size).toBe(1);\n    const remaining = [...data.styles.values()][0];\n    expect(remaining.property).toBe(\"alignSelf\");\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/layout/shared/grid-utils.ts",
    "content": "import type {\n  Instance,\n  StyleSource,\n  StyleSourceSelections,\n  StyleSources,\n  Styles,\n} from \"@webstudio-is/sdk\";\nimport type { StyleProperty } from \"@webstudio-is/css-engine\";\n\nconst gridPlacementProperties: ReadonlySet<StyleProperty> = new Set([\n  \"gridColumnStart\",\n  \"gridColumnEnd\",\n  \"gridRowStart\",\n  \"gridRowEnd\",\n]);\n\n/**\n * Check if an instance is auto-placed in a grid by examining its local styles.\n * Returns true when no grid placement property has a non-auto value.\n */\nexport const isAutoGridPlacement = ({\n  styles,\n  styleSources,\n  styleSourceSelections,\n  instanceId,\n}: {\n  styles: Styles;\n  styleSources: StyleSources;\n  styleSourceSelections: StyleSourceSelections;\n  instanceId: Instance[\"id\"];\n}): boolean => {\n  const selection = styleSourceSelections.get(instanceId);\n  if (selection === undefined) {\n    return true;\n  }\n  const localIds = new Set<StyleSource[\"id\"]>();\n  for (const id of selection.values) {\n    if (styleSources.get(id)?.type === \"local\") {\n      localIds.add(id);\n    }\n  }\n  if (localIds.size === 0) {\n    return true;\n  }\n  for (const styleDecl of styles.values()) {\n    if (localIds.has(styleDecl.styleSourceId) === false) {\n      continue;\n    }\n    if (gridPlacementProperties.has(styleDecl.property) === false) {\n      continue;\n    }\n    if (\n      styleDecl.value.type === \"keyword\" &&\n      styleDecl.value.value === \"auto\"\n    ) {\n      continue;\n    }\n    return false;\n  }\n  return true;\n};\n\n/**\n * Remove grid placement styles from an instance's local style sources.\n * Used after duplication to prevent overlapping grid children.\n */\nexport const resetGridChildPlacement = ({\n  styles,\n  styleSources,\n  styleSourceSelections,\n  instanceId,\n}: {\n  styles: Styles;\n  styleSources: StyleSources;\n  styleSourceSelections: StyleSourceSelections;\n  instanceId: Instance[\"id\"];\n}) => {\n  const selection = styleSourceSelections.get(instanceId);\n  if (selection === undefined) {\n    return;\n  }\n  const localIds = new Set<StyleSource[\"id\"]>();\n  for (const id of selection.values) {\n    if (styleSources.get(id)?.type === \"local\") {\n      localIds.add(id);\n    }\n  }\n  for (const [key, styleDecl] of styles) {\n    if (localIds.has(styleDecl.styleSourceId) === false) {\n      continue;\n    }\n    if (gridPlacementProperties.has(styleDecl.property)) {\n      styles.delete(key);\n    }\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/list-item.tsx",
    "content": "import { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { Grid, theme } from \"@webstudio-is/design-system\";\nimport { SelectControl } from \"../controls\";\nimport { StyleSection } from \"../shared/style-section\";\nimport { PropertyLabel } from \"../property-label\";\n\nexport const properties = [\"list-style-type\"] satisfies CssProperty[];\n\nexport const Section = () => {\n  return (\n    <StyleSection label=\"List Item\" properties={properties}>\n      <Grid gap={2} css={{ gridTemplateColumns: `1fr ${theme.spacing[21]}` }}>\n        <PropertyLabel\n          label=\"List Style Type\"\n          description={propertyDescriptions.listStyleType}\n          properties={[\"list-style-type\"]}\n        />\n        <SelectControl property=\"list-style-type\" />\n      </Grid>\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/outline/outline.stories.tsx",
    "content": "import { useEffect } from \"react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $instances,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { Section } from \"./outline\";\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(new Map([[\"local\", { id: \"local\", type: \"local\" }]]));\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$instances.set(\n  new Map([\n    [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n  ])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nconst solidOutline: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"outlineStyle\",\n  value: { type: \"keyword\", value: \"solid\" },\n};\n\nconst outlineWidth: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"outlineWidth\",\n  value: { type: \"unit\", value: 2, unit: \"px\" },\n};\n\nconst dashedOutline: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"outlineStyle\",\n  value: { type: \"keyword\", value: \"dashed\" },\n};\n\nconst outlineColor: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"outlineColor\",\n  value: { type: \"keyword\", value: \"red\" },\n};\n\nconst outlineOffset: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"outlineOffset\",\n  value: { type: \"unit\", value: 4, unit: \"px\" },\n};\n\nconst WithSolidOutlineVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([\n        [getStyleDeclKey(solidOutline), solidOutline],\n        [getStyleDeclKey(outlineWidth), outlineWidth],\n      ])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nconst WithDashedOutlineVariant = () => {\n  useEffect(() => {\n    $styles.set(\n      new Map([\n        [getStyleDeclKey(dashedOutline), dashedOutline],\n        [getStyleDeclKey(outlineColor), outlineColor],\n        [getStyleDeclKey(outlineWidth), outlineWidth],\n        [getStyleDeclKey(outlineOffset), outlineOffset],\n      ])\n    );\n  }, []);\n  return (\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  );\n};\n\nexport const Outline = () => (\n  <StorySection title=\"Outline\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <Section />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With solid outline</Text>\n        <WithSolidOutlineVariant />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">With dashed outline</Text>\n        <WithDashedOutlineVariant />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Outline\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/outline/outline.tsx",
    "content": "import { Grid, theme } from \"@webstudio-is/design-system\";\nimport {\n  MinusIcon,\n  DashedBorderIcon,\n  DottedBorderIcon,\n  XSmallIcon,\n} from \"@webstudio-is/icons\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport { ColorControl, TextControl } from \"../../controls\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { ToggleGroupControl } from \"../../controls/toggle-group/toggle-group-control\";\n\nexport const properties = [\n  \"outline-style\",\n  \"outline-color\",\n  \"outline-width\",\n  \"outline-offset\",\n] satisfies CssProperty[];\n\nexport const Section = () => {\n  const outlineStyle = useComputedStyleDecl(\"outline-style\");\n  const outlineStyleValue = toValue(outlineStyle.cascadedValue);\n\n  return (\n    <StyleSection label=\"Outline\" properties={properties}>\n      <Grid\n        css={{\n          gridTemplateColumns: `1fr ${theme.spacing[22]}`,\n        }}\n        gap={2}\n      >\n        <PropertyLabel\n          label=\"Style\"\n          description={propertyDescriptions.outlineStyle}\n          properties={[\"outline-style\"]}\n        />\n        <ToggleGroupControl\n          label=\"Style\"\n          properties={[\"outline-style\"]}\n          items={[\n            { child: <XSmallIcon />, value: \"none\" },\n            { child: <MinusIcon />, value: \"solid\" },\n            { child: <DashedBorderIcon />, value: \"dashed\" },\n            { child: <DottedBorderIcon />, value: \"dotted\" },\n          ]}\n        />\n\n        {outlineStyleValue !== \"none\" && (\n          <>\n            <PropertyLabel\n              label=\"Color\"\n              description={propertyDescriptions.outlineColor}\n              properties={[\"outline-color\"]}\n            />\n            <ColorControl property=\"outline-color\" />\n            <PropertyLabel\n              label=\"Width\"\n              description={propertyDescriptions.outlineWidth}\n              properties={[\"outline-width\"]}\n            />\n            <TextControl property=\"outline-width\" />\n            <PropertyLabel\n              label=\"Offset\"\n              description={propertyDescriptions.outlineOffset}\n              properties={[\"outline-offset\"]}\n            />\n            <TextControl property=\"outline-offset\" />\n          </>\n        )}\n      </Grid>\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/position/inset-control.stories.tsx",
    "content": "import type { Meta } from \"@storybook/react\";\nimport { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, StyleDecl } from \"@webstudio-is/sdk\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { InsetControl } from \"./inset-control\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport {\n  $breakpoints,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { $awareness } from \"~/shared/awareness\";\n\nconst top: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"top\",\n  value: {\n    type: \"unit\",\n    value: 0,\n    unit: \"px\",\n  },\n};\n\nconst right: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"right\",\n  value: {\n    type: \"unit\",\n    value: 123.27,\n    unit: \"rem\",\n  },\n};\n\nconst bottom: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"bottom\",\n  value: {\n    type: \"keyword\",\n    value: \"auto\",\n  },\n};\n\nconst left: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"left\",\n  value: {\n    type: \"unit\",\n    value: -20,\n    unit: \"%\",\n  },\n};\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(\n  new Map([\n    [\n      \"local\",\n      {\n        id: \"local\",\n        type: \"local\",\n      },\n    ],\n  ])\n);\n$styles.set(\n  new Map([\n    [getStyleDeclKey(top), top],\n    [getStyleDeclKey(right), right],\n    [getStyleDeclKey(bottom), bottom],\n    [getStyleDeclKey(left), left],\n  ])\n);\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nexport const Inset = () => {\n  return (\n    <StorySection title=\"Inset control\">\n      <Box css={{ width: theme.sizes.sidebarWidth }}>\n        <InsetControl />\n      </Box>\n    </StorySection>\n  );\n};\n\nexport default {\n  title: \"Style panel/Inset\",\n  component: Inset,\n} as Meta<typeof Inset>;\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/position/inset-control.tsx",
    "content": "import { useRef, useState } from \"react\";\nimport { Grid, theme } from \"@webstudio-is/design-system\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { useKeyboardNavigation } from \"../shared/keyboard\";\nimport { createBatchUpdate, deleteProperty } from \"../../shared/use-style-data\";\nimport { useScrub } from \"../shared/scrub\";\nimport { ValueText } from \"../shared/value-text\";\nimport { useComputedStyleDecl, useComputedStyles } from \"../../shared/model\";\nimport { useModifierKeys, type Modifiers } from \"../../shared/modifier-keys\";\nimport { InputPopover } from \"../shared/input-popover\";\nimport { InsetLayout, type InsetProperty } from \"./inset-layout\";\nimport { getInsetModifiersGroup, InsetTooltip } from \"./inset-tooltip\";\n\nconst movementMapInset = {\n  top: [\"bottom\", \"right\", \"bottom\", \"left\"],\n  right: [\"top\", \"left\", \"bottom\", \"left\"],\n  bottom: [\"top\", \"right\", \"top\", \"left\"],\n  left: [\"top\", \"right\", \"bottom\", \"right\"],\n} as const;\n\nconst Cell = ({\n  scrubStatus,\n  property,\n  getActiveProperties,\n  onHover,\n  isPopoverOpen,\n  onPopoverClose,\n}: {\n  isPopoverOpen: boolean;\n  onPopoverClose: () => void;\n  scrubStatus: ReturnType<typeof useScrub>;\n  property: InsetProperty;\n  getActiveProperties: (modifiers?: Modifiers) => CssProperty[];\n  onHover: (target: HoverTarget | undefined) => void;\n}) => {\n  const styleDecl = useComputedStyleDecl(property);\n  const finalValue: StyleValue | undefined =\n    (scrubStatus.isActive && scrubStatus.values[property]) ||\n    styleDecl.cascadedValue;\n\n  return (\n    <>\n      <InputPopover\n        styleSource={styleDecl.source.name}\n        value={finalValue}\n        isOpen={isPopoverOpen}\n        property={property}\n        getActiveProperties={getActiveProperties}\n        onClose={onPopoverClose}\n      />\n      <InsetTooltip property={property} preventOpen={scrubStatus.isActive}>\n        <ValueText\n          value={finalValue}\n          source={styleDecl.source.name}\n          onMouseEnter={(event) =>\n            onHover({ property, element: event.currentTarget })\n          }\n          onMouseLeave={() => onHover(undefined)}\n        />\n      </InsetTooltip>\n    </>\n  );\n};\n\ntype HoverTarget = {\n  element: HTMLElement;\n  property: InsetProperty;\n};\n\nexport const InsetControl = () => {\n  const styles = useComputedStyles([\"top\", \"right\", \"bottom\", \"left\"]);\n  const [hoverTarget, setHoverTarget] = useState<HoverTarget>();\n  const styleValue = styles.find(\n    (styleDecl) => styleDecl.property === hoverTarget?.property\n  );\n\n  const scrubStatus = useScrub({\n    value: styleValue?.usedValue,\n    target: styleValue?.cascadedValue.type === \"unit\" ? hoverTarget : undefined,\n    getModifiersGroup: getInsetModifiersGroup,\n    onChange: (values, options) => {\n      const batch = createBatchUpdate();\n      for (const property of [\"top\", \"right\", \"bottom\", \"left\"] as const) {\n        const value = values[property];\n        if (value !== undefined) {\n          batch.setProperty(property)(value);\n        }\n      }\n      batch.publish(options);\n    },\n  });\n\n  const [openProperty, setOpenProperty] = useState<InsetProperty>();\n  const [activePopoverProperties, setActivePopoverProperties] = useState<\n    undefined | CssProperty[]\n  >();\n  const modifiers = useModifierKeys();\n  const handleOpenProperty = (property: undefined | InsetProperty) => {\n    setOpenProperty(property);\n    setActivePopoverProperties(\n      property ? getInsetModifiersGroup(property, modifiers) : undefined\n    );\n  };\n\n  const layoutRef = useRef<HTMLDivElement>(null);\n\n  const keyboardNavigation = useKeyboardNavigation({\n    onOpen: handleOpenProperty,\n    movementMap: movementMapInset,\n  });\n\n  // by deafult highlight hovered or scrubbed properties\n  // if popover is open, highlight its property and hovered properties\n  const activeProperties = [\n    ...(activePopoverProperties ?? scrubStatus.properties),\n  ];\n  // if keyboard navigation is active, highlight its active property\n  if (keyboardNavigation.isActive) {\n    activeProperties.push(keyboardNavigation.activeProperty);\n  }\n  const getActiveProperties = (modifiers?: Modifiers) => {\n    return modifiers && openProperty\n      ? getInsetModifiersGroup(openProperty, modifiers)\n      : activeProperties;\n  };\n\n  const handleHover = (target: HoverTarget | undefined) => {\n    setHoverTarget(target);\n    keyboardNavigation.handleHover(target?.property);\n  };\n\n  return (\n    <Grid\n      ref={layoutRef}\n      tabIndex={0}\n      css={{\n        // Create stacking context to prevent z-index issues with internal z-indexes\n        zIndex: 0,\n        // InputPopover is not working properly without position relative\n        position: \"relative\",\n        width: theme.spacing[22],\n        height: theme.spacing[18],\n        \"&:focus-visible\": {\n          borderRadius: theme.borderRadius[3],\n          outline: `1px solid ${theme.colors.backgroundPrimary}`,\n          outlineOffset: -1,\n        },\n      }}\n      onFocus={keyboardNavigation.handleFocus}\n      onBlur={keyboardNavigation.handleBlur}\n      onKeyDown={keyboardNavigation.handleKeyDown}\n      onMouseMove={keyboardNavigation.handleMouseMove}\n      onMouseLeave={keyboardNavigation.handleMouseLeave}\n      onClick={(event) => {\n        const property = hoverTarget?.property;\n        const styleValueSource = styles.find(\n          (styleDecl) => styleDecl.property === property\n        )?.source.name;\n        if (\n          event.altKey &&\n          property &&\n          // reset when the value is set and after try to edit two sides\n          (styleValueSource === \"local\" || styleValueSource === \"overwritten\")\n        ) {\n          deleteProperty(property);\n          return;\n        }\n        handleOpenProperty(property);\n      }}\n    >\n      <InsetLayout\n        getActiveProperties={getActiveProperties}\n        renderCell={(property) => (\n          <Cell\n            scrubStatus={scrubStatus}\n            property={property}\n            getActiveProperties={getActiveProperties}\n            onHover={handleHover}\n            isPopoverOpen={openProperty === property}\n            onPopoverClose={() => {\n              if (openProperty === property) {\n                handleOpenProperty(undefined);\n                layoutRef.current?.focus();\n              }\n            }}\n          />\n        )}\n        onHover={handleHover}\n      />\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/position/inset-layout.stories.tsx",
    "content": "import type { Meta } from \"@storybook/react\";\nimport { InsetLayout as InsetLayoutComponent } from \"./inset-layout\";\nimport {\n  Flex,\n  Grid,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\n\nconst Cell = ({ label = \"auto\" }: { label?: string }) => (\n  <Text variant={\"spaceSectionValueText\"}>{label}</Text>\n);\n\nexport const InsetLayout = () => (\n  <StorySection title=\"Inset layout\">\n    <Flex direction=\"column\" gap=\"3\" css={{ width: theme.sizes.sidebarWidth }}>\n      <Text variant=\"labels\">Default (no active properties)</Text>\n      <Grid css={{ height: theme.spacing[18] }}>\n        <InsetLayoutComponent\n          renderCell={() => <Cell />}\n          getActiveProperties={() => []}\n          onHover={() => {}}\n        />\n      </Grid>\n\n      <Text variant=\"labels\">Single side active (top)</Text>\n      <Grid css={{ height: theme.spacing[18] }}>\n        <InsetLayoutComponent\n          renderCell={(property) => (\n            <Cell label={property === \"top\" ? \"10px\" : \"auto\"} />\n          )}\n          getActiveProperties={() => [\"top\"] as CssProperty[]}\n          onHover={() => {}}\n        />\n      </Grid>\n\n      <Text variant=\"labels\">Opposing pair active (left + right)</Text>\n      <Grid css={{ height: theme.spacing[18] }}>\n        <InsetLayoutComponent\n          renderCell={(property) => {\n            if (property === \"left\") {\n              return <Cell label=\"20%\" />;\n            }\n            if (property === \"right\") {\n              return <Cell label=\"5rem\" />;\n            }\n            return <Cell />;\n          }}\n          getActiveProperties={() => [\"left\", \"right\"] as CssProperty[]}\n          onHover={() => {}}\n        />\n      </Grid>\n\n      <Text variant=\"labels\">All sides active</Text>\n      <Grid css={{ height: theme.spacing[18] }}>\n        <InsetLayoutComponent\n          renderCell={(property) => {\n            const values: Record<string, string> = {\n              top: \"0px\",\n              right: \"123.27rem\",\n              bottom: \"auto\",\n              left: \"-20%\",\n            };\n            return <Cell label={values[property] ?? \"auto\"} />;\n          }}\n          getActiveProperties={() =>\n            [\"top\", \"right\", \"bottom\", \"left\"] as CssProperty[]\n          }\n          onHover={() => {}}\n        />\n      </Grid>\n\n      <Text variant=\"labels\">Adjacent sides active (top + right)</Text>\n      <Grid css={{ height: theme.spacing[18] }}>\n        <InsetLayoutComponent\n          renderCell={(property) => {\n            const values: Record<string, string> = {\n              top: \"10px\",\n              right: \"20px\",\n            };\n            return <Cell label={values[property] ?? \"auto\"} />;\n          }}\n          getActiveProperties={() => [\"top\", \"right\"] as CssProperty[]}\n          onHover={() => {}}\n        />\n      </Grid>\n\n      <Text variant=\"labels\">Three sides active (top, right, bottom)</Text>\n      <Grid css={{ height: theme.spacing[18] }}>\n        <InsetLayoutComponent\n          renderCell={(property) => {\n            const values: Record<string, string> = {\n              top: \"5px\",\n              right: \"10rem\",\n              bottom: \"15%\",\n            };\n            return <Cell label={values[property] ?? \"auto\"} />;\n          }}\n          getActiveProperties={() =>\n            [\"top\", \"right\", \"bottom\"] as CssProperty[]\n          }\n          onHover={() => {}}\n        />\n      </Grid>\n    </Flex>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Inset Layout\",\n  component: InsetLayoutComponent,\n} as Meta<typeof InsetLayoutComponent>;\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/position/inset-layout.tsx",
    "content": "import type { MouseEvent } from \"react\";\nimport { Grid, Box, theme, styled } from \"@webstudio-is/design-system\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport type { Modifiers } from \"../../shared/modifier-keys\";\n\nconst RECT_HEIGHT = 6;\nconst RECT_WIDTH = 42;\nconst RECT_BORDER_RADIUS = 1;\nconst OUTER_BORDER_RADIUS = 3;\n\nconst Trapezoid = styled(\"div\", {\n  borderRadius: OUTER_BORDER_RADIUS,\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n\n  \"&:hover\": {\n    backgroundColor: theme.colors.backgroundSpacingHover,\n  },\n  variants: {\n    isActive: {\n      true: {\n        backgroundColor: theme.colors.backgroundSpacingHover,\n      },\n    },\n  },\n});\n\nconst TopBottom = styled(Trapezoid, {\n  zIndex: 0,\n  backgroundColor: theme.colors.backgroundSpacingTopBottom,\n});\n\nconst LeftRight = styled(Trapezoid, {\n  zIndex: 1,\n  backgroundColor: theme.colors.backgroundSpacingLeftRight,\n});\n\nexport type InsetProperty = \"top\" | \"right\" | \"bottom\" | \"left\";\n\ntype InsetLayoutProps = {\n  renderCell: (property: InsetProperty) => React.ReactNode;\n  onHover: (\n    args: { element: HTMLElement; property: InsetProperty } | undefined\n  ) => void;\n  getActiveProperties: (modifiers?: Modifiers) => CssProperty[];\n};\n/**\n *  Grid schema for graphical layout\n *  ```\n *    1  23         45  6\n *  1 -------------------\n *    |  ||         ||  |\n *  2 -------------------\n *  3 -------------------\n *    |  ||         ||  |\n *  4 -------------------\n *  ```\n *\n **/\nexport const InsetLayout = ({\n  renderCell,\n  onHover,\n  getActiveProperties,\n}: InsetLayoutProps) => {\n  const createHandleHover = (property: InsetProperty) => ({\n    onMouseEnter: (e: MouseEvent<HTMLDivElement>) => {\n      onHover({\n        element: e.currentTarget,\n        property,\n      });\n    },\n    onMouseLeave: () => {\n      onHover(undefined);\n    },\n  });\n  const activeProperties = getActiveProperties();\n\n  return (\n    <Grid\n      css={{\n        borderRadius: OUTER_BORDER_RADIUS,\n\n        borderColor: theme.colors.borderMain,\n        borderWidth: 1,\n        borderStyle: \"solid\",\n\n        userSelect: \"none\",\n\n        gridTemplateColumns: `1fr ${RECT_BORDER_RADIUS / 2}px ${\n          RECT_WIDTH - RECT_BORDER_RADIUS\n        }px ${RECT_BORDER_RADIUS / 2}px 1fr`,\n\n        gridTemplateRows: `1fr ${RECT_HEIGHT}px 1fr`,\n      }}\n    >\n      <Box\n        css={{\n          borderRadius: RECT_BORDER_RADIUS,\n          gridArea: \"2/2/3/5\",\n          backgroundColor: theme.colors.backgroundControls,\n          borderWidth: 1,\n          borderStyle: \"solid\",\n          borderColor: theme.colors.borderMain,\n          zIndex: 2,\n        }}\n      />\n      <LeftRight\n        css={{\n          gridArea: \"1/1/4/3\",\n          clipPath: `polygon(0 0, 100% calc(50% - ${\n            RECT_HEIGHT / 2 - RECT_BORDER_RADIUS / 2\n          }px), 100% calc(50% + ${\n            RECT_HEIGHT / 2 - RECT_BORDER_RADIUS / 2\n          }px), 0% 100%)`,\n          cursor: \"w-resize\",\n        }}\n        isActive={activeProperties?.includes(\"left\")}\n        {...createHandleHover(\"left\")}\n      />\n\n      <LeftRight\n        css={{\n          gridArea: \"1/1/4/3\",\n          zIndex: 2,\n          visibility: \"hidden\",\n        }}\n      >\n        <Box\n          css={{\n            visibility: \"visible\",\n            margin: theme.spacing[2],\n            display: \"flex\",\n            justifyContent: \"center\",\n          }}\n        >\n          {renderCell(\"left\")}\n        </Box>\n      </LeftRight>\n\n      <LeftRight\n        css={{\n          gridArea: \"1/4/4/6\",\n          clipPath: `polygon(0 calc(50% - ${\n            RECT_HEIGHT / 2 - RECT_BORDER_RADIUS / 2\n          }px), 100% 0, 100% 100%, 0 calc(50% + ${\n            RECT_HEIGHT / 2 - RECT_BORDER_RADIUS / 2\n          }px))`,\n          cursor: \"e-resize\",\n        }}\n        isActive={activeProperties?.includes(\"right\")}\n        {...createHandleHover(\"right\")}\n      />\n\n      <LeftRight\n        css={{\n          gridArea: \"1/4/4/6\",\n          zIndex: 2,\n          visibility: \"hidden\",\n        }}\n      >\n        <Box\n          css={{\n            visibility: \"visible\",\n            margin: theme.spacing[2],\n            display: \"flex\",\n            justifyContent: \"center\",\n          }}\n        >\n          {renderCell(\"right\")}\n        </Box>\n      </LeftRight>\n\n      <TopBottom\n        css={{\n          gridArea: \"1/1/2/6\",\n          cursor: \"n-resize\",\n        }}\n        isActive={activeProperties?.includes(\"top\")}\n        {...createHandleHover(\"top\")}\n      >\n        {renderCell(\"top\")}\n      </TopBottom>\n\n      <TopBottom\n        css={{\n          gridArea: \"3/1/4/6\",\n          cursor: \"s-resize\",\n        }}\n        isActive={activeProperties?.includes(\"bottom\")}\n        {...createHandleHover(\"bottom\")}\n      >\n        {renderCell(\"bottom\")}\n      </TopBottom>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/position/inset-tooltip.tsx",
    "content": "import { useState, type ReactElement } from \"react\";\nimport { Tooltip } from \"@webstudio-is/design-system\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { useModifierKeys } from \"../../shared/modifier-keys\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\nimport type { InsetProperty } from \"./inset-layout\";\nimport { PropertyInfo } from \"../../property-label\";\nimport { useComputedStyles } from \"../../shared/model\";\n\nconst opposingInsetGroups = [\n  [\"top\", \"bottom\"],\n  [\"left\", \"right\"],\n] satisfies CssProperty[][];\n\nconst circleInsetGroups = [\n  [\"top\", \"right\", \"bottom\", \"left\"],\n] satisfies CssProperty[][];\n\nexport const getInsetModifiersGroup = (\n  property: CssProperty,\n  modifiers: { shiftKey: boolean; altKey: boolean }\n) => {\n  let groups: CssProperty[][] = [];\n\n  if (modifiers.shiftKey) {\n    groups = circleInsetGroups;\n  } else if (modifiers.altKey) {\n    groups = opposingInsetGroups;\n  }\n\n  return groups.find((group) => group.includes(property)) ?? [property];\n};\n\nconst sides = {\n  top: \"top\",\n  right: \"left\",\n  bottom: \"bottom\",\n  left: \"left\",\n} as const;\n\nconst propertyContents: {\n  properties: CssProperty[];\n  label: string;\n  description: string;\n}[] = [\n  {\n    properties: [\"top\", \"bottom\"],\n    label: \"Vertical position\",\n    description:\n      \"Sets the top and bottom position of an element relative to its nearest positioned ancestor.\",\n  },\n\n  {\n    properties: [\"left\", \"right\"],\n    label: \"Horizontal position\",\n    description:\n      \"Sets the left and right position of an element relative to its nearest positioned ancestor.\",\n  },\n\n  {\n    properties: [\"top\", \"right\", \"bottom\", \"left\"],\n    label: \"Inset position\",\n    description:\n      \"Sets the top, right, bottom and left position of an element relative to its nearest positioned ancestor.\",\n  },\n];\n\nconst isSameUnorderedArrays = <Item,>(\n  arrA: readonly Item[],\n  arrB: readonly Item[]\n) => {\n  if (arrA.length !== arrB.length) {\n    return false;\n  }\n\n  const union = new Set([...arrA, ...arrB]);\n  return union.size === arrA.length;\n};\n\nexport const InsetTooltip = ({\n  property,\n  children,\n  preventOpen,\n}: {\n  property: InsetProperty;\n  children: ReactElement;\n  preventOpen: boolean;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const modifiers = useModifierKeys();\n\n  const properties = [...getInsetModifiersGroup(property, modifiers)];\n  const styles = useComputedStyles(properties);\n\n  const resetProperties = () => {\n    const batch = createBatchUpdate();\n    for (const property of properties) {\n      batch.deleteProperty(property);\n    }\n    batch.publish();\n  };\n\n  const propertyContent = propertyContents.find((propertyContent) =>\n    isSameUnorderedArrays(propertyContent.properties, properties)\n  );\n\n  const handleOpenChange = (value: boolean) => {\n    if (preventOpen && value === true) {\n      return;\n    }\n    setIsOpen(value);\n  };\n\n  return (\n    <Tooltip\n      open={isOpen}\n      onOpenChange={handleOpenChange}\n      side={sides[property]}\n      // prevent closing tooltip on content click\n      onPointerDown={(event) => event.preventDefault()}\n      triggerProps={{\n        onClick: (event) => {\n          if (event.altKey) {\n            event.preventDefault();\n            resetProperties();\n            return;\n          }\n        },\n      }}\n      content={\n        <PropertyInfo\n          title={propertyContent?.label ?? \"\"}\n          description={propertyContent?.description}\n          styles={styles}\n          onReset={() => {\n            resetProperties();\n            handleOpenChange(false);\n          }}\n        />\n      }\n    >\n      {/* @todo show tooltip on focus */}\n      <div style={{ maxWidth: \"100%\" }}>{children}</div>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/position/position.tsx",
    "content": "import { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport { Grid, theme } from \"@webstudio-is/design-system\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport { SelectControl, TextControl } from \"../../controls\";\nimport { PropertyLabel } from \"../../property-label\";\nimport {\n  useComputedStyleDecl,\n  useParentComputedStyleDecl,\n} from \"../../shared/model\";\nimport { InsetControl } from \"./inset-control\";\n\nexport const properties = [\n  \"position\",\n  \"z-index\",\n  \"top\",\n  \"right\",\n  \"bottom\",\n  \"left\",\n] satisfies CssProperty[];\n\nexport const Section = () => {\n  const position = useComputedStyleDecl(\"position\");\n  const positionValue = toValue(position.computedValue);\n  const showInsetControl =\n    positionValue === \"relative\" ||\n    positionValue === \"absolute\" ||\n    positionValue === \"fixed\" ||\n    positionValue === \"sticky\";\n\n  const parentDisplay = useParentComputedStyleDecl(\"display\");\n  const parentDisplayValue = toValue(parentDisplay.computedValue);\n  const showZindexControl =\n    showInsetControl ||\n    parentDisplayValue === \"flex\" ||\n    parentDisplayValue === \"grid\" ||\n    parentDisplayValue === \"inline-flex\" ||\n    parentDisplayValue === \"inline-grid\";\n\n  return (\n    <StyleSection label=\"Position\" properties={properties}>\n      <Grid gap={2}>\n        <Grid gap={2} css={{ gridTemplateColumns: `1fr ${theme.spacing[23]}` }}>\n          <PropertyLabel\n            label=\"Position\"\n            description={propertyDescriptions.position}\n            properties={[\"position\"]}\n          />\n          <SelectControl property=\"position\" />\n          {showZindexControl && showInsetControl === false && (\n            <>\n              <PropertyLabel\n                label=\"Z Index\"\n                description={propertyDescriptions.zIndex}\n                properties={[\"z-index\"]}\n              />\n              <TextControl property=\"z-index\" />\n            </>\n          )}\n        </Grid>\n        {showInsetControl && (\n          <Grid gap={3} columns={2}>\n            <InsetControl />\n            <Grid gap={1}>\n              <PropertyLabel\n                label=\"Z Index\"\n                description={propertyDescriptions.zIndex}\n                properties={[\"z-index\"]}\n              />\n              <TextControl property=\"z-index\" />\n            </Grid>\n          </Grid>\n        )}\n      </Grid>\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/sections.ts",
    "content": "import type { ReactNode } from \"react\";\nimport * as layout from \"./layout/layout\";\nimport * as flexChild from \"./flex-child/flex-child\";\nimport * as gridChild from \"./grid-child/grid-child\";\nimport * as listItem from \"./list-item\";\nimport * as space from \"./space/space\";\nimport * as size from \"./size/size\";\nimport * as position from \"./position/position\";\nimport * as typography from \"./typography/typography\";\nimport * as backgrounds from \"./backgrounds/backgrounds\";\nimport * as borders from \"./borders/borders\";\nimport * as boxShadows from \"./box-shadows/box-shadows\";\nimport * as filter from \"./filter/filter\";\nimport * as transitions from \"./transitions/transitions\";\nimport * as outline from \"./outline/outline\";\nimport * as advanced from \"./advanced/advanced\";\nimport * as textShadows from \"./text-shadows/text-shadows\";\nimport * as backdropFilter from \"./backdrop-filter/backdrop-filter\";\nimport * as transforms from \"./transforms/transforms\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\n\nexport const sections = new Map<\n  string,\n  {\n    properties: CssProperty[];\n    Section: () => ReactNode;\n  }\n>([\n  [\"layout\", layout],\n  [\"flexChild\", flexChild],\n  [\"gridChild\", gridChild],\n  [\"listItem\", listItem],\n  [\"space\", space],\n  [\"size\", size],\n  [\"position\", position],\n  [\"typography\", typography],\n  [\"textShadows\", textShadows],\n  [\"backgrounds\", backgrounds],\n  [\"borders\", borders],\n  [\"boxShadows\", boxShadows],\n  [\"filter\", filter],\n  [\"backdropFilters\", backdropFilter],\n  [\"transitions\", transitions],\n  [\"transforms\", transforms],\n  [\"outline\", outline],\n  [\"advanced\", advanced],\n]);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/shared/align-self.tsx",
    "content": "import { Grid } from \"@webstudio-is/design-system\";\nimport {\n  XSmallIcon,\n  AlignSelfStartIcon,\n  AlignSelfEndIcon,\n  AlignSelfCenterIcon,\n  AlignSelfBaselineIcon,\n  AlignSelfStretchIcon,\n} from \"@webstudio-is/icons\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { ToggleGroupControl } from \"../../controls/toggle-group/toggle-group-control\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\n\ntype AlignSelfControlProps = {\n  /**\n   * \"flex\" uses flex-start/flex-end values\n   * \"grid\" uses start/end values\n   */\n  variant: \"flex\" | \"grid\";\n};\n\nexport const AlignSelfControl = ({ variant }: AlignSelfControlProps) => {\n  const startValue = variant === \"flex\" ? \"flex-start\" : \"start\";\n  const endValue = variant === \"flex\" ? \"flex-end\" : \"end\";\n  const axisName = variant === \"flex\" ? \"cross axis\" : \"block axis\";\n  const parentProperty =\n    variant === \"flex\" ? \"align-items\" : \"align-items or align-content\";\n\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <PropertyLabel\n        label=\"Align\"\n        description={propertyDescriptions.alignSelf}\n        properties={[\"align-self\"]}\n      />\n      <ToggleGroupControl\n        label=\"Align\"\n        properties={[\"align-self\"]}\n        items={[\n          {\n            child: <XSmallIcon />,\n            description: `The element's alignment is determined by its parent's ${parentProperty} property.`,\n            value: \"auto\",\n          },\n          {\n            child: <AlignSelfStartIcon />,\n            description: `The element is aligned at the start of the ${axisName}.`,\n            value: startValue,\n          },\n          {\n            child: <AlignSelfCenterIcon />,\n            description: `The element is centered along the ${axisName}.`,\n            value: \"center\",\n          },\n          {\n            child: <AlignSelfEndIcon />,\n            description: `The element is aligned at the end of the ${axisName}.`,\n            value: endValue,\n          },\n          {\n            child: <AlignSelfStretchIcon />,\n            description: `The element is stretched to fill the entire ${axisName}.`,\n            value: \"stretch\",\n          },\n          {\n            child: <AlignSelfBaselineIcon />,\n            description: `The element is aligned to the baseline along the ${axisName}.`,\n            value: \"baseline\",\n          },\n        ]}\n      />\n    </Grid>\n  );\n};\n\nexport const JustifySelfControl = () => {\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <PropertyLabel\n        label=\"Justify\"\n        description={propertyDescriptions.justifySelf}\n        properties={[\"justify-self\"]}\n      />\n      <ToggleGroupControl\n        label=\"Justify\"\n        properties={[\"justify-self\"]}\n        items={[\n          {\n            child: <XSmallIcon />,\n            description:\n              \"The element's justification is determined by its parent's justify-items property.\",\n            value: \"auto\",\n          },\n          {\n            child: (\n              <AlignSelfStartIcon style={{ transform: \"rotate(-90deg)\" }} />\n            ),\n            description:\n              \"The element is aligned at the start of the inline axis.\",\n            value: \"start\",\n          },\n          {\n            child: (\n              <AlignSelfCenterIcon style={{ transform: \"rotate(-90deg)\" }} />\n            ),\n            description: \"The element is centered along the inline axis.\",\n            value: \"center\",\n          },\n          {\n            child: <AlignSelfEndIcon style={{ transform: \"rotate(-90deg)\" }} />,\n            description:\n              \"The element is aligned at the end of the inline axis.\",\n            value: \"end\",\n          },\n          {\n            child: (\n              <AlignSelfStretchIcon style={{ transform: \"rotate(-90deg)\" }} />\n            ),\n            description:\n              \"The element is stretched to fill the entire inline axis.\",\n            value: \"stretch\",\n          },\n          {\n            child: (\n              <AlignSelfBaselineIcon style={{ transform: \"rotate(-90deg)\" }} />\n            ),\n            description:\n              \"The element is aligned to the baseline of the parent.\",\n            value: \"baseline\",\n          },\n        ]}\n      />\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/shared/input-popover.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  keyframes,\n  styled,\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  Flex,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport {\n  CssValueInput,\n  type IntermediateStyleValue,\n} from \"../../shared/css-value-input\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\nimport type { StyleValueSourceColor } from \"~/shared/style-object-model\";\nimport { $availableUnitVariables } from \"../../shared/model\";\nimport type { Modifiers } from \"../../shared/modifier-keys\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { humanizeString } from \"~/shared/string-utils\";\n\nconst slideUpAndFade = keyframes({\n  \"0%\": { opacity: 0, transform: \"scale(0.8)\" },\n  \"100%\": { opacity: 1, transform: \"scale(1)\" },\n});\n\nconst Input = ({\n  styleSource,\n  value,\n  property,\n  getActiveProperties,\n  onClosePopover,\n}: {\n  styleSource: StyleValueSourceColor;\n  property: CssProperty;\n  getActiveProperties: (modifiers?: Modifiers) => CssProperty[];\n  value: StyleValue;\n  onClosePopover: () => void;\n}) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  return (\n    <CssValueInput\n      minWidth=\"6ch\"\n      styleSource={styleSource}\n      property={property}\n      value={value}\n      intermediateValue={intermediateValue}\n      getOptions={() => $availableUnitVariables.get()}\n      fieldSizing=\"content\"\n      onChange={(styleValue) => {\n        setIntermediateValue(styleValue);\n        const activeProperties = getActiveProperties();\n        if (styleValue === undefined) {\n          const batch = createBatchUpdate();\n          for (const property of activeProperties) {\n            batch.deleteProperty(property);\n          }\n          batch.publish({ isEphemeral: true });\n          return;\n        }\n        if (styleValue.type !== \"intermediate\") {\n          const batch = createBatchUpdate();\n          for (const property of activeProperties) {\n            batch.setProperty(property)(styleValue);\n          }\n          batch.publish({ isEphemeral: true });\n        }\n      }}\n      onHighlight={(styleValue) => {\n        if (styleValue === undefined) {\n          const batch = createBatchUpdate();\n          const activeProperties = getActiveProperties();\n          for (const property of activeProperties) {\n            batch.deleteProperty(property);\n          }\n          batch.publish({ isEphemeral: true });\n          return;\n        }\n        const batch = createBatchUpdate();\n        batch.setProperty(property)(styleValue);\n        batch.publish({ isEphemeral: true });\n      }}\n      onChangeComplete={({ value, close = true, altKey, shiftKey }) => {\n        const activeProperties = getActiveProperties({\n          altKey,\n          shiftKey,\n          ctrlKey: false,\n          metaKey: false,\n        });\n\n        setIntermediateValue(undefined);\n        const batch = createBatchUpdate();\n        for (const property of activeProperties) {\n          batch.setProperty(property)(value);\n        }\n        batch.publish();\n        if (close) {\n          onClosePopover();\n        }\n      }}\n      onAbort={() => {\n        const batch = createBatchUpdate();\n        batch.deleteProperty(property);\n        batch.publish({ isEphemeral: true });\n      }}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        const batch = createBatchUpdate();\n        const activeProperties = getActiveProperties();\n        for (const property of activeProperties) {\n          batch.deleteProperty(property);\n        }\n        batch.publish();\n        onClosePopover();\n      }}\n    />\n  );\n};\n\n// trigger is used only for positioning\nconst Trigger = styled(\"div\", { position: \"absolute\", width: 0, height: 0 });\n\nconst PopoverContentStyled = styled(PopoverContent, {\n  flexDirection: \"row\",\n  gap: theme.spacing[5],\n  minWidth: 0,\n  minHeight: 0,\n  border: `1px solid ${theme.colors.borderMain}`,\n  borderRadius: theme.borderRadius[7],\n  background: theme.colors.backgroundPanel,\n  padding: theme.spacing[5],\n  boxShadow: theme.shadows.menuDropShadow,\n  animationDuration: \"200ms\",\n  animationTimingFunction: theme.easing.easeOut,\n  '&[data-state=\"open\"]': { animationName: slideUpAndFade },\n});\n\nexport const InputPopover = ({\n  styleSource,\n  property,\n  getActiveProperties,\n  value,\n  isOpen,\n  onClose,\n}: {\n  styleSource: StyleValueSourceColor;\n  property: CssProperty;\n  getActiveProperties: (modifiers?: Modifiers) => CssProperty[];\n  value: StyleValue;\n  isOpen: boolean;\n  onClose: () => void;\n}) => {\n  return (\n    <Popover\n      open={isOpen}\n      onOpenChange={(nextOpen) => {\n        if (nextOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <PopoverTrigger asChild>\n        <Trigger />\n      </PopoverTrigger>\n      <PopoverContentStyled\n        sideOffset={-24}\n        // prevent propagating click on input or combobox menu\n        // and closing popover before applying changes\n        onClick={(event) => event.stopPropagation()}\n      >\n        <PropertyLabel\n          label={humanizeString(property)}\n          description={propertyDescriptions[property]}\n          properties={[property]}\n        />\n        <Flex css={{ maxWidth: theme.spacing[30] }}>\n          <Input\n            styleSource={styleSource}\n            value={value}\n            property={property}\n            getActiveProperties={getActiveProperties}\n            onClosePopover={onClose}\n          />\n        </Flex>\n      </PopoverContentStyled>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/shared/keyboard.ts",
    "content": "import { useState, useRef } from \"react\";\nimport type { FocusEvent, FocusEventHandler, KeyboardEvent } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\n\nconst movementKeys = [\n  \"ArrowUp\",\n  \"ArrowRight\",\n  \"ArrowDown\",\n  \"ArrowLeft\",\n] as const;\n\n/**\n * useFocusWithin does't work with popovers, implement it using debounce\n */\nconst useFocusWithinDebounce = (\n  props: {\n    onFocusWithin: FocusEventHandler<Element>;\n    onBlurWithin: FocusEventHandler<Element>;\n  },\n  timeout: number\n) => {\n  const isFocusedRef = useRef(false);\n\n  const handleFocusBlur = useDebouncedCallback(\n    (isFocus: boolean, event: FocusEvent<Element>) => {\n      if (isFocus && isFocusedRef.current === false) {\n        isFocusedRef.current = true;\n        props.onFocusWithin(event);\n        return;\n      }\n      if (isFocus === false && isFocusedRef.current === true) {\n        isFocusedRef.current = false;\n        props.onBlurWithin(event);\n      }\n    },\n    timeout\n  );\n\n  const handleFocus = (event: FocusEvent<Element>) => {\n    // ...event because we debounce handleFocusBlur, and react reuses events\n    handleFocusBlur(true, { ...event });\n  };\n\n  const handleBlur = (event: FocusEvent<Element>) => {\n    handleFocusBlur(false, event);\n  };\n\n  return {\n    onFocus: handleFocus,\n    onBlur: handleBlur,\n  };\n};\n\nexport const useKeyboardNavigation = <\n  P extends string,\n  T extends {\n    readonly [key in P]: readonly [\n      ArrowUp: P,\n      ArrowRight: P,\n      ArrowDown: P,\n      ArrowLeft: P,\n    ];\n  },\n>({\n  onOpen,\n  movementMap,\n}: {\n  onOpen: (property: keyof T) => void;\n  movementMap: T;\n}) => {\n  const [activeProperty, setActiveProperty] = useState<keyof T>(\n    Object.keys(movementMap)[0] as P\n  );\n\n  const [hoverActiveProperty, setHoverActiveProperty] = useState<keyof T>(\n    Object.keys(movementMap)[0] as P\n  );\n\n  const [isActive, setIsActive] = useState(false);\n\n  const isMouseInsideRef = useRef(false);\n\n  const handleActiveChange = (value: boolean) => {\n    setIsActive(value);\n\n    if (value === false) {\n      setActiveProperty(hoverActiveProperty);\n    }\n  };\n\n  const handleFocusInternal = (event: FocusEvent<Element>) => {\n    if (event.currentTarget.matches(\":focus-visible\")) {\n      handleActiveChange(true);\n    }\n  };\n\n  const handleBlurInternal = () => {\n    handleActiveChange(false);\n  };\n\n  const { onFocus: handleFocus, onBlur: handleBlur } = useFocusWithinDebounce(\n    {\n      onFocusWithin: handleFocusInternal,\n      onBlurWithin: handleBlurInternal,\n    },\n    100\n  );\n\n  const handleHover = (property: keyof T | undefined) => {\n    // keep active property in sync with hover (makes UX more intuitive)\n    if (property) {\n      setHoverActiveProperty(property);\n      if (isActive === false) {\n        setActiveProperty(property);\n      }\n    }\n  };\n\n  const handleMouseMove = () => {\n    handleActiveChange(false);\n    isMouseInsideRef.current = true;\n  };\n\n  const handleMouseLeave = () => {\n    isMouseInsideRef.current = false;\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n    // ignore events originating from popover input or something\n    if (event.target !== event.currentTarget) {\n      return;\n    }\n\n    if (\n      event.key === \"ArrowUp\" ||\n      event.key === \"ArrowRight\" ||\n      event.key === \"ArrowDown\" ||\n      event.key === \"ArrowLeft\"\n    ) {\n      event.preventDefault(); // prevent scrolling\n      const key = event.key;\n\n      handleActiveChange(true);\n\n      if (isActive || isMouseInsideRef.current) {\n        setActiveProperty(\n          (property) => movementMap[property][movementKeys.indexOf(key)]\n        );\n      }\n    }\n\n    if (event.key === \"Enter\") {\n      handleActiveChange(true);\n      event.preventDefault(); // not sure we need this, but just in case\n      onOpen(activeProperty);\n    }\n  };\n\n  return {\n    activeProperty,\n    isActive,\n    handleHover,\n    // these are supposed to be put on the root element of the control\n    handleMouseMove,\n    handleMouseLeave,\n    handleFocus,\n    handleBlur,\n    handleKeyDown,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/shared/order.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Flex,\n  FloatingPanel,\n  Grid,\n  theme,\n  ToggleGroup,\n  ToggleGroupButton,\n} from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport {\n  XSmallIcon,\n  OrderFirstIcon,\n  OrderLastIcon,\n  EllipsesIcon,\n} from \"@webstudio-is/icons\";\nimport { TextControl } from \"../../controls\";\nimport { ToggleGroupTooltip } from \"../../controls/toggle-group/toggle-group-control\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { setProperty } from \"../../shared/use-style-data\";\n\nconst OrderPopover = () => {\n  return (\n    <FloatingPanel\n      title=\"Order\"\n      placement=\"bottom-within\"\n      content={\n        <Grid\n          css={{\n            gridTemplateColumns: \"4fr 6fr\",\n            padding: theme.panel.padding,\n          }}\n          gap={2}\n        >\n          <PropertyLabel\n            label=\"Order\"\n            description={propertyDescriptions.order}\n            properties={[\"order\"]}\n          />\n          <TextControl property=\"order\" />\n        </Grid>\n      }\n    >\n      <Flex>\n        <EllipsesIcon />\n      </Flex>\n    </FloatingPanel>\n  );\n};\n\nexport const OrderControl = () => {\n  const order = useComputedStyleDecl(\"order\");\n  const selectedValue = toValue(order.cascadedValue);\n  const items = [\n    {\n      child: <XSmallIcon />,\n      description: \"Don't change\",\n      value: \"0\",\n      code: \"order: 0;\",\n    },\n    {\n      child: <OrderFirstIcon />,\n      description: \"Make first\",\n      value: \"-1\",\n      code: \"order: -1;\",\n    },\n    {\n      child: <OrderLastIcon />,\n      description: \"Make last\",\n      value: \"1\",\n      code: \"order: 1;\",\n    },\n    {\n      child: <OrderPopover />,\n      description: \"Customize order\",\n      value: \"\",\n      code: `order: ${selectedValue};`,\n    },\n  ];\n  // Issue: The tooltip's grace area is too big and overlaps with nearby buttons,\n  // preventing the tooltip from changing when the buttons are hovered over in certain cases.\n  // To solve issue and allow tooltips to change on button hover,\n  // we close the button tooltip in the ToggleGroupButton.onMouseEnter handler.\n  // onMouseEnter used to preserve default hovering behavior on tooltip.\n  const [activeTooltip, setActiveTooltip] = useState<undefined | string>();\n  return (\n    <Grid css={{ gridTemplateColumns: \"3fr 8fr\" }}>\n      <PropertyLabel\n        label=\"Order\"\n        description={propertyDescriptions.order}\n        properties={[\"order\"]}\n      />\n      <ToggleGroup\n        color={order.source.name}\n        type=\"single\"\n        value={selectedValue}\n        onValueChange={(value) => {\n          switch (value) {\n            case \"0\":\n            case \"1\":\n            case \"-1\": {\n              setProperty(\"order\")({\n                type: \"unit\",\n                value: Number(value),\n                unit: \"number\",\n              });\n              break;\n            }\n          }\n        }}\n      >\n        {items.map((item) => (\n          <ToggleGroupTooltip\n            key={item.value}\n            isOpen={item.value === activeTooltip}\n            onOpenChange={(isOpen) =>\n              setActiveTooltip(isOpen ? item.value : undefined)\n            }\n            isSelected={item.value === selectedValue}\n            label=\"Order\"\n            code={item.code}\n            description={item.description}\n            properties={[\"order\"]}\n          >\n            <ToggleGroupButton\n              aria-checked={\n                item.value === selectedValue ||\n                // order takes integer value, so the value can be anything from +- 0-9\n                // And we already have toggle buttons for -1, 0, 1\n                // https://developer.mozilla.org/en-US/docs/Web/CSS/order#formal_syntax\n                (item.value === \"\" &&\n                  (Number.parseFloat(selectedValue) > 1 ||\n                    Number.parseFloat(selectedValue) < -1))\n              }\n              value={item.value}\n              onMouseEnter={() =>\n                // reset only when highlighted is not active\n                setActiveTooltip((prevValue) =>\n                  prevValue === item.value ? prevValue : undefined\n                )\n              }\n            >\n              {item.child}\n            </ToggleGroupButton>\n          </ToggleGroupTooltip>\n        ))}\n      </ToggleGroup>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/shared/scrub.tsx",
    "content": "import { useState, useEffect, useRef } from \"react\";\nimport type {\n  CssProperty,\n  StyleValue,\n  UnitValue,\n} from \"@webstudio-is/css-engine\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { numericScrubControl } from \"@webstudio-is/design-system\";\nimport { isValidDeclaration } from \"@webstudio-is/css-data\";\nimport { useModifierKeys } from \"../../shared/modifier-keys\";\nimport type { StyleUpdateOptions } from \"../../shared/use-style-data\";\nimport { parseIntermediateOrInvalidValue } from \"../../shared/css-value-input/parse-intermediate-or-invalid-value\";\nimport type { CssValueInputValue } from \"../../shared/css-value-input/css-value-input\";\n\ntype ScrubStatus<P extends string> = {\n  isActive: boolean;\n\n  // Properties that should be affected on the next pointer move.\n  // We keep track of these properties even when scrub is not active.\n  properties: ReadonlyArray<P>;\n\n  // When scrub is active, this contains ephemeral values for all properties\n  // that have been affected during current scrub.\n  //\n  // Note that Object.keys(values) != properties above,\n  // because user can press and release modifier keys multiple times during scrub.\n  values: Partial<Record<P, StyleValue>>;\n};\n\ntype HoverTarget<P> = {\n  property: P;\n  element: HTMLElement | SVGElement;\n};\n\n/**\n * Scrub hook for visual spacing diagram (box model UI)\n * - Works on any HTML/SVG element via hover target\n * - Supports multi-property editing with modifier keys (Shift/Alt)\n * - Has direction awareness (horizontal/vertical based on property)\n * - Returns status object with isActive, properties, and values\n * - Used in space.tsx for drag-to-adjust on the visual box model\n */\nexport const useScrub = <P extends CssProperty>(props: {\n  value?: StyleValue;\n  target: HoverTarget<P> | undefined;\n  getModifiersGroup: (\n    property: P,\n    modifiers: { shiftKey: boolean; altKey: boolean }\n  ) => ReadonlyArray<P>;\n  onChange: (\n    values: Partial<Record<P, StyleValue>>,\n    options: StyleUpdateOptions\n  ) => void;\n}): ScrubStatus<P> => {\n  // we want to hold on to the target while scrub is active even if hover changes\n  const [activeTarget, setActiveTarget] = useState<HoverTarget<P>>();\n  const finalTarget = activeTarget ?? props.target;\n\n  const modifiers = useModifierKeys();\n\n  const properties =\n    finalTarget === undefined\n      ? []\n      : props.getModifiersGroup(finalTarget.property, modifiers);\n\n  const [values, setValues] = useState<Partial<Record<P, StyleValue>>>({});\n\n  // we need these in useEffect, but don't want as dependencies\n  const nonDependencies = useRef({ props, values, properties });\n  nonDependencies.current = { props, values, properties };\n\n  const unitRef = useRef<undefined | UnitValue[\"unit\"]>(undefined);\n\n  useEffect(() => {\n    if (finalTarget === undefined) {\n      return;\n    }\n\n    // Don't activate scrub for keyword values\n    if (props.value?.type === \"keyword\") {\n      return;\n    }\n\n    const property = finalTarget.property;\n\n    const handleChange = (event: { value: number }, isEphemeral: boolean) => {\n      // for TypeScript\n      if (unitRef.current === undefined) {\n        return;\n      }\n\n      const { values, properties, props } = nonDependencies.current;\n\n      let value: CssValueInputValue = {\n        type: \"unit\",\n        value: event.value,\n        unit: unitRef.current,\n      } as const;\n\n      if (isValidDeclaration(property, toValue(value)) === false) {\n        value = parseIntermediateOrInvalidValue(property, {\n          type: \"intermediate\",\n          value: `${value.value}`,\n          unit: value.unit,\n        });\n\n        // In case of negative values for some properties, we might end up with invalid value.\n        if (value.type === \"invalid\") {\n          // Try return unitless\n          if (isValidDeclaration(property, \"0\")) {\n            value = {\n              type: \"unit\",\n              unit: \"number\",\n              value: 0,\n            };\n          }\n        }\n      }\n\n      const nextValues = { ...values };\n      for (const property of properties) {\n        nextValues[property] = value;\n      }\n\n      props.onChange(nextValues, { isEphemeral });\n\n      setValues(isEphemeral ? nextValues : {});\n    };\n\n    return numericScrubControl(finalTarget.element, {\n      direction:\n        property.toLowerCase().endsWith(\"left\") ||\n        property.toLowerCase().endsWith(\"right\")\n          ? \"horizontal\"\n          : \"vertical\",\n      getInitialValue() {\n        const { value } = nonDependencies.current.props;\n        if (value?.type === \"unit\") {\n          unitRef.current = value.unit;\n          return value.value;\n        }\n        // In case of Auto value\n        unitRef.current = \"px\";\n        return 0;\n      },\n      onValueInput(event) {\n        handleChange(event, true);\n      },\n      onValueChange(event) {\n        handleChange(event, false);\n      },\n      onStatusChange(status) {\n        setActiveTarget(\n          status === \"scrubbing\"\n            ? nonDependencies.current.props.target\n            : undefined\n        );\n      },\n    });\n  }, [finalTarget, props.value?.type]);\n\n  return {\n    isActive: activeTarget !== undefined,\n    properties,\n    values,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/shared/value-text.tsx",
    "content": "import { styled, Text } from \"@webstudio-is/design-system\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { useEffect, useMemo, type ComponentProps } from \"react\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { scrollByPointer } from \"../../shared/scroll-by-pointer\";\n\nconst Container = styled(\"button\", {\n  // fit-content is not needed for the \"button\" element,\n  // leave it here in case of tag change\n  width: \"fit-content\",\n  maxWidth: \"100%\",\n  display: \"flex\",\n  flexWrap: \"nowrap\",\n  alignItems: \"baseline\",\n  justifyContent: \"start\",\n  border: \"none\",\n  borderRadius: theme.borderRadius[3],\n  paddingBlock: theme.spacing[2],\n  paddingInline: 0,\n  overflow: \"hidden\",\n  whiteSpace: \"nowrap\",\n  // We want value to have `default` cursor to indicate that it's clickable,\n  // unlike the rest of the value area that has cursor that indicates scrubbing.\n  // Click and scrub works everywhere anyway, but we want cursors to be different.\n  //\n  // In order to have control over cursor we're setting pointerEvents to \"all\" here\n  // because SpaceLayout sets it to \"none\" for cells' content.\n  pointerEvents: \"all\",\n\n  \"&:focus-visible\": {\n    outline: \"none\",\n  },\n\n  variants: {\n    source: {\n      default: {\n        color: theme.colors.foregroundMain,\n        backgroundColor: \"transparent\",\n      },\n      local: {\n        color: theme.colors.foregroundLocalMain,\n        backgroundColor: theme.colors.backgroundLocalMain,\n      },\n      overwritten: {\n        color: theme.colors.foregroundOverwrittenMain,\n        backgroundColor: theme.colors.backgroundOverwrittenMain,\n      },\n      preset: {\n        color: theme.colors.foregroundMain,\n        backgroundColor: theme.colors.backgroundPresetMain,\n      },\n      remote: {\n        color: theme.colors.foregroundRemoteMain,\n        backgroundColor: theme.colors.backgroundRemoteMain,\n      },\n    },\n  },\n  compoundVariants: [\n    { source: \"default\", css: { color: theme.colors.foregroundTextSubtle } },\n  ],\n});\n\nexport const ValueText = ({\n  value,\n  source,\n  ...rest\n}: { value: StyleValue } & Omit<ComponentProps<typeof Container>, \"value\">) => {\n  const children = useMemo(() => {\n    if (value.type === \"unit\") {\n      // we want to show \"0\" rather than \"0px\" for default values for cleaner UI\n      if (source === \"default\" && value.unit === \"px\" && value.value === 0) {\n        return <Text variant=\"spaceSectionValueText\">{value.value}</Text>;\n      }\n\n      /**\n       * To prevent span width to change replace all numbers with wide characters. \"3\" looks like the wides with current font\n       **/\n      const wideValue = `${value.value}`.replace(/\\d/g, \"3\");\n\n      return (\n        <>\n          <Text variant=\"spaceSectionValueText\">\n            {value.value}\n            <Text\n              variant=\"spaceSectionValueText\"\n              css={{ height: 0, visibility: \"hidden\" }}\n            >\n              {wideValue}\n            </Text>\n          </Text>\n          {value.unit !== \"number\" && (\n            <Text variant=\"spaceSectionUnitText\">{value.unit}</Text>\n          )}\n        </>\n      );\n    }\n\n    if (value.type === \"var\") {\n      return <Text variant=\"spaceSectionValueText\">{value.value}</Text>;\n    }\n\n    return <Text variant=\"spaceSectionValueText\">{toValue(value)}</Text>;\n  }, [value, source]);\n\n  const { abort, ...autoScrollProps } = useMemo(scrollByPointer, []);\n\n  useEffect(() => {\n    return () => abort(\"unmount\");\n  }, [abort]);\n\n  return (\n    <Container source={source} {...rest} {...autoScrollProps} tabIndex={-1}>\n      {children}\n    </Container>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/size/size.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./size\";\n\nexport const Size = () => (\n  <StorySection title=\"Size\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Size\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/size/size.tsx",
    "content": "import {\n  camelCaseProperty,\n  propertyDescriptions,\n} from \"@webstudio-is/css-data\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  Flex,\n  Grid,\n  IconButton,\n  Separator,\n  styled,\n  FloatingPanel,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { PositionControl, SelectControl, TextControl } from \"../../controls\";\nimport {\n  EyeOpenIcon,\n  AutoScrollIcon,\n  EllipsesIcon,\n  CropIcon,\n  EyeClosedIcon,\n} from \"@webstudio-is/icons\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport { ToggleGroupControl } from \"../../controls/toggle-group/toggle-group-control\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { PropertyLabel } from \"../../property-label\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { deleteProperty } from \"../../shared/use-style-data\";\n\nconst SizeProperty = ({ property }: { property: CssProperty }) => {\n  return (\n    <Grid gap={1}>\n      <PropertyLabel\n        label={humanizeString(property)}\n        description={propertyDescriptions[camelCaseProperty(property)]}\n        properties={[property]}\n      />\n      <TextControl property={property} />\n    </Grid>\n  );\n};\n\nconst ObjectPosition = () => {\n  const styleDecl = useComputedStyleDecl(\"object-position\");\n  return (\n    <Flex justify=\"end\">\n      <FloatingPanel\n        title=\"Object Position\"\n        placement=\"bottom-within\"\n        content={\n          <Flex css={{ padding: theme.panel.padding }}>\n            <PositionControl property=\"object-position\" styleDecl={styleDecl} />\n          </Flex>\n        }\n      >\n        <IconButton\n          variant={styleDecl.source.name}\n          onClick={(event) => {\n            if (event.altKey) {\n              event.preventDefault();\n              deleteProperty(\"object-position\");\n            }\n          }}\n        >\n          <EllipsesIcon />\n        </IconButton>\n      </FloatingPanel>\n    </Flex>\n  );\n};\n\nexport const properties = [\n  \"width\",\n  \"height\",\n  \"min-width\",\n  \"min-height\",\n  \"max-width\",\n  \"max-height\",\n  \"overflow-x\",\n  \"overflow-y\",\n  \"object-fit\",\n  \"object-position\",\n  \"aspect-ratio\",\n] satisfies Array<CssProperty>;\n\nconst SectionLayout = styled(Grid, {\n  columnGap: theme.spacing[5],\n  rowGap: theme.spacing[5],\n  paddingInline: theme.panel.paddingInline,\n});\n\nexport const Section = () => {\n  return (\n    <StyleSection label=\"Size\" properties={properties} fullWidth>\n      <SectionLayout columns={2}>\n        <SizeProperty property=\"width\" />\n        <SizeProperty property=\"height\" />\n        <SizeProperty property=\"min-width\" />\n        <SizeProperty property=\"min-height\" />\n        <SizeProperty property=\"max-width\" />\n        <SizeProperty property=\"max-height\" />\n        <PropertyLabel\n          label=\"Aspect Ratio\"\n          description={propertyDescriptions.aspectRatio}\n          properties={[\"aspect-ratio\"]}\n        />\n        <TextControl property=\"aspect-ratio\" />\n      </SectionLayout>\n      <Separator />\n      <SectionLayout columns={2}>\n        <PropertyLabel\n          label=\"Overflow\"\n          description={propertyDescriptions.overflow}\n          properties={[\"overflow-x\", \"overflow-y\"]}\n        />\n        <ToggleGroupControl\n          label=\"Overflow\"\n          properties={[\"overflow-x\", \"overflow-y\"]}\n          items={[\n            {\n              child: <EyeOpenIcon />,\n              description: propertyDescriptions[\"overflowX:visible\"],\n              value: \"visible\",\n            },\n            {\n              child: <CropIcon />,\n              description: propertyDescriptions[\"overflowX:clip\"],\n              value: \"clip\",\n            },\n            {\n              child: <EyeClosedIcon />,\n              description: propertyDescriptions[\"overflowX:hidden\"],\n              value: \"hidden\",\n            },\n            {\n              child: <AutoScrollIcon />,\n              description: propertyDescriptions[\"overflowX:auto\"],\n              value: \"auto\",\n            },\n          ]}\n        />\n        <PropertyLabel\n          label=\"Object Fit\"\n          description={propertyDescriptions.objectFit}\n          properties={[\"object-fit\"]}\n        />\n        <SelectControl property=\"object-fit\" />\n        <PropertyLabel\n          label=\"Object Position\"\n          description={propertyDescriptions.objectPosition}\n          properties={[\"object-position\"]}\n        />\n        <ObjectPosition />\n      </SectionLayout>\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/space/layout.stories.tsx",
    "content": "import type { Meta } from \"@storybook/react\";\nimport { useState } from \"react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { SpaceLayout } from \"./layout\";\nimport { ValueText } from \"../shared/value-text\";\n\nexport default {\n  title: \"Style panel/Space\",\n} as Meta;\n\nexport const Layout = () => (\n  <StorySection title=\"Layout\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Placeholder cells</Text>\n        <Box css={{ width: theme.sizes.sidebarWidth }}>\n          <SpaceLayout\n            renderCell={() => <div style={{ color: \"red\" }}>·</div>}\n            onHover={() => {}}\n          />\n        </Box>\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Value texts</Text>\n        <ValueTextsSection />\n      </Flex>\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Active properties</Text>\n        <ActivePropertiesSection />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n\nconst allSources = [\n  \"local\",\n  \"overwritten\",\n  \"preset\",\n  \"default\",\n  \"remote\",\n] as const;\nconst allValues = [\n  { type: \"unit\" as const, value: 0, unit: \"px\" as const },\n  { type: \"unit\" as const, value: 10, unit: \"rem\" as const },\n  { type: \"unit\" as const, value: 100, unit: \"px\" as const },\n  { type: \"unit\" as const, value: 1000, unit: \"px\" as const },\n  { type: \"keyword\" as const, value: \"auto\" },\n  { type: \"keyword\" as const, value: \"revert-layer\" },\n];\n\nconst ValueTextsSection = () => {\n  const [_hovered, setHovered] = useState<{ property: string }>();\n  return (\n    <Flex direction=\"column\" gap=\"5\">\n      {allSources.map((source) => (\n        <Flex key={source} direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Source: {source}</Text>\n          <Box css={{ width: theme.sizes.sidebarWidth }}>\n            <SpaceLayout\n              onHover={setHovered}\n              onClick={() => undefined}\n              renderCell={() => (\n                <ValueText\n                  source={source}\n                  value={{ type: \"unit\", value: 100, unit: \"px\" }}\n                />\n              )}\n            />\n          </Box>\n        </Flex>\n      ))}\n      <Text variant=\"labels\">Different values</Text>\n      {allValues.map((value, index) => (\n        <Flex key={index} direction=\"column\" gap=\"1\">\n          <Text variant=\"mono\" css={{ fontSize: 11 }}>\n            {JSON.stringify(value)}\n          </Text>\n          <Box css={{ width: theme.sizes.sidebarWidth }}>\n            <SpaceLayout\n              onHover={setHovered}\n              onClick={() => undefined}\n              renderCell={() => <ValueText source=\"local\" value={value} />}\n            />\n          </Box>\n        </Flex>\n      ))}\n    </Flex>\n  );\n};\n\nconst ActivePropertiesSection = () => (\n  <Flex direction=\"column\" gap=\"5\">\n    <Flex direction=\"column\" gap=\"1\">\n      <Text variant=\"labels\">Active margin sides (top + bottom)</Text>\n      <Box css={{ width: theme.sizes.sidebarWidth }}>\n        <SpaceLayout\n          onHover={() => {}}\n          activeProperties={[\"margin-top\", \"margin-bottom\"]}\n          renderCell={() => (\n            <ValueText\n              source=\"local\"\n              value={{ type: \"unit\", value: 10, unit: \"px\" }}\n            />\n          )}\n        />\n      </Box>\n    </Flex>\n    <Flex direction=\"column\" gap=\"1\">\n      <Text variant=\"labels\">Active padding sides (left + right)</Text>\n      <Box css={{ width: theme.sizes.sidebarWidth }}>\n        <SpaceLayout\n          onHover={() => {}}\n          activeProperties={[\"padding-left\", \"padding-right\"]}\n          renderCell={() => (\n            <ValueText\n              source=\"local\"\n              value={{ type: \"unit\", value: 20, unit: \"px\" }}\n            />\n          )}\n        />\n      </Box>\n    </Flex>\n    <Flex direction=\"column\" gap=\"1\">\n      <Text variant=\"labels\">All properties active</Text>\n      <Box css={{ width: theme.sizes.sidebarWidth }}>\n        <SpaceLayout\n          onHover={() => {}}\n          activeProperties={[\n            \"margin-top\",\n            \"margin-right\",\n            \"margin-bottom\",\n            \"margin-left\",\n            \"padding-top\",\n            \"padding-right\",\n            \"padding-bottom\",\n            \"padding-left\",\n          ]}\n          renderCell={() => (\n            <ValueText\n              source=\"local\"\n              value={{ type: \"unit\", value: 8, unit: \"px\" }}\n            />\n          )}\n        />\n      </Box>\n    </Flex>\n  </Flex>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/space/layout.tsx",
    "content": "import { forwardRef, useId } from \"react\";\nimport type { ComponentProps, Ref } from \"react\";\nimport { styled, theme } from \"@webstudio-is/design-system\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  spaceProperties,\n  type HoverTarget,\n  type SpaceStyleProperty,\n} from \"./properties\";\n\nconst VALUE_WIDTH = 36;\nconst VALUE_HEIGHT = 24;\n\nconst BORDER = 1;\nconst INNER_MARGIN = 3;\n\nconst MOST_INNER_WIDTH = 62;\nconst MOST_INNER_HEIGHT = 6;\n\nconst INNER_WIDTH = MOST_INNER_WIDTH + (VALUE_WIDTH + BORDER) * 2;\nconst INNER_HEIGHT = MOST_INNER_HEIGHT + (VALUE_HEIGHT + BORDER) * 2;\n\nconst TOTAL_WIDTH = INNER_WIDTH + (INNER_MARGIN + VALUE_WIDTH + BORDER) * 2;\nconst TOTAL_HEIGHT = INNER_HEIGHT + (INNER_MARGIN + VALUE_HEIGHT + BORDER) * 2;\n\n// in SVG stroke is always in the middle of the line\nconst emulateInnerStroke = ({\n  width,\n  height,\n  x,\n  y,\n  strokeWidth = 1,\n}: {\n  width: number; // total desired size including border\n  height: number;\n  x: number;\n  y: number;\n  strokeWidth?: number;\n}) => ({\n  x: x + strokeWidth / 2,\n  y: y + strokeWidth / 2,\n  width: width - strokeWidth,\n  height: height - strokeWidth,\n});\n\nconst ValueArea = styled(\"path\", {\n  fill: theme.colors.backgroundSpacingTopBottom,\n  variants: {\n    side: {\n      top: { cursor: \"n-resize\" },\n      bottom: { cursor: \"s-resize\" },\n      right: {\n        cursor: \"e-resize\",\n        fill: theme.colors.backgroundSpacingLeftRight,\n      },\n      left: {\n        cursor: \"w-resize\",\n        fill: theme.colors.backgroundSpacingLeftRight,\n      },\n    },\n    isActive: {\n      true: {\n        fill: theme.colors.backgroundSpacingHover,\n      },\n    },\n  },\n});\n\nconst OuterRect = styled(\n  (props: ComponentProps<\"rect\">) => (\n    <rect\n      rx=\"2.5\"\n      {...emulateInnerStroke({\n        width: TOTAL_WIDTH,\n        height: TOTAL_HEIGHT,\n        x: 0,\n        y: 0,\n      })}\n      {...props}\n    />\n  ),\n  { stroke: theme.colors.borderMain }\n);\n\nconst InnerOuterRect = styled(\n  (props: ComponentProps<\"rect\">) => {\n    const width = INNER_WIDTH + INNER_MARGIN * 2;\n    const height = INNER_HEIGHT + INNER_MARGIN * 2;\n    return (\n      <rect\n        rx=\"2.5\"\n        {...emulateInnerStroke({\n          width,\n          height,\n          x: (TOTAL_WIDTH - width) / 2,\n          y: (TOTAL_HEIGHT - height) / 2,\n        })}\n        {...props}\n      />\n    );\n  },\n  { stroke: theme.colors.borderMain, fill: theme.colors.backgroundControls }\n);\n\nconst InnerRect = styled(\n  (props: ComponentProps<\"rect\">) => (\n    <rect\n      rx=\".5\"\n      {...emulateInnerStroke({\n        width: INNER_WIDTH,\n        height: INNER_HEIGHT,\n        x: (TOTAL_WIDTH - INNER_WIDTH) / 2,\n        y: (TOTAL_HEIGHT - INNER_HEIGHT) / 2,\n      })}\n      {...props}\n    />\n  ),\n  { stroke: theme.colors.borderMain }\n);\n\nconst MostInnerRect = styled(\n  (props: ComponentProps<\"rect\">) => {\n    return (\n      <rect\n        rx=\".5\"\n        {...emulateInnerStroke({\n          width: MOST_INNER_WIDTH,\n          height: MOST_INNER_HEIGHT,\n          x: (TOTAL_WIDTH - MOST_INNER_WIDTH) / 2,\n          y: (TOTAL_HEIGHT - MOST_INNER_HEIGHT) / 2,\n        })}\n        {...props}\n      />\n    );\n  },\n  { stroke: theme.colors.borderMain, fill: theme.colors.backgroundControls }\n);\n\nconst gap = `${INNER_MARGIN + BORDER}px`;\nconst Grid = styled(\"div\", {\n  position: \"absolute\",\n  top: 1,\n  left: 1,\n  right: 1,\n  bottom: 1,\n  display: \"grid\",\n  columnGap: gap,\n  // minmax here is a hack: https://css-tricks.com/preventing-a-grid-blowout/\n  gridTemplateColumns: `${VALUE_WIDTH}px ${VALUE_WIDTH}px minmax(0, 1fr) ${VALUE_WIDTH}px ${VALUE_WIDTH}px`,\n  // gap is inserted manually because we don't want it around the \"auto\" row\n  gridTemplateRows: `${VALUE_HEIGHT}px ${gap} ${VALUE_HEIGHT}px auto ${VALUE_HEIGHT}px ${gap} ${VALUE_HEIGHT}px`,\n  pointerEvents: \"none\",\n});\n\nconst Container = styled(\"div\", {\n  userSelect: \"none\",\n  position: \"relative\",\n  width: TOTAL_WIDTH,\n  height: TOTAL_HEIGHT,\n  \"&:focus-visible\": { outline: \"none\" },\n\n  // Grid happens to be positioned perfectly for the focus outline\n  // (both in z-order and in top/left)\n  [`&:focus-visible > ${Grid}`]: {\n    borderRadius: theme.borderRadius[3],\n    outline: `1px solid ${theme.colors.borderFocus}`,\n  },\n});\n\nconst Cell = styled(\"div\", {\n  position: \"relative\",\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  maxWidth: \"100%\",\n  variants: {\n    property: {\n      \"margin-top\": { gridColumn: \"2 / 5\", gridRow: \"1\" },\n      \"margin-right\": { gridColumn: \"5\", gridRow: \"1 / 8\" },\n      \"margin-bottom\": { gridColumn: \"2 / 5\", gridRow: \"7\" },\n      \"margin-left\": { gridColumn: \"1\", gridRow: \"1 / 8\" },\n      \"padding-top\": { gridColumn: \"3 / 4\", gridRow: \"3\" },\n      \"padding-right\": { gridColumn: \"4\", gridRow: \"3 / 6\" },\n      \"padding-bottom\": { gridColumn: \"3 / 4\", gridRow: \"5\" },\n      \"padding-left\": { gridColumn: \"2\", gridRow: \"3 / 6\" },\n    },\n  },\n});\n\nconst Label = styled(\"div\", {\n  color: theme.colors.foregroundTextSubtle,\n  textTransform: \"uppercase\",\n  fontSize: theme.deprecatedFontSize[1],\n  lineHeight: 1,\n  marginTop: 3,\n  marginLeft: 4,\n  gridColumn: \"1 / 6\",\n  gridRow: \"1\",\n  variants: {\n    inner: { true: { gridColumn: \"2 / 5\", gridRow: \"3\" } },\n  },\n});\n\nconst getSide = (property: SpaceStyleProperty) => {\n  switch (property) {\n    case \"margin-top\":\n    case \"padding-top\":\n      return \"top\";\n    case \"margin-right\":\n    case \"padding-right\":\n      return \"right\";\n    case \"margin-bottom\":\n    case \"padding-bottom\":\n      return \"bottom\";\n    case \"margin-left\":\n    case \"padding-left\":\n      return \"left\";\n  }\n};\n\nconst getPath = (property: SpaceStyleProperty) => {\n  const width = TOTAL_WIDTH;\n  const height = TOTAL_HEIGHT;\n  // distance between LeftValueArea's and RightValueArea's tips in the middle\n  const tips =\n    MOST_INNER_WIDTH - MOST_INNER_HEIGHT * (VALUE_WIDTH / VALUE_HEIGHT);\n\n  switch (getSide(property)) {\n    case \"top\":\n      return `M${width} 0H0L${(width - tips) / 2} ${height / 2}H${\n        (width + tips) / 2\n      }L${width} 0Z`;\n    case \"right\":\n      return `M${width} ${height}L${(width + tips) / 2} ${\n        height / 2\n      }L${width} 0V${height}Z`;\n    case \"bottom\":\n      return `M${width} ${height}H0L${(width - tips) / 2} ${height / 2}H${\n        (width + tips) / 2\n      }L${width} ${height}Z`;\n    case \"left\":\n      return `M0 0L${(width - tips) / 2} ${height / 2}L0 ${height}V0Z`;\n  }\n};\n\ntype LayoutProps = {\n  onFocus?: ComponentProps<\"div\">[\"onFocus\"];\n  onBlur?: ComponentProps<\"div\">[\"onBlur\"];\n  onKeyDown?: ComponentProps<\"div\">[\"onKeyDown\"];\n  onClick?: ComponentProps<\"div\">[\"onClick\"];\n  onMouseLeave?: ComponentProps<\"div\">[\"onMouseLeave\"];\n  onMouseMove?: ComponentProps<\"div\">[\"onMouseMove\"];\n  onHover: (hoverTarget: HoverTarget | undefined) => void;\n  activeProperties?: CssProperty[];\n  renderCell: (args: { property: SpaceStyleProperty }) => React.ReactNode;\n};\n\nexport const SpaceLayout = forwardRef(\n  (\n    {\n      onFocus,\n      onBlur,\n      onKeyDown,\n      onClick,\n      onHover,\n      onMouseLeave,\n      onMouseMove,\n      activeProperties,\n      renderCell,\n    }: LayoutProps,\n    ref: Ref<HTMLDivElement>\n  ) => {\n    const outerClipId = useId();\n    const innerClipId = useId();\n\n    const renderValueArea = (property: SpaceStyleProperty) => (\n      <ValueArea\n        side={getSide(property)}\n        d={getPath(property)}\n        onMouseEnter={(event) =>\n          onHover({ element: event.currentTarget, property })\n        }\n        onMouseLeave={() => onHover(undefined)}\n        isActive={activeProperties?.includes(property)}\n      />\n    );\n\n    return (\n      <Container\n        onFocus={onFocus}\n        onBlur={onBlur}\n        onClick={onClick}\n        onKeyDown={onKeyDown}\n        onMouseLeave={onMouseLeave}\n        onMouseMove={onMouseMove}\n        tabIndex={0}\n        ref={ref}\n      >\n        <svg\n          width={TOTAL_WIDTH}\n          height={TOTAL_HEIGHT}\n          viewBox={`0 0 ${TOTAL_WIDTH} ${TOTAL_HEIGHT}`}\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <g clipPath={`url(#${outerClipId})`}>\n            {renderValueArea(\"margin-top\")}\n            {renderValueArea(\"margin-right\")}\n            {renderValueArea(\"margin-bottom\")}\n            {renderValueArea(\"margin-left\")}\n          </g>\n\n          <OuterRect />\n          <InnerOuterRect />\n\n          <g clipPath={`url(#${innerClipId})`}>\n            {renderValueArea(\"padding-top\")}\n            {renderValueArea(\"padding-right\")}\n            {renderValueArea(\"padding-bottom\")}\n            {renderValueArea(\"padding-left\")}\n          </g>\n\n          <InnerRect />\n          <MostInnerRect />\n\n          <defs>\n            <clipPath id={outerClipId}>\n              <OuterRect />\n            </clipPath>\n            <clipPath id={innerClipId}>\n              <InnerRect />\n            </clipPath>\n          </defs>\n        </svg>\n        <Grid>\n          <Label>Margin</Label>\n          <Label inner>Padding</Label>\n\n          {spaceProperties.map((property) => (\n            <Cell property={property} key={property}>\n              {renderCell({ property })}\n            </Cell>\n          ))}\n        </Grid>\n      </Container>\n    );\n  }\n);\nSpaceLayout.displayName = \"SpaceLayout\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/space/properties.ts",
    "content": "import type { CssProperty } from \"@webstudio-is/css-engine\";\n\nexport const spaceProperties = [\n  \"margin-top\",\n  \"margin-right\",\n  \"margin-bottom\",\n  \"margin-left\",\n  \"padding-top\",\n  \"padding-right\",\n  \"padding-bottom\",\n  \"padding-left\",\n] satisfies CssProperty[];\n\nexport type SpaceStyleProperty = (typeof spaceProperties)[number];\n\nexport type HoverTarget = {\n  property: SpaceStyleProperty;\n  element: SVGElement | HTMLElement;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/space/space.tsx",
    "content": "import { useState, useRef } from \"react\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { SpaceLayout } from \"./layout\";\nimport { ValueText } from \"../shared/value-text\";\nimport { useScrub } from \"../shared/scrub\";\nimport {\n  spaceProperties,\n  type HoverTarget,\n  type SpaceStyleProperty,\n} from \"./properties\";\nimport { InputPopover } from \"../shared/input-popover\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport { useKeyboardNavigation } from \"../shared/keyboard\";\nimport { useComputedStyleDecl, useComputedStyles } from \"../../shared/model\";\nimport { createBatchUpdate, deleteProperty } from \"../../shared/use-style-data\";\nimport { useModifierKeys, type Modifiers } from \"../../shared/modifier-keys\";\n\nconst movementMapSpace = {\n  \"margin-top\": [\"margin-bottom\", \"margin-right\", \"padding-top\", \"margin-left\"],\n  \"margin-right\": [\n    \"margin-top\",\n    \"margin-left\",\n    \"margin-bottom\",\n    \"padding-right\",\n  ],\n  \"margin-bottom\": [\n    \"padding-bottom\",\n    \"margin-right\",\n    \"margin-top\",\n    \"margin-left\",\n  ],\n  \"margin-left\": [\n    \"margin-top\",\n    \"padding-left\",\n    \"margin-bottom\",\n    \"margin-right\",\n  ],\n  \"padding-top\": [\n    \"margin-top\",\n    \"padding-right\",\n    \"padding-bottom\",\n    \"padding-left\",\n  ],\n  \"padding-right\": [\n    \"padding-top\",\n    \"margin-right\",\n    \"padding-bottom\",\n    \"padding-bottom\",\n  ],\n  \"padding-bottom\": [\n    \"padding-top\",\n    \"padding-right\",\n    \"margin-bottom\",\n    \"padding-left\",\n  ],\n  \"padding-left\": [\n    \"padding-top\",\n    \"padding-top\",\n    \"padding-bottom\",\n    \"margin-left\",\n  ],\n} as const satisfies Record<SpaceStyleProperty, SpaceStyleProperty[]>;\n\nconst opposingSpaceGroups = [\n  [\"padding-top\", \"padding-bottom\"],\n  [\"padding-right\", \"padding-left\"],\n  [\"margin-top\", \"margin-bottom\"],\n  [\"margin-right\", \"margin-left\"],\n] satisfies CssProperty[][];\n\nconst circleSpaceGroups = [\n  [\"padding-top\", \"padding-right\", \"padding-bottom\", \"padding-left\"],\n  [\"margin-top\", \"margin-right\", \"margin-bottom\", \"margin-left\"],\n] satisfies CssProperty[][];\n\nconst getSpaceModifiersGroup = (\n  property: CssProperty,\n  modifiers: { shiftKey: boolean; altKey: boolean }\n) => {\n  let groups: CssProperty[][] = [];\n\n  if (modifiers.shiftKey) {\n    groups = circleSpaceGroups;\n  } else if (modifiers.altKey) {\n    groups = opposingSpaceGroups;\n  }\n\n  return groups.find((group) => group.includes(property)) ?? [property];\n};\n\nconst Cell = ({\n  isPopoverOpen,\n  onPopoverClose,\n  onHover,\n  property,\n  getActiveProperties,\n  scrubStatus,\n}: {\n  isPopoverOpen: boolean;\n  onPopoverClose: () => void;\n  onHover: (target: HoverTarget | undefined) => void;\n  property: SpaceStyleProperty;\n  getActiveProperties: (modifiers?: Modifiers) => CssProperty[];\n  scrubStatus: ReturnType<typeof useScrub>;\n}) => {\n  const styleDecl = useComputedStyleDecl(property);\n  const finalValue =\n    (scrubStatus.isActive && scrubStatus.values[property]) ||\n    styleDecl.cascadedValue;\n\n  return (\n    <>\n      <InputPopover\n        styleSource={styleDecl.source.name}\n        value={finalValue}\n        isOpen={isPopoverOpen}\n        property={property}\n        getActiveProperties={getActiveProperties}\n        onClose={onPopoverClose}\n      />\n      <ValueText\n        value={finalValue}\n        source={styleDecl.source.name}\n        onMouseEnter={(event) =>\n          onHover({ property, element: event.currentTarget })\n        }\n        onMouseLeave={() => onHover(undefined)}\n        onClick={(event) => {\n          if (event.altKey) {\n            deleteProperty(property);\n          }\n        }}\n      />\n    </>\n  );\n};\n\nexport { spaceProperties as properties };\n\nexport const Section = () => {\n  const styles = useComputedStyles(spaceProperties);\n  const [hoverTarget, setHoverTarget] = useState<HoverTarget>();\n  const styleValue = styles.find(\n    (styleDecl) => styleDecl.property === hoverTarget?.property\n  );\n  const scrubStatus = useScrub({\n    value: styleValue?.usedValue,\n    target: styleValue?.cascadedValue.type === \"unit\" ? hoverTarget : undefined,\n    getModifiersGroup: getSpaceModifiersGroup,\n    onChange: (values, options) => {\n      const batch = createBatchUpdate();\n      for (const property of spaceProperties) {\n        const value = values[property];\n        if (value !== undefined) {\n          batch.setProperty(property)(value);\n        }\n      }\n      batch.publish(options);\n    },\n  });\n\n  const [openProperty, setOpenProperty] = useState<CssProperty>();\n  const [activePopoverProperties, setActivePopoverProperties] = useState<\n    undefined | CssProperty[]\n  >();\n  const modifiers = useModifierKeys();\n  const handleOpenProperty = (property: undefined | SpaceStyleProperty) => {\n    setOpenProperty(property);\n    setActivePopoverProperties(\n      property ? getSpaceModifiersGroup(property, modifiers) : undefined\n    );\n  };\n\n  const layoutRef = useRef<HTMLDivElement>(null);\n\n  const keyboardNavigation = useKeyboardNavigation({\n    onOpen: handleOpenProperty,\n    movementMap: movementMapSpace,\n  });\n\n  // by deafult highlight hovered or scrubbed properties\n  // if popover is open, highlight its property and hovered properties\n  const activeProperties: CssProperty[] = [\n    ...(activePopoverProperties ?? scrubStatus.properties),\n  ];\n  // if keyboard navigation is active, highlight its active property\n  if (keyboardNavigation.isActive) {\n    activeProperties.push(keyboardNavigation.activeProperty);\n  }\n\n  const getActiveProperties = (modifiers?: Modifiers) => {\n    return modifiers && openProperty\n      ? getSpaceModifiersGroup(openProperty, modifiers)\n      : activeProperties;\n  };\n\n  const handleHover = (target: HoverTarget | undefined) => {\n    setHoverTarget(target);\n    keyboardNavigation.handleHover(target?.property);\n  };\n\n  return (\n    <StyleSection label=\"Space\" properties={spaceProperties}>\n      <SpaceLayout\n        ref={layoutRef}\n        onClick={(event) => {\n          const property = hoverTarget?.property;\n          const styleValueSource = styles.find(\n            (styleDecl) => styleDecl.property === property\n          )?.source.name;\n\n          if (\n            property &&\n            // reset when the value is set and after try to edit two sides\n            (styleValueSource === \"local\" || styleValueSource === \"overwritten\")\n          ) {\n            if (event.shiftKey && event.altKey) {\n              const properties = getSpaceModifiersGroup(property, {\n                shiftKey: true,\n                altKey: false,\n              });\n              const batch = createBatchUpdate();\n              for (const property of properties) {\n                batch.deleteProperty(property);\n              }\n              batch.publish();\n              return;\n            }\n            if (event.altKey) {\n              return;\n            }\n          }\n          handleOpenProperty(property);\n        }}\n        onHover={handleHover}\n        onFocus={keyboardNavigation.handleFocus}\n        onBlur={keyboardNavigation.handleBlur}\n        activeProperties={activeProperties}\n        onKeyDown={keyboardNavigation.handleKeyDown}\n        onMouseMove={keyboardNavigation.handleMouseMove}\n        onMouseLeave={keyboardNavigation.handleMouseLeave}\n        renderCell={({ property }) => (\n          <Cell\n            isPopoverOpen={openProperty === property}\n            onPopoverClose={() => {\n              if (openProperty === property) {\n                handleOpenProperty(undefined);\n                layoutRef.current?.focus();\n              }\n            }}\n            onHover={handleHover}\n            property={property}\n            getActiveProperties={getActiveProperties}\n            scrubStatus={scrubStatus}\n          />\n        )}\n      />\n    </StyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/space/tooltip.tsx",
    "content": "import { useState, type ReactElement } from \"react\";\nimport { deleteProperty } from \"../../shared/use-style-data\";\nimport { Tooltip } from \"@webstudio-is/design-system\";\nimport { PropertyInfo } from \"../../property-label\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport type { SpaceStyleProperty } from \"./properties\";\n\nconst sides = {\n  \"padding-top\": \"top\",\n  \"padding-right\": \"top\",\n  \"padding-bottom\": \"bottom\",\n  \"padding-left\": \"left\",\n  \"margin-top\": \"top\",\n  \"margin-right\": \"left\",\n  \"margin-bottom\": \"bottom\",\n  \"margin-left\": \"right\",\n} as const;\n\nconst propertyContents: {\n  properties: SpaceStyleProperty[];\n  label: string;\n  description: string;\n}[] = [\n  // Padding\n  {\n    properties: [\"padding-top\", \"padding-bottom\"],\n    label: \"Vertical Padding\",\n    description:\n      \"Defines the space between the content of an element and its top and bottom border. Can affect layout height.\",\n  },\n\n  {\n    properties: [\"padding-left\", \"padding-right\"],\n    label: \"Horizontal Padding\",\n    description:\n      \"Defines the space between the content of an element and its left and right border. Can affect layout width.\",\n  },\n\n  {\n    properties: [\n      \"padding-top\",\n      \"padding-bottom\",\n      \"padding-left\",\n      \"padding-right\",\n    ],\n    label: \"Padding\",\n    description:\n      \"Defines the space between the content of an element and its border. Can affect layout size.\",\n  },\n  // Margin\n  {\n    properties: [\"margin-top\", \"margin-bottom\"],\n    label: \"Vertical Margin\",\n    description: \"Sets the margin at the top and bottom of an element.\",\n  },\n\n  {\n    properties: [\"margin-left\", \"margin-right\"],\n    label: \"Horizontal Margin\",\n    description: \"Sets the margin at the left and right of an element.\",\n  },\n\n  {\n    properties: [\"margin-top\", \"margin-bottom\", \"margin-left\", \"margin-right\"],\n    label: \"Margin\",\n    description: \"Sets the margin of an element.\",\n  },\n];\n\nconst isSameUnorderedArrays = (\n  arrA: readonly string[],\n  arrB: readonly string[]\n) => {\n  if (arrA.length !== arrB.length) {\n    return false;\n  }\n\n  const union = new Set([...arrA, ...arrB]);\n  return union.size === arrA.length;\n};\n\nexport const SpaceTooltip = ({\n  property,\n  children,\n  preventOpen,\n}: {\n  property: SpaceStyleProperty;\n  children: ReactElement;\n  preventOpen: boolean;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const properties = [property];\n  const styles = useComputedStyles(properties);\n\n  const propertyContent = propertyContents.find((propertyContent) =>\n    isSameUnorderedArrays(propertyContent.properties, properties)\n  );\n\n  const handleOpenChange = (value: boolean) => {\n    if (preventOpen && value === true) {\n      return;\n    }\n    setIsOpen(value);\n  };\n\n  return (\n    <Tooltip\n      open={isOpen}\n      onOpenChange={handleOpenChange}\n      side={sides[property]}\n      // prevent closing tooltip on content click\n      onPointerDown={(event) => event.preventDefault()}\n      triggerProps={{\n        onClick: (event) => {\n          if (event.altKey) {\n            event.preventDefault();\n            deleteProperty(property);\n            return;\n          }\n        },\n      }}\n      content={\n        <PropertyInfo\n          title={propertyContent?.label ?? \"\"}\n          description={propertyContent?.description}\n          styles={styles}\n          onReset={() => {\n            deleteProperty(property);\n            handleOpenChange(false);\n          }}\n        />\n      }\n    >\n      {/* @todo show tooltip on focus */}\n      <div>{children}</div>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/text-shadows/text-shadows.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./text-shadows\";\n\nexport const TextShadows = () => (\n  <StorySection title=\"Text shadows\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Text Shadows\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/text-shadows/text-shadows.tsx",
    "content": "import {\n  toValue,\n  type CssProperty,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { RepeatedStyleSection } from \"../../shared/style-section\";\nimport { ShadowContent } from \"../../shared/shadow-content\";\nimport {\n  addRepeatedStyleItem,\n  editRepeatedStyleItem,\n  getComputedRepeatedItem,\n  RepeatedStyle,\n} from \"../../shared/repeated-style\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\n\nexport const properties = [\"text-shadow\"] satisfies [\n  CssProperty,\n  ...CssProperty[],\n];\n\nconst label = \"Text shadows\";\nconst initialTextShadow = \"0px 2px 5px rgba(0, 0, 0, 0.2)\";\n\nconst getItemProps = (layer: StyleValue, computedLayer?: StyleValue) => {\n  const shadowValue =\n    computedLayer?.type === \"shadow\" ? computedLayer : undefined;\n  const labels = [];\n  if (layer.type === \"var\") {\n    labels.push(`--${layer.value}`);\n  } else if (shadowValue) {\n    labels.push(toValue(shadowValue.offsetX));\n    labels.push(toValue(shadowValue.offsetY));\n    labels.push(toValue(shadowValue.blur));\n  } else {\n    labels.push(toValue(shadowValue));\n  }\n  const color = shadowValue?.color ? toValue(shadowValue.color) : undefined;\n  return { label: labels.join(\" \"), color };\n};\n\nexport const Section = () => {\n  const styleDecl = useComputedStyleDecl(\"text-shadow\");\n\n  return (\n    <RepeatedStyleSection\n      label={label}\n      description=\"Adds shadow effects around a text.\"\n      properties={properties}\n      onAdd={() => {\n        addRepeatedStyleItem(\n          [styleDecl],\n          parseCssFragment(initialTextShadow, [\"text-shadow\"])\n        );\n      }}\n    >\n      <RepeatedStyle\n        label={label}\n        styles={[styleDecl]}\n        getItemProps={(index, layer) =>\n          getItemProps(layer, getComputedRepeatedItem(styleDecl, index))\n        }\n        renderItemContent={(index, value) => (\n          <ShadowContent\n            index={index}\n            layer={value}\n            computedLayer={getComputedRepeatedItem(styleDecl, index)}\n            property=\"text-shadow\"\n            propertyValue={toValue(value)}\n            onEditLayer={(index, value, options) => {\n              editRepeatedStyleItem(\n                [styleDecl],\n                index,\n                new Map([[\"text-shadow\", value]]),\n                options\n              );\n            }}\n          />\n        )}\n      />\n    </RepeatedStyleSection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-and-perspective-origin.tsx",
    "content": "import {\n  camelCaseProperty,\n  propertyDescriptions,\n  propertySyntaxes,\n} from \"@webstudio-is/css-data\";\nimport { Flex, Grid, PositionGrid } from \"@webstudio-is/design-system\";\nimport {\n  KeywordValue,\n  StyleValue,\n  TupleValue,\n  UnitValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport { useMemo } from \"react\";\nimport { extractTransformOrPerspectiveOriginValues } from \"./transform-extractors\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport {\n  setProperty,\n  type StyleUpdateOptions,\n} from \"../../shared/use-style-data\";\nimport { PropertyInlineLabel, PropertyLabel } from \"../../property-label\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { humanizeString } from \"~/shared/string-utils\";\n\n// Fake properties to use in the CssValueInputContainer\n// x, y axis takes length | percentage | keyword\n// z axis takes length\nconst fakePropertyX: CssProperty = \"background-position-x\";\nconst fakePropertyY: CssProperty = \"background-position-y\";\nconst fakePropertyZ: CssProperty = \"outline-offset\";\n\nconst keywordToValue: Record<string, number> = {\n  left: 0,\n  right: 100,\n  center: 50,\n  top: 0,\n  bottom: 100,\n};\n\nexport const calculatePositionFromOrigin = (value: StyleValue | undefined) => {\n  if (value === undefined) {\n    return 50;\n  }\n\n  if (value.type === \"unit\") {\n    return value.value;\n  }\n\n  if (value.type === \"keyword\") {\n    return keywordToValue[value.value];\n  }\n\n  return 0;\n};\n\nexport const TransformAndPerspectiveOrigin = ({\n  property,\n}: {\n  property: CssProperty;\n}) => {\n  const styleDecl = useComputedStyleDecl(property);\n  const value = styleDecl.cascadedValue;\n  const origin = useMemo((): {\n    x: KeywordValue | UnitValue;\n    y: KeywordValue | UnitValue;\n    z?: UnitValue;\n  } => {\n    if (value.type === \"tuple\" || value.type === \"keyword\") {\n      return extractTransformOrPerspectiveOriginValues(value);\n    }\n    return {\n      x: { type: \"unit\", value: 50, unit: \"%\" },\n      y: { type: \"unit\", value: 50, unit: \"%\" },\n    };\n  }, [value]);\n\n  const xInfo = useMemo(() => calculatePositionFromOrigin(origin?.x), [origin]);\n  const yInfo = useMemo(() => calculatePositionFromOrigin(origin?.y), [origin]);\n  const xOriginKeywords: Array<KeywordValue> = [\"left\", \"center\", \"right\"].map(\n    (value) => ({\n      type: \"keyword\",\n      value,\n    })\n  );\n  const yOriginKeywords: Array<KeywordValue> = [\"top\", \"center\", \"bottom\"].map(\n    (value) => ({\n      type: \"keyword\",\n      value,\n    })\n  );\n\n  const handleValueChange = (\n    index: number,\n    value: StyleValue,\n    options?: StyleUpdateOptions\n  ) => {\n    if (value.type === \"invalid\") {\n      return;\n    }\n\n    let newValue: UnitValue | KeywordValue | undefined;\n\n    if (\n      value.type === \"layers\" &&\n      (value.value[0].type === \"unit\" || value.value[0].type === \"keyword\")\n    ) {\n      newValue = value.value[0];\n    }\n\n    if (value.type === \"unit\" || value.type === \"keyword\") {\n      newValue = value;\n    }\n\n    if (newValue === undefined) {\n      return;\n    }\n\n    const newTupleValue: TupleValue = {\n      type: \"tuple\",\n      value: [origin.x, origin.y],\n    };\n\n    if (origin.z !== undefined) {\n      newTupleValue.value.push(origin.z);\n    }\n\n    newTupleValue.value.splice(index, 1, newValue);\n    setProperty(property)(newTupleValue, options);\n  };\n\n  const handlePositionGridChange = (position: { x: number; y: number }) => {\n    const value: TupleValue = {\n      type: \"tuple\",\n      value: [\n        {\n          type: \"unit\",\n          value: position.x,\n          unit: \"%\",\n        },\n        {\n          type: \"unit\",\n          value: position.y,\n          unit: \"%\",\n        },\n      ],\n    };\n\n    if (property === \"transform-origin\" && origin.z !== undefined) {\n      value.value.push(origin.z);\n    }\n\n    setProperty(property)(value, { isEphemeral: false });\n  };\n\n  return (\n    <Grid gap=\"2\">\n      <PropertyLabel\n        label={humanizeString(property)}\n        description={propertyDescriptions[camelCaseProperty(property)]}\n        properties={[property]}\n      />\n      <Flex gap=\"6\">\n        <Grid css={{ gridTemplateColumns: \"1fr 2fr\" }} align=\"center\" gapX=\"2\">\n          <PositionGrid\n            selectedPosition={{ x: xInfo, y: yInfo }}\n            onSelect={handlePositionGridChange}\n          />\n          <Flex gap=\"2\" direction=\"column\">\n            <Grid\n              gap=\"2\"\n              align=\"center\"\n              css={{ gridTemplateColumns: \"auto 1fr\" }}\n            >\n              <PropertyInlineLabel\n                label=\"X\"\n                title={\n                  property === \"transform-origin\" ? \"X Offset\" : \"X Position\"\n                }\n                description={\n                  property === \"transform-origin\"\n                    ? propertySyntaxes.transformOriginY\n                    : propertySyntaxes.perspectiveOriginX\n                }\n              />\n              <CssValueInputContainer\n                value={origin.x}\n                getOptions={() => xOriginKeywords}\n                styleSource=\"local\"\n                property={fakePropertyX}\n                onDelete={() => {}}\n                onUpdate={(value, options) =>\n                  handleValueChange(0, value, options)\n                }\n              />\n            </Grid>\n            <Grid\n              gap=\"2\"\n              align=\"center\"\n              css={{ gridTemplateColumns: \"auto 1fr\" }}\n            >\n              <PropertyInlineLabel\n                label=\"Y\"\n                title={\n                  property === \"transform-origin\" ? \"Y Offset\" : \"Y Position\"\n                }\n                description={\n                  property === \"transform-origin\"\n                    ? propertySyntaxes.transformOriginY\n                    : propertySyntaxes.perspectiveOriginY\n                }\n              />\n              <CssValueInputContainer\n                value={origin.y}\n                getOptions={() => yOriginKeywords}\n                styleSource=\"local\"\n                property={fakePropertyY}\n                onDelete={() => {}}\n                onUpdate={(value, options) =>\n                  handleValueChange(1, value, options)\n                }\n              />\n            </Grid>\n            {property === \"transform-origin\" && origin.z !== undefined && (\n              <Grid\n                gap=\"2\"\n                align=\"center\"\n                css={{ gridTemplateColumns: \"auto 1fr\" }}\n              >\n                <PropertyInlineLabel\n                  label=\"Z\"\n                  title=\"Z Offset\"\n                  description={propertySyntaxes.transformOriginZ}\n                />\n                <CssValueInputContainer\n                  value={origin.z}\n                  styleSource=\"local\"\n                  property={fakePropertyZ}\n                  onDelete={() => {}}\n                  onUpdate={(value, options) =>\n                    handleValueChange(2, value, options)\n                  }\n                />\n              </Grid>\n            )}\n          </Flex>\n        </Grid>\n      </Flex>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-extractors.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  extractRotatePropertiesFromTransform,\n  extractSkewPropertiesFromTransform,\n  extractTransformOrPerspectiveOriginValues,\n} from \"./transform-extractors\";\nimport { parseCssValue } from \"@webstudio-is/css-data\";\nimport type { TupleValue } from \"@webstudio-is/css-engine\";\n\ndescribe(\"extractRotatePropertiesFromTransform\", () => {\n  test(\"parses transform and returns undefined if no rotate values exists\", () => {\n    expect(\n      extractRotatePropertiesFromTransform(\n        parseCssValue(\"transform\", \"scale(1.5)\")\n      )\n    ).toEqual({\n      rotateX: undefined,\n      rotateY: undefined,\n      rotateZ: undefined,\n    });\n  });\n\n  test(\"parses transform and returns rotate values\", () => {\n    expect(\n      extractRotatePropertiesFromTransform(\n        parseCssValue(\"transform\", \"rotateX(0deg) rotateY(10deg) scale(1.5)\")\n      )\n    ).toEqual({\n      rotateX: {\n        type: \"function\",\n        args: {\n          type: \"layers\",\n          value: [\n            {\n              type: \"unit\",\n              unit: \"deg\",\n              value: 0,\n            },\n          ],\n        },\n        name: \"rotateX\",\n      },\n      rotateY: {\n        type: \"function\",\n        args: {\n          type: \"layers\",\n          value: [\n            {\n              type: \"unit\",\n              unit: \"deg\",\n              value: 10,\n            },\n          ],\n        },\n        name: \"rotateY\",\n      },\n    });\n  });\n});\n\ndescribe(\"extractSkewPropertiesFromTransform\", () => {\n  test(\"parses transform and returns undefined if no skew properties exists\", () => {\n    expect(\n      extractSkewPropertiesFromTransform(\n        parseCssValue(\"transform\", \"rotateX(0deg) rotateY(0deg) scale(1.5)\")\n      )\n    ).toEqual({ skewX: undefined, skewY: undefined });\n  });\n\n  test(\"parses transform and extracts valid skew properties\", () => {\n    expect(\n      extractSkewPropertiesFromTransform(\n        parseCssValue(\n          \"transform\",\n          \"skewX(10deg) skewY(20deg) rotate(30deg) scale(1.5)\"\n        )\n      )\n    ).toEqual({\n      skewX: {\n        type: \"function\",\n        args: {\n          type: \"layers\",\n          value: [\n            {\n              type: \"unit\",\n              unit: \"deg\",\n              value: 10,\n            },\n          ],\n        },\n        name: \"skewX\",\n      },\n      skewY: {\n        type: \"function\",\n        args: {\n          type: \"layers\",\n          value: [\n            {\n              type: \"unit\",\n              unit: \"deg\",\n              value: 20,\n            },\n          ],\n        },\n        name: \"skewY\",\n      },\n    });\n  });\n});\n\ndescribe(\"extractTransformOrPerspectiveOriginValues\", () => {\n  test(\"parses transform-origin and returns the individual properties from the value\", () => {\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"center\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"keyword\", value: \"center\" },\n      y: { type: \"keyword\", value: \"center\" },\n      z: undefined,\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"top\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"keyword\", value: \"center\" },\n      y: { type: \"keyword\", value: \"top\" },\n      z: undefined,\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"right\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"keyword\", value: \"right\" },\n      y: { type: \"keyword\", value: \"center\" },\n      z: undefined,\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"45px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"unit\", unit: \"px\", value: 45 },\n      y: { type: \"keyword\", value: \"center\" },\n      z: undefined,\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"20px 40px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"unit\", unit: \"px\", value: 20 },\n      y: { type: \"unit\", unit: \"px\", value: 40 },\n      z: undefined,\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"10px 20px 30px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"unit\", unit: \"px\", value: 10 },\n      y: { type: \"unit\", unit: \"px\", value: 20 },\n      z: { type: \"unit\", unit: \"px\", value: 30 },\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"left top 30px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"keyword\", value: \"left\" },\n      y: { type: \"keyword\", value: \"top\" },\n      z: { type: \"unit\", value: 30, unit: \"px\" },\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"bottom right 60px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"keyword\", value: \"right\" },\n      y: { type: \"keyword\", value: \"bottom\" },\n      z: { type: \"unit\", value: 60, unit: \"px\" },\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"left 50% 60px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"keyword\", value: \"left\" },\n      y: { type: \"unit\", value: 50, unit: \"%\" },\n      z: { type: \"unit\", value: 60, unit: \"px\" },\n    });\n\n    expect(\n      extractTransformOrPerspectiveOriginValues(\n        parseCssValue(\"transform-origin\", \"50% bottom 60px\") as TupleValue\n      )\n    ).toEqual({\n      x: { type: \"unit\", value: 50, unit: \"%\" },\n      y: { type: \"keyword\", value: \"bottom\" },\n      z: { type: \"unit\", value: 60, unit: \"px\" },\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-extractors.ts",
    "content": "import type {\n  FunctionValue,\n  KeywordValue,\n  LayersValue,\n  StyleValue,\n  TupleValue,\n  UnitValue,\n} from \"@webstudio-is/css-engine\";\n\nexport const extractRotatePropertiesFromTransform = (transform: StyleValue) => {\n  let rotateX: FunctionValue | undefined;\n  let rotateY: FunctionValue | undefined;\n  let rotateZ: FunctionValue | undefined;\n\n  if (transform.type !== \"tuple\") {\n    return { rotateX, rotateY, rotateZ };\n  }\n\n  for (const item of transform.value) {\n    if (item.type === \"function\" && item.name === \"rotateX\") {\n      rotateX = item;\n    }\n\n    if (item.type === \"function\" && item.name === \"rotateY\") {\n      rotateY = item;\n    }\n\n    if (item.type === \"function\" && item.name === \"rotateZ\") {\n      rotateZ = item;\n    }\n  }\n\n  return { rotateX, rotateY, rotateZ };\n};\n\nexport const extractSkewPropertiesFromTransform = (skew: StyleValue) => {\n  type SkewFunction = FunctionValue & { args: LayersValue };\n  let skewX: SkewFunction | undefined = undefined;\n  let skewY: SkewFunction | undefined = undefined;\n\n  if (skew.type !== \"tuple\") {\n    return { skewX, skewY };\n  }\n\n  for (const item of skew.value) {\n    if (item.type === \"function\" && item.name === \"skewX\") {\n      skewX = item as SkewFunction;\n    }\n\n    if (item.type === \"function\" && item.name === \"skewY\") {\n      skewY = item as SkewFunction;\n    }\n  }\n\n  return { skewX, skewY };\n};\n\nconst isValidTransformOriginValue = (\n  value: StyleValue\n): value is UnitValue | KeywordValue => {\n  return value.type === \"unit\" || value.type === \"keyword\";\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/perspective-origin#syntax\n// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin#syntax\n// Both transform and perspective origin shares the same syntax for their values.\n// The only difference is `transform-origin` can have a 3rd value for z-axis.\nexport const extractTransformOrPerspectiveOriginValues = (\n  value: TupleValue | KeywordValue\n): {\n  x: KeywordValue | UnitValue;\n  y: KeywordValue | UnitValue;\n  z?: UnitValue;\n} => {\n  const xAxisKeywordValues: Array<string> = [\"left\", \"right\"];\n  const yAxisKeywordValues: Array<string> = [\"top\", \"bottom\"];\n  let x: KeywordValue | UnitValue = { type: \"keyword\", value: \"center\" };\n  let y: KeywordValue | UnitValue = { type: \"keyword\", value: \"center\" };\n  let z: UnitValue | undefined;\n\n  if (value.type === \"keyword\") {\n    if (value.value === \"center\") {\n      return {\n        x: value,\n        y: value,\n      };\n    }\n\n    if (yAxisKeywordValues.includes(value.value)) {\n      return {\n        x: x,\n        y: value,\n      };\n    }\n\n    if (xAxisKeywordValues.includes(value.value)) {\n      return {\n        x: value,\n        y: y,\n      };\n    }\n\n    return {\n      x: value,\n      y: value,\n    };\n  }\n\n  // https://www.w3.org/TR/css-transforms-1/#transform-origin-property\n  // If only one value is specified, the second value is assumed to be center.\n  if (value.value.length === 1 && value.value[0].type === \"unit\") {\n    x = value.value[0];\n  }\n\n  if (value.value.length === 1 && value.value[0].type === \"keyword\") {\n    if (yAxisKeywordValues.includes(value.value[0].value)) {\n      y = value.value[0];\n    }\n\n    if (xAxisKeywordValues.includes(value.value[0].value)) {\n      x = value.value[0];\n    }\n  }\n\n  // If keywords are used for x and y axises, their values can be swapped\n  // and they are still valid. So, we are making sure that x is left or right\n  // and y is top or bottom by checking their values.\n\n  if (value.value.length === 2) {\n    const [first, second] = value.value;\n    if (\n      isValidTransformOriginValue(first) === true &&\n      isValidTransformOriginValue(second) === true\n    ) {\n      x = first;\n      y = second;\n\n      if (\n        first.type === \"keyword\" &&\n        xAxisKeywordValues.includes(first.value)\n      ) {\n        x = first;\n        y = second;\n      } else if (\n        first.type === \"keyword\" &&\n        yAxisKeywordValues.includes(first.value)\n      ) {\n        y = first;\n        x = second;\n      }\n    }\n  }\n\n  if (value.value.length === 3) {\n    const [first, second, third] = value.value;\n    if (\n      isValidTransformOriginValue(first) === true &&\n      isValidTransformOriginValue(second) === true &&\n      third.type === \"unit\"\n    ) {\n      x = first;\n      y = second;\n      z = third;\n\n      if (\n        first.type === \"keyword\" &&\n        xAxisKeywordValues.includes(first.value)\n      ) {\n        x = first;\n        y = second;\n      } else if (\n        first.type === \"keyword\" &&\n        yAxisKeywordValues.includes(first.value)\n      ) {\n        y = first;\n        x = second;\n      }\n    }\n  }\n\n  return {\n    x,\n    y,\n    z,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-rotate.tsx",
    "content": "import { Flex, Grid } from \"@webstudio-is/design-system\";\nimport {\n  XAxisRotateIcon,\n  YAxisRotateIcon,\n  ZAxisRotateIcon,\n} from \"@webstudio-is/icons\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { propertySyntaxes } from \"@webstudio-is/css-data\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport {\n  $availableUnitVariables,\n  useComputedStyleDecl,\n} from \"../../shared/model\";\nimport { updateTransformFunction } from \"./transform-utils\";\n\nexport const RotatePanelContent = () => {\n  const styleDecl = useComputedStyleDecl(\"transform\");\n  const tuple =\n    styleDecl.cascadedValue.type === \"tuple\"\n      ? styleDecl.cascadedValue\n      : undefined;\n  let rotateX: StyleValue = { type: \"unit\", value: 0, unit: \"deg\" };\n  let rotateY: StyleValue = { type: \"unit\", value: 0, unit: \"deg\" };\n  let rotateZ: StyleValue = { type: \"unit\", value: 0, unit: \"deg\" };\n  for (const item of tuple?.value ?? []) {\n    if (item.type === \"function\" && item.args.type === \"layers\") {\n      if (item.name === \"rotateX\") {\n        rotateX = item.args.value[0];\n      }\n      if (item.name === \"rotateY\") {\n        rotateY = item.args.value[0];\n      }\n      if (item.name === \"rotateZ\") {\n        rotateZ = item.args.value[0];\n      }\n    }\n  }\n\n  return (\n    <Flex direction=\"column\" gap={2}>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n      >\n        <XAxisRotateIcon />\n        <PropertyInlineLabel\n          label=\"Rotate X\"\n          description={propertySyntaxes.rotateX}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property=\"rotate\"\n          getOptions={() => $availableUnitVariables.get()}\n          value={rotateX}\n          onUpdate={(value, options) =>\n            updateTransformFunction(styleDecl, \"rotateX\", value, options)\n          }\n          onDelete={() => {}}\n        />\n      </Grid>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n      >\n        <YAxisRotateIcon />\n        <PropertyInlineLabel\n          label=\"Rotate Y\"\n          description={propertySyntaxes.rotateY}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property=\"rotate\"\n          getOptions={() => $availableUnitVariables.get()}\n          value={rotateY}\n          onUpdate={(value, options) =>\n            updateTransformFunction(styleDecl, \"rotateY\", value, options)\n          }\n          onDelete={() => {}}\n        />\n      </Grid>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n      >\n        <ZAxisRotateIcon />\n        <PropertyInlineLabel\n          label=\"Rotate Z\"\n          description={propertySyntaxes.rotateZ}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property=\"rotate\"\n          getOptions={() => $availableUnitVariables.get()}\n          value={rotateZ}\n          onUpdate={(value, options) =>\n            updateTransformFunction(styleDecl, \"rotateZ\", value, options)\n          }\n          onDelete={() => {}}\n        />\n      </Grid>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-scale.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Box,\n  EnhancedTooltip,\n  Flex,\n  Grid,\n  SmallToggleButton,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport type { StyleValue, CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  BorderRadiusIcon,\n  XAxisIcon,\n  YAxisIcon,\n  ZAxisIcon,\n  Link2Icon,\n  Link2UnlinkedIcon,\n} from \"@webstudio-is/icons\";\nimport { propertySyntaxes } from \"@webstudio-is/css-data\";\nimport { CssValueInput } from \"../../shared/css-value-input\";\nimport {\n  deleteProperty,\n  setProperty,\n  type StyleUpdateOptions,\n} from \"../../shared/use-style-data\";\nimport type { IntermediateStyleValue } from \"../../shared/css-value-input/css-value-input\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport {\n  $availableUnitVariables,\n  useComputedStyleDecl,\n} from \"../../shared/model\";\n\nconst property: CssProperty = \"scale\";\n\nexport const ScalePanelContent = () => {\n  const styleDecl = useComputedStyleDecl(property);\n  const tuple =\n    styleDecl.cascadedValue.type === \"tuple\"\n      ? styleDecl.cascadedValue\n      : undefined;\n  const [scaleX, scaleY, scaleZ] = tuple?.value ?? [];\n  const [isScalingLocked, setScalingLock] = useState(true);\n  const [intermediateScalingX, setIntermediateScalingX] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n  const [intermediateScalingY, setIntermediateScalingY] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n  const [intermediateScalingZ, setIntermediateScalingZ] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  const setAxis = (\n    axis: number,\n    newValue: StyleValue,\n    options?: StyleUpdateOptions\n  ) => {\n    if (tuple === undefined) {\n      return;\n    }\n\n    if (newValue.type === \"tuple\") {\n      [newValue] = newValue.value;\n    }\n    if (newValue.type !== \"unit\" && newValue.type !== \"var\") {\n      newValue = { type: \"unit\", value: 100, unit: \"%\" };\n    }\n\n    const newTuple = structuredClone(tuple);\n    newTuple.value[axis] = newValue;\n    if (isScalingLocked) {\n      if (axis === 0 || axis === 1) {\n        newTuple.value[0] = newValue;\n        newTuple.value[1] = newValue;\n      }\n    }\n    setProperty(property)(newTuple, options);\n  };\n\n  const toggleScaling = () => {\n    const lockScaling = isScalingLocked === true ? false : true;\n    setScalingLock(lockScaling);\n    if (lockScaling === true) {\n      setAxis(1, scaleX);\n    }\n  };\n\n  return (\n    <Flex>\n      <Flex direction=\"column\" gap={2}>\n        <Grid\n          gap={1}\n          css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n        >\n          <XAxisIcon />\n          <PropertyInlineLabel\n            label=\"Scale X\"\n            description={propertySyntaxes.scaleX}\n          />\n          <CssValueInput\n            styleSource=\"local\"\n            property={property}\n            getOptions={() => $availableUnitVariables.get()}\n            value={scaleX}\n            intermediateValue={intermediateScalingX}\n            onChange={(value) => {\n              setIntermediateScalingX(value);\n              if (isScalingLocked) {\n                setIntermediateScalingY(value);\n              }\n              if (value === undefined) {\n                deleteProperty(property, { isEphemeral: true });\n              } else if (value.type !== \"intermediate\") {\n                setAxis(0, value, { isEphemeral: true });\n              }\n            }}\n            onChangeComplete={({ value }) => {\n              setIntermediateScalingX(undefined);\n              setIntermediateScalingY(undefined);\n              setAxis(0, value);\n            }}\n            onHighlight={(value) => {\n              if (value === undefined) {\n                deleteProperty(property, { isEphemeral: true });\n              } else {\n                setAxis(0, value, { isEphemeral: true });\n              }\n            }}\n            onAbort={() => deleteProperty(property, { isEphemeral: true })}\n            onReset={() => {\n              deleteProperty(property);\n            }}\n          />\n        </Grid>\n        <Grid\n          gap={1}\n          css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n        >\n          <YAxisIcon />\n          <PropertyInlineLabel\n            label=\"Scale Y\"\n            description={propertySyntaxes.scaleY}\n          />\n          <CssValueInput\n            styleSource=\"local\"\n            property={property}\n            getOptions={() => $availableUnitVariables.get()}\n            value={scaleY}\n            intermediateValue={intermediateScalingY}\n            onChange={(value) => {\n              setIntermediateScalingY(value);\n              if (isScalingLocked) {\n                setIntermediateScalingX(value);\n              }\n              if (value === undefined) {\n                deleteProperty(property, { isEphemeral: true });\n              } else if (value.type !== \"intermediate\") {\n                setAxis(1, value, { isEphemeral: true });\n              }\n            }}\n            onChangeComplete={({ value }) => {\n              setIntermediateScalingY(undefined);\n              setIntermediateScalingX(undefined);\n              setAxis(1, value);\n            }}\n            onHighlight={(value) => {\n              if (value === undefined) {\n                deleteProperty(property, { isEphemeral: true });\n              } else {\n                setAxis(1, value, { isEphemeral: true });\n              }\n            }}\n            onAbort={() => deleteProperty(property, { isEphemeral: true })}\n            onReset={() => {\n              deleteProperty(property);\n            }}\n          />\n        </Grid>\n        <Grid\n          gap={1}\n          css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n        >\n          <ZAxisIcon />\n          <PropertyInlineLabel\n            label=\"Scale Z\"\n            description={propertySyntaxes.scaleZ}\n          />\n          <CssValueInput\n            styleSource=\"local\"\n            property={property}\n            getOptions={() => $availableUnitVariables.get()}\n            value={scaleZ}\n            intermediateValue={intermediateScalingZ}\n            onChange={(value) => {\n              setIntermediateScalingZ(value);\n              if (value === undefined) {\n                deleteProperty(property, { isEphemeral: true });\n              } else if (value.type !== \"intermediate\") {\n                setAxis(2, value, { isEphemeral: true });\n              }\n            }}\n            onChangeComplete={({ value }) => {\n              setIntermediateScalingZ(undefined);\n              setAxis(2, value, { isEphemeral: false });\n            }}\n            onHighlight={(value) => {\n              if (value === undefined) {\n                deleteProperty(property, { isEphemeral: true });\n              } else {\n                setAxis(2, value, { isEphemeral: true });\n              }\n            }}\n            onAbort={() => deleteProperty(property, { isEphemeral: true })}\n            onReset={() => {\n              deleteProperty(property);\n            }}\n          />\n        </Grid>\n      </Flex>\n      <Flex\n        direction=\"column\"\n        css={{ paddingLeft: theme.spacing[3], width: theme.spacing[11] }}\n      >\n        <Box css={{ rotate: \"90deg\" }}>\n          <BorderRadiusIcon size=\"12\" />\n        </Box>\n        <EnhancedTooltip\n          content={\n            isScalingLocked === true\n              ? \"Unlink scale-x and scale-y values\"\n              : \"Link scale-x and scale-y values\"\n          }\n        >\n          <SmallToggleButton\n            css={{ rotate: \"90deg\" }}\n            pressed={isScalingLocked}\n            onPressedChange={toggleScaling}\n            variant=\"normal\"\n            icon={isScalingLocked ? <Link2Icon /> : <Link2UnlinkedIcon />}\n          />\n        </EnhancedTooltip>\n        <Box css={{ rotate: \"180deg\" }}>\n          <BorderRadiusIcon size=\"12\" />\n        </Box>\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-skew.tsx",
    "content": "import { Flex, Grid } from \"@webstudio-is/design-system\";\nimport { XAxisIcon, YAxisIcon } from \"@webstudio-is/icons\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { propertySyntaxes } from \"@webstudio-is/css-data\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport { useComputedStyleDecl } from \"../../shared/model\";\nimport { updateTransformFunction } from \"./transform-utils\";\nimport { extractSkewPropertiesFromTransform } from \"./transform-extractors\";\n\n// We use fakeProperty to pass for the CssValueInputContainer.\n// https://developer.mozilla.org/en-US/docs/Web/CSS/rotate#formal_syntax\n// angle\nconst fakeProperty = \"rotate\";\n\nconst defaultAngle: StyleValue = { type: \"unit\", value: 0, unit: \"deg\" };\n\nexport const SkewPanelContent = () => {\n  const styleDecl = useComputedStyleDecl(\"transform\");\n  const { skewX: skewXFn, skewY: skewYFn } = extractSkewPropertiesFromTransform(\n    styleDecl.cascadedValue\n  );\n\n  const skewX: StyleValue = skewXFn?.args.value[0] ?? defaultAngle;\n  const skewY: StyleValue = skewYFn?.args.value[0] ?? defaultAngle;\n\n  return (\n    <Flex direction=\"column\" gap={2}>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n      >\n        <XAxisIcon />\n        <PropertyInlineLabel\n          label=\"Skew X\"\n          description={propertySyntaxes.skewX}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property={fakeProperty}\n          value={skewX}\n          onUpdate={(value, options) =>\n            updateTransformFunction(styleDecl, \"skewX\", value, options)\n          }\n          onDelete={() => {}}\n        />\n      </Grid>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n      >\n        <YAxisIcon />\n        <PropertyInlineLabel\n          label=\"Skew Y\"\n          description={propertySyntaxes.skewY}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property={fakeProperty}\n          value={skewY}\n          onUpdate={(value, options) =>\n            updateTransformFunction(styleDecl, \"skewY\", value, options)\n          }\n          onDelete={() => {}}\n        />\n      </Grid>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-translate.tsx",
    "content": "import { Flex, Grid } from \"@webstudio-is/design-system\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { XAxisIcon, YAxisIcon, ZAxisIcon } from \"@webstudio-is/icons\";\nimport { propertySyntaxes } from \"@webstudio-is/css-data\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport {\n  setProperty,\n  type StyleUpdateOptions,\n} from \"../../shared/use-style-data\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport {\n  $availableUnitVariables,\n  useComputedStyleDecl,\n} from \"../../shared/model\";\n\nconst property: CssProperty = \"translate\";\n\nexport const TranslatePanelContent = () => {\n  const styleDecl = useComputedStyleDecl(property);\n  const tuple =\n    styleDecl.cascadedValue.type === \"tuple\"\n      ? styleDecl.cascadedValue\n      : undefined;\n  const [translateX, translateY, translateZ] = tuple?.value ?? [];\n\n  const setAxis = (\n    axis: number,\n    newValue: StyleValue,\n    options?: StyleUpdateOptions\n  ) => {\n    if (tuple === undefined) {\n      return;\n    }\n\n    // For individual translate properties, we are passing the property as translate.\n    // This is sending back either tuple or a unit value when manually edited and when  scrub is used respectively.\n    if (newValue.type === \"tuple\") {\n      [newValue] = newValue.value;\n    }\n    if (newValue.type !== \"unit\" && newValue.type !== \"var\") {\n      newValue = { type: \"unit\", value: 0, unit: \"px\" };\n    }\n\n    const newTuple = structuredClone(tuple);\n    newTuple.value[axis] = newValue;\n    setProperty(property)(newTuple, options);\n  };\n\n  return (\n    <Flex direction=\"column\" gap={2}>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 2fr 2fr\" }}\n      >\n        <XAxisIcon />\n        <PropertyInlineLabel\n          label=\"Translate X\"\n          description={propertySyntaxes.translateX}\n        />\n\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property={property}\n          getOptions={() => $availableUnitVariables.get()}\n          value={translateX}\n          onUpdate={(newValue, options) => setAxis(0, newValue, options)}\n          onDelete={(options) =>\n            setProperty(property)(styleDecl.cascadedValue, options)\n          }\n        />\n      </Grid>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 2fr 2fr\" }}\n      >\n        <YAxisIcon />\n        <PropertyInlineLabel\n          label=\"Translate Y\"\n          description={propertySyntaxes.translateY}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property={property}\n          getOptions={() => $availableUnitVariables.get()}\n          value={translateY}\n          onUpdate={(newValue, options) => setAxis(1, newValue, options)}\n          onDelete={(options) =>\n            setProperty(property)(styleDecl.cascadedValue, options)\n          }\n        />\n      </Grid>\n      <Grid\n        gap={1}\n        css={{ alignItems: \"center\", gridTemplateColumns: \"auto 1fr 1fr\" }}\n      >\n        <ZAxisIcon />\n        <PropertyInlineLabel\n          label=\"Translate Z\"\n          description={propertySyntaxes.translateZ}\n        />\n        <CssValueInputContainer\n          styleSource=\"local\"\n          property={property}\n          getOptions={() => $availableUnitVariables.get()}\n          value={translateZ}\n          onUpdate={(newValue, options) => setAxis(2, newValue, options)}\n          onDelete={(options) =>\n            setProperty(property)(styleDecl.cascadedValue, options)\n          }\n        />\n      </Grid>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transform-utils.ts",
    "content": "import { parseCssValue } from \"@webstudio-is/css-data\";\nimport {\n  FunctionValue,\n  StyleValue,\n  toValue,\n  type TupleValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  deleteProperty,\n  setProperty,\n  type StyleUpdateOptions,\n} from \"../../shared/use-style-data\";\nimport {\n  extractRotatePropertiesFromTransform,\n  extractSkewPropertiesFromTransform,\n} from \"./transform-extractors\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\n\nexport const transformPanels = [\n  \"translate\",\n  \"scale\",\n  \"rotate\",\n  \"skew\",\n] as const;\n\nexport type TransformPanel = (typeof transformPanels)[number];\n\nconst defaultTranslate = \"0px 0px 0px\";\nconst defaultScale = \"100% 100% 100%\";\nconst defaultRotate = \"rotateX(0deg) rotateY(0deg) rotateZ(0deg)\";\nconst defaultSkew = \"skewX(0deg) skewY(0deg)\";\n\nexport const getHumanizedTextFromTransformLayer = (\n  panel: TransformPanel,\n  value: StyleValue\n): { label: string; value: StyleValue } | undefined => {\n  switch (panel) {\n    case \"translate\":\n      return {\n        label: `Translate: ${toValue({ ...value, hidden: false })}`,\n        value,\n      };\n\n    case \"scale\":\n      return {\n        label: `Scale: ${toValue({ ...value, hidden: false })}`,\n        value,\n      };\n\n    case \"rotate\": {\n      const rotate = extractRotatePropertiesFromTransform(value);\n      const { rotateX, rotateY, rotateZ } = rotate;\n      if (\n        rotateX === undefined ||\n        rotateY === undefined ||\n        rotateZ === undefined\n      ) {\n        return;\n      }\n\n      return {\n        label: `Rotate: ${toValue(rotateX.args)} ${toValue(rotateY.args)} ${toValue(rotateZ.args)}`,\n        value: {\n          type: \"tuple\",\n          value: [rotateX, rotateY, rotateZ],\n          hidden: rotateX.hidden || rotateY.hidden || rotateZ.hidden,\n        },\n      };\n    }\n\n    case \"skew\": {\n      const skew = extractSkewPropertiesFromTransform(value);\n      const { skewX, skewY } = skew;\n\n      if (skewX === undefined || skewY === undefined) {\n        return;\n      }\n\n      return {\n        label: `Skew: ${toValue(skewX.args)} ${toValue(skewY.args)}`,\n        value: {\n          type: \"tuple\",\n          value: [skewX, skewY],\n          hidden: skewX.hidden || skewY.hidden,\n        },\n      };\n    }\n  }\n};\n\nexport const addDefaultsForTransormSection = (props: {\n  panel: (typeof transformPanels)[number];\n  styles: ComputedStyleDecl[];\n}) => {\n  const { panel, styles } = props;\n\n  switch (panel) {\n    case \"translate\": {\n      const translate = parseCssValue(\"translate\", defaultTranslate);\n      return setProperty(\"translate\")(translate);\n    }\n\n    case \"scale\": {\n      const scale = parseCssValue(\"scale\", defaultScale);\n      return setProperty(\"scale\")(scale);\n    }\n\n    case \"skew\":\n    case \"rotate\": {\n      const parsedValue = parseCssValue(\n        \"transform\",\n        panel === \"rotate\" ? defaultRotate : defaultSkew\n      );\n      const transform = styles.find(\n        (styleDecl) => styleDecl.property === \"transform\"\n      )?.cascadedValue;\n\n      // rotate and skew are maintained using tuple\n      // If the existing value is anything other than tuple.\n      // We need to update the property to use tuples\n      if (transform === undefined || transform.type !== \"tuple\") {\n        return setProperty(\"transform\")(parsedValue);\n      }\n\n      if (parsedValue.type === \"tuple\" && transform.type === \"tuple\") {\n        const filteredValues = removeRotateOrSkewValues(panel, transform);\n\n        return setProperty(\"transform\")({\n          ...transform,\n          value: [...parsedValue.value, ...filteredValues],\n        });\n      }\n    }\n  }\n};\n\nexport const isTransformPanelPropertyUsed = ({\n  panel,\n  styles,\n}: {\n  panel: (typeof transformPanels)[number];\n  styles: ComputedStyleDecl[];\n}): boolean => {\n  switch (panel) {\n    case \"translate\":\n    case \"scale\": {\n      const styleDecl = styles.find(\n        (styleDecl) => styleDecl.property === panel\n      );\n      return styleDecl?.cascadedValue.type === \"tuple\";\n    }\n    case \"rotate\": {\n      const styleDecl = styles.find(\n        (styleDecl) => styleDecl.property === \"transform\"\n      );\n      return (\n        styleDecl?.cascadedValue.type === \"tuple\" &&\n        extractRotatePropertiesFromTransform(styleDecl.cascadedValue)\n          .rotateX !== undefined\n      );\n    }\n    case \"skew\": {\n      const styleDecl = styles.find(\n        (styleDecl) => styleDecl.property === \"transform\"\n      );\n      return (\n        styleDecl?.cascadedValue.type === \"tuple\" &&\n        extractSkewPropertiesFromTransform(styleDecl.cascadedValue).skewX !==\n          undefined\n      );\n    }\n    default:\n      return false;\n  }\n};\n\nexport const removeRotateOrSkewValues = (\n  panel: TransformPanel,\n  value: TupleValue\n) => {\n  const propKeys =\n    panel === \"rotate\" ? [\"rotateX\", \"rotateY\", \"rotateZ\"] : [\"skewX\", \"skewY\"];\n  return value.value.filter(\n    (item) => item.type === \"function\" && propKeys.includes(item.name) === false\n  );\n};\n\nexport const handleDeleteTransformProperty = ({\n  panel,\n  value,\n}: {\n  panel: TransformPanel;\n  value: StyleValue;\n}) => {\n  switch (panel) {\n    case \"scale\":\n    case \"translate\":\n      deleteProperty(panel);\n      break;\n\n    case \"rotate\": {\n      if (value.type !== \"tuple\") {\n        return;\n      }\n      const filteredValues = removeRotateOrSkewValues(\"rotate\", value);\n      if (filteredValues.length === 0) {\n        deleteProperty(\"transform\");\n        return;\n      }\n      setProperty(\"transform\")({\n        ...value,\n        value: filteredValues,\n      });\n      break;\n    }\n\n    case \"skew\": {\n      if (value.type !== \"tuple\") {\n        return;\n      }\n      const filteredValues = removeRotateOrSkewValues(\"skew\", value);\n      if (filteredValues.length === 0) {\n        deleteProperty(\"transform\");\n        return;\n      }\n      setProperty(\"transform\")({\n        ...value,\n        value: filteredValues,\n      });\n    }\n  }\n};\n\nexport const handleHideTransformProperty = ({\n  panel,\n  value,\n}: {\n  panel: TransformPanel;\n  value: StyleValue;\n}) => {\n  switch (panel) {\n    case \"scale\":\n    case \"translate\": {\n      if (value.type !== \"tuple\") {\n        return;\n      }\n      setProperty(panel)({\n        ...value,\n        hidden: value.hidden ? false : true,\n      });\n      break;\n    }\n\n    case \"rotate\": {\n      if (value.type !== \"tuple\") {\n        return;\n      }\n      const newValue: TupleValue = {\n        ...value,\n        value: [...removeRotateOrSkewValues(\"rotate\", value)],\n      };\n      const rotate = extractRotatePropertiesFromTransform(value);\n      const { rotateX, rotateY, rotateZ } = rotate;\n\n      if (rotateX) {\n        newValue.value.unshift({\n          ...rotateX,\n          hidden: rotateX.hidden ? false : true,\n        });\n      }\n\n      if (rotateY) {\n        newValue.value.unshift({\n          ...rotateY,\n          hidden: rotateY.hidden ? false : true,\n        });\n      }\n\n      if (rotateZ) {\n        newValue.value.unshift({\n          ...rotateZ,\n          hidden: rotateZ.hidden ? false : true,\n        });\n      }\n\n      setProperty(\"transform\")(newValue);\n      break;\n    }\n\n    case \"skew\": {\n      if (value.type !== \"tuple\") {\n        return;\n      }\n      const newValue: TupleValue = {\n        ...value,\n        value: [...removeRotateOrSkewValues(\"skew\", value)],\n      };\n      const skew = extractSkewPropertiesFromTransform(value);\n      const { skewX, skewY } = skew;\n\n      if (skewX) {\n        newValue.value.push({\n          ...skewX,\n          hidden: skewX.hidden ? false : true,\n        });\n      }\n\n      if (skewY) {\n        newValue.value.push({\n          ...skewY,\n          hidden: skewY.hidden ? false : true,\n        });\n      }\n\n      setProperty(\"transform\")(newValue);\n      break;\n    }\n  }\n};\n\nconst transformFunctions = [\"rotateX\", \"rotateY\", \"rotateZ\", \"skewX\", \"skewY\"];\n\nexport const updateTransformFunction = (\n  styleDecl: ComputedStyleDecl,\n  name: string,\n  newValue: StyleValue,\n  options?: StyleUpdateOptions\n) => {\n  const tuple =\n    styleDecl.cascadedValue.type === \"tuple\"\n      ? styleDecl.cascadedValue\n      : undefined;\n  if (tuple === undefined) {\n    return;\n  }\n\n  if (newValue.type === \"tuple\") {\n    [newValue] = newValue.value;\n  }\n  if (newValue.type !== \"unit\" && newValue.type !== \"var\") {\n    newValue = { type: \"unit\", value: 0, unit: \"deg\" };\n  }\n\n  const matched = new Map<string, FunctionValue>();\n  for (const item of tuple.value) {\n    if (item.type === \"function\") {\n      matched.set(item.name, item);\n    }\n  }\n  matched.set(name, {\n    type: \"function\",\n    name,\n    args: { type: \"layers\", value: [newValue] },\n  });\n  const newTuple = structuredClone(tuple);\n  // recreate tuple with strictly ordered functions\n  newTuple.value = [];\n  for (const name of transformFunctions) {\n    const functionValue = matched.get(name);\n    if (functionValue) {\n      newTuple.value.push(functionValue);\n    }\n  }\n  setProperty(\"transform\")(newTuple, options);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transforms.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./transforms\";\n\nexport const Transforms = () => (\n  <StorySection title=\"Transforms\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Transforms\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transforms/transforms.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport {\n  CssValueListArrowFocus,\n  CssValueListItem,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n  Flex,\n  Grid,\n  IconButton,\n  Label,\n  SectionTitle,\n  SectionTitleButton,\n  SmallIconButton,\n  SmallToggleButton,\n  theme,\n  Tooltip,\n  FloatingPanel,\n} from \"@webstudio-is/design-system\";\nimport {\n  EyeClosedIcon,\n  PlusIcon,\n  MinusIcon,\n  EyeOpenIcon,\n  EllipsesIcon,\n} from \"@webstudio-is/icons\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { getDots } from \"../../shared/style-section\";\nimport {\n  getPriorityStyleValueSource,\n  PropertyLabel,\n  PropertySectionLabel,\n} from \"../../property-label\";\nimport { useComputedStyleDecl, useComputedStyles } from \"../../shared/model\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\nimport { TextControl } from \"../../controls\";\nimport {\n  addDefaultsForTransormSection,\n  isTransformPanelPropertyUsed,\n  handleDeleteTransformProperty,\n  handleHideTransformProperty,\n  getHumanizedTextFromTransformLayer,\n  transformPanels,\n  type TransformPanel,\n} from \"./transform-utils\";\nimport { TranslatePanelContent } from \"./transform-translate\";\nimport { ScalePanelContent } from \"./transform-scale\";\nimport { RotatePanelContent } from \"./transform-rotate\";\nimport { SkewPanelContent } from \"./transform-skew\";\nimport { TransformAndPerspectiveOrigin } from \"./transform-and-perspective-origin\";\n\nconst label = \"Transforms\";\n\nconst advancedProperties = [\n  \"transform-origin\",\n  \"backface-visibility\",\n  \"perspective\",\n  \"perspective-origin\",\n] satisfies [CssProperty, ...CssProperty[]];\n\nexport const properties = [\n  \"transform\",\n  \"translate\",\n  \"scale\",\n  ...advancedProperties,\n] satisfies [CssProperty, ...CssProperty[]];\n\nconst TransformAdvancedButton = forwardRef<\n  ElementRef<\"button\">,\n  ComponentProps<\"button\">\n>((props, ref) => {\n  const styles = useComputedStyles(advancedProperties);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  return (\n    <Tooltip content=\"Advanced transform options\">\n      <IconButton\n        {...props}\n        ref={ref}\n        variant={styleValueSourceColor}\n        onClick={(event) => {\n          if (event.altKey) {\n            const batch = createBatchUpdate();\n            for (const property of advancedProperties) {\n              batch.deleteProperty(property);\n            }\n            batch.publish();\n            return;\n          }\n          props.onClick?.(event);\n        }}\n      >\n        <EllipsesIcon />\n      </IconButton>\n    </Tooltip>\n  );\n});\n\nconst TransformAdvancedPopover = () => {\n  return (\n    <FloatingPanel\n      title=\"Advanced Transform\"\n      placement=\"bottom-within\"\n      content={\n        <Grid gap=\"2\" css={{ padding: theme.panel.padding }}>\n          <Grid css={{ gridTemplateColumns: `2fr 1fr` }}>\n            <PropertyLabel\n              label=\"Backface Visibility\"\n              description={propertyDescriptions.backfaceVisibility}\n              properties={[\"backface-visibility\"]}\n            />\n            <TextControl property=\"backface-visibility\" />\n          </Grid>\n          <TransformAndPerspectiveOrigin property=\"transform-origin\" />\n          <Grid css={{ gridTemplateColumns: `2fr 1fr` }}>\n            <PropertyLabel\n              label=\"Perspective\"\n              description={propertyDescriptions.perspective}\n              properties={[\"perspective\"]}\n            />\n            <TextControl property=\"perspective\" />\n          </Grid>\n          <TransformAndPerspectiveOrigin property=\"perspective-origin\" />\n        </Grid>\n      }\n    >\n      <TransformAdvancedButton />\n    </FloatingPanel>\n  );\n};\n\nexport const Section = () => {\n  const [isOpen, setIsOpen] = useOpenState(label);\n\n  const styles = useComputedStyles(properties);\n  const isAnyTransformPropertyAdded = transformPanels.some((panel) =>\n    isTransformPanelPropertyUsed({\n      panel,\n      styles,\n    })\n  );\n  const dots = getDots(styles);\n\n  return (\n    <CollapsibleSectionRoot\n      fullWidth\n      label={label}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      trigger={\n        <SectionTitle\n          inactive={dots.length === 0}\n          collapsible={dots.length !== 0}\n          dots={dots}\n          suffix={\n            <Flex gap=\"1\" align=\"center\">\n              <TransformAdvancedPopover />\n              <DropdownMenu>\n                <DropdownMenuTrigger asChild>\n                  <SectionTitleButton\n                    prefix={<PlusIcon />}\n                  ></SectionTitleButton>\n                </DropdownMenuTrigger>\n                <DropdownMenuContent\n                  collisionPadding={16}\n                  css={{ width: theme.spacing[24] }}\n                >\n                  {transformPanels.map((panel) => (\n                    <DropdownMenuItem\n                      disabled={isTransformPanelPropertyUsed({\n                        panel,\n                        styles,\n                      })}\n                      key={panel}\n                      onSelect={() => {\n                        addDefaultsForTransormSection({\n                          panel,\n                          styles,\n                        });\n                        setIsOpen(true);\n                      }}\n                    >\n                      {humanizeString(panel)}\n                    </DropdownMenuItem>\n                  ))}\n                </DropdownMenuContent>\n              </DropdownMenu>\n            </Flex>\n          }\n        >\n          <PropertySectionLabel\n            label={label}\n            description={propertyDescriptions.transform}\n            properties={properties}\n          />\n        </SectionTitle>\n      }\n    >\n      {isAnyTransformPropertyAdded && (\n        <CssValueListArrowFocus>\n          <Flex direction=\"column\">\n            {transformPanels.map(\n              (panel, index) =>\n                isTransformPanelPropertyUsed({\n                  panel,\n                  styles,\n                }) && (\n                  <TransformSection key={panel} index={index} panel={panel} />\n                )\n            )}\n          </Flex>\n        </CssValueListArrowFocus>\n      )}\n    </CollapsibleSectionRoot>\n  );\n};\n\nconst TransformSection = ({\n  panel,\n  index,\n}: {\n  index: number;\n  panel: TransformPanel;\n}) => {\n  const property = panel === \"rotate\" || panel === \"skew\" ? \"transform\" : panel;\n  const styleDecl = useComputedStyleDecl(property);\n  const values = getHumanizedTextFromTransformLayer(\n    panel,\n    styleDecl.cascadedValue\n  );\n  if (values === undefined) {\n    return;\n  }\n  const { value, label } = values;\n\n  return (\n    <FloatingPanel\n      title={humanizeString(panel)}\n      content={\n        <Flex direction=\"column\" css={{ padding: theme.panel.padding }}>\n          {panel === \"translate\" && <TranslatePanelContent />}\n          {panel === \"scale\" && <ScalePanelContent />}\n          {panel === \"rotate\" && <RotatePanelContent />}\n          {panel === \"skew\" && <SkewPanelContent />}\n        </Flex>\n      }\n    >\n      <CssValueListItem\n        id={panel}\n        index={index}\n        hidden={value.hidden}\n        label={<Label truncate>{label}</Label>}\n        buttons={\n          <>\n            <SmallToggleButton\n              variant=\"normal\"\n              pressed={value.hidden}\n              tabIndex={-1}\n              onPressedChange={() =>\n                handleHideTransformProperty({\n                  panel,\n                  value: styleDecl.cascadedValue,\n                })\n              }\n              icon={value.hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}\n            />\n            <SmallIconButton\n              variant=\"destructive\"\n              tabIndex={-1}\n              icon={<MinusIcon />}\n              onClick={() =>\n                handleDeleteTransformProperty({\n                  panel,\n                  value: styleDecl.cascadedValue,\n                })\n              }\n            />\n          </>\n        }\n      ></CssValueListItem>\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transitions/transition-content.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  toValue,\n  type InvalidValue,\n  type StyleValue,\n  type TupleValueItem,\n} from \"@webstudio-is/css-engine\";\nimport {\n  Flex,\n  Label,\n  TextArea,\n  theme,\n  textVariants,\n  Separator,\n  Tooltip,\n  Text,\n  Grid,\n} from \"@webstudio-is/design-system\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport { propertiesData, propertyDescriptions } from \"@webstudio-is/css-data\";\nimport { type IntermediateStyleValue } from \"../../shared/css-value-input\";\nimport { CssValueInputContainer } from \"../../shared/css-value-input\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\nimport { PropertyInlineLabel } from \"../../property-label\";\nimport { TransitionProperty } from \"./transition-property\";\nimport {\n  $availableVariables,\n  $availableUnitVariables,\n  useComputedStyles,\n} from \"../../shared/model\";\nimport {\n  editRepeatedStyleItem,\n  setRepeatedStyleItem,\n} from \"../../shared/repeated-style\";\n\nconst getLayer = (value: undefined | StyleValue, index: number) =>\n  value?.type === \"layers\" ? value.value[index] : undefined;\n\nexport const TransitionContent = ({ index }: { index: number }) => {\n  const styles = useComputedStyles([\n    \"transition-property\",\n    \"transition-duration\",\n    \"transition-timing-function\",\n    \"transition-delay\",\n    \"transition-behavior\",\n  ]);\n  const [\n    transitionProperty,\n    transitionDuration,\n    transitionTimingFunction,\n    transitionDelay,\n    transitionBehavior,\n  ] = styles;\n\n  const property = getLayer(transitionProperty.cascadedValue, index);\n  const duration = getLayer(transitionDuration.cascadedValue, index);\n  const timingFunction = getLayer(\n    transitionTimingFunction.cascadedValue,\n    index\n  );\n  const delay = getLayer(transitionDelay.cascadedValue, index);\n  const behavior = getLayer(transitionBehavior.cascadedValue, index);\n\n  const [intermediateValue, setIntermediateValue] = useState<\n    IntermediateStyleValue | InvalidValue | undefined\n  >(() => ({\n    type: \"intermediate\",\n    value: toValue({\n      type: \"tuple\",\n      value: [property, duration, timingFunction, delay, behavior].filter(\n        Boolean\n      ) as TupleValueItem[],\n    }),\n  }));\n\n  const handleChange = (value: string) => {\n    setIntermediateValue({\n      type: \"intermediate\",\n      value,\n    });\n  };\n\n  const handleComplete = () => {\n    if (intermediateValue === undefined) {\n      return;\n    }\n    editRepeatedStyleItem(\n      styles,\n      index,\n      parseCssFragment(intermediateValue.value, [\"transition\"])\n    );\n  };\n\n  const updateIntermediateValue = (params: {\n    property?: StyleValue;\n    timing?: StyleValue;\n    delay?: StyleValue;\n    duration?: StyleValue;\n  }) => {\n    const shorthand = toValue({\n      type: \"tuple\",\n      value: [\n        params.property ?? property,\n        params.duration ?? duration,\n        params.delay ?? delay,\n        params.timing ?? timingFunction,\n      ].filter((item): item is TupleValueItem => item !== undefined),\n    });\n    setIntermediateValue({\n      type: \"intermediate\",\n      value: shorthand,\n    });\n  };\n\n  return (\n    <Flex direction=\"column\">\n      <Grid\n        gap=\"2\"\n        css={{\n          padding: theme.panel.padding,\n          gridTemplateColumns: `1fr ${theme.spacing[23]}`,\n          gridTemplateRows: theme.spacing[13],\n        }}\n      >\n        <PropertyInlineLabel\n          label=\"Property\"\n          description={propertyDescriptions.transitionProperty}\n          properties={[\"transition-property\"]}\n        />\n        <TransitionProperty\n          value={property ?? propertiesData[\"transition-property\"].initial}\n          onChange={(value) => {\n            updateIntermediateValue({ property: value });\n            setRepeatedStyleItem(transitionProperty, index, value);\n          }}\n        />\n\n        <PropertyInlineLabel\n          label=\"Duration\"\n          description={propertyDescriptions.transitionDuration}\n          properties={[\"transition-duration\"]}\n        />\n        <CssValueInputContainer\n          property=\"transition-duration\"\n          styleSource=\"local\"\n          getOptions={() => $availableUnitVariables.get()}\n          value={duration ?? propertiesData[\"transition-duration\"].initial}\n          onDelete={() => {}}\n          onUpdate={(value, options) => {\n            if (value === undefined) {\n              return;\n            }\n            if (value.type === \"layers\") {\n              [value] = value.value;\n            }\n            if (value.type === \"unit\" || value.type === \"var\") {\n              updateIntermediateValue({ duration: value });\n              setRepeatedStyleItem(transitionDuration, index, value, options);\n            }\n          }}\n        />\n\n        <PropertyInlineLabel\n          label=\"Delay\"\n          description={propertyDescriptions.transitionDelay}\n          properties={[\"transition-delay\"]}\n        />\n        <CssValueInputContainer\n          property=\"transition-delay\"\n          styleSource=\"local\"\n          getOptions={() => $availableUnitVariables.get()}\n          value={delay ?? propertiesData[\"transition-delay\"].initial}\n          onDelete={() => {}}\n          onUpdate={(value, options) => {\n            if (value === undefined) {\n              return;\n            }\n            if (value.type === \"layers\") {\n              [value] = value.value;\n            }\n            if (value.type === \"unit\" || value.type === \"var\") {\n              updateIntermediateValue({ delay: value });\n              setRepeatedStyleItem(transitionDelay, index, value, options);\n            }\n          }}\n        />\n\n        <PropertyInlineLabel\n          label=\"Easing\"\n          description={propertyDescriptions.transitionTimingFunction}\n          properties={[\"transition-timing-function\"]}\n        />\n        <CssValueInputContainer\n          property=\"transition-timing-function\"\n          styleSource=\"local\"\n          getOptions={() => [\n            { type: \"keyword\", value: \"linear\" },\n            { type: \"keyword\", value: \"ease\" },\n            { type: \"keyword\", value: \"ease-in\" },\n            { type: \"keyword\", value: \"ease-out\" },\n            { type: \"keyword\", value: \"ease-in-out\" },\n            { type: \"keyword\", value: \"step-start\" },\n            { type: \"keyword\", value: \"step-end\" },\n            ...$availableVariables.get(),\n          ]}\n          value={\n            timingFunction ??\n            propertiesData[\"transition-timing-function\"].initial\n          }\n          onDelete={() => {}}\n          onUpdate={(value, options) => {\n            if (value === undefined) {\n              return;\n            }\n            if (value.type === \"layers\") {\n              [value] = value.value;\n            }\n            if (value.type === \"keyword\" || value.type === \"var\") {\n              updateIntermediateValue({ timing: value });\n              setRepeatedStyleItem(\n                transitionTimingFunction,\n                index,\n                value,\n                options\n              );\n            }\n          }}\n        />\n      </Grid>\n\n      <Separator css={{ gridColumn: \"span 2\" }} />\n      <Flex\n        direction=\"column\"\n        css={{\n          padding: theme.panel.padding,\n          gap: theme.spacing[3],\n          minWidth: theme.spacing[30],\n        }}\n      >\n        <Label>\n          <Flex align=\"center\" gap=\"1\">\n            Code\n            <Tooltip\n              variant=\"wrapped\"\n              content={\n                <Text>\n                  Paste CSS code for a transition or part of a transition, for\n                  example:\n                  <br />\n                  <br />\n                  <Text variant=\"monoBold\">opacity 200ms ease 0s</Text>\n                </Text>\n              }\n            >\n              <InfoCircleIcon />\n            </Tooltip>\n          </Flex>\n        </Label>\n        <TextArea\n          rows={3}\n          name=\"description\"\n          css={{ minHeight: theme.spacing[14], ...textVariants.mono }}\n          color={intermediateValue?.type === \"invalid\" ? \"error\" : undefined}\n          value={intermediateValue?.value ?? \"\"}\n          onChange={handleChange}\n          onBlur={handleComplete}\n          onKeyDown={(event) => {\n            event.stopPropagation();\n\n            if (event.key === \"Enter\") {\n              handleComplete();\n              event.preventDefault();\n            }\n          }}\n        />\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transitions/transition-property.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { matchSorter } from \"match-sorter\";\nimport { animatableProperties } from \"@webstudio-is/css-data\";\nimport {\n  InputField,\n  ComboboxRoot,\n  ComboboxAnchor,\n  useCombobox,\n  ComboboxContent,\n  ComboboxLabel,\n  ComboboxListbox,\n  ComboboxListboxItem,\n  ComboboxSeparator,\n  NestedInputButton,\n  ComboboxScrollArea,\n} from \"@webstudio-is/design-system\";\nimport {\n  toValue,\n  type KeywordValue,\n  type StyleValue,\n  type UnparsedValue,\n} from \"@webstudio-is/css-engine\";\nimport { setUnion } from \"~/shared/shim\";\nimport { $computedStyleDeclarations } from \"../../shared/model\";\n\ntype AnimatableProperty = (typeof animatableProperties)[number];\n\nconst commonTransitionProperties = [\n  \"all\",\n  \"opacity\",\n  \"margin\",\n  \"padding\",\n  \"border\",\n  \"transform\",\n  \"filter\",\n  \"flex\",\n  \"background-color\",\n];\n\nconst isAnimatableProperty = (\n  property: string\n): property is AnimatableProperty => {\n  if (property === \"all\") {\n    return true;\n  }\n\n  return [...commonTransitionProperties, ...animatableProperties].some(\n    (item) => item === property\n  );\n};\n\ntype AnimatableProperties = (typeof animatableProperties)[number];\ntype NameAndLabel = { name: string; label?: string };\ntype TransitionPropertyProps = {\n  value: StyleValue;\n  onChange: (value: KeywordValue | UnparsedValue) => void;\n};\n\nconst commonPropertiesSet = new Set(commonTransitionProperties);\n\n/**\n * animatable and inherited properties\n * on current breakpoints across all states\n */\nconst $animatableDefinedProperties = computed(\n  [$computedStyleDeclarations],\n  (computedStyleDeclarations) => {\n    const animatableProperties = new Set<string>();\n    for (const { property } of computedStyleDeclarations) {\n      if (isAnimatableProperty(property)) {\n        animatableProperties.add(property);\n      }\n    }\n    return animatableProperties;\n  }\n);\n\nexport const TransitionProperty = ({\n  value,\n  onChange,\n}: TransitionPropertyProps) => {\n  const animatableDefinedProperties = useStore($animatableDefinedProperties);\n  const valueString = toValue(value);\n  const [inputValue, setInputValue] = useState<string>(valueString);\n  useEffect(() => setInputValue(valueString), [valueString]);\n\n  const properties = Array.from(\n    setUnion(\n      setUnion(animatableDefinedProperties, commonPropertiesSet),\n      new Set(animatableProperties)\n    )\n  );\n\n  const {\n    items,\n    isOpen,\n    getComboboxProps,\n    getToggleButtonProps,\n    getInputProps,\n    getMenuProps,\n    getItemProps,\n  } = useCombobox<NameAndLabel>({\n    getItems: () =>\n      properties.map((prop) => ({\n        name: prop,\n        label: prop === \"transform\" ? `${prop} (rotate, skew)` : prop,\n      })),\n    value: { name: inputValue as AnimatableProperties, label: inputValue },\n    selectedItem: undefined,\n    itemToString: (value) => value?.label || \"\",\n    onItemSelect: (prop) => saveAnimatableProperty(prop.name),\n    onChange: (value) => setInputValue(value ?? \"\"),\n\n    // We are splitting the items into two lists.\n    // But when users pass a input, the list is filtered and mixed together.\n    // The UI is still showing the lists as separated. But the items are mixed together in background.\n    // Since, first we show the properties on instance and then common-properties\n    // followed by filtered-properties. We can use matchSorter to sort the items.\n\n    match: (search, itemsToFilter, itemToString) => {\n      if (search === \"\") {\n        return itemsToFilter;\n      }\n\n      const sortedItems = matchSorter(itemsToFilter, search, {\n        keys: [itemToString],\n        sorter: (rankedItems) =>\n          rankedItems.sort((a, b) => {\n            // Keep the proeprties on instance at the top\n            if (animatableDefinedProperties.has(a.item.name)) {\n              return -1;\n            }\n            if (animatableDefinedProperties.has(b.item.name)) {\n              return 1;\n            }\n\n            // Keep the common properties at the top as well\n            if (commonPropertiesSet.has(a.item.name)) {\n              return -1;\n            }\n            if (commonPropertiesSet.has(b.item.name)) {\n              return 1;\n            }\n\n            // Maintain original rank if neither is prioritized\n            return a.rank - b.rank;\n          }),\n      });\n\n      return sortedItems;\n    },\n  });\n\n  const commonProperties = items.filter(\n    (item) =>\n      commonPropertiesSet.has(item.name) === true &&\n      animatableDefinedProperties.has(item.name) === false\n  );\n  const filteredProperties = items.filter(\n    (item) =>\n      commonPropertiesSet.has(item.name) === false &&\n      animatableDefinedProperties.has(item.name) === false\n  );\n  const propertiesDefinedOnInstance: Array<NameAndLabel> = items.filter(\n    (item) => animatableDefinedProperties.has(item.name)\n  );\n\n  const saveAnimatableProperty = (propertyName: string) => {\n    if (isAnimatableProperty(propertyName) === false) {\n      return;\n    }\n    setInputValue(propertyName);\n    onChange({ type: \"unparsed\", value: propertyName });\n  };\n\n  const renderItem = (item: NameAndLabel, index: number) => {\n    return (\n      <ComboboxListboxItem\n        {...getItemProps({\n          item,\n          index,\n        })}\n        key={item.name}\n        selected={item.name === inputValue}\n      >\n        {item?.label ?? \"\"}\n      </ComboboxListboxItem>\n    );\n  };\n\n  return (\n    <ComboboxRoot open={isOpen}>\n      <div {...getComboboxProps()}>\n        <ComboboxAnchor>\n          <InputField\n            autoFocus\n            {...getInputProps({\n              onKeyDown: (event) => {\n                if (event.key === \"Enter\") {\n                  saveAnimatableProperty(inputValue);\n                }\n                event.stopPropagation();\n              },\n            })}\n            placeholder=\"all\"\n            suffix={<NestedInputButton {...getToggleButtonProps()} />}\n          />\n        </ComboboxAnchor>\n        <ComboboxContent align=\"end\" sideOffset={5}>\n          <ComboboxListbox {...getMenuProps()}>\n            <ComboboxScrollArea>\n              {isOpen && (\n                <>\n                  {propertiesDefinedOnInstance.length > 0 && (\n                    <>\n                      <ComboboxLabel>Defined</ComboboxLabel>\n                      {propertiesDefinedOnInstance.map((property, index) =>\n                        renderItem(property, index)\n                      )}\n                      <ComboboxSeparator />\n                    </>\n                  )}\n\n                  <ComboboxLabel>Common</ComboboxLabel>\n                  {commonProperties.map((property, index) =>\n                    renderItem(\n                      property,\n                      propertiesDefinedOnInstance.length + index\n                    )\n                  )}\n                  <ComboboxSeparator />\n\n                  {filteredProperties.map((property, index) =>\n                    renderItem(\n                      property,\n                      propertiesDefinedOnInstance.length +\n                        commonProperties.length +\n                        index\n                    )\n                  )}\n                </>\n              )}\n            </ComboboxScrollArea>\n          </ComboboxListbox>\n        </ComboboxContent>\n      </div>\n    </ComboboxRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transitions/transitions.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { getStyleDeclKey, StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $pages,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { Section } from \"./transitions\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\n\nconst transitionProperty: StyleDecl = {\n  breakpointId: \"base\",\n  styleSourceId: \"local\",\n  property: \"transitionProperty\",\n  value: {\n    type: \"layers\",\n    value: [\n      { type: \"unparsed\", value: \"opacity\" },\n      { type: \"unparsed\", value: \"transform\" },\n      { type: \"keyword\", value: \"all\" },\n    ],\n  },\n};\n\nregisterContainers();\n$breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n$selectedBreakpointId.set(\"base\");\n$styleSources.set(\n  new Map([\n    [\n      \"local\",\n      {\n        id: \"local\",\n        type: \"local\",\n      },\n    ],\n  ])\n);\n$styles.set(\n  new Map([[getStyleDeclKey(transitionProperty), transitionProperty]])\n);\n$styleSourceSelections.set(\n  new Map([[\"box\", { instanceId: \"box\", values: [\"local\"] }]])\n);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"box\",\n  })\n);\n$awareness.set({\n  pageId: \"homePageId\",\n  instanceSelector: [\"box\"],\n});\n\nexport const Transitions = () => (\n  <StorySection title=\"Transitions\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Transitions\",\n  component: Transitions,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/transitions/transitions.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { PlusIcon } from \"@webstudio-is/icons\";\nimport {\n  SectionTitle,\n  SectionTitleButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { propertiesData } from \"@webstudio-is/css-data\";\nimport {\n  toValue,\n  type CssProperty,\n  type LayerValueItem,\n} from \"@webstudio-is/css-engine\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport { $selectedOrLastStyleSourceSelector } from \"~/shared/nano-states\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { repeatUntil } from \"~/shared/array-utils\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { getDots } from \"../../shared/style-section\";\nimport {\n  addRepeatedStyleItem,\n  RepeatedStyle,\n} from \"../../shared/repeated-style\";\nimport { PropertySectionLabel } from \"../../property-label\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { TransitionContent } from \"./transition-content\";\nimport { parseCssFragment } from \"../../shared/css-fragment\";\n\nconst transitionLongHandProperties = [\n  \"transition-property\",\n  \"transition-timing-function\",\n  \"transition-delay\",\n  \"transition-duration\",\n  \"transition-behavior\",\n] as const satisfies CssProperty[];\n\nexport { transitionLongHandProperties as properties };\n\nconst label = \"Transitions\";\n\nconst getTransitionLayers = (\n  styles: ComputedStyleDecl[],\n  property: (typeof transitionLongHandProperties)[number]\n) => {\n  const transitionPropertyValue = styles[0].cascadedValue;\n  const currentPropertyValue = styles.find(\n    (styleDecl) => styleDecl.property === property\n  )?.cascadedValue;\n  const transitionPropertiesCount =\n    transitionPropertyValue.type === \"layers\"\n      ? transitionPropertyValue.value.length\n      : 0;\n  const definedLayers: LayerValueItem[] =\n    currentPropertyValue?.type === \"layers\"\n      ? currentPropertyValue.value\n      : [propertiesData[property].initial as LayerValueItem];\n  return repeatUntil(definedLayers, transitionPropertiesCount);\n};\n\nconst getLayerLabel = ({\n  styles,\n  index,\n}: {\n  styles: ComputedStyleDecl[];\n  index: number;\n}) => {\n  // show label without hidden replacement\n  const propertyLayer = getTransitionLayers(styles, \"transition-property\")[\n    index\n  ];\n  const property = humanizeString(toValue({ ...propertyLayer, hidden: false }));\n  const duration = toValue(\n    getTransitionLayers(styles, \"transition-duration\")[index]\n  );\n  const timingFunctionLayer = getTransitionLayers(\n    styles,\n    \"transition-timing-function\"\n  )[index];\n  const timingFunction = toValue({ ...timingFunctionLayer, hidden: false });\n  const delay = toValue(getTransitionLayers(styles, \"transition-delay\")[index]);\n  return `${property}: ${duration} ${timingFunction} ${delay}`;\n};\n\nexport const Section = () => {\n  const [isOpen, setIsOpen] = useOpenState(label);\n\n  const selectedOrLastStyleSourceSelector = useStore(\n    $selectedOrLastStyleSourceSelector\n  );\n\n  const isStyleInLocalState =\n    selectedOrLastStyleSourceSelector &&\n    selectedOrLastStyleSourceSelector.state === undefined;\n  const styles = useComputedStyles(transitionLongHandProperties);\n  const dots = getDots(styles);\n\n  return (\n    <CollapsibleSectionRoot\n      fullWidth\n      label={label}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      trigger={\n        <SectionTitle\n          inactive={dots.length === 0}\n          collapsible={dots.length !== 0}\n          dots={dots}\n          suffix={\n            <Tooltip\n              content={\n                isStyleInLocalState === false\n                  ? \"Transitions can only be added in local state\"\n                  : \"Add a transition\"\n              }\n            >\n              <SectionTitleButton\n                disabled={isStyleInLocalState === false}\n                prefix={<PlusIcon />}\n                onClick={() => {\n                  setIsOpen(true);\n                  addRepeatedStyleItem(\n                    styles,\n                    parseCssFragment(\"all 200ms ease 0ms normal\", [\n                      \"transition\",\n                    ])\n                  );\n                }}\n              />\n            </Tooltip>\n          }\n        >\n          <PropertySectionLabel\n            label={label}\n            description=\"Animate the transition between states on this instance.\"\n            properties={transitionLongHandProperties}\n          />\n        </SectionTitle>\n      }\n    >\n      <RepeatedStyle\n        label={label}\n        styles={styles}\n        getItemProps={(index) => ({\n          label: getLayerLabel({ styles, index }),\n        })}\n        renderItemContent={(index) => <TransitionContent index={index} />}\n      />\n    </CollapsibleSectionRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/typography/typography.stories.tsx",
    "content": "import { Box, StorySection, theme } from \"@webstudio-is/design-system\";\nimport { Section } from \"./typography\";\n\nexport const Typography = () => (\n  <StorySection title=\"Typography\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Section />\n    </Box>\n  </StorySection>\n);\n\nexport default {\n  title: \"Style panel/Typography\",\n  component: Section,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/sections/typography/typography.tsx",
    "content": "import { forwardRef, type ComponentProps } from \"react\";\nimport {\n  Flex,\n  Grid,\n  EnhancedTooltip,\n  theme,\n  IconButton,\n  Box,\n  FloatingPanel,\n} from \"@webstudio-is/design-system\";\nimport { propertyDescriptions } from \"@webstudio-is/css-data\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  XSmallIcon,\n  EllipsesIcon,\n  ArrowRightIcon,\n  ArrowLeftIcon,\n  TextAlignCenterIcon,\n  TextAlignJustifyIcon,\n  TextAlignLeftIcon,\n  TextAlignRightIcon,\n  TextCapitalizeIcon,\n  MinusIcon,\n  TextItalicIcon,\n  TextLowercaseIcon,\n  TextStrikethroughIcon,\n  TextTruncateIcon,\n  TextUnderlineIcon,\n  TextUppercaseIcon,\n} from \"@webstudio-is/icons\";\nimport { ToggleGroupControl } from \"../../controls/toggle-group/toggle-group-control\";\nimport {\n  ColorControl,\n  FontFamilyControl,\n  FontWeightControl,\n  SelectControl,\n  TextControl,\n} from \"../../controls\";\nimport { StyleSection } from \"../../shared/style-section\";\nimport {\n  getPriorityStyleValueSource,\n  PropertyLabel,\n} from \"../../property-label\";\nimport { useComputedStyles } from \"../../shared/model\";\nimport { createBatchUpdate } from \"../../shared/use-style-data\";\n\nconst advancedProperties: CssProperty[] = [\n  \"white-space-collapse\",\n  \"text-wrap-mode\",\n  \"text-wrap-style\",\n  \"direction\",\n  \"hyphens\",\n  \"text-overflow\",\n];\n\nexport const properties = [\n  \"font-family\",\n  \"font-weight\",\n  \"font-size\",\n  \"line-height\",\n  \"color\",\n  \"text-align\",\n  \"font-style\",\n  \"text-decoration-line\",\n  \"letter-spacing\",\n  \"text-transform\",\n  ...advancedProperties,\n] satisfies CssProperty[];\n\nexport const Section = () => {\n  return (\n    <StyleSection label=\"Typography\" properties={properties}>\n      <Flex gap=\"2\" direction=\"column\">\n        <TypographySectionFont />\n        <TypographySectionSizing />\n        <TypographySectionAdvanced />\n      </Flex>\n    </StyleSection>\n  );\n};\n\nconst TypographySectionFont = () => {\n  return (\n    <Grid css={{ gridTemplateColumns: \"4fr 6fr\" }} gap={2}>\n      <PropertyLabel\n        label=\"Family\"\n        description={propertyDescriptions.fontFamily}\n        properties={[\"font-family\"]}\n      />\n      <FontFamilyControl />\n      <PropertyLabel\n        label=\"Weight\"\n        description={propertyDescriptions.fontWeight}\n        properties={[\"font-weight\"]}\n      />\n      <FontWeightControl />\n      <PropertyLabel\n        label=\"Color\"\n        description={propertyDescriptions.color}\n        properties={[\"color\"]}\n      />\n      <ColorControl property=\"color\" />\n    </Grid>\n  );\n};\n\nconst TypographySectionSizing = () => {\n  return (\n    <Grid gap=\"2\" css={{ gridTemplateColumns: \"1fr 1fr 1fr\" }}>\n      <Grid gap=\"1\">\n        <PropertyLabel\n          label=\"Size\"\n          description={propertyDescriptions.fontSize}\n          properties={[\"font-size\"]}\n        />\n        <TextControl property=\"font-size\" />\n      </Grid>\n      <Grid gap=\"1\">\n        <PropertyLabel\n          label=\"Height\"\n          description={propertyDescriptions.lineHeight}\n          properties={[\"line-height\"]}\n        />\n        <TextControl property=\"line-height\" />\n      </Grid>\n      <Grid gap=\"1\">\n        <PropertyLabel\n          label=\"Spacing\"\n          description={propertyDescriptions.letterSpacing}\n          properties={[\"letter-spacing\"]}\n        />\n        <TextControl property=\"letter-spacing\" />\n      </Grid>\n    </Grid>\n  );\n};\n\nconst TypographySectionAdvanced = () => {\n  return (\n    <Grid gap=\"2\" columns=\"2\">\n      <ToggleGroupControl\n        properties={[\"text-align\"]}\n        items={[\n          {\n            child: <TextAlignLeftIcon />,\n            description: \"Aligns the text based on the writing direction.\",\n            value: \"start\",\n          },\n          {\n            child: <TextAlignCenterIcon />,\n            description: \"Centers the text horizontally within its container.\",\n            value: \"center\",\n          },\n          {\n            child: <TextAlignRightIcon />,\n            description: \"Aligns the text based on the writing direction.\",\n            value: \"end\",\n          },\n          {\n            child: <TextAlignJustifyIcon />,\n            description:\n              \"Adjusts word spacing to align text to both the left and right edges of the container\",\n            value: \"justify\",\n          },\n        ]}\n      />\n      <ToggleGroupControl\n        properties={[\"text-decoration-line\"]}\n        items={[\n          {\n            child: <XSmallIcon />,\n            description: \"No decoration is applied to the text.\",\n            value: \"none\",\n          },\n          {\n            child: <TextUnderlineIcon />,\n            description: \"Adds a horizontal line underneath the text.\",\n            value: \"underline\",\n          },\n          {\n            child: <TextStrikethroughIcon />,\n            description:\n              \"Draws a horizontal line through the middle of the text.\",\n            value: \"line-through\",\n          },\n        ]}\n      />\n      <ToggleGroupControl\n        properties={[\"text-transform\"]}\n        items={[\n          {\n            child: <XSmallIcon />,\n            description:\n              \"No transformation is applied to the text. The text appears as it is.\",\n            value: \"none\",\n          },\n          {\n            child: <TextUppercaseIcon />,\n            description:\n              \"Transforms the text to appear in all uppercase letters.\",\n            value: \"uppercase\",\n          },\n          {\n            child: <TextCapitalizeIcon />,\n            description:\n              \"Transforms the first character of each word to uppercase, while the remaining characters are in lowercase.\",\n            value: \"capitalize\",\n          },\n          {\n            child: <TextLowercaseIcon />,\n            description:\n              \" Transforms the text to appear in all lowercase letters.\",\n            value: \"lowercase\",\n          },\n        ]}\n      />\n      <Grid align=\"end\" gap=\"1\" css={{ gridTemplateColumns: \"3fr 1fr\" }}>\n        <ToggleGroupControl\n          properties={[\"font-style\"]}\n          items={[\n            {\n              child: <XSmallIcon />,\n              description:\n                \"The default value. The text appears in a normal, upright style.\",\n              value: \"normal\",\n            },\n            {\n              child: <TextItalicIcon />,\n              description:\n                \"The text appears in italic style, where it is slanted to the right.\",\n              value: \"italic\",\n            },\n          ]}\n        />\n        <TypographySectionAdvancedPopover />\n      </Grid>\n    </Grid>\n  );\n};\n\nconst AdvancedOptionsButton = forwardRef<\n  HTMLButtonElement,\n  ComponentProps<typeof IconButton> & {\n    /** https://www.radix-ui.com/docs/primitives/components/collapsible#trigger */\n    \"data-state\"?: \"open\" | \"closed\";\n  }\n>(({ onClick, ...rest }, ref) => {\n  const styles = useComputedStyles(advancedProperties);\n  const styleValueSourceColor = getPriorityStyleValueSource(styles);\n  return (\n    <Flex>\n      <EnhancedTooltip content=\"More typography options\">\n        <IconButton\n          {...rest}\n          onClick={(event) => {\n            if (event.altKey) {\n              const batch = createBatchUpdate();\n              for (const property of advancedProperties) {\n                batch.deleteProperty(property);\n              }\n              batch.publish();\n              return;\n            }\n            onClick?.(event);\n          }}\n          variant={styleValueSourceColor}\n          ref={ref}\n        >\n          <EllipsesIcon />\n        </IconButton>\n      </EnhancedTooltip>\n    </Flex>\n  );\n});\nAdvancedOptionsButton.displayName = \"AdvancedOptionsButton\";\n\nconst TypographySectionAdvancedPopover = () => {\n  return (\n    <FloatingPanel\n      title=\"Advanced Typography\"\n      placement=\"bottom-within\"\n      content={\n        <Grid\n          css={{\n            padding: theme.panel.padding,\n            gap: theme.spacing[9],\n            width: theme.spacing[30],\n          }}\n        >\n          <Grid css={{ gridTemplateColumns: \"5fr 5fr\" }} gap={2}>\n            <PropertyLabel\n              label=\"White Space Collapse\"\n              description={propertyDescriptions.whiteSpaceCollapse}\n              properties={[\"white-space-collapse\"]}\n            />\n            <SelectControl property=\"white-space-collapse\" />\n            <PropertyLabel\n              label=\"Text Wrap Mode\"\n              description={propertyDescriptions.textWrapMode}\n              properties={[\"text-wrap-mode\"]}\n            />\n            <SelectControl property=\"text-wrap-mode\" />\n            <PropertyLabel\n              label=\"Text Wrap Style\"\n              description={propertyDescriptions.textWrapStyle}\n              properties={[\"text-wrap-style\"]}\n            />\n            <SelectControl property=\"text-wrap-style\" />\n            <PropertyLabel\n              label=\"Direction\"\n              description={propertyDescriptions.direction}\n              properties={[\"direction\"]}\n            />\n            <Box css={{ justifySelf: \"end\" }}>\n              <ToggleGroupControl\n                properties={[\"direction\"]}\n                items={[\n                  {\n                    child: <ArrowRightIcon />,\n                    description:\n                      \"Sets the text direction to left-to-right, which is the default for most languages.\",\n                    value: \"ltr\",\n                  },\n                  {\n                    child: <ArrowLeftIcon />,\n                    description:\n                      \"Sets the text direction to right-to-left, typically used for languages such as Arabic or Hebrew.\",\n                    value: \"rtl\",\n                  },\n                ]}\n              />\n            </Box>\n            <PropertyLabel\n              label=\"Hyphens\"\n              description={propertyDescriptions.hyphens}\n              properties={[\"hyphens\"]}\n            />\n            <Box css={{ justifySelf: \"end\" }}>\n              <ToggleGroupControl\n                properties={[\"hyphens\"]}\n                items={[\n                  {\n                    child: <XSmallIcon />,\n                    description:\n                      \"Disables hyphenation of words. Words will not be hyphenated even if they exceed the width of their container.\",\n                    value: \"manual\",\n                  },\n                  {\n                    child: <MinusIcon />,\n                    description:\n                      \"Enables automatic hyphenation of words. The browser will hyphenate long words at appropriate points to fit within the width of their container.\",\n                    value: \"auto\",\n                  },\n                ]}\n              />\n            </Box>\n            <PropertyLabel\n              label=\"Text Overflow\"\n              description={propertyDescriptions.textOverflow}\n              properties={[\"text-overflow\"]}\n            />\n            <Box css={{ justifySelf: \"end\" }}>\n              <ToggleGroupControl\n                properties={[\"text-overflow\"]}\n                items={[\n                  {\n                    child: <XSmallIcon />,\n                    description:\n                      \"The overflowing text is clipped and hidden without any indication.\",\n                    value: \"clip\",\n                  },\n                  {\n                    child: <TextTruncateIcon />,\n                    description:\n                      \"The overflowing text is truncated with an ellipsis (...) to indicate that there is more content. To make the text-overflow: ellipsis property work, you need to set the following CSS properties: text-wrap-mode: nowrap; overflow: hidden;\",\n                    value: \"ellipsis\",\n                  },\n                ]}\n              />\n            </Box>\n          </Grid>\n        </Grid>\n      }\n    >\n      <AdvancedOptionsButton />\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/color-picker.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  toValue,\n  type StyleValue,\n  type KeywordValue,\n  type VarValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport { Box, ColorPickerPopover } from \"@webstudio-is/design-system\";\nimport { CssValueInput } from \"./css-value-input\";\nimport type { IntermediateStyleValue } from \"./css-value-input/css-value-input\";\n\ntype ColorPickerProps = {\n  onChange: (value: StyleValue) => void;\n  onChangeComplete: (value: StyleValue) => void;\n  onReset: () => void;\n  onAbort: () => void;\n  value: StyleValue;\n  currentColor: StyleValue;\n  getOptions?: () => Array<KeywordValue | VarValue>;\n  property: CssProperty;\n  disabled?: boolean;\n};\n\nexport const ColorPickerControl = ({\n  value,\n  currentColor,\n  getOptions,\n  property,\n  disabled,\n  onChange,\n  onChangeComplete,\n  onAbort,\n  onReset,\n}: ColorPickerProps) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  return (\n    <CssValueInput\n      aria-disabled={disabled}\n      styleSource=\"default\"\n      prefix={\n        <Box css={{ paddingLeft: 2 }}>\n          <ColorPickerPopover\n            value={currentColor}\n            onChange={(styleValue) => {\n              setIntermediateValue(styleValue);\n              if (styleValue) {\n                onChange(styleValue);\n              } else {\n                onAbort();\n              }\n            }}\n            onChangeComplete={(value) => {\n              setIntermediateValue(undefined);\n              onChangeComplete(value);\n            }}\n          />\n        </Box>\n      }\n      showSuffix={false}\n      property={property}\n      value={value}\n      intermediateValue={intermediateValue}\n      getOptions={getOptions}\n      onChange={(styleValue) => {\n        if (styleValue === undefined) {\n          setIntermediateValue(styleValue);\n          onAbort();\n          return;\n        }\n        if (styleValue.type === \"intermediate\") {\n          setIntermediateValue(styleValue);\n          return;\n        }\n        if (\n          styleValue.type === \"rgb\" ||\n          styleValue.type === \"color\" ||\n          styleValue.type === \"keyword\" ||\n          styleValue.type === \"var\" ||\n          styleValue.type === \"invalid\"\n        ) {\n          setIntermediateValue(styleValue);\n          onChange(styleValue);\n          return;\n        }\n\n        setIntermediateValue({\n          type: \"intermediate\",\n          value: toValue(styleValue),\n        });\n      }}\n      onHighlight={(styleValue) => {\n        if (styleValue) {\n          onChange(styleValue);\n        } else {\n          onAbort();\n        }\n      }}\n      onChangeComplete={({ value }) => {\n        if (\n          value.type === \"rgb\" ||\n          value.type === \"color\" ||\n          value.type === \"keyword\" ||\n          value.type === \"var\"\n        ) {\n          setIntermediateValue(undefined);\n          onChangeComplete(value);\n          return;\n        }\n        // In case value is parsed to something wrong\n        const invalidValue: StyleValue = {\n          type: \"invalid\",\n          value: toValue(value),\n        };\n        setIntermediateValue(invalidValue);\n        onChange(invalidValue);\n      }}\n      onAbort={onAbort}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        onReset();\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-fragment.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { parseCssFragment } from \"./css-fragment\";\nimport { parseCssValue } from \"@webstudio-is/css-data\";\nimport { setEnv } from \"@webstudio-is/feature-flags\";\n\nsetEnv(\"*\");\n\ntest(\"parse var()\", () => {\n  const result = new Map([\n    [\"background-image\", parseCssValue(\"background-image\", \"var(--bg)\")],\n  ]);\n  expect(\n    parseCssFragment(\"var(--bg)\", [\"background-image\", \"background\"])\n  ).toEqual(result);\n  expect(\n    parseCssFragment(\"background-image: var(--bg)\", [\n      \"background-image\",\n      \"background\",\n    ])\n  ).toEqual(result);\n});\n\ntest(\"fallback further to valid values\", () => {\n  const result = new Map([\n    [\"background-image\", parseCssValue(\"background-image\", \"none\")],\n    [\"background-position-x\", parseCssValue(\"background-position-x\", \"0%\")],\n    [\"background-position-y\", parseCssValue(\"background-position-y\", \"0%\")],\n    [\"background-size\", parseCssValue(\"background-size\", \"auto auto\")],\n    [\"background-repeat\", parseCssValue(\"background-repeat\", \"repeat\")],\n    [\"background-attachment\", parseCssValue(\"background-attachment\", \"scroll\")],\n    [\"background-origin\", parseCssValue(\"background-origin\", \"padding-box\")],\n    [\"background-clip\", parseCssValue(\"background-clip\", \"border-box\")],\n    [\n      \"background-color\",\n      parseCssValue(\"background-color\", \"rgba(255, 255, 255, 1)\"),\n    ],\n  ]);\n  expect(parseCssFragment(\"#fff\", [\"background-image\", \"background\"])).toEqual(\n    result\n  );\n});\n\ntest(\"parse shorthand property\", () => {\n  const result = new Map([\n    [\"transition-property\", parseCssValue(\"transition-property\", \"opacity\")],\n    [\"transition-duration\", parseCssValue(\"transition-duration\", \"1s\")],\n    [\n      \"transition-timing-function\",\n      parseCssValue(\"transition-timing-function\", \"ease\"),\n    ],\n    [\"transition-delay\", parseCssValue(\"transition-delay\", \"0s\")],\n    [\"transition-behavior\", parseCssValue(\"transition-behavior\", \"normal\")],\n  ]);\n  expect(parseCssFragment(\"opacity 1s\", [\"transition\"])).toEqual(result);\n  expect(parseCssFragment(\"transition: opacity 1s\", [\"transition\"])).toEqual(\n    result\n  );\n});\n\ntest(\"parse longhand properties\", () => {\n  expect(\n    parseCssFragment(\n      `\n       transition-property: opacity;\n       transition-duration: 1s;\n     `,\n      [\"transition\"]\n    )\n  ).toEqual(\n    new Map([\n      [\"transition-property\", parseCssValue(\"transition-property\", \"opacity\")],\n      [\"transition-duration\", parseCssValue(\"transition-duration\", \"1s\")],\n    ])\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-fragment.tsx",
    "content": "import { useMemo, useRef, type ComponentProps, type ReactNode } from \"react\";\nimport { matchSorter, type RankingInfo } from \"match-sorter\";\nimport { EditorView, keymap, tooltips } from \"@codemirror/view\";\nimport { css } from \"@codemirror/lang-css\";\nimport {\n  autocompletion,\n  completionKeymap,\n  type CompletionSource,\n} from \"@codemirror/autocomplete\";\nimport { parseCss, shorthandProperties } from \"@webstudio-is/css-data\";\nimport { css as style, type CSS } from \"@webstudio-is/design-system\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  EditorContent,\n  EditorDialog,\n  EditorDialogButton,\n  EditorDialogControl,\n  getCodeEditorCssVars,\n} from \"~/shared/code-editor-base\";\nimport { $availableVariables } from \"./model\";\n\ntype ShorthandProperty = (typeof shorthandProperties)[number];\n\nexport { getCodeEditorCssVars };\n\nexport const parseCssFragment = (\n  css: string,\n  fallbacks: (CssProperty | ShorthandProperty)[]\n): Map<CssProperty, StyleValue> => {\n  let parsed = parseCss(`.styles{${css}}`);\n  if (parsed.length === 0) {\n    for (const fallbackProperty of fallbacks) {\n      parsed = parseCss(`.styles{${fallbackProperty}: ${css}}`);\n      parsed = parsed.filter((styleDecl) => styleDecl.value.type !== \"invalid\");\n      if (parsed.length > 0) {\n        break;\n      }\n    }\n  }\n  return new Map(\n    parsed.map((styleDecl) => [styleDecl.property, styleDecl.value])\n  );\n};\n\nconst compareVariables = (left: RankingInfo, right: RankingInfo) => {\n  return left.rankedValue.localeCompare(right.rankedValue, undefined, {\n    numeric: true,\n  });\n};\n\nconst scopeCompletionSource: CompletionSource = (context) => {\n  const word = context.matchBefore(/[-\\w()]+/);\n  if (word === null || (word.from === word.to && false === context.explicit)) {\n    return null;\n  }\n  const search = word.text;\n  const availableVariables = $availableVariables.get();\n  const options = availableVariables.map((varValue) => ({\n    label: `var(--${varValue.value})`,\n    displayLabel: `--${varValue.value}`,\n  }));\n  const matches = matchSorter(options, search, {\n    keys: [\"label\"],\n    baseSort: compareVariables,\n  });\n  return {\n    from: word.from,\n    to: word.to,\n    filter: false,\n    options: matches,\n  };\n};\n\nconst wrapperStyle = style({\n  position: \"relative\",\n  ...getCodeEditorCssVars({ minHeight: \"3lh\", maxHeight: \"6lh\" }),\n});\n\nexport const CssFragmentEditorContent = ({\n  onKeyDown,\n  ...props\n}: ComponentProps<typeof EditorContent> & {\n  onKeyDown?: (event: KeyboardEvent) => void;\n}) => {\n  const onKeyDownRef = useRef(onKeyDown);\n  onKeyDownRef.current = onKeyDown;\n  const extensions = useMemo(() => {\n    return [\n      css(),\n      // render autocomplete in body\n      // to prevent popover scroll overflow\n      tooltips({ parent: document.body }),\n      autocompletion({\n        override: [scopeCompletionSource],\n        icons: false,\n      }),\n      keymap.of([...completionKeymap]),\n      EditorView.domEventHandlers({\n        keydown(event) {\n          onKeyDownRef.current?.(event);\n        },\n      }),\n    ];\n  }, []);\n  return <EditorContent extensions={extensions} {...props} />;\n};\n\nexport const CssFragmentEditor = ({\n  content,\n  onOpenChange,\n  css,\n}: {\n  content: ReactNode;\n  onOpenChange?: (newOpen: boolean) => void;\n  css?: CSS;\n}) => {\n  return (\n    <div className={wrapperStyle({ css })}>\n      <EditorDialogControl>\n        {content}\n        <EditorDialog\n          onOpenChange={onOpenChange}\n          title=\"CSS Value\"\n          content={content}\n        >\n          <EditorDialogButton />\n        </EditorDialog>\n      </EditorDialogControl>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/convert-units.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { convertUnits } from \"./convert-units\";\n\nconst unitSizes = {\n  ch: 8,\n  vw: 3.2,\n  vh: 4.8,\n  em: 14,\n  rem: 16,\n  px: 1,\n};\n\ndescribe(\"Evaluate math\", () => {\n  test(\"convert same units\", () => {\n    expect(convertUnits(unitSizes)(100, \"px\", \"px\")).toEqual(100);\n    expect(convertUnits(unitSizes)(100, \"deg\", \"deg\")).toEqual(100);\n  });\n\n  test(\"do nothing if can't convert\", () => {\n    expect(convertUnits(unitSizes)(100, \"deg\", \"rad\")).toEqual(100);\n  });\n\n  test(\"convert size units\", () => {\n    expect(convertUnits(unitSizes)(224, \"px\", \"em\")).toEqual(16);\n    expect(convertUnits(unitSizes)(16, \"em\", \"px\")).toEqual(224);\n\n    expect(convertUnits(unitSizes)(16, \"em\", \"rem\")).toEqual(14);\n    expect(convertUnits(unitSizes)(14, \"rem\", \"em\")).toEqual(16);\n\n    expect(convertUnits(unitSizes)(320, \"px\", \"vw\")).toEqual(100);\n    expect(convertUnits(unitSizes)(480, \"px\", \"vh\")).toEqual(100);\n\n    expect(convertUnits(unitSizes)(100, \"vw\", \"rem\")).toEqual(20);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/convert-units.ts",
    "content": "import type { Unit } from \"@webstudio-is/css-engine\";\nimport {\n  convertibleUnits,\n  type ConvertibleUnit,\n  type UnitSizes,\n} from \"~/shared/nano-states\";\n\nconst isConvertibleUnit = (unit: Unit): unit is ConvertibleUnit =>\n  convertibleUnits.includes(unit as ConvertibleUnit);\n\nexport const convertUnits =\n  (unitSizes: UnitSizes) =>\n  (value: number, from: Unit, to: Unit): number => {\n    if (from === to) {\n      return value;\n    }\n\n    if (isConvertibleUnit(from) && isConvertibleUnit(to)) {\n      return (value * unitSizes[from]) / unitSizes[to];\n    }\n    return value;\n  };\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input-container.tsx",
    "content": "import { type ComponentProps, useState } from \"react\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { CssValueInput, type IntermediateStyleValue } from \"./css-value-input\";\nimport type { StyleUpdateOptions } from \"../use-style-data\";\n\ntype CssValueInputContainerProps = Omit<\n  ComponentProps<typeof CssValueInput>,\n  | \"onChange\"\n  | \"onHighlight\"\n  | \"onReset\"\n  | \"onAbort\"\n  | \"intermediateValue\"\n  | \"onChangeComplete\"\n  | \"setProperty\"\n> & {\n  onUpdate: (style: StyleValue, options?: StyleUpdateOptions) => void;\n  onDelete: (options?: StyleUpdateOptions) => void;\n  onChangeComplete?: ComponentProps<typeof CssValueInput>[\"onChangeComplete\"];\n  onReset?: ComponentProps<typeof CssValueInput>[\"onReset\"];\n};\n\nexport const CssValueInputContainer = ({\n  property,\n  onUpdate,\n  onDelete,\n  onChangeComplete,\n  onReset,\n  ...props\n}: CssValueInputContainerProps) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    StyleValue | IntermediateStyleValue\n  >();\n\n  return (\n    <CssValueInput\n      {...props}\n      property={property}\n      intermediateValue={intermediateValue}\n      onChange={(styleValue) => {\n        setIntermediateValue(styleValue);\n\n        if (styleValue === undefined) {\n          onDelete({ isEphemeral: true });\n          return;\n        }\n\n        if (styleValue.type !== \"intermediate\") {\n          onUpdate(styleValue, { isEphemeral: true });\n        }\n      }}\n      onHighlight={(styleValue) => {\n        if (styleValue !== undefined) {\n          onUpdate(styleValue, { isEphemeral: true });\n        } else {\n          onDelete({ isEphemeral: true });\n        }\n      }}\n      onChangeComplete={(event) => {\n        onUpdate(event.value, { isEphemeral: false });\n        setIntermediateValue(undefined);\n        onChangeComplete?.(event);\n      }}\n      onAbort={() => {\n        onDelete({ isEphemeral: true });\n      }}\n      onReset={() => {\n        setIntermediateValue(undefined);\n        onDelete();\n        onReset?.();\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.stories.tsx",
    "content": "import * as React from \"react\";\nimport {\n  Flex,\n  InputField,\n  SmallIconButton,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport type { StyleValue, CssProperty } from \"@webstudio-is/css-engine\";\nimport type { StyleValueSourceColor } from \"~/shared/style-object-model\";\nimport {\n  CssValueInput as CssValueInputComponent,\n  type CssValueInputValue,\n} from \"./css-value-input\";\nimport type { UnitOption } from \"./unit-select\";\nimport { action } from \"@storybook/addon-actions\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { EyeOpenIcon, Link2Icon } from \"@webstudio-is/icons\";\n\nexport default {\n  title: \"Style panel/CSS Value Input\",\n  component: CssValueInputComponent,\n};\n\nconst CssValueInputVariant = ({\n  label,\n  initialValue,\n  property,\n  options,\n  containerWidth,\n  showOutput,\n  styleSource = \"preset\",\n  disabled,\n  icon,\n  showSuffix,\n  unitOptions,\n  placeholder,\n  minWidth,\n  prefix,\n}: {\n  label: string;\n  initialValue: StyleValue;\n  property: CssProperty;\n  options?: Array<{ type: \"keyword\"; value: string }>;\n  containerWidth?: number;\n  showOutput?: boolean;\n  styleSource?: StyleValueSourceColor;\n  disabled?: boolean;\n  icon?: React.ReactNode;\n  showSuffix?: boolean;\n  unitOptions?: UnitOption[];\n  placeholder?: string;\n  minWidth?: string;\n  prefix?: React.ReactNode;\n}) => {\n  const [value, setValue] = React.useState<StyleValue>(initialValue);\n  const [intermediateValue, setIntermediateValue] = React.useState<\n    CssValueInputValue | undefined\n  >();\n\n  const input = (\n    <CssValueInputComponent\n      styleSource={styleSource}\n      property={property}\n      value={value}\n      intermediateValue={intermediateValue}\n      getOptions={options ? () => options : undefined}\n      onChange={setIntermediateValue}\n      onHighlight={(v) => action(\"onHighlight\")(v)}\n      onChangeComplete={({ value: v }) => {\n        setValue(v);\n        setIntermediateValue(undefined);\n        action(\"onChangeComplete\")(v);\n      }}\n      onAbort={() => action(\"onAbort\")()}\n      onReset={() => action(\"onReset\")()}\n      disabled={disabled}\n      icon={icon}\n      showSuffix={showSuffix}\n      unitOptions={unitOptions}\n      placeholder={placeholder}\n      minWidth={minWidth}\n      prefix={prefix}\n    />\n  );\n\n  return (\n    <Flex direction=\"column\" gap=\"1\">\n      <Text variant=\"labels\">{label}</Text>\n      {containerWidth ? (\n        <Flex css={{ width: containerWidth }}>\n          {input}\n          {showOutput && (\n            <InputField\n              readOnly\n              value={\n                value\n                  ? intermediateValue?.type === \"intermediate\"\n                    ? intermediateValue.value + intermediateValue.unit\n                    : toValue(value)\n                  : \"\"\n              }\n            />\n          )}\n        </Flex>\n      ) : (\n        input\n      )}\n    </Flex>\n  );\n};\n\nconst keywordOptions = [\n  { type: \"keyword\" as const, value: \"auto\" },\n  { type: \"keyword\" as const, value: \"min-content\" },\n  { type: \"keyword\" as const, value: \"max-content\" },\n  { type: \"keyword\" as const, value: \"fit-content\" },\n];\n\nexport const CSSValueInput = () => (\n  <>\n    <StorySection title=\"Keywords and units\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"Keywords (width)\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Icons (align-items)\"\n          initialValue={{ type: \"keyword\", value: \"space-around\" }}\n          property=\"align-items\"\n          options={[\n            { type: \"keyword\", value: \"normal\" },\n            { type: \"keyword\", value: \"start\" },\n            { type: \"keyword\", value: \"end\" },\n            { type: \"keyword\", value: \"center\" },\n            { type: \"keyword\", value: \"stretch\" },\n            { type: \"keyword\", value: \"space-around\" },\n            { type: \"keyword\", value: \"space-between\" },\n          ]}\n        />\n        <CssValueInputVariant\n          label=\"Units (row-gap)\"\n          initialValue={{ type: \"unit\", value: 100, unit: \"px\" }}\n          property=\"row-gap\"\n          options={keywordOptions}\n          showOutput\n        />\n        <CssValueInputVariant\n          label=\"Oversized (100px container)\"\n          initialValue={{\n            type: \"var\",\n            value: \"start-test-test-test-test-test-test-test-end\",\n          }}\n          property=\"align-items\"\n          containerWidth={100}\n        />\n        <CssValueInputVariant\n          label=\"With text prefix\"\n          initialValue={{ type: \"unit\", value: 10, unit: \"px\" }}\n          property=\"row-gap\"\n          prefix={<Text>X</Text>}\n        />\n        <CssValueInputVariant\n          label=\"With placeholder\"\n          initialValue={{ type: \"keyword\", value: \"\" }}\n          property=\"width\"\n          placeholder=\"Enter value\\u2026\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Only px and %\"\n          initialValue={{ type: \"unit\", value: 100, unit: \"px\" }}\n          property=\"width\"\n          unitOptions={[\n            { id: \"px\", label: \"px\", type: \"unit\" },\n            { id: \"%\", label: \"%\", type: \"unit\" },\n          ]}\n        />\n        <CssValueInputVariant\n          label=\"Units with keyword option\"\n          initialValue={{ type: \"unit\", value: 2, unit: \"rem\" }}\n          property=\"width\"\n          unitOptions={[\n            { id: \"px\", label: \"px\", type: \"unit\" },\n            { id: \"rem\", label: \"rem\", type: \"unit\" },\n            { id: \"auto\", label: \"auto\", type: \"keyword\" },\n          ]}\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Style sources\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"Default\"\n          styleSource=\"default\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Preset\"\n          styleSource=\"preset\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Local\"\n          styleSource=\"local\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Remote\"\n          styleSource=\"remote\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Overwritten\"\n          styleSource=\"overwritten\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Disabled\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"Disabled with keyword\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          disabled\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Disabled with unit\"\n          initialValue={{ type: \"unit\", value: 42, unit: \"px\" }}\n          property=\"row-gap\"\n          disabled\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"With icon\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"With eye icon\"\n          initialValue={{ type: \"unit\", value: 100, unit: \"px\" }}\n          property=\"width\"\n          icon={<SmallIconButton icon={<EyeOpenIcon />} />}\n        />\n        <CssValueInputVariant\n          label=\"With link icon\"\n          initialValue={{ type: \"unit\", value: 16, unit: \"px\" }}\n          property=\"row-gap\"\n          icon={<SmallIconButton icon={<Link2Icon />} />}\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Hidden suffix\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"Suffix shown (default)\"\n          initialValue={{ type: \"unit\", value: 16, unit: \"px\" }}\n          property=\"row-gap\"\n          showSuffix\n        />\n        <CssValueInputVariant\n          label=\"Suffix hidden\"\n          initialValue={{ type: \"unit\", value: 16, unit: \"px\" }}\n          property=\"row-gap\"\n          showSuffix={false}\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Value types\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"Keyword value\"\n          initialValue={{ type: \"keyword\", value: \"auto\" }}\n          property=\"width\"\n          options={keywordOptions}\n        />\n        <CssValueInputVariant\n          label=\"Unit value (px)\"\n          initialValue={{ type: \"unit\", value: 100, unit: \"px\" }}\n          property=\"width\"\n        />\n        <CssValueInputVariant\n          label=\"Unit value (rem)\"\n          initialValue={{ type: \"unit\", value: 2, unit: \"rem\" }}\n          property=\"width\"\n        />\n        <CssValueInputVariant\n          label=\"Unit value (%)\"\n          initialValue={{ type: \"unit\", value: 50, unit: \"%\" }}\n          property=\"width\"\n        />\n        <CssValueInputVariant\n          label=\"Var value\"\n          initialValue={{ type: \"var\", value: \"my-custom-var\" }}\n          property=\"width\"\n        />\n        <CssValueInputVariant\n          label=\"Var with fallback\"\n          initialValue={{\n            type: \"var\",\n            value: \"brand-color\",\n            fallback: { type: \"keyword\", value: \"red\" },\n          }}\n          property=\"color\"\n        />\n        <CssValueInputVariant\n          label=\"Invalid value\"\n          initialValue={{ type: \"invalid\", value: \"not-a-valid-value\" }}\n          property=\"width\"\n        />\n        <CssValueInputVariant\n          label=\"Unparsed value\"\n          initialValue={{\n            type: \"unparsed\",\n            value: \"calc(100% - 20px)\",\n          }}\n          property=\"width\"\n        />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Min width\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <CssValueInputVariant\n          label=\"Default min width\"\n          initialValue={{ type: \"unit\", value: 0, unit: \"px\" }}\n          property=\"width\"\n        />\n        <CssValueInputVariant\n          label=\"Min width 120px\"\n          initialValue={{ type: \"unit\", value: 0, unit: \"px\" }}\n          property=\"width\"\n          minWidth=\"120px\"\n        />\n      </Flex>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx",
    "content": "import {\n  Box,\n  useCombobox,\n  ComboboxRoot,\n  ComboboxContent,\n  ComboboxAnchor,\n  ComboboxListbox,\n  ComboboxListboxItem,\n  ComboboxItemDescription,\n  ComboboxScrollArea,\n  numericScrubControl,\n  NestedInputButton,\n  NestedIconLabel,\n  InputField,\n  handleNumericInputArrowKeys,\n  theme,\n  Flex,\n  styled,\n  Text,\n  ColorThumb,\n} from \"@webstudio-is/design-system\";\nimport type {\n  CssProperty,\n  KeywordValue,\n  StyleValue,\n  Unit,\n  VarValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  type KeyboardEventHandler,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  useMemo,\n  type ComponentProps,\n  type RefObject,\n} from \"react\";\nimport { useUnitSelect, type UnitOption } from \"./unit-select\";\nimport { parseIntermediateOrInvalidValue } from \"./parse-intermediate-or-invalid-value\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport {\n  camelCaseProperty,\n  declarationDescriptions,\n  isValidDeclaration,\n} from \"@webstudio-is/css-data\";\nimport { $selectedInstanceSizes } from \"~/shared/nano-states\";\nimport { convertUnits } from \"./convert-units\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { composeEventHandlers } from \"~/shared/event-utils\";\nimport type { StyleValueSourceColor } from \"~/shared/style-object-model\";\nimport {\n  cssButtonDisplay,\n  isComplexValue,\n  ValueEditorDialog,\n} from \"./value-editor-dialog\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport { scrollByPointer } from \"../scroll-by-pointer\";\n\n// Subjective adjust ment based on how it feels on macbook/trackpad.\n// It won't be ideal for everyone with different input devices and preferences.\n// Ideally we also need some kind of acceleration setting with 1 value.\nconst scrubUnitAcceleration = new Map<Unit, number>([\n  [\"rem\", 1 / 16],\n  [\"em\", 1 / 16],\n  [\"%\", 1 / 10],\n  [\"dvw\", 1 / 10],\n  [\"dvh\", 1 / 10],\n  [\"number\", 1 / 20],\n]);\n\n/**\n * Scrub hook for numeric input fields\n * - Works specifically on <input> elements (returns refs to attach)\n * - Handles single property at a time\n * - Manages intermediate values during typing\n * - Has abort functionality (ESC key restores original)\n * - Integrates with input focus/blur/selection behavior\n * - Custom shouldHandleEvent for preventing scrub on vars/unit selects\n */\nconst useScrub = ({\n  value,\n  intermediateValue,\n  defaultUnit,\n  property,\n  onChange,\n  onChangeComplete,\n  onAbort,\n  shouldHandleEvent,\n}: {\n  defaultUnit: Unit | undefined;\n  value: CssValueInputValue;\n  intermediateValue: CssValueInputValue | undefined;\n  property: CssProperty;\n  onChange: (value: CssValueInputValue | undefined) => void;\n  onChangeComplete: (value: StyleValue) => void;\n  onAbort: () => void;\n  shouldHandleEvent?: (node: Node) => boolean;\n}): [\n  RefObject<HTMLInputElement | null>,\n  RefObject<HTMLInputElement | null>,\n] => {\n  const scrubRef = useRef<HTMLInputElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const onChangeRef = useRef(onChange);\n  const onChangeCompleteRef = useRef(onChangeComplete);\n  const valueRef = useRef(value);\n\n  const intermediateValueRef = useRef(intermediateValue);\n\n  onChangeCompleteRef.current = onChangeComplete;\n  onChangeRef.current = onChange;\n\n  valueRef.current = value;\n\n  const updateIntermediateValue = useEffectEvent(() => {\n    intermediateValueRef.current = intermediateValue;\n  });\n\n  const onAbortStable = useEffectEvent(onAbort);\n\n  // const type = valueRef.current.type;\n\n  // Since scrub is going to call onChange and onChangeComplete callbacks, it will result in a new value and potentially new callback refs.\n  // We need this effect to ONLY run when type or unit changes, but not when callbacks or value.value changes.\n  useEffect(() => {\n    const inputRefCurrent = inputRef.current;\n    const scrubRefCurrent = scrubRef.current;\n\n    // Support only auto keyword to be scrubbable\n    if (inputRefCurrent === null || scrubRefCurrent === null) {\n      return;\n    }\n\n    // Don't activate scrub for keyword values\n    if (valueRef.current.type === \"keyword\") {\n      return;\n    }\n\n    // Don't activate scrub for non-numeric intermediate values\n    if (valueRef.current.type === \"intermediate\") {\n      const numericValue = Number.parseFloat(valueRef.current.value);\n      if (Number.isNaN(numericValue)) {\n        return;\n      }\n    }\n\n    let unit: Unit = defaultUnit ?? \"number\";\n\n    const validateValue = (numericValue: number) => {\n      let value: CssValueInputValue = {\n        type: \"unit\",\n        unit,\n        value: numericValue,\n      };\n\n      if (\n        value.type === \"unit\" &&\n        isValidDeclaration(property, toValue(value)) === false\n      ) {\n        value = parseIntermediateOrInvalidValue(property, {\n          type: \"intermediate\",\n          value: `${value.value}`,\n          unit: value.unit,\n        });\n\n        // In case of negative values for some properties, we might end up with invalid value.\n        if (value.type === \"invalid\") {\n          // Try 0 with same unit\n          if (isValidDeclaration(property, `0${unit}`)) {\n            return {\n              type: \"unit\",\n              unit,\n              value: 0,\n            } as const;\n          }\n\n          // Try unitless (in case of unit above was `number`\n          if (isValidDeclaration(property, \"0\")) {\n            return {\n              type: \"unit\",\n              unit: \"number\",\n              value: 0,\n            } as const;\n          }\n        }\n      }\n      return value;\n    };\n\n    return numericScrubControl(scrubRefCurrent, {\n      distanceThreshold: 2,\n      getAcceleration() {\n        if (valueRef.current.type === \"unit\") {\n          return scrubUnitAcceleration.get(valueRef.current.unit);\n        }\n      },\n      // @todo: after this https://github.com/webstudio-is/webstudio/issues/564\n      // we can switch back on using just initial value\n      //\n      // For now we are reusing controls for different selectedInstanceData,\n      // and the best here is to call useEffect every time selectedInstanceData changes\n      // and recreate numericScrubControl.\n      // Until we have decision do we use key properties for this or not,\n      // on of the solution to get value inside scrub is to use ref and lazy getter.\n      // Getter to avoid recreating scrub on every value change\n      getInitialValue() {\n        if (valueRef.current.type === \"unit\") {\n          return valueRef.current.value;\n        }\n        return 0;\n      },\n      onStart() {\n        if (valueRef.current.type === \"unit\") {\n          unit = valueRef.current.unit;\n        }\n\n        updateIntermediateValue();\n      },\n      onAbort() {\n        onAbortStable();\n        // Returning focus that we've moved above\n        scrubRef.current?.removeAttribute(\"tabindex\");\n        onChangeRef.current(intermediateValueRef.current);\n\n        // Otherwise selectionchange event can be triggered after 300-1000ms after focus\n        requestAnimationFrame(() => {\n          inputRef.current?.focus();\n          inputRef.current?.select();\n        });\n      },\n      onValueInput(event) {\n        // Moving focus to container of the input to hide the caret\n        // (it makes text harder to read and may jump around as you scrub)\n        scrubRef.current?.setAttribute(\"tabindex\", \"-1\");\n        scrubRef.current?.focus();\n        const value = validateValue(event.value);\n\n        onChangeRef.current(value);\n      },\n      onValueChange(event) {\n        // Will work without but depends on order of setState updates\n        // at text-control, now fixed in both places (order of updates is right, and batched here)\n        const value = validateValue(event.value);\n\n        onChangeCompleteRef.current(value);\n\n        // Returning focus that we've moved above\n        scrubRef.current?.removeAttribute(\"tabindex\");\n\n        // Otherwise selectionchange event can be triggered after 300-1000ms after focus\n        requestAnimationFrame(() => {\n          inputRef.current?.focus();\n          inputRef.current?.select();\n        });\n      },\n      shouldHandleEvent,\n    });\n    // value.type and value.unit are intentionally in the dependency array\n    // to re-setup scrub when the value type changes (e.g., from keyword to unit)\n  }, [\n    shouldHandleEvent,\n    property,\n    defaultUnit,\n    value.type,\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    \"unit\" in value ? value.unit : undefined,\n  ]);\n\n  return [scrubRef, inputRef];\n};\n\nexport const isNumericString = (input: string) =>\n  String(input).trim().length !== 0 && Number.isNaN(Number(input)) === false;\n\nexport type IntermediateStyleValue = {\n  type: \"intermediate\";\n  value: string;\n  unit?: Unit;\n};\n\nexport type CssValueInputValue = StyleValue | IntermediateStyleValue;\n\ntype Modifiers = {\n  altKey: boolean;\n  shiftKey: boolean;\n};\n\ntype ChangeCompleteEvent = {\n  type:\n    | \"enter\"\n    | \"blur\"\n    | \"scrub-end\"\n    | \"unit-select\"\n    | \"keyword-select\"\n    | \"delta\"\n    | \"dialog-change-complete\";\n  value: StyleValue;\n  close?: boolean;\n} & Modifiers;\n\ntype CssValueInputProps = Pick<\n  ComponentProps<typeof InputField>,\n  | \"variant\"\n  | \"text\"\n  | \"autoFocus\"\n  | \"disabled\"\n  | \"aria-disabled\"\n  | \"fieldSizing\"\n  | \"prefix\"\n  | \"inputRef\"\n> & {\n  styleSource: StyleValueSourceColor;\n  property: CssProperty;\n  value: StyleValue | undefined;\n  intermediateValue: CssValueInputValue | undefined;\n  /**\n   * Selected item in the dropdown\n   */\n  getOptions?: () => Array<\n    KeywordValue | VarValue | (KeywordValue & { description?: string })\n  >;\n  onChange: (value: CssValueInputValue | undefined) => void;\n  onChangeComplete: (event: ChangeCompleteEvent) => void;\n  onHighlight: (value: StyleValue | undefined) => void;\n  // Does not reset intermediate changes.\n  onAbort: () => void;\n  // Resets the value to default even if it has intermediate changes.\n  onReset: () => void;\n  icon?: ReactNode;\n  showSuffix?: boolean;\n  unitOptions?: UnitOption[];\n  id?: string;\n  placeholder?: string;\n  minWidth?: string;\n};\n\nconst initialValue: IntermediateStyleValue = {\n  type: \"intermediate\",\n  value: \"\",\n};\n\nconst itemToString = (item: CssValueInputValue | null) => {\n  if (item === null) {\n    return \"\";\n  }\n  if (item.type === \"var\") {\n    // Use toValue to include fallback when present\n    return toValue(item as StyleValue);\n  }\n  if (item.type === \"keyword\") {\n    // E.g. we want currentcolor to be lower case\n    return toValue(item).toLocaleLowerCase();\n  }\n  if (item.type === \"intermediate\" || item.type === \"unit\") {\n    return String(item.value);\n  }\n  return toValue(item);\n};\n\nconst Description = styled(Box, { width: theme.spacing[27] });\n\n/**\n * Common:\n * - Free text editing\n * - Enter or blur calls onChangeComplete\n * - Value prop can be of type \"invalid\" and render invalid mode of the input (red outline)\n *\n * Unit mode:\n * - When entire text is a number we automatically switch to unit mode on keydown\n * - Unit selection on unit button click or focus+enter\n * - When selecting unit arrow keys are used to navigate unit items\n * - When selecting unit Enter key or click is used to select item\n * - When selecting unit Escape key is used to close list\n * - Key up and down on focused input increment/decrement the value\n *   - shift key modifier increases/decreases value by 10\n *   - option/alt key modifier increases/decreases value by 0.1\n *   - no modifier increases/decreases value by 1\n *   - does not open the combobox when the input is a number (CSS root variables can include numbers in their names)\n * - Scrub interaction\n * - Click outside, unit selection or escape when list is open should unfocus the unit select trigger\n *\n * Options mode:\n * - When any character in the input is not a number we automatically switch to keywords mode on keydown\n * - Filterable keywords list (click on chevron or arrow down to show the list)\n * - Arrow keys are used to navigate keyword items\n * - Enter key or click is used to select item when list is open\n * - Escape key is used to close list\n * - When hovering over keywords list, onItemHighlight is called\n *\n * Features outside of this input (non standard):\n * - Typing number + unit (e.g. \"12px\") in unit mode will change the selected unit on blur/enter\n * - Evaluated math expression: \"2px + 3em\" (like CSS calc())\n */\nexport const CssValueInput = ({\n  id,\n  autoFocus,\n  icon,\n  prefix,\n  showSuffix = true,\n  styleSource,\n  property,\n  getOptions = () => [],\n  onHighlight,\n  onAbort,\n  onReset,\n  disabled,\n  [\"aria-disabled\"]: ariaDisabled,\n  fieldSizing,\n  variant,\n  text,\n  unitOptions,\n  placeholder,\n  minWidth = \"2ch\",\n  ...props\n}: CssValueInputProps) => {\n  const value = props.intermediateValue ?? props.value ?? initialValue;\n  const valueRef = useRef(value);\n  valueRef.current = value;\n  // Used to show description\n  const [highlightedValue, setHighlighedValue] = useState<\n    StyleValue | undefined\n  >();\n\n  const defaultUnit =\n    unitOptions?.[0]?.type === \"unit\" ? unitOptions[0].id : undefined;\n\n  const onChange = (input: string | undefined) => {\n    if (input === undefined) {\n      props.onChange(undefined);\n      return;\n    }\n    // We don't know what's inside the input,\n    // preserve current unit value if exists\n    props.onChange({\n      type: \"intermediate\",\n      value: input,\n      unit: \"unit\" in value ? value.unit : undefined,\n    });\n  };\n\n  const onChangeComplete = (\n    event: {\n      type: ChangeCompleteEvent[\"type\"];\n      value: CssValueInputValue;\n      close?: boolean;\n    } & Partial<Modifiers>\n  ) => {\n    const { value } = event;\n    const defaultProps = { altKey: false, shiftKey: false };\n\n    // We are resetting by setting the value to an empty string\n    if (value.type === \"intermediate\" && value.value === \"\") {\n      closeMenu();\n      onReset();\n      return;\n    }\n\n    if (value.type !== \"intermediate\" && value.type !== \"invalid\") {\n      // variables fallback is used for preview value while autocompleting\n      // removed fallback when select variable to not pollute values\n      let newValue = value;\n      if (value.type === \"var\") {\n        newValue = { ...value };\n        delete newValue.fallback;\n      }\n      // The value might be valid but not selected from the combo menu. Close the menu.\n      closeMenu();\n      props.onChangeComplete({ ...defaultProps, ...event, value: newValue });\n      return;\n    }\n\n    const parsedValue = parseIntermediateOrInvalidValue(\n      property,\n      value,\n      defaultUnit\n    );\n\n    if (parsedValue.type === \"invalid\") {\n      props.onChange(parsedValue);\n      return;\n    }\n\n    // The value might be valid but not selected from the combo menu. Close the menu.\n    closeMenu();\n    props.onChangeComplete({\n      ...defaultProps,\n      ...event,\n      value: parsedValue,\n    });\n  };\n\n  const {\n    items,\n    getInputProps,\n    getComboboxProps,\n    getToggleButtonProps,\n    getMenuProps,\n    getItemProps,\n    isOpen,\n    highlightedIndex,\n    closeMenu,\n  } = useCombobox<CssValueInputValue>({\n    inputId: id,\n    // Used for description to match the item when nothing is highlighted yet and value is still in non keyword mode\n    getItems: getOptions,\n    value,\n    selectedItem: props.value,\n    itemToString,\n    onChange: (inputValue) => {\n      onChange(inputValue);\n    },\n    onItemSelect: (value) => {\n      onChangeComplete({ value, type: \"keyword-select\" });\n    },\n    onItemHighlight: (value) => {\n      if (value == null) {\n        onHighlight(undefined);\n        setHighlighedValue(undefined);\n        return;\n      }\n\n      if (value.type !== \"intermediate\") {\n        onHighlight(value);\n        setHighlighedValue(value);\n      }\n    },\n  });\n\n  const inputProps = getInputProps();\n\n  const [isUnitsOpen, unitSelectElement] = useUnitSelect({\n    options: unitOptions,\n    property,\n    value,\n    onChange: (unitOrKeyword) => {\n      if (unitOrKeyword.type === \"keyword\") {\n        onChangeComplete({ value: unitOrKeyword, type: \"unit-select\" });\n        return;\n      }\n\n      const unit = unitOrKeyword.value;\n\n      // value looks like a number and just edited (type === \"intermediate\")\n      // no additional conversions are necessary\n      if (\n        value.type === \"intermediate\" &&\n        Number.isNaN(Number.parseFloat(value.value)) === false\n      ) {\n        onChangeComplete({ value: { ...value, unit }, type: \"unit-select\" });\n        return;\n      }\n\n      const { unitSizes, propertySizes } = $selectedInstanceSizes.get();\n\n      // Value not edited by the user, we need to convert it to the new unit\n      if (value.type === \"unit\") {\n        const convertedValue = convertUnits(unitSizes)(\n          value.value,\n          value.unit,\n          unit\n        );\n\n        onChangeComplete({\n          value: {\n            type: \"unit\",\n            value: Number.parseFloat(convertedValue.toFixed(2)),\n            unit,\n          },\n          type: \"unit-select\",\n        });\n        return;\n      }\n\n      // value is a keyword or non numeric, try get browser style value and convert it\n      if (value.type === \"keyword\" || value.type === \"intermediate\") {\n        const styleValue = propertySizes[property];\n        const convertedValue = convertUnits(unitSizes)(\n          styleValue?.value ?? 0,\n          styleValue?.unit ?? \"number\",\n          unit\n        );\n\n        onChangeComplete({\n          value: {\n            type: \"unit\",\n            value: Number.parseFloat(convertedValue.toFixed(2)),\n            unit,\n          },\n          type: \"unit-select\",\n        });\n        return;\n      }\n\n      onChangeComplete({\n        value: {\n          type: \"intermediate\",\n          value: toValue(value),\n          unit,\n        },\n        type: \"unit-select\",\n      });\n    },\n    onCloseAutoFocus(event) {\n      // We don't want to focus the unit trigger when closing the select (no matter if unit was selected, clicked outside or esc was pressed)\n      event.preventDefault();\n      // Instead we want to focus the input\n      inputRef.current?.focus();\n    },\n  });\n\n  const shouldHandleEvent = useCallback((node: Node) => {\n    // prevent scrubbing when css variable is selected\n    if (valueRef.current.type === \"var\") {\n      return false;\n    }\n    // prevent scrubbing when unit select is in use\n    if (suffixRef.current?.contains?.(node)) {\n      return false;\n    }\n    return true;\n  }, []);\n\n  const [scrubRef, inputRef] = useScrub({\n    defaultUnit,\n    value,\n    property,\n    intermediateValue: props.intermediateValue,\n    onChange: props.onChange,\n    onChangeComplete: (value) => onChangeComplete({ value, type: \"scrub-end\" }),\n    shouldHandleEvent,\n    onAbort,\n  });\n\n  const menuProps = getMenuProps();\n\n  const getInputValue = () => {\n    const isFocused = document.activeElement === inputRef.current;\n    // When input is not focused, we are removing var() to fit more into the small inputs.\n    return value.type === \"var\" && isFocused === false\n      ? `--${value.value}`\n      : inputProps.value;\n  };\n  const handleOnBlur: KeyboardEventHandler = (event) => {\n    inputProps.onBlur(event);\n\n    // Restore the value without var()\n    if (event.target instanceof HTMLInputElement) {\n      event.target.value = getInputValue();\n    }\n    // When unit select is open, onBlur is triggered,though we don't want a change event in this case.\n    if (isUnitsOpen) {\n      return;\n    }\n\n    // If the menu is open and visible we don't want to trigger onChangeComplete\n    // as it will be done by Downshift\n    if (isOpen && menuProps.empty === false) {\n      return;\n    }\n\n    // Probably no changes have been made at this point\n    // In that case we will call onAbort instead of onChangeComplete\n    if (props.intermediateValue === undefined) {\n      if (props.value === undefined) {\n        onAbort();\n      }\n      return;\n    }\n\n    onChangeComplete({ value, type: \"blur\" });\n  };\n\n  const finalPrefix =\n    prefix ||\n    (icon && <NestedIconLabel color={styleSource}>{icon}</NestedIconLabel>);\n\n  const keywordButtonElement =\n    value.type === \"keyword\" && items.length !== 0 ? (\n      <NestedInputButton\n        {...getToggleButtonProps()}\n        data-state={isOpen ? \"open\" : \"closed\"}\n        tabIndex={-1}\n      />\n    ) : undefined;\n\n  let description;\n  // When user hovers or focuses an item in the combobox list we want to show the description of the item and otherwise show the description of the current value\n  const valueForDescription =\n    highlightedValue?.type === \"keyword\"\n      ? highlightedValue\n      : highlightedValue?.type === \"var\"\n        ? undefined\n        : props.value?.type === \"keyword\"\n          ? props.value\n          : items[0]?.type === \"keyword\"\n            ? items[0]\n            : undefined;\n\n  if (valueForDescription) {\n    const option = getOptions().find(\n      (item) =>\n        item.type === \"keyword\" && item.value === valueForDescription.value\n    );\n    if (\n      option !== undefined &&\n      \"description\" in option &&\n      option?.description\n    ) {\n      description = option.description;\n    } else {\n      const key = `${camelCaseProperty(property)}:${toValue(\n        valueForDescription\n      )}` as keyof typeof declarationDescriptions;\n      description = declarationDescriptions[key];\n    }\n  } else if (highlightedValue?.type === \"var\") {\n    description = \"CSS custom property (variable)\";\n  } else if (highlightedValue === undefined) {\n    description = \"Select item\";\n  }\n\n  // Init with non breaking space to avoid jumping when description is empty\n  description = description ?? \"\\u00A0\";\n\n  const descriptions = items\n    .map((item) =>\n      item.type === \"keyword\"\n        ? declarationDescriptions[\n            `${camelCaseProperty(property)}:${toValue(\n              item\n            )}` as keyof typeof declarationDescriptions\n          ]\n        : undefined\n    )\n    .filter(Boolean)\n    .map((descr) => <Description>{descr}</Description>);\n\n  const handleUpDownNumeric = (event: KeyboardEvent<HTMLInputElement>) => {\n    const isComboOpen = isOpen && !menuProps.empty;\n\n    if (isUnitsOpen || isComboOpen) {\n      return;\n    }\n\n    if (\n      (value.type === \"unit\" ||\n        (value.type === \"intermediate\" && isNumericString(value.value))) &&\n      value.unit !== undefined &&\n      (event.key === \"ArrowUp\" || event.key === \"ArrowDown\")\n    ) {\n      const inputValue =\n        value.type === \"unit\" ? value.value : Number(value.value.trim());\n\n      const meta = { altKey: event.altKey, shiftKey: event.shiftKey };\n\n      const newValue = {\n        type: \"unit\" as const,\n        value: handleNumericInputArrowKeys(inputValue, event),\n        unit: value.unit,\n      };\n\n      onChangeComplete({\n        value: newValue,\n        ...meta,\n        type: \"delta\",\n        close: false,\n      });\n      event.preventDefault();\n    }\n  };\n\n  const handleEnter = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (\n      isUnitsOpen ||\n      (isOpen && !menuProps.empty && highlightedIndex !== -1)\n    ) {\n      return;\n    }\n\n    const meta = { altKey: event.altKey, shiftKey: event.shiftKey };\n\n    if (event.key === \"Enter\") {\n      onChangeComplete({ type: \"enter\", value, ...meta });\n    }\n  };\n\n  const handleDelete = (event: KeyboardEvent<HTMLInputElement>) => {\n    if (event.key === \"Backspace\" && inputProps.value === \"\") {\n      // - allows to close the menu\n      // - prevents baspace from deleting the value AFTER its already reseted to default, e.g. we get \"aut\" instead of \"auto\"\n      event.preventDefault();\n      closeMenu();\n      onReset();\n    }\n  };\n\n  const { abort, ...autoScrollProps } = useMemo(() => {\n    return scrollByPointer();\n  }, []);\n\n  useEffect(() => {\n    return () => abort(\"unmount\");\n  }, [abort]);\n\n  useEffect(() => {\n    if (inputRef.current === null) {\n      return;\n    }\n\n    const abortController = new AbortController();\n\n    const options = {\n      signal: abortController.signal,\n    };\n\n    let focusTime = 0;\n    inputRef.current.addEventListener(\n      \"selectionchange\",\n      () => {\n        if (Date.now() - focusTime < 150) {\n          inputRef.current?.select();\n        }\n      },\n      options\n    );\n    inputRef.current.addEventListener(\n      \"focus\",\n      () => {\n        if (inputRef.current === null) {\n          return;\n        }\n\n        focusTime = Date.now();\n      },\n      options\n    );\n\n    return () => {\n      abortController.abort();\n    };\n  }, [inputRef]);\n\n  const inputPropsHandleKeyDown = composeEventHandlers(\n    [\n      handleUpDownNumeric,\n      inputProps.onKeyDown,\n      handleEnter,\n      handleDelete,\n      (event: KeyboardEvent) => {\n        // When dropdown is open - we are loosing focus to the combobox.\n        // When menu gets closed via Escape - we want to restore the focus.\n        if (event.key === \"Escape\" && isOpen) {\n          requestAnimationFrame(() => {\n            inputRef.current?.focus();\n          });\n        }\n      },\n    ],\n    {\n      // Pass prevented events to the combobox (e.g., the Escape key doesn't work otherwise, as it's blocked by Radix)\n      checkForDefaultPrevented: false,\n    }\n  );\n\n  const suffixRef = useRef<HTMLDivElement | null>(null);\n  const valueEditorButtonElement = isComplexValue(value) ? (\n    <ValueEditorDialog\n      property={property}\n      value={inputProps.value}\n      onChangeComplete={(value) => {\n        onChangeComplete({\n          type: \"dialog-change-complete\",\n          value,\n          close: false,\n        });\n      }}\n    />\n  ) : undefined;\n  const invalidValueElement = value.type === \"invalid\" ? <></> : undefined;\n  const suffixElement =\n    invalidValueElement ??\n    unitSelectElement ??\n    keywordButtonElement ??\n    valueEditorButtonElement;\n\n  return (\n    <ComboboxRoot open={isOpen}>\n      <Box {...getComboboxProps()}>\n        <ComboboxAnchor asChild>\n          <InputField\n            id={id}\n            variant={variant}\n            disabled={disabled}\n            aria-disabled={ariaDisabled}\n            fieldSizing={fieldSizing}\n            placeholder={placeholder}\n            {...inputProps}\n            {...autoScrollProps}\n            value={getInputValue()}\n            onFocus={(event) => {\n              if (event.target instanceof HTMLInputElement) {\n                // We are setting the value on focus because we might have removed the var() from the value,\n                // but once focused, we need to show the full value\n                event.target.value = itemToString(value);\n              }\n            }}\n            autoFocus={autoFocus}\n            onBlur={handleOnBlur}\n            onKeyDown={inputPropsHandleKeyDown}\n            inputRef={mergeRefs(\n              inputRef,\n              props.inputRef,\n              disabled ? undefined : scrubRef\n            )}\n            name={property}\n            color={value.type === \"invalid\" ? \"error\" : undefined}\n            prefix={finalPrefix}\n            suffix={\n              <Flex align=\"center\" ref={suffixRef}>\n                {suffixElement}\n              </Flex>\n            }\n            css={{\n              cursor: \"default\",\n              minWidth,\n              \"&:hover\": {\n                [cssButtonDisplay]: \"block\",\n              },\n            }}\n            text={text}\n          />\n        </ComboboxAnchor>\n        {isOpen && (\n          <ComboboxContent align=\"start\" sideOffset={2} collisionPadding={10}>\n            <ComboboxListbox {...menuProps}>\n              <ComboboxScrollArea>\n                {items.map((item, index) => (\n                  <ComboboxListboxItem\n                    {...getItemProps({ item, index })}\n                    key={index}\n                  >\n                    {item.type === \"var\" ? (\n                      <Flex justify=\"between\" align=\"center\" grow gap={2}>\n                        <Box>--{item.value}</Box>\n                        {item.fallback?.type === \"unit\" && (\n                          <Text variant=\"small\" color=\"subtle\">\n                            {toValue(item.fallback)}\n                          </Text>\n                        )}\n                        {(item.fallback?.type === \"rgb\" ||\n                          item.fallback?.type === \"color\") && (\n                          <ColorThumb color={toValue(item.fallback)} />\n                        )}\n                      </Flex>\n                    ) : (\n                      itemToString(item)\n                    )}\n                  </ComboboxListboxItem>\n                ))}\n              </ComboboxScrollArea>\n              {descriptions.length > 0 && (\n                <ComboboxItemDescription descriptions={descriptions}>\n                  <Description>{description}</Description>\n                </ComboboxItemDescription>\n              )}\n            </ComboboxListbox>\n          </ComboboxContent>\n        )}\n      </Box>\n    </ComboboxRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/evaluate-math.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { evaluateMath } from \"./evaluate-math\";\n\ndescribe(\"Evaluate math\", () => {\n  test(\"should evaluate simple math\", () => {\n    expect(evaluateMath(\"1 + 1\")).toEqual(2);\n    expect(evaluateMath(\"4 + 2 * 3\")).toEqual(10);\n    expect(evaluateMath(\"4 / 2 * 3 - 1\")).toEqual(5);\n  });\n\n  test(\"should evaluate simple floating math\", () => {\n    expect(evaluateMath(\"1.0 + 1.1\")).toEqual(2.1);\n    expect(evaluateMath(\"4 + 2.5 * 3.4\")).toEqual(12.5);\n    expect(evaluateMath(\"4.5 / 2 * 3.3 - 1\")).toEqual(6.425);\n  });\n\n  test(\"return undefined if math is wrong\", () => {\n    expect(evaluateMath(\"1 ++ 1\")).toEqual(undefined);\n    expect(evaluateMath(\"++1\")).toEqual(undefined);\n    expect(evaluateMath(\"1 * 2 *\")).toEqual(undefined);\n  });\n\n  test(\"return undefined if any symbols excepts numbers or spaces in input\", () => {\n    expect(evaluateMath(\"1 + 1 p\")).toEqual(undefined);\n    expect(evaluateMath(\"e\")).toEqual(undefined);\n    expect(evaluateMath(\"{1+2}\")).toEqual(undefined);\n    expect(evaluateMath(\"alert('hello')\")).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/evaluate-math.ts",
    "content": "export const evaluateMath = (expression: string) => {\n  if (/^[\\d\\s.+*/-]+$/.test(expression) === false) {\n    return;\n  }\n  try {\n    // Eval is safe here because of the regex above\n    const result = eval(`(${expression})`);\n    if (typeof result === \"number\") {\n      return result;\n    }\n  } catch {\n    return;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/index.ts",
    "content": "export { CssValueInput, type IntermediateStyleValue } from \"./css-value-input\";\nexport { CssValueInputContainer } from \"./css-value-input-container\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts",
    "content": "import { kebabCase } from \"change-case\";\nimport type {\n  StyleValue,\n  InvalidValue,\n  Unit,\n  CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport {\n  units,\n  parseCssValue,\n  cssTryParseValue,\n  propertiesData,\n} from \"@webstudio-is/css-data\";\nimport type { IntermediateStyleValue } from \"./css-value-input\";\nimport { evaluateMath } from \"./evaluate-math\";\n\nconst unitsList = Object.values(units).flat();\n\nconst getDefaultUnit = (property: CssProperty): Unit => {\n  const unitGroups = propertiesData[property]?.unitGroups ?? [];\n\n  for (const unitGroup of unitGroups) {\n    if (unitGroup === \"number\") {\n      continue;\n    }\n\n    if (unitGroup === \"length\") {\n      return \"px\";\n    }\n\n    return units[unitGroup][0]!;\n  }\n\n  if (unitGroups.includes(\"number\" as never)) {\n    return \"number\";\n  }\n\n  return \"px\";\n};\n\nexport const parseIntermediateOrInvalidValue = (\n  property: CssProperty,\n  styleValue: IntermediateStyleValue | InvalidValue,\n  defaultUnit: Unit = getDefaultUnit(property),\n  originalValue?: string\n): StyleValue => {\n  let value = styleValue.value.trim();\n  if (value.endsWith(\";\")) {\n    value = value.slice(0, -1);\n  }\n\n  // Round values for properties that require integers when unit is \"number\"\n  if (\"unit\" in styleValue && styleValue.unit === \"number\") {\n    const numericValue = Number(value);\n    if (!Number.isNaN(numericValue) && !Number.isInteger(numericValue)) {\n      value = String(Math.round(numericValue));\n    }\n  }\n\n  // When user enters a number, we don't know if its a valid unit value,\n  // so we are going to parse it with a unit and if its not invalid - we take it.\n  const ast = cssTryParseValue(value);\n\n  if (ast === undefined) {\n    return {\n      type: \"invalid\",\n      value: originalValue ?? value,\n    };\n  }\n\n  const node =\n    \"children\" in ast && ast.children?.size === 1\n      ? ast.children.first\n      : undefined;\n\n  if (node?.type === \"Number\") {\n    const unit = \"unit\" in styleValue ? styleValue.unit : undefined;\n\n    // Use number as a fallback for custom properties\n    const fallbackUnitAsString = property.startsWith(\"--\")\n      ? \"\"\n      : defaultUnit === \"number\"\n        ? \"\"\n        : defaultUnit;\n\n    const testUnit = unit === \"number\" ? \"\" : (unit ?? fallbackUnitAsString);\n\n    const styleInput = parseCssValue(property, `${value}${testUnit}`);\n\n    if (styleInput.type !== \"invalid\") {\n      return styleInput;\n    }\n  }\n\n  // Probably value is already valid, use it\n  let styleInput = parseCssValue(property, value);\n\n  if (styleInput.type !== \"invalid\") {\n    return styleInput;\n  }\n\n  if (\"unit\" in styleValue && styleValue.unit === \"number\") {\n    // when unit is number some properties supports only integer\n    // for example z-index\n    styleInput = parseCssValue(property, value);\n    if (styleInput.type !== \"invalid\") {\n      return styleInput;\n    }\n\n    // Most css props supports 0 as unitless value, but not other numbers.\n    // Its possible that we had { value: 0, unit: \"number\" } and value has changed\n    // Lets try to parse it as px value\n    styleInput = parseCssValue(property, `${value}px`);\n\n    if (styleInput.type !== \"invalid\") {\n      return styleInput;\n    }\n  }\n\n  // Probably in kebab-case value will be valid\n  styleInput = parseCssValue(property, kebabCase(value));\n\n  if (styleInput.type !== \"invalid\") {\n    return styleInput;\n  }\n\n  // Try evaluate something like 10px + 4 or 13 + 4em\n  // Try to extract/remove anything similar to unit value\n  const unitRegex = new RegExp(`(?:${unitsList.join(\"|\")})`, \"g\");\n  let matchedUnit = value.match(unitRegex)?.[0];\n\n  let unitlessValue = value.replace(unitRegex, \"\");\n\n  // If value ends with \"-\" it is probably a unitless value i.e. unit = number\n  if (unitlessValue.endsWith(\"-\")) {\n    unitlessValue = unitlessValue.slice(0, -1).trim();\n    // If we have matched unit, use it, otherwise try unitless value\n    matchedUnit = matchedUnit === undefined ? \"\" : matchedUnit;\n  }\n\n  // Try to evaluate math expression if possible\n  const mathResult = evaluateMath(unitlessValue);\n\n  if (mathResult != null) {\n    const unit =\n      matchedUnit ?? (\"unit\" in styleValue ? (styleValue.unit ?? \"px\") : \"px\");\n    styleInput = parseCssValue(property, `${String(mathResult)}${unit}`);\n\n    if (styleInput.type !== \"invalid\") {\n      return styleInput;\n    }\n\n    // If math expression is valid, use it as a value\n    styleInput = parseCssValue(property, String(mathResult));\n\n    if (styleInput.type !== \"invalid\") {\n      return styleInput;\n    }\n  }\n\n  // Last chance probably it's a color without #\n  styleInput = parseCssValue(property, `#${value}`);\n\n  if (styleInput.type !== \"invalid\") {\n    return styleInput;\n  }\n\n  // Users often mistype comma instead of dot and we want to be tolerant to that.\n  // We need to try replace comma with dot and then try all parsing options again.\n  if (value.includes(\",\")) {\n    return parseIntermediateOrInvalidValue(\n      property,\n      {\n        ...styleValue,\n        value: value.replace(/,/g, \".\"),\n      },\n      defaultUnit,\n      originalValue ?? value\n    );\n  }\n\n  // If we are here it means that value can be Valid but our parseCssValue can't handle it\n  // or value is invalid\n  return {\n    type: \"invalid\",\n    value: originalValue ?? value,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { parseIntermediateOrInvalidValue } from \"./parse-intermediate-or-invalid-value\";\n\nconst properties = [\"width\", \"line-height\"] as const;\n\ntest(\"forgive trailing semicolon\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"width\", {\n      type: \"intermediate\",\n      value: \"20px;\",\n    })\n  ).toEqual({ type: \"unit\", value: 20, unit: \"px\" });\n  expect(\n    parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"red;\",\n    })\n  ).toEqual({ type: \"keyword\", value: \"red\" });\n});\n\ndescribe(\"Parse intermediate or invalid value without math evaluation\", () => {\n  test(\"not lost unit value\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10\",\n        unit: \"em\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 10,\n        unit: \"em\",\n      });\n    }\n  });\n\n  test.each(properties)(`fallback to px for property = \"%s\"`, (propery) => {\n    const result = parseIntermediateOrInvalidValue(propery, {\n      type: \"intermediate\",\n      value: \"10\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"px\",\n    });\n  });\n\n  test(\"fallback to % if px is not supported\", () => {\n    const result = parseIntermediateOrInvalidValue(\"opacity\", {\n      type: \"intermediate\",\n      value: \"10\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"%\",\n    });\n  });\n\n  test(\"switch on new unit if previous not known\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10rem\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 10,\n        unit: \"rem\",\n      });\n    }\n  });\n\n  test(\"switch on new unit if previous present\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10rem\",\n        unit: \"em\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 10,\n        unit: \"rem\",\n      });\n    }\n  });\n\n  test(\"accept keywords\", () => {\n    expect(\n      parseIntermediateOrInvalidValue(\"width\", {\n        type: \"intermediate\",\n        value: \"auto\",\n        unit: \"em\",\n      })\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n    expect(\n      parseIntermediateOrInvalidValue(\"line-height\", {\n        type: \"intermediate\",\n        value: \"normal\",\n        unit: \"em\",\n      })\n    ).toEqual({ type: \"keyword\", value: \"normal\" });\n  });\n\n  test(\"accept keywords written as pascal case\", () => {\n    expect(\n      parseIntermediateOrInvalidValue(\"width\", {\n        type: \"intermediate\",\n        value: \"Auto\",\n        unit: \"em\",\n      })\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n    expect(\n      parseIntermediateOrInvalidValue(\"line-height\", {\n        type: \"intermediate\",\n        value: \"Normal\",\n        unit: \"em\",\n      })\n    ).toEqual({ type: \"keyword\", value: \"normal\" });\n  });\n\n  test(\"keyword with pascal case name\", () => {\n    const result = parseIntermediateOrInvalidValue(\"box-sizing\", {\n      type: \"intermediate\",\n      value: \"Border Box\",\n      unit: \"em\",\n    });\n\n    expect(result).toEqual({\n      type: \"keyword\",\n      value: \"border-box\",\n    });\n  });\n\n  test(\"tolerate comma instead of dot typo\", () => {\n    const result = parseIntermediateOrInvalidValue(\"width\", {\n      type: \"intermediate\",\n      value: \"2,5\",\n      unit: \"rem\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 2.5,\n      unit: \"rem\",\n    });\n  });\n\n  test(\"tolerate comma instead of dot typo with unit input\", () => {\n    const result = parseIntermediateOrInvalidValue(\"width\", {\n      type: \"intermediate\",\n      value: \"2,5rem\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 2.5,\n      unit: \"rem\",\n    });\n  });\n\n  test(\"tolerate comma instead of dot typo while correctly parsing legit comma inside value\", () => {\n    const result = parseIntermediateOrInvalidValue(\"transition-duration\", {\n      type: \"intermediate\",\n      value: \"1s, 2s\",\n    });\n    expect(result).toEqual({\n      type: \"layers\",\n      value: [\n        { type: \"unit\", unit: \"s\", value: 1 },\n        { type: \"unit\", unit: \"s\", value: 2 },\n      ],\n    });\n  });\n});\n\ndescribe(\"Parse intermediate or invalid value with math evaluation\", () => {\n  test(\"not lost unit value\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10+10\",\n        unit: \"em\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 20,\n        unit: \"em\",\n      });\n    }\n  });\n\n  test(\"fallback to px\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10 + 10\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 20,\n        unit: \"px\",\n      });\n    }\n  });\n\n  test(\"tolerate comma instead of dot\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"1,1 + 1,2\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 2.3,\n        unit: \"px\",\n      });\n    }\n  });\n\n  test(\"tolerate comma instead of dot with unit\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"1,1px + 1,2rem\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 2.3,\n        unit: \"px\",\n      });\n    }\n  });\n\n  test(\"switch on new unit if previous not known\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10rem + 15px\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 25,\n        unit: \"rem\",\n      });\n    }\n  });\n\n  test(\"switch on new unit if previous present\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"intermediate\",\n        value: \"10rem + 15\",\n        unit: \"em\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 25,\n        unit: \"rem\",\n      });\n    }\n  });\n});\n\ndescribe(\"Parse invalid\", () => {\n  test(\"fallback to px\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"invalid\",\n        value: \"10\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 10,\n        unit: \"px\",\n      });\n    }\n  });\n\n  test(\"switch on new unit if previous not known\", () => {\n    for (const propery of properties) {\n      const result = parseIntermediateOrInvalidValue(propery, {\n        type: \"invalid\",\n        value: \"10rem + 15px\",\n      });\n\n      expect(result).toEqual({\n        type: \"unit\",\n        value: 25,\n        unit: \"rem\",\n      });\n    }\n  });\n});\n\ndescribe(\"Returns invalid if can't parse\", () => {\n  test(\"do not accept unknown units\", () => {\n    const result = parseIntermediateOrInvalidValue(\"width\", {\n      type: \"intermediate\",\n      value: \"10ee\",\n    });\n\n    expect(result).toEqual({\n      type: \"invalid\",\n      value: \"10ee\",\n    });\n  });\n\n  test(\"do not accept wrong keywords\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"auto\",\n    });\n\n    expect(result).toEqual({\n      type: \"invalid\",\n      value: \"auto\",\n    });\n  });\n});\n\ndescribe(\"Value ending with `-` should be considered unitless\", () => {\n  test(\"Unitless intermediate transformed to unitless\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"10-\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"number\",\n    });\n  });\n\n  test(\"Unit intermediate transformed to unitless\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"10-\",\n      unit: \"em\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"number\",\n    });\n  });\n\n  test(\"Unit intermediate with space transformed to unitless\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"10 -\",\n      unit: \"em\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"number\",\n    });\n  });\n\n  test(\"Unit number intermediate transformed to unitless\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"10\",\n      unit: \"number\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"number\",\n    });\n  });\n\n  test(\"Unitless expression transformed to unitless\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"10 + 20 -\",\n      unit: \"px\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 30,\n      unit: \"number\",\n    });\n  });\n\n  test(\"Expression containing unit and unitless must be a unit\", () => {\n    const result = parseIntermediateOrInvalidValue(\"line-height\", {\n      type: \"intermediate\",\n      value: \"10px + 20 -\",\n      unit: \"px\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 30,\n      unit: \"px\",\n    });\n  });\n\n  test(\"top with 0 should be unitless\", () => {\n    const result = parseIntermediateOrInvalidValue(\"top\", {\n      type: \"intermediate\",\n      value: \"0-\",\n      unit: \"em\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 0,\n      unit: \"number\",\n    });\n  });\n\n  test(\"top with value 10 should have unit px\", () => {\n    const result = parseIntermediateOrInvalidValue(\"top\", {\n      type: \"intermediate\",\n      value: \"10\",\n      unit: \"number\",\n    });\n\n    expect(result).toEqual({\n      type: \"unit\",\n      value: 10,\n      unit: \"px\",\n    });\n  });\n});\n\ndescribe(\"Colors\", () => {\n  test(\"color with value rgba(0,0,0,0)\", () => {\n    const result = parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"rgba(10,20,30,0.5)\",\n    });\n\n    expect(result).toEqual({\n      type: \"color\",\n      alpha: 0.5,\n      colorSpace: \"srgb\",\n      components: [0.0392, 0.0784, 0.1176],\n    });\n  });\n\n  test(\"color with value hex\", () => {\n    const result = parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"#f00\",\n    });\n\n    expect(result).toEqual({\n      type: \"color\",\n      alpha: 1,\n      colorSpace: \"srgb\",\n      components: [1, 0, 0],\n    });\n  });\n\n  test(\"color with value with long hex\", () => {\n    const result = parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"#f0ee0f\",\n    });\n\n    expect(result).toEqual({\n      type: \"color\",\n      alpha: 1,\n      colorSpace: \"srgb\",\n      components: [0.9412, 0.9333, 0.0588],\n    });\n  });\n\n  test(\"color with value hex without #\", () => {\n    const result = parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"f00\",\n    });\n\n    expect(result).toEqual({\n      type: \"color\",\n      alpha: 1,\n      colorSpace: \"srgb\",\n      components: [1, 0, 0],\n    });\n  });\n\n  test(\"color with value long hex without #\", () => {\n    const result = parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"f0ee0f\",\n    });\n\n    expect(result).toEqual({\n      type: \"color\",\n      alpha: 1,\n      colorSpace: \"srgb\",\n      components: [0.9412, 0.9333, 0.0588],\n    });\n  });\n\n  test(\"color with value rgba(0,0,0,0)\", () => {\n    const result = parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"rgba(10,20,30,0.5)\",\n      unit: \"px\",\n    });\n\n    expect(result).toEqual({\n      type: \"color\",\n      alpha: 0.5,\n      colorSpace: \"srgb\",\n      components: [0.0392, 0.0784, 0.1176],\n    });\n  });\n});\n\ntest(\"parse css variable reference\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"var(--color)\",\n    })\n  ).toEqual({\n    type: \"var\",\n    value: \"color\",\n  });\n});\n\ntest(\"parse unit in css variable\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"10px\",\n    })\n  ).toEqual({\n    type: \"unit\",\n    value: 10,\n    unit: \"px\",\n  });\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"10\",\n      unit: \"px\",\n    })\n  ).toEqual({\n    type: \"unit\",\n    value: 10,\n    unit: \"px\",\n  });\n});\n\ntest(\"prefer unitless css variable\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"1\",\n      unit: undefined,\n    })\n  ).toEqual({ type: \"unit\", value: 1, unit: \"number\" });\n\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"1\",\n      unit: \"number\",\n    })\n  ).toEqual({ type: \"unit\", value: 1, unit: \"number\" });\n});\n\ntest(\"parse color in css variable\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"#0f0f0f\",\n    })\n  ).toEqual({\n    type: \"color\",\n    colorSpace: \"srgb\",\n    components: [0.0588, 0.0588, 0.0588],\n    alpha: 1,\n  });\n});\n\ntest(\"parse css variables as unparsed\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"url(https://my-image.com)\",\n    })\n  ).toEqual({\n    type: \"unparsed\",\n    value: \"url(https://my-image.com)\",\n  });\n  expect(\n    parseIntermediateOrInvalidValue(\"--size\", {\n      type: \"intermediate\",\n      value: \"url(https://my-image.com)\",\n      unit: \"px\",\n    })\n  ).toEqual({\n    type: \"unparsed\",\n    value: \"url(https://my-image.com)\",\n  });\n});\n\ntest(\"parse z-index\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"z-index\", {\n      type: \"intermediate\",\n      value: \"6.5\",\n      unit: \"number\",\n    })\n  ).toEqual({\n    type: \"unit\",\n    value: 7,\n    unit: \"number\",\n  });\n});\n\ntest(\"parse color\", () => {\n  expect(\n    parseIntermediateOrInvalidValue(\"color\", {\n      type: \"intermediate\",\n      value: \"linear-gradient(red, blue)\",\n      unit: undefined,\n    })\n  ).toEqual({\n    type: \"invalid\",\n    value: \"linear-gradient(red, blue)\",\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/unit-select-options.ts",
    "content": "import type { CssValueInputValue } from \"./css-value-input\";\nimport {\n  propertiesData,\n  units,\n  isValidDeclaration,\n} from \"@webstudio-is/css-data\";\nimport type { UnitOption } from \"./unit-select\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\n\n// To make sorting stable\nconst preferedSorting = [\n  \"number\",\n  \"px\",\n  ...units.percentage,\n  \"em\",\n  \"rem\",\n  \"svw\",\n  \"svh\",\n  \"lvw\",\n  \"lvh\",\n  \"dvw\",\n  \"dvh\",\n  ...units.length,\n  ...units.angle,\n  ...units.decibel,\n  ...units.flex,\n  ...units.frequency,\n  ...units.resolution,\n  ...units.semitones,\n  ...units.time,\n];\n\nconst initialLengthUnits = [\n  \"px\",\n  \"em\",\n  \"rem\",\n  \"ch\",\n  \"svw\",\n  \"svh\",\n  \"lvw\",\n  \"lvh\",\n] as const;\n\nexport const buildOptions = (\n  property: CssProperty,\n  value: CssValueInputValue,\n  nestedSelectButtonUnitless: string\n) => {\n  const unit =\n    value.type === \"unit\" || value.type === \"intermediate\"\n      ? value.unit\n      : undefined;\n\n  const options: UnitOption[] = [];\n\n  // show at least current unit when no property meta is available\n  // for example in custom properties\n  const unitGroups = propertiesData[property]?.unitGroups ?? [];\n\n  for (const unitGroup of unitGroups) {\n    if (unitGroup === \"number\") {\n      options.push({\n        id: \"number\",\n        type: \"unit\",\n        label: nestedSelectButtonUnitless,\n      });\n      continue;\n    }\n\n    const visibleUnits =\n      unitGroup === \"length\" ? initialLengthUnits : units[unitGroup];\n    for (const unit of visibleUnits) {\n      options.push({\n        id: unit,\n        type: \"unit\",\n        label: unit,\n      });\n    }\n  }\n\n  // Special case for 0, which is often used as a unitless value\n  const showUnitless =\n    value.type === \"unit\" || value.type === \"intermediate\"\n      ? isValidDeclaration(property, `${value.value}`)\n      : false;\n\n  if (\n    showUnitless &&\n    options.some((option) => option.id === \"number\") === false\n  ) {\n    options.push({\n      id: \"number\",\n      type: \"unit\",\n      label: nestedSelectButtonUnitless,\n    });\n  }\n\n  // Add a valid unit, such as ch or vw, to the list of options, even if it's not already visible\n  // This allows the currently selected unit to be displayed selected when the menu is opened\n  if (\n    unit !== undefined &&\n    options.some((option) => option.id === unit) === false\n  ) {\n    options.push({\n      id: unit,\n      type: \"unit\",\n      label: unit === \"number\" ? nestedSelectButtonUnitless : unit,\n    });\n  }\n\n  const indexSortValue = (number: number) =>\n    number === -1 ? Number.POSITIVE_INFINITY : number;\n\n  // Use a stable sort for known dimensions, such as percentages after lengths\n  // This ensures that the order of options remains consistent between renders\n  options.sort(\n    (optionA, optionB) =>\n      indexSortValue(preferedSorting.indexOf(optionA.id)) -\n      indexSortValue(preferedSorting.indexOf(optionB.id))\n  );\n\n  return options;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/unit-select.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { nestedSelectButtonUnitless } from \"@webstudio-is/design-system\";\nimport { buildOptions } from \"./unit-select-options\";\n\ndescribe(\"Unit menu options\", () => {\n  test(\"Should show options\", () => {\n    expect(\n      buildOptions(\n        \"width\",\n        { value: 10, type: \"unit\", unit: \"px\" },\n        nestedSelectButtonUnitless\n      )\n    ).toMatchInlineSnapshot(`\n[\n  {\n    \"id\": \"px\",\n    \"label\": \"px\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"%\",\n    \"label\": \"%\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"em\",\n    \"label\": \"em\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"rem\",\n    \"label\": \"rem\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"svw\",\n    \"label\": \"svw\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"svh\",\n    \"label\": \"svh\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"lvw\",\n    \"label\": \"lvw\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"lvh\",\n    \"label\": \"lvh\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"ch\",\n    \"label\": \"ch\",\n    \"type\": \"unit\",\n  },\n]\n`);\n  });\n\n  test(\"Should show options with unitless if value supports unitless\", () => {\n    expect(\n      buildOptions(\n        \"width\",\n        { value: 0, type: \"unit\", unit: \"px\" },\n        nestedSelectButtonUnitless\n      ).some((option) => option.id === \"number\")\n    ).toBe(true);\n  });\n\n  test(\"Should show options with units if value is keyword\", () => {\n    expect(\n      buildOptions(\n        \"width\",\n        { value: \"auto\", type: \"keyword\" },\n        nestedSelectButtonUnitless\n      )\n    ).toMatchInlineSnapshot(`\n[\n  {\n    \"id\": \"px\",\n    \"label\": \"px\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"%\",\n    \"label\": \"%\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"em\",\n    \"label\": \"em\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"rem\",\n    \"label\": \"rem\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"svw\",\n    \"label\": \"svw\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"svh\",\n    \"label\": \"svh\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"lvw\",\n    \"label\": \"lvw\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"lvh\",\n    \"label\": \"lvh\",\n    \"type\": \"unit\",\n  },\n  {\n    \"id\": \"ch\",\n    \"label\": \"ch\",\n    \"type\": \"unit\",\n  },\n]\n`);\n  });\n\n  test(\"Should add unit to options even if it's not in a visibleLengthUnits\", () => {\n    expect(\n      buildOptions(\n        \"width\",\n        { value: 10, type: \"unit\", unit: \"ch\" },\n        nestedSelectButtonUnitless\n      ).some((option) => option.id === \"ch\")\n    ).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/unit-select.tsx",
    "content": "import { useState, useMemo, type JSX } from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport type { CssProperty, Unit } from \"@webstudio-is/css-engine\";\nimport {\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n  SelectViewport,\n  SelectItem,\n  SelectContent,\n  NestedInputButton,\n  nestedSelectButtonUnitless,\n} from \"@webstudio-is/design-system\";\nimport { ChevronDownIcon, ChevronUpIcon } from \"@webstudio-is/icons\";\nimport type { CssValueInputValue } from \"./css-value-input\";\nimport { buildOptions } from \"./unit-select-options\";\n\nexport type UnitOption =\n  | {\n      id: Unit;\n      label: string;\n      type: \"unit\";\n    }\n  | { id: string; label: string; type: \"keyword\" };\n\ntype UseUnitSelectType = {\n  property: CssProperty;\n  value: CssValueInputValue;\n  onChange: (\n    value: { type: \"unit\"; value: Unit } | { type: \"keyword\"; value: string }\n  ) => void;\n  onCloseAutoFocus: (event: Event) => void;\n  options?: UnitOption[];\n};\n\nexport const useUnitSelect = ({\n  property,\n  value,\n  onChange,\n  onCloseAutoFocus,\n  options: unitOptions,\n}: UseUnitSelectType): [boolean, JSX.Element | undefined] => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const options = useMemo(\n    () =>\n      unitOptions ?? buildOptions(property, value, nestedSelectButtonUnitless),\n    [property, value, unitOptions]\n  );\n\n  const unit =\n    value.type === \"unit\" || value.type === \"intermediate\"\n      ? value.unit\n      : undefined;\n\n  const unitOrKeywordValue: string | undefined =\n    unit ?? (value.type === \"keyword\" ? value.value : undefined);\n\n  if (\n    options.length === 0 ||\n    value.type === \"var\" ||\n    value.type === \"unparsed\" ||\n    value.type === \"invalid\" ||\n    unitOrKeywordValue === undefined\n  ) {\n    return [isOpen, undefined];\n  }\n\n  const select = (\n    <UnitSelect\n      value={unitOrKeywordValue}\n      label={unit ?? nestedSelectButtonUnitless}\n      options={options}\n      open={isOpen}\n      onCloseAutoFocus={onCloseAutoFocus}\n      onOpenChange={setIsOpen}\n      onChange={(option) => {\n        if (option.type === \"keyword\") {\n          onChange({ type: \"keyword\", value: option.id });\n          return;\n        }\n\n        onChange({ type: \"unit\", value: option.id });\n      }}\n    />\n  );\n\n  return [isOpen, select];\n};\n\ntype UnitSelectProps = {\n  options: Array<UnitOption>;\n  value?: string | undefined;\n  label?: string | undefined;\n  onChange: (value: UnitOption) => void;\n  onOpenChange: (open: boolean) => void;\n  onCloseAutoFocus: (event: Event) => void;\n  open: boolean;\n};\n\nconst UnitSelect = ({\n  options,\n  value,\n  label,\n  onChange,\n  onOpenChange,\n  onCloseAutoFocus,\n  open,\n}: UnitSelectProps) => {\n  return (\n    <SelectPrimitive.Root\n      value={value}\n      onValueChange={(value) => {\n        const optionValue = options.find((option) => option.id === value);\n        if (optionValue === undefined) {\n          return;\n        }\n        onChange(optionValue);\n      }}\n      onOpenChange={onOpenChange}\n      open={open}\n    >\n      <SelectPrimitive.SelectTrigger asChild>\n        <NestedInputButton tabIndex={-1}>\n          <SelectPrimitive.Value>\n            {value === \"number\" ? nestedSelectButtonUnitless : label}\n          </SelectPrimitive.Value>\n        </NestedInputButton>\n      </SelectPrimitive.SelectTrigger>\n      <SelectPrimitive.Portal>\n        <SelectContent\n          onCloseAutoFocus={onCloseAutoFocus}\n          onEscapeKeyDown={() => {\n            // We need to use onEscapeKeyDown and close explicitly as we prevented default at onKeyDown\n            // We can't prevent this event here as it's too late and the non-prevented event is already dispatched\n            // to the ancestors\n            onOpenChange(false);\n          }}\n          onKeyDown={(event) => {\n            // Prevent Esc key to be processed at the parent Component\n            if (event.key === \"Escape\") {\n              event.preventDefault();\n            }\n          }}\n        >\n          <SelectScrollUpButton>\n            <ChevronUpIcon />\n          </SelectScrollUpButton>\n          <SelectViewport>\n            {options.map(({ id, label }) => (\n              <SelectItem key={id} value={id} text=\"sentence\">\n                {label}\n              </SelectItem>\n            ))}\n          </SelectViewport>\n          <SelectScrollDownButton>\n            <ChevronDownIcon />\n          </SelectScrollDownButton>\n        </SelectContent>\n      </SelectPrimitive.Portal>\n    </SelectPrimitive.Root>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/css-value-input/value-editor-dialog.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport {\n  NestedInputButton,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { MaximizeIcon } from \"@webstudio-is/icons\";\nimport type {\n  CssProperty,\n  InvalidValue,\n  StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { EditorDialog } from \"~/shared/code-editor-base\";\nimport { CssFragmentEditorContent } from \"../css-fragment\";\nimport type {\n  CssValueInputValue,\n  IntermediateStyleValue,\n} from \"./css-value-input\";\nimport { parseIntermediateOrInvalidValue } from \"./parse-intermediate-or-invalid-value\";\n\nexport const cssButtonDisplay = \"--ws-css-value-input-maximize-button-display\";\n\n// Hand-picking values that are considered complex and should get a maximize button for the dialog.\n// Not showing the maximize everywhere because most values don't need that and it takes space.\nexport const isComplexValue = (value: CssValueInputValue) => {\n  if (value.type === \"unparsed\" || value.type === \"function\") {\n    return true;\n  }\n\n  if (value.type === \"tuple\" || value.type === \"layers\") {\n    for (const nestedValue of value.value) {\n      const nestedValueIsComplex = isComplexValue(nestedValue);\n      if (nestedValueIsComplex) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n};\n\nexport const ValueEditorDialog = ({\n  property,\n  value,\n  onChangeComplete,\n}: {\n  property: CssProperty;\n  value: string;\n  onChangeComplete: (value: StyleValue) => void;\n}) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    IntermediateStyleValue | InvalidValue | undefined\n  >({ type: \"intermediate\", value });\n\n  useEffect(() => {\n    setIntermediateValue({ type: \"intermediate\", value });\n  }, [value]);\n\n  const handleChange = (value: string) => {\n    const parsedValue = parseIntermediateOrInvalidValue(property, {\n      type: \"intermediate\",\n      value,\n    });\n\n    if (parsedValue.type === \"invalid\") {\n      setIntermediateValue({\n        type: \"invalid\",\n        value,\n      });\n      return;\n    }\n\n    if (parsedValue) {\n      setIntermediateValue({\n        type: \"intermediate\",\n        value,\n      });\n    }\n\n    return parsedValue;\n  };\n\n  const handleChangeComplete = (value: string) => {\n    const parsedValue = handleChange(value);\n    if (parsedValue) {\n      onChangeComplete(parsedValue);\n    }\n  };\n\n  return (\n    <EditorDialog\n      title=\"CSS Value\"\n      placement=\"bottom-within\"\n      height={200}\n      width={Number.parseFloat(rawTheme.sizes.sidebarWidth)}\n      content={\n        <CssFragmentEditorContent\n          autoFocus\n          value={intermediateValue?.value ?? value ?? \"\"}\n          invalid={intermediateValue?.type === \"invalid\"}\n          showShortcuts\n          onChange={handleChange}\n          onChangeComplete={handleChangeComplete}\n        />\n      }\n    >\n      <NestedInputButton\n        tabIndex={-1}\n        css={{\n          display: `var(${cssButtonDisplay}, none)`,\n          background: theme.colors.backgroundControls,\n          '&[data-state=\"open\"]': {\n            display: \"block\",\n          },\n        }}\n      >\n        <MaximizeIcon size={12} />\n      </NestedInputButton>\n    </EditorDialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/filter-content.tsx",
    "content": "import {\n  type InvalidValue,\n  type TupleValue,\n  toValue,\n  StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  Flex,\n  theme,\n  Label,\n  TextArea,\n  textVariants,\n  Separator,\n  Select,\n  Grid,\n} from \"@webstudio-is/design-system\";\nimport { useEffect, useState, type JSX } from \"react\";\nimport {\n  CssValueInputContainer,\n  type IntermediateStyleValue,\n} from \"../shared/css-value-input\";\nimport { parseCssValue } from \"@webstudio-is/css-data\";\nimport type { StyleUpdateOptions } from \"../shared/use-style-data\";\nimport { ShadowContent } from \"./shadow-content\";\nimport { parseCssFragment } from \"./css-fragment\";\n\n// filters can't be validated directly in the css-engine. Because, these are not properties\n// but functions that proeprties accept. So, we need to validate them manually using fake proeprties\n// which accepts the same values as the filter functions. This is a bit hacky but it works.\n//\n// https://developer.mozilla.org/en-US/docs/Web/CSS/opacity#syntax\n// number  | percentage\n//\n// https://developer.mozilla.org/en-US/docs/Web/CSS/outline-offset#syntax\n// length\n// https://developer.mozilla.org/en-US/docs/Web/CSS/rotate#formal_syntax\n// angle\n\nconst filterFunctions = {\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur#syntax\n  // length\n  blur: { default: \"0px\", fakeProperty: \"outline-offset\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/brightness#formal_syntax\n  // number | percentage\n  brightness: { default: \"0%\", fakeProperty: \"opacity\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/contrast#formal_syntax\n  // number  | percentage\n  contrast: { default: \"0%\", fakeProperty: \"opacity\" },\n  // text-shaodw and drop-shaodow accepts the same args so we can use the same component\n  // and pass the args as value and property\n  \"drop-shadow\": {\n    default: \"0px 2px 5px rgba(0, 0, 0, 0.2)\",\n    fakeProperty: \"text-shadow\",\n  },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/grayscale#syntax\n  // number  | percentage\n  grayscale: { default: \"0%\", fakeProperty: \"opacity\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/hue-rotate#syntax\n  // angle\n  \"hue-rotate\": { default: \"0deg\", fakeProperty: \"rotate\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/invert#syntax\n  // number  | percentage\n  invert: { default: \"0%\", fakeProperty: \"opacity\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/opacity#syntax\n  // number  | percentage\n  opacity: { default: \"0%\", fakeProperty: \"opacity\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/saturate#syntax\n  // number | percentage\n  saturate: { default: \"0%\", fakeProperty: \"opacity\" },\n  // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/sepia#parameters\n  // number | percentage\n  sepia: { default: \"0%\", fakeProperty: \"opacity\" },\n} as const;\n\ntype FilterContentProps = {\n  index: number;\n  property: \"filter\" | \"backdrop-filter\";\n  layer: StyleValue;\n  propertyValue: string;\n  tooltip: JSX.Element;\n  onEditLayer: (\n    index: number,\n    layers: TupleValue,\n    options: StyleUpdateOptions\n  ) => void;\n};\n\ntype FilterFunction = keyof typeof filterFunctions;\n\nconst isFilterFunction = (value: string): value is FilterFunction =>\n  Object.keys(filterFunctions).includes(value);\n\nexport const FilterSectionContent = ({\n  index,\n  property,\n  propertyValue,\n  onEditLayer,\n  tooltip,\n  layer,\n}: FilterContentProps) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    IntermediateStyleValue | InvalidValue | undefined\n  >();\n  const [filterFunction, setFilterFunction] = useState<\n    FilterFunction | undefined\n  >(undefined);\n  const [filterFunctionValue, setFilterFunctionValue] = useState<\n    StyleValue | undefined\n  >(undefined);\n\n  useEffect(() => {\n    if (layer.type !== \"function\" || isFilterFunction(layer.name) === false) {\n      return;\n    }\n    if (layer.args.type !== \"tuple\" && layer.args.type !== \"shadow\") {\n      return;\n    }\n    setFilterFunction(layer.name);\n    setIntermediateValue({\n      type: \"intermediate\",\n      value: propertyValue,\n    });\n    if (layer.args.type === \"tuple\") {\n      setFilterFunctionValue(layer.args.value[0]);\n    }\n    if (layer.args.type === \"shadow\") {\n      setFilterFunctionValue(layer.args);\n    }\n  }, [layer, propertyValue]);\n\n  const handleFilterFunctionChange = (filterName: FilterFunction) => {\n    const defaultFilterValue = filterFunctions[filterName];\n    setFilterFunction(filterName);\n    const functionValue = parseCssValue(\n      defaultFilterValue.fakeProperty,\n      defaultFilterValue.default\n    );\n\n    setFilterFunctionValue(functionValue);\n    handleComplete(`${filterName}(${toValue(functionValue)})`);\n  };\n\n  const handleFilterFunctionValueChange = (\n    value: StyleValue,\n    options: StyleUpdateOptions = { isEphemeral: false }\n  ) => {\n    setFilterFunctionValue(value);\n    handleComplete(`${filterFunction}(${toValue(value)})`, options);\n  };\n\n  const handleComplete = (\n    value: string,\n    options: StyleUpdateOptions = { isEphemeral: false }\n  ) => {\n    const parsed = parseCssFragment(value, [property]);\n    const parsedValue = parsed.get(property);\n    const invalid = parsedValue === undefined || parsedValue.type === \"invalid\";\n    setIntermediateValue({\n      type: invalid ? \"invalid\" : \"intermediate\",\n      value,\n    });\n    if (parsedValue?.type === \"tuple\") {\n      onEditLayer(index, parsedValue, options);\n    }\n  };\n\n  return (\n    <Flex direction=\"column\">\n      <Flex direction=\"column\" gap=\"2\" css={{ padding: theme.panel.padding }}>\n        <Grid\n          gap=\"2\"\n          css={{\n            gridTemplateColumns: \"1fr 3fr\",\n            alignItems: \"center\",\n          }}\n        >\n          <Flex align=\"center\">\n            <Label>Function</Label>\n          </Flex>\n          <Select\n            name=\"filterFunction\"\n            placeholder=\"Select Filter\"\n            options={Object.keys(filterFunctions) as FilterFunction[]}\n            value={filterFunction ?? \"blur\"}\n            onChange={handleFilterFunctionChange}\n          />\n        </Grid>\n        {filterFunction !== \"drop-shadow\" ? (\n          <Grid\n            gap=\"2\"\n            css={{\n              gridTemplateColumns: \"1fr 3fr\",\n              alignItems: \"center\",\n            }}\n          >\n            <Flex align=\"center\">\n              <Label>Value</Label>\n            </Flex>\n            <CssValueInputContainer\n              key=\"functionValue\"\n              property={\n                filterFunction\n                  ? filterFunctions[filterFunction].fakeProperty\n                  : \"outline-offset\"\n              }\n              styleSource=\"local\"\n              value={\n                filterFunctionValue ?? {\n                  type: \"unit\",\n                  value: 0,\n                  unit: \"px\",\n                }\n              }\n              onUpdate={handleFilterFunctionValueChange}\n              onDelete={() => {}}\n            />\n          </Grid>\n        ) : undefined}\n      </Flex>\n\n      {filterFunction === \"drop-shadow\" && layer.type === \"function\" && (\n        <ShadowContent\n          index={index}\n          property=\"drop-shadow\"\n          layer={layer.args}\n          propertyValue={toValue(layer.args)}\n          hideCodeEditor={true}\n          onEditLayer={(_, dropShadowLayers, options) => {\n            handleComplete(\n              `drop-shadow(${toValue(dropShadowLayers)})`,\n              options\n            );\n          }}\n        />\n      )}\n\n      <Separator css={{ gridAutoColumns: \"span 2\" }} />\n      <Flex\n        direction=\"column\"\n        css={{\n          padding: theme.panel.padding,\n          gap: theme.spacing[3],\n          minWidth: theme.spacing[30],\n        }}\n      >\n        <Label>\n          <Flex align={\"center\"} gap={1}>\n            Code\n            {tooltip}\n          </Flex>\n        </Label>\n        <TextArea\n          rows={3}\n          name=\"description\"\n          value={intermediateValue?.value ?? \"\"}\n          css={{ minHeight: theme.spacing[14], ...textVariants.mono }}\n          color={intermediateValue?.type === \"invalid\" ? \"error\" : undefined}\n          onChange={(value) =>\n            setIntermediateValue({ type: \"intermediate\", value })\n          }\n          onBlur={() => {\n            if (intermediateValue !== undefined) {\n              handleComplete(intermediateValue.value);\n            }\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\" && intermediateValue !== undefined) {\n              handleComplete(intermediateValue.value);\n              // On pressing Enter, the textarea is creating a new line.\n              // In-order to prevent it and update the content.\n              // We prevent the default behaviour\n              event.preventDefault();\n            }\n          }}\n        />\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/instances-kv.ts",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { map } from \"nanostores\";\nimport { useCallback } from \"react\";\nimport { $selectedInstance } from \"~/shared/awareness\";\n\nconst instancesKv = map<Record<string, unknown>>({});\n\n/**\n * This code creates a selected instance key-value store that maintains instance-specific state for a UI.\n * It differs from useState in that it uses defaultValue instead of initialValue as the second parameter,\n * allowing the default UI behavior to be used until the user modifies the state.\n */\nexport const useSelectedInstanceKv = <T>(key: string, defaultValue: T) => {\n  const instance = useStore($selectedInstance);\n  const instanceKey = `${instance?.id}-${key}`;\n\n  const mapStore = useStore(instancesKv, {\n    keys: [instanceKey],\n  });\n\n  const setValue = useCallback(\n    (value: T) => {\n      instancesKv.setKey(instanceKey, value);\n    },\n    [instanceKey]\n  );\n\n  return [(mapStore[instanceKey] as T) ?? defaultValue, setValue] as const;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/model.tsx",
    "content": "import { useMemo, useRef } from \"react\";\nimport type { HtmlTags } from \"html-tags\";\nimport { computed, type ReadableAtom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { propertiesData } from \"@webstudio-is/css-data\";\nimport {\n  compareMedia,\n  hyphenateProperty,\n  toVarFallback,\n  type CssProperty,\n  type StyleValue,\n  type VarValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  ROOT_INSTANCE_ID,\n  type Styles,\n  type StyleSourceSelections,\n  type Breakpoint,\n  type Instance,\n  type WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport { rootComponent } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $props,\n  $registeredComponentMetas,\n  $selectedBreakpoint,\n  $selectedInstanceStates,\n  $selectedOrLastStyleSourceSelector,\n  $styles,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport {\n  getComputedStyleDecl,\n  getPresetStyleDeclKey,\n  type ComputedStyleDecl,\n  type StyleObjectModel,\n} from \"~/shared/style-object-model\";\nimport {\n  $selectedInstancePathWithRoot,\n  type InstancePath,\n} from \"~/shared/awareness\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\n\nconst $presetStyles = computed($registeredComponentMetas, (metas) => {\n  const presetStyles = new Map<string, StyleValue>();\n  for (const [component, meta] of metas) {\n    for (const [tag, styles] of Object.entries(meta.presetStyle ?? {})) {\n      for (const styleDecl of styles) {\n        const key = getPresetStyleDeclKey({\n          component,\n          tag,\n          state: styleDecl.state,\n          property: styleDecl.property,\n        });\n        presetStyles.set(key, styleDecl.value);\n      }\n    }\n  }\n  return presetStyles;\n});\n\nconst $tagByInstanceId = computed($props, (props) => {\n  const tagByInstanceId = new Map<Instance[\"id\"], string>();\n  for (const prop of props.values()) {\n    if (prop.type === \"string\" && prop.name === \"tag\") {\n      tagByInstanceId.set(prop.instanceId, prop.value);\n    }\n  }\n  return tagByInstanceId;\n});\n\nexport const $instanceTags = computed(\n  [$registeredComponentMetas, $selectedInstancePathWithRoot, $tagByInstanceId],\n  (metas, instancePath, tagByInstanceId) => {\n    const instanceTags = new Map<Instance[\"id\"], HtmlTags>();\n    if (instancePath === undefined) {\n      return instanceTags;\n    }\n    for (const { instance } of instancePath) {\n      const meta = metas.get(instance.component);\n      const tags = Object.keys(meta?.presetStyle ?? {});\n      if (tags.length > 0) {\n        const metaTag = tags[0];\n        const propTag = tagByInstanceId.get(instance.id);\n        const tag = instance.tag ?? propTag ?? metaTag;\n        instanceTags.set(instance.id, tag as HtmlTags);\n      }\n    }\n    return instanceTags;\n  }\n);\n\nconst $instanceComponents = computed(\n  $selectedInstancePathWithRoot,\n  (instancePath) => {\n    const instanceComponents = new Map<Instance[\"id\"], Instance[\"component\"]>([\n      [ROOT_INSTANCE_ID, rootComponent],\n    ]);\n    if (instancePath === undefined) {\n      return instanceComponents;\n    }\n    // store only component for selected instance and ancestors\n    // to avoid iterating over all instances in the project\n    for (const { instance } of instancePath) {\n      instanceComponents.set(instance.id, instance.component);\n    }\n    return instanceComponents;\n  }\n);\n\nexport const $matchingBreakpoints = computed(\n  [$breakpoints, $selectedBreakpoint],\n  (breakpoints, selectedBreakpoint) => {\n    const sortedBreakpoints = Array.from(breakpoints.values()).sort(\n      compareMedia\n    );\n    const matchingBreakpoints: Breakpoint[\"id\"][] = [];\n    for (const breakpoint of sortedBreakpoints) {\n      matchingBreakpoints.push(breakpoint.id);\n      if (breakpoint.id === selectedBreakpoint?.id) {\n        break;\n      }\n    }\n    return matchingBreakpoints;\n  }\n);\n\nconst getDefinedStyles = ({\n  instancePath,\n  metas,\n  matchingBreakpoints: matchingBreakpointsArray,\n  styleSourceSelections,\n  styles,\n}: {\n  instancePath: InstancePath;\n  metas: Map<string, WsComponentMeta>;\n  matchingBreakpoints: Breakpoint[\"id\"][];\n  styleSourceSelections: StyleSourceSelections;\n  styles: Styles;\n}) => {\n  type Defined = {\n    property: CssProperty;\n    listed?: boolean;\n  };\n\n  const inheritedStyleSources = new Set();\n  const instanceStyleSources = new Set();\n  const matchingBreakpoints = new Set(matchingBreakpointsArray);\n  const startingInstanceSelector = instancePath[0].instanceSelector;\n\n  const instanceStyles = new Set<Defined>();\n  const inheritedStyles = new Set<Defined>();\n  const presetStyles = new Set<Defined>();\n\n  for (const { instance } of instancePath) {\n    const meta = metas.get(instance.component);\n    for (const preset of Object.values(meta?.presetStyle ?? {})) {\n      for (const styleDecl of preset) {\n        presetStyles.add({ property: styleDecl.property });\n      }\n    }\n    const styleSources = styleSourceSelections.get(instance.id)?.values;\n    if (styleSources) {\n      for (const styleSourceId of styleSources) {\n        if (instance.id === startingInstanceSelector[0]) {\n          instanceStyleSources.add(styleSourceId);\n        } else {\n          inheritedStyleSources.add(styleSourceId);\n        }\n      }\n    }\n  }\n  for (const styleDecl of styles.values()) {\n    const property = hyphenateProperty(styleDecl.property);\n    if (\n      matchingBreakpoints.has(styleDecl.breakpointId) &&\n      instanceStyleSources.has(styleDecl.styleSourceId)\n    ) {\n      instanceStyles.add({\n        property,\n        listed: styleDecl.listed,\n      });\n    }\n    // custom properties are always inherited\n    const inherited = propertiesData[property]?.inherited ?? true;\n    if (\n      matchingBreakpoints.has(styleDecl.breakpointId) &&\n      inheritedStyleSources.has(styleDecl.styleSourceId) &&\n      inherited\n    ) {\n      inheritedStyles.add({\n        property,\n        listed: styleDecl.listed,\n      });\n    }\n  }\n\n  // We are sorting by alphabet within each group.\n  const sortByProperty = (a: { property: string }, b: { property: string }) => {\n    return Intl.Collator().compare(a.property, b.property);\n  };\n\n  return [\n    ...Array.from(instanceStyles).sort(sortByProperty),\n    ...Array.from(inheritedStyles).sort(sortByProperty),\n    ...Array.from(presetStyles).sort(sortByProperty),\n  ];\n};\n\nexport const $styleObjectModel = computed(\n  [\n    $styles,\n    $styleSourceSelections,\n    $presetStyles,\n    $instanceTags,\n    $instanceComponents,\n    $matchingBreakpoints,\n    $selectedInstanceStates,\n  ],\n  (\n    styles,\n    styleSourceSelections,\n    presetStyles,\n    instanceTags,\n    instanceComponents,\n    matchingBreakpoints,\n    matchingStates\n  ): StyleObjectModel => {\n    return {\n      styles,\n      styleSourceSelections,\n      presetStyles,\n      instanceTags,\n      instanceComponents,\n      matchingBreakpoints,\n      matchingStates,\n    };\n  }\n);\n\nexport const $computedStyleDeclarations = computed(\n  [\n    $styleObjectModel,\n    $selectedInstancePathWithRoot,\n    $selectedOrLastStyleSourceSelector,\n    $registeredComponentMetas,\n    $matchingBreakpoints,\n    $styleSourceSelections,\n    $styles,\n  ],\n  (\n    model,\n    instancePath,\n    styleSourceSelector,\n    metas,\n    matchingBreakpoints,\n    styleSourceSelections,\n    styles\n  ) => {\n    if (instancePath === undefined) {\n      return [];\n    }\n    const definedStyles = getDefinedStyles({\n      instancePath,\n      metas,\n      matchingBreakpoints,\n      styleSourceSelections,\n      styles,\n    });\n    // In advanced mode we assume user knows the properties they need, so we don't need to show these.\n    // @todo will be fully deleted https://github.com/webstudio-is/webstudio/issues/4871\n    definedStyles.push(\n      { property: \"cursor\" },\n      { property: \"mix-blend-mode\" },\n      { property: \"opacity\" },\n      { property: \"pointer-events\" },\n      { property: \"user-select\" }\n    );\n    const computedStyles = new Map<string, ComputedStyleDecl>();\n    for (const { property, listed } of definedStyles) {\n      // deduplicate by property name\n      if (computedStyles.has(property)) {\n        continue;\n      }\n      const computedStyleDecl = getComputedStyleDecl({\n        model,\n        instanceSelector: instancePath?.[0].instanceSelector,\n        styleSourceId: styleSourceSelector?.styleSourceId,\n        state: styleSourceSelector?.state,\n        property,\n      });\n      // @todo We will delete it once we have added additional filters to advanced panel and\n      // don't need to differentiate this any more.\n      computedStyleDecl.listed = listed;\n\n      computedStyles.set(property, computedStyleDecl);\n    }\n    return Array.from(computedStyles.values());\n  }\n);\n\nexport const $availableVariables = computed(\n  $computedStyleDeclarations,\n  (computedStyles) => {\n    const availableVariables: VarValue[] = [];\n    for (const styleDecl of computedStyles) {\n      if (styleDecl.property.startsWith(\"--\")) {\n        availableVariables.push({\n          type: \"var\",\n          value: styleDecl.property.slice(2),\n          fallback: toVarFallback(styleDecl.computedValue),\n        });\n      }\n    }\n    return availableVariables;\n  }\n);\n\nexport const $availableUnitVariables = computed(\n  $availableVariables,\n  (availableVariables) =>\n    availableVariables.filter(\n      (value) =>\n        value.fallback?.type === \"unit\" ||\n        value.fallback?.type === \"keyword\" ||\n        value.fallback?.type === \"unparsed\"\n    )\n);\n\nexport const $availableColorVariables = computed(\n  $availableVariables,\n  (availableVariables) =>\n    availableVariables.filter(\n      (value) =>\n        value.fallback?.type === \"rgb\" ||\n        value.fallback?.type === \"color\" ||\n        value.fallback?.type === \"keyword\" ||\n        value.fallback?.type === \"unparsed\"\n    )\n);\n\nexport const createComputedStyleDeclStore = (property: CssProperty) => {\n  return computed(\n    [\n      $styleObjectModel,\n      $selectedInstancePathWithRoot,\n      $selectedOrLastStyleSourceSelector,\n    ],\n    (model, instancePath, styleSourceSelector) => {\n      return getComputedStyleDecl({\n        model,\n        instanceSelector: instancePath?.[0].instanceSelector,\n        styleSourceId: styleSourceSelector?.styleSourceId,\n        state: styleSourceSelector?.state,\n        property,\n      });\n    }\n  );\n};\n\nexport const useStyleObjectModel = () => {\n  return useStore($styleObjectModel);\n};\n\nexport const useComputedStyleDecl = (property: CssProperty) => {\n  const $store = useMemo(\n    () => createComputedStyleDeclStore(property),\n    [property]\n  );\n  return useStore($store);\n};\n\nconst $closestStylableInstanceSelector = computed(\n  [$selectedInstancePathWithRoot, $registeredComponentMetas],\n  (instancePath, metas) => {\n    // ignore unstylable instances which do not affect parent/child relationships\n    if (instancePath === undefined) {\n      return;\n    }\n    const match = instancePath.find(({ instance }, index) => {\n      // start with parent\n      if (index === 0) {\n        return false;\n      }\n      return metas.get(instance.component)?.presetStyle !== undefined;\n    });\n    return match?.instanceSelector;\n  }\n);\n\nexport const useParentComputedStyleDecl = (property: CssProperty) => {\n  const $store = useMemo(\n    () =>\n      computed(\n        [$styleObjectModel, $closestStylableInstanceSelector],\n        (model, instanceSelector) => {\n          return getComputedStyleDecl({\n            model,\n            instanceSelector,\n            property,\n          });\n        }\n      ),\n    [property]\n  );\n  return useStore($store);\n};\n\nexport const getInstanceStyleDecl = (\n  property: CssProperty,\n  instanceSelector: InstanceSelector\n) => {\n  return getComputedStyleDecl({\n    model: $styleObjectModel.get(),\n    instanceSelector,\n    property,\n  });\n};\n\nexport const useComputedStyles = (properties: CssProperty[]) => {\n  // cache each computed style store\n  const cachedStores = useRef(\n    new Map<CssProperty, ReadableAtom<ComputedStyleDecl>>()\n  );\n  const stores: ReadableAtom<ComputedStyleDecl>[] = [];\n  for (const property of properties) {\n    let store = cachedStores.current.get(property);\n    if (store === undefined) {\n      store = createComputedStyleDeclStore(property);\n      cachedStores.current.set(property, store);\n    }\n    stores.push(store);\n  }\n  // combine all styles into single list\n  const $styles = useMemo(() => {\n    return computed(stores, (...computedStyles) => computedStyles);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [properties.join()]);\n  return useStore($styles);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/modifier-keys.ts",
    "content": "import { useState, useEffect } from \"react\";\nimport { shallowEqual } from \"shallow-equal\";\n\nexport type Modifiers = {\n  altKey: boolean;\n  shiftKey: boolean;\n  ctrlKey: boolean;\n  metaKey: boolean;\n};\n\n// Used for combined mouse and keyboard interactions, like scrubbing while holding ALT.\n// If it's just a keyboard interaction, you should already have a keyboard event at hand.\nexport const useModifierKeys = () => {\n  const [state, setState] = useState({\n    shiftKey: false,\n    altKey: false,\n    ctrlKey: false,\n    metaKey: false,\n  });\n\n  useEffect(() => {\n    const handler = (event: KeyboardEvent | MouseEvent) => {\n      const newState = {\n        shiftKey: event.shiftKey,\n        altKey: event.altKey,\n        ctrlKey: event.ctrlKey,\n        metaKey: event.metaKey,\n      };\n\n      setState((prev) => {\n        if (shallowEqual(prev, newState)) {\n          return prev;\n        }\n\n        return newState;\n      });\n    };\n\n    window.addEventListener(\"keydown\", handler);\n    window.addEventListener(\"keyup\", handler);\n    // The use of only the keyup/keydown events may not be sufficient.\n    // on a Mac, when the meta-shift-4 (printscreen) combination is triggered, there is a possibility of losing the keyup event.\n    window.addEventListener(\"mousemove\", handler);\n\n    return () => {\n      window.removeEventListener(\"keydown\", handler);\n      window.removeEventListener(\"keyup\", handler);\n      window.removeEventListener(\"mousemove\", handler);\n    };\n  }, []);\n\n  return state;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/recent-selectors.ts",
    "content": "import type { Styles } from \"@webstudio-is/sdk\";\n\n/**\n * Extract all unique selectors (states) used in the project styles\n * Returns them sorted by most recently used (based on order in styles map)\n */\nexport const getUsedSelectors = (styles: Styles): string[] => {\n  const selectorsSet = new Set<string>();\n  const selectorsOrder: string[] = [];\n\n  // Iterate through all styles and collect unique states\n  for (const styleDecl of styles.values()) {\n    if (styleDecl.state && styleDecl.state.trim()) {\n      const selector = styleDecl.state;\n      if (!selectorsSet.has(selector)) {\n        selectorsSet.add(selector);\n        // Add to front to maintain most recent first\n        selectorsOrder.unshift(selector);\n      }\n    }\n  }\n\n  return selectorsOrder;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/repeated-style.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\nimport { setEnv } from \"@webstudio-is/feature-flags\";\nimport {\n  toValue,\n  type CssProperty,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { parseCssValue } from \"@webstudio-is/css-data\";\nimport {\n  addRepeatedStyleItem,\n  deleteRepeatedStyleItem,\n  editRepeatedStyleItem,\n  getRepeatedStyleItem,\n  setRepeatedStyleItem,\n  swapRepeatedStyleItems,\n  toggleRepeatedStyleItem,\n} from \"./repeated-style\";\nimport { createComputedStyleDeclStore } from \"./model\";\nimport { parseCssFragment } from \"./css-fragment\";\nimport {\n  $breakpoints,\n  $instances,\n  $selectedBreakpointId,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n} from \"~/shared/nano-states\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { setProperty } from \"./use-style-data\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { $awareness } from \"~/shared/awareness\";\n\nsetEnv(\"*\");\nregisterContainers();\n\nbeforeEach(() => {\n  $breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"\" }]]));\n  $selectedBreakpointId.set(\"base\");\n  $awareness.set({\n    pageId: \"\",\n    instanceSelector: [\"box\"],\n  });\n  $instances.set(\n    new Map([\n      [\"box\", { type: \"instance\", id: \"box\", component: \"Box\", children: [] }],\n    ])\n  );\n  $styleSourceSelections.set(new Map());\n  $styleSources.set(new Map());\n  $styles.set(new Map());\n});\n\nconst setRawProperty = (property: CssProperty, value: string) => {\n  setProperty(property)(parseCssValue(property, value));\n};\n\ntest(\"get repeated style item by index\", () => {\n  const cascadedValue: StyleValue = {\n    type: \"layers\",\n    value: [\n      { type: \"keyword\", value: \"red\" },\n      { type: \"keyword\", value: \"green\" },\n      { type: \"keyword\", value: \"blue\" },\n    ],\n  };\n  const styleDecl: ComputedStyleDecl = {\n    property: \"color\",\n    source: {\n      name: \"default\",\n    },\n    cascadedValue,\n    computedValue: cascadedValue,\n    usedValue: cascadedValue,\n  };\n  expect(getRepeatedStyleItem(styleDecl, 0)).toEqual({\n    type: \"keyword\",\n    value: \"red\",\n  });\n  expect(getRepeatedStyleItem(styleDecl, 1)).toEqual({\n    type: \"keyword\",\n    value: \"green\",\n  });\n  expect(getRepeatedStyleItem(styleDecl, 2)).toEqual({\n    type: \"keyword\",\n    value: \"blue\",\n  });\n  // repeat values\n  expect(getRepeatedStyleItem(styleDecl, 3)).toEqual({\n    type: \"keyword\",\n    value: \"red\",\n  });\n  expect(getRepeatedStyleItem(styleDecl, 4)).toEqual({\n    type: \"keyword\",\n    value: \"green\",\n  });\n  expect(getRepeatedStyleItem(styleDecl, 5)).toEqual({\n    type: \"keyword\",\n    value: \"blue\",\n  });\n});\n\ndescribe(\"add repeated item\", () => {\n  test(\"add layer to var\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    setRawProperty(\"transition-property\", \"var(--my-property)\");\n    expect($transitionProperty.get().cascadedValue.type).toEqual(\"var\");\n    addRepeatedStyleItem(\n      [$transitionProperty.get()],\n      parseCssFragment(\"opacity\", [\"transition-property\"])\n    );\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"var(--my-property), opacity\"\n    );\n  });\n\n  test(\"add layer to repeated style\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    addRepeatedStyleItem(\n      [$transitionProperty.get()],\n      parseCssFragment(\"opacity\", [\"transition-property\"])\n    );\n    addRepeatedStyleItem(\n      [$transitionProperty.get()],\n      parseCssFragment(\"transform\", [\"transition-property\"])\n    );\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"opacity, transform\"\n    );\n  });\n\n  test(\"add tuple to repeated style\", () => {\n    const $filter = createComputedStyleDeclStore(\"filter\");\n    addRepeatedStyleItem(\n      [$filter.get()],\n      parseCssFragment(\"blur(5px)\", [\"filter\"])\n    );\n    addRepeatedStyleItem(\n      [$filter.get()],\n      parseCssFragment(\"brightness(0.5)\", [\"filter\"])\n    );\n    expect(toValue($filter.get().cascadedValue)).toEqual(\n      \"blur(5px) brightness(0.5)\"\n    );\n  });\n\n  test(\"ignore when new item is not layers or tuple\", () => {\n    const $backgroundColor = createComputedStyleDeclStore(\"background-color\");\n    addRepeatedStyleItem(\n      [$backgroundColor.get()],\n      parseCssFragment(\"none\", [\"background\"])\n    );\n    expect($backgroundColor.get().source.name).toEqual(\"default\");\n    expect(toValue($backgroundColor.get().cascadedValue)).toEqual(\n      \"transparent\"\n    );\n  });\n\n  test(\"align properties with primary property\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    const $transitionDuration = createComputedStyleDeclStore(\n      \"transition-duration\"\n    );\n    const $transitionDelay = createComputedStyleDeclStore(\"transition-delay\");\n    setRawProperty(\"transition-property\", \"opacity, transform\");\n    setRawProperty(\"transition-duration\", \"1s\");\n    addRepeatedStyleItem(\n      [\n        $transitionProperty.get(),\n        $transitionDuration.get(),\n        $transitionDelay.get(),\n      ],\n      parseCssFragment(\"width 2s\", [\"transition\"])\n    );\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"opacity, transform, width\"\n    );\n    expect(toValue($transitionDuration.get().cascadedValue)).toEqual(\n      \"1s, 1s, 2s\"\n    );\n    expect(toValue($transitionDelay.get().cascadedValue)).toEqual(\"0s, 0s, 0s\");\n  });\n});\n\ndescribe(\"edit item in repeated style\", () => {\n  test(\"edit single variable\", () => {\n    const $backgroundImage = createComputedStyleDeclStore(\"background-image\");\n    setRawProperty(\"background-image\", \"none\");\n    editRepeatedStyleItem(\n      [$backgroundImage.get()],\n      0,\n      parseCssFragment(\"var(--gradient1)\", [\"background-image\"])\n    );\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\n      \"var(--gradient1)\"\n    );\n    expect($backgroundImage.get().cascadedValue.type).toEqual(\"var\");\n    editRepeatedStyleItem(\n      [$backgroundImage.get()],\n      // use greater index when access computed items\n      2,\n      parseCssFragment(\"var(--gradient2)\", [\"background-image\"])\n    );\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\n      \"var(--gradient2)\"\n    );\n    expect($backgroundImage.get().cascadedValue.type).toEqual(\"var\");\n  });\n\n  test(\"edit variable in multiple layers\", () => {\n    const $backgroundImage = createComputedStyleDeclStore(\"background-image\");\n    setRawProperty(\"background-image\", \"none, none\");\n    editRepeatedStyleItem(\n      [$backgroundImage.get()],\n      1,\n      parseCssFragment(\"var(--gradient1)\", [\"background-image\"])\n    );\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\n      \"none, var(--gradient1)\"\n    );\n    expect($backgroundImage.get().cascadedValue.type).toEqual(\"layers\");\n    editRepeatedStyleItem(\n      [$backgroundImage.get()],\n      1,\n      parseCssFragment(\"var(--gradient2)\", [\"background-image\"])\n    );\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\n      \"none, var(--gradient2)\"\n    );\n    expect($backgroundImage.get().cascadedValue.type).toEqual(\"layers\");\n  });\n\n  test(\"edit layer\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    setRawProperty(\"transition-property\", \"opacity, transform\");\n    editRepeatedStyleItem(\n      [$transitionProperty.get()],\n      1,\n      parseCssFragment(\"width\", [\"transition-property\"])\n    );\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"opacity, width\"\n    );\n  });\n\n  test(\"edit tuple\", () => {\n    const $filter = createComputedStyleDeclStore(\"filter\");\n    setRawProperty(\"filter\", \"blur(5px) brightness(0.5)\");\n    editRepeatedStyleItem(\n      [$filter.get()],\n      1,\n      parseCssFragment(\"contrast(200%)\", [\"filter\"])\n    );\n    expect(toValue($filter.get().cascadedValue)).toEqual(\n      \"blur(5px) contrast(200%)\"\n    );\n  });\n});\n\ntest(\"set layers item into repeated style\", () => {\n  const $transitionProperty = createComputedStyleDeclStore(\n    \"transition-property\"\n  );\n  setRawProperty(\"transition-property\", \"opacity, transform\");\n  setRepeatedStyleItem($transitionProperty.get(), 0, {\n    type: \"unparsed\",\n    value: \"width\",\n  });\n  expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n    \"width, transform\"\n  );\n  // out of bounds will repeat existing values\n  setRepeatedStyleItem($transitionProperty.get(), 3, {\n    type: \"unparsed\",\n    value: \"left\",\n  });\n  expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n    \"width, transform, width, left\"\n  );\n});\n\ntest(\"unpack item from layers value in repeated style\", () => {\n  const $transitionProperty = createComputedStyleDeclStore(\n    \"transition-property\"\n  );\n  setRawProperty(\"transition-property\", \"opacity, transform\");\n  setRepeatedStyleItem($transitionProperty.get(), 1, {\n    type: \"layers\",\n    value: [{ type: \"unparsed\", value: \"width\" }],\n  });\n  expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n    \"opacity, width\"\n  );\n});\n\ntest(\"set item on second layer when property is not defined on first layer\", () => {\n  // This tests the fix for the bug where setting a property like background-clip\n  // on layer 1 when layer 0 doesn't have that property would create undefined values\n  const $backgroundClip = createComputedStyleDeclStore(\"background-clip\");\n\n  // Set two background images\n  setRawProperty(\"background-image\", \"url(image1.jpg), url(image2.jpg)\");\n\n  // background-clip is not yet defined, so cascadedValue should be default\n  expect($backgroundClip.get().source.name).toEqual(\"default\");\n\n  // Now set background-clip on the second layer (index 1)\n  // This should fill index 0 with the initial value (border-box) instead of undefined\n  setRepeatedStyleItem($backgroundClip.get(), 1, {\n    type: \"keyword\",\n    value: \"padding-box\",\n  });\n\n  // Should have created layers with initial value at index 0\n  expect(toValue($backgroundClip.get().cascadedValue)).toEqual(\n    \"border-box, padding-box\"\n  );\n  expect($backgroundClip.get().cascadedValue.type).toEqual(\"layers\");\n\n  // Verify the values are properly set and not undefined\n  const cascadedValue = $backgroundClip.get().cascadedValue;\n  if (cascadedValue.type === \"layers\") {\n    expect(cascadedValue.value[0]).toEqual({\n      type: \"keyword\",\n      value: \"border-box\",\n    });\n    expect(cascadedValue.value[1]).toEqual({\n      type: \"keyword\",\n      value: \"padding-box\",\n    });\n    // Ensure no undefined values\n    expect(cascadedValue.value[0]).toBeDefined();\n    expect(cascadedValue.value[1]).toBeDefined();\n  }\n});\n\ndescribe(\"delete repeated item\", () => {\n  test(\"delete var or other not releated value in repeated style\", () => {\n    const $backgroundImage = createComputedStyleDeclStore(\"background-image\");\n    // var()\n    setRawProperty(\"background-image\", \"var(--my-bg)\");\n    deleteRepeatedStyleItem([$backgroundImage.get()], 0);\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\"none\");\n    // inherit\n    setRawProperty(\"background-image\", \"inherit\");\n    deleteRepeatedStyleItem([$backgroundImage.get()], 0);\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\"none\");\n  });\n\n  test(\"convert to var when it is the only left in repeated style\", () => {\n    const $backgroundImage = createComputedStyleDeclStore(\"background-image\");\n    // var()\n    setRawProperty(\"background-image\", \"var(--bg1), var(--bg2)\");\n    deleteRepeatedStyleItem([$backgroundImage.get()], 1);\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\"var(--bg1)\");\n    expect($backgroundImage.get().cascadedValue.type).toEqual(\"var\");\n  });\n\n  test(\"delete layer from repeated style\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    setRawProperty(\"transition-property\", \"opacity, transform\");\n    deleteRepeatedStyleItem([$transitionProperty.get()], 0);\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"transform\"\n    );\n  });\n\n  test(\"delete value without layers from repeated style\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    setRawProperty(\"transition-property\", \"opacity\");\n    expect($transitionProperty.get().source.name).toEqual(\"local\");\n    deleteRepeatedStyleItem([$transitionProperty.get()], 0);\n    expect($transitionProperty.get().source.name).toEqual(\"default\");\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\"all\");\n  });\n\n  test(\"align layers with primary when toggling toggle\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    const $transitionDuration = createComputedStyleDeclStore(\n      \"transition-duration\"\n    );\n    setRawProperty(\"transition-property\", \"opacity, transform, color\");\n    setRawProperty(\"transition-duration\", \"1s, 2s\");\n    deleteRepeatedStyleItem(\n      [$transitionProperty.get(), $transitionDuration.get()],\n      0\n    );\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"transform, color\"\n    );\n    // color should not switch to 1s when hide first layer\n    expect(toValue($transitionDuration.get().cascadedValue)).toEqual(\"2s, 1s\");\n  });\n\n  test(\"delete tuple from repeated style\", () => {\n    const $filter = createComputedStyleDeclStore(\"filter\");\n    setRawProperty(\"filter\", \"blur(5px) brightness(0.5)\");\n    deleteRepeatedStyleItem([$filter.get()], 0);\n    expect(toValue($filter.get().cascadedValue)).toEqual(\"brightness(0.5)\");\n  });\n});\n\ndescribe(\"toggle repeated item\", () => {\n  test(\"toggle var in repeated style\", () => {\n    const $backgroundImage = createComputedStyleDeclStore(\"background-image\");\n    setRawProperty(\"background-image\", \"var(--my-bg)\");\n    toggleRepeatedStyleItem([$backgroundImage.get()], 0);\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\"\");\n    toggleRepeatedStyleItem([$backgroundImage.get()], 0);\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\n      \"var(--my-bg)\"\n    );\n  });\n\n  test(\"ignore toggling not repeated value in repeated style\", () => {\n    const $backgroundImage = createComputedStyleDeclStore(\"background-image\");\n    setRawProperty(\"background-image\", \"inherit\");\n    toggleRepeatedStyleItem([$backgroundImage.get()], 0);\n    expect(toValue($backgroundImage.get().cascadedValue)).toEqual(\"inherit\");\n  });\n\n  test(\"toggle layer in repeated style\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    setRawProperty(\"transition-property\", \"opacity, transform\");\n    toggleRepeatedStyleItem([$transitionProperty.get()], 0);\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"transform\"\n    );\n    toggleRepeatedStyleItem([$transitionProperty.get()], 0);\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"opacity, transform\"\n    );\n  });\n\n  test(\"toggle tuple in repeated style\", () => {\n    const $filter = createComputedStyleDeclStore(\"filter\");\n    setRawProperty(\"filter\", \"blur(5px) brightness(0.5)\");\n    toggleRepeatedStyleItem([$filter.get()], 0);\n    expect(toValue($filter.get().cascadedValue)).toEqual(\"brightness(0.5)\");\n    toggleRepeatedStyleItem([$filter.get()], 0);\n    expect(toValue($filter.get().cascadedValue)).toEqual(\n      \"blur(5px) brightness(0.5)\"\n    );\n  });\n\n  test(\"align layers with primary when toggling toggle\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    const $transitionDuration = createComputedStyleDeclStore(\n      \"transition-duration\"\n    );\n    setRawProperty(\"transition-property\", \"opacity, transform, color\");\n    setRawProperty(\"transition-duration\", \"1s, 2s\");\n    toggleRepeatedStyleItem(\n      [$transitionProperty.get(), $transitionDuration.get()],\n      0\n    );\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"transform, color\"\n    );\n    // color should not switch to 1s when hide first layer\n    expect(toValue($transitionDuration.get().cascadedValue)).toEqual(\"2s, 1s\");\n  });\n});\n\ndescribe(\"swap repeated items\", () => {\n  test(\"swap layers in repeated style\", () => {\n    const $transitionProperty = createComputedStyleDeclStore(\n      \"transition-property\"\n    );\n    setRawProperty(\"transition-property\", \"opacity, transform\");\n    swapRepeatedStyleItems([$transitionProperty.get()], 0, 1);\n    expect(toValue($transitionProperty.get().cascadedValue)).toEqual(\n      \"transform, opacity\"\n    );\n  });\n\n  test(\"add tuple items in repeated style\", () => {\n    const $filter = createComputedStyleDeclStore(\"filter\");\n    setRawProperty(\"filter\", \"blur(5px) brightness(0.5)\");\n    swapRepeatedStyleItems([$filter.get()], 0, 1);\n    expect(toValue($filter.get().cascadedValue)).toEqual(\n      \"brightness(0.5) blur(5px)\"\n    );\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/repeated-style.tsx",
    "content": "import { useMemo, type ComponentProps, type JSX, type ReactNode } from \"react\";\nimport {\n  toValue,\n  type CssProperty,\n  type LayersValue,\n  type StyleValue,\n  type TupleValue,\n  type UnparsedValue,\n} from \"@webstudio-is/css-engine\";\nimport { parseCssValue, propertiesData } from \"@webstudio-is/css-data\";\nimport { EyeClosedIcon, EyeOpenIcon, MinusIcon } from \"@webstudio-is/icons\";\nimport {\n  CssValueListArrowFocus,\n  CssValueListItem,\n  Flex,\n  Label,\n  SmallIconButton,\n  SmallToggleButton,\n  toast,\n  useSortable,\n  FloatingPanel,\n  ColorThumb,\n} from \"@webstudio-is/design-system\";\nimport { repeatUntil } from \"~/shared/array-utils\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { createBatchUpdate, type StyleUpdateOptions } from \"./use-style-data\";\n\nconst isRepeatedValue = (\n  styleValue: StyleValue\n): styleValue is LayersValue | TupleValue =>\n  styleValue.type === \"layers\" || styleValue.type === \"tuple\";\n\nconst reparseComputedValue = (styleDecl: ComputedStyleDecl) => {\n  const property = styleDecl.property;\n  const serialized = toValue(styleDecl.computedValue);\n  return parseCssValue(property, serialized);\n};\n\nexport const getComputedRepeatedItem = (\n  styleDecl: ComputedStyleDecl,\n  index: number\n) => {\n  const value = reparseComputedValue(styleDecl);\n  const items = isRepeatedValue(value) ? value.value : [];\n  if (\n    isRepeatedValue(styleDecl.cascadedValue) &&\n    styleDecl.cascadedValue.value.length !== items.length\n  ) {\n    return;\n  }\n  return items[index % items.length];\n};\n\nconst isItemHidden = (styleValue: StyleValue, index: number) => {\n  let hidden = false;\n  if (styleValue.type === \"var\") {\n    hidden = styleValue.hidden ?? false;\n  }\n  if (isRepeatedValue(styleValue)) {\n    hidden = styleValue.value[index].hidden ?? false;\n  }\n  return hidden;\n};\n\n/**\n * shows cascaded value\n * or resolves css variable and provide reparsed version\n * so for example gradients in variable are unparsed\n * but this utility converts them into layers\n */\nconst getComputedValue = (styleDecl: ComputedStyleDecl) => {\n  if (styleDecl.cascadedValue.type === \"var\") {\n    const property = styleDecl.property;\n    const serialized = toValue(styleDecl.computedValue);\n    return parseCssValue(property, serialized);\n  }\n  return styleDecl.cascadedValue;\n};\n\nexport const getRepeatedStyleItem = (\n  styleDecl: ComputedStyleDecl,\n  index: number\n) => {\n  const styleValue = getComputedValue(styleDecl);\n  if (styleValue.type === \"layers\" || styleValue.type === \"tuple\") {\n    return styleValue.value[index % styleValue.value.length];\n  }\n};\n\ntype ItemType = \"layers\" | \"tuple\";\n\nconst normalizeStyleValue = (\n  styleDecl: ComputedStyleDecl,\n  primaryValue: StyleValue,\n  itemType: ItemType = primaryValue.type === \"tuple\" ? \"tuple\" : \"layers\"\n) => {\n  const primaryItemsCount =\n    primaryValue.type === itemType ? primaryValue.value.length : 0;\n  const value = styleDecl.cascadedValue;\n  const items = value.type === itemType ? value.value : [];\n  // prefill initial value when no items to repeated\n  if (items.length === 0 && primaryItemsCount > 0) {\n    const meta = propertiesData[styleDecl.property];\n    if (meta) {\n      items.push(meta.initial as UnparsedValue);\n    }\n  }\n  return {\n    type: itemType,\n    value: repeatUntil(items, primaryItemsCount),\n  } as TupleValue | LayersValue;\n};\n\nexport const addRepeatedStyleItem = (\n  styles: ComputedStyleDecl[],\n  newItems: Map<CssProperty, StyleValue>\n) => {\n  if (styles[0].cascadedValue.type === \"var\") {\n    const primaryValue = reparseComputedValue(styles[0]);\n    if (isRepeatedValue(primaryValue) && primaryValue.value.length > 1) {\n      toast.error(\"Cannot add styles to css variable\");\n      return;\n    }\n  }\n  const batch = createBatchUpdate();\n  const currentStyles = new Map(\n    styles.map((styleDecl) => [styleDecl.property, styleDecl])\n  );\n  const primaryValue = styles[0].cascadedValue;\n  let primaryCount = 0;\n  if (isRepeatedValue(primaryValue)) {\n    primaryCount = primaryValue.value.length;\n  }\n  if (primaryValue.type === \"var\") {\n    primaryCount = 1;\n  }\n  for (const [property, newValue] of newItems) {\n    if (newValue.type !== \"layers\" && newValue.type !== \"tuple\") {\n      continue;\n    }\n    // infer type from new items\n    // because current values could be css wide keywords\n    const valueType: ItemType = newValue.type;\n    const styleDecl = currentStyles.get(property);\n    if (styleDecl === undefined) {\n      continue;\n    }\n    let oldItems: StyleValue[] = [];\n    if (styleDecl.cascadedValue.type === \"var\") {\n      oldItems = repeatUntil([styleDecl.cascadedValue], primaryCount);\n    } else if (styleDecl.cascadedValue.type === valueType) {\n      oldItems = repeatUntil(styleDecl.cascadedValue.value, primaryCount);\n    } else if (primaryCount > 0) {\n      const meta = propertiesData[property];\n      if (meta) {\n        oldItems = repeatUntil([meta.initial], primaryCount);\n      }\n    }\n    batch.setProperty(property)({\n      type: valueType,\n      value: [...oldItems, ...newValue.value] as UnparsedValue[],\n    });\n  }\n  batch.publish();\n};\n\nexport const editRepeatedStyleItem = (\n  styles: ComputedStyleDecl[],\n  index: number,\n  newItems: Map<CssProperty, StyleValue>,\n  options?: StyleUpdateOptions\n) => {\n  const batch = createBatchUpdate();\n  const currentStyles = new Map(\n    styles.map((styleDecl) => [styleDecl.property, styleDecl])\n  );\n  for (const [property, value] of newItems) {\n    const styleDecl = currentStyles.get(property);\n    if (styleDecl === undefined) {\n      continue;\n    }\n    let items: StyleValue[] = [];\n    if (styleDecl.cascadedValue.type === \"var\") {\n      items = [styleDecl.cascadedValue];\n    }\n    if (isRepeatedValue(styleDecl.cascadedValue)) {\n      items = styleDecl.cascadedValue.value;\n    }\n    if (items.length <= 1 && value.type === \"var\") {\n      batch.setProperty(property)(value);\n    } else {\n      let valueType;\n      if (isRepeatedValue(styleDecl.cascadedValue)) {\n        valueType = styleDecl.cascadedValue.type;\n      }\n      if (isRepeatedValue(value)) {\n        valueType = value.type;\n      }\n      if (valueType === undefined) {\n        continue;\n      }\n      const newItems: StyleValue[] = [...items];\n      if (value.type === \"var\") {\n        newItems.splice(index, 1, value);\n      }\n      if (isRepeatedValue(value)) {\n        newItems.splice(index, 1, ...value.value);\n      }\n      const newValue = {\n        type: valueType,\n        value: newItems as UnparsedValue[],\n      };\n      batch.setProperty(property)(newValue);\n    }\n  }\n  batch.publish(options);\n};\n\nexport const setRepeatedStyleItem = (\n  styleDecl: ComputedStyleDecl,\n  index: number,\n  newItem: StyleValue,\n  options?: StyleUpdateOptions\n) => {\n  const batch = createBatchUpdate();\n  const value = styleDecl.cascadedValue;\n  const valueType: ItemType = value.type === \"tuple\" ? \"tuple\" : \"layers\";\n  const oldItems = value.type === valueType ? value.value : [];\n\n  // Fill missing items with initial value instead of undefined\n  const newItems: StyleValue[] = repeatUntil(oldItems, index);\n  if (oldItems.length === 0 && index > 0) {\n    const meta = propertiesData[styleDecl.property];\n    if (meta) {\n      const initialValue = meta.initial as UnparsedValue;\n      for (let i = 0; i < index; i++) {\n        newItems[i] = initialValue;\n      }\n    }\n  }\n\n  // unpack item when layers or tuple is provided\n  newItems[index] = newItem.type === valueType ? newItem.value[0] : newItem;\n  batch.setProperty(styleDecl.property)({\n    type: valueType,\n    value: newItems as UnparsedValue[],\n  });\n  batch.publish(options);\n};\n\nexport const deleteRepeatedStyleItem = (\n  styles: ComputedStyleDecl[],\n  index: number\n) => {\n  const batch = createBatchUpdate();\n  const primaryValue = styles[0].cascadedValue;\n  const primaryCount = isRepeatedValue(primaryValue)\n    ? primaryValue.value.length\n    : index + 1;\n  for (const styleDecl of styles) {\n    const property = styleDecl.property;\n    const newValue = structuredClone(styleDecl.cascadedValue);\n    if (isRepeatedValue(newValue)) {\n      newValue.value = repeatUntil(newValue.value, primaryCount);\n      newValue.value.splice(index, 1);\n      if (newValue.value.length === 1 && newValue.value[0].type === \"var\") {\n        batch.setProperty(property)(newValue.value[0]);\n      } else if (newValue.value.length > 0) {\n        batch.setProperty(property)(newValue);\n      } else {\n        // delete empty layers or tuple\n        batch.deleteProperty(property);\n      }\n    } else {\n      batch.deleteProperty(property);\n    }\n  }\n  batch.publish();\n};\n\nexport const toggleRepeatedStyleItem = (\n  styles: ComputedStyleDecl[],\n  index: number\n) => {\n  const batch = createBatchUpdate();\n  const primaryValue = styles[0].cascadedValue;\n  const primaryCount = isRepeatedValue(primaryValue)\n    ? primaryValue.value.length\n    : index + 1;\n  const isHidden = isItemHidden(primaryValue, index);\n  for (const styleDecl of styles) {\n    const property = styleDecl.property;\n    const newValue = structuredClone(styleDecl.cascadedValue);\n    if (newValue.type === \"var\") {\n      newValue.hidden = !isHidden;\n      batch.setProperty(property)(newValue);\n    }\n    if (isRepeatedValue(newValue)) {\n      newValue.value = repeatUntil(newValue.value, primaryCount);\n      newValue.value[index] = structuredClone(newValue.value[index]);\n      newValue.value[index].hidden = !isHidden;\n      batch.setProperty(property)(newValue);\n    }\n    // other values are repeated automatically\n    // and it is irrelevant to change their visibility\n  }\n  batch.publish();\n};\n\nexport const swapRepeatedStyleItems = (\n  styles: ComputedStyleDecl[],\n  oldIndex: number,\n  newIndex: number\n) => {\n  if (styles[0].cascadedValue.type === \"var\") {\n    toast.error(\"Cannot reorder styles from css variable\");\n    return;\n  }\n  const batch = createBatchUpdate();\n  const primaryValue = styles[0].cascadedValue;\n  for (const styleDecl of styles) {\n    const newValue = normalizeStyleValue(styleDecl, primaryValue);\n    // You can swap only if there are at least two layers\n    // As we are checking across multiple properties, we can't be sure\n    // which property don't have two layers so we are checking here.\n    if (newValue.value.length >= 2) {\n      const oldItem = newValue.value[oldIndex];\n      newValue.value.splice(oldIndex, 1);\n      newValue.value.splice(newIndex, 0, oldItem);\n    }\n    batch.setProperty(styleDecl.property)(newValue);\n  }\n  batch.publish();\n};\n\nexport const RepeatedStyle = (props: {\n  label: string;\n  styles: ComputedStyleDecl[];\n  getItemProps: (\n    index: number,\n    primaryValue: StyleValue\n  ) => { label: string; color?: string };\n  floatingPanelOffset?: ComponentProps<typeof FloatingPanel>[\"offset\"];\n  renderThumbnail?: (index: number, primaryItem: StyleValue) => JSX.Element;\n  renderItemContent: (index: number, primaryItem: StyleValue) => JSX.Element;\n  renderItemButtons?: (\n    index: number,\n    primaryItem: StyleValue,\n    options: { isHidden: boolean; canBeChanged: boolean }\n  ) => ReactNode;\n  renderPanelTitleSuffix?: (\n    index: number,\n    primaryItem: StyleValue,\n    options: { isHidden: boolean; canBeChanged: boolean }\n  ) => ReactNode;\n}) => {\n  const {\n    label,\n    styles,\n    getItemProps,\n    renderThumbnail,\n    renderItemContent,\n    floatingPanelOffset,\n    renderItemButtons,\n    renderPanelTitleSuffix,\n  } = props;\n  // first property should describe the amount of layers or tuple items\n  const primaryValue = styles[0].cascadedValue;\n  let primaryItems: StyleValue[] = [];\n  if (primaryValue.type === \"var\") {\n    const reparsed = reparseComputedValue(styles[0]);\n    if (isRepeatedValue(reparsed)) {\n      primaryItems = repeatUntil([primaryValue], reparsed.value.length);\n    }\n  }\n  if (isRepeatedValue(primaryValue)) {\n    primaryItems = primaryValue.value;\n  }\n\n  const sortableItems = useMemo(\n    () =>\n      Array.from(Array(primaryItems.length), (_, index) => ({\n        id: String(index),\n        index,\n      })),\n    [primaryItems.length]\n  );\n\n  const { dragItemId, placementIndicator, sortableRefCallback } = useSortable({\n    items: sortableItems,\n    onSort: (newIndex, oldIndex) =>\n      swapRepeatedStyleItems(styles, oldIndex, newIndex),\n  });\n\n  if (primaryItems.length === 0) {\n    return;\n  }\n\n  return (\n    <CssValueListArrowFocus dragItemId={dragItemId}>\n      <Flex direction=\"column\" ref={sortableRefCallback}>\n        {primaryItems.map((primaryItem, index) => {\n          const id = String(index);\n          const { label: itemLabel, color: itemColor } = getItemProps(\n            index,\n            primaryItem\n          );\n          const isHidden = isItemHidden(styles[0].cascadedValue, index);\n          const canBeChanged =\n            styles[0].cascadedValue.type === \"var\" ? index === 0 : true;\n          const customButtons = renderItemButtons?.(index, primaryItem, {\n            isHidden,\n            canBeChanged,\n          });\n          const panelTitleSuffix = renderPanelTitleSuffix?.(\n            index,\n            primaryItem,\n            {\n              isHidden,\n              canBeChanged,\n            }\n          );\n          return (\n            <FloatingPanel\n              key={index}\n              title={label}\n              content={renderItemContent(index, primaryItem)}\n              titleSuffix={panelTitleSuffix}\n              offset={floatingPanelOffset}\n            >\n              <CssValueListItem\n                id={id}\n                draggable\n                active={dragItemId === id}\n                index={index}\n                label={<Label truncate>{itemLabel}</Label>}\n                hidden={isHidden}\n                thumbnail={\n                  renderThumbnail?.(index, primaryItem) ??\n                  (itemColor ? <ColorThumb color={itemColor} /> : undefined)\n                }\n                buttons={\n                  <>\n                    {customButtons}\n                    <SmallToggleButton\n                      variant=\"normal\"\n                      pressed={isHidden}\n                      disabled={false === canBeChanged}\n                      tabIndex={-1}\n                      icon={isHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}\n                      onPressedChange={() =>\n                        toggleRepeatedStyleItem(styles, index)\n                      }\n                    />\n                    <SmallIconButton\n                      variant=\"destructive\"\n                      disabled={false === canBeChanged}\n                      tabIndex={-1}\n                      icon={<MinusIcon />}\n                      onClick={() => deleteRepeatedStyleItem(styles, index)}\n                    />\n                  </>\n                }\n              />\n            </FloatingPanel>\n          );\n        })}\n        {placementIndicator}\n      </Flex>\n    </CssValueListArrowFocus>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/scroll-by-pointer.ts",
    "content": "const scrollAhead = (element: HTMLElement, clientX: number) => {\n  if (element.scrollWidth === element.clientWidth) {\n    // Nothing to scroll.\n    return false;\n  }\n  const inputRect = element.getBoundingClientRect();\n\n  // Calculate the relative x position of the mouse within the input element\n  const relativeMouseX = clientX - inputRect.x;\n\n  // Calculate the percentage position (0% at the beginning, 100% at the end)\n  const inputWidth = inputRect.width;\n  const mousePercentageX = Math.ceil((relativeMouseX / inputWidth) * 100);\n\n  // Apply acceleration based on the relative position of the mouse\n  // Closer to the beginning (-20%), closer to the end (+20%)\n  const accelerationFactor = (mousePercentageX - 50) / 50;\n  const adjustedMousePercentageX = Math.min(\n    Math.max(mousePercentageX + accelerationFactor * 20, 0),\n    100\n  );\n\n  // Calculate the scroll position corresponding to the adjusted percentage\n  const scrollPosition =\n    (adjustedMousePercentageX / 100) *\n    (element.scrollWidth - element.clientWidth);\n\n  // Scroll the input element\n  element.scroll({ left: scrollPosition });\n  return true;\n};\n\n// We don't want to scroll if the element is focused.\n// E.g. this is important for inputs, where the user might be interacting with text.\nconst isFocused = (element: HTMLElement) => element === document.activeElement;\n\n/**\n * Scroll any element horizontally based on pointer position.\n * Used in CSSValueInput and Spacing UI to show a string that is too long.\n */\nexport const scrollByPointer = () => {\n  let abortController = new AbortController();\n\n  const abort = (reason: string) => {\n    abortController.abort(reason);\n  };\n\n  const onMouseOver = (event: React.MouseEvent) => {\n    const element = event.currentTarget;\n    if (element instanceof HTMLElement === false) {\n      return;\n    }\n    if (isFocused(element)) {\n      abort(\"focused\");\n      return;\n    }\n    if (scrollAhead(element, event.clientX) === false) {\n      return;\n    }\n\n    abortController = new AbortController();\n    element?.addEventListener(\n      \"mousemove\",\n      (event: MouseEvent) => {\n        const element = event.currentTarget;\n        if (element instanceof HTMLElement === false) {\n          return;\n        }\n\n        if (isFocused(element)) {\n          abort(\"focused\");\n          return;\n        }\n        requestAnimationFrame(() => {\n          scrollAhead(element, event.clientX);\n        });\n      },\n      {\n        signal: abortController.signal,\n        passive: true,\n      }\n    );\n  };\n\n  const onMouseOut = (event: React.MouseEvent) => {\n    abort(\"mouseout\");\n    const element = event.currentTarget;\n    if (element instanceof HTMLElement === false) {\n      return;\n    }\n    if (isFocused(element)) {\n      abort(\"focused\");\n      return;\n    }\n    element.scroll({\n      left: 0,\n      behavior: \"smooth\",\n    });\n  };\n\n  return {\n    abort,\n    onMouseOver,\n    onMouseOut,\n    onFocus() {\n      abort(\"focus\");\n    },\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/shadow-content.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport {\n  toValue,\n  type ShadowValue,\n  type InvalidValue,\n  type LayersValue,\n  type StyleValue,\n  type VarValue,\n  type CssProperty,\n  type RgbValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  keywordValues,\n  parseCssValue,\n  propertySyntaxes,\n} from \"@webstudio-is/css-data\";\nimport {\n  Flex,\n  Grid,\n  Label,\n  Separator,\n  Text,\n  theme,\n  ToggleGroup,\n  ToggleGroupButton,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport {\n  InfoCircleIcon,\n  ShadowInsetIcon,\n  ShadowNormalIcon,\n} from \"@webstudio-is/icons\";\nimport { humanizeString } from \"~/shared/string-utils\";\nimport { PropertyInlineLabel } from \"../property-label\";\nimport type { IntermediateStyleValue } from \"./css-value-input\";\nimport { CssValueInputContainer } from \"./css-value-input\";\nimport type { StyleUpdateOptions } from \"./use-style-data\";\nimport {\n  CssFragmentEditor,\n  CssFragmentEditorContent,\n  parseCssFragment,\n} from \"./css-fragment\";\nimport { ColorPickerControl } from \"./color-picker\";\nimport { $availableColorVariables, $availableUnitVariables } from \"./model\";\n\n/*\n  When it comes to checking and validating individual CSS properties for the box-shadow,\n  splitting them fails the validation. As it needs a minimum of 2 values to validate.\n  Instead, a workaround is to use a fallback CSS property\n  that can handle the same values as the input being validated.\n\n  Here's the box-shadow property with its components:\n\n  box-shadow: color, inset, offsetX, offsetY, blur, spread;\n  You can check more details from the spec\n  https://www.w3.org/TR/css-backgrounds-3/#box-shadow\n\n  offsetX: length, takes positive and negative values.\n  offsetY: length, takes positive and negative values.\n  blur: length, takes only positive values.\n  spread: length, takes both positive and negative values.\n\n  outline-offset: length, takes positive and negative values.\n  https://www.w3.org/TR/css-ui-4/#outline-offset\n\n  border-top-width: length, takes only positive values.\n  https://www.w3.org/TR/css-backgrounds-3/#propdef-border-top-width\n*/\n\ntype ShadowContentProps = {\n  index: number;\n  property: \"box-shadow\" | \"text-shadow\" | \"drop-shadow\";\n  layer: StyleValue;\n  computedLayer?: StyleValue;\n  propertyValue: string;\n  onEditLayer: (\n    index: number,\n    layers: LayersValue | VarValue,\n    options: StyleUpdateOptions\n  ) => void;\n  hideCodeEditor?: boolean;\n};\n\nconst shadowPropertySyntaxes = {\n  \"box-shadow\": {\n    x: propertySyntaxes.boxShadowOffsetX,\n    y: propertySyntaxes.boxShadowOffsetY,\n    blur: propertySyntaxes.boxShadowBlurRadius,\n    spread: propertySyntaxes.boxShadowSpreadRadius,\n    color: propertySyntaxes.boxShadowColor,\n    position: propertySyntaxes.boxShadowPosition,\n  },\n  \"text-shadow\": {\n    x: propertySyntaxes.textShadowOffsetX,\n    y: propertySyntaxes.textShadowOffsetY,\n    blur: propertySyntaxes.textShadowBlurRadius,\n    color: propertySyntaxes.textShadowColor,\n  },\n  \"drop-shadow\": {\n    x: propertySyntaxes.dropShadowOffsetX,\n    y: propertySyntaxes.dropShadowOffsetY,\n    blur: propertySyntaxes.dropShadowBlurRadius,\n    color: propertySyntaxes.dropShadowColor,\n  },\n} as const;\n\nconst defaultColor: RgbValue = {\n  type: \"rgb\",\n  r: 0,\n  g: 0,\n  b: 0,\n  alpha: 1,\n};\n\nexport const ShadowContent = ({\n  layer,\n  computedLayer,\n  index,\n  property,\n  propertyValue,\n  hideCodeEditor = false,\n  onEditLayer,\n}: ShadowContentProps) => {\n  const [intermediateValue, setIntermediateValue] = useState<\n    IntermediateStyleValue | InvalidValue | undefined\n  >({ type: \"intermediate\", value: propertyValue });\n  useEffect(() => {\n    setIntermediateValue({ type: \"intermediate\", value: propertyValue });\n  }, [propertyValue]);\n  const parsedShadowProperty: CssProperty =\n    property === \"drop-shadow\" ? \"text-shadow\" : property;\n  // try to reparse computed value\n  // which can contain parsable value after variables substitution\n  if (computedLayer?.type === \"unparsed\") {\n    const styleValue = parseCssValue(parsedShadowProperty, computedLayer.value);\n    if (styleValue.type === \"layers\") {\n      [computedLayer] = styleValue.value;\n    }\n  }\n  let shadowValue: ShadowValue = {\n    type: \"shadow\",\n    position: \"outset\",\n    offsetX: { type: \"unit\", value: 0, unit: \"px\" },\n    offsetY: { type: \"unit\", value: 0, unit: \"px\" },\n  };\n  if (layer.type === \"shadow\") {\n    shadowValue = layer;\n  }\n  if (layer.type === \"var\" && computedLayer?.type === \"shadow\") {\n    shadowValue = computedLayer;\n  }\n  if (layer.type === \"unparsed\" && computedLayer?.type === \"shadow\") {\n    shadowValue = computedLayer;\n  }\n  const computedShadow =\n    computedLayer?.type === \"shadow\" ? computedLayer : shadowValue;\n\n  const disabledControls = layer.type === \"var\" || layer.type === \"unparsed\";\n\n  const handleChange = (value: string) => {\n    setIntermediateValue({\n      type: \"intermediate\",\n      value,\n    });\n  };\n\n  const handleComplete = () => {\n    if (intermediateValue === undefined) {\n      return;\n    }\n    // prevent reparsing value from string when not changed\n    // because it may contain css variables\n    // which cannot be safely parsed into ShadowValue\n    if (intermediateValue.value === propertyValue) {\n      return;\n    }\n    // dropShadow is a function under the filter property.\n    // To parse the value correctly, we need to change the property to textShadow.\n    // https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/drop-shadow#formal_syntax\n    // https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow#formal_syntax\n    // Both share a similar syntax but the property name is different.\n    const parsed = parseCssFragment(intermediateValue.value, [\n      parsedShadowProperty,\n    ]);\n    const parsedValue = parsed.get(parsedShadowProperty);\n    if (parsedValue?.type === \"layers\" || parsedValue?.type === \"var\") {\n      onEditLayer(index, parsedValue, { isEphemeral: false });\n      return;\n    }\n    setIntermediateValue({\n      type: \"invalid\",\n      value: intermediateValue.value,\n    });\n  };\n\n  const updateShadow = (\n    params: Partial<ShadowValue>,\n    options: StyleUpdateOptions = { isEphemeral: false }\n  ) => {\n    const newLayer: ShadowValue = { ...shadowValue, ...params };\n    setIntermediateValue({\n      type: \"intermediate\",\n      value: toValue(newLayer),\n    });\n    onEditLayer(index, { type: \"layers\", value: [newLayer] }, options);\n  };\n\n  return (\n    <Flex direction=\"column\">\n      <Grid\n        gap=\"2\"\n        css={{\n          padding: theme.panel.padding,\n          gridTemplateColumns:\n            property === \"box-shadow\" ? \"1fr 1fr\" : \"1fr 1fr 1fr\",\n        }}\n      >\n        <Flex direction=\"column\" gap=\"1\">\n          <PropertyInlineLabel\n            label=\"X\"\n            title=\"Offset X\"\n            description={shadowPropertySyntaxes[property].x}\n          />\n          <CssValueInputContainer\n            // outline-offset is a fake property for validating box-shadow's offsetX.\n            property=\"outline-offset\"\n            styleSource=\"local\"\n            aria-disabled={disabledControls}\n            getOptions={() => $availableUnitVariables.get()}\n            value={shadowValue.offsetX}\n            onUpdate={(value, options) => {\n              if (value.type === \"unit\" || value.type === \"var\") {\n                updateShadow({ offsetX: value }, options);\n              }\n            }}\n            onDelete={(options) =>\n              updateShadow({ offsetX: shadowValue.offsetX }, options)\n            }\n          />\n        </Flex>\n\n        <Flex direction=\"column\" gap=\"1\">\n          <PropertyInlineLabel\n            label=\"Y\"\n            title=\"Offset Y\"\n            description={shadowPropertySyntaxes[property].y}\n          />\n          <CssValueInputContainer\n            // outline-offset is a fake property for validating box-shadow's offsetY.\n            property=\"outline-offset\"\n            styleSource=\"local\"\n            aria-disabled={disabledControls}\n            getOptions={() => $availableUnitVariables.get()}\n            value={shadowValue.offsetY}\n            onUpdate={(value, options) => {\n              if (value.type === \"unit\" || value.type === \"var\") {\n                updateShadow({ offsetY: value }, options);\n              }\n            }}\n            onDelete={(options) =>\n              updateShadow({ offsetY: shadowValue.offsetY }, options)\n            }\n          />\n        </Flex>\n\n        <Flex direction=\"column\" gap=\"1\">\n          <PropertyInlineLabel\n            label=\"Blur\"\n            title=\"Blur Radius\"\n            description={shadowPropertySyntaxes[property].blur}\n          />\n          <CssValueInputContainer\n            // border-top-width is a fake property for validating box-shadow's blur.\n            property=\"border-top-width\"\n            styleSource=\"local\"\n            aria-disabled={disabledControls}\n            getOptions={() => $availableUnitVariables.get()}\n            value={shadowValue.blur ?? { type: \"unit\", value: 0, unit: \"px\" }}\n            onUpdate={(value, options) => {\n              if (value.type === \"unit\" || value.type === \"var\") {\n                updateShadow({ blur: value }, options);\n              }\n            }}\n            onDelete={(options) =>\n              updateShadow({ blur: shadowValue.blur }, options)\n            }\n          />\n        </Flex>\n\n        {property === \"box-shadow\" ? (\n          <Flex direction=\"column\" gap=\"1\">\n            <PropertyInlineLabel\n              label=\"Spread\"\n              title=\"Spread Radius\"\n              description={shadowPropertySyntaxes[\"box-shadow\"].spread}\n            />\n            <CssValueInputContainer\n              // outline-offset is a fake property for validating box-shadow's spread.\n              property=\"outline-offset\"\n              styleSource=\"local\"\n              aria-disabled={disabledControls}\n              getOptions={() => $availableUnitVariables.get()}\n              value={\n                shadowValue.spread ?? { type: \"unit\", value: 0, unit: \"px\" }\n              }\n              onUpdate={(value, options) => {\n                if (value.type === \"unit\" || value.type === \"var\") {\n                  updateShadow({ spread: value }, options);\n                }\n              }}\n              onDelete={(options) =>\n                updateShadow({ spread: shadowValue.spread }, options)\n              }\n            />\n          </Flex>\n        ) : null}\n      </Grid>\n\n      <Grid\n        gap=\"2\"\n        css={{\n          padding: theme.panel.padding,\n          ...(property === \"box-shadow\" && { gridTemplateColumns: \"3fr 1fr\" }),\n        }}\n      >\n        <Flex direction=\"column\" gap=\"1\">\n          <PropertyInlineLabel\n            label=\"Color\"\n            description={shadowPropertySyntaxes[property].color}\n          />\n          <ColorPickerControl\n            property=\"color\"\n            aria-disabled={disabledControls}\n            value={shadowValue.color ?? defaultColor}\n            currentColor={computedShadow?.color ?? defaultColor}\n            getOptions={() => [\n              ...(keywordValues.color ?? []).map((item) => ({\n                type: \"keyword\" as const,\n                value: item,\n              })),\n              ...$availableColorVariables.get(),\n            ]}\n            onChange={(value) => {\n              if (\n                value.type === \"rgb\" ||\n                value.type === \"color\" ||\n                value.type === \"var\"\n              ) {\n                updateShadow({ color: value }, { isEphemeral: true });\n              }\n            }}\n            onChangeComplete={(value) => {\n              if (\n                value.type === \"rgb\" ||\n                value.type === \"color\" ||\n                value.type === \"var\"\n              ) {\n                updateShadow({ color: value });\n              }\n            }}\n            onAbort={() => updateShadow({ color: shadowValue.color })}\n            onReset={() => updateShadow({ color: undefined })}\n          />\n        </Flex>\n\n        {property === \"box-shadow\" ? (\n          <Flex direction=\"column\" gap=\"1\">\n            <PropertyInlineLabel\n              label={humanizeString(shadowValue.position)}\n              description={shadowPropertySyntaxes[\"box-shadow\"].position}\n            />\n            <ToggleGroup\n              type=\"single\"\n              aria-disabled={disabledControls}\n              value={shadowValue.position}\n              defaultValue=\"inset\"\n              onValueChange={(value) =>\n                updateShadow({ position: value as ShadowValue[\"position\"] })\n              }\n            >\n              <Tooltip content=\"Outset\">\n                <ToggleGroupButton value=\"outset\">\n                  <ShadowNormalIcon />\n                </ToggleGroupButton>\n              </Tooltip>\n              <Tooltip content=\"Inset\">\n                <ToggleGroupButton value=\"inset\">\n                  <ShadowInsetIcon />\n                </ToggleGroupButton>\n              </Tooltip>\n            </ToggleGroup>\n          </Flex>\n        ) : null}\n      </Grid>\n\n      {hideCodeEditor === false ? (\n        <>\n          <Separator css={{ gridColumn: \"span 2\" }} />\n          <Flex\n            direction=\"column\"\n            css={{\n              padding: theme.panel.padding,\n              gap: theme.spacing[3],\n              minWidth: theme.spacing[30],\n            }}\n          >\n            <Label>\n              <Flex align={\"center\"} gap={1}>\n                Code\n                <Tooltip\n                  variant=\"wrapped\"\n                  content={\n                    <Text>\n                      Paste a {property} CSS code without the property name, for\n                      example:\n                      <br /> <br />\n                      <Text variant=\"monoBold\">\n                        0px 2px 5px 0px rgba(0, 0, 0, 0.2)\n                      </Text>\n                    </Text>\n                  }\n                >\n                  <InfoCircleIcon />\n                </Tooltip>\n              </Flex>\n            </Label>\n            <CssFragmentEditor\n              content={\n                <CssFragmentEditorContent\n                  invalid={intermediateValue?.type === \"invalid\"}\n                  autoFocus={disabledControls}\n                  value={intermediateValue?.value ?? propertyValue ?? \"\"}\n                  onChange={handleChange}\n                  onChangeComplete={handleComplete}\n                />\n              }\n            />\n          </Flex>\n        </>\n      ) : undefined}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/show-more.stories.tsx",
    "content": "import { Box, StorySection, Text, theme } from \"@webstudio-is/design-system\";\nimport { ShowMore as ShowMoreComponent } from \"./show-more\";\n\nexport default {\n  title: \"Style panel/Show More\",\n  component: ShowMoreComponent,\n};\n\nexport const ShowMore = () => (\n  <StorySection title=\"Show More\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <ShowMoreComponent\n        styleConfigs={[\n          <Text key=\"1\">First config item</Text>,\n          <Text key=\"2\">Second config item</Text>,\n          <Text key=\"3\">Third config item</Text>,\n        ]}\n      />\n    </Box>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/show-more.tsx",
    "content": "import { useState, type JSX } from \"react\";\nimport { Flex, Button, Collapsible } from \"@webstudio-is/design-system\";\nimport { ChevronDownIcon, ChevronRightIcon } from \"@webstudio-is/icons\";\n\nexport const ShowMore = ({\n  styleConfigs,\n}: {\n  styleConfigs: Array<JSX.Element | null>;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  if (styleConfigs.length === 0) {\n    return null;\n  }\n  return (\n    <Collapsible.Root asChild onOpenChange={setIsOpen}>\n      <Flex direction=\"column\" gap=\"3\">\n        <Collapsible.Trigger asChild>\n          <Button\n            css={{ width: \"100%\" }}\n            prefix={isOpen ? <ChevronDownIcon /> : <ChevronRightIcon />}\n          >\n            Show more\n          </Button>\n        </Collapsible.Trigger>\n        <Collapsible.Content asChild>\n          <Flex direction=\"column\" gap=\"3\">\n            {styleConfigs}\n          </Flex>\n        </Collapsible.Content>\n      </Flex>\n    </Collapsible.Root>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/style-section.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { PlusIcon } from \"@webstudio-is/icons\";\nimport type { CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  SectionTitle,\n  SectionTitleButton,\n  SectionTitleLabel,\n} from \"@webstudio-is/design-system\";\nimport {\n  CollapsibleSectionRoot,\n  useOpenState,\n} from \"~/builder/shared/collapsible-section\";\nimport { useComputedStyles } from \"./model\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { PropertySectionLabel } from \"../property-label\";\n\nexport const getDots = (styles: ComputedStyleDecl[]) => {\n  const dots = new Set<\"local\" | \"overwritten\" | \"remote\">();\n\n  for (const styleDecl of styles) {\n    // Unparsed values are not editable directly in the section, so we don't show the dot\n    if (styleDecl.usedValue.type === \"guaranteedInvalid\") {\n      return [];\n    }\n\n    const source = styleDecl.source.name;\n    if (source === \"local\" || source === \"overwritten\" || source === \"remote\") {\n      dots.add(source);\n    }\n  }\n\n  return Array.from(dots);\n};\n\nexport const StyleSection = (props: {\n  label: string;\n  properties: CssProperty[];\n  // @todo remove to keep sections consistent\n  fullWidth?: boolean;\n  suffix?: ReactNode;\n  children: ReactNode;\n}) => {\n  const { label, children, properties, fullWidth, suffix } = props;\n  const [isOpen, setIsOpen] = useOpenState(label);\n  const styles = useComputedStyles(properties);\n  return (\n    <CollapsibleSectionRoot\n      label={label}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      trigger={\n        <SectionTitle dots={getDots(styles)} suffix={suffix}>\n          <SectionTitleLabel>{label}</SectionTitleLabel>\n        </SectionTitle>\n      }\n      fullWidth={fullWidth}\n    >\n      {children}\n    </CollapsibleSectionRoot>\n  );\n};\n\nexport const RepeatedStyleSection = (props: {\n  label: string;\n  description: string;\n  properties: [CssProperty, ...CssProperty[]];\n  collapsible?: boolean;\n  onAdd: () => void;\n  children: ReactNode;\n}) => {\n  const { label, description, children, properties, onAdd, collapsible } =\n    props;\n  const [isOpen, setIsOpen] = useOpenState(label);\n  const styles = useComputedStyles(properties);\n  const dots = getDots(styles);\n\n  return (\n    <CollapsibleSectionRoot\n      fullWidth\n      label={label}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n      trigger={\n        <SectionTitle\n          inactive={dots.length === 0}\n          collapsible={collapsible ?? dots.length !== 0}\n          dots={getDots(styles)}\n          suffix={\n            <SectionTitleButton\n              prefix={<PlusIcon />}\n              onClick={() => {\n                setIsOpen(true);\n                onAdd();\n              }}\n            />\n          }\n        >\n          <PropertySectionLabel\n            label={label}\n            description={description}\n            properties={properties}\n          />\n        </SectionTitle>\n      }\n    >\n      {children}\n    </CollapsibleSectionRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/shared/use-style-data.ts",
    "content": "import { getStyleDeclKey, type StyleDecl } from \"@webstudio-is/sdk\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { camelCaseProperty } from \"@webstudio-is/css-data\";\nimport {\n  $selectedBreakpoint,\n  $selectedOrLastStyleSourceSelector,\n  $selectedStyleSource,\n  $styleSourceSelections,\n  $styleSources,\n  $styles,\n} from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { $ephemeralStyles } from \"~/canvas/stores\";\nimport { $selectedInstance } from \"~/shared/awareness\";\n\ntype StyleUpdate =\n  | {\n      operation: \"delete\";\n      property: CssProperty;\n    }\n  | {\n      operation: \"set\";\n      property: CssProperty;\n      value: StyleValue;\n    };\n\nexport type StyleUpdateOptions = { isEphemeral?: boolean; listed?: boolean };\n\nexport type SetValue = (\n  style: StyleValue,\n  options?: StyleUpdateOptions\n) => void;\n\nexport type SetProperty = (property: CssProperty) => SetValue;\n\nexport type DeleteProperty = (\n  property: CssProperty,\n  options?: StyleUpdateOptions\n) => void;\n\nexport type CreateBatchUpdate = () => {\n  setProperty: (property: CssProperty) => (style: StyleValue) => void;\n  deleteProperty: (property: CssProperty) => void;\n  publish: (options?: StyleUpdateOptions) => void;\n};\n\nconst publishUpdates = (\n  type: \"update\" | \"preview\",\n  updates: StyleUpdate[],\n  options: StyleUpdateOptions\n) => {\n  if (updates.length === 0) {\n    return;\n  }\n\n  const selectedInstance = $selectedInstance.get();\n  const selectedBreakpoint = $selectedBreakpoint.get();\n  const selectedStyleSource = $selectedStyleSource.get();\n  const styleSourceSelector = $selectedOrLastStyleSourceSelector.get();\n\n  if (\n    selectedInstance === undefined ||\n    selectedBreakpoint === undefined ||\n    selectedStyleSource === undefined ||\n    styleSourceSelector === undefined\n  ) {\n    return;\n  }\n\n  if (type === \"preview\") {\n    const ephemeralStyles: ReturnType<typeof $ephemeralStyles.get> = [];\n    for (const update of updates) {\n      if (update.operation === \"set\") {\n        ephemeralStyles.push({\n          breakpointId: selectedBreakpoint.id,\n          styleSourceId: styleSourceSelector.styleSourceId,\n          state: styleSourceSelector.state,\n          property: camelCaseProperty(update.property),\n          value: update.value,\n          listed: options.listed,\n        });\n      }\n    }\n    $ephemeralStyles.set(ephemeralStyles);\n    return;\n  }\n\n  $ephemeralStyles.set([]);\n  serverSyncStore.createTransaction(\n    [$styleSourceSelections, $styleSources, $styles],\n    (styleSourceSelections, styleSources, styles) => {\n      const instanceId = selectedInstance.id;\n      const breakpointId = selectedBreakpoint.id;\n      // set only selected style source and update selection with it\n      // generated local style source will not be written if not selected\n      styleSources.set(selectedStyleSource.id, selectedStyleSource);\n      const selectionValues =\n        styleSourceSelections.get(instanceId)?.values ?? [];\n      if (\n        selectionValues.includes(styleSourceSelector.styleSourceId) === false\n      ) {\n        styleSourceSelections.set(instanceId, {\n          instanceId,\n          values: [...selectionValues, styleSourceSelector.styleSourceId],\n        });\n      }\n\n      for (const update of updates) {\n        if (update.operation === \"set\") {\n          const styleDecl: StyleDecl = {\n            breakpointId,\n            styleSourceId: styleSourceSelector.styleSourceId,\n            state: styleSourceSelector.state,\n            property: camelCaseProperty(update.property),\n            value: update.value,\n            listed: options.listed,\n          };\n          styles.set(getStyleDeclKey(styleDecl), styleDecl);\n        }\n\n        if (update.operation === \"delete\") {\n          const styleDeclKey = getStyleDeclKey({\n            breakpointId,\n            styleSourceId: styleSourceSelector.styleSourceId,\n            state: styleSourceSelector.state,\n            property: camelCaseProperty(update.property),\n          });\n          styles.delete(styleDeclKey);\n        }\n      }\n    }\n  );\n};\n\nexport const setProperty: SetProperty = (property) => {\n  return (value, options: StyleUpdateOptions = { isEphemeral: false }) => {\n    if (value.type !== \"invalid\") {\n      const updates = [{ operation: \"set\" as const, property, value }];\n      const type = options.isEphemeral ? \"preview\" : \"update\";\n      publishUpdates(type, updates, options);\n    }\n  };\n};\n\nexport const deleteProperty = (\n  property: CssProperty,\n  options: StyleUpdateOptions = { isEphemeral: false }\n) => {\n  const updates = [{ operation: \"delete\" as const, property }];\n  const type = options.isEphemeral ? \"preview\" : \"update\";\n  publishUpdates(type, updates, options);\n};\n\nexport const createBatchUpdate = () => {\n  let updates: StyleUpdate[] = [];\n\n  const setProperty = (property: CssProperty) => {\n    const setValue = (value: StyleValue) => {\n      if (value.type === \"invalid\") {\n        return;\n      }\n      updates.push({ operation: \"set\", property, value });\n    };\n    return setValue;\n  };\n\n  const deleteProperty = (property: CssProperty) => {\n    updates.push({ operation: \"delete\", property });\n  };\n\n  const publish = (options: StyleUpdateOptions = { isEphemeral: false }) => {\n    if (!updates.length) {\n      return;\n    }\n    const type = options.isEphemeral ? \"preview\" : \"update\";\n    publishUpdates(type, updates, options);\n    updates = [];\n  };\n\n  return {\n    setProperty,\n    deleteProperty,\n    publish,\n  };\n};\n\nexport const resetEphemeralStyles = () => {\n  $ephemeralStyles.set([]);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-panel.tsx",
    "content": "import { useState } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  theme,\n  Box,\n  Card,\n  Text,\n  Separator,\n  ScrollArea,\n  DropdownMenu,\n  DropdownMenuTrigger,\n  IconButton,\n  DropdownMenuContent,\n  DropdownMenuRadioItem,\n  MenuCheckedIcon,\n  DropdownMenuRadioGroup,\n  rawTheme,\n  Kbd,\n  Flex,\n  DropdownMenuSeparator,\n  DropdownMenuItem,\n} from \"@webstudio-is/design-system\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport { EllipsesIcon } from \"@webstudio-is/icons\";\nimport { $selectedInstanceRenderState } from \"~/shared/nano-states\";\nimport { $selectedInstance } from \"~/shared/awareness\";\nimport { CollapsibleProvider } from \"~/builder/shared/collapsible-section\";\nimport {\n  $settings,\n  getSetting,\n  setSetting,\n  type Settings,\n} from \"~/builder/shared/client-settings\";\nimport { sections } from \"./sections\";\nimport { StyleSourcesSection } from \"./style-source-section\";\nimport {\n  $instanceTags,\n  useComputedStyleDecl,\n  useParentComputedStyleDecl,\n} from \"./shared/model\";\n\nconst $selectedInstanceTag = computed(\n  [$selectedInstance, $instanceTags],\n  (selectedInstance, instanceTags) => {\n    if (selectedInstance === undefined) {\n      return;\n    }\n    return instanceTags.get(selectedInstance.id);\n  }\n);\n\nexport const ModeMenu = () => {\n  const value = getSetting(\"stylePanelMode\");\n  const [focusedValue, setFocusedValue] = useState<string>(value);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <IconButton>\n          <EllipsesIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        sideOffset={Number.parseFloat(rawTheme.spacing[5])}\n        css={{ width: theme.spacing[26] }}\n      >\n        <DropdownMenuRadioGroup\n          value={value}\n          onValueChange={(value) => {\n            setSetting(\"stylePanelMode\", value as Settings[\"stylePanelMode\"]);\n          }}\n        >\n          <DropdownMenuRadioItem\n            value=\"default\"\n            icon={<MenuCheckedIcon />}\n            onFocus={() => setFocusedValue(\"default\")}\n          >\n            Default\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem\n            value=\"focus\"\n            icon={<MenuCheckedIcon />}\n            onFocus={() => setFocusedValue(\"focus\")}\n          >\n            <Flex justify=\"between\" grow>\n              <Text variant=\"labels\">Focus mode</Text>\n              <Kbd value={[\"alt\", \"shift\", \"s\"]} />\n            </Flex>\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem\n            value=\"advanced\"\n            icon={<MenuCheckedIcon />}\n            onFocus={() => setFocusedValue(\"advanced\")}\n          >\n            <Flex justify=\"between\" grow>\n              <Text variant=\"labels\">Advanced mode</Text>\n              <Kbd value={[\"alt\", \"shift\", \"a\"]} />\n            </Flex>\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n        <DropdownMenuSeparator />\n\n        {focusedValue === \"default\" && (\n          <DropdownMenuItem hint>\n            All sections are open by default.\n          </DropdownMenuItem>\n        )}\n        {focusedValue === \"focus\" && (\n          <DropdownMenuItem hint>\n            Only one section is open at a time.\n          </DropdownMenuItem>\n        )}\n        {focusedValue === \"advanced\" && (\n          <DropdownMenuItem hint>Advanced section only.</DropdownMenuItem>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const StylePanel = () => {\n  const { stylePanelMode } = useStore($settings);\n  const selectedInstanceRenderState = useStore($selectedInstanceRenderState);\n  const tag = useStore($selectedInstanceTag);\n  const display = toValue(useComputedStyleDecl(\"display\").computedValue);\n  const parentDisplay = toValue(\n    useParentComputedStyleDecl(\"display\").computedValue\n  );\n\n  // If selected instance is not rendered on the canvas,\n  // style panel will not work, because it needs the element in DOM in order to work.\n  // See <SelectedInstanceConnector> for more details.\n  if (selectedInstanceRenderState === \"notMounted\") {\n    return (\n      <Box css={{ p: theme.spacing[5] }}>\n        <Card css={{ p: theme.spacing[9], width: \"100%\" }}>\n          <Text>Select an instance on the canvas</Text>\n        </Card>\n      </Box>\n    );\n  }\n\n  const all = [];\n\n  for (const [category, { Section }] of sections.entries()) {\n    // In advanced mode we only need to show advanced panel\n    if (stylePanelMode === \"advanced\" && category !== \"advanced\") {\n      continue;\n    }\n    // show flex child UI only when parent is flex or inline-flex\n    if (category === \"flexChild\" && parentDisplay.includes(\"flex\") === false) {\n      continue;\n    }\n    // show grid child UI only when parent is grid or inline-grid\n    if (category === \"gridChild\" && parentDisplay.includes(\"grid\") === false) {\n      continue;\n    }\n    // allow customizing list item type only for list and list item\n    if (\n      category === \"listItem\" &&\n      tag !== \"ul\" &&\n      tag !== \"ol\" &&\n      tag !== \"li\"\n    ) {\n      continue;\n    }\n    // non-replaced inline boxes cannot be transformed\n    // https://drafts.csswg.org/css-transforms-1/#css-values\n    if (\n      category === \"transforms\" &&\n      (display === \"inline\" ||\n        display === \"table-column\" ||\n        display === \"table-column-group\")\n    ) {\n      continue;\n    }\n    all.push(<Section key={category} />);\n  }\n\n  return (\n    <>\n      <Box css={{ padding: theme.panel.padding }}>\n        <Text variant=\"titles\" css={{ paddingBlock: theme.panel.paddingBlock }}>\n          Style sources\n        </Text>\n        <StyleSourcesSection />\n      </Box>\n      <Separator />\n      <ScrollArea>\n        <CollapsibleProvider\n          accordion={stylePanelMode === \"focus\"}\n          initialOpen={stylePanelMode === \"focus\" ? \"Layout\" : \"*\"}\n        >\n          {all}\n        </CollapsibleProvider>\n      </ScrollArea>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/index.ts",
    "content": "export * from \"./style-source-input\";\nexport * from \"./style-source-badge\";\nexport type {\n  ItemSelector,\n  ItemSource,\n  StyleSourceError,\n} from \"./style-source-control\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/style-source-badge.stories.tsx",
    "content": "import type { StoryFn } from \"@storybook/react\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { StyleSourceBadge as StyleSourceBadgeComponent } from \"./style-source-badge\";\n\nexport default {\n  title: \"Style panel/Style Source Badge\",\n  component: StyleSourceBadgeComponent,\n};\n\nexport const StyleSourceBadge: StoryFn<\n  typeof StyleSourceBadgeComponent\n> = () => (\n  <StorySection title=\"Style Source Badge\">\n    <Box css={{ width: theme.sizes.sidebarWidth }}>\n      <Flex direction=\"column\" gap=\"3\">\n        <Text variant=\"labels\">All source variants</Text>\n        <Flex gap=\"1\" wrap=\"wrap\">\n          <StyleSourceBadgeComponent source=\"local\" variant=\"small\">\n            Local\n          </StyleSourceBadgeComponent>\n          <StyleSourceBadgeComponent source=\"token\" variant=\"small\">\n            Token\n          </StyleSourceBadgeComponent>\n          <StyleSourceBadgeComponent source=\"tag\" variant=\"small\">\n            Tag\n          </StyleSourceBadgeComponent>\n          <StyleSourceBadgeComponent source=\"breakpoint\" variant=\"small\">\n            Breakpoint\n          </StyleSourceBadgeComponent>\n          <StyleSourceBadgeComponent source=\"instance\" variant=\"small\">\n            Instance\n          </StyleSourceBadgeComponent>\n        </Flex>\n\n        <Text variant=\"labels\">Long label (truncation)</Text>\n        <Flex gap=\"1\">\n          <StyleSourceBadgeComponent source=\"token\" variant=\"small\">\n            Very long token name that should be truncated\n          </StyleSourceBadgeComponent>\n        </Flex>\n      </Flex>\n    </Box>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/style-source-badge.tsx",
    "content": "import { styled, Text } from \"@webstudio-is/design-system\";\nimport { theme } from \"@webstudio-is/design-system\";\n\nexport const StyleSourceBadge = styled(Text, {\n  display: \"inline-flex\",\n  borderRadius: theme.borderRadius[2],\n  px: theme.spacing[3],\n  height: theme.spacing[9],\n  color: theme.colors.foregroundContrastMain,\n  alignItems: \"center\",\n  maxWidth: \"100%\",\n  whiteSpace: \"nowrap\",\n  overflow: \"hidden\",\n  // @tood doesn't work in tooltips, needs a workaround\n  textOverflow: \"ellipsis\",\n  variants: {\n    source: {\n      local: {\n        backgroundColor: theme.colors.backgroundStyleSourceLocal,\n      },\n      token: {\n        backgroundColor: theme.colors.backgroundStyleSourceToken,\n      },\n      tag: {\n        backgroundColor: theme.colors.backgroundStyleSourceTag,\n      },\n      breakpoint: {\n        backgroundColor: theme.colors.backgroundStyleSourceBreakpoint,\n      },\n      instance: {\n        backgroundColor: theme.colors.backgroundNeutralMain,\n        color: theme.colors.foregroundMain,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/style-source-control.tsx",
    "content": "import {\n  Text,\n  styled,\n  Box,\n  theme,\n  Flex,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport type { StyleSource } from \"@webstudio-is/sdk\";\nimport { type ReactNode } from \"react\";\nimport { useContentEditable } from \"~/shared/dom-hooks\";\n\nconst menuTriggerVisibilityVar = \"--ws-style-source-menu-trigger-visibility\";\nconst menuTriggerVisibilityOverrideVar =\n  \"--ws-style-source-menu-trigger-visibility-override\";\nconst menuTriggerGradientVar = \"--ws-style-source-menu-trigger-gradient\";\n\nexport const menuCssVars = ({\n  show,\n  override = false,\n}: {\n  show: boolean;\n  override?: boolean;\n}) => {\n  const property = override\n    ? menuTriggerVisibilityOverrideVar\n    : menuTriggerVisibilityVar;\n\n  return {\n    [property]: show ? \"visible\" : \"hidden\",\n  };\n};\n\nexport type ItemSource = \"token\" | \"tag\" | \"local\";\n\nexport type ItemSelector = {\n  styleSourceId: string;\n  state?: string;\n};\n\ntype EditableTextProps = {\n  value: string;\n  isEditing: boolean;\n  onChangeEditing: (isEditing: boolean) => void;\n  onChangeValue: (value: string) => void;\n};\n\nconst EditableText = ({\n  value,\n  isEditing,\n  onChangeEditing,\n  onChangeValue,\n}: EditableTextProps) => {\n  const { ref, handlers } = useContentEditable({\n    isEditable: true,\n    isEditing,\n    onChangeEditing,\n    onChangeValue,\n    value,\n  });\n\n  return (\n    <Text\n      truncate\n      ref={ref}\n      spellCheck={false}\n      userSelect={isEditing ? \"text\" : \"none\"}\n      css={{\n        // prevent collapsing horizontally editable text when empty\n        flexGrow: 1,\n        outline: \"none\",\n        textOverflow: isEditing ? \"clip\" : \"ellipsis\",\n        cursor: isEditing ? \"auto\" : \"default\",\n      }}\n      {...handlers}\n    >\n      {value}\n    </Text>\n  );\n};\n\nconst StyleSourceContainer = styled(Box, {\n  display: \"inline-flex\",\n  borderRadius: theme.borderRadius[3],\n  minWidth: theme.spacing[14],\n  maxWidth: \"100%\",\n  height: theme.spacing[10],\n  position: \"relative\",\n  overflow: \"hidden\",\n  alignItems: \"center\",\n  color: theme.colors.foregroundContrastMain,\n  ...menuCssVars({ show: false }),\n  \"&:hover\": menuCssVars({ show: true }),\n  variants: {\n    source: {\n      local: {\n        order: 1,\n        backgroundColor: theme.colors.backgroundStyleSourceLocal,\n        [menuTriggerGradientVar]:\n          theme.colors.backgroundStyleSourceGradientLocal,\n      },\n      token: {\n        backgroundColor: theme.colors.backgroundStyleSourceToken,\n        [menuTriggerGradientVar]:\n          theme.colors.backgroundStyleSourceGradientToken,\n      },\n      tag: {\n        backgroundColor: theme.colors.backgroundStyleSourceTag,\n        [menuTriggerGradientVar]: theme.colors.backgroundStyleSourceGradientTag,\n      },\n    },\n    selected: {\n      true: {},\n      false: {\n        \"&:not(:hover)\": {\n          backgroundColor: theme.colors.backgroundStyleSourceNeutral,\n          [menuTriggerGradientVar]:\n            theme.colors.backgroundStyleSourceGradientUnselected,\n        },\n      },\n    },\n    disabled: {\n      true: {\n        \"&:not(:hover)\": {\n          backgroundColor: theme.colors.backgroundStyleSourceDisabled,\n        },\n      },\n      false: {},\n    },\n    hasError: {\n      true: {\n        backgroundColor: theme.colors.backgroundDestructiveMain,\n      },\n    },\n  },\n});\n\nconst StyleSourceButton = styled(\"button\", {\n  all: \"unset\",\n  flexGrow: 1,\n  display: \"block\",\n  boxSizing: \"border-box\",\n  maxWidth: \"100%\",\n  variants: {\n    isEditing: {\n      true: {\n        color: theme.colors.foregroundMain,\n        backgroundColor: theme.colors.backgroundControls,\n      },\n      false: {},\n    },\n  },\n});\n\nconst StyleSourceState = styled(Text, {\n  padding: theme.spacing[3],\n  borderTopRightRadius: theme.borderRadius[3],\n  borderBottomRightRadius: theme.borderRadius[3],\n  cursor: \"default\",\n  variants: {\n    source: {\n      local: {\n        backgroundColor: theme.colors.backgroundStyleSourceLocal,\n      },\n      token: {\n        backgroundColor: theme.colors.backgroundStyleSourceToken,\n      },\n      tag: {\n        backgroundColor: theme.colors.backgroundStyleSourceTag,\n      },\n    },\n  },\n});\n\nconst LocalStyleIcon = ({ size = 16, showDot = true }) => {\n  return (\n    <svg viewBox=\"0 0 16 16\" width={size} height={size} fill=\"none\">\n      <path\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"\n      />\n      {showDot && (\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 9.333a1.333 1.333 0 1 0 0-2.666 1.333 1.333 0 0 0 0 2.666Z\"\n        />\n      )}\n    </svg>\n  );\n};\n\nconst errors = {\n  minlength: \"Token must be at least 1 character long\",\n  duplicate: \"Token already exists\",\n} as const;\n\nexport type StyleSourceError = {\n  type: keyof typeof errors;\n  id: StyleSource[\"id\"];\n};\n\ntype StyleSourceControlProps = {\n  id: StyleSource[\"id\"];\n  error?: StyleSourceError;\n  label: string;\n  menu: ReactNode;\n  selected: boolean;\n  state: undefined | string;\n  stateLabel: undefined | string;\n  disabled: boolean;\n  isEditing: boolean;\n  isDragging: boolean;\n  hasStyles: boolean;\n  source: ItemSource;\n  onSelect: () => void;\n  onChangeValue: (value: string) => void;\n  onChangeEditing: (isEditing: boolean) => void;\n  onOpenMenu?: () => void;\n};\n\nexport const StyleSourceControl = ({\n  id,\n  menu,\n  selected,\n  state,\n  stateLabel,\n  error,\n  disabled,\n  isEditing,\n  isDragging,\n  hasStyles,\n  source,\n  label,\n  onChangeValue,\n  onChangeEditing,\n  onSelect,\n  onOpenMenu,\n}: StyleSourceControlProps) => {\n  const showMenu = isEditing === false && isDragging === false;\n\n  const handleContextMenu = (event: React.MouseEvent) => {\n    if (showMenu && disabled === false && isEditing === false) {\n      event.preventDefault();\n      onOpenMenu?.();\n    }\n  };\n\n  return (\n    <Tooltip\n      content={error ? errors[error.type] : \"\"}\n      open={error !== undefined}\n    >\n      <StyleSourceContainer\n        data-id={id}\n        source={source}\n        selected={selected && state === undefined}\n        disabled={disabled}\n        aria-current={selected && state === undefined}\n        role=\"button\"\n        hasError={error !== undefined}\n      >\n        <Flex grow css={{ padding: theme.spacing[2] }}>\n          <StyleSourceButton\n            disabled={disabled || isEditing}\n            isEditing={isEditing}\n            tabIndex={-1}\n            onClick={onSelect}\n            onContextMenu={handleContextMenu}\n          >\n            {source === \"local\" ? (\n              <Flex justify=\"center\" align=\"center\">\n                <Box\n                  // We need this so that the small local button has a bigger clickable surface\n                  css={{ position: \"absolute\", inset: 0 }}\n                />\n                <LocalStyleIcon showDot={hasStyles} />\n              </Flex>\n            ) : (\n              <Flex align=\"center\" justify=\"center\" gap=\"1\">\n                <EditableText\n                  isEditing={isEditing}\n                  onChangeEditing={onChangeEditing}\n                  onChangeValue={onChangeValue}\n                  value={label}\n                />\n                {hasStyles === false && isEditing === false && (\n                  <LocalStyleIcon showDot={hasStyles} />\n                )}\n              </Flex>\n            )}\n          </StyleSourceButton>\n        </Flex>\n        {stateLabel !== undefined && (\n          <Tooltip content={state || stateLabel} side=\"top\">\n            <StyleSourceState source={source}>{stateLabel}</StyleSourceState>\n          </Tooltip>\n        )}\n        {showMenu && menu}\n      </StyleSourceContainer>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/style-source-input.stories.tsx",
    "content": "import { nanoid } from \"nanoid\";\nimport { useState } from \"react\";\nimport { Flex, StorySection, Text, theme } from \"@webstudio-is/design-system\";\nimport {\n  type ItemSelector,\n  type ItemSource,\n  StyleSourceInput as StyleSourceInputComponent,\n} from \".\";\n\nexport default {\n  title: \"Style panel/Style Source Input\",\n  component: StyleSourceInputComponent,\n};\n\ntype Item = {\n  id: string;\n  label: string;\n  source: ItemSource;\n  disabled: boolean;\n  states: string[];\n};\n\nconst localItem: Item = {\n  id: nanoid(),\n  label: \"Local\",\n  source: \"local\",\n  disabled: false,\n  states: [],\n};\n\nconst getItems = (): Array<Item> => [\n  {\n    id: nanoid(),\n    label: \"Token\",\n    source: \"token\",\n    disabled: false,\n    states: [],\n  },\n  {\n    id: nanoid(),\n    label: \"Tag\",\n    source: \"tag\",\n    disabled: false,\n    states: [],\n  },\n];\n\nconst createItem = (\n  label: string,\n  value: Array<Item>,\n  setValue: (value: Array<Item>) => void\n) => {\n  const item: Item = {\n    id: nanoid(),\n    label,\n    source: \"token\",\n    disabled: false,\n    states: [],\n  };\n  setValue([...value, item]);\n};\n\nconst removeItem = (\n  itemIdToRemove: Item[\"id\"],\n  value: Array<Item>,\n  setValue: (value: Array<Item>) => void\n) => {\n  setValue(value.filter((item) => item.id !== itemIdToRemove));\n};\n\nexport const StyleSourceInput = () => {\n  const [value, setValue] = useState<Array<Item>>([\n    localItem,\n    ...getItems(),\n    {\n      id: nanoid(),\n      label: \"Disabled\",\n      source: \"token\",\n      disabled: true,\n      states: [],\n    },\n  ]);\n  const [selectedItemSelector, setSelectedItemSelector] = useState<\n    undefined | ItemSelector\n  >({ styleSourceId: localItem.id });\n  const [editingItemId, setEditingItemId] = useState<undefined | Item[\"id\"]>();\n\n  const truncatedId = nanoid();\n  const [truncated, setTruncated] = useState<Array<Item>>([\n    {\n      id: truncatedId,\n      label:\n        \"Local Something Something Something Something Something Something Something\",\n      source: \"token\",\n      disabled: false,\n      states: [],\n    },\n  ]);\n\n  return (\n    <StorySection title=\"Style Source Input\">\n      <Flex\n        direction=\"column\"\n        gap=\"5\"\n        css={{ maxWidth: theme.sizes.sidebarWidth }}\n      >\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Complete (with editing & disabling)</Text>\n          <StyleSourceInputComponent\n            inputRef={() => {}}\n            css={{ width: theme.sizes.sidebarWidth }}\n            items={getItems()}\n            value={value}\n            selectedItemSelector={selectedItemSelector}\n            editingItemId={editingItemId}\n            onSelectItem={setSelectedItemSelector}\n            onEditItem={setEditingItemId}\n            onCreateItem={(label) => {\n              createItem(label, value, setValue);\n            }}\n            onSelectAutocompleteItem={(item) => {\n              setValue([...value, item]);\n            }}\n            onDetachItem={(itemToRemove) => {\n              removeItem(itemToRemove, value, setValue);\n            }}\n            onChangeItem={(changedItem) => {\n              setValue(\n                value.map((item) =>\n                  item.id === changedItem.id ? changedItem : item\n                )\n              );\n            }}\n            onDisableItem={(id) => {\n              setValue(\n                value.map((item) =>\n                  item.id === id ? { ...item, disabled: true } : item\n                )\n              );\n            }}\n            onEnableItem={(id) => {\n              setValue(\n                value.map((item) =>\n                  item.id === id ? { ...item, disabled: false } : item\n                )\n              );\n            }}\n            onSort={setValue}\n          />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Truncated item</Text>\n          <StyleSourceInputComponent\n            inputRef={() => {}}\n            css={{ width: theme.sizes.sidebarWidth }}\n            items={getItems()}\n            value={truncated}\n            selectedItemSelector={{ styleSourceId: truncatedId }}\n            onCreateItem={(label) => {\n              createItem(label, truncated, setTruncated);\n            }}\n            onSelectAutocompleteItem={(item) => {\n              setTruncated([...truncated, item]);\n            }}\n            onDetachItem={(id) => {\n              removeItem(id, truncated, setTruncated);\n            }}\n            onSort={setTruncated}\n          />\n        </Flex>\n      </Flex>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/style-source-input.tsx",
    "content": "/*\n * Style Source Input functionality\n * - Type a new input with autocomplete\n * - Select an existing source from a list\n * - Enter a new source\n * - Hover the source to see the menu\n * - Menu provides: Remove, Duplicate, Disable, Edit name\n * - Drag and drop to reorder\n * - Click to toggle select/unselect\n * - Double click to edit name\n * - Local source can only be disabled, nothing else should be possible\n */\n\nimport { nanoid } from \"nanoid\";\nimport { useFocusWithin } from \"@react-aria/interactions\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Box,\n  ComboboxListbox,\n  ComboboxListboxItem,\n  ComboboxRoot,\n  ComboboxAnchor,\n  ComboboxContent,\n  useCombobox,\n  type CSS,\n  ComboboxLabel,\n  ComboboxSeparator,\n  InputField,\n  theme,\n  styled,\n  ComboboxScrollArea,\n} from \"@webstudio-is/design-system\";\nimport type { StyleSource } from \"@webstudio-is/sdk\";\nimport {\n  forwardRef,\n  useState,\n  Fragment,\n  type ComponentProps,\n  type ForwardRefRenderFunction,\n  type RefObject,\n  type ReactNode,\n  useRef,\n  useCallback,\n} from \"react\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport {\n  type ItemSource,\n  type ItemSelector,\n  type StyleSourceError,\n  menuCssVars,\n  StyleSourceControl,\n} from \"./style-source-control\";\nimport { useSortable } from \"./use-sortable\";\nimport { matchSorter } from \"match-sorter\";\nimport { StyleSourceBadge } from \"./style-source-badge\";\nimport { $computedStyleDeclarations } from \"../shared/model\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\nimport { StyleSourceMenu, type SelectorConfig } from \"./style-source-menu\";\n\ntype IntermediateItem = {\n  id: StyleSource[\"id\"];\n  label: string;\n  disabled: boolean;\n  source: ItemSource;\n  isAdded?: boolean;\n  states: string[];\n};\n\nconst TextFieldContainer = styled(\"div\", {\n  // Custom\n  display: \"flex\",\n  flexWrap: \"wrap\",\n  alignItems: \"center\",\n  backgroundColor: theme.colors.backgroundControls,\n  gap: theme.spacing[2],\n  padding: theme.spacing[2],\n  borderRadius: theme.borderRadius[4],\n  minWidth: 0,\n  border: `1px solid transparent`,\n  \"&:hover\": {\n    borderColor: theme.colors.borderMain,\n  },\n  \"&:focus-within\": {\n    borderColor: theme.colors.borderFocus,\n  },\n});\n\ntype TextFieldBaseWrapperProps<Item extends IntermediateItem> = Omit<\n  ComponentProps<typeof InputField>,\n  \"value\"\n> &\n  Pick<ComponentProps<typeof TextFieldContainer>, \"css\"> & {\n    value: Array<Item>;\n    selectedItemSelector: undefined | ItemSelector;\n    label: string;\n    containerRef?: RefObject<HTMLDivElement>;\n    inputRef?: RefObject<HTMLInputElement>;\n    renderMenu: (params: {\n      item: Item;\n      hasStyles: boolean;\n      open: boolean;\n      onOpenChange: (open: boolean) => void;\n    }) => ReactNode;\n    onChangeItem?: (item: Item) => void;\n    onSort?: (items: Array<Item>) => void;\n    onSelectItem?: (itemSelector: ItemSelector) => void;\n    onEditItem?: (id?: Item[\"id\"]) => void;\n    editingItemId?: Item[\"id\"];\n    states: { label: string; selector: string }[];\n    error?: StyleSourceError;\n  };\n\n// Wrapper component to manage menu state per item\nconst StyleSourceControlWithMenu = <Item extends IntermediateItem>({\n  item,\n  hasStyles,\n  renderMenu,\n  ...props\n}: Omit<\n  ComponentProps<typeof StyleSourceControl>,\n  \"menu\" | \"id\" | \"hasStyles\" | \"onOpenMenu\"\n> & {\n  item: Item;\n  hasStyles: boolean;\n  renderMenu: (params: {\n    item: Item;\n    hasStyles: boolean;\n    open: boolean;\n    onOpenChange: (open: boolean) => void;\n  }) => ReactNode;\n}) => {\n  const [menuOpen, setMenuOpen] = useState(false);\n  return (\n    <StyleSourceControl\n      {...props}\n      id={item.id}\n      hasStyles={hasStyles}\n      onOpenMenu={() => setMenuOpen(true)}\n      menu={renderMenu({\n        item,\n        hasStyles,\n        open: menuOpen,\n        onOpenChange: setMenuOpen,\n      })}\n    />\n  );\n};\n\n// Returns true if style source has defined styles including on the states.\nconst getHasStylesMap = <Item extends IntermediateItem>(\n  styleSourceItems: Array<Item>,\n  computedStyleDeclarations: Array<ComputedStyleDecl>\n) => {\n  const map = new Map<Item[\"id\"], boolean>();\n  for (const item of styleSourceItems) {\n    // Style source has styles on states.\n    if (item.states.length > 0) {\n      map.set(item.id, true);\n    }\n    for (const style of computedStyleDeclarations) {\n      if (item.id === style.source.styleSourceId) {\n        map.set(item.id, true);\n        break;\n      }\n    }\n  }\n  return map;\n};\n\nconst TextFieldBase: ForwardRefRenderFunction<\n  HTMLDivElement,\n  TextFieldBaseWrapperProps<IntermediateItem>\n> = (props, forwardedRef) => {\n  const {\n    css,\n    containerRef,\n    inputRef,\n    onFocus,\n    onBlur,\n    onClick,\n    onKeyDown,\n    label,\n    value,\n    selectedItemSelector,\n    renderMenu,\n    onChangeItem,\n    onSort,\n    onSelectItem,\n    onEditItem,\n    editingItemId,\n    states,\n    error,\n    ...textFieldProps\n  } = props;\n  const { sortableRefCallback, dragItemId, placementIndicator } = useSortable({\n    items: value,\n    onSort,\n  });\n  const internalInputRef = useRef<HTMLInputElement>(null);\n\n  const { focusWithinProps } = useFocusWithin({\n    onFocusWithin: onFocus,\n    onBlurWithin: onBlur,\n  });\n\n  const onClickCapture = useCallback(() => {\n    internalInputRef.current?.focus();\n  }, [internalInputRef]);\n\n  const computedStyleDeclarations = useStore($computedStyleDeclarations);\n\n  const hasStyles = useCallback(\n    (styleSourceId: string) => {\n      const hasStylesMap = getHasStylesMap(value, computedStyleDeclarations);\n      return hasStylesMap.get(styleSourceId) ?? false;\n    },\n    [value, computedStyleDeclarations]\n  );\n\n  return (\n    <TextFieldContainer\n      {...focusWithinProps}\n      onClickCapture={onClickCapture}\n      // Setting tabIndex to -1 to allow this element to be focused via JavaScript.\n      // This is used when we need to hide the caret but want to:\n      //   1. keep the visual focused state of the component\n      //   2. keep focus somewhere insisde the component to not trigger some focus-trap logic\n      tabIndex={-1}\n      ref={mergeRefs(forwardedRef, containerRef ?? null, sortableRefCallback)}\n      css={css}\n      style={\n        dragItemId ? menuCssVars({ show: false, override: true }) : undefined\n      }\n      onKeyDown={onKeyDown}\n    >\n      {/* We want input to be the first element in DOM so it receives the focus first */}\n      <InputField\n        {...textFieldProps}\n        variant=\"chromeless\"\n        css={{\n          fontVariantNumeric: \"tabular-nums\",\n          lineHeight: 1,\n          order: 1,\n          flex: 1,\n          \"&:focus-within, &:hover\": {\n            borderColor: \"transparent\",\n          },\n        }}\n        size=\"1\"\n        value={label}\n        onClick={onClick}\n        inputRef={mergeRefs(internalInputRef, inputRef)}\n        spellCheck={false}\n        aria-label=\"New Style Source Input\"\n      />\n      {value.map((item) => (\n        <StyleSourceControlWithMenu\n          key={item.id}\n          item={item}\n          renderMenu={renderMenu}\n          selected={item.id === selectedItemSelector?.styleSourceId}\n          state={\n            item.id === selectedItemSelector?.styleSourceId\n              ? selectedItemSelector.state\n              : undefined\n          }\n          label={item.label}\n          stateLabel={\n            item.id === selectedItemSelector?.styleSourceId &&\n            selectedItemSelector.state\n              ? states.find((s) => s.selector === selectedItemSelector.state)\n                  ?.label || selectedItemSelector.state\n              : undefined\n          }\n          error={item.id === error?.id ? error : undefined}\n          disabled={item.disabled}\n          isDragging={item.id === dragItemId}\n          isEditing={item.id === editingItemId}\n          hasStyles={hasStyles(item.id)}\n          source={item.source}\n          onChangeEditing={(isEditing) => {\n            onEditItem?.(isEditing ? item.id : undefined);\n          }}\n          onSelect={() => onSelectItem?.({ styleSourceId: item.id })}\n          onChangeValue={(label) => {\n            onEditItem?.();\n            onChangeItem?.({ ...item, label });\n          }}\n        />\n      ))}\n      {placementIndicator}\n    </TextFieldContainer>\n  );\n};\n\nconst TextField = forwardRef(TextFieldBase);\nTextField.displayName = \"TextField\";\n\ntype StyleSourceInputProps<Item extends IntermediateItem> = {\n  inputRef: (element: HTMLInputElement | null) => void;\n  error?: StyleSourceError;\n  items?: Array<Item>;\n  value?: Array<Item>;\n  selectedItemSelector: undefined | ItemSelector;\n  editingItemId?: Item[\"id\"];\n  componentStates?: SelectorConfig[];\n  onSelectAutocompleteItem?: (item: Item) => void;\n  onDetachItem?: (id: Item[\"id\"]) => void;\n  onDeleteItem?: (id: Item[\"id\"]) => void;\n  onClearStyles?: (id: Item[\"id\"]) => void;\n  onDuplicateItem?: (id: Item[\"id\"]) => void;\n  onConvertToToken?: (id: Item[\"id\"]) => void;\n  onCreateItem?: (id: Item[\"id\"], label: string) => void;\n  onChangeItem?: (item: Item) => void;\n  onSelectItem?: (item: ItemSelector) => void;\n  onEditItem?: (id?: Item[\"id\"]) => void;\n  onDisableItem?: (id: Item[\"id\"]) => void;\n  onEnableItem?: (id: Item[\"id\"]) => void;\n  onSort?: (items: Array<Item>) => void;\n  css?: CSS;\n};\n\nconst newItemId = \"__NEW__\";\n\n// Maximum amount of tokens we show in the combobox\nconst maxSuggestedTokens = 50;\n\nconst matchOrSuggestToCreate = (\n  search: string,\n  items: IntermediateItem[],\n  itemToString: (item: IntermediateItem | null) => string\n): IntermediateItem[] => {\n  const matched = matchSorter(items, search, {\n    keys: [itemToString],\n  });\n  const order: ItemSource[] = [\"token\"];\n  matched.sort((leftItem, rightItem) => {\n    return order.indexOf(leftItem.source) - order.indexOf(rightItem.source);\n  });\n  if (\n    search.trim() !== \"\" &&\n    itemToString(matched[0]).toLocaleLowerCase() !==\n      search.toLocaleLowerCase().trim()\n  ) {\n    matched.unshift({\n      id: newItemId,\n      label: search.trim(),\n      disabled: false,\n      source: \"token\",\n      isAdded: false,\n      states: [],\n    });\n  }\n  // skip already added values\n  return matched\n    .filter((item) => item.isAdded === false)\n    .slice(0, maxSuggestedTokens);\n};\n\nconst markAddedValues = <Item extends IntermediateItem>(\n  items: Item[],\n  value: Item[]\n) => {\n  const valueIds = new Set();\n  for (const item of value) {\n    valueIds.add(item.id);\n  }\n  return items.map((item) => ({ ...item, isAdded: valueIds.has(item.id) }));\n};\n\nexport const StyleSourceInput = (\n  props: StyleSourceInputProps<IntermediateItem>\n) => {\n  const value = props.value ?? [];\n  const [label, setLabel] = useState(\"\");\n\n  const {\n    items,\n    getInputProps,\n    getComboboxProps,\n    getMenuProps,\n    getItemProps,\n    isOpen,\n  } = useCombobox<IntermediateItem>({\n    getItems: () => markAddedValues(props.items ?? [], value),\n    value: {\n      label,\n      disabled: false,\n      id: \"\",\n      source: \"local\",\n      states: [],\n    },\n    selectedItem: undefined,\n    match: matchOrSuggestToCreate,\n    itemToString: (item) => (item ? item.label : \"\"),\n    onItemSelect(item) {\n      setLabel(\"\");\n      if (item.id === newItemId) {\n        props.onCreateItem?.(nanoid(), item.label);\n      } else {\n        props.onSelectAutocompleteItem?.(item);\n      }\n    },\n    onChange(label) {\n      setLabel(label ?? \"\");\n    },\n  });\n\n  const inputProps = getInputProps({\n    onKeyDown(event) {\n      if (\n        event.key === \"Backspace\" &&\n        label === \"\" &&\n        props.editingItemId === undefined\n      ) {\n        const item = value[value.length - 2];\n        if (item) {\n          props.onDetachItem?.(item.id);\n        }\n      }\n    },\n  });\n\n  let hasNewTokenItem = false;\n  let hasGlobalTokenItem = false;\n\n  const states = props.componentStates ?? [];\n\n  return (\n    <ComboboxRoot open={isOpen}>\n      <Box {...getComboboxProps()}>\n        <ComboboxAnchor>\n          <TextField\n            // @todo inputProps is any which breaks all types passed to TextField\n            {...inputProps}\n            inputRef={props.inputRef}\n            error={props.error}\n            renderMenu={({ item, hasStyles, open, onOpenChange }) => (\n              <StyleSourceMenu\n                open={open}\n                onOpenChange={onOpenChange}\n                selectedItemSelector={props.selectedItemSelector}\n                item={item}\n                hasStyles={hasStyles}\n                states={states}\n                onAddSelector={(itemId, selector) => {\n                  props.onSelectItem?.({\n                    styleSourceId: itemId,\n                    state: selector,\n                  });\n                }}\n                onSelect={props.onSelectItem}\n                onDuplicate={props.onDuplicateItem}\n                onConvertToToken={props.onConvertToToken}\n                onEnable={props.onEnableItem}\n                onDisable={props.onDisableItem}\n                onEdit={props.onEditItem}\n                onDetach={props.onDetachItem}\n                onDelete={props.onDeleteItem}\n                onClearStyles={props.onClearStyles}\n              />\n            )}\n            onChangeItem={props.onChangeItem}\n            onSelectItem={props.onSelectItem}\n            onEditItem={props.onEditItem}\n            onSort={props.onSort}\n            label={label}\n            value={value}\n            states={states}\n            selectedItemSelector={props.selectedItemSelector}\n            css={props.css}\n            editingItemId={props.editingItemId}\n          />\n        </ComboboxAnchor>\n        <ComboboxContent align=\"start\" sideOffset={5}>\n          <ComboboxListbox {...getMenuProps()}>\n            <ComboboxScrollArea>\n              {isOpen &&\n                items.map((item, index) => {\n                  if (item.source === \"local\") {\n                    return;\n                  }\n\n                  if (item.id === newItemId) {\n                    hasNewTokenItem = true;\n                    const { key, ...itemProps } = getItemProps({ item, index });\n                    return (\n                      <Fragment key={index}>\n                        <ComboboxLabel>New token</ComboboxLabel>\n                        <ComboboxListboxItem\n                          {...itemProps}\n                          key={key}\n                          selectable={false}\n                        >\n                          <div>\n                            Create{\" \"}\n                            <StyleSourceBadge source=\"token\">\n                              {item.label}\n                            </StyleSourceBadge>\n                          </div>\n                        </ComboboxListboxItem>\n                      </Fragment>\n                    );\n                  }\n\n                  let label = null;\n                  if (item.source === \"token\" && hasGlobalTokenItem === false) {\n                    hasGlobalTokenItem = true;\n                    label = (\n                      <>\n                        {hasNewTokenItem && <ComboboxSeparator />}\n                        <ComboboxLabel>Global tokens</ComboboxLabel>\n                      </>\n                    );\n                  }\n\n                  const { key, ...itemProps } = getItemProps({ item, index });\n                  return (\n                    <Fragment key={index}>\n                      {label}\n                      <ComboboxListboxItem\n                        {...itemProps}\n                        key={key}\n                        selectable={false}\n                      >\n                        <StyleSourceBadge source={item.source}>\n                          {item.label}\n                        </StyleSourceBadge>\n                      </ComboboxListboxItem>\n                    </Fragment>\n                  );\n                })}\n            </ComboboxScrollArea>\n          </ComboboxListbox>\n        </ComboboxContent>\n      </Box>\n    </ComboboxRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/style-source-menu.tsx",
    "content": "import { Fragment, useState } from \"react\";\nimport {\n  Box,\n  Combobox,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n  Flex,\n  InputErrorsTooltip,\n  rawTheme,\n  styled,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { CheckMarkIcon, ChevronDownIcon, DotIcon } from \"@webstudio-is/icons\";\nimport {\n  pseudoClassDescriptions,\n  pseudoElementDescriptions,\n  validateSelector,\n} from \"@webstudio-is/css-data\";\nimport {\n  menuCssVars,\n  type ItemSource,\n  type ItemSelector,\n} from \"./style-source-control\";\n\nexport type SelectorConfig = {\n  type: \"state\" | \"pseudoElement\";\n  selector: string;\n  label: string;\n  description?: string;\n  source?: \"native\" | \"component\" | \"custom\";\n};\n\ntype IntermediateItem = {\n  id: string;\n  label: string;\n  disabled: boolean;\n  source: ItemSource;\n  isAdded?: boolean;\n  states: string[];\n};\n\nconst menuTriggerVisibilityVar = \"--ws-style-source-menu-trigger-visibility\";\nconst menuTriggerVisibilityOverrideVar =\n  \"--ws-style-source-menu-trigger-visibility-override\";\nconst visibility = `var(${menuTriggerVisibilityOverrideVar}, var(${menuTriggerVisibilityVar}))`;\nconst menuTriggerGradientVar = \"--ws-style-source-menu-trigger-gradient\";\n\nconst MenuTrigger = styled(\"button\", {\n  display: \"inline-flex\",\n  border: \"none\",\n  boxSizing: \"border-box\",\n  minWidth: 0,\n  alignItems: \"center\",\n  position: \"absolute\",\n  right: 0,\n  top: 0,\n  height: \"100%\",\n  padding: 0,\n  borderTopRightRadius: theme.borderRadius[4],\n  borderBottomRightRadius: theme.borderRadius[4],\n  color: theme.colors.foregroundContrastMain,\n  visibility,\n  \"&:hover, &[data-state=open]\": {\n    ...menuCssVars({ show: true }),\n    \"&::after\": {\n      content: '\"\"',\n      display: \"block\",\n      position: \"absolute\",\n      top: 0,\n      right: 0,\n      width: \"100%\",\n      height: \"100%\",\n      visibility,\n      backgroundColor: theme.colors.backgroundButtonHover,\n      borderTopRightRadius: theme.borderRadius[4],\n      borderBottomRightRadius: theme.borderRadius[4],\n      pointerEvents: \"none\",\n    },\n  },\n});\n\nconst MenuTriggerGradient = styled(Box, {\n  position: \"absolute\",\n  top: 0,\n  right: 0,\n  width: theme.sizes.controlHeight,\n  height: \"100%\",\n  visibility,\n  background: `var(${menuTriggerGradientVar})`,\n  borderTopRightRadius: theme.borderRadius[4],\n  borderBottomRightRadius: theme.borderRadius[4],\n  pointerEvents: \"none\",\n});\n\nconst selectorLabels = [\n  \"state\",\n  \"pseudoElement\",\n] satisfies SelectorConfig[\"type\"][];\n\nconst categoryLabels: Record<SelectorConfig[\"type\"], string> = {\n  state: \"States\",\n  pseudoElement: \"Pseudo elements\",\n};\n\nconst menuActionDescriptions = {\n  rename: \"Change the name of this token to better describe its purpose.\",\n  duplicate: \"Create a copy of this token with all its styles.\",\n  convertToToken:\n    \"Turn local styles into a reusable token you can apply to other elements.\",\n  clearStyles: \"Remove all styles from this local style source.\",\n  detach: \"Remove this token from the element without deleting it.\",\n  delete: \"Permanently delete this token and all its styles from the project.\",\n} as const;\n\ntype MenuAction = keyof typeof menuActionDescriptions;\n\n// All available CSS selectors for autocomplete\nconst allSelectors = [\n  ...Object.keys(pseudoClassDescriptions),\n  ...Object.keys(pseudoElementDescriptions),\n];\n\nconst getDescription = (selector: string, type: \"state\" | \"pseudoElement\") => {\n  // Normalize the selector to match the description keys\n  const normalized = selector.startsWith(\":\") ? selector : `:${selector}`;\n  const doubleColon = selector.startsWith(\"::\")\n    ? selector\n    : `::${selector.replace(/^:/, \"\")}`;\n\n  if (type === \"pseudoElement\") {\n    return (\n      pseudoElementDescriptions[doubleColon] ??\n      pseudoElementDescriptions[selector]\n    );\n  }\n  return (\n    pseudoClassDescriptions[normalized] ?? pseudoClassDescriptions[selector]\n  );\n};\n\nconst getSelectorDescription = (selector: string | null | undefined) => {\n  if (selector === undefined || selector === null) {\n    return;\n  }\n  // Determine type based on which description object contains the selector\n  const type =\n    selector in pseudoElementDescriptions ? \"pseudoElement\" : \"state\";\n  const description = getDescription(selector, type);\n  if (description === undefined) {\n    return;\n  }\n  return <Box css={{ maxWidth: theme.spacing[26] }}>{description}</Box>;\n};\n\nconst SelectorCombobox = ({\n  existingSelectors,\n  onSelect,\n}: {\n  existingSelectors: string[];\n  onSelect: (selector: string) => void;\n}) => {\n  const [value, setValue] = useState(\"\");\n  const [error, setError] = useState<string>();\n\n  const handleSubmit = (selector: string) => {\n    const validation = validateSelector(selector);\n    if (validation.success === false) {\n      setError(validation.error);\n      return;\n    }\n    setError(undefined);\n    onSelect(selector);\n    setValue(\"\");\n  };\n\n  const availableItems = allSelectors.filter((selector) =>\n    existingSelectors.every((s) => s !== selector)\n  );\n\n  return (\n    <form\n      onKeyDown={(event) => event.stopPropagation()}\n      onSubmit={(event) => {\n        event.preventDefault();\n        if (value.trim()) {\n          handleSubmit(value.trim());\n        }\n      }}\n    >\n      <InputErrorsTooltip\n        variant=\"wrapped\"\n        errors={error ? [error] : undefined}\n      >\n        <Combobox<string>\n          autoFocus={false}\n          placeholder=\"::before\"\n          suffix={<span />}\n          color={error ? \"error\" : undefined}\n          getItems={() => availableItems}\n          value={value}\n          itemToString={(item) => item ?? \"\"}\n          getDescription={getSelectorDescription}\n          onItemSelect={(item) => {\n            if (item) {\n              handleSubmit(item);\n            }\n          }}\n          onChange={(newValue) => {\n            setValue(newValue ?? \"\");\n            if (error) {\n              setError(undefined);\n            }\n          }}\n        />\n      </InputErrorsTooltip>\n    </form>\n  );\n};\n\ntype StyleSourceMenuProps = {\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  selectedItemSelector: undefined | ItemSelector;\n  item: IntermediateItem;\n  hasStyles: boolean;\n  states: SelectorConfig[];\n  onAddSelector?: (itemId: IntermediateItem[\"id\"], selector: string) => void;\n  onSelect?: (itemSelector: ItemSelector) => void;\n  onEdit?: (itemId: IntermediateItem[\"id\"]) => void;\n  onDuplicate?: (itemId: IntermediateItem[\"id\"]) => void;\n  onConvertToToken?: (itemId: IntermediateItem[\"id\"]) => void;\n  onDisable?: (itemId: IntermediateItem[\"id\"]) => void;\n  onEnable?: (itemId: IntermediateItem[\"id\"]) => void;\n  onDetach?: (itemId: IntermediateItem[\"id\"]) => void;\n  onDelete?: (itemId: IntermediateItem[\"id\"]) => void;\n  onClearStyles?: (itemId: IntermediateItem[\"id\"]) => void;\n};\n\nexport const StyleSourceMenu = (props: StyleSourceMenuProps) => {\n  const [highlightedSelector, setHighlightedSelector] = useState<{\n    selector: string;\n    type: \"state\" | \"pseudoElement\";\n    description?: string;\n  }>();\n  const [highlightedAction, setHighlightedAction] = useState<MenuAction>();\n\n  // Get description for highlighted or selected item\n  const selectedState = props.selectedItemSelector?.state;\n  const selectedConfig = selectedState\n    ? props.states.find((s) => s.selector === selectedState)\n    : undefined;\n  const descriptionSelector =\n    highlightedSelector ??\n    (selectedConfig\n      ? {\n          selector: selectedConfig.selector,\n          type: selectedConfig.type,\n          description: selectedConfig.description,\n        }\n      : undefined);\n\n  // Priority: action description > selector description > source description\n  const actionDescription = highlightedAction\n    ? menuActionDescriptions[highlightedAction]\n    : undefined;\n\n  const selectorDescription = descriptionSelector\n    ? (descriptionSelector.description ??\n      getDescription(descriptionSelector.selector, descriptionSelector.type))\n    : undefined;\n\n  // Get source description based on item source\n  const sourceDescription =\n    props.item.source === \"local\"\n      ? \"Style instances without creating a token or override a token locally.\"\n      : props.item.source === \"token\"\n        ? \"Reuse styles across multiple instances by creating a token.\"\n        : undefined;\n\n  const description =\n    actionDescription ?? selectorDescription ?? sourceDescription;\n\n  return (\n    <DropdownMenu modal open={props.open} onOpenChange={props.onOpenChange}>\n      <DropdownMenuTrigger asChild>\n        <MenuTrigger aria-label=\"Menu Button\">\n          <MenuTriggerGradient />\n          <ChevronDownIcon style={{ position: \"relative\" }} />\n        </MenuTrigger>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        onCloseAutoFocus={(event) => event.preventDefault()}\n        autoFocus\n        css={{ maxWidth: theme.spacing[26] }}\n      >\n        <DropdownMenuLabel>\n          <Flex gap=\"1\" justify=\"between\" align=\"center\">\n            <Text css={{ fontWeight: \"bold\" }} truncate>\n              {props.item.label}\n            </Text>\n            {props.hasStyles && (\n              <DotIcon size=\"12\" color={rawTheme.colors.foregroundPrimary} />\n            )}\n          </Flex>\n        </DropdownMenuLabel>\n        {props.item.source !== \"local\" && (\n          <DropdownMenuItem\n            onFocus={() => {\n              setHighlightedSelector(undefined);\n              setHighlightedAction(\"rename\");\n            }}\n            onSelect={() => props.onEdit?.(props.item.id)}\n          >\n            Rename\n          </DropdownMenuItem>\n        )}\n        {props.item.source !== \"local\" && (\n          <DropdownMenuItem\n            onFocus={() => {\n              setHighlightedSelector(undefined);\n              setHighlightedAction(\"duplicate\");\n            }}\n            onSelect={() => props.onDuplicate?.(props.item.id)}\n          >\n            Duplicate\n          </DropdownMenuItem>\n        )}\n        {props.item.source === \"local\" && (\n          <DropdownMenuItem\n            onFocus={() => {\n              setHighlightedSelector(undefined);\n              setHighlightedAction(\"convertToToken\");\n            }}\n            onSelect={() => props.onConvertToToken?.(props.item.id)}\n          >\n            Convert to token\n          </DropdownMenuItem>\n        )}\n        {props.item.source === \"local\" && (\n          <DropdownMenuItem\n            destructive={true}\n            onFocus={() => {\n              setHighlightedSelector(undefined);\n              setHighlightedAction(\"clearStyles\");\n            }}\n            onSelect={() => props.onClearStyles?.(props.item.id)}\n          >\n            Clear styles\n          </DropdownMenuItem>\n        )}\n        {props.item.source !== \"local\" && (\n          <DropdownMenuItem\n            onFocus={() => {\n              setHighlightedSelector(undefined);\n              setHighlightedAction(\"detach\");\n            }}\n            onSelect={() => props.onDetach?.(props.item.id)}\n          >\n            Detach\n          </DropdownMenuItem>\n        )}\n        {props.item.source !== \"local\" && (\n          <DropdownMenuItem\n            destructive={true}\n            onFocus={() => {\n              setHighlightedSelector(undefined);\n              setHighlightedAction(\"delete\");\n            }}\n            onSelect={() => props.onDelete?.(props.item.id)}\n          >\n            Delete\n          </DropdownMenuItem>\n        )}\n        {selectorLabels.map((currentCategory) => {\n          const categoryStates = props.states.filter(\n            ({ type }) => type === currentCategory\n          );\n          if (categoryStates.length === 0) {\n            return;\n          }\n          return (\n            <Fragment key={currentCategory}>\n              <DropdownMenuSeparator />\n              <DropdownMenuLabel>\n                {categoryLabels[currentCategory]}\n              </DropdownMenuLabel>\n              {categoryStates.map(\n                ({ label, selector, source, type, description }, index) => {\n                  const previousItem = categoryStates[index - 1];\n                  const showSeparator =\n                    index > 0 &&\n                    ((source === \"component\" &&\n                      previousItem?.source !== \"component\") ||\n                      (source === \"custom\" &&\n                        previousItem?.source !== \"custom\"));\n\n                  return (\n                    <Fragment key={selector}>\n                      {showSeparator && <DropdownMenuSeparator />}\n                      <DropdownMenuItem\n                        withIndicator={true}\n                        onFocus={() => {\n                          setHighlightedAction(undefined);\n                          setHighlightedSelector({\n                            selector,\n                            type,\n                            description,\n                          });\n                        }}\n                        icon={\n                          props.item.id ===\n                            props.selectedItemSelector?.styleSourceId &&\n                          selector === props.selectedItemSelector.state && (\n                            <CheckMarkIcon\n                              color={\n                                props.item.states.includes(selector)\n                                  ? rawTheme.colors.foregroundPrimary\n                                  : rawTheme.colors.foregroundIconMain\n                              }\n                              size={12}\n                            />\n                          )\n                        }\n                        onSelect={() =>\n                          props.onSelect?.({\n                            styleSourceId: props.item.id,\n                            state:\n                              props.selectedItemSelector?.state === selector\n                                ? undefined\n                                : selector,\n                          })\n                        }\n                      >\n                        <Flex justify=\"between\" align=\"center\" grow>\n                          <Text variant=\"labels\" truncate>\n                            {label}\n                          </Text>\n                          {props.item.states.includes(selector) && (\n                            <DotIcon\n                              size=\"12\"\n                              color={rawTheme.colors.foregroundPrimary}\n                            />\n                          )}\n                        </Flex>\n                      </DropdownMenuItem>\n                    </Fragment>\n                  );\n                }\n              )}\n            </Fragment>\n          );\n        })}\n        <DropdownMenuSeparator />\n        <DropdownMenuLabel>Add more</DropdownMenuLabel>\n        <Box css={{ padding: theme.spacing[4] }}>\n          <SelectorCombobox\n            existingSelectors={props.states.map((state) => state.selector)}\n            onSelect={(selector) =>\n              props.onAddSelector?.(props.item.id, selector)\n            }\n          />\n        </Box>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem hint>{description}</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source/use-sortable.tsx",
    "content": "import { useState, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  type Placement,\n  PlacementIndicator,\n  useDrag,\n  useDragCursor,\n  useDrop,\n  type DropTarget,\n  computeIndicatorPlacement,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport type { ItemSource } from \"./style-source-control\";\n\ntype UseSortable<Item> = {\n  items: Array<Item>;\n  onSort?: (items: Array<Item>) => void;\n};\n\nconst getItemId = (element: Element) =>\n  element instanceof HTMLElement ? element.dataset?.id : undefined;\n\nconst sharedDropOptions = {\n  getValidChildren(parent: Element) {\n    return [...parent.children].filter(\n      (child) => getItemId(child) !== undefined\n    );\n  },\n  childrenOrientation: { type: \"horizontal\", reverse: false },\n} as const;\n\nexport const useSortable = <Item extends { id: string; source: ItemSource }>({\n  items,\n  onSort,\n}: UseSortable<Item>) => {\n  const [dropTarget, setDropTarget] = useState<DropTarget<true>>();\n  const [placementIndicator, setPlacementIndicator] = useState<\n    undefined | Placement\n  >();\n  const [dragItemId, setDragItemId] = useState<string>();\n  const rootRef = useRef<HTMLDivElement | null>(null);\n\n  useDragCursor(dragItemId !== undefined);\n\n  // drop target is always root\n  // we need useDrop only for dropTarget.placement & dropTarget.indexWithinChildren\n  const useDropHandlers = useDrop<true>({\n    ...sharedDropOptions,\n    elementToData() {\n      return true;\n    },\n    swapDropTarget() {\n      if (rootRef.current === null) {\n        throw new Error(\"Unexpected empty rootRef during drag\");\n      }\n      return { data: true, element: rootRef.current };\n    },\n    onDropTargetChange(dropTarget) {\n      setDropTarget(dropTarget);\n      if (dropTarget === undefined) {\n        setPlacementIndicator(undefined);\n      } else {\n        // when local style source is last always drop before it\n        if (items.at(-1)?.source === \"local\") {\n          const lastIndex = items.length - 1;\n          const { placement } = dropTarget;\n          const { indexAdjustment, closestChildIndex } = placement;\n          placement.indexAdjustment = Math.min(indexAdjustment, lastIndex - 1);\n          placement.closestChildIndex = Math.min(\n            closestChildIndex,\n            lastIndex - 1\n          );\n        }\n        setPlacementIndicator(\n          computeIndicatorPlacement({\n            ...sharedDropOptions,\n            placement: dropTarget.placement,\n            element: dropTarget.element,\n          })\n        );\n      }\n    },\n  });\n\n  const useDragHandlers = useDrag<string>({\n    elementToData(element) {\n      // disable drag unless there are at least 2 items\n      if (items.length < 2) {\n        return false;\n      }\n\n      const closest = element.closest(\"[data-id]\");\n      if (closest === null) {\n        return false;\n      }\n\n      const itemId = getItemId(closest);\n      if (itemId === undefined) {\n        return false;\n      }\n\n      return itemId;\n    },\n    onStart({ data: itemId }) {\n      const item = items.find((item) => item.id === itemId);\n      if (items.at(-1) === item && item?.source === \"local\") {\n        toast.error(\"Local style source is always last and can not be moved\");\n        useDragHandlers.cancelCurrentDrag();\n        return;\n      }\n\n      setDragItemId(itemId);\n      useDropHandlers.handleStart();\n    },\n    onMove: (point) => {\n      useDropHandlers.handleMove(point);\n    },\n    onEnd({ isCanceled }) {\n      useDropHandlers.handleEnd({ isCanceled });\n      setDragItemId(undefined);\n      setDropTarget(undefined);\n      setPlacementIndicator(undefined);\n\n      if (isCanceled || dropTarget === undefined || dragItemId === undefined) {\n        return;\n      }\n\n      const oldIndex = items.findIndex((item) => item.id === dragItemId);\n      if (oldIndex !== -1) {\n        let newIndex = dropTarget.indexWithinChildren;\n\n        // placement.index does not take into account the fact that the drag item will be removed.\n        // we need to do this to account for it.\n        if (oldIndex < newIndex) {\n          newIndex = Math.max(0, newIndex - 1);\n        }\n        // when local style source is last always drop before it\n        if (items.at(-1)?.source === \"local\") {\n          const lastIndex = items.length - 1;\n          newIndex = Math.min(newIndex, lastIndex - 1);\n        }\n\n        if (oldIndex !== newIndex) {\n          const newItems = [...items];\n          newItems.splice(oldIndex, 1);\n          newItems.splice(newIndex, 0, items[oldIndex]);\n          onSort?.(newItems);\n        }\n      }\n    },\n  });\n\n  const placementIndicatorElement = placementIndicator\n    ? createPortal(\n        <PlacementIndicator placement={placementIndicator} />,\n        document.body\n      )\n    : undefined;\n\n  const sortableRefCallback = (element: HTMLDivElement | null) => {\n    useDropHandlers.rootRef(element);\n    useDragHandlers.rootRef(element);\n    rootRef.current = element;\n  };\n\n  return {\n    sortableRefCallback,\n    dragItemId,\n    placementIndicator: placementIndicatorElement,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source-section.test.ts",
    "content": "import { enableMapSet } from \"immer\";\nimport { expect, test, describe } from \"vitest\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport {\n  $instances,\n  $selectedStyleSources,\n  $styleSourceSelections,\n  $styleSources,\n} from \"~/shared/nano-states\";\nimport { addStyleSourceToInstance, __testing__ } from \"./style-source-section\";\nimport { $awareness } from \"~/shared/awareness\";\n\nconst { getComponentStates } = __testing__;\n\nenableMapSet();\nregisterContainers();\n\ndescribe(\"getComponentStates\", () => {\n  test(\"returns predefined states for tag\", () => {\n    const result = getComponentStates({\n      predefinedStates: [\":visited\", \":active\"],\n      componentStates: [],\n      instanceStyleSourceIds: new Set(),\n      styles: [],\n      selectedStyleState: undefined,\n    });\n\n    // Should include universal states (:hover, :focus, etc.) and tag-specific states\n    expect(result.some((s) => s.selector === \":hover\")).toBe(true);\n    expect(result.some((s) => s.selector === \":visited\")).toBe(true);\n    expect(result.some((s) => s.selector === \":active\")).toBe(true);\n    expect(result.every((s) => s.source === \"native\")).toBe(true);\n  });\n\n  test(\"includes selectors from instance styles only\", () => {\n    const result = getComponentStates({\n      predefinedStates: [],\n      componentStates: [],\n      instanceStyleSourceIds: new Set([\"style1\"]),\n      styles: [\n        { styleSourceId: \"style1\", state: \"::before\" },\n        { styleSourceId: \"other\", state: \"::after\" },\n      ],\n      selectedStyleState: undefined,\n    });\n\n    // Should include ::before from instance's style source\n    expect(result.some((s) => s.selector === \"::before\")).toBe(true);\n    // Should NOT include ::after from other style source\n    expect(result.some((s) => s.selector === \"::after\")).toBe(false);\n  });\n\n  test(\"marks custom selectors correctly\", () => {\n    const result = getComponentStates({\n      predefinedStates: [],\n      componentStates: [],\n      instanceStyleSourceIds: new Set([\"style1\"]),\n      styles: [{ styleSourceId: \"style1\", state: \"::before\" }],\n      selectedStyleState: undefined,\n    });\n\n    const beforeSelector = result.find((s) => s.selector === \"::before\");\n    expect(beforeSelector?.source).toBe(\"custom\");\n    expect(beforeSelector?.type).toBe(\"pseudoElement\");\n  });\n\n  test(\"includes currently selected state even without styles\", () => {\n    const result = getComponentStates({\n      predefinedStates: [],\n      componentStates: [],\n      instanceStyleSourceIds: new Set(),\n      styles: [],\n      selectedStyleState: \"::marker\",\n    });\n\n    expect(result.some((s) => s.selector === \"::marker\")).toBe(true);\n  });\n\n  test(\"includes component states\", () => {\n    const result = getComponentStates({\n      predefinedStates: [],\n      componentStates: [\n        { label: \"Open\", selector: \"[data-state=open]\" },\n        { label: \"Closed\", selector: \"[data-state=closed]\" },\n      ],\n      instanceStyleSourceIds: new Set(),\n      styles: [],\n      selectedStyleState: undefined,\n    });\n\n    const openState = result.find((s) => s.selector === \"[data-state=open]\");\n    expect(openState?.source).toBe(\"component\");\n    expect(openState?.label).toBe(\"Open\");\n  });\n\n  test(\"removes selector when styles are cleared\", () => {\n    // First, with styles\n    const withStyles = getComponentStates({\n      predefinedStates: [],\n      componentStates: [],\n      instanceStyleSourceIds: new Set([\"style1\"]),\n      styles: [{ styleSourceId: \"style1\", state: \"::before\" }],\n      selectedStyleState: undefined,\n    });\n    expect(withStyles.some((s) => s.selector === \"::before\")).toBe(true);\n\n    // After clearing styles (empty styles array)\n    const withoutStyles = getComponentStates({\n      predefinedStates: [],\n      componentStates: [],\n      instanceStyleSourceIds: new Set([\"style1\"]),\n      styles: [],\n      selectedStyleState: undefined,\n    });\n    expect(withoutStyles.some((s) => s.selector === \"::before\")).toBe(false);\n  });\n});\n\ntest(\"add style source to instance\", () => {\n  $instances.set(\n    new Map([\n      [\n        \"body\",\n        { type: \"instance\", id: \"body\", component: \"Body\", children: [] },\n      ],\n    ])\n  );\n  $awareness.set({\n    pageId: \"\",\n    instanceSelector: [\"body\"],\n  });\n  $styleSources.set(new Map([[\"local1\", { id: \"local1\", type: \"local\" }]]));\n  $styleSourceSelections.set(new Map());\n  $selectedStyleSources.set(new Map());\n\n  addStyleSourceToInstance(\"token1\");\n  expect($styleSourceSelections.get().get(\"body\")).toEqual({\n    instanceId: \"body\",\n    values: [\"token1\"],\n  });\n  expect($selectedStyleSources.get().get(\"body\")).toEqual(\"token1\");\n\n  // put new style source last\n  addStyleSourceToInstance(\"local1\");\n  expect($styleSourceSelections.get().get(\"body\")).toEqual({\n    instanceId: \"body\",\n    values: [\"token1\", \"local1\"],\n  });\n\n  // put new token before local\n  addStyleSourceToInstance(\"token2\");\n  expect($styleSourceSelections.get().get(\"body\")).toEqual({\n    instanceId: \"body\",\n    values: [\"token1\", \"token2\", \"local1\"],\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/features/style-panel/style-source-section.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { nanoid } from \"nanoid\";\nimport { computed } from \"nanostores\";\nimport { pseudoClassesByTag } from \"@webstudio-is/html-data\";\nimport { isPseudoElement } from \"@webstudio-is/css-data\";\nimport {\n  type Instance,\n  type StyleSource,\n  type StyleSourceToken,\n  type StyleSourceSelections,\n  type StyleDecl,\n  type StyleSources,\n  getStyleDeclKey,\n} from \"@webstudio-is/sdk\";\nimport { type ItemSource, StyleSourceInput } from \"./style-source\";\nimport {\n  renameStyleSource,\n  type RenameStyleSourceError,\n  deleteStyleSource,\n  DeleteStyleSourceDialog,\n} from \"~/builder/shared/style-source-actions\";\nimport {\n  $registeredComponentMetas,\n  $selectedInstanceStatesByStyleSourceId,\n  $selectedInstanceStyleSources,\n  $selectedOrLastStyleSourceSelector,\n  $selectedStyleSources,\n  $selectedStyleState,\n  $styleSourceSelections,\n  $styleSources,\n  $styles,\n} from \"~/shared/nano-states\";\nimport { removeByMutable } from \"~/shared/array-utils\";\nimport { cloneStyles } from \"~/shared/tree-utils\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { subscribe } from \"~/shared/pubsub\";\nimport { $selectedInstance } from \"~/shared/awareness\";\nimport { $instanceTags } from \"./shared/model\";\n\n// Declare command for this module\ndeclare module \"~/shared/pubsub\" {\n  interface CommandRegistry {\n    focusStyleSourceInput: undefined;\n  }\n}\n\nconst selectStyleSource = (\n  styleSourceId: StyleSource[\"id\"],\n  state?: StyleDecl[\"state\"]\n) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  const selectedStyleSources = new Map($selectedStyleSources.get());\n  selectedStyleSources.set(instanceId, styleSourceId);\n  $selectedStyleSources.set(selectedStyleSources);\n  $selectedStyleState.set(state);\n};\n\nconst deselectMatchingStyleSource = (styleSourceId: StyleSource[\"id\"]) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  const selectedStyleSources = new Map($selectedStyleSources.get());\n  if (selectedStyleSources.get(instanceId) === styleSourceId) {\n    selectedStyleSources.delete(instanceId);\n    $selectedStyleSources.set(selectedStyleSources);\n    $selectedStyleState.set(undefined);\n  }\n};\n\nconst getOrCreateStyleSourceSelectionMutable = (\n  styleSourceSelections: StyleSourceSelections,\n  selectedInstanceId: Instance[\"id\"]\n) => {\n  let styleSourceSelection = styleSourceSelections.get(selectedInstanceId);\n  if (styleSourceSelection === undefined) {\n    styleSourceSelection = {\n      instanceId: selectedInstanceId,\n      values: [],\n    };\n    styleSourceSelections.set(selectedInstanceId, styleSourceSelection);\n  }\n  return styleSourceSelection;\n};\n\nconst addStyleSourceToInstanceMutable = (\n  styleSourceSelections: StyleSourceSelections,\n  styleSources: StyleSources,\n  instanceId: Instance[\"id\"],\n  newStyleSourceId: StyleSource[\"id\"]\n) => {\n  const styleSourceSelection = getOrCreateStyleSourceSelectionMutable(\n    styleSourceSelections,\n    instanceId\n  );\n  if (styleSourceSelection.values.includes(newStyleSourceId) === false) {\n    const lastStyleSourceId = styleSourceSelection.values.at(-1);\n    const lastStyleSource =\n      lastStyleSourceId === undefined\n        ? undefined\n        : styleSources.get(lastStyleSourceId);\n    // when local style source exists insert before it\n    if (lastStyleSource?.type === \"local\") {\n      styleSourceSelection.values.splice(-1, 0, newStyleSourceId);\n    } else {\n      styleSourceSelection.values.push(newStyleSourceId);\n    }\n  }\n};\n\nconst createStyleSource = (id: StyleSource[\"id\"], name: string) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  const newStyleSource: StyleSource = {\n    type: \"token\",\n    id,\n    name,\n  };\n  serverSyncStore.createTransaction(\n    [$styleSources, $styleSourceSelections],\n    (styleSources, styleSourceSelections) => {\n      styleSources.set(newStyleSource.id, newStyleSource);\n      addStyleSourceToInstanceMutable(\n        styleSourceSelections,\n        styleSources,\n        instanceId,\n        newStyleSource.id\n      );\n    }\n  );\n  selectStyleSource(newStyleSource.id);\n};\n\nexport const addStyleSourceToInstance = (\n  newStyleSourceId: StyleSource[\"id\"]\n) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  serverSyncStore.createTransaction(\n    [$styleSourceSelections, $styleSources],\n    (styleSourceSelections, styleSources) => {\n      addStyleSourceToInstanceMutable(\n        styleSourceSelections,\n        styleSources,\n        instanceId,\n        newStyleSourceId\n      );\n    }\n  );\n  selectStyleSource(newStyleSourceId);\n};\n\nconst removeStyleSourceFromInstance = (styleSourceId: StyleSource[\"id\"]) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  serverSyncStore.createTransaction(\n    [$styleSourceSelections],\n    (styleSourceSelections) => {\n      const styleSourceSelection = styleSourceSelections.get(instanceId);\n      if (styleSourceSelection === undefined) {\n        return;\n      }\n      removeByMutable(\n        styleSourceSelection.values,\n        (item) => item === styleSourceId\n      );\n    }\n  );\n  // reset selected style source if necessary\n  deselectMatchingStyleSource(styleSourceId);\n};\n\nconst duplicateStyleSource = (styleSourceId: StyleSource[\"id\"]) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  const styleSources = $styleSources.get();\n  // style source may not exist in store which means\n  // temporary generated local stye source was not applied yet\n  const styleSource = styleSources.get(styleSourceId);\n  if (styleSource === undefined || styleSource.type === \"local\") {\n    return;\n  }\n\n  const newStyleSource: StyleSource = {\n    type: \"token\",\n    id: nanoid(),\n    name: `${styleSource.name} (copy)`,\n  };\n  const clonedStyleSourceIds = new Map();\n  clonedStyleSourceIds.set(styleSourceId, newStyleSource.id);\n  const clonedStyles = cloneStyles($styles.get(), clonedStyleSourceIds);\n\n  serverSyncStore.createTransaction(\n    [$styleSources, $styles, $styleSourceSelections],\n    (styleSources, styles, styleSourceSelections) => {\n      const styleSourceSelection = styleSourceSelections.get(instanceId);\n      if (styleSourceSelection === undefined) {\n        return;\n      }\n      // put new style source after original one\n      const position = styleSourceSelection.values.indexOf(styleSourceId);\n      styleSourceSelection.values.splice(position + 1, 0, newStyleSource.id);\n      styleSources.set(newStyleSource.id, newStyleSource);\n      for (const styleDecl of clonedStyles) {\n        styles.set(getStyleDeclKey(styleDecl), styleDecl);\n      }\n    }\n  );\n\n  selectStyleSource(newStyleSource.id);\n\n  return newStyleSource.id;\n};\n\nconst convertLocalStyleSourceToToken = (styleSourceId: StyleSource[\"id\"]) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  const newStyleSource: StyleSource = {\n    type: \"token\",\n    id: styleSourceId,\n    name: \"Local (Copy)\",\n  };\n  serverSyncStore.createTransaction(\n    [$styleSources, $styleSourceSelections],\n    (styleSources, styleSourceSelections) => {\n      const styleSourceSelection = getOrCreateStyleSourceSelectionMutable(\n        styleSourceSelections,\n        instanceId\n      );\n      // generated local style source was not applied so put last\n      if (styleSourceSelection.values.includes(newStyleSource.id) === false) {\n        styleSourceSelection.values.push(newStyleSource.id);\n      }\n      styleSources.set(newStyleSource.id, newStyleSource);\n    }\n  );\n  selectStyleSource(newStyleSource.id);\n};\n\nconst reorderStyleSources = (styleSourceIds: StyleSource[\"id\"][]) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  serverSyncStore.createTransaction(\n    [$styleSourceSelections],\n    (styleSourceSelections) => {\n      const styleSourceSelection = styleSourceSelections.get(instanceId);\n      if (styleSourceSelection === undefined) {\n        return;\n      }\n      styleSourceSelection.values = styleSourceIds;\n    }\n  );\n};\n\nconst clearStyles = (styleSourceId: StyleSource[\"id\"]) => {\n  serverSyncStore.createTransaction([$styles], (styles) => {\n    for (const [styleDeclKey, styleDecl] of styles) {\n      if (styleDecl.styleSourceId === styleSourceId) {\n        styles.delete(styleDeclKey);\n      }\n    }\n  });\n};\n\ntype SelectorConfig = {\n  type: \"state\" | \"pseudoElement\";\n  selector: string;\n  label: string;\n  source: \"native\" | \"component\" | \"custom\";\n};\n\nconst getComponentStates = ({\n  predefinedStates,\n  componentStates,\n  instanceStyleSourceIds,\n  styles,\n  selectedStyleState,\n}: {\n  predefinedStates: string[];\n  componentStates: Array<{ label: string; selector: string }>;\n  instanceStyleSourceIds: Set<StyleSource[\"id\"]>;\n  styles: Iterable<Pick<StyleDecl, \"state\" | \"styleSourceId\">>;\n  selectedStyleState: string | undefined;\n}): SelectorConfig[] => {\n  const allStates = [...pseudoClassesByTag[\"*\"], ...predefinedStates];\n\n  const usedSelectors = new Set<string>();\n  for (const styleDecl of styles) {\n    if (\n      styleDecl.state &&\n      styleDecl.state.trim() &&\n      instanceStyleSourceIds.has(styleDecl.styleSourceId)\n    ) {\n      usedSelectors.add(styleDecl.state);\n    }\n  }\n\n  // Show selected state in menu immediately, before any styles are added\n  if (selectedStyleState && selectedStyleState.trim()) {\n    usedSelectors.add(selectedStyleState);\n  }\n\n  const allStateSelectors = new Set([...allStates, ...usedSelectors]);\n\n  const toConfig = (selector: string): SelectorConfig => ({\n    type: isPseudoElement(selector) ? \"pseudoElement\" : \"state\",\n    label: selector,\n    selector,\n    source: allStates.includes(selector) ? \"native\" : \"custom\",\n  });\n\n  const states = Array.from(allStateSelectors)\n    .filter((state) => !isPseudoElement(state))\n    .map(toConfig);\n\n  const pseudoElements = Array.from(allStateSelectors)\n    .filter(isPseudoElement)\n    .map(toConfig);\n\n  const componentStatesConfig = componentStates.map((item) => ({\n    type: \"state\" as const,\n    ...item,\n    source: \"component\" as const,\n  }));\n\n  return [...states, ...componentStatesConfig, ...pseudoElements];\n};\n\nconst $componentStates = computed(\n  [\n    $selectedInstance,\n    $registeredComponentMetas,\n    $instanceTags,\n    $styles,\n    $selectedStyleState,\n    $styleSourceSelections,\n  ],\n  (\n    selectedInstance,\n    registeredComponentMetas,\n    instanceTags,\n    styles,\n    selectedStyleState,\n    styleSourceSelections\n  ) => {\n    if (selectedInstance === undefined) {\n      return;\n    }\n    const tag = instanceTags.get(selectedInstance.id);\n    const meta = registeredComponentMetas.get(selectedInstance.component);\n\n    return getComponentStates({\n      predefinedStates: pseudoClassesByTag[tag ?? \"\"] ?? [],\n      componentStates: meta?.states ?? [],\n      instanceStyleSourceIds: new Set(\n        styleSourceSelections.get(selectedInstance.id)?.values\n      ),\n      styles: styles.values(),\n      selectedStyleState,\n    });\n  }\n);\n\ntype StyleSourceInputItem = {\n  id: StyleSource[\"id\"];\n  label: string;\n  disabled: boolean;\n  source: ItemSource;\n  states: string[];\n};\n\nconst convertToInputItem = (\n  styleSource: StyleSource,\n  states: string[]\n): StyleSourceInputItem => {\n  return {\n    id: styleSource.id,\n    label: styleSource.type === \"local\" ? \"Local\" : styleSource.name,\n    disabled: false,\n    source: styleSource.type,\n    states,\n  };\n};\n\nconst $availableStyleSources = computed([$styleSources], (styleSources) => {\n  const availableStylesSources: StyleSourceInputItem[] = [];\n  for (const styleSource of styleSources.values()) {\n    if (styleSource.type === \"local\") {\n      continue;\n    }\n    availableStylesSources.push(convertToInputItem(styleSource, []));\n  }\n  return availableStylesSources;\n});\n\nexport const StyleSourcesSection = () => {\n  const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);\n  const componentStates = useStore($componentStates);\n  const availableStyleSources = useStore($availableStyleSources);\n  const selectedInstanceStyleSources = useStore($selectedInstanceStyleSources);\n  const selectedInstanceStatesByStyleSourceId = useStore(\n    $selectedInstanceStatesByStyleSourceId\n  );\n  const selectedOrLastStyleSourceSelector = useStore(\n    $selectedOrLastStyleSourceSelector\n  );\n\n  // Subscribe to focusStyleSourceInput command\n  useEffect(() => {\n    const unsubscribe = subscribe(\"command:focusStyleSourceInput\", () => {\n      if (inputRef) {\n        inputRef.focus();\n      }\n    });\n    return unsubscribe;\n  }, [inputRef]);\n\n  const value = selectedInstanceStyleSources.map((styleSource) =>\n    convertToInputItem(\n      styleSource,\n      selectedInstanceStatesByStyleSourceId.get(styleSource.id) ?? []\n    )\n  );\n\n  const [editingItemId, setEditingItemId] = useState<StyleSource[\"id\"]>();\n\n  const [tokenToDelete, setTokenToDelete] = useState<StyleSourceToken>();\n  const [error, setError] = useState<RenameStyleSourceError>();\n\n  const setEditingItem = (id?: StyleSource[\"id\"]) => {\n    // User finished editing or started editing a different token\n    if (error && (id === undefined || id !== error.id)) {\n      setError(undefined);\n    }\n    setEditingItemId(id);\n  };\n\n  return (\n    <>\n      <StyleSourceInput\n        inputRef={setInputRef}\n        error={error}\n        items={availableStyleSources}\n        value={value}\n        selectedItemSelector={selectedOrLastStyleSourceSelector}\n        componentStates={componentStates}\n        onCreateItem={createStyleSource}\n        onSelectAutocompleteItem={({ id }) => {\n          addStyleSourceToInstance(id);\n        }}\n        onDuplicateItem={(id) => {\n          const newId = duplicateStyleSource(id);\n          if (newId !== undefined) {\n            setEditingItem(newId);\n          }\n        }}\n        onConvertToToken={(id) => {\n          convertLocalStyleSourceToToken(id);\n          setEditingItem(id);\n        }}\n        onClearStyles={clearStyles}\n        onDetachItem={(id) => {\n          removeStyleSourceFromInstance(id);\n        }}\n        onDeleteItem={(id) => {\n          const styleSources = $styleSources.get();\n          const token = styleSources.get(id);\n          if (token?.type === \"token\") {\n            setTokenToDelete(token);\n          }\n        }}\n        onSort={(items) => {\n          reorderStyleSources(items.map((item) => item.id));\n        }}\n        onSelectItem={(styleSourceSelector) => {\n          selectStyleSource(\n            styleSourceSelector.styleSourceId,\n            styleSourceSelector.state\n          );\n        }}\n        // style source renaming\n        editingItemId={editingItemId}\n        onEditItem={(id) => {\n          setEditingItem(id);\n          // prevent deselect after renaming\n          if (id !== undefined) {\n            selectStyleSource(id);\n          }\n        }}\n        onChangeItem={(item) => {\n          const error = renameStyleSource(item.id, item.label);\n          if (error) {\n            setError(error);\n            setEditingItem(item.id);\n            return;\n          }\n          setError(undefined);\n        }}\n      />\n      <DeleteStyleSourceDialog\n        styleSource={tokenToDelete}\n        onClose={() => {\n          setTokenToDelete(undefined);\n        }}\n        onConfirm={(styleSourceId) => {\n          deleteStyleSource(styleSourceId);\n          setTokenToDelete(undefined);\n        }}\n      />\n    </>\n  );\n};\n\nexport const __testing__ = { getComponentStates };\n"
  },
  {
    "path": "apps/builder/app/builder/features/sync-status.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { atom } from \"nanostores\";\nimport {\n  Flex,\n  rawTheme,\n  Text,\n  theme,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport { OfflineIcon } from \"@webstudio-is/icons\";\nimport { useEffect } from \"react\";\nimport { $queueStatus } from \"~/shared/sync/project-queue\";\n\nconst $isOnline = atom(false);\n\nconst subscribeIsOnline = () => {\n  const handle = () => $isOnline.set(navigator.onLine);\n  addEventListener(\"offline\", handle);\n  addEventListener(\"online\", handle);\n  return () => {\n    removeEventListener(\"offline\", handle);\n    removeEventListener(\"online\", handle);\n  };\n};\n\nexport const SyncStatus = () => {\n  const statusObject = useStore($queueStatus);\n  const isOnline = useStore($isOnline);\n  useEffect(subscribeIsOnline, []);\n\n  if (\n    statusObject.status === \"idle\" ||\n    statusObject.status === \"running\" ||\n    statusObject.status === \"recovering\"\n  ) {\n    return null;\n  }\n\n  const containerProps = {\n    align: \"center\" as const,\n    justify: \"center\" as const,\n    css: { height: theme.spacing[\"15\"] },\n    shrink: false,\n  };\n\n  if (statusObject.status === \"failed\") {\n    return (\n      <Tooltip\n        variant=\"wrapped\"\n        content={\n          <Text>\n            {isOnline ? (\n              <>\n                Experiencing connectivity issues. Your changes will be synced\n                with Webstudio once resolved.\n              </>\n            ) : (\n              <>\n                Offline changes will be synced with Webstudio once you go\n                online.\n                <br />\n                Please check your internet connection.\n              </>\n            )}\n          </Text>\n        }\n      >\n        <Flex {...containerProps}>\n          <OfflineIcon\n            aria-label={`Sync status: failed`}\n            color={rawTheme.colors.foregroundDestructive}\n          />\n        </Flex>\n      </Tooltip>\n    );\n  }\n\n  if (statusObject.status === \"fatal\") {\n    return (\n      <Flex {...containerProps}>\n        <Tooltip variant=\"wrapped\" content={<>{statusObject.error}</>}>\n          <OfflineIcon\n            aria-label={`Sync status: fatal`}\n            color={rawTheme.colors.foregroundDestructive}\n          />\n        </Tooltip>\n      </Flex>\n    );\n  }\n\n  /* exhaustive check */\n  statusObject satisfies never;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/view-mode.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { Flex, rawTheme, Tooltip, theme } from \"@webstudio-is/design-system\";\nimport { CloudIcon } from \"@webstudio-is/icons\";\nimport { $authPermit } from \"~/shared/nano-states\";\n\nexport const ViewMode = () => {\n  const authPermit = useStore($authPermit);\n\n  if (authPermit !== \"view\") {\n    return;\n  }\n\n  return (\n    <Tooltip content={\"View mode. Your changes will not be saved\"}>\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ height: theme.spacing[\"15\"] }}\n        shrink={false}\n      >\n        <CloudIcon\n          color={rawTheme.colors.backgroundAlertMain}\n          aria-label=\"View mode. Your changes will not be saved\"\n        />\n      </Flex>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-iframe.tsx",
    "content": "import { forwardRef, useMemo, useRef, type JSX, type RefObject } from \"react\";\nimport {\n  css,\n  canvasPointerEventsPropertyName,\n} from \"@webstudio-is/design-system\";\nimport { useUnmount } from \"~/shared/hook-utils/use-mount\";\nimport { $canvasIframeState } from \"~/shared/nano-states\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport {\n  $scale,\n  $canvasWidth,\n  $canvasRect,\n} from \"~/builder/shared/nano-states\";\nimport { useWindowResizeDebounced } from \"~/shared/dom-hooks\";\nimport { mergeRefs } from \"@react-aria/utils\";\n\nconst iframeStyle = css({\n  border: \"none\",\n  pointerEvents: `var(${canvasPointerEventsPropertyName})`,\n  height: \"100%\",\n  width: \"100%\",\n  backgroundColor: \"#fff\",\n});\n\ntype CanvasIframeProps = JSX.IntrinsicElements[\"iframe\"];\n\nconst CanvasRectUpdater = ({\n  iframeRef,\n}: {\n  iframeRef: RefObject<null | HTMLIFrameElement>;\n}) => {\n  const [updateCallback, setUpdateCallback] = useState<\n    undefined | (() => void)\n  >(undefined);\n\n  useEffect(() => {\n    updateCallback?.();\n  }, [updateCallback]);\n\n  const updateRect = useCallback(() => {\n    // create new function to trigger effect\n    const task = () => {\n      if (iframeRef.current === null) {\n        return;\n      }\n\n      const rect = iframeRef.current.getBoundingClientRect();\n\n      $canvasRect.set(\n        new DOMRect(\n          Math.round(rect.x),\n          Math.round(rect.y),\n          Math.round(rect.width),\n          Math.round(rect.height)\n        )\n      );\n    };\n\n    setUpdateCallback(() => task);\n  }, [iframeRef]);\n\n  useEffect(() => {\n    updateRect();\n    const $scaleUnsubscribe = $scale.listen(updateRect);\n    const $canvasWidthUnsubscribe = $canvasWidth.listen(updateRect);\n\n    return () => {\n      $scaleUnsubscribe();\n      $canvasWidthUnsubscribe();\n    };\n  }, [updateRect]);\n\n  useWindowResizeDebounced(() => {\n    updateRect();\n  });\n\n  return null;\n};\n\nexport const CanvasIframe = forwardRef<HTMLIFrameElement, CanvasIframeProps>(\n  (props, ref) => {\n    const iframeRef = useRef<HTMLIFrameElement | null>(null);\n\n    const merrgedRef = useMemo(() => mergeRefs(ref, iframeRef), [ref]);\n\n    useUnmount(() => {\n      // Unmount does't work inside iframe.\n      $canvasIframeState.set(\"idle\");\n    });\n\n    return (\n      <>\n        <iframe\n          {...props}\n          ref={merrgedRef}\n          className={iframeStyle()}\n          credentialless=\"true\"\n        />\n        <CanvasRectUpdater iframeRef={iframeRef} />\n      </>\n    );\n  }\n);\n\nCanvasIframe.displayName = \"CanvasIframe\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/apply-scale.ts",
    "content": "import type { Rect } from \"@webstudio-is/design-system\";\n\nexport const applyScale = (rect: Rect, scale: number = 1) => {\n  // Calculate in the \"scale\" that is applied to the canvas\n  const scaleFactor = scale / 100;\n  return {\n    top: Math.round(rect.top * scaleFactor),\n    left: Math.round(rect.left * scaleFactor),\n    width: Math.round(rect.width * scaleFactor),\n    height: Math.round(rect.height * scaleFactor),\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/block-editor-context-menu.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { styled } from \"@webstudio-is/design-system\";\n\nimport {\n  $instances,\n  $modifierKeys,\n  $textEditingInstanceSelector,\n  $textEditorContextMenu,\n  $textEditorContextMenuCommand,\n  findTemplates,\n} from \"~/shared/nano-states\";\nimport { applyScale } from \"./outline\";\nimport { $scale } from \"~/builder/shared/nano-states\";\nimport { TemplatesMenu } from \"./outline/block-instance-outline\";\nimport { insertTemplateAt } from \"./outline/block-utils\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { shallowEqual } from \"shallow-equal\";\n\nconst TriggerButton = styled(\"button\", {\n  position: \"absolute\",\n  appearance: \"none\",\n  backgroundColor: \"transparent\",\n  outline: \"none\",\n  pointerEvents: \"all\",\n  border: \"none\",\n  overflow: \"hidden\",\n  padding: 0,\n});\n\nconst InertController = ({\n  onChange,\n}: {\n  onChange: (inert: boolean) => void;\n}) => {\n  const handleChange = useEffectEvent(onChange);\n\n  useEffect(() => {\n    const timeout = setTimeout(() => {\n      handleChange(false);\n    }, 0);\n\n    return () => {\n      clearTimeout(timeout);\n    };\n  }, []);\n\n  return null;\n};\n\nconst mod = (n: number, m: number): number => {\n  return ((n % m) + m) % m;\n};\n\nconst triggerTooltipContent = <>\"Templates\"</>;\n\nconst Menu = ({\n  cursorRect,\n  anchor,\n  templates,\n}: {\n  cursorRect: DOMRect;\n  anchor: InstanceSelector;\n  templates: [instance: Instance, instanceSelector: InstanceSelector][];\n}) => {\n  const [inert, setInert] = useState(true);\n  const modifierKeys = useStore($modifierKeys);\n  const scale = useStore($scale);\n  const rect = applyScale(cursorRect, scale);\n\n  const [filtered, setFiltered] = useState({ repeat: 0, templates });\n  const [value, setValue] = useState<InstanceSelector | undefined>(\n    templates[0]?.[1] ?? undefined\n  );\n\n  const [intermediateValue, setIntermediateValue] = useState<\n    InstanceSelector | undefined\n  >();\n\n  const handleValueChangeComplete = useCallback(\n    (templateSelector: InstanceSelector) => {\n      const insertBefore = modifierKeys.altKey;\n      insertTemplateAt(templateSelector, anchor, insertBefore);\n    },\n    [anchor, modifierKeys.altKey]\n  );\n\n  const currentValue = intermediateValue ?? value;\n\n  useEffect(() => {\n    return $textEditorContextMenuCommand.listen((command) => {\n      if (command === undefined) {\n        return;\n      }\n      const type = command.type;\n\n      switch (type) {\n        case \"filter\": {\n          const filter = command.value.toLowerCase();\n          const filteredTemplates = templates.filter(([template]) => {\n            const title = template.label ?? template.component;\n            return title.toLowerCase().includes(filter);\n          });\n\n          setFiltered((prev) => {\n            if (filteredTemplates.length === 0) {\n              return { repeat: prev.repeat + 1, templates: [] };\n            }\n\n            return { repeat: 0, templates: filteredTemplates };\n          });\n\n          setValue(filteredTemplates[0]?.[1] ?? undefined);\n          break;\n        }\n\n        case \"selectNext\": {\n          const index = filtered.templates.findIndex(([_, selector]) =>\n            shallowEqual(selector, currentValue)\n          );\n          const nextIndex = mod(index + 1, filtered.templates.length);\n          setValue(filtered.templates[nextIndex]?.[1] ?? undefined);\n          setIntermediateValue(undefined);\n          break;\n        }\n        case \"selectPrevious\": {\n          const index = filtered.templates.findIndex(([_, selector]) =>\n            shallowEqual(selector, currentValue)\n          );\n          const prevIndex = mod(index - 1, filtered.templates.length);\n          setValue(filtered.templates[prevIndex]?.[1] ?? undefined);\n          setIntermediateValue(undefined);\n          break;\n        }\n\n        case \"enter\": {\n          if (currentValue !== undefined) {\n            handleValueChangeComplete(currentValue);\n          }\n          break;\n        }\n\n        default:\n          (type) satisfies never;\n      }\n    });\n  }, [filtered.templates, templates, currentValue, handleValueChangeComplete]);\n\n  // @todo repeat and close\n\n  return (\n    <>\n      <TemplatesMenu\n        open={true}\n        onOpenChange={(open) => {\n          if (open) {\n            return;\n          }\n          $textEditorContextMenu.set(undefined);\n        }}\n        anchor={anchor}\n        triggerTooltipContent={triggerTooltipContent}\n        templates={filtered.templates}\n        value={currentValue}\n        onValueChangeComplete={handleValueChangeComplete}\n        onValueChange={setIntermediateValue}\n        modal={false}\n        inert={inert}\n        preventFocusOnHover={true}\n      >\n        <TriggerButton\n          css={{\n            top: rect.top,\n            left: rect.left,\n            width: rect.width,\n            height: rect.height,\n          }}\n        ></TriggerButton>\n      </TemplatesMenu>\n      <InertController onChange={setInert} />\n    </>\n  );\n};\n\nexport const TextEditorContextMenu = () => {\n  const textEditingInstanceSelector = useStore($textEditingInstanceSelector);\n  const textEditorContextMenu = useStore($textEditorContextMenu);\n  const instances = useStore($instances);\n\n  if (textEditorContextMenu === undefined) {\n    return;\n  }\n\n  if (textEditingInstanceSelector === undefined) {\n    return;\n  }\n\n  const templates = findTemplates(\n    textEditingInstanceSelector.selector,\n    instances\n  );\n\n  if (templates === undefined) {\n    return;\n  }\n\n  return (\n    <Menu\n      key={JSON.stringify(textEditingInstanceSelector.selector)}\n      cursorRect={textEditorContextMenu.cursorRect}\n      anchor={textEditingInstanceSelector.selector}\n      templates={templates}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/canvas-instance-context-menu.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { $instanceContextMenu } from \"~/shared/nano-states\";\nimport { useEffect, useRef } from \"react\";\nimport { selectInstance } from \"~/shared/awareness\";\nimport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n} from \"@webstudio-is/design-system\";\nimport { MenuItems } from \"~/builder/shared/instance-context-menu\";\nimport { $scale, $canvasRect } from \"~/builder/shared/nano-states\";\nimport { applyScale } from \"./outline\";\n\nexport const CanvasInstanceContextMenu = () => {\n  const contextMenu = useStore($instanceContextMenu);\n  const scale = useStore($scale);\n  const canvasRect = useStore($canvasRect);\n  const triggerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (contextMenu && triggerRef.current && canvasRect) {\n      selectInstance(contextMenu.instanceSelector);\n\n      // Calculate the scaled and offset position\n      const scaledPosition = applyScale(\n        {\n          left: contextMenu.position.x,\n          top: contextMenu.position.y,\n          width: 0,\n          height: 0,\n        },\n        scale\n      );\n\n      // Trigger context menu by dispatching a contextmenu event with proper coordinates\n      const event = new MouseEvent(\"contextmenu\", {\n        bubbles: true,\n        cancelable: true,\n        view: window,\n        clientX: canvasRect.left + scaledPosition.left,\n        clientY: canvasRect.top + scaledPosition.top,\n      });\n      triggerRef.current.dispatchEvent(event);\n    }\n  }, [contextMenu, canvasRect, scale]);\n\n  const handleOpenChange = (open: boolean) => {\n    if (!open) {\n      $instanceContextMenu.set(undefined);\n    }\n  };\n\n  if (!contextMenu || !canvasRect) {\n    return;\n  }\n\n  return (\n    <ContextMenu onOpenChange={handleOpenChange}>\n      <ContextMenuTrigger ref={triggerRef} style={{ display: \"none\" }} />\n      <ContextMenuContent>\n        <MenuItems />\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/canvas-tools.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { css } from \"@webstudio-is/design-system\";\nimport { PlacementIndicator } from \"@webstudio-is/design-system\";\nimport {\n  $instances,\n  $isPreviewMode,\n  $dragAndDropState,\n  $canvasToolsVisible,\n} from \"~/shared/nano-states\";\nimport {\n  CollaborativeInstanceOutline,\n  HoveredInstanceOutline,\n  SelectedInstanceOutline,\n} from \"./outline\";\nimport { GridGuides } from \"./grid-guides\";\n\nimport { Label } from \"./outline/label\";\nimport { Outline } from \"./outline/outline\";\nimport { useSubscribeDragAndDropState } from \"./use-subscribe-drag-drop-state\";\nimport { applyScale } from \"./outline\";\nimport {\n  $clampingRect,\n  $scale,\n  $isStylePanelGridVisible,\n} from \"~/builder/shared/nano-states\";\nimport { BlockChildHoveredInstanceOutline } from \"./outline/block-instance-outline\";\nimport { TextEditorContextMenu } from \"./block-editor-context-menu\";\nimport { CanvasInstanceContextMenu } from \"./canvas-instance-context-menu\";\n\nconst containerStyle = css({\n  position: \"absolute\",\n  inset: 0,\n  pointerEvents: \"none\",\n  variants: {\n    overflow: {\n      hidden: {\n        overflow: \"hidden\",\n      },\n    },\n  },\n});\n\nexport const CanvasTools = () => {\n  // @todo try to setup cross-frame atoms to avoid this\n  useSubscribeDragAndDropState();\n  const canvasToolsVisible = useStore($canvasToolsVisible);\n  const isPreviewMode = useStore($isPreviewMode);\n  const dragAndDropState = useStore($dragAndDropState);\n  const instances = useStore($instances);\n  const scale = useStore($scale);\n  const clampingRect = useStore($clampingRect);\n  const isStylePanelGridVisible = useStore($isStylePanelGridVisible);\n\n  if (!canvasToolsVisible) {\n    return;\n  }\n\n  if (clampingRect === undefined) {\n    return;\n  }\n\n  if (dragAndDropState.isDragging) {\n    if (dragAndDropState.placementIndicator === undefined) {\n      return;\n    }\n    const { dropTarget, placementIndicator } = dragAndDropState;\n    const dropTargetInstance =\n      dropTarget === undefined\n        ? undefined\n        : instances.get(dropTarget.itemSelector[0]);\n    const rect = applyScale(placementIndicator.parentRect, scale);\n\n    return dropTargetInstance ? (\n      <div className={containerStyle({ overflow: \"hidden\" })}>\n        <Outline rect={rect} clampingRect={clampingRect}>\n          <Label instance={dropTargetInstance} instanceRect={rect} />\n        </Outline>\n        {placementIndicator !== undefined && (\n          <PlacementIndicator placement={placementIndicator} scale={scale} />\n        )}\n      </div>\n    ) : null;\n  }\n\n  return (\n    <>\n      {isPreviewMode === false && (\n        <>\n          {isStylePanelGridVisible && <GridGuides />}\n          <SelectedInstanceOutline />\n          <HoveredInstanceOutline />\n          <CollaborativeInstanceOutline />\n          <BlockChildHoveredInstanceOutline />\n          <TextEditorContextMenu />\n          <CanvasInstanceContextMenu />\n        </>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/grid-guides.tsx",
    "content": "import { useMemo, useLayoutEffect, useRef } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { css } from \"@webstudio-is/design-system\";\nimport { theme, textVariants } from \"@webstudio-is/design-system\";\nimport { $gridCellData } from \"~/shared/nano-states\";\nimport {\n  $scale,\n  $gridEditingTrack,\n  $gridEditingArea,\n} from \"~/builder/shared/nano-states\";\nimport { $ephemeralStyles } from \"~/canvas/stores\";\nimport { parseGridAreas } from \"@webstudio-is/css-data\";\n\n// Compute the AABB offset that CSS transforms cause, by letting the browser\n// do all parsing and matrix composition via a hidden probe element.\n// No manual parsing of translate/rotate/scale values — works with any CSS,\n// including variables already resolved to computed values on the canvas.\nconst getTransformOffset = (cssText: string): { dx: number; dy: number } => {\n  const probe = document.createElement(\"div\");\n  // Apply the same CSS as the canvas element (width, height, transform,\n  // transform-origin, translate, rotate, scale, etc.)\n  probe.style.cssText = cssText;\n  // Override layout: fixed at viewport origin, invisible\n  probe.style.position = \"fixed\";\n  probe.style.left = \"0px\";\n  probe.style.top = \"0px\";\n  probe.style.visibility = \"hidden\";\n  probe.style.pointerEvents = \"none\";\n  document.body.appendChild(probe);\n  const bcr = probe.getBoundingClientRect();\n  document.body.removeChild(probe);\n  // Without transforms the probe sits at (0, 0).\n  // The delta is the displacement caused by CSS transforms.\n  return { dx: bcr.left, dy: bcr.top };\n};\n\nconst containerStyle = css({\n  position: \"absolute\",\n  inset: 0,\n  pointerEvents: \"none\",\n  overflow: \"hidden\",\n  // contain: strict must live here, not on the grid mirror div.\n  // The mirror applies the user's CSS transform, so placing contain\n  // on it would clip cells relative to the transformed box instead\n  // of the viewport-aligned overlay boundary.\n  contain: \"strict\",\n});\n\n// Use the browser's CSS parser to extract a single property from cssText.\n// Never write ad-hoc regex/string parsers for CSS values.\nconst probe = document.createElement(\"div\");\nconst getPropertyFromCssText = (cssText: string, property: string): string => {\n  probe.style.cssText = cssText;\n  return probe.style.getPropertyValue(property);\n};\n\n// Build a map of \"col,row\" → area name from cssText.\n// Only the top-left cell of each named area gets an entry so the\n// label appears once, not in every spanned cell.\nconst getAreaNamesByCell = (cssText: string): Map<string, string> => {\n  const areas = getPropertyFromCssText(cssText, \"grid-template-areas\");\n  const result = new Map<string, string>();\n  for (const { name, columnStart, rowStart } of parseGridAreas(areas)) {\n    result.set(`${columnStart},${rowStart}`, name);\n  }\n  return result;\n};\n\n// Build a set of all \"col,row\" keys that belong to any named area.\n// Used to suppress per-cell outlines inside areas.\nconst getAreaCells = (cssText: string): Set<string> => {\n  const areas = getPropertyFromCssText(cssText, \"grid-template-areas\");\n  const result = new Set<string>();\n  for (const { columnStart, columnEnd, rowStart, rowEnd } of parseGridAreas(\n    areas\n  )) {\n    for (let row = rowStart; row < rowEnd; row++) {\n      for (let col = columnStart; col < columnEnd; col++) {\n        result.add(`${col},${row}`);\n      }\n    }\n  }\n  return result;\n};\n\n// Return the full parsed area spans for rendering merged area outlines.\nconst getAreaSpans = (cssText: string) => {\n  const areas = getPropertyFromCssText(cssText, \"grid-template-areas\");\n  return parseGridAreas(areas);\n};\n\nconst cellStyle = css({\n  pointerEvents: \"none\",\n  outline: `1px dashed ${theme.colors.borderMain}`,\n  outlineOffset: \"-0.5px\",\n  // Area cells suppress their per-cell outline; a merged area outline\n  // div renders a single border around the whole area instead.\n  \"&[data-area]\": {\n    outline: \"none\",\n  },\n});\n\nconst areaOutlineStyle = css({\n  pointerEvents: \"none\",\n  outline: `1px solid ${theme.colors.borderMain}`,\n  outlineOffset: \"-0.5px\",\n});\n\nconst areaLabelStyle = css(textVariants.regular, {\n  position: \"absolute\",\n  top: 2,\n  left: 4,\n  color: theme.colors.foregroundSubtle,\n  pointerEvents: \"none\",\n  whiteSpace: \"nowrap\",\n  overflow: \"hidden\",\n  textOverflow: \"ellipsis\",\n  maxWidth: \"calc(100% - 8px)\",\n});\n\nconst highlightStyle = css({\n  pointerEvents: \"none\",\n  backgroundColor: \"oklch(94.8% 0.027 246.4 / 0.6)\",\n});\n\nexport const GridGuides = () => {\n  const gridCellData = useStore($gridCellData);\n  const gridEditingTrack = useStore($gridEditingTrack);\n  const gridEditingArea = useStore($gridEditingArea);\n  const scale = useStore($scale);\n  const ephemeralStyles = useStore($ephemeralStyles);\n  const mirrorRef = useRef<HTMLDivElement>(null);\n\n  const resolvedCssText = gridCellData?.resolvedCssText;\n\n  // Compute the transform-induced offset so we can position the wrapper at\n  // the untransformed layout origin. The mirror div then re-applies the CSS\n  // transforms, landing the overlay at the correct visual position.\n  // Uses a hidden probe element — the browser handles all CSS parsing.\n  const transformOffset = useMemo(\n    () =>\n      resolvedCssText ? getTransformOffset(resolvedCssText) : { dx: 0, dy: 0 },\n    [resolvedCssText]\n  );\n\n  // Parse area names from the cssText — only the top-left cell gets a label\n  const areaNames = useMemo(\n    () => getAreaNamesByCell(resolvedCssText ?? \"\"),\n    [resolvedCssText]\n  );\n\n  // Set of all cells covered by any named area — for outline styling\n  const areaCells = useMemo(\n    () => getAreaCells(resolvedCssText ?? \"\"),\n    [resolvedCssText]\n  );\n\n  // Full area spans for rendering merged outlines\n  const areaSpans = useMemo(\n    () => getAreaSpans(resolvedCssText ?? \"\"),\n    [resolvedCssText]\n  );\n\n  // Apply canvas CSS to the mirror div via cssText — bypasses React's\n  // style system so we can sync any property without type gymnastics.\n  // useLayoutEffect (not useEffect) to apply styles synchronously before\n  // paint, avoiding a one-frame flash of unstyled content.\n  useLayoutEffect(() => {\n    if (mirrorRef.current && resolvedCssText !== undefined) {\n      mirrorRef.current.style.cssText = resolvedCssText;\n    }\n  }, [resolvedCssText]);\n\n  if (!gridCellData || ephemeralStyles.length !== 0) {\n    return null;\n  }\n\n  const {\n    bcr,\n    untransformedWidth,\n    untransformedHeight,\n    columnCount,\n    rowCount,\n  } = gridCellData;\n\n  const scaleFactor = scale / 100;\n\n  // Recover untransformed position: BCR includes transforms, subtract them.\n  const untransformedLeft = bcr.left - transformOffset.dx;\n  const untransformedTop = bcr.top - transformOffset.dy;\n\n  // Generate one cell per grid slot\n  const cells: Array<{ col: number; row: number }> = [];\n  for (let row = 1; row <= rowCount; row++) {\n    for (let col = 1; col <= columnCount; col++) {\n      cells.push({ col, row });\n    }\n  }\n\n  return (\n    <div className={containerStyle()}>\n      {/* Positioning wrapper: our scale + translate to place the overlay */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          transform: `scale(${scaleFactor}) translate3d(${untransformedLeft}px, ${untransformedTop}px, 0)`,\n          transformOrigin: \"0 0\",\n          width: untransformedWidth,\n          height: untransformedHeight,\n          pointerEvents: \"none\",\n        }}\n      >\n        {/* Grid mirror: faithfully reproduces the canvas element's CSS.\n            Styles applied via cssText in useEffect — adding a new synced\n            property only requires a one-line change in grid-guide-utils. */}\n        <div ref={mirrorRef}>\n          {cells.map(({ col, row }) => {\n            const areaName = areaNames.get(`${col},${row}`);\n            const isArea = areaCells.has(`${col},${row}`);\n            return (\n              <div\n                key={`${col}-${row}`}\n                className={cellStyle()}\n                data-area={isArea ? \"\" : undefined}\n                style={{\n                  gridColumn: col,\n                  gridRow: row,\n                  position: \"relative\",\n                }}\n              >\n                {areaName && (\n                  <span className={areaLabelStyle()}>{areaName}</span>\n                )}\n              </div>\n            );\n          })}\n          {/* Merged area outlines: one div per named area spanning its\n              full grid region, rendered after cells so it paints on top. */}\n          {areaSpans.map((area) => (\n            <div\n              key={`area-outline-${area.name}`}\n              className={areaOutlineStyle()}\n              style={{\n                gridColumn: `${area.columnStart} / ${area.columnEnd}`,\n                gridRow: `${area.rowStart} / ${area.rowEnd}`,\n              }}\n            />\n          ))}\n          {gridEditingTrack && (\n            <div\n              className={highlightStyle()}\n              style={\n                gridEditingTrack.type === \"column\"\n                  ? {\n                      gridColumn: gridEditingTrack.index + 1,\n                      gridRow: \"1 / -1\",\n                    }\n                  : {\n                      gridColumn: \"1 / -1\",\n                      gridRow: gridEditingTrack.index + 1,\n                    }\n              }\n            />\n          )}\n          {gridEditingArea && (\n            <div\n              className={highlightStyle()}\n              style={{\n                gridColumn: `${gridEditingArea.columnStart} / ${gridEditingArea.columnEnd}`,\n                gridRow: `${gridEditingArea.rowStart} / ${gridEditingArea.rowEnd}`,\n              }}\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/index.ts",
    "content": "export { CanvasTools } from \"./canvas-tools\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/media-badge.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { Flex, Text, css, theme } from \"@webstudio-is/design-system\";\nimport { $selectedBreakpoint } from \"~/shared/nano-states\";\n\nconst labelStyle = css({\n  position: \"absolute\",\n  top: \"50%\",\n  left: `calc(-${theme.spacing[23]})`,\n  transform: \"rotate(-90deg)\",\n});\n\nconst badgeStyle = css({\n  background: theme.colors.backgroundTopbarHover,\n  px: theme.spacing[3],\n  py: theme.spacing[2],\n  borderRadius: theme.borderRadius[3],\n});\n\nexport const MediaBadge = () => {\n  const breakpoint = useStore($selectedBreakpoint);\n  if (breakpoint === undefined) {\n    return null;\n  }\n  const media =\n    breakpoint.maxWidth !== undefined\n      ? `@media (max-width: ${breakpoint.maxWidth}px)`\n      : breakpoint.minWidth !== undefined\n        ? `@media (min-width: ${breakpoint.minWidth}px)`\n        : undefined;\n  if (media === undefined) {\n    return null;\n  }\n  return (\n    <Flex gap=\"2\" className={labelStyle()} align=\"center\">\n      <Text variant=\"labels\" color=\"contrast\" className={badgeStyle()}>\n        {breakpoint.maxWidth ?? breakpoint.minWidth}\n      </Text>\n      <Text variant=\"labels\">{media}</Text>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/block-instance-outline.tsx",
    "content": "import { useRef, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Box,\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n  Flex,\n  theme,\n  IconButton,\n  Tooltip,\n  Kbd,\n  Text,\n  Grid,\n  DropdownMenuSeparator,\n  menuItemCss,\n} from \"@webstudio-is/design-system\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { PlusIcon, TrashIcon } from \"@webstudio-is/icons\";\nimport {\n  $blockChildOutline,\n  $hoveredInstanceOutline,\n  $hoveredInstanceSelector,\n  $instances,\n  $isContentMode,\n  $modifierKeys,\n  findBlockSelector,\n  findTemplates,\n  type BlockChildOutline,\n} from \"~/shared/nano-states\";\nimport { $clampingRect, $scale } from \"~/builder/shared/nano-states\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\nimport {\n  deleteInstanceMutable,\n  updateWebstudioData,\n} from \"~/shared/instance-utils\";\nimport { skipInertHandlersAttribute } from \"~/builder/shared/inert-handlers\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport { getInstancePath } from \"~/shared/awareness\";\nimport { insertTemplateAt } from \"./block-utils\";\nimport { Outline } from \"./outline\";\nimport { applyScale } from \"../apply-scale\";\nimport {\n  getInstanceLabel,\n  InstanceIcon,\n} from \"~/builder/shared/instance-label\";\n\nexport const TemplatesMenu = ({\n  onOpenChange,\n  open,\n  children,\n  anchor,\n  triggerTooltipContent,\n  templates,\n  value,\n  onValueChangeComplete,\n  onValueChange,\n  modal,\n  inert,\n  preventFocusOnHover,\n}: {\n  children: React.ReactNode;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  anchor: InstanceSelector;\n  triggerTooltipContent: JSX.Element;\n  templates: [instance: Instance, instanceSelector: InstanceSelector][];\n  value: InstanceSelector | undefined;\n  onValueChangeComplete: (value: InstanceSelector) => void;\n  onValueChange?: undefined | ((value: InstanceSelector | undefined) => void);\n  modal: boolean;\n  inert: boolean;\n  preventFocusOnHover: boolean;\n}) => {\n  const instances = useStore($instances);\n  const modifierKeys = useStore($modifierKeys);\n\n  const blockInstanceSelector = findBlockSelector(anchor, instances);\n\n  const handleValueChangeComplete = useEffectEvent((value: string) => {\n    const templateSelector = JSON.parse(value) as InstanceSelector;\n    onValueChangeComplete(templateSelector);\n  });\n\n  const handleValueChange = useEffectEvent(\n    (value: InstanceSelector | undefined) => {\n      onValueChange?.(value);\n    }\n  );\n\n  if (blockInstanceSelector === undefined) {\n    return;\n  }\n\n  const blockInstance = instances.get(blockInstanceSelector[0]);\n\n  if (blockInstance === undefined) {\n    return;\n  }\n\n  // 1 child is Templates instance\n  const hasChildren = blockInstance.children.length > 1;\n\n  const menuItems = templates?.map(([template, templateSelector]) => ({\n    id: template.id,\n    icon: <InstanceIcon instance={{ component: template.component }} />,\n    title: getInstanceLabel(template),\n    value: templateSelector,\n  }));\n\n  return (\n    <DropdownMenu onOpenChange={onOpenChange} open={open} modal={modal}>\n      <Tooltip\n        content={triggerTooltipContent}\n        side=\"top\"\n        disableHoverableContent\n      >\n        <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n      </Tooltip>\n      <DropdownMenuContent\n        align=\"start\"\n        sideOffset={4}\n        collisionPadding={16}\n        side=\"bottom\"\n        loop\n        // @todo remove inert after creation\n        {...(inert ? { inert: \"\" } : {})}\n      >\n        {templates.length > 0 ? (\n          <>\n            <DropdownMenuRadioGroup\n              value={value !== undefined ? JSON.stringify(value) : value}\n              onValueChange={handleValueChangeComplete}\n            >\n              {menuItems?.map((item) => (\n                <DropdownMenuRadioItem\n                  aria-selected={\n                    JSON.stringify(item.value) === JSON.stringify(value)\n                  }\n                  onPointerEnter={() => {\n                    handleValueChange(value);\n                  }}\n                  onPointerMove={\n                    preventFocusOnHover\n                      ? (e) => {\n                          e.preventDefault();\n                        }\n                      : undefined\n                  }\n                  onPointerLeave={\n                    preventFocusOnHover\n                      ? (e) => {\n                          handleValueChange(undefined);\n                          e.preventDefault();\n                        }\n                      : undefined\n                  }\n                  onPointerDown={\n                    preventFocusOnHover\n                      ? (event) => {\n                          event.preventDefault();\n                        }\n                      : undefined\n                  }\n                  key={item.id}\n                  value={JSON.stringify(item.value)}\n                  {...{ [skipInertHandlersAttribute]: true }}\n                >\n                  <Flex css={{ px: theme.spacing[3] }} gap={2} data-xxx>\n                    {item.icon}\n                    <Box css={{ textTransform: \"none\" }}>{item.title}</Box>\n                  </Flex>\n                </DropdownMenuRadioItem>\n              ))}\n            </DropdownMenuRadioGroup>\n            <DropdownMenuSeparator />\n            <div className={menuItemCss({ hint: true })}>\n              <Grid css={{ width: theme.spacing[25] }}>\n                <Flex\n                  gap={1}\n                  css={{ display: hasChildren ? \"none\" : undefined }}\n                >\n                  <Kbd value={[\"click\"]} />\n                  <Text>to add before</Text>\n                </Flex>\n\n                <Flex\n                  gap={1}\n                  css={{\n                    order: modifierKeys.altKey ? 2 : 0,\n                    display: hasChildren ? undefined : \"none\",\n                  }}\n                >\n                  <Kbd value={[\"click\"]} />\n                  <Text>to add after</Text>\n                </Flex>\n                <Flex\n                  gap={1}\n                  css={{\n                    order: 1,\n                    display: hasChildren ? undefined : \"none\",\n                  }}\n                >\n                  <Kbd value={[\"alt\", \"click\"]} /> <Text>to add before</Text>\n                </Flex>\n              </Grid>\n            </div>\n          </>\n        ) : (\n          <div className={menuItemCss({ hint: true })}>\n            <Grid css={{ width: theme.spacing[25] }}>\n              <Text>No Results</Text>\n            </Grid>\n          </div>\n        )}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\nexport const BlockChildHoveredInstanceOutline = () => {\n  const blockChildOutline = useStore($blockChildOutline);\n  const scale = useStore($scale);\n  const isContentMode = useStore($isContentMode);\n  const modifierKeys = useStore($modifierKeys);\n  const instances = useStore($instances);\n  const clampingRect = useStore($clampingRect);\n\n  const timeoutRef = useRef<undefined | ReturnType<typeof setTimeout>>(\n    undefined\n  );\n  const [buttonOutline, setButtonOutline] = useState<\n    undefined | BlockChildOutline\n  >(undefined);\n\n  const outline = blockChildOutline ?? buttonOutline;\n\n  const [isMenuOpen, setIsMenuOpen] = useState(false);\n\n  if (!isContentMode) {\n    return;\n  }\n\n  if (outline === undefined) {\n    return;\n  }\n\n  if (clampingRect === undefined) {\n    return;\n  }\n\n  const blockInstanceSelector = findBlockSelector(outline.selector, instances);\n\n  if (blockInstanceSelector === undefined) {\n    return;\n  }\n\n  const blockInstance = instances.get(blockInstanceSelector[0]);\n\n  if (blockInstance === undefined) {\n    return;\n  }\n\n  const templates = findTemplates(outline.selector, instances);\n\n  if (templates === undefined) {\n    return;\n  }\n\n  if (templates.length === 0) {\n    return;\n  }\n\n  // 1 child is Templates instance\n  const hasChildren = blockInstance.children.length > 1;\n\n  const rect = applyScale(outline.rect, scale);\n\n  // Check if the top edge of the component is hidden (clipped by viewport/clamping)\n  const isTopEdgeHidden = rect.top < clampingRect.top;\n\n  const isAddMode = isMenuOpen || !modifierKeys.altKey || !hasChildren;\n\n  const tooltipContent = (\n    <Grid>\n      <Flex gap={1} css={{ order: isAddMode ? 0 : 2 }}>\n        <Kbd value={[\"click\"]} color=\"contrast\" />\n        <Text color=\"subtle\">to add block</Text>\n      </Flex>\n      <Flex\n        gap={1}\n        css={{ order: 1, display: !hasChildren ? \"none\" : undefined }}\n      >\n        <Kbd value={[\"alt\", \"click\"]} color=\"contrast\" />{\" \"}\n        <Text color=\"subtle\">to delete</Text>\n      </Flex>\n    </Grid>\n  );\n\n  return (\n    <Outline rect={rect} clampingRect={clampingRect}>\n      <Flex\n        css={{\n          position: \"absolute\",\n          left: 0,\n          paddingRight: theme.sizes.controlHeight,\n          ...(isTopEdgeHidden\n            ? {\n                bottom: `calc(-${theme.sizes.controlHeight} )`,\n                clipPath: `polygon(0% 0%, 100% 0%, ${theme.sizes.controlHeight} 100%, 0% 100%)`,\n              }\n            : {\n                top: `calc(-${theme.sizes.controlHeight})`,\n                clipPath: `polygon(0% 0%, ${theme.sizes.controlHeight} 0%, 100% 100%, 0% 100%)`,\n              }),\n          // Define grace area for the button\n          pointerEvents: isMenuOpen ? \"none\" : \"all\",\n        }}\n        onMouseEnter={() => {\n          clearTimeout(timeoutRef.current);\n          setButtonOutline(outline);\n        }}\n        onMouseLeave={() => {\n          if (isMenuOpen) {\n            return;\n          }\n\n          clearTimeout(timeoutRef.current);\n\n          timeoutRef.current = setTimeout(() => {\n            setButtonOutline(undefined);\n          }, 100);\n        }}\n      >\n        <TemplatesMenu\n          open={isMenuOpen}\n          onOpenChange={(open) => {\n            if (!isAddMode) {\n              return;\n            }\n\n            setIsMenuOpen(open);\n\n            if (!open) {\n              setButtonOutline(undefined);\n            }\n          }}\n          anchor={outline.selector}\n          triggerTooltipContent={tooltipContent}\n          templates={templates}\n          onValueChangeComplete={(templateSelector) => {\n            const insertBefore = modifierKeys.altKey;\n            insertTemplateAt(templateSelector, outline.selector, insertBefore);\n          }}\n          value={undefined}\n          modal={true}\n          inert={false}\n          preventFocusOnHover={false}\n        >\n          <IconButton\n            variant={isAddMode ? \"local\" : \"overwritten\"}\n            onClick={() => {\n              if (isAddMode) {\n                return;\n              }\n\n              updateWebstudioData((data) => {\n                deleteInstanceMutable(\n                  data,\n                  getInstancePath(outline.selector, data.instances)\n                );\n              });\n\n              setButtonOutline(undefined);\n              $blockChildOutline.set(undefined);\n              $hoveredInstanceSelector.set(undefined);\n              $hoveredInstanceOutline.set(undefined);\n            }}\n            css={{\n              borderStyle: \"solid\",\n              borderColor: isAddMode\n                ? `oklch(from ${theme.colors.backgroundPrimary} l c h / 0.7)`\n                : undefined,\n              borderRadius: theme.borderRadius[4],\n              ...(isTopEdgeHidden\n                ? {\n                    borderTopLeftRadius: 0,\n                    borderTopRightRadius: 0,\n                  }\n                : {\n                    borderBottomLeftRadius: 0,\n                    borderBottomRightRadius: 0,\n                  }),\n              // Define grace area for the button\n              pointerEvents: isMenuOpen ? \"none\" : \"all\",\n            }}\n          >\n            {isAddMode ? <PlusIcon /> : <TrashIcon />}\n          </IconButton>\n        </TemplatesMenu>\n      </Flex>\n    </Outline>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/block-utils.ts",
    "content": "import type { Instance, Instances } from \"@webstudio-is/sdk\";\nimport { blockTemplateComponent } from \"@webstudio-is/sdk\";\nimport { shallowEqual } from \"shallow-equal\";\nimport { selectInstance } from \"~/shared/awareness\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport { findAvailableVariables } from \"~/shared/data-variables\";\nimport {\n  extractWebstudioFragment,\n  findAllEditableInstanceSelector,\n  getWebstudioData,\n  insertInstanceChildrenMutable,\n  insertWebstudioFragmentCopy,\n  updateWebstudioData,\n  detectFragmentTokenConflicts,\n} from \"~/shared/instance-utils\";\nimport {\n  $instances,\n  $project,\n  $registeredComponentMetas,\n  $textEditingInstanceSelector,\n  findBlockChildSelector,\n  findBlockSelector,\n} from \"~/shared/nano-states\";\nimport type { DroppableTarget, InstanceSelector } from \"~/shared/tree-utils\";\n\nconst getInsertionIndex = (\n  anchor: InstanceSelector,\n  instances: Instances,\n  insertBefore: boolean = false\n) => {\n  const blockSelector = findBlockSelector(anchor, instances);\n  if (blockSelector === undefined) {\n    return;\n  }\n\n  const insertAtInitialPosition = shallowEqual(blockSelector, anchor);\n\n  const blockInstance = instances.get(blockSelector[0]);\n\n  if (blockInstance === undefined) {\n    return;\n  }\n\n  const childBlockSelector = findBlockChildSelector(anchor);\n\n  if (childBlockSelector === undefined) {\n    return;\n  }\n\n  const index = blockInstance.children.findIndex((child) => {\n    if (child.type !== \"id\") {\n      return false;\n    }\n\n    if (insertAtInitialPosition) {\n      return instances.get(child.value)?.component === blockTemplateComponent;\n    }\n\n    return child.value === childBlockSelector[0];\n  });\n\n  if (index === -1) {\n    return;\n  }\n\n  // Independent of insertBefore, we always insert after the Templates instance\n  if (insertAtInitialPosition) {\n    return index + 1;\n  }\n\n  return insertBefore ? index : index + 1;\n};\n\nexport const insertListItemAt = (listItemSelector: InstanceSelector) => {\n  const project = $project.get();\n  const instances = $instances.get();\n  if (project === undefined) {\n    return;\n  }\n\n  const parentSelector = listItemSelector.slice(1);\n\n  const parentInstance = instances.get(parentSelector[0]);\n\n  if (parentInstance === undefined) {\n    return;\n  }\n\n  const position =\n    1 +\n    parentInstance.children.findIndex(\n      (child) => child.type === \"id\" && child.value === listItemSelector[0]\n    );\n\n  if (position === 0) {\n    return;\n  }\n\n  const target: DroppableTarget = {\n    parentSelector,\n    position,\n  };\n\n  const fragment = extractWebstudioFragment(\n    getWebstudioData(),\n    listItemSelector[0]\n  );\n\n  fragment.instances = structuredClone(fragment.instances);\n  fragment.instances.splice(1);\n  fragment.instances[0].children = [];\n\n  updateWebstudioData((data) => {\n    const { newInstanceIds } = insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: findAvailableVariables({\n        ...data,\n        startingInstanceId: target.parentSelector[0],\n      }),\n      projectId: project.id,\n    });\n    const newRootInstanceId = newInstanceIds.get(fragment.instances[0].id);\n    if (newRootInstanceId === undefined) {\n      return;\n    }\n    const children: Instance[\"children\"] = [\n      { type: \"id\", value: newRootInstanceId },\n    ];\n\n    insertInstanceChildrenMutable(data, children, target);\n\n    const selectedInstanceSelector = [\n      newRootInstanceId,\n      ...target.parentSelector,\n    ];\n\n    $textEditingInstanceSelector.set({\n      selector: selectedInstanceSelector,\n      reason: \"new\",\n    });\n\n    selectInstance(selectedInstanceSelector);\n  });\n};\n\nexport const insertTemplateAt = async (\n  templateSelector: InstanceSelector,\n  anchor: InstanceSelector,\n  insertBefore: boolean\n) => {\n  const project = $project.get();\n  const instances = $instances.get();\n  if (project === undefined) {\n    return;\n  }\n\n  const fragment = extractWebstudioFragment(\n    getWebstudioData(),\n    templateSelector[0]\n  );\n\n  const parentSelector = findBlockSelector(anchor, instances);\n\n  if (parentSelector === undefined) {\n    return;\n  }\n\n  const position = getInsertionIndex(anchor, instances, insertBefore);\n\n  if (position === undefined) {\n    return;\n  }\n\n  const target: DroppableTarget = {\n    parentSelector,\n    position,\n  };\n\n  try {\n    const conflicts = detectFragmentTokenConflicts({ fragment });\n    const conflictResolution =\n      conflicts.length > 0\n        ? await builderApi.showTokenConflictDialog(conflicts)\n        : \"theirs\";\n\n    updateWebstudioData((data) => {\n      const { newInstanceIds } = insertWebstudioFragmentCopy({\n        data,\n        fragment,\n        availableVariables: findAvailableVariables({\n          ...data,\n          startingInstanceId: target.parentSelector[0],\n        }),\n        projectId: project.id,\n        conflictResolution,\n      });\n      const newRootInstanceId = newInstanceIds.get(fragment.instances[0].id);\n      if (newRootInstanceId === undefined) {\n        return;\n      }\n      const children: Instance[\"children\"] = [\n        { type: \"id\", value: newRootInstanceId },\n      ];\n\n      insertInstanceChildrenMutable(data, children, target);\n\n      const selectedInstanceSelector = [\n        newRootInstanceId,\n        ...target.parentSelector,\n      ];\n\n      const selectors: InstanceSelector[] = [];\n\n      findAllEditableInstanceSelector({\n        instanceSelector: selectedInstanceSelector,\n        instances: data.instances,\n        props: data.props,\n        metas: $registeredComponentMetas.get(),\n        results: selectors,\n      });\n\n      const editableInstanceSelector = selectors[0];\n\n      if (editableInstanceSelector) {\n        $textEditingInstanceSelector.set({\n          selector: editableInstanceSelector,\n          reason: \"new\",\n        });\n      } else {\n        $textEditingInstanceSelector.set(undefined);\n      }\n\n      selectInstance([newRootInstanceId, ...target.parentSelector]);\n    });\n  } catch {\n    // User cancelled the operation\n    return;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/collaborative-instance-outline.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { $collaborativeInstanceRect } from \"~/shared/nano-states\";\nimport { Outline } from \"./outline\";\nimport { applyScale } from \"../apply-scale\";\nimport { $scale, $clampingRect } from \"~/builder/shared/nano-states\";\nimport { $ephemeralStyles } from \"~/canvas/stores\";\n\n// Outline of an instance that is being edited by AI or a human collaborator.\nexport const CollaborativeInstanceOutline = () => {\n  const scale = useStore($scale);\n  const instanceRect = useStore($collaborativeInstanceRect);\n  const ephemeralStyles = useStore($ephemeralStyles);\n  const clampingRect = useStore($clampingRect);\n\n  if (\n    instanceRect === undefined ||\n    ephemeralStyles.length !== 0 ||\n    clampingRect === undefined\n  ) {\n    return;\n  }\n\n  const rect = applyScale(instanceRect, scale);\n\n  return (\n    <Outline\n      variant=\"collaboration\"\n      rect={rect}\n      clampingRect={clampingRect}\n    ></Outline>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/hovered-instance-outline.tsx",
    "content": "import { shallowEqual } from \"shallow-equal\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  $blockChildOutline,\n  $hoveredInstanceOutlineAndInstance,\n  $hoveredInstanceSelector,\n  $instances,\n  $isContentMode,\n  $textEditingInstanceSelector,\n} from \"~/shared/nano-states\";\nimport { $clampingRect, $scale } from \"~/builder/shared/nano-states\";\nimport { findClosestSlot } from \"~/shared/instance-utils\";\nimport { isDescendantOrSelf } from \"~/shared/tree-utils\";\nimport { Outline } from \"./outline\";\nimport { Label } from \"./label\";\nimport { applyScale } from \"../apply-scale\";\n\nexport const HoveredInstanceOutline = () => {\n  const instances = useStore($instances);\n  const hoveredInstanceSelector = useStore($hoveredInstanceSelector);\n  const blockChildOutline = useStore($blockChildOutline);\n  const outline = useStore($hoveredInstanceOutlineAndInstance);\n  const scale = useStore($scale);\n  const textEditingInstanceSelector = useStore($textEditingInstanceSelector);\n  const isContentMode = useStore($isContentMode);\n  const clampingRect = useStore($clampingRect);\n\n  if (\n    outline === undefined ||\n    hoveredInstanceSelector === undefined ||\n    clampingRect === undefined\n  ) {\n    return;\n  }\n\n  if (isContentMode) {\n    if (shallowEqual(blockChildOutline?.selector, hoveredInstanceSelector)) {\n      return;\n    }\n  }\n\n  if (\n    textEditingInstanceSelector?.selector &&\n    isDescendantOrSelf(\n      hoveredInstanceSelector,\n      textEditingInstanceSelector.selector\n    )\n  ) {\n    return;\n  }\n\n  const variant = findClosestSlot(instances, hoveredInstanceSelector)\n    ? \"slot\"\n    : \"default\";\n  const rect = applyScale(outline.rect, scale);\n\n  return (\n    <Outline rect={rect} clampingRect={clampingRect} variant={variant}>\n      <Label\n        variant={variant}\n        instance={outline.instance}\n        instanceRect={rect}\n      />\n    </Outline>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/index.ts",
    "content": "export { SelectedInstanceOutline } from \"./selected-instance-outline\";\nexport { HoveredInstanceOutline } from \"./hovered-instance-outline\";\nexport { CollaborativeInstanceOutline } from \"./collaborative-instance-outline\";\nexport { applyScale } from \"../apply-scale\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/label.tsx",
    "content": "import { useCallback, useState } from \"react\";\nimport { styled, type Rect } from \"@webstudio-is/design-system\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport {\n  InstanceIcon,\n  getInstanceLabel,\n} from \"~/builder/shared/instance-label\";\n\ntype LabelPosition = \"top\" | \"inside\" | \"bottom\";\ntype LabelRefCallback = (element: HTMLElement | null) => void;\n\n/**\n * Detects if there is no space on top and for the label and tells to show it inside.\n * - if there is enough space for the label on top of the instance - top\n * - else if instance height is more than 250px - bottom\n * - else inside-top - last resort because it covers a bit of the instance content\n */\nconst useLabelPosition = (\n  instanceRect: Rect\n): [LabelRefCallback, LabelPosition] => {\n  const [position, setPosition] = useState<LabelPosition>(\"top\");\n\n  const ref = useCallback(\n    (element: null | HTMLElement) => {\n      if (element === null || instanceRect === undefined) {\n        return;\n      }\n      const labelRect = element.getBoundingClientRect();\n      let nextPosition: LabelPosition = \"top\";\n      // Label won't fit above the instance outline\n      if (labelRect.height > instanceRect.top) {\n        nextPosition = instanceRect.height < 250 ? \"bottom\" : \"inside\";\n      }\n      setPosition(nextPosition);\n    },\n    [instanceRect]\n  );\n\n  return [ref, position];\n};\n\nconst LabelContainer = styled(\n  \"div\",\n  {\n    position: \"absolute\",\n    display: \"flex\",\n    padding: `0 ${theme.spacing[3]}`,\n    height: theme.spacing[10],\n    color: \"white\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n    gap: theme.spacing[3],\n    fontSize: theme.deprecatedFontSize[3],\n    fontFamily: theme.fonts.sans,\n    lineHeight: 1,\n    minWidth: theme.spacing[13],\n    whiteSpace: \"nowrap\",\n  },\n  {\n    variants: {\n      position: {\n        top: {\n          left: -1,\n          top: `-${theme.spacing[10]}`,\n          borderTopLeftRadius: theme.borderRadius[4],\n          borderTopRightRadius: theme.borderRadius[4],\n        },\n        inside: {\n          top: 0,\n          borderBottomLeftRadius: theme.borderRadius[4],\n          borderBottomRightRadius: theme.borderRadius[4],\n        },\n        bottom: {\n          left: -1,\n          bottom: `-${theme.spacing[10]}`,\n          borderBottomLeftRadius: theme.borderRadius[4],\n          borderBottomRightRadius: theme.borderRadius[4],\n        },\n      },\n      variant: {\n        default: {\n          backgroundColor: theme.colors.backgroundPrimary,\n        },\n        slot: {\n          backgroundColor: theme.colors.foregroundReusable,\n        },\n      },\n    },\n    defaultVariants: { variant: \"default\" },\n  }\n);\n\ntype LabelProps = {\n  instance: { label?: string; component: Instance[\"component\"] };\n  instanceRect: Rect;\n  variant?: \"default\" | \"slot\";\n};\n\nexport const Label = ({ instance, instanceRect, variant }: LabelProps) => {\n  const [labelRef, position] = useLabelPosition(instanceRect);\n  return (\n    <LabelContainer position={position} variant={variant} ref={labelRef}>\n      <InstanceIcon size=\"1em\" instance={instance} />\n      {getInstanceLabel(instance)}\n    </LabelContainer>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/outline.stories.tsx",
    "content": "import { Box, Flex, Grid, StorySection } from \"@webstudio-is/design-system\";\nimport { Outline as OutlineComponent } from \"./outline\";\n\nexport default {\n  title: \"Canvas tools/Outline\",\n  component: OutlineComponent,\n};\n\nexport const Outline = () => (\n  <StorySection title=\"Outline\">\n    <Grid gap={3} columns={2} css={{ margin: 16 }}>\n      <Grid\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Selected outline\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n        />\n      </Grid>\n\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Collaboration outline\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n          variant=\"collaboration\"\n        />\n      </Flex>\n\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Collaboration outline over Selected\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n        />\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n          variant=\"collaboration\"\n        />\n      </Flex>\n\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Selected outline over Collaboration\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n          variant=\"collaboration\"\n        />\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n        />\n      </Flex>\n\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Clamped left\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(-10, 0, 150, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n        />\n      </Flex>\n\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Clamped right\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(0, 0, 160, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n        />\n      </Flex>\n\n      <Flex\n        align=\"center\"\n        justify=\"center\"\n        css={{ position: \"relative\", height: 150, width: 150 }}\n      >\n        <Box css={{ width: \"min-content\", textAlign: \"center\" }}>\n          Clamped left-right\n        </Box>\n        <OutlineComponent\n          rect={new DOMRect(-10, 0, 170, 150)}\n          clampingRect={new DOMRect(0, 0, 150, 150)}\n        />\n      </Flex>\n    </Grid>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/outline.tsx",
    "content": "import { useMemo, type ReactNode } from \"react\";\nimport { css, keyframes, type Rect } from \"@webstudio-is/design-system\";\nimport { theme } from \"@webstudio-is/design-system\";\n\nconst angleVar = `--ws-outline-angle`;\n\n// Won't work in current FF/Safari, but outline will still work, just no animation.\nconst propertyStyle = (\n  <style>{`\n    @property ${angleVar} {\n      syntax: '<angle>';\n      initial-value: 0deg;\n      inherits: false;\n    }\n  `}</style>\n);\n\nconst angleKeyframes = keyframes({\n  to: {\n    [angleVar]: \"360deg\",\n  },\n});\n\nconst baseOutlineStyle = css({\n  borderWidth: 1,\n  variants: {\n    variant: {\n      default: {\n        borderStyle: \"solid\",\n        borderColor: `oklch(from ${theme.colors.backgroundPrimary} l c h / 0.7)`,\n      },\n      collaboration: {\n        [angleVar]: `0deg`,\n        borderStyle: \"solid\",\n        borderImage: `conic-gradient(from var(${angleVar}), #39FBBB 0%, #4A4EFA 12.5%, #E63CFE 25%, #FFAE3C 37.5%, #39FBBB 50%, #4A4EFA 62.5%, #E63CFE 75%, #FFAE3C 87.5%) 1`,\n        animation: `2s ${angleKeyframes} linear infinite`,\n      },\n      slot: {\n        borderStyle: \"solid\",\n        borderColor: theme.colors.foregroundReusable,\n      },\n    },\n\n    isLeftClamped: {\n      true: {\n        borderLeftWidth: 0,\n      },\n    },\n    isRightClamped: {\n      true: {\n        borderRightWidth: 0,\n      },\n    },\n    isBottomClamped: {\n      true: {\n        borderBottomWidth: 0,\n      },\n    },\n    isTopClamped: {\n      true: {\n        borderTopWidth: 0,\n      },\n    },\n  },\n  defaultVariants: { variant: \"default\" },\n});\n\nconst baseStyle = css({\n  boxSizing: \"border-box\",\n  position: \"absolute\",\n  display: \"grid\",\n  pointerEvents: \"none\",\n  top: 0,\n  left: 0,\n});\n\nconst useDynamicStyle = (rect?: Rect) => {\n  return useMemo(() => {\n    if (rect === undefined) {\n      return;\n    }\n    return {\n      transform: `translate3d(${rect.left}px, ${rect.top}px, 0)`,\n      width: rect.width,\n      height: rect.height,\n    };\n  }, [rect]);\n};\n\ntype OutlineProps = {\n  children?: ReactNode;\n  rect: Rect;\n  clampingRect: Rect;\n  variant?: \"default\" | \"collaboration\" | \"slot\";\n};\n\nexport const Outline = ({\n  children,\n  rect,\n  clampingRect,\n  variant,\n}: OutlineProps) => {\n  const outlineRect = {\n    top: Math.max(rect.top, clampingRect.top),\n    height:\n      Math.min(rect.top + rect.height, clampingRect.top + clampingRect.height) -\n      Math.max(rect.top, clampingRect.top),\n\n    left: Math.max(rect.left, clampingRect.left),\n    width:\n      Math.min(rect.left + rect.width, clampingRect.left + clampingRect.width) -\n      Math.max(rect.left, clampingRect.left),\n  };\n  const dynamicStyle = useDynamicStyle(outlineRect);\n  if (outlineRect.width <= 0 || outlineRect.height <= 0) {\n    return;\n  }\n\n  const isLeftClamped = rect.left < outlineRect.left;\n  const isTopClamped = rect.top < outlineRect.top;\n\n  const isRightClamped =\n    Math.round(rect.left + rect.width) > Math.round(clampingRect.width);\n\n  const isBottomClamped =\n    Math.round(rect.top + rect.height) > Math.round(clampingRect.height);\n\n  return (\n    <>\n      {propertyStyle}\n      <div\n        className={`${baseStyle()} ${baseOutlineStyle({ variant, isLeftClamped, isRightClamped, isBottomClamped, isTopClamped })}`}\n        style={dynamicStyle}\n      >\n        {children}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/outline/selected-instance-outline.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  $instances,\n  $selectedInstanceOutlineAndInstance,\n  $selectedInstanceSelector,\n} from \"~/shared/nano-states\";\nimport { $textEditingInstanceSelector } from \"~/shared/nano-states\";\nimport { isDescendantOrSelf } from \"~/shared/tree-utils\";\nimport { Outline } from \"./outline\";\nimport { applyScale } from \"../apply-scale\";\nimport { $clampingRect, $scale } from \"~/builder/shared/nano-states\";\nimport { findClosestSlot } from \"~/shared/instance-utils\";\nimport { $ephemeralStyles } from \"~/canvas/stores\";\n\nexport const SelectedInstanceOutline = () => {\n  const instances = useStore($instances);\n  const selectedInstanceSelector = useStore($selectedInstanceSelector);\n  const textEditingInstanceSelector = useStore($textEditingInstanceSelector);\n  const outline = useStore($selectedInstanceOutlineAndInstance);\n  const scale = useStore($scale);\n  const ephemeralStyles = useStore($ephemeralStyles);\n  const clampingRect = useStore($clampingRect);\n\n  if (selectedInstanceSelector === undefined) {\n    return;\n  }\n\n  if (clampingRect === undefined) {\n    return;\n  }\n\n  const isEditingCurrentInstance =\n    textEditingInstanceSelector !== undefined &&\n    isDescendantOrSelf(\n      selectedInstanceSelector,\n      textEditingInstanceSelector.selector\n    );\n\n  if (\n    isEditingCurrentInstance ||\n    outline === undefined ||\n    ephemeralStyles.length !== 0\n  ) {\n    return;\n  }\n\n  const variant = findClosestSlot(instances, selectedInstanceSelector)\n    ? \"slot\"\n    : \"default\";\n  const rect = applyScale(outline.rect, scale);\n\n  return <Outline rect={rect} clampingRect={clampingRect} variant={variant} />;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/resize-handles.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { findApplicableMedia } from \"@webstudio-is/css-engine\";\nimport {\n  css,\n  disableCanvasPointerEvents,\n  numericScrubControl,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { useEffect, useRef } from \"react\";\nimport { $canvasWidth } from \"~/builder/shared/nano-states\";\nimport { minCanvasWidth } from \"~/shared/breakpoints\";\nimport {\n  $breakpoints,\n  $isResizingCanvas,\n  $selectedBreakpointId,\n} from \"~/shared/nano-states\";\n\nconst handlesContainerStyle = css({\n  position: \"absolute\",\n  top: 0,\n  width: 5,\n  bottom: 0,\n  cursor: \"col-resize\",\n  pointerEvents: \"auto\",\n  color: \"transparent\",\n  \"&::before\": {\n    position: \"absolute\",\n    content: '\"\"',\n    inset: 0,\n    background: \"currentColor\",\n  },\n  \"& svg\": {\n    position: \"absolute\",\n    top: \"50%\",\n    right: 0,\n    transform: \"translateX(100%)\",\n    color: theme.colors.foregroundSubtle,\n  },\n  \"&[data-align=left]\": {\n    left: 0,\n  },\n  \"&[data-align=right]\": {\n    right: 0,\n  },\n  \"&[data-state=resizing]::before\": {\n    display: \"none\",\n  },\n  \"&:hover, &:has(+ &:hover), &:hover+&\": {\n    \"&::before, & svg\": {\n      color: theme.colors.backgroundPrimaryLight,\n    },\n  },\n  // A little specificity hack to override the previou selector\n  \"&&[data-state=resizing] svg\": {\n    color: theme.colors.foregroundSubtle,\n  },\n});\n\nconst handleIcon = (\n  <svg\n    width=\"14\"\n    height=\"40\"\n    viewBox=\"0 0 14 40\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      d=\"M0 0H10C12.2091 0 14 1.79086 14 4V36C14 38.2091 12.2091 40 10 40H0V0Z\"\n      fill=\"currentColor\"\n    />\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M4.5 8C4.77614 8 5 8.22386 5 8.5L5 31.5C5 31.7761 4.77614 32 4.5 32C4.22386 32 4 31.7761 4 31.5L4 8.5C4 8.22386 4.22386 8 4.5 8Z\"\n      fill=\"#C1C8CD\"\n    />\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M8.5 8C8.77614 8 9 8.22386 9 8.5L9 31.5C9 31.7761 8.77614 32 8.5 32C8.22386 32 8 31.7761 8 31.5L8 8.5C8 8.22386 8.22386 8 8.5 8Z\"\n      fill=\"#C1C8CD\"\n    />\n  </svg>\n);\n\nconst updateBreakpoint = (width: number) => {\n  const applicableBreakpoint = findApplicableMedia(\n    Array.from($breakpoints.get().values()),\n    width\n  );\n  if (applicableBreakpoint) {\n    $selectedBreakpointId.set(applicableBreakpoint.id);\n  }\n};\n\nconst useScrub = ({ side }: { side: \"right\" | \"left\" }) => {\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (ref.current === null) {\n      return;\n    }\n\n    let enableCanvasPointerEvents: (() => void) | undefined;\n\n    const disposeScrubControl = numericScrubControl(ref.current, {\n      getInitialValue() {\n        return $canvasWidth.get() ?? 0;\n      },\n      getValue(state, movement) {\n        const value =\n          side === \"left\"\n            ? //  * 2 is a compensation for the fact that canvas is centered, so when we scrub, width has to change twice faster,\n              // otherwise cursor will be faster than the edge movement\n              state.value - movement * 2\n            : state.value + movement * 2;\n        return Math.max(value, minCanvasWidth);\n      },\n      onStatusChange(status) {\n        if (status === \"scrubbing\") {\n          enableCanvasPointerEvents?.();\n          enableCanvasPointerEvents = disableCanvasPointerEvents();\n          $isResizingCanvas.set(true);\n          return;\n        }\n        enableCanvasPointerEvents?.();\n\n        $isResizingCanvas.set(false);\n      },\n      onValueInput(event) {\n        $canvasWidth.set(event.value);\n        updateBreakpoint(event.value);\n      },\n    });\n\n    return () => {\n      enableCanvasPointerEvents?.();\n      disposeScrubControl();\n    };\n  }, [side]);\n\n  return ref;\n};\n\nexport const ResizeHandles = () => {\n  const isResizing = useStore($isResizingCanvas);\n  const leftRef = useScrub({ side: \"left\" });\n  const rightRef = useScrub({ side: \"right\" });\n  const state = isResizing ? \"resizing\" : \"idle\";\n\n  return (\n    <>\n      <div\n        ref={leftRef}\n        data-state={state}\n        data-align=\"left\"\n        className={handlesContainerStyle()}\n      />\n      <div\n        ref={rightRef}\n        data-state={state}\n        data-align=\"right\"\n        className={handlesContainerStyle()}\n      >\n        {handleIcon}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/text-toolbar.tsx",
    "content": "import { useRef, useEffect } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { computePosition, flip, offset, shift } from \"@floating-ui/dom\";\nimport { theme, Flex, IconButton, Tooltip } from \"@webstudio-is/design-system\";\nimport {\n  SuperscriptIcon,\n  SubscriptIcon,\n  XSmallIcon,\n  BoldIcon,\n  TextItalicIcon,\n  LinkIcon,\n  PaintBrushIcon,\n} from \"@webstudio-is/icons\";\nimport { $selectedInstanceSelector } from \"~/shared/nano-states\";\nimport { type TextToolbarState, $textToolbar } from \"~/shared/nano-states\";\nimport { $scale } from \"~/builder/shared/nano-states\";\nimport { emitCommand } from \"~/builder/shared/commands\";\nimport { $instanceTags } from \"../../style-panel/shared/model\";\n\nconst getRectForRelativeRect = (\n  parent: DOMRect,\n  rel: DOMRect,\n  scale: number\n) => {\n  const scaleRatio = scale / 100;\n  return {\n    x: parent.x + rel.x * scaleRatio,\n    y: parent.y + rel.y * scaleRatio,\n    width: rel.width * scaleRatio,\n    height: rel.height * scaleRatio,\n    top: parent.top + rel.top * scaleRatio,\n    left: parent.left + rel.left * scaleRatio,\n    bottom: parent.top + rel.bottom * scaleRatio,\n    right: parent.left + rel.right * scaleRatio,\n  };\n};\n\nconst $isWithinLink = computed(\n  [$selectedInstanceSelector, $instanceTags],\n  (selectedInstanceSelector, instanceTags) => {\n    if (selectedInstanceSelector === undefined) {\n      return false;\n    }\n    for (const instanceId of selectedInstanceSelector) {\n      const tag = instanceTags.get(instanceId);\n      if (tag === \"a\") {\n        return true;\n      }\n    }\n    return false;\n  }\n);\n\ntype ToolbarProps = {\n  state: TextToolbarState;\n  scale: number;\n};\n\nconst Toolbar = ({ state, scale }: ToolbarProps) => {\n  const isWithinLink = useStore($isWithinLink);\n\n  const rootRef = useRef<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    if (state.selectionRect === undefined) {\n      return;\n    }\n    if (rootRef.current?.parentElement) {\n      const floating = rootRef.current;\n      const parent = rootRef.current.parentElement;\n      const newRect = getRectForRelativeRect(\n        parent.getBoundingClientRect(),\n        state.selectionRect,\n        scale\n      );\n      const reference = {\n        getBoundingClientRect: () => newRect,\n      };\n      computePosition(reference, floating, {\n        placement: \"top\",\n        // offset should be first for shift and flip\n        // to consider it while detecting overflow\n        middleware: [offset(12), shift({ padding: 4 }), flip()],\n      }).then(({ x, y }) => {\n        floating.style.transform = `translate(${x}px, ${y}px)`;\n      });\n    }\n  }, [state.selectionRect, scale]);\n\n  const isCleared =\n    state.isBold === false &&\n    state.isItalic === false &&\n    state.isSuperscript === false &&\n    state.isSubscript === false &&\n    state.isLink === false &&\n    state.isSpan === false;\n\n  return (\n    <Flex\n      ref={rootRef}\n      gap={2}\n      css={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        pointerEvents: \"auto\",\n        background: theme.colors.backgroundPanel,\n        padding: theme.spacing[3],\n        borderRadius: theme.borderRadius[6],\n        border: `1px solid ${theme.colors.borderMain}`,\n        filter:\n          \"drop-shadow(0px 2px 7px rgba(0, 0, 0, 0.1)) drop-shadow(0px 5px 17px rgba(0, 0, 0, 0.15))\",\n      }}\n      onClick={(event) => {\n        event.stopPropagation();\n      }}\n      // We use onPointerDown here to prevent the canvas from being inert (see builder.tsx for more details)\n      onPointerDown={(event) => {\n        // We don't want the logic in the builder to make canvas inert to be triggered\n        event.preventDefault();\n      }}\n    >\n      <Tooltip content=\"Clear styles\">\n        <IconButton\n          aria-label=\"Clear styles\"\n          disabled={isCleared}\n          onClick={() => emitCommand(\"formatClear\")}\n        >\n          <XSmallIcon />\n        </IconButton>\n      </Tooltip>\n\n      <Tooltip content=\"Bold\">\n        <IconButton\n          aria-label=\"Bold\"\n          variant={state.isBold ? \"local\" : \"default\"}\n          onClick={() => emitCommand(\"formatBold\")}\n        >\n          <BoldIcon />\n        </IconButton>\n      </Tooltip>\n\n      <Tooltip content=\"Italic\">\n        <IconButton\n          aria-label=\"Italic\"\n          variant={state.isItalic ? \"local\" : \"default\"}\n          onClick={() => emitCommand(\"formatItalic\")}\n        >\n          <TextItalicIcon />\n        </IconButton>\n      </Tooltip>\n\n      <Tooltip content=\"Superscript\">\n        <IconButton\n          aria-label=\"Superscript\"\n          variant={state.isSuperscript ? \"local\" : \"default\"}\n          onClick={() => emitCommand(\"formatSuperscript\")}\n        >\n          <SuperscriptIcon />\n        </IconButton>\n      </Tooltip>\n\n      <Tooltip content=\"Subscript\">\n        <IconButton\n          aria-label=\"Subscript\"\n          variant={state.isSubscript ? \"local\" : \"default\"}\n          onClick={() => emitCommand(\"formatSubscript\")}\n        >\n          <SubscriptIcon />\n        </IconButton>\n      </Tooltip>\n\n      {isWithinLink === false && (\n        <Tooltip content=\"Inline link\">\n          <IconButton\n            aria-label=\"Inline link\"\n            variant={state.isLink ? \"local\" : \"default\"}\n            onClick={() => emitCommand(\"formatLink\")}\n          >\n            <LinkIcon />\n          </IconButton>\n        </Tooltip>\n      )}\n\n      <Tooltip content=\"Wrap with span\">\n        <IconButton\n          aria-label=\"Wrap with span\"\n          variant={state.isSpan ? \"local\" : \"default\"}\n          onClick={() => emitCommand(\"formatSpan\")}\n        >\n          <PaintBrushIcon />\n        </IconButton>\n      </Tooltip>\n    </Flex>\n  );\n};\n\nexport const TextToolbar = () => {\n  const textToolbar = useStore($textToolbar);\n  const scale = useStore($scale);\n  const selectedInstanceSelector = useStore($selectedInstanceSelector);\n\n  if (\n    textToolbar?.selectionRect === undefined ||\n    selectedInstanceSelector === undefined\n  ) {\n    return null;\n  }\n\n  return <Toolbar state={textToolbar} scale={scale} />;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/canvas-tools/use-subscribe-drag-drop-state.ts",
    "content": "import { useSubscribe } from \"~/shared/pubsub\";\nimport { $dragAndDropState } from \"~/shared/nano-states\";\n\nexport const useSubscribeDragAndDropState = () => {\n  useSubscribe(\"dragStart\", (dragPayload) => {\n    // It's possible that dropTargetChange comes before dragStart.\n    // So it's important to spread the current ...state here.\n    $dragAndDropState.set({\n      ...$dragAndDropState.get(),\n      isDragging: true,\n      dragPayload,\n    });\n  });\n\n  useSubscribe(\"dropTargetChange\", (dropTarget) => {\n    $dragAndDropState.set({ ...$dragAndDropState.get(), dropTarget });\n  });\n\n  useSubscribe(\"dragEnd\", () => {\n    $dragAndDropState.set({ isDragging: false });\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/index.ts",
    "content": "export * from \"./workspace\";\nexport * from \"./canvas-iframe\";\n"
  },
  {
    "path": "apps/builder/app/builder/features/workspace/workspace.tsx",
    "content": "import { useEffect, useRef, type ReactNode } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { theme, css } from \"@webstudio-is/design-system\";\nimport {\n  $canvasWidth,\n  $scale,\n  $workspaceRect,\n} from \"~/builder/shared/nano-states\";\nimport { $textEditingInstanceSelector } from \"~/shared/nano-states\";\nimport { CanvasTools } from \"./canvas-tools\";\nimport { selectInstance } from \"~/shared/awareness\";\nimport { ResizeHandles } from \"./canvas-tools/resize-handles\";\nimport { MediaBadge } from \"./canvas-tools/media-badge\";\nimport { useSetCanvasWidth } from \"~/builder/shared/calc-canvas-width\";\n\nconst workspaceStyle = css({\n  flexGrow: 1,\n  background: theme.colors.backgroundCanvas,\n  position: \"relative\",\n  // Prevent scrollIntoView from scrolling the whole page\n  // Commented to see what it will break\n  // overflow: \"clip\",\n});\n\nconst canvasContainerStyle = css({\n  position: \"absolute\",\n  transformOrigin: \"0 0\",\n  // We had a case where some Windows 10 + Chrome 129 users couldn't scroll iframe canvas.\n  willChange: \"transform\",\n});\n\nconst useMeasureWorkspace = () => {\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const element = ref.current;\n    if (element === null) {\n      return;\n    }\n    const observer = new ResizeObserver((entries) => {\n      $workspaceRect.set(entries[0].contentRect);\n    });\n    observer.observe(element);\n    return () => {\n      observer.disconnect();\n    };\n  }, []);\n\n  return ref;\n};\n\nconst getCanvasStyle = (\n  scale: number,\n  workspaceRect?: DOMRect,\n  canvasWidth?: number\n) => {\n  let canvasHeight;\n\n  // For some reason scale is 0 in chrome dev tools mobile touch simulated vervsion.\n  if (workspaceRect?.height && scale !== 0) {\n    canvasHeight = workspaceRect.height / (scale / 100);\n  }\n\n  return {\n    width: canvasWidth ?? \"100%\",\n    height: canvasHeight ?? \"100%\",\n    left: \"50%\",\n    transform: `scale(${scale}%) translateX(-50%)`,\n  };\n};\n\nconst useCanvasStyle = () => {\n  const scale = useStore($scale);\n  const workspaceRect = useStore($workspaceRect);\n  const canvasWidth = useStore($canvasWidth);\n\n  return getCanvasStyle(scale, workspaceRect, canvasWidth);\n};\n\nconst useOutlineStyle = () => {\n  const scale = useStore($scale);\n  const workspaceRect = useStore($workspaceRect);\n  const canvasWidth = useStore($canvasWidth);\n  const style = getCanvasStyle(100, workspaceRect, canvasWidth);\n\n  return {\n    ...style,\n    width:\n      canvasWidth === undefined ? \"100%\" : (canvasWidth ?? 0) * (scale / 100),\n  } as const;\n};\n\ntype WorkspaceProps = {\n  children: ReactNode;\n};\n\nexport const Workspace = ({ children }: WorkspaceProps) => {\n  const canvasStyle = useCanvasStyle();\n  const workspaceRef = useMeasureWorkspace();\n  useSetCanvasWidth();\n  const handleWorkspaceClick = () => {\n    selectInstance(undefined);\n    $textEditingInstanceSelector.set(undefined);\n  };\n  const outlineStyle = useOutlineStyle();\n\n  return (\n    <>\n      <div\n        className={workspaceStyle()}\n        onClick={handleWorkspaceClick}\n        ref={workspaceRef}\n      >\n        <div className={canvasContainerStyle()} style={canvasStyle}>\n          {children}\n        </div>\n        <div\n          data-name=\"canvas-tools-wrapper\"\n          className={canvasContainerStyle({ css: { pointerEvents: \"none\" } })}\n          style={outlineStyle}\n        >\n          <MediaBadge />\n          <ResizeHandles />\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport const CanvasToolsContainer = () => {\n  const outlineStyle = useOutlineStyle();\n\n  return (\n    <div\n      data-name=\"canvas-tools-wrapper\"\n      className={canvasContainerStyle()}\n      style={outlineStyle}\n    >\n      <CanvasTools />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/index.client.ts",
    "content": "export * from \"./builder\";\n"
  },
  {
    "path": "apps/builder/app/builder/inspector.tsx",
    "content": "import { useRef } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport {\n  theme,\n  PanelTabs,\n  PanelTabsList,\n  PanelTabsTrigger,\n  PanelTabsContent,\n  Card,\n  Text,\n  EnhancedTooltipProvider,\n  Flex,\n  ScrollArea,\n  Separator,\n  Tooltip,\n  Kbd,\n} from \"@webstudio-is/design-system\";\nimport { ModeMenu, StylePanel } from \"~/builder/features/style-panel\";\nimport { SettingsPanel } from \"~/builder/features/settings-panel\";\nimport {\n  $registeredComponentMetas,\n  $dragAndDropState,\n  $isDesignMode,\n} from \"~/shared/nano-states\";\nimport { NavigatorTree } from \"~/builder/features/navigator\";\nimport type { Settings } from \"~/builder/shared/client-settings\";\nimport { $activeInspectorPanel } from \"~/builder/shared/nano-states\";\nimport {\n  $selectedInstance,\n  $selectedInstanceKey,\n  $selectedPage,\n} from \"~/shared/awareness\";\nimport { InstanceIcon, getInstanceLabel } from \"./shared/instance-label\";\n\nconst InstanceInfo = ({ instance }: { instance: Instance }) => {\n  return (\n    <Flex gap=\"1\" align=\"center\">\n      <Flex shrink={false}>\n        <InstanceIcon instance={instance} />\n      </Flex>\n      <Text truncate variant=\"labels\">\n        {getInstanceLabel(instance)}\n      </Text>\n    </Flex>\n  );\n};\n\ntype InspectorProps = {\n  navigatorLayout: Settings[\"navigatorLayout\"];\n};\n\nconst contentStyle = {\n  display: \"flex\",\n  flexDirection: \"column\",\n  overflow: \"auto\",\n};\n\nconst $isDragging = computed([$dragAndDropState], (state) => state.isDragging);\n\nexport const Inspector = ({ navigatorLayout }: InspectorProps) => {\n  const selectedInstance = useStore($selectedInstance);\n  const selectedInstanceKey = useStore($selectedInstanceKey);\n  const tabsRef = useRef<HTMLDivElement>(null);\n  const isDragging = useStore($isDragging);\n  const metas = useStore($registeredComponentMetas);\n  const selectedPage = useStore($selectedPage);\n  const activeInspectorPanel = useStore($activeInspectorPanel);\n  const isDesignMode = useStore($isDesignMode);\n\n  if (navigatorLayout === \"docked\" && isDragging) {\n    return <NavigatorTree />;\n  }\n\n  if (selectedInstance === undefined || selectedInstanceKey === undefined) {\n    return (\n      <Flex css={{ p: theme.spacing[9] }}>\n        {/* @todo: use this space for something more usefull: a-la figma's no instance selected sate, maybe create an issue with a more specific proposal? */}\n        <Card css={{ p: theme.spacing[9], width: \"100%\" }}>\n          <Text>Select an instance on the canvas</Text>\n        </Card>\n      </Flex>\n    );\n  }\n\n  const meta = metas.get(selectedInstance.component);\n  const documentType = selectedPage?.meta.documentType ?? \"html\";\n\n  type PanelName = \"style\" | \"settings\";\n\n  const availablePanels = new Set<PanelName>();\n  availablePanels.add(\"settings\");\n  if (\n    // forbid styling body in xml document\n    documentType === \"html\" &&\n    // forbid styling components without preset\n    meta?.presetStyle !== undefined &&\n    isDesignMode\n  ) {\n    availablePanels.add(\"style\");\n  }\n\n  return (\n    <EnhancedTooltipProvider\n      delayDuration={1200}\n      disableHoverableContent={false}\n      skipDelayDuration={0}\n    >\n      <PanelTabs\n        ref={tabsRef}\n        data-floating-panel-container\n        value={\n          availablePanels.has(activeInspectorPanel)\n            ? activeInspectorPanel\n            : Array.from(availablePanels)[0]\n        }\n        onValueChange={(panel) => {\n          $activeInspectorPanel.set(panel as PanelName);\n        }}\n        asChild\n      >\n        <Flex direction=\"column\">\n          <PanelTabsList>\n            {availablePanels.has(\"style\") && (\n              <Tooltip\n                variant=\"wrapped\"\n                content={\n                  <Text>\n                    CSS for the selected instance&nbsp;&nbsp;\n                    <Kbd value={[\"S\"]} color=\"moreSubtle\" />\n                  </Text>\n                }\n              >\n                <div>\n                  <PanelTabsTrigger value=\"style\">Style</PanelTabsTrigger>\n                </div>\n              </Tooltip>\n            )}\n            {availablePanels.has(\"settings\") && (\n              <Tooltip\n                variant=\"wrapped\"\n                content={\n                  <Text>\n                    Settings, properties and attributes of the selected\n                    instance&nbsp;&nbsp;\n                    <Kbd value={[\"D\"]} color=\"moreSubtle\" />\n                  </Text>\n                }\n              >\n                <div>\n                  <PanelTabsTrigger value=\"settings\">Settings</PanelTabsTrigger>\n                </div>\n              </Tooltip>\n            )}\n          </PanelTabsList>\n          <Separator />\n          <PanelTabsContent value=\"style\" css={contentStyle} tabIndex={-1}>\n            <Flex\n              justify=\"between\"\n              align=\"center\"\n              shrink={false}\n              css={{\n                paddingInline: theme.panel.paddingInline,\n                height: theme.spacing[13],\n              }}\n            >\n              <InstanceInfo instance={selectedInstance} />\n              <ModeMenu />\n            </Flex>\n            <StylePanel />\n          </PanelTabsContent>\n          <PanelTabsContent value=\"settings\" css={contentStyle} tabIndex={-1}>\n            <ScrollArea>\n              <Flex\n                justify=\"between\"\n                align=\"center\"\n                shrink={false}\n                css={{\n                  paddingInline: theme.panel.paddingInline,\n                  height: theme.spacing[13],\n                }}\n              >\n                <InstanceInfo instance={selectedInstance} />\n              </Flex>\n              <SettingsPanel\n                // Re-render when instance changes\n                key={selectedInstance.id}\n                selectedInstance={selectedInstance}\n                selectedInstanceKey={selectedInstanceKey}\n              />\n            </ScrollArea>\n          </PanelTabsContent>\n        </Flex>\n      </PanelTabs>\n    </EnhancedTooltipProvider>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-filters.tsx",
    "content": "import { useMemo } from \"react\";\nimport { Select } from \"@webstudio-is/design-system\";\nimport type { AllowedFileExtension } from \"@webstudio-is/sdk\";\n\nconst CATEGORY_ALL = \"All\" as const;\n\n/**\n * Display categories for file types in the UI\n * These match the comment sections in ALLOWED_FILE_TYPES\n */\nconst DISPLAY_CATEGORIES = [\n  CATEGORY_ALL,\n  \"Images\",\n  \"Documents\",\n  \"Video\",\n  \"Audio\",\n  \"Code\",\n  \"Archives\",\n  \"Fonts\",\n] as const;\n\ntype DisplayCategory = (typeof DISPLAY_CATEGORIES)[number];\n\n/**\n * Type-safe mapping of file extensions to display categories.\n *\n * TypeScript enforces that:\n * 1. All keys must be valid extensions from ALLOWED_FILE_TYPES\n * 2. All extensions from ALLOWED_FILE_TYPES must be included (completeness check)\n * 3. All values must be valid DisplayCategory types\n *\n * When adding a new file extension to ALLOWED_FILE_TYPES, TypeScript will\n * produce a compile error until that extension is also added to this mapping.\n */\nconst EXTENSION_TO_DISPLAY_CATEGORY: Record<\n  AllowedFileExtension,\n  DisplayCategory\n> = {\n  // Documents\n  pdf: \"Documents\",\n  doc: \"Documents\",\n  docx: \"Documents\",\n  xls: \"Documents\",\n  xlsx: \"Documents\",\n  csv: \"Documents\",\n  ppt: \"Documents\",\n  pptx: \"Documents\",\n\n  // Code\n  txt: \"Code\",\n  md: \"Code\",\n  js: \"Code\",\n  css: \"Code\",\n  json: \"Code\",\n  html: \"Code\",\n  xml: \"Code\",\n\n  // Archives\n  zip: \"Archives\",\n  rar: \"Archives\",\n\n  // Audio\n  mp3: \"Audio\",\n  wav: \"Audio\",\n  ogg: \"Audio\",\n  m4a: \"Audio\",\n\n  // Video\n  mp4: \"Video\",\n  mov: \"Video\",\n  avi: \"Video\",\n  webm: \"Video\",\n\n  // Images\n  jpg: \"Images\",\n  jpeg: \"Images\",\n  png: \"Images\",\n  gif: \"Images\",\n  svg: \"Images\",\n  webp: \"Images\",\n  avif: \"Images\",\n  ico: \"Images\",\n  bmp: \"Images\",\n  tif: \"Images\",\n  tiff: \"Images\",\n\n  // Fonts\n  woff: \"Fonts\",\n  woff2: \"Fonts\",\n  ttf: \"Fonts\",\n  otf: \"Fonts\",\n};\n\n/**\n * Get array of extensions for a given display category\n */\nconst getExtensionsForCategory = (\n  category: DisplayCategory\n): AllowedFileExtension[] | \"*\" => {\n  if (category === CATEGORY_ALL) {\n    return \"*\";\n  }\n  return Object.entries(EXTENSION_TO_DISPLAY_CATEGORY)\n    .filter(([, cat]) => cat === category)\n    .map(([ext]) => ext as AllowedFileExtension);\n};\n\ntype AssetFiltersProps = {\n  formatCounts: Partial<Record<AllowedFileExtension, number>>;\n  value: AllowedFileExtension[] | \"*\";\n  onChange: (extensions: AllowedFileExtension[] | \"*\") => void;\n};\n\nexport const AssetFilters = ({\n  formatCounts,\n  value,\n  onChange,\n}: AssetFiltersProps) => {\n  // Aggregate extension counts by display category\n  const categoryCounts = useMemo(() => {\n    const counts: Record<DisplayCategory, number> = {\n      [CATEGORY_ALL]: 0,\n      Images: 0,\n      Documents: 0,\n      Video: 0,\n      Audio: 0,\n      Code: 0,\n      Archives: 0,\n      Fonts: 0,\n    };\n\n    Object.entries(formatCounts).forEach(([ext, count]) => {\n      const category =\n        EXTENSION_TO_DISPLAY_CATEGORY[ext as AllowedFileExtension];\n      if (category && count !== undefined) {\n        counts[category] += count;\n        counts[CATEGORY_ALL] += count;\n      }\n    });\n\n    return counts;\n  }, [formatCounts]);\n\n  // Compute display category from extensions array\n  const selectedCategory: DisplayCategory =\n    value === \"*\"\n      ? CATEGORY_ALL\n      : (EXTENSION_TO_DISPLAY_CATEGORY[value[0]] as\n          | DisplayCategory\n          | undefined) || CATEGORY_ALL;\n\n  const options = DISPLAY_CATEGORIES.map((category) => ({\n    label: `${category} (${categoryCounts[category] || 0})`,\n    value: category,\n  }));\n\n  const selectedOption = options.find((opt) => opt.value === selectedCategory);\n\n  return (\n    <Select\n      options={options}\n      value={selectedOption}\n      onChange={(option: { label: string; value: DisplayCategory }) => {\n        onChange(getExtensionsForCategory(option.value));\n      }}\n      getLabel={(option) => option.label}\n      getValue={(option) => option.value}\n      css={{ flexGrow: 1 }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-info.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { Pages, Props, Styles, Asset } from \"@webstudio-is/sdk\";\nimport type {\n  ImageValue,\n  FontFamilyValue,\n  StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { __testing__ } from \"./asset-info\";\n\nconst { traverseStyleValue, calculateUsagesByAssetId } = __testing__;\n\ndescribe(\"asset-info\", () => {\n  describe(\"traverseStyleValue\", () => {\n    test(\"calls callback for image values\", () => {\n      const imageValue: ImageValue = {\n        type: \"image\",\n        value: { type: \"asset\", value: \"asset-1\" },\n      };\n      const results: (ImageValue | FontFamilyValue)[] = [];\n      traverseStyleValue(imageValue, (value) => results.push(value));\n      expect(results).toEqual([imageValue]);\n    });\n\n    test(\"traverses tuple values\", () => {\n      const imageValue1: ImageValue = {\n        type: \"image\",\n        value: { type: \"asset\", value: \"asset-1\" },\n      };\n      const imageValue2: ImageValue = {\n        type: \"image\",\n        value: { type: \"asset\", value: \"asset-2\" },\n      };\n      const tupleValue: StyleValue = {\n        type: \"tuple\",\n        value: [imageValue1, imageValue2],\n      };\n      const results: (ImageValue | FontFamilyValue)[] = [];\n      traverseStyleValue(tupleValue, (value) => results.push(value));\n      expect(results).toEqual([imageValue1, imageValue2]);\n    });\n\n    test(\"traverses layers values\", () => {\n      const imageValue1: ImageValue = {\n        type: \"image\",\n        value: { type: \"asset\", value: \"asset-1\" },\n      };\n      const imageValue2: ImageValue = {\n        type: \"image\",\n        value: { type: \"asset\", value: \"asset-2\" },\n      };\n      const layersValue: StyleValue = {\n        type: \"layers\",\n        value: [imageValue1, imageValue2],\n      };\n      const results: (ImageValue | FontFamilyValue)[] = [];\n      traverseStyleValue(layersValue, (value) => results.push(value));\n      expect(results).toEqual([imageValue1, imageValue2]);\n    });\n\n    test(\"does not call callback for other value types\", () => {\n      const keywordValue: StyleValue = {\n        type: \"keyword\",\n        value: \"auto\",\n      };\n      const results: (ImageValue | FontFamilyValue)[] = [];\n      traverseStyleValue(keywordValue, (value) => results.push(value));\n      expect(results).toEqual([]);\n    });\n\n    test(\"traverses nested tuple and layers\", () => {\n      const imageValue: ImageValue = {\n        type: \"image\",\n        value: { type: \"asset\", value: \"asset-1\" },\n      };\n      const nestedTuple: StyleValue = {\n        type: \"tuple\",\n        value: [imageValue],\n      };\n      const layersValue: StyleValue = {\n        type: \"layers\",\n        value: [nestedTuple],\n      };\n      const results: (ImageValue | FontFamilyValue)[] = [];\n      traverseStyleValue(layersValue, (value) => results.push(value));\n      expect(results).toEqual([imageValue]);\n    });\n\n    test(\"calls callback for fontFamily values\", () => {\n      const fontFamilyValue: FontFamilyValue = {\n        type: \"fontFamily\",\n        value: [\"CustomFont\", \"Arial\", \"sans-serif\"],\n      };\n      const results: (ImageValue | FontFamilyValue)[] = [];\n      traverseStyleValue(fontFamilyValue, (value) => results.push(value));\n      expect(results).toEqual([fontFamilyValue]);\n    });\n  });\n\n  describe(\"calculateUsagesByAssetId\", () => {\n    test(\"tracks favicon asset usage\", () => {\n      const pages: Pages = {\n        meta: { faviconAssetId: \"favicon-asset\" },\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"favicon-asset\")).toEqual([{ type: \"favicon\" }]);\n    });\n\n    test(\"tracks social image asset usage\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: { socialImageAssetId: \"social-asset\" },\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"social-asset\")).toEqual([\n        { type: \"socialImage\", pageId: \"home\" },\n      ]);\n    });\n\n    test(\"tracks marketplace thumbnail asset usage\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [\n          {\n            id: \"page-1\",\n            name: \"Page 1\",\n            path: \"/page-1\",\n            title: \"Page 1\",\n            meta: {},\n            rootInstanceId: \"root-1\",\n            marketplace: { thumbnailAssetId: \"thumbnail-asset\" },\n          },\n        ],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"thumbnail-asset\")).toEqual([\n        { type: \"marketplaceThumbnail\", pageId: \"page-1\" },\n      ]);\n    });\n\n    test(\"tracks prop asset usage\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map([\n        [\n          \"prop-1\",\n          {\n            id: \"prop-1\",\n            instanceId: \"instance-1\",\n            type: \"asset\",\n            name: \"src\",\n            value: \"asset-1\",\n          },\n        ],\n      ]);\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"asset-1\")).toEqual([\n        { type: \"prop\", propId: \"prop-1\" },\n      ]);\n    });\n\n    test(\"ignores width and height props\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map([\n        [\n          \"prop-width\",\n          {\n            id: \"prop-width\",\n            instanceId: \"instance-1\",\n            type: \"asset\",\n            name: \"width\",\n            value: \"asset-1\",\n          },\n        ],\n        [\n          \"prop-height\",\n          {\n            id: \"prop-height\",\n            instanceId: \"instance-1\",\n            type: \"asset\",\n            name: \"height\",\n            value: \"asset-1\",\n          },\n        ],\n      ]);\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"asset-1\")).toBeUndefined();\n    });\n\n    test(\"tracks image asset usage in styles\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:property\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"backgroundImage\",\n            value: {\n              type: \"image\",\n              value: { type: \"asset\", value: \"asset-1\" },\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"asset-1\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:property\" },\n      ]);\n    });\n\n    test(\"returns empty map when no usages\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.size).toBe(0);\n    });\n\n    test(\"aggregates multiple usages for same asset\", () => {\n      const pages: Pages = {\n        meta: { faviconAssetId: \"asset-1\" },\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: { socialImageAssetId: \"asset-1\" },\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map([\n        [\n          \"prop-1\",\n          {\n            id: \"prop-1\",\n            instanceId: \"instance-1\",\n            type: \"asset\",\n            name: \"src\",\n            value: \"asset-1\",\n          },\n        ],\n      ]);\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:property\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"backgroundImage\",\n            value: {\n              type: \"image\",\n              value: { type: \"asset\", value: \"asset-1\" },\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"asset-1\")).toHaveLength(4);\n      expect(usages.get(\"asset-1\")).toEqual([\n        { type: \"favicon\" },\n        { type: \"socialImage\", pageId: \"home\" },\n        { type: \"prop\", propId: \"prop-1\" },\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:property\" },\n      ]);\n    });\n\n    test(\"handles undefined pages\", () => {\n      const pages = undefined;\n      const props: Props = new Map();\n      const styles: Styles = new Map();\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.size).toBe(0);\n    });\n\n    test(\"tracks multiple assets in tuple style value\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:property\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"backgroundImage\",\n            value: {\n              type: \"tuple\",\n              value: [\n                {\n                  type: \"image\",\n                  value: { type: \"asset\", value: \"asset-1\" },\n                },\n                {\n                  type: \"image\",\n                  value: { type: \"asset\", value: \"asset-2\" },\n                },\n              ],\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"asset-1\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:property\" },\n      ]);\n      expect(usages.get(\"asset-2\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:property\" },\n      ]);\n    });\n\n    test(\"tracks font asset usage in fontFamily styles\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:fontFamily\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"fontFamily\",\n            value: {\n              type: \"fontFamily\",\n              value: [\"CustomFont\", \"Arial\", \"sans-serif\"],\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>([\n        [\n          \"font-asset-1\",\n          {\n            id: \"font-asset-1\",\n            type: \"font\",\n            name: \"CustomFont\",\n            format: \"woff2\",\n            size: 5000,\n            meta: { family: \"CustomFont\", style: \"normal\", weight: 400 },\n            createdAt: \"2024-01-01\",\n            projectId: \"project-id\",\n          },\n        ],\n      ]);\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"font-asset-1\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:fontFamily\" },\n      ]);\n    });\n\n    test(\"tracks multiple font assets in same fontFamily style\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:fontFamily\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"fontFamily\",\n            value: {\n              type: \"fontFamily\",\n              value: [\"CustomFont\", \"AnotherFont\"],\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>([\n        [\n          \"font-asset-1\",\n          {\n            id: \"font-asset-1\",\n            type: \"font\",\n            name: \"CustomFont\",\n            format: \"woff2\",\n            size: 5000,\n            meta: { family: \"CustomFont\", style: \"normal\", weight: 400 },\n            createdAt: \"2024-01-01\",\n            projectId: \"project-id\",\n          },\n        ],\n        [\n          \"font-asset-2\",\n          {\n            id: \"font-asset-2\",\n            type: \"font\",\n            name: \"AnotherFont\",\n            format: \"woff2\",\n            size: 6000,\n            meta: { family: \"AnotherFont\", style: \"normal\", weight: 400 },\n            createdAt: \"2024-01-01\",\n            projectId: \"project-id\",\n          },\n        ],\n      ]);\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"font-asset-1\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:fontFamily\" },\n      ]);\n      expect(usages.get(\"font-asset-2\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:fontFamily\" },\n      ]);\n    });\n\n    test(\"ignores font families without matching assets\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:fontFamily\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"fontFamily\",\n            value: {\n              type: \"fontFamily\",\n              value: [\"Arial\", \"sans-serif\"],\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>();\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.size).toBe(0);\n    });\n\n    test(\"tracks same font used in multiple styles\", () => {\n      const pages: Pages = {\n        meta: {},\n        homePage: {\n          id: \"home\",\n          name: \"Home\",\n          path: \"\",\n          title: \"Home\",\n          meta: {},\n          rootInstanceId: \"root\",\n        },\n        pages: [],\n        folders: [],\n      };\n      const props: Props = new Map();\n      const styles: Styles = new Map([\n        [\n          \"style-1:breakpoint-1:fontFamily\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-1\",\n            property: \"fontFamily\",\n            value: {\n              type: \"fontFamily\",\n              value: [\"CustomFont\"],\n            },\n          },\n        ],\n        [\n          \"style-2:breakpoint-1:fontFamily\",\n          {\n            breakpointId: \"breakpoint-1\",\n            styleSourceId: \"style-2\",\n            property: \"fontFamily\",\n            value: {\n              type: \"fontFamily\",\n              value: [\"CustomFont\"],\n            },\n          },\n        ],\n      ]);\n      const assets = new Map<Asset[\"id\"], Asset>([\n        [\n          \"font-asset-1\",\n          {\n            id: \"font-asset-1\",\n            type: \"font\",\n            name: \"CustomFont\",\n            format: \"woff2\",\n            size: 5000,\n            meta: { family: \"CustomFont\", style: \"normal\", weight: 400 },\n            createdAt: \"2024-01-01\",\n            projectId: \"project-id\",\n          },\n        ],\n      ]);\n\n      const usages = calculateUsagesByAssetId({ pages, props, styles, assets });\n\n      expect(usages.get(\"font-asset-1\")).toEqual([\n        { type: \"style\", styleDeclKey: \"style-1:breakpoint-1:fontFamily\" },\n        { type: \"style\", styleDeclKey: \"style-2:breakpoint-1:fontFamily\" },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-info.tsx",
    "content": "import isValidFilename from \"valid-filename\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport prettyBytes from \"pretty-bytes\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { getMimeTypeByExtension } from \"@webstudio-is/sdk\";\nimport type { Asset, Pages, Props, Styles, Instance } from \"@webstudio-is/sdk\";\nimport type {\n  ImageValue,\n  FontFamilyValue,\n  StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport { mapGetOrInsert } from \"~/shared/shim\";\nimport {\n  Box,\n  Button,\n  css,\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogTrigger,\n  Flex,\n  Grid,\n  IconButton,\n  InputErrorsTooltip,\n  InputField,\n  Label,\n  Popover,\n  PopoverContent,\n  PopoverTitle,\n  PopoverTrigger,\n  SmallIconButton,\n  styled,\n  Text,\n  textVariants,\n  theme,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport {\n  AspectRatioIcon,\n  CloudIcon,\n  CopyIcon,\n  DimensionsIcon,\n  DownloadIcon,\n  GearIcon,\n  InfoCircleIcon,\n  PageIcon,\n  TrashIcon,\n} from \"@webstudio-is/icons\";\nimport { hyphenateProperty } from \"@webstudio-is/css-engine\";\nimport {\n  $assets,\n  $authPermit,\n  $editingPageId,\n  $instances,\n  $pages,\n  $props,\n  $styles,\n  $styleSourceSelections,\n  $userPlanFeatures,\n} from \"~/shared/nano-states\";\nimport { $openProjectSettings } from \"~/shared/nano-states/project-settings\";\nimport {\n  $awareness,\n  findAwarenessByInstanceId,\n  selectPage,\n} from \"~/shared/awareness\";\nimport { updateWebstudioData } from \"~/shared/instance-utils\";\nimport { deleteAssets } from \"~/builder/shared/assets\";\nimport {\n  $activeInspectorPanel,\n  setActiveSidebarPanel,\n} from \"~/builder/shared/nano-states\";\nimport {\n  formatAssetName,\n  parseAssetName,\n  getAssetUrl,\n} from \"~/builder/shared/assets/asset-utils\";\nimport { getFormattedAspectRatio } from \"./utils\";\nimport { CopyToClipboard } from \"~/shared/copy-to-clipboard\";\n\ntype AssetUsage =\n  | { type: \"favicon\" }\n  | { type: \"socialImage\"; pageId: string }\n  | { type: \"marketplaceThumbnail\"; pageId: string }\n  | { type: \"prop\"; propId: string }\n  | { type: \"style\"; styleDeclKey: string };\n\nconst traverseStyleValue = (\n  styleValue: StyleValue,\n  callback: (value: ImageValue | FontFamilyValue) => void\n) => {\n  if (styleValue.type === \"image\") {\n    callback(styleValue);\n  }\n  if (styleValue.type === \"fontFamily\") {\n    callback(styleValue);\n  }\n  if (styleValue.type === \"tuple\") {\n    for (const item of styleValue.value) {\n      traverseStyleValue(item, callback);\n    }\n  }\n  if (styleValue.type === \"layers\") {\n    for (const item of styleValue.value) {\n      traverseStyleValue(item, callback);\n    }\n  }\n};\n\nexport const calculateUsagesByAssetId = ({\n  pages,\n  props,\n  styles,\n  assets,\n}: {\n  pages: Pages | undefined;\n  props: Props;\n  styles: Styles;\n  assets: Map<Asset[\"id\"], Asset>;\n}): Map<Asset[\"id\"], AssetUsage[]> => {\n  const usagesByAsset = new Map<Asset[\"id\"], AssetUsage[]>();\n\n  // Build font family to asset ID map once for O(1) lookups\n  const fontFamilyToAssetId = new Map<string, Asset[\"id\"]>();\n  for (const asset of assets.values()) {\n    if (asset.type === \"font\") {\n      fontFamilyToAssetId.set(asset.meta.family, asset.id);\n    }\n  }\n\n  if (pages?.meta?.faviconAssetId) {\n    const usages = mapGetOrInsert(usagesByAsset, pages.meta.faviconAssetId, []);\n    usages.push({ type: \"favicon\" });\n  }\n  if (pages) {\n    for (const page of [pages.homePage, ...pages.pages]) {\n      if (page.meta.socialImageAssetId) {\n        const usages = mapGetOrInsert(\n          usagesByAsset,\n          page.meta.socialImageAssetId,\n          []\n        );\n        usages.push({ type: \"socialImage\", pageId: page.id });\n      }\n      if (page.marketplace?.thumbnailAssetId) {\n        const usages = mapGetOrInsert(\n          usagesByAsset,\n          page.marketplace.thumbnailAssetId,\n          []\n        );\n        usages.push({ type: \"marketplaceThumbnail\", pageId: page.id });\n      }\n    }\n  }\n  for (const prop of props.values()) {\n    if (\n      prop.type === \"asset\" &&\n      // ignore width and height properties which are specific to size\n      prop.name !== \"width\" &&\n      prop.name !== \"height\"\n    ) {\n      const usages = mapGetOrInsert(usagesByAsset, prop.value, []);\n      usages.push({ type: \"prop\", propId: prop.id });\n    }\n  }\n  for (const [styleDeclKey, styleDecl] of styles) {\n    traverseStyleValue(styleDecl.value, (value) => {\n      if (value.type === \"image\" && value.value.type === \"asset\") {\n        const usages = mapGetOrInsert(usagesByAsset, value.value.value, []);\n        usages.push({ type: \"style\", styleDeclKey });\n      }\n      if (value.type === \"fontFamily\") {\n        // Match each font family name to its asset ID\n        for (const fontFamily of value.value) {\n          const assetId = fontFamilyToAssetId.get(fontFamily);\n          if (assetId !== undefined) {\n            const usages = mapGetOrInsert(usagesByAsset, assetId, []);\n            usages.push({ type: \"style\", styleDeclKey });\n          }\n        }\n      }\n    });\n  }\n  return usagesByAsset;\n};\n\nconst $usagesByAssetId = computed(\n  [$pages, $props, $styles, $assets],\n  (pages, props, styles, assets) => {\n    return calculateUsagesByAssetId({ pages, props, styles, assets });\n  }\n);\n\nconst buttonLinkClass = css({\n  all: \"unset\",\n  cursor: \"pointer\",\n  ...textVariants.link,\n}).toString();\n\nconst AssetUsagesList = ({ usages }: { usages: AssetUsage[] }) => {\n  const props = useStore($props);\n  const styles = useStore($styles);\n  return (\n    <Text as=\"ul\" css={{ paddingLeft: \"1em\", listStyleType: '\"-\"' }}>\n      {usages.map((usage, index) => {\n        if (usage.type === \"favicon\") {\n          return (\n            <li key={index}>\n              <button\n                className={buttonLinkClass}\n                onClick={() => {\n                  $openProjectSettings.set(\"general\");\n                  setActiveSidebarPanel(\"auto\");\n                }}\n              >\n                Favicon\n              </button>\n            </li>\n          );\n        }\n        if (usage.type === \"socialImage\") {\n          return (\n            <li key={index}>\n              <button\n                className={buttonLinkClass}\n                onClick={() => {\n                  selectPage(usage.pageId);\n                  setActiveSidebarPanel(\"pages\");\n                  $editingPageId.set(usage.pageId);\n                }}\n              >\n                Page social image\n              </button>\n            </li>\n          );\n        }\n        if (usage.type === \"marketplaceThumbnail\") {\n          return (\n            <li key={index}>\n              <button\n                className={buttonLinkClass}\n                onClick={() => {\n                  selectPage(usage.pageId);\n                  setActiveSidebarPanel(\"pages\");\n                  $editingPageId.set(usage.pageId);\n                }}\n              >\n                Marketplace page thumbnail\n              </button>\n            </li>\n          );\n        }\n        if (usage.type === \"prop\") {\n          return (\n            <li key={index}>\n              <button\n                className={buttonLinkClass}\n                onClick={() => {\n                  const pages = $pages.get();\n                  const instances = $instances.get();\n                  const prop = $props.get().get(usage.propId);\n                  if (!prop || !pages) {\n                    return;\n                  }\n                  const awareness = findAwarenessByInstanceId(\n                    pages,\n                    instances,\n                    prop.instanceId\n                  );\n                  $awareness.set(awareness);\n                  setActiveSidebarPanel(\"auto\");\n                  $activeInspectorPanel.set(\"settings\");\n                }}\n              >\n                \"{props.get(usage.propId)?.name}\" property\n              </button>\n            </li>\n          );\n        }\n        if (usage.type === \"style\") {\n          const styleDecl = styles.get(usage.styleDeclKey);\n          const property = styleDecl\n            ? hyphenateProperty(styleDecl.property)\n            : undefined;\n          return (\n            <li key={index}>\n              <button\n                className={buttonLinkClass}\n                onClick={() => {\n                  const pages = $pages.get();\n                  const instances = $instances.get();\n                  const styleDecl = $styles.get().get(usage.styleDeclKey);\n                  const styleSourceSelections = $styleSourceSelections.get();\n                  if (!styleDecl) {\n                    return;\n                  }\n                  let styleInstanceId: undefined | Instance[\"id\"];\n                  for (const {\n                    instanceId,\n                    values,\n                  } of styleSourceSelections.values()) {\n                    if (values.includes(styleDecl.styleSourceId)) {\n                      styleInstanceId = instanceId;\n                      break;\n                    }\n                  }\n                  if (!styleInstanceId || !pages) {\n                    return;\n                  }\n                  const awareness = findAwarenessByInstanceId(\n                    pages,\n                    instances,\n                    styleInstanceId\n                  );\n                  $awareness.set(awareness);\n                  setActiveSidebarPanel(\"auto\");\n                  $activeInspectorPanel.set(\"style\");\n                }}\n              >\n                \"{property}\" style\n              </button>\n            </li>\n          );\n        }\n        usage satisfies never;\n      })}\n    </Text>\n  );\n};\n\nconst UsageDot = styled(Box, {\n  width: 6,\n  height: 6,\n  backgroundColor: \"#000\",\n  border: \"1px solid white\",\n  boxShadow: \"0 0 3px rgb(0, 0, 0)\",\n  borderRadius: \"50%\",\n  pointerEvents: \"none\",\n});\n\nconst useLocalValue = <Type extends string>(\n  savedValue: Type,\n  onSave: (value: Type) => void\n) => {\n  const [localValue, setLocalValue] = useState(savedValue);\n\n  const save = () => {\n    if (localValue !== savedValue) {\n      // To synchronize with setState immediately followed by save\n      onSave(localValue);\n    }\n  };\n\n  const saveDebounced = useDebouncedCallback(save, 500);\n  const updateLocalValue = (value: Type) => {\n    setLocalValue(value);\n    saveDebounced();\n  };\n\n  // onBlur will not trigger if control is unmounted when props panel is closed or similar.\n  // So we're saving at the unmount\n  // store save in ref to access latest saved value from render\n  // instead of stale one\n  const saveRef = useRef(save);\n  saveRef.current = save;\n  useEffect(() => {\n    // access ref in the moment of unmount\n    return () => saveRef.current();\n  }, []);\n\n  return [\n    /**\n     * Contains:\n     *  - either the latest `savedValue`\n     *  - or the latest value set via `set()`\n     * (whichever changed most recently)\n     */\n    localValue,\n    updateLocalValue,\n  ] as const;\n};\n\nconst AssetInfoContent = ({\n  asset,\n  usages,\n}: {\n  asset: Asset;\n  usages: AssetUsage[];\n}) => {\n  const userPlanFeatures = useStore($userPlanFeatures);\n  const hasPaidPlan = userPlanFeatures.purchases.length > 0;\n  const { size, meta, id, name } = asset;\n  const { basename, ext } = parseAssetName(name);\n  const [filenameError, setFilenameError] = useState<string>();\n  const [filename, setFilename] = useLocalValue(\n    asset.filename ?? basename,\n    (newFilename) => {\n      const assetId = asset.id;\n      // validate filename\n      if (!isValidFilename(newFilename)) {\n        setFilenameError(\"Invalid filename\");\n        return;\n      }\n      // validate duplicates\n      for (const asset of $assets.get().values()) {\n        if (asset.id !== assetId) {\n          const filename =\n            asset.filename ?? parseAssetName(asset.name).basename;\n          if (newFilename === filename) {\n            setFilenameError(\"Filename already used\");\n            return;\n          }\n        }\n      }\n      updateWebstudioData((data) => {\n        const asset = data.assets.get(assetId);\n        if (asset) {\n          asset.filename = newFilename;\n        }\n      });\n    }\n  );\n  const [description, setDescription] = useLocalValue(\n    asset.description ?? \"\",\n    (newDescription) => {\n      const assetId = asset.id;\n      updateWebstudioData((data) => {\n        const asset = data.assets.get(assetId);\n        if (asset) {\n          asset.description = newDescription;\n        }\n      });\n    }\n  );\n\n  const authPermit = useStore($authPermit);\n\n  let downloadError: undefined | string;\n  if (authPermit === \"view\") {\n    downloadError =\n      \"Unavailable in View mode. Switch to Edit to download assets.\";\n  } else if (!hasPaidPlan) {\n    downloadError = \"Upgrade to Pro to download assets.\";\n  }\n\n  return (\n    <>\n      <Box css={{ padding: theme.panel.padding }}>\n        <Grid\n          columns={2}\n          css={{ gridTemplateColumns: \"auto auto\" }}\n          align=\"center\"\n          gap={3}\n        >\n          <Flex align=\"center\" css={{ gap: theme.spacing[3] }}>\n            <CloudIcon />\n            <Text variant=\"labels\">{prettyBytes(size)}</Text>\n          </Flex>\n          <Flex align=\"center\" css={{ gap: theme.spacing[3] }}>\n            <PageIcon />\n            <Text variant=\"labels\">\n              {getMimeTypeByExtension(ext) ?? \"unknown\"}\n            </Text>\n          </Flex>\n          {\"width\" in meta && \"height\" in meta && (\n            <>\n              <Flex align=\"center\" gap={1}>\n                <DimensionsIcon />\n                <Text variant=\"labels\">\n                  {meta.width} x {meta.height}\n                </Text>\n              </Flex>\n              <Flex align=\"center\" gap={1}>\n                <AspectRatioIcon />\n                <Text variant=\"labels\">{getFormattedAspectRatio(meta)}</Text>\n              </Flex>\n            </>\n          )}\n          <Flex align=\"center\" css={{ gap: theme.spacing[3] }}>\n            <Flex\n              css={{\n                width: 16,\n                height: 16,\n                justifyContent: \"center\",\n                alignItems: \"center\",\n              }}\n            >\n              <UsageDot />\n            </Flex>\n            <Text variant=\"labels\">{usages.length} uses</Text>\n          </Flex>\n        </Grid>\n      </Box>\n\n      <Grid css={{ padding: theme.panel.padding, gap: 4 }}>\n        <Label htmlFor=\"asset-manager-filename\">Name</Label>\n        <InputErrorsTooltip\n          errors={filenameError ? [filenameError] : undefined}\n        >\n          <InputField\n            id=\"asset-manager-filename\"\n            color={filenameError ? \"error\" : undefined}\n            value={filename}\n            onChange={(event) => {\n              setFilename(event.target.value);\n              setFilenameError(undefined);\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid css={{ padding: theme.panel.padding, gap: 4 }}>\n        <Label\n          htmlFor=\"asset-manager-description\"\n          css={{ display: \"flex\", alignItems: \"center\", gap: 4 }}\n        >\n          Description\n          <Tooltip\n            variant=\"wrapped\"\n            content=\"The description is used as the default “alt” text for the image.\"\n          >\n            <InfoCircleIcon />\n          </Tooltip>\n        </Label>\n        <InputField\n          id=\"asset-manager-description\"\n          placeholder='Enter \"alt\" text'\n          value={description}\n          onChange={(event) => setDescription(event.target.value)}\n        />\n      </Grid>\n\n      <Grid css={{ padding: theme.panel.padding, gap: 4 }}>\n        <Label htmlFor=\"asset-manager-id\">ID</Label>\n        <InputField\n          id=\"asset-manager-id\"\n          readOnly\n          value={id}\n          suffix={\n            <Flex justify=\"center\" css={{ paddingInline: theme.spacing[2] }}>\n              <CopyToClipboard text={id}>\n                <SmallIconButton icon={<CopyIcon />} />\n              </CopyToClipboard>\n            </Flex>\n          }\n        />\n      </Grid>\n\n      <Flex justify=\"between\" css={{ padding: theme.panel.padding }}>\n        {authPermit === \"view\" ? (\n          <Tooltip side=\"bottom\" content=\"View mode. You can't delete assets.\">\n            <Button disabled color=\"destructive\" prefix={<TrashIcon />}>\n              Delete\n            </Button>\n          </Tooltip>\n        ) : usages.length === 0 ? (\n          <Button\n            color=\"destructive\"\n            onClick={() => deleteAssets([id])}\n            prefix={<TrashIcon />}\n          >\n            Delete\n          </Button>\n        ) : (\n          <Dialog>\n            <DialogTrigger asChild>\n              <Button>Review & delete</Button>\n            </DialogTrigger>\n            <DialogContent minWidth={360}>\n              <DialogTitle>Delete asset?</DialogTitle>\n              <Box css={{ padding: theme.panel.padding }}>\n                <Text css={{ marginBottom: \"1em\" }}>\n                  This asset is used in following places:\n                </Text>\n                <AssetUsagesList usages={usages} />\n                <Flex>\n                  <Button\n                    css={{\n                      marginLeft: \"auto\",\n                      marginTop: theme.panel.paddingBlock,\n                    }}\n                    color=\"destructive\"\n                    prefix={<TrashIcon />}\n                    onClick={() => deleteAssets([id])}\n                  >\n                    Delete\n                  </Button>\n                </Flex>\n              </Box>\n            </DialogContent>\n          </Dialog>\n        )}\n\n        {downloadError ? (\n          <Tooltip side=\"bottom\" content={downloadError}>\n            <IconButton disabled>\n              <DownloadIcon />\n            </IconButton>\n          </Tooltip>\n        ) : (\n          <Tooltip side=\"bottom\" content=\"Download asset\">\n            <IconButton\n              as=\"a\"\n              download={formatAssetName(asset)}\n              href={getAssetUrl(asset, window.location.origin).href}\n            >\n              <DownloadIcon />\n            </IconButton>\n          </Tooltip>\n        )}\n      </Flex>\n    </>\n  );\n};\n\nconst triggerVisibilityVar = `--ws-asset-info-trigger-visibility`;\n\nexport const assetInfoCssVars = ({ show }: { show: boolean }) => ({\n  [triggerVisibilityVar]: show ? \"visible\" : \"hidden\",\n});\n\nexport const AssetInfo = ({ asset }: { asset: Asset }) => {\n  const usagesByAssetId = useStore($usagesByAssetId);\n  const usages = usagesByAssetId.get(asset.id) ?? [];\n  return (\n    <>\n      <Popover modal>\n        <PopoverTrigger asChild>\n          <SmallIconButton\n            title=\"Options\"\n            tabIndex={-1}\n            css={{\n              visibility: `var(${triggerVisibilityVar}, hidden)`,\n              position: \"absolute\",\n              color: theme.colors.backgroundIconSubtle,\n              top: theme.spacing[3],\n              right: theme.spacing[3],\n              cursor: \"pointer\",\n              transition: \"opacity 100ms ease\",\n              \"& svg\": {\n                fill: `oklch(from ${theme.colors.white} l c h / 0.9)`,\n              },\n              \"&:hover\": {\n                color: theme.colors.backgroundIconSubtle,\n                background: \"transparent\",\n                \"& svg\": {\n                  fill: `oklch(from ${theme.colors.white} l c h / 1)`,\n                },\n              },\n            }}\n            icon={<GearIcon />}\n          />\n        </PopoverTrigger>\n        <PopoverContent css={{ minWidth: 250 }}>\n          <PopoverTitle>Asset details</PopoverTitle>\n          <AssetInfoContent asset={asset} usages={usages} />\n        </PopoverContent>\n      </Popover>\n      {usages.length === 0 && (\n        <UsageDot css={{ position: \"absolute\", top: 9, right: 9 }} />\n      )}\n    </>\n  );\n};\n\nexport const __testing__ = {\n  traverseStyleValue,\n  calculateUsagesByAssetId,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-manager.stories.tsx",
    "content": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { AssetManager as AssetManagerComponent } from \"./asset-manager\";\nimport {\n  ALLOWED_FILE_TYPES,\n  IMAGE_EXTENSIONS,\n  VIDEO_EXTENSIONS,\n} from \"@webstudio-is/sdk\";\nimport type { Asset, AllowedFileExtension } from \"@webstudio-is/sdk\";\nimport { $assets } from \"~/shared/nano-states\";\nimport { useEffect } from \"react\";\n\n// Create mock assets for every file type\nconst createMockAssets = (): Asset[] => {\n  const extensions = Object.keys(ALLOWED_FILE_TYPES).filter(\n    (ext) =>\n      !IMAGE_EXTENSIONS.includes(ext as AllowedFileExtension) &&\n      !VIDEO_EXTENSIONS.includes(ext as AllowedFileExtension)\n  );\n  const assets: Asset[] = [];\n\n  extensions.forEach((ext, index) => {\n    assets.push({\n      id: `asset-${index}`,\n      name: `example-file.${ext}`,\n      type: \"file\" as const, // Use \"file\" type to show icons instead of trying to load images\n      format: ext,\n      size: 1024 * (index + 1),\n      meta: { width: 0, height: 0 },\n      createdAt: new Date().toISOString(),\n      projectId: \"mock-project\",\n      description: `Example ${ext.toUpperCase()} file`,\n    });\n  });\n\n  return assets;\n};\n\nexport default {\n  title: \"Asset Manager\",\n  component: AssetManagerComponent,\n} satisfies Meta;\n\nconst AssetManagerStory = () => {\n  useEffect(() => {\n    const assets = createMockAssets();\n    $assets.set(new Map(assets.map((asset) => [asset.id, asset])));\n  }, []);\n\n  return (\n    <StorySection title=\"Asset Manager\">\n      <div style={{ width: 400, display: \"flex\" }}>\n        <AssetManagerComponent />\n      </div>\n    </StorySection>\n  );\n};\n\nexport const AssetManager: StoryObj = {\n  render: () => <AssetManagerStory />,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-manager.tsx",
    "content": "import { useState, useMemo } from \"react\";\nimport {\n  Flex,\n  Grid,\n  findNextListItemIndex,\n  theme,\n  useSearchFieldKeys,\n} from \"@webstudio-is/design-system\";\nimport type { Asset, AllowedFileExtension } from \"@webstudio-is/sdk\";\nimport { AssetsShell, type AssetContainer, useAssets } from \"../assets\";\nimport { AssetThumbnail } from \"./asset-thumbnail\";\nimport { AssetFilters } from \"./asset-filters\";\nimport { AssetSortSelect } from \"./asset-sort\";\nimport {\n  getInitialExtensions,\n  calculateFormatCounts,\n  filterAndSortAssets,\n  findAssetIndex,\n  type SortState,\n} from \"./utils\";\n\ntype AssetManagerProps = {\n  onChange?: (assetId: Asset[\"id\"]) => void;\n  /** acceptable file types in the `<imput accept>` attribute format */\n  accept?: string;\n};\n\nexport const AssetManager = ({ accept = \"*\", onChange }: AssetManagerProps) => {\n  const { assetContainers } = useAssets();\n\n  const [selectedIndex, setSelectedIndex] = useState(-1);\n\n  const [selectedExtensions, setSelectedExtensions] = useState<\n    AllowedFileExtension[] | \"*\"\n  >(() => getInitialExtensions(accept, assetContainers));\n\n  const [sortState, setSortState] = useState<SortState>({\n    sortBy: \"createdAt\",\n    order: \"desc\",\n  });\n\n  const formatCounts = useMemo(\n    () => calculateFormatCounts(assetContainers),\n    [assetContainers]\n  );\n\n  const searchProps = useSearchFieldKeys({\n    onMove({ direction }) {\n      if (direction === \"current\") {\n        const assetContainer = filteredItems[selectedIndex];\n        if (assetContainer?.status === \"uploaded\") {\n          onChange?.(assetContainer.asset.id);\n        }\n        return;\n      }\n      const nextIndex = findNextListItemIndex(\n        selectedIndex,\n        filteredItems.length,\n        direction\n      );\n      setSelectedIndex(nextIndex);\n    },\n  });\n\n  const filteredItems = useMemo(\n    () =>\n      filterAndSortAssets({\n        assetContainers,\n        selectedExtensions,\n        searchQuery: searchProps.value,\n        sortState,\n      }),\n    [assetContainers, selectedExtensions, searchProps.value, sortState]\n  );\n\n  const handleSelect = (assetContainer?: AssetContainer) => {\n    setSelectedIndex(findAssetIndex(filteredItems, assetContainer?.asset.id));\n  };\n\n  return (\n    <AssetsShell\n      filters={\n        <Flex gap=\"2\" grow>\n          <AssetFilters\n            formatCounts={formatCounts}\n            value={selectedExtensions}\n            onChange={setSelectedExtensions}\n          />\n          <AssetSortSelect value={sortState} onValueChange={setSortState} />\n        </Flex>\n      }\n      searchProps={searchProps}\n      isEmpty={filteredItems.length === 0}\n      type=\"file\"\n      accept={accept}\n    >\n      <>\n        <Grid\n          columns={3}\n          gap=\"2\"\n          css={{ paddingInline: theme.panel.paddingInline }}\n        >\n          {filteredItems.map((assetContainer, index) => (\n            <AssetThumbnail\n              key={assetContainer.asset.id}\n              assetContainer={assetContainer}\n              onSelect={handleSelect}\n              onChange={(assetContainer) => {\n                onChange?.(assetContainer.asset.id);\n              }}\n              state={index === selectedIndex ? \"selected\" : undefined}\n            />\n          ))}\n        </Grid>\n      </>\n    </AssetsShell>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-sort.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuLabel,\n  Button,\n  MenuCheckedIcon,\n} from \"@webstudio-is/design-system\";\nimport {\n  ChevronDownIcon,\n  CalendarIcon,\n  ArrowDownAZIcon,\n  ArrowDownZAIcon,\n  ArrowDownWideNarrowIcon,\n  ArrowDownNarrowWideIcon,\n} from \"@webstudio-is/icons\";\nimport type { SortState, SortField, SortOrder } from \"./utils\";\n\ntype AssetSortSelectProps = {\n  value: SortState;\n  onValueChange: (value: SortState) => void;\n};\n\nexport const AssetSortSelect = ({\n  value,\n  onValueChange,\n}: AssetSortSelectProps) => {\n  const { sortBy, order } = value;\n\n  const sortLabel =\n    sortBy === \"name\"\n      ? \"Alphabetical\"\n      : sortBy === \"size\"\n        ? \"File size\"\n        : \"Date created\";\n\n  const sortIcon =\n    sortBy === \"name\" ? (\n      order === \"asc\" ? (\n        <ArrowDownAZIcon />\n      ) : (\n        <ArrowDownZAIcon />\n      )\n    ) : sortBy === \"size\" ? (\n      order === \"desc\" ? (\n        <ArrowDownWideNarrowIcon />\n      ) : (\n        <ArrowDownNarrowWideIcon />\n      )\n    ) : (\n      <CalendarIcon />\n    );\n\n  const handleSortChange = (newSortBy: SortField, newOrder: SortOrder) => {\n    // When switching to alphabetical sorting, default to A→Z (asc)\n    // When switching to date/size sorting, default to newest/largest first (desc)\n    if (newSortBy !== sortBy) {\n      onValueChange({\n        sortBy: newSortBy,\n        order: newSortBy === \"name\" ? \"asc\" : \"desc\",\n      });\n    } else {\n      onValueChange({ sortBy: newSortBy, order: newOrder });\n    }\n  };\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button color=\"ghost\" prefix={sortIcon} suffix={<ChevronDownIcon />}>\n          {sortLabel}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuLabel>Sort</DropdownMenuLabel>\n        <DropdownMenuRadioGroup\n          value={sortBy}\n          onValueChange={(value) => handleSortChange(value as SortField, order)}\n        >\n          <DropdownMenuRadioItem value=\"name\" icon={<MenuCheckedIcon />}>\n            Alphabetical\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"createdAt\" icon={<MenuCheckedIcon />}>\n            Date created\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"size\" icon={<MenuCheckedIcon />}>\n            File size\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n        <DropdownMenuSeparator />\n        <DropdownMenuLabel>Order</DropdownMenuLabel>\n        <DropdownMenuRadioGroup\n          value={order}\n          onValueChange={(value) =>\n            handleSortChange(sortBy, value as SortOrder)\n          }\n        >\n          {sortBy === \"name\" ? (\n            <>\n              <DropdownMenuRadioItem value=\"asc\" icon={<MenuCheckedIcon />}>\n                A→Z\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"desc\" icon={<MenuCheckedIcon />}>\n                Z→A\n              </DropdownMenuRadioItem>\n            </>\n          ) : sortBy === \"size\" ? (\n            <>\n              <DropdownMenuRadioItem value=\"desc\" icon={<MenuCheckedIcon />}>\n                Largest first\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"asc\" icon={<MenuCheckedIcon />}>\n                Smallest first\n              </DropdownMenuRadioItem>\n            </>\n          ) : (\n            <>\n              <DropdownMenuRadioItem value=\"desc\" icon={<MenuCheckedIcon />}>\n                Newest first\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"asc\" icon={<MenuCheckedIcon />}>\n                Oldest first\n              </DropdownMenuRadioItem>\n            </>\n          )}\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/asset-thumbnail.tsx",
    "content": "import type { KeyboardEvent, FocusEvent } from \"react\";\nimport { Box, Flex, styled, Text } from \"@webstudio-is/design-system\";\nimport { PageIcon, TextCapitalizeIcon } from \"@webstudio-is/icons\";\nimport { wsVideoLoader } from \"@webstudio-is/image\";\nimport { UploadingAnimation } from \"./uploading-animation\";\nimport { AssetInfo, assetInfoCssVars } from \"./asset-info\";\nimport type { AssetContainer } from \"~/builder/shared/assets\";\nimport { Image } from \"./image\";\nimport brokenImage from \"~/shared/images/broken-image-placeholder.svg\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport {\n  formatAssetName,\n  parseAssetName,\n} from \"~/builder/shared/assets/asset-utils\";\nimport type { IconComponent } from \"@webstudio-is/icons\";\nimport type { AllowedFileExtension } from \"@webstudio-is/sdk\";\nimport {\n  FILE_EXTENSIONS_BY_CATEGORY,\n  detectAssetType,\n} from \"@webstudio-is/sdk\";\nimport type { MimeCategory } from \"@webstudio-is/sdk\";\n\nconst FORMAT_CATEGORIES = FILE_EXTENSIONS_BY_CATEGORY;\n\nconst CATEGORY_ICON_MAP: Partial<Record<MimeCategory, IconComponent>> = {\n  font: TextCapitalizeIcon,\n};\n\nconst getFileIcon = (format: string): IconComponent => {\n  const lowerFormat = format.toLowerCase();\n\n  // Check which category this format belongs to\n  for (const [category, extensions] of Object.entries(FORMAT_CATEGORIES)) {\n    if (extensions.includes(lowerFormat as AllowedFileExtension)) {\n      return CATEGORY_ICON_MAP[category as MimeCategory] ?? PageIcon;\n    }\n  }\n\n  // Default to PageIcon if not found in any category\n  return PageIcon;\n};\n\nconst StyledWebstudioImage = styled(Image, {\n  position: \"absolute\",\n  width: \"100%\",\n  height: \"100%\",\n  objectFit: \"contain\",\n\n  // This is shown only if an image was not loaded and broken\n  // From the spec:\n  // - The pseudo-elements generated by ::before and ::after are contained by the element's formatting box,\n  //   and thus don't apply to \"replaced\" elements such as <img>, or to <br> elements\n  // Not in spec but supported by all browsers:\n  // - broken image is not a \"replaced\" element so this style is applied\n  \"&::after\": {\n    content: \"' '\",\n    position: \"absolute\",\n    width: \"100%\",\n    height: \"100%\",\n    left: 0,\n    top: 0,\n    backgroundSize: \"contain\",\n    backgroundRepeat: \"no-repeat\",\n    backgroundPosition: \"center\",\n    backgroundImage: `url(${brokenImage})`,\n  },\n});\n\nconst StyledWebstudioVideo = styled(\"video\", {\n  position: \"absolute\",\n  width: \"100%\",\n  height: \"100%\",\n  objectFit: \"contain\",\n\n  // This is shown only if an image was not loaded and broken\n  // From the spec:\n  // - The pseudo-elements generated by ::before and ::after are contained by the element's formatting box,\n  //   and thus don't apply to \"replaced\" elements such as <img>, or to <br> elements\n  // Not in spec but supported by all browsers:\n  // - broken image is not a \"replaced\" element so this style is applied\n  \"&::after\": {\n    content: \"' '\",\n    position: \"absolute\",\n    width: \"100%\",\n    height: \"100%\",\n    left: 0,\n    top: 0,\n    backgroundSize: \"contain\",\n    backgroundRepeat: \"no-repeat\",\n    backgroundPosition: \"center\",\n    backgroundImage: `url(${brokenImage})`,\n  },\n});\n\nconst ThumbnailContainer = styled(Box, {\n  position: \"relative\",\n  display: \"flex\",\n  justifyContent: \"center\",\n  alignItems: \"center\",\n  flexDirection: \"column\",\n  borderRadius: theme.borderRadius[4],\n  outline: \"none\",\n  gap: theme.spacing[3],\n  overflow: \"hidden\",\n  padding: 2,\n  \"&:hover\": {\n    ...assetInfoCssVars({ show: true }),\n    backgroundColor: theme.colors.backgroundAssetcardHover,\n  },\n  variants: {\n    status: {\n      uploading: {},\n      uploaded: {},\n      deleting: {},\n    },\n    state: {\n      selected: {\n        outline: `1px solid ${theme.colors.borderFocus}`,\n        outlineOffset: -1,\n        backgroundColor: theme.colors.backgroundAssetcardHover,\n        ...assetInfoCssVars({ show: true }),\n      },\n    },\n  },\n});\n\nconst Thumbnail = styled(Box, {\n  width: \"100%\",\n  height: theme.spacing[19],\n  flexShrink: 0,\n  position: \"relative\",\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n});\n\nconst GenericFilePreview = ({\n  ext,\n  format,\n}: {\n  ext: string;\n  format: string;\n}) => {\n  const Icon = getFileIcon(format);\n  const showExtension = Icon === PageIcon;\n\n  return (\n    <Box css={{ position: \"relative\" }}>\n      <Icon size={48} strokeWidth={0.5} />\n      {showExtension && (\n        <Text\n          variant=\"tiny\"\n          color=\"subtle\"\n          css={{\n            position: \"absolute\",\n            top: 30,\n            left: \"50%\",\n            transform: \"translateX(-50%)\",\n          }}\n        >\n          {ext.toUpperCase()}\n        </Text>\n      )}\n    </Box>\n  );\n};\n\ntype AssetThumbnailProps = {\n  assetContainer: AssetContainer;\n  onSelect: (assetContainer?: AssetContainer) => void;\n  onChange?: (assetContainer: AssetContainer) => void;\n  state?: \"selected\";\n};\n\nexport const AssetThumbnail = ({\n  assetContainer,\n  onSelect,\n  onChange,\n  state,\n}: AssetThumbnailProps) => {\n  const { asset } = assetContainer;\n  const { basename, ext } = parseAssetName(asset.name);\n  const alt = asset.description ?? formatAssetName(asset);\n  const isUploading = assetContainer.status === \"uploading\";\n  const assetType = detectAssetType(asset.name);\n\n  return (\n    <ThumbnailContainer\n      title={alt}\n      tabIndex={0}\n      status={assetContainer.status}\n      state={state}\n      onFocus={() => {\n        onSelect?.(assetContainer);\n      }}\n      onBlur={(event: FocusEvent) => {\n        const isFocusWithin = event.currentTarget.contains(event.relatedTarget);\n        if (isFocusWithin === false) {\n          onSelect();\n        }\n      }}\n      onKeyDown={(event: KeyboardEvent) => {\n        if (event.code === \"Enter\") {\n          onChange?.(assetContainer);\n        }\n      }}\n    >\n      <Thumbnail\n        onClick={() => {\n          onChange?.(assetContainer);\n        }}\n      >\n        {assetType === \"image\" ? (\n          // Image files - show preview\n          <StyledWebstudioImage\n            assetId={asset.id}\n            name={asset.name}\n            objectURL={\n              assetContainer.status === \"uploading\"\n                ? assetContainer.objectURL\n                : undefined\n            }\n            alt={alt}\n            // width={64} used for Image optimizations it should be approximately equal to the width of the picture on the screen in px\n            width={64}\n          />\n        ) : assetType === \"video\" ? (\n          // Video files - show video thumbnail (first frame)\n          <StyledWebstudioVideo\n            src={\n              assetContainer.status === \"uploading\"\n                ? assetContainer.objectURL\n                : wsVideoLoader({ src: asset.name })\n            }\n          />\n        ) : (\n          // Other files - show icon based on category\n          <GenericFilePreview ext={ext} format={asset.format} />\n        )}\n      </Thumbnail>\n      <Flex css={{ width: \"100%\", paddingBottom: 4 }} justify=\"center\">\n        <Text variant=\"tiny\" truncate>\n          {asset.filename ?? basename}\n        </Text>\n        <Text variant=\"tiny\">.{ext}</Text>\n      </Flex>\n      {assetContainer.status === \"uploaded\" && (\n        <AssetInfo asset={assetContainer.asset} />\n      )}\n      {isUploading && <UploadingAnimation />}\n    </ThumbnailContainer>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/delete-unused-assets.tsx",
    "content": "import { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n  ScrollArea,\n  Button,\n  Text,\n  Flex,\n  theme,\n  toast,\n  Box,\n} from \"@webstudio-is/design-system\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport { $assets, $pages, $props, $styles } from \"~/shared/nano-states\";\nimport { deleteAssets } from \"~/builder/shared/assets\";\nimport { formatAssetName } from \"~/builder/shared/assets/asset-utils\";\nimport { calculateUsagesByAssetId } from \"./asset-info\";\n\nconst $isDeleteUnusedAssetsDialogOpen = atom(false);\n\nexport const openDeleteUnusedAssetsDialog = () => {\n  $isDeleteUnusedAssetsDialogOpen.set(true);\n};\n\nconst getUnusedAssets = () => {\n  const assets = $assets.get();\n  const usagesByAssetId = calculateUsagesByAssetId({\n    pages: $pages.get(),\n    props: $props.get(),\n    styles: $styles.get(),\n    assets,\n  });\n  const unusedAssets: Asset[] = [];\n  for (const asset of assets.values()) {\n    const usages = usagesByAssetId.get(asset.id);\n    if (usages === undefined || usages.length === 0) {\n      unusedAssets.push(asset);\n    }\n  }\n  return unusedAssets;\n};\n\nconst DeleteUnusedAssetsDialogContent = ({\n  onClose,\n}: {\n  onClose: () => void;\n}) => {\n  const unusedAssets = getUnusedAssets();\n\n  return (\n    <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n      {unusedAssets.length === 0 ? (\n        <Text>There are no unused assets to delete.</Text>\n      ) : (\n        <>\n          <Text>\n            Delete {unusedAssets.length} unused{\" \"}\n            {unusedAssets.length === 1 ? \"asset\" : \"assets\"} from the project?\n          </Text>\n\n          <ScrollArea>\n            <Box css={{ maxHeight: 200 }}>\n              <Flex direction=\"column\" gap=\"1\">\n                {unusedAssets.map((asset) => (\n                  <Text key={asset.id} variant=\"mono\" truncate>\n                    {formatAssetName(asset)}\n                  </Text>\n                ))}\n              </Flex>\n            </Box>\n          </ScrollArea>\n        </>\n      )}\n      <Flex direction=\"rowReverse\" gap=\"2\">\n        {unusedAssets.length > 0 && (\n          <Button\n            color=\"destructive\"\n            onClick={() => {\n              const count = unusedAssets.length;\n              deleteAssets(unusedAssets.map((asset) => asset.id));\n              onClose();\n              toast.success(\n                `Deleted ${count} unused ${count === 1 ? \"asset\" : \"assets\"}`\n              );\n            }}\n            autoFocus\n          >\n            Delete\n          </Button>\n        )}\n        <DialogClose>\n          <Button color=\"ghost\">\n            {unusedAssets.length > 0 ? \"Cancel\" : \"Close\"}\n          </Button>\n        </DialogClose>\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const DeleteUnusedAssetsDialog = () => {\n  const open = useStore($isDeleteUnusedAssetsDialogOpen);\n  const handleClose = () => {\n    $isDeleteUnusedAssetsDialogOpen.set(false);\n  };\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          handleClose();\n        }\n      }}\n    >\n      <DialogContent\n        width={400}\n        onKeyDown={(event) => {\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete unused assets</DialogTitle>\n        <DeleteUnusedAssetsDialogContent onClose={handleClose} />\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/image.tsx",
    "content": "import { Image as WebstudioImage, wsImageLoader } from \"@webstudio-is/image\";\n\ntype ImageProps = {\n  assetId: string;\n  objectURL: string | undefined;\n  name: string;\n  alt: string;\n  width: number;\n  className?: string;\n};\n\nexport const Image = ({\n  className,\n  objectURL,\n  assetId,\n  name,\n  alt,\n  width,\n}: ImageProps) => {\n  const optimize = objectURL === undefined;\n\n  // Avoid image flickering on switching from preview to asset (during upload)\n  // Possible optimisation, we can set it to \"sync\" only if asset.path has changed or add isNew prop to UploadedAssetContainer\n  const decoding = \"sync\";\n\n  const src = objectURL ?? name;\n\n  return (\n    <WebstudioImage\n      className={className}\n      style={{\n        // Prevent native image drag in Image Manager to avoid issues with monitorForExternal\n        // from @atlaskit/pragmatic-drag-and-drop, which incorrectly identifies it as an external drag operation\n        // when used inside an iframe.\n        // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n        // @ts-ignore\n        WebkitUserDrag: \"none\",\n        maxWidth: \"100%\",\n      }}\n      key={assetId}\n      loader={wsImageLoader}\n      decoding={decoding}\n      src={src}\n      width={width}\n      optimize={optimize}\n      alt={alt}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/index.ts",
    "content": "export * from \"./asset-manager\";\nexport { getFormattedAspectRatio } from \"./utils\";\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/uploading-animation.tsx",
    "content": "import { rawTheme, styled } from \"@webstudio-is/design-system\";\nimport { SpinnerIcon } from \"@webstudio-is/icons\";\n\nconst AnimationContainer = styled(\"div\", {\n  position: \"absolute\",\n  top: 0,\n  left: 0,\n  height: \"100%\",\n  width: \"100%\",\n  display: \"flex\",\n  flexDirection: \"column\",\n  justifyContent: \"center\",\n  alignItems: \"center\",\n  pointerEvents: \"none\",\n});\n\nexport const UploadingAnimation = () => {\n  return (\n    <AnimationContainer>\n      <SpinnerIcon size={rawTheme.spacing[15]} />\n    </AnimationContainer>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { AssetContainer } from \"../assets\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport {\n  getInitialExtensions,\n  calculateFormatCounts,\n  filterAndSortAssets,\n  findAssetIndex,\n  sortAssets,\n  getAssetFormat,\n  type SortState,\n} from \"./utils\";\n\nconst createAssetContainer = (\n  id: string,\n  name: string,\n  format: string,\n  type: string,\n  createdAt: string\n): AssetContainer =>\n  ({\n    status: \"uploaded\" as const,\n    asset: {\n      id,\n      name,\n      format,\n      type,\n      createdAt,\n      projectId: \"test-project\",\n      size: 1000,\n    },\n  }) as AssetContainer;\n\ndescribe(\"getAssetFormat\", () => {\n  test(\"returns lowercase format\", () => {\n    const asset = { format: \"JPEG\" } as Pick<Asset, \"format\">;\n    expect(getAssetFormat(asset)).toBe(\"jpeg\");\n  });\n\n  test(\"handles already lowercase format\", () => {\n    const asset = { format: \"png\" } as Pick<Asset, \"format\">;\n    expect(getAssetFormat(asset)).toBe(\"png\");\n  });\n\n  test(\"handles mixed case format\", () => {\n    const asset = { format: \"WoFf2\" } as Pick<Asset, \"format\">;\n    expect(getAssetFormat(asset)).toBe(\"woff2\");\n  });\n\n  test(\"returns undefined for undefined format\", () => {\n    const asset = { format: undefined } as unknown as Pick<Asset, \"format\">;\n    expect(getAssetFormat(asset)).toBeUndefined();\n  });\n\n  test(\"handles empty string format\", () => {\n    const asset = { format: \"\" } as Pick<Asset, \"format\">;\n    expect(getAssetFormat(asset)).toBe(\"\");\n  });\n});\n\ndescribe(\"getInitialExtensions\", () => {\n  test(\"returns * for wildcard accept\", () => {\n    const containers: AssetContainer[] = [];\n    expect(getInitialExtensions(\"*\", containers)).toBe(\"*\");\n  });\n\n  test(\"returns * for empty accept\", () => {\n    const containers: AssetContainer[] = [];\n    expect(getInitialExtensions(\"\", containers)).toBe(\"*\");\n  });\n\n  test(\"extracts extensions from containers matching accept pattern\", () => {\n    const containers = [\n      createAssetContainer(\"1\", \"image.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n      createAssetContainer(\"2\", \"image.png\", \"png\", \"image\", \"2024-01-02\"),\n      createAssetContainer(\"3\", \"font.woff2\", \"woff2\", \"font\", \"2024-01-03\"),\n    ];\n    const result = getInitialExtensions(\"image/*\", containers);\n    expect(result).toEqual([\"jpeg\", \"png\"]);\n  });\n\n  test(\"does not include duplicate extensions\", () => {\n    const containers = [\n      createAssetContainer(\"1\", \"image1.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n      createAssetContainer(\"2\", \"image2.jpg\", \"jpeg\", \"image\", \"2024-01-02\"),\n    ];\n    const result = getInitialExtensions(\"image/*\", containers);\n    expect(result).toEqual([\"jpeg\"]);\n  });\n\n  test(\"returns * when no containers match\", () => {\n    const containers = [\n      createAssetContainer(\"1\", \"font.woff2\", \"woff2\", \"font\", \"2024-01-01\"),\n    ];\n    const result = getInitialExtensions(\"image/*\", containers);\n    expect(result).toBe(\"*\");\n  });\n});\n\ndescribe(\"calculateFormatCounts\", () => {\n  test(\"counts formats correctly\", () => {\n    const containers = [\n      createAssetContainer(\"1\", \"image1.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n      createAssetContainer(\"2\", \"image2.jpg\", \"jpeg\", \"image\", \"2024-01-02\"),\n      createAssetContainer(\"3\", \"image.png\", \"png\", \"image\", \"2024-01-03\"),\n      createAssetContainer(\"4\", \"font.woff2\", \"woff2\", \"font\", \"2024-01-04\"),\n    ];\n    const counts = calculateFormatCounts(containers);\n    expect(counts).toEqual({\n      jpeg: 2,\n      png: 1,\n      woff2: 1,\n    });\n  });\n\n  test(\"returns empty object for empty containers\", () => {\n    const counts = calculateFormatCounts([]);\n    expect(counts).toEqual({});\n  });\n\n  test(\"skips assets with undefined format\", () => {\n    const containers = [\n      createAssetContainer(\"1\", \"image.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n      {\n        status: \"uploaded\" as const,\n        asset: {\n          id: \"2\",\n          name: \"no-format\",\n          format: undefined,\n          type: \"file\",\n          createdAt: \"2024-01-02\",\n          projectId: \"test-project\",\n          size: 1000,\n        },\n      } as unknown as AssetContainer,\n    ];\n    const counts = calculateFormatCounts(containers);\n    expect(counts).toEqual({ jpeg: 1 });\n  });\n});\n\ndescribe(\"filterAndSortAssets\", () => {\n  const containers = [\n    createAssetContainer(\"1\", \"apple.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n    createAssetContainer(\"2\", \"banana.png\", \"png\", \"image\", \"2024-01-02\"),\n    createAssetContainer(\"3\", \"cherry.jpg\", \"jpeg\", \"image\", \"2024-01-03\"),\n    createAssetContainer(\"4\", \"date.woff2\", \"woff2\", \"font\", \"2024-01-04\"),\n  ];\n\n  test(\"returns all assets when selectedExtensions is *\", () => {\n    const sortState: SortState = { sortBy: \"createdAt\", order: \"asc\" };\n    const result = filterAndSortAssets({\n      assetContainers: containers,\n      selectedExtensions: \"*\",\n      searchQuery: \"\",\n      sortState,\n    });\n    expect(result).toHaveLength(4);\n  });\n\n  test(\"filters by selected extensions\", () => {\n    const sortState: SortState = { sortBy: \"createdAt\", order: \"asc\" };\n    const result = filterAndSortAssets({\n      assetContainers: containers,\n      selectedExtensions: [\"jpeg\"],\n      searchQuery: \"\",\n      sortState,\n    });\n    expect(result).toHaveLength(2);\n    expect(result.every((c) => c.asset.format === \"jpeg\")).toBe(true);\n  });\n\n  test(\"filters by search query\", () => {\n    const sortState: SortState = { sortBy: \"createdAt\", order: \"asc\" };\n    const result = filterAndSortAssets({\n      assetContainers: containers,\n      selectedExtensions: \"*\",\n      searchQuery: \"apple\",\n      sortState,\n    });\n    expect(result).toHaveLength(1);\n    expect(result[0].asset.name).toBe(\"apple.jpg\");\n  });\n\n  test(\"applies both extension filter and search\", () => {\n    const sortState: SortState = { sortBy: \"createdAt\", order: \"asc\" };\n    const result = filterAndSortAssets({\n      assetContainers: containers,\n      selectedExtensions: [\"jpeg\", \"png\"],\n      searchQuery: \"a\",\n      sortState,\n    });\n    expect(result).toHaveLength(2);\n    expect(result.map((c) => c.asset.name)).toEqual([\n      \"apple.jpg\",\n      \"banana.png\",\n    ]);\n  });\n\n  test(\"returns empty array when no matches\", () => {\n    const sortState: SortState = { sortBy: \"createdAt\", order: \"asc\" };\n    const result = filterAndSortAssets({\n      assetContainers: containers,\n      selectedExtensions: [\"jpeg\"],\n      searchQuery: \"xyz\",\n      sortState,\n    });\n    expect(result).toHaveLength(0);\n  });\n});\n\ndescribe(\"findAssetIndex\", () => {\n  const containers = [\n    createAssetContainer(\"1\", \"apple.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n    createAssetContainer(\"2\", \"banana.png\", \"png\", \"image\", \"2024-01-02\"),\n    createAssetContainer(\"3\", \"cherry.jpg\", \"jpeg\", \"image\", \"2024-01-03\"),\n  ];\n\n  test(\"finds asset index by id\", () => {\n    expect(findAssetIndex(containers, \"2\")).toBe(1);\n  });\n\n  test(\"returns -1 for non-existent id\", () => {\n    expect(findAssetIndex(containers, \"999\")).toBe(-1);\n  });\n\n  test(\"returns -1 for undefined id\", () => {\n    expect(findAssetIndex(containers, undefined)).toBe(-1);\n  });\n\n  test(\"finds first asset\", () => {\n    expect(findAssetIndex(containers, \"1\")).toBe(0);\n  });\n\n  test(\"finds last asset\", () => {\n    expect(findAssetIndex(containers, \"3\")).toBe(2);\n  });\n});\n\ndescribe(\"sortAssets\", () => {\n  const containers = [\n    createAssetContainer(\"1\", \"zebra.jpg\", \"jpeg\", \"image\", \"2024-01-03\"),\n    createAssetContainer(\"2\", \"apple.png\", \"png\", \"image\", \"2024-01-01\"),\n    createAssetContainer(\"3\", \"mango.jpg\", \"jpeg\", \"image\", \"2024-01-02\"),\n  ];\n\n  test(\"sorts by name ascending\", () => {\n    const result = sortAssets(containers, { sortBy: \"name\", order: \"asc\" });\n    expect(result.map((c) => c.asset.name)).toEqual([\n      \"apple.png\",\n      \"mango.jpg\",\n      \"zebra.jpg\",\n    ]);\n  });\n\n  test(\"sorts by name descending\", () => {\n    const result = sortAssets(containers, { sortBy: \"name\", order: \"desc\" });\n    expect(result.map((c) => c.asset.name)).toEqual([\n      \"zebra.jpg\",\n      \"mango.jpg\",\n      \"apple.png\",\n    ]);\n  });\n\n  test(\"sorts by createdAt ascending\", () => {\n    const result = sortAssets(containers, {\n      sortBy: \"createdAt\",\n      order: \"asc\",\n    });\n    expect(result.map((c) => c.asset.id)).toEqual([\"2\", \"3\", \"1\"]);\n  });\n\n  test(\"sorts by createdAt descending\", () => {\n    const result = sortAssets(containers, {\n      sortBy: \"createdAt\",\n      order: \"desc\",\n    });\n    expect(result.map((c) => c.asset.id)).toEqual([\"1\", \"3\", \"2\"]);\n  });\n\n  test(\"sorts by size ascending\", () => {\n    const smallFile = {\n      status: \"uploaded\" as const,\n      asset: {\n        id: \"small\",\n        name: \"small.jpg\",\n        format: \"jpeg\",\n        type: \"image\",\n        createdAt: \"2024-01-01\",\n        projectId: \"test-project\",\n        size: 100,\n      },\n    } as AssetContainer;\n\n    const largeFile = {\n      status: \"uploaded\" as const,\n      asset: {\n        id: \"large\",\n        name: \"large.jpg\",\n        format: \"jpeg\",\n        type: \"image\",\n        createdAt: \"2024-01-02\",\n        projectId: \"test-project\",\n        size: 1000,\n      },\n    } as AssetContainer;\n\n    const result = sortAssets([largeFile, smallFile], {\n      sortBy: \"size\",\n      order: \"asc\",\n    });\n    expect(result.map((c) => c.asset.id)).toEqual([\"small\", \"large\"]);\n  });\n\n  test(\"sorts by size descending\", () => {\n    const smallFile = {\n      status: \"uploaded\" as const,\n      asset: {\n        id: \"small\",\n        name: \"small.jpg\",\n        format: \"jpeg\",\n        type: \"image\",\n        createdAt: \"2024-01-01\",\n        projectId: \"test-project\",\n        size: 100,\n      },\n    } as AssetContainer;\n\n    const largeFile = {\n      status: \"uploaded\" as const,\n      asset: {\n        id: \"large\",\n        name: \"large.jpg\",\n        format: \"jpeg\",\n        type: \"image\",\n        createdAt: \"2024-01-02\",\n        projectId: \"test-project\",\n        size: 1000,\n      },\n    } as AssetContainer;\n\n    const result = sortAssets([smallFile, largeFile], {\n      sortBy: \"size\",\n      order: \"desc\",\n    });\n    expect(result.map((c) => c.asset.id)).toEqual([\"large\", \"small\"]);\n  });\n\n  test(\"puts uploading assets at the end when sorting by size\", () => {\n    const uploadedFile = {\n      status: \"uploaded\" as const,\n      asset: {\n        id: \"uploaded\",\n        name: \"uploaded.jpg\",\n        format: \"jpeg\",\n        type: \"image\",\n        createdAt: \"2024-01-01\",\n        projectId: \"test-project\",\n        size: 500,\n      },\n    } as AssetContainer;\n\n    const uploadingFile = {\n      status: \"uploading\" as const,\n      asset: {\n        id: \"uploading\",\n        name: \"uploading.jpg\",\n        format: \"jpeg\",\n        type: \"image\",\n        projectId: \"test-project\",\n      },\n      objectURL: \"blob:...\",\n    } as AssetContainer;\n\n    const result = sortAssets([uploadingFile, uploadedFile], {\n      sortBy: \"size\",\n      order: \"asc\",\n    });\n    expect(result.map((c) => c.asset.id)).toEqual([\"uploaded\", \"uploading\"]);\n  });\n\n  test(\"is case-insensitive when sorting by name\", () => {\n    const containers = [\n      createAssetContainer(\"1\", \"Zebra.jpg\", \"jpeg\", \"image\", \"2024-01-01\"),\n      createAssetContainer(\"2\", \"apple.png\", \"png\", \"image\", \"2024-01-02\"),\n      createAssetContainer(\"3\", \"MANGO.jpg\", \"jpeg\", \"image\", \"2024-01-03\"),\n    ];\n\n    const result = sortAssets(containers, { sortBy: \"name\", order: \"asc\" });\n    expect(result.map((c) => c.asset.name)).toEqual([\n      \"apple.png\",\n      \"MANGO.jpg\",\n      \"Zebra.jpg\",\n    ]);\n  });\n\n  test(\"does not mutate original array\", () => {\n    const original = [...containers];\n    sortAssets(containers, { sortBy: \"name\", order: \"asc\" });\n    expect(containers).toEqual(original);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/asset-manager/utils.ts",
    "content": "import { matchSorter } from \"match-sorter\";\nimport type { AllowedFileExtension, Asset } from \"@webstudio-is/sdk\";\nimport {\n  acceptToMimePatterns,\n  doesAssetMatchMimePatterns,\n} from \"@webstudio-is/sdk\";\nimport type { AssetContainer } from \"../assets\";\n\nexport type SortField = \"name\" | \"size\" | \"createdAt\";\nexport type SortOrder = \"asc\" | \"desc\";\n\nexport type SortState = {\n  sortBy: SortField;\n  order: SortOrder;\n};\n\n/**\n * Safely gets the asset format as a lowercase extension.\n */\nexport const getAssetFormat = (\n  asset: Pick<Asset, \"format\">\n): AllowedFileExtension | undefined => {\n  return asset.format?.toLowerCase() as AllowedFileExtension | undefined;\n};\n\nconst gcd = (a: number, b: number): number => {\n  return b ? gcd(b, a % b) : a;\n};\n\nexport const getFormattedAspectRatio = ({\n  width,\n  height,\n}: {\n  width?: number;\n  height?: number;\n}): string => {\n  if (width === undefined || height === undefined) {\n    return \"\";\n  }\n  const divisor = gcd(width, height);\n\n  return `${width / divisor}:${height / divisor}`;\n};\n\n/**\n * Sort asset containers by field and order\n */\nexport const sortAssets = (\n  assetContainers: AssetContainer[],\n  sortState: SortState\n): AssetContainer[] => {\n  const { sortBy, order } = sortState;\n  const sorted = [...assetContainers];\n\n  sorted.sort((a, b) => {\n    let comparison = 0;\n\n    if (sortBy === \"name\") {\n      const aName = a.asset.name.toLowerCase();\n      const bName = b.asset.name.toLowerCase();\n      comparison = aName.localeCompare(bName);\n    } else if (sortBy === \"size\") {\n      const aSize =\n        a.status === \"uploaded\" ? a.asset.size : Number.MAX_SAFE_INTEGER;\n      const bSize =\n        b.status === \"uploaded\" ? b.asset.size : Number.MAX_SAFE_INTEGER;\n      comparison = aSize - bSize;\n    } else if (sortBy === \"createdAt\") {\n      const aCreated = a.status === \"uploaded\" ? a.asset.createdAt || \"\" : \"\";\n      const bCreated = b.status === \"uploaded\" ? b.asset.createdAt || \"\" : \"\";\n      comparison = new Date(aCreated).getTime() - new Date(bCreated).getTime();\n    }\n\n    return order === \"asc\" ? comparison : -comparison;\n  });\n\n  return sorted;\n};\n\n/**\n * Get initial selected extensions based on accept pattern\n */\nexport const getInitialExtensions = (\n  accept: string,\n  assetContainers: AssetContainer[]\n): AllowedFileExtension[] | \"*\" => {\n  const patterns = acceptToMimePatterns(accept);\n  if (patterns === \"*\") {\n    return \"*\";\n  }\n  const extensions: AllowedFileExtension[] = [];\n  assetContainers.forEach((container) => {\n    if (doesAssetMatchMimePatterns(container.asset, patterns)) {\n      const ext = getAssetFormat(container.asset);\n      if (ext !== undefined && !extensions.includes(ext)) {\n        extensions.push(ext);\n      }\n    }\n  });\n  return extensions.length > 0 ? extensions : \"*\";\n};\n\n/**\n * Calculate format counts for all assets\n */\nexport const calculateFormatCounts = (\n  assetContainers: AssetContainer[]\n): Partial<Record<AllowedFileExtension, number>> => {\n  const counts: Partial<Record<AllowedFileExtension, number>> = {};\n  assetContainers.forEach((container) => {\n    const ext = getAssetFormat(container.asset);\n    if (ext !== undefined) {\n      counts[ext] = (counts[ext] || 0) + 1;\n    }\n  });\n  return counts;\n};\n\n/**\n * Filter and sort asset containers\n */\nexport const filterAndSortAssets = ({\n  assetContainers,\n  selectedExtensions,\n  searchQuery,\n  sortState,\n}: {\n  assetContainers: AssetContainer[];\n  selectedExtensions: AllowedFileExtension[] | \"*\";\n  searchQuery: string;\n  sortState: SortState;\n}): AssetContainer[] => {\n  // Filter by selected extensions\n  let filtered = assetContainers;\n  if (selectedExtensions !== \"*\") {\n    filtered = assetContainers.filter((item) => {\n      const ext = getAssetFormat(item.asset);\n      return ext !== undefined && selectedExtensions.includes(ext);\n    });\n  }\n\n  // Apply search\n  if (searchQuery !== \"\") {\n    filtered = matchSorter(filtered, searchQuery, {\n      keys: [(item) => item.asset.name],\n    });\n  }\n\n  // Apply sorting\n  return sortAssets(filtered, sortState);\n};\n\n/**\n * Find index of asset container by asset id\n */\nexport const findAssetIndex = (\n  assetContainers: AssetContainer[],\n  assetId?: string\n): number => {\n  if (assetId === undefined) {\n    return -1;\n  }\n  return assetContainers.findIndex((item) => item.asset.id === assetId);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/asset-upload.test.ts",
    "content": "import { describe, test, expect, vi, afterEach } from \"vitest\";\nimport {\n  acceptFileTypeSpecifier,\n  validateFiles,\n  acceptUploadType,\n} from \"./asset-upload\";\n\nconst mockToast = vi.hoisted(() => ({\n  error: vi.fn(),\n}));\n\nvi.mock(\"@webstudio-is/design-system\", async (importOriginal) => {\n  const actual =\n    await importOriginal<typeof import(\"@webstudio-is/design-system\")>();\n  return {\n    ...actual,\n    toast: mockToast,\n  };\n});\n\ndescribe(\"validateFiles\", () => {\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  test(\"returns all files when under size limit\", () => {\n    const smallFile1 = new File([\"a\".repeat(1000)], \"small1.jpg\", {\n      type: \"image/jpeg\",\n    });\n    const smallFile2 = new File([\"b\".repeat(2000)], \"small2.jpg\", {\n      type: \"image/jpeg\",\n    });\n\n    const result = validateFiles([smallFile1, smallFile2]);\n\n    expect(result).toHaveLength(2);\n    expect(result).toContain(smallFile1);\n    expect(result).toContain(smallFile2);\n    expect(mockToast.error).not.toHaveBeenCalled();\n  });\n\n  test(\"filters out files exceeding size limit and shows toast error\", () => {\n    const smallFile = new File([\"a\".repeat(1000)], \"small.jpg\", {\n      type: \"image/jpeg\",\n    });\n    // Create a file object with a large size property (without actually creating the data)\n    const largeFile = new File([\"content\"], \"large.jpg\", {\n      type: \"image/jpeg\",\n    });\n    Object.defineProperty(largeFile, \"size\", {\n      value: 5 * 1024 * 1024, // 5MB (exceeds 4.5MB limit)\n    });\n\n    const result = validateFiles([smallFile, largeFile]);\n\n    expect(result).toHaveLength(1);\n    expect(result).toContain(smallFile);\n    expect(mockToast.error).toHaveBeenCalledWith(\n      'Asset \"large.jpg\" cannot be bigger than 4.5MB'\n    );\n  });\n\n  test(\"shows multiple toast errors for multiple large files\", () => {\n    const largeFile1 = new File([\"content1\"], \"large1.jpg\", {\n      type: \"image/jpeg\",\n    });\n    Object.defineProperty(largeFile1, \"size\", {\n      value: 5 * 1024 * 1024, // 5MB\n    });\n\n    const largeFile2 = new File([\"content2\"], \"large2.jpg\", {\n      type: \"image/jpeg\",\n    });\n    Object.defineProperty(largeFile2, \"size\", {\n      value: 5 * 1024 * 1024, // 5MB\n    });\n\n    const result = validateFiles([largeFile1, largeFile2]);\n\n    expect(result).toHaveLength(0);\n    expect(mockToast.error).toHaveBeenCalledTimes(2);\n    expect(mockToast.error).toHaveBeenCalledWith(\n      'Asset \"large1.jpg\" cannot be bigger than 4.5MB'\n    );\n    expect(mockToast.error).toHaveBeenCalledWith(\n      'Asset \"large2.jpg\" cannot be bigger than 4.5MB'\n    );\n  });\n});\n\ndescribe(\"acceptFileTypeSpecifier\", () => {\n  test(\"accepts wildcard *\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptFileTypeSpecifier(\"*\", jpegFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\"*\", pdfFile)).toBe(true);\n  });\n\n  test(\"accepts mime type wildcards like image/*\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pngFile = new File([], \"image.png\", { type: \"image/png\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptFileTypeSpecifier(\"image/*\", jpegFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\"image/*\", pngFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\"image/*\", pdfFile)).toBe(false);\n  });\n\n  test(\"accepts specific mime types\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pngFile = new File([], \"image.png\", { type: \"image/png\" });\n\n    expect(acceptFileTypeSpecifier(\"image/jpeg\", jpegFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\"image/jpeg\", pngFile)).toBe(false);\n  });\n\n  test(\"accepts file extensions\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pngFile = new File([], \"image.png\", { type: \"image/png\" });\n\n    expect(acceptFileTypeSpecifier(\".jpg\", jpegFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\".jpg\", pngFile)).toBe(false);\n  });\n\n  test(\"accepts multiple specifiers\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pngFile = new File([], \"image.png\", { type: \"image/png\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptFileTypeSpecifier(\"image/jpeg, image/png\", jpegFile)).toBe(\n      true\n    );\n    expect(acceptFileTypeSpecifier(\"image/jpeg, image/png\", pngFile)).toBe(\n      true\n    );\n    expect(acceptFileTypeSpecifier(\"image/jpeg, image/png\", pdfFile)).toBe(\n      false\n    );\n  });\n\n  test(\"handles mixed specifiers\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pngFile = new File([], \"image.png\", { type: \"image/png\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptFileTypeSpecifier(\".jpg, image/png\", jpegFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\".jpg, image/png\", pngFile)).toBe(true);\n    expect(acceptFileTypeSpecifier(\".jpg, image/png\", pdfFile)).toBe(false);\n  });\n\n  test(\"handles whitespace in specifiers\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n\n    expect(acceptFileTypeSpecifier(\" image/jpeg , image/png \", jpegFile)).toBe(\n      true\n    );\n  });\n});\n\ndescribe(\"acceptUploadType\", () => {\n  test(\"uses custom accept string when provided\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptUploadType(\"file\", \"image/*\", jpegFile)).toBe(true);\n    expect(acceptUploadType(\"file\", \"image/*\", pdfFile)).toBe(false);\n  });\n\n  test(\"uses acceptMap for asset type when accept is undefined\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptUploadType(\"image\", undefined, jpegFile)).toBe(true);\n    expect(acceptUploadType(\"image\", undefined, pdfFile)).toBe(false);\n  });\n\n  test(\"accepts all files when acceptMap has undefined for type\", () => {\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    expect(acceptUploadType(\"file\", undefined, jpegFile)).toBe(true);\n    expect(acceptUploadType(\"file\", undefined, pdfFile)).toBe(true);\n  });\n\n  test(\"accepts font files for font type\", () => {\n    const woffFile = new File([], \"font.woff\", { type: \"font/woff\" });\n    const jpegFile = new File([], \"image.jpg\", { type: \"image/jpeg\" });\n\n    expect(acceptUploadType(\"font\", undefined, woffFile)).toBe(true);\n    expect(acceptUploadType(\"font\", undefined, jpegFile)).toBe(false);\n  });\n\n  test(\"respects custom accept over default type mapping\", () => {\n    const pdfFile = new File([], \"document.pdf\", { type: \"application/pdf\" });\n\n    // Even though type is \"image\", custom accept=\"*\" should allow PDF\n    expect(acceptUploadType(\"image\", \"*\", pdfFile)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/asset-upload.tsx",
    "content": "import { type ChangeEvent, useRef } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { Button, Flex, Tooltip, toast } from \"@webstudio-is/design-system\";\nimport { UploadIcon } from \"@webstudio-is/icons\";\nimport { IMAGE_MIME_TYPES, detectAssetType } from \"@webstudio-is/sdk\";\nimport { MAX_UPLOAD_SIZE, toBytes } from \"@webstudio-is/asset-uploader\";\nimport type { AssetType } from \"@webstudio-is/asset-uploader\";\nimport { FONT_MIME_TYPES } from \"@webstudio-is/fonts\";\nimport { uploadAssets } from \"./upload-assets\";\nimport { $authPermit } from \"~/shared/nano-states\";\n\nconst maxSize = toBytes(MAX_UPLOAD_SIZE);\n\nexport const validateFiles = (files: File[]) => {\n  const exceedSizeFiles = files.filter((file) => file.size > maxSize);\n  for (const file of exceedSizeFiles) {\n    toast.error(\n      `Asset \"${file.name}\" cannot be bigger than ${MAX_UPLOAD_SIZE}MB`\n    );\n  }\n  return files.filter((file) => file.size <= maxSize);\n};\n\nconst useUpload = () => {\n  const inputRef = useRef<HTMLInputElement | null>(null);\n\n  const onChange = (event: ChangeEvent<HTMLFormElement>) => {\n    const form = event.currentTarget;\n    const input = inputRef.current;\n    if (input === null) {\n      return;\n    }\n    const files = validateFiles(Array.from(input?.files ?? []));\n\n    // Group files by their detected type\n    const filesByType = new Map<AssetType, File[]>();\n    for (const file of files) {\n      const detectedType = detectAssetType(file.name);\n      if (!filesByType.has(detectedType)) {\n        filesByType.set(detectedType, []);\n      }\n      filesByType.get(detectedType)!.push(file);\n    }\n\n    // Upload each group with the correct type\n    for (const [detectedType, filesOfType] of filesByType) {\n      uploadAssets(detectedType, filesOfType);\n    }\n\n    form.reset();\n  };\n\n  return { inputRef, onChange };\n};\n\nconst acceptMap = {\n  image: IMAGE_MIME_TYPES.join(\", \"),\n  font: FONT_MIME_TYPES,\n  video: undefined, // Videos can be uploaded through file type\n  // We allow everything by default for files, specific validation happens serverside\n  file: undefined,\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers\nexport const acceptFileTypeSpecifier = (specifiers: string, file: File) => {\n  const specifierArray = specifiers\n    .split(\",\")\n    .map((specifier) => specifier.trim());\n\n  return specifierArray.some((specifier) => {\n    if (specifier === \"*\") {\n      return true;\n    }\n\n    if (specifier.startsWith(\".\")) {\n      return file.name.endsWith(specifier);\n    }\n\n    if (specifier.endsWith(\"/*\")) {\n      const mimeCategory = specifier.slice(0, -2);\n      return file.type.startsWith(mimeCategory + \"/\");\n    }\n\n    return specifier === file.type;\n  });\n};\n\nexport const acceptUploadType = (\n  assetType: AssetType,\n  accept: string | undefined,\n  file: File\n) => {\n  if (accept !== undefined) {\n    return acceptFileTypeSpecifier(accept, file);\n  }\n\n  const accepted = acceptMap[assetType];\n  if (accepted === undefined) {\n    return true;\n  }\n\n  return acceptFileTypeSpecifier(accepted, file);\n};\n\ntype AssetUploadProps = {\n  type: AssetType;\n  accept?: string;\n};\n\nconst EnabledAssetUpload = ({ accept, type }: AssetUploadProps) => {\n  const { inputRef, onChange } = useUpload();\n\n  return (\n    <form onChange={onChange}>\n      <input\n        accept={accept ?? acceptMap[type]}\n        type=\"file\"\n        name={type}\n        multiple\n        ref={inputRef}\n        style={{ display: \"none\" }}\n      />\n      <Button\n        color=\"ghost\"\n        type=\"button\"\n        onClick={() => inputRef?.current?.click()}\n        prefix={<UploadIcon />}\n      ></Button>\n    </form>\n  );\n};\n\nexport const AssetUpload = ({ type, accept }: AssetUploadProps) => {\n  const authPermit = useStore($authPermit);\n\n  if (authPermit !== \"view\") {\n    // Split into a separate component to avoid using `useUpload` hook unnecessarily\n    // (It's hard to mock this hook in storybook)\n    return <EnabledAssetUpload type={type} accept={accept} />;\n  }\n\n  return (\n    <Flex>\n      <Tooltip side=\"bottom\" content=\"View mode. You can't upload assets.\">\n        <Button css={{ flexGrow: 1 }} prefix={<UploadIcon />} disabled={true}>\n          Upload\n        </Button>\n      </Tooltip>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/asset-utils.test.ts",
    "content": "import { expect, test, describe } from \"vitest\";\nimport {\n  parseAssetName,\n  formatAssetName,\n  getImageNameAndType,\n  getSha256Hash,\n  detectAssetType,\n  uploadingFileDataToAsset,\n} from \"./asset-utils\";\nimport type { Asset } from \"@webstudio-is/sdk\";\n\ndescribe(\"parseAssetName\", () => {\n  test(\"parses name with hash and extension\", () => {\n    expect(parseAssetName(\"hello_hash.ext\")).toEqual({\n      basename: \"hello\",\n      hash: \"hash\",\n      ext: \"ext\",\n    });\n  });\n\n  test(\"parses name without hash\", () => {\n    expect(parseAssetName(\"hello.ext\")).toEqual({\n      basename: \"hello\",\n      hash: \"\",\n      ext: \"ext\",\n    });\n  });\n\n  test(\"parses name with multiple underscores\", () => {\n    expect(parseAssetName(\"hello_hash1.ext_hash2\")).toEqual({\n      basename: \"hello\",\n      hash: \"hash1\",\n      ext: \"ext_hash2\",\n    });\n  });\n\n  test(\"parses name with hash but no extension\", () => {\n    expect(parseAssetName(\"hello_hash1_hash2\")).toEqual({\n      basename: \"hello_hash1\",\n      hash: \"hash2\",\n      ext: \"\",\n    });\n  });\n});\n\ndescribe(\"formatAssetName\", () => {\n  test(\"formats asset with filename\", () => {\n    const asset: Pick<Asset, \"name\" | \"filename\"> = {\n      name: \"uploaded_abc123.jpg\",\n      filename: \"myimage\",\n    };\n    expect(formatAssetName(asset)).toBe(\"myimage.jpg\");\n  });\n\n  test(\"formats asset without filename\", () => {\n    const asset: Pick<Asset, \"name\" | \"filename\"> = {\n      name: \"uploaded_abc123.jpg\",\n      filename: undefined,\n    };\n    expect(formatAssetName(asset)).toBe(\"uploaded.jpg\");\n  });\n\n  test(\"formats asset with no extension\", () => {\n    const asset: Pick<Asset, \"name\" | \"filename\"> = {\n      name: \"uploaded_abc123\",\n      filename: \"document\",\n    };\n    expect(formatAssetName(asset)).toBe(\"document.\");\n  });\n});\n\ndescribe(\"getImageNameAndType\", () => {\n  test(\"returns MIME type and filename for valid image\", () => {\n    const result = getImageNameAndType(\"photo.jpg\");\n    expect(result).toEqual([\"image/jpeg\", \"photo.jpg\"]);\n  });\n\n  test(\"handles different image extensions\", () => {\n    expect(getImageNameAndType(\"image.png\")).toEqual([\n      \"image/png\",\n      \"image.png\",\n    ]);\n    expect(getImageNameAndType(\"graphic.gif\")).toEqual([\n      \"image/gif\",\n      \"graphic.gif\",\n    ]);\n    expect(getImageNameAndType(\"vector.svg\")).toEqual([\n      \"image/svg+xml\",\n      \"vector.svg\",\n    ]);\n  });\n\n  test(\"is case-insensitive\", () => {\n    const result = getImageNameAndType(\"PHOTO.JPG\");\n    expect(result).toEqual([\"image/jpeg\", \"PHOTO.JPG\"]);\n  });\n\n  test(\"returns undefined for non-image files\", () => {\n    expect(getImageNameAndType(\"document.pdf\")).toBeUndefined();\n    expect(getImageNameAndType(\"video.mp4\")).toBeUndefined();\n    expect(getImageNameAndType(\"audio.mp3\")).toBeUndefined();\n  });\n\n  test(\"returns undefined for files without extension\", () => {\n    expect(getImageNameAndType(\"filename\")).toBeUndefined();\n  });\n\n  test(\"handles files with multiple dots\", () => {\n    const result = getImageNameAndType(\"my.photo.file.png\");\n    expect(result).toEqual([\"image/png\", \"my.photo.file.png\"]);\n  });\n});\n\ndescribe(\"getSha256Hash\", () => {\n  test(\"generates consistent hash for same input\", async () => {\n    const hash1 = await getSha256Hash(\"test string\");\n    const hash2 = await getSha256Hash(\"test string\");\n    expect(hash1).toBe(hash2);\n  });\n\n  test(\"generates different hashes for different inputs\", async () => {\n    const hash1 = await getSha256Hash(\"string1\");\n    const hash2 = await getSha256Hash(\"string2\");\n    expect(hash1).not.toBe(hash2);\n  });\n\n  test(\"generates 64 character hex string\", async () => {\n    const hash = await getSha256Hash(\"test\");\n    expect(hash).toMatch(/^[0-9a-f]{64}$/);\n  });\n\n  test(\"handles empty string\", async () => {\n    const hash = await getSha256Hash(\"\");\n    expect(hash).toMatch(/^[0-9a-f]{64}$/);\n  });\n\n  test(\"generates expected hash for known input\", async () => {\n    // SHA-256 of \"hello\" is known\n    const hash = await getSha256Hash(\"hello\");\n    expect(hash).toBe(\n      \"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824\"\n    );\n  });\n});\n\ndescribe(\"detectAssetType\", () => {\n  test(\"detects image files\", () => {\n    expect(detectAssetType(\"photo.jpg\")).toBe(\"image\");\n    expect(detectAssetType(\"image.png\")).toBe(\"image\");\n    expect(detectAssetType(\"graphic.gif\")).toBe(\"image\");\n    expect(detectAssetType(\"vector.svg\")).toBe(\"image\");\n    expect(detectAssetType(\"picture.webp\")).toBe(\"image\");\n  });\n\n  test(\"detects font files\", () => {\n    expect(detectAssetType(\"font.woff\")).toBe(\"font\");\n    expect(detectAssetType(\"font.woff2\")).toBe(\"font\");\n    expect(detectAssetType(\"font.ttf\")).toBe(\"font\");\n    expect(detectAssetType(\"font.otf\")).toBe(\"font\");\n  });\n\n  test(\"detects video files\", () => {\n    expect(detectAssetType(\"video.mp4\")).toBe(\"video\");\n    expect(detectAssetType(\"video.webm\")).toBe(\"video\");\n    expect(detectAssetType(\"video.mov\")).toBe(\"video\");\n    expect(detectAssetType(\"video.avi\")).toBe(\"video\");\n  });\n\n  test(\"returns file for other types\", () => {\n    expect(detectAssetType(\"document.pdf\")).toBe(\"file\");\n    expect(detectAssetType(\"audio.mp3\")).toBe(\"file\");\n    expect(detectAssetType(\"data.json\")).toBe(\"file\");\n    expect(detectAssetType(\"doc.docx\")).toBe(\"file\");\n  });\n\n  test(\"is case-insensitive\", () => {\n    expect(detectAssetType(\"PHOTO.JPG\")).toBe(\"image\");\n    expect(detectAssetType(\"FONT.WOFF2\")).toBe(\"font\");\n    expect(detectAssetType(\"VIDEO.MP4\")).toBe(\"video\");\n    expect(detectAssetType(\"DOC.PDF\")).toBe(\"file\");\n  });\n\n  test(\"handles files without extension\", () => {\n    expect(detectAssetType(\"filename\")).toBe(\"file\");\n  });\n\n  test(\"handles files with multiple dots\", () => {\n    expect(detectAssetType(\"my.photo.file.png\")).toBe(\"image\");\n    expect(detectAssetType(\"my.font.file.woff2\")).toBe(\"font\");\n    expect(detectAssetType(\"my.video.file.mp4\")).toBe(\"video\");\n    expect(detectAssetType(\"my.doc.file.pdf\")).toBe(\"file\");\n  });\n});\n\ndescribe(\"uploadingFileDataToAsset\", () => {\n  test(\"extracts format from MIME type for font with valid MIME\", () => {\n    const file = new File([\"content\"], \"InterVariable.woff2\", {\n      type: \"font/woff2\",\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"font\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"InterVariable.woff2\",\n      format: \"woff2\",\n      type: \"font\",\n    });\n  });\n\n  test(\"falls back to filename extension when MIME type is missing\", () => {\n    const file = new File([\"content\"], \"InterVariable.woff2\", {\n      type: \"\", // Empty MIME type\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"font\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"InterVariable.woff2\",\n      format: \"woff2\",\n      type: \"font\",\n    });\n  });\n\n  test(\"handles image files with valid MIME type\", () => {\n    const file = new File([\"content\"], \"photo.jpg\", {\n      type: \"image/jpeg\",\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"image\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"photo.jpg\",\n      format: \"jpeg\",\n      type: \"image\",\n    });\n  });\n\n  test(\"handles video files\", () => {\n    const file = new File([\"content\"], \"video.mp4\", {\n      type: \"video/mp4\",\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"video\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"video.mp4\",\n      format: \"mp4\",\n      type: \"file\",\n    });\n  });\n\n  test(\"handles generic file types\", () => {\n    const file = new File([\"content\"], \"document.pdf\", {\n      type: \"application/pdf\",\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"file\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"document.pdf\",\n      format: \"pdf\",\n      type: \"file\",\n    });\n  });\n\n  test(\"extracts format from filename when MIME type has no subtype\", () => {\n    const file = new File([\"content\"], \"font.ttf\", {\n      type: \"font\",\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"font\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"font.ttf\",\n      format: \"ttf\",\n      type: \"font\",\n    });\n  });\n\n  test(\"handles files with no extension\", () => {\n    const file = new File([\"content\"], \"README\", {\n      type: \"\",\n    });\n    const result = uploadingFileDataToAsset({\n      source: \"file\",\n      file,\n      assetId: \"test-id\",\n      type: \"file\",\n      objectURL: \"blob:test\",\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-id\",\n      name: \"README\",\n      format: \"\",\n      type: \"file\",\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/asset-utils.ts",
    "content": "import type {\n  Asset,\n  FontAsset,\n  ImageAsset,\n  AllowedFileExtension,\n} from \"@webstudio-is/sdk\";\nimport { nanoid } from \"nanoid\";\nimport {\n  getMimeTypeByExtension,\n  IMAGE_EXTENSIONS,\n  detectAssetType,\n  getAssetUrl,\n} from \"@webstudio-is/sdk\";\nimport type { UploadingFileData } from \"~/shared/nano-states\";\n\nexport { detectAssetType, getAssetUrl };\n\nexport const getImageNameAndType = (fileName: string) => {\n  // Extract extension from filename\n  const extractedExt = fileName.split(\".\").pop()?.toLowerCase();\n\n  if (!extractedExt) {\n    return;\n  }\n\n  // Check if it's a valid image extension\n  if (!IMAGE_EXTENSIONS.includes(extractedExt as AllowedFileExtension)) {\n    return;\n  }\n\n  return [getMimeTypeByExtension(extractedExt)!, fileName] as const;\n};\n\nconst extractImageNameAndMimeTypeFromUrl = (url: URL) => {\n  const nameFromPath = url.pathname\n    .split(\"/\")\n    .map(getImageNameAndType)\n    .filter(Boolean)[0];\n\n  if (nameFromPath != null) {\n    return nameFromPath;\n  }\n\n  const nameFromSearchParams = [...url.searchParams.values()]\n    .map(getImageNameAndType)\n    .filter(Boolean)[0];\n\n  if (nameFromSearchParams != null) {\n    return nameFromSearchParams;\n  }\n\n  // Any image format is suitable\n  const FALLBACK_URL_TYPE = \"image/png\";\n\n  return [FALLBACK_URL_TYPE, `${nanoid()}.png`] as const;\n};\n\nconst bufferToHex = (buffer: ArrayBuffer) => {\n  const byteArray = new Uint8Array(buffer);\n  return Array.from(byteArray, (byte) =>\n    byte.toString(16).padStart(2, \"0\")\n  ).join(\"\");\n};\n\nexport const getSha256Hash = async (data: string) => {\n  const encoder = new TextEncoder();\n  const dataBuffer = encoder.encode(data);\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", dataBuffer);\n  return bufferToHex(hashBuffer);\n};\n\nconst readFileAsArrayBuffer = (file: File): Promise<ArrayBuffer> =>\n  new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => resolve(reader.result as ArrayBuffer);\n    reader.onerror = () => reject(reader.error);\n    reader.readAsArrayBuffer(file);\n  });\n\nexport const getSha256HashOfFile = async (file: File) => {\n  const arrayBuffer = await readFileAsArrayBuffer(file);\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", arrayBuffer);\n  return bufferToHex(hashBuffer);\n};\n\nexport const getMimeType = (file: File | URL) => {\n  if (file instanceof File) {\n    return file.type;\n  }\n\n  return extractImageNameAndMimeTypeFromUrl(file)[0];\n};\n\nexport const getFileName = (file: File | URL) => {\n  if (file instanceof File) {\n    return file.name;\n  }\n\n  return extractImageNameAndMimeTypeFromUrl(file)[1];\n};\n\nexport const uploadingFileDataToAsset = (\n  fileData: UploadingFileData\n): Asset => {\n  const fileOrUrl =\n    fileData.source === \"file\" ? fileData.file : new URL(fileData.url);\n  const fileName = getFileName(fileOrUrl);\n  const mimeType = getMimeType(fileOrUrl);\n\n  // Extract format from MIME type if available, otherwise from filename extension\n  let format = mimeType.split(\"/\")[1];\n  if (!format) {\n    // Fallback to file extension if MIME type doesn't provide format\n    const match = fileName.match(/\\.([^.]+)$/);\n    format = match ? match[1].toLowerCase() : \"\";\n  }\n\n  const assetType = detectAssetType(fileName);\n\n  if (assetType === \"video\") {\n    // Videos should be file type, not image type\n    const asset: Asset = {\n      id: fileData.assetId,\n      name: fileName,\n      format,\n      type: \"file\",\n      description: \"\",\n      createdAt: \"\",\n      projectId: \"\",\n      size: 0,\n      meta: {},\n    };\n\n    return asset;\n  }\n\n  if (assetType === \"image\") {\n    const asset: ImageAsset = {\n      id: fileData.assetId,\n      name: fileName,\n      format,\n      type: \"image\",\n      description: \"\",\n      createdAt: \"\",\n      projectId: \"\",\n      size: 0,\n\n      meta: {\n        width: Number.NaN,\n        height: Number.NaN,\n      },\n    };\n\n    return asset;\n  }\n\n  if (assetType === \"font\") {\n    const asset: FontAsset = {\n      id: fileData.assetId,\n      name: fileName,\n      format: format as FontAsset[\"format\"],\n      type: \"font\",\n      description: \"\",\n      createdAt: \"\",\n      projectId: \"\",\n      size: 0,\n      meta: {\n        family: \"system\",\n        style: \"normal\",\n        weight: 400,\n      },\n    };\n\n    return asset;\n  }\n\n  // Default to file type for all other types (documents, code, audio, etc.)\n  const asset: Asset = {\n    id: fileData.assetId,\n    name: fileName,\n    format,\n    type: \"file\",\n    description: \"\",\n    createdAt: \"\",\n    projectId: \"\",\n    size: 0,\n    meta: {},\n  };\n\n  return asset;\n};\n\ntype ParsedAssetName = {\n  basename: string;\n  hash: string;\n  ext: string;\n};\n\nexport const parseAssetName = (name: string): ParsedAssetName => {\n  let hash = \"\";\n  let ext = \"\";\n  const lastDotAt = name.lastIndexOf(\".\");\n  if (lastDotAt > -1) {\n    ext = name.slice(lastDotAt + 1);\n    name = name.slice(0, lastDotAt);\n  }\n  const lastUnderscoreAt = name.lastIndexOf(\"_\");\n  if (lastUnderscoreAt > -1) {\n    hash = name.slice(lastUnderscoreAt + 1);\n    name = name.slice(0, lastUnderscoreAt);\n  }\n  return { basename: name, hash, ext };\n};\n\nexport const formatAssetName = (asset: Pick<Asset, \"name\" | \"filename\">) => {\n  const { basename, ext } = parseAssetName(asset.name);\n  const formattedName = `${asset.filename ?? basename}.${ext}`;\n  return formattedName;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/assets-shell.tsx",
    "content": "import {\n  useEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type JSX,\n} from \"react\";\nimport type { AssetType } from \"@webstudio-is/asset-uploader\";\nimport {\n  Flex,\n  ScrollArea,\n  SearchField,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { acceptUploadType, validateFiles } from \"./asset-upload\";\nimport { detectAssetType } from \"@webstudio-is/sdk\";\nimport { NotFound } from \"./not-found\";\nimport { Separator } from \"./separator\";\nimport { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\";\nimport {\n  containsFiles,\n  getFiles,\n} from \"@atlaskit/pragmatic-drag-and-drop/external/file\";\nimport { dropTargetForExternal } from \"@atlaskit/pragmatic-drag-and-drop/external/adapter\";\nimport invariant from \"tiny-invariant\";\nimport type { ContainsSource } from \"@atlaskit/pragmatic-drag-and-drop/dist/types/public-utils/external/native-types\";\nimport { uploadAssets } from \"./upload-assets\";\nimport { UploadIcon } from \"@webstudio-is/icons\";\nimport {\n  IDLE,\n  isBlockedByBackdrop,\n  registerDrop,\n  useExternalDragStateEffect,\n  type ExternalMonitorDragState,\n} from \"./drag-monitor\";\n\ntype AssetsShellProps = {\n  filters?: JSX.Element;\n  searchProps: ComponentProps<typeof SearchField>;\n  children: JSX.Element;\n  type: AssetType;\n  accept?: string;\n  isEmpty: boolean;\n};\n\nconst containsFilesOrUri = (parameter: ContainsSource) => {\n  return (\n    containsFiles(parameter) || parameter.source.types.includes(\"text/uri-list\")\n  );\n};\n\nconst OVER = 2;\ntype DropTargetState = typeof IDLE | typeof OVER;\n\nexport const AssetsShell = ({\n  filters,\n  searchProps,\n  isEmpty,\n  children,\n  type,\n  accept,\n}: AssetsShellProps) => {\n  const ref = useRef<HTMLDivElement>(null);\n  const [monitorState, setMonitorState] =\n    useState<ExternalMonitorDragState>(IDLE);\n\n  const [dropTargetState, setDropTargetState] = useState<DropTargetState>(IDLE);\n\n  useExternalDragStateEffect((state) => {\n    const element = ref.current;\n\n    if (element == null) {\n      return;\n    }\n\n    if (state === IDLE) {\n      setMonitorState(IDLE);\n      return;\n    }\n\n    if (isBlockedByBackdrop(element)) {\n      setMonitorState(IDLE);\n      return;\n    }\n\n    setMonitorState(state);\n  });\n\n  /**\n   * Allow URL drop for images only\n   */\n  const containsByType = type === \"image\" ? containsFilesOrUri : containsFiles;\n\n  useEffect(() => {\n    const element = ref.current;\n\n    invariant(element);\n\n    // Do not react if any dialog is opened above\n    const isBlockedByBackdropCallback = (\n      blocked: typeof containsByType,\n      notBlocked: typeof containsByType\n    ) => {\n      return (parameter: ContainsSource) => {\n        // Check if this element is the original element or its descendant\n        return isBlockedByBackdrop(element)\n          ? blocked(parameter)\n          : notBlocked(parameter);\n      };\n    };\n\n    return combine(\n      dropTargetForExternal({\n        element: element,\n        canDrop: isBlockedByBackdropCallback(() => false, containsByType),\n        onDragEnter: () => setDropTargetState(OVER),\n        onDragLeave: () => setDropTargetState(IDLE),\n        onDrop: async ({ source }) => {\n          registerDrop();\n          setMonitorState(IDLE);\n          setDropTargetState(IDLE);\n\n          const droppedUrls = await Promise.all(\n            source.items\n              .filter((item) => item.type === \"text/uri-list\")\n              .map(\n                (item) =>\n                  new Promise<URL>((resolve) =>\n                    item.getAsString((str) => resolve(new URL(str)))\n                  )\n              )\n          );\n\n          const droppedFiles = validateFiles(getFiles({ source }));\n\n          const files = droppedFiles\n            .filter((file) => file != null)\n            .filter((file) => {\n              if (acceptUploadType(type, accept, file)) {\n                return true;\n              }\n\n              console.warn(\n                `Unsupported file dropped for type=${type}, accept=${accept} and file.type=${file.type}, file.name=${file.name}`\n              );\n              return false;\n            });\n\n          // Group files by their detected type\n          const filesByType = new Map<string, File[]>();\n          for (const file of files) {\n            const detectedType = detectAssetType(file.name);\n            if (!filesByType.has(detectedType)) {\n              filesByType.set(detectedType, []);\n            }\n            filesByType.get(detectedType)!.push(file);\n          }\n\n          // Upload each group with the correct type\n          for (const [detectedType, filesOfType] of filesByType) {\n            uploadAssets(detectedType as AssetType, filesOfType);\n          }\n\n          uploadAssets(type, droppedUrls);\n        },\n      })\n    );\n  }, [accept, containsByType, type]);\n\n  const dragState = Math.max(monitorState, dropTargetState);\n\n  return (\n    <Flex\n      ref={ref}\n      direction=\"column\"\n      css={{\n        overflow: \"hidden\",\n        paddingBlock: theme.panel.paddingBlock,\n        flex: 1,\n        position: \"relative\",\n      }}\n    >\n      <Flex\n        css={{ padding: theme.panel.padding }}\n        gap=\"2\"\n        wrap=\"wrap\"\n        shrink={false}\n      >\n        <SearchField\n          css={{ flexGrow: 1 }}\n          {...searchProps}\n          autoFocus\n          placeholder=\"Search\"\n        />\n        {filters}\n      </Flex>\n      <Separator />\n      {isEmpty && <NotFound />}\n      <ScrollArea css={{ display: \"flex\", flexDirection: \"column\" }}>\n        {children}\n      </ScrollArea>\n      <Flex\n        css={{\n          position: \"absolute\",\n          inset: 0,\n          display: dragState !== IDLE ? \"flex\" : \"none\",\n          backgroundColor: theme.colors.backgroundPanel,\n          opacity: 0.85,\n          color:\n            dragState === OVER\n              ? theme.colors.foregroundMain\n              : theme.colors.foregroundSubtle,\n        }}\n      >\n        <Flex\n          align=\"center\"\n          justify=\"center\"\n          css={{\n            position: \"absolute\",\n            inset: theme.spacing[4],\n            border: `2px dashed ${dragState === OVER ? theme.colors.foregroundMain : theme.colors.foregroundMoreSubtle}`,\n          }}\n        >\n          <Flex align={\"center\"} gap={1}>\n            <UploadIcon />\n\n            <Text variant={\"regularBold\"}>Drop files here</Text>\n          </Flex>\n        </Flex>\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/delete-assets.ts",
    "content": "import type { Asset } from \"@webstudio-is/sdk\";\nimport { $assets } from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { onNextTransactionComplete } from \"~/shared/sync/project-queue\";\nimport { invalidateAssets } from \"~/shared/resources\";\n\nexport const deleteAssets = (assetIds: Asset[\"id\"][]) => {\n  serverSyncStore.createTransaction([$assets], (assets) => {\n    for (const assetId of assetIds) {\n      assets.delete(assetId);\n    }\n  });\n\n  // Wait for server to confirm transaction, then invalidate cache\n  onNextTransactionComplete(() => {\n    invalidateAssets();\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/drag-monitor.tsx",
    "content": "import { atom, computed } from \"nanostores\";\nimport { useEffect, useState } from \"react\";\nimport { monitorForExternal } from \"@atlaskit/pragmatic-drag-and-drop/external/adapter\";\nimport { preventUnhandled } from \"@atlaskit/pragmatic-drag-and-drop/prevent-unhandled\";\nimport { containsFiles } from \"@atlaskit/pragmatic-drag-and-drop/external/file\";\nimport type { ContainsSource } from \"@atlaskit/pragmatic-drag-and-drop/dist/types/public-utils/external/native-types\";\nimport { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\";\nimport { canvasApi } from \"~/shared/canvas-api\";\nimport { useDebouncedCallback } from \"use-debounce\";\n\nimport { $canvasIframeState } from \"~/shared/nano-states\";\nimport invariant from \"tiny-invariant\";\nimport { getAllElementsBoundingBox } from \"~/shared/dom-utils\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\n\nexport const IDLE = 0;\nexport const POTENTIAL = 1;\n\nexport type ExternalMonitorDragState = typeof IDLE | typeof POTENTIAL;\n\nconst $monitorDragState = atom<ExternalMonitorDragState>(IDLE);\nconst $monitorCanvasDragState = atom<ExternalMonitorDragState>(IDLE);\n\nconst $externalDragState = computed(\n  [$monitorDragState, $monitorCanvasDragState],\n  (monitorDragState, monitorCanvasDragState) => {\n    return Math.max(\n      monitorDragState,\n      monitorCanvasDragState\n    ) as ExternalMonitorDragState;\n  }\n);\n\nconst containsFilesOrUri = (parameter: ContainsSource) => {\n  return (\n    containsFiles(parameter) || parameter.source.types.includes(\"text/uri-list\")\n  );\n};\n\nlet usageCounter = 0;\n\nexport const ExternalDragDropMonitor = () => {\n  const [refresh, setRefresh] = useState(0);\n\n  const handleBuilderOnDrop = useDebouncedCallback(() => {\n    $monitorDragState.set(IDLE);\n  }, 300);\n\n  const handleCanvasOnDrop = useDebouncedCallback(() => {\n    $monitorCanvasDragState.set(IDLE);\n  }, 300);\n\n  const preventUnhandledStop = useDebouncedCallback(() => {\n    preventUnhandled.stop();\n    canvasApi.preventUnhandled.stop();\n  }, 300);\n\n  useEffect(() => {\n    usageCounter += 1;\n\n    invariant(usageCounter === 1, \"Monitor can be used only once per app\");\n\n    return () => {\n      usageCounter -= 1;\n    };\n  }, []);\n\n  useEffect(() => {\n    if (false === canvasApi.isInitialized()) {\n      return $canvasIframeState.listen((state) => {\n        if (state === \"ready\") {\n          setRefresh((prev) => prev + 1);\n        }\n      });\n    }\n\n    const preventUnhandledStart = () => {\n      preventUnhandled.start();\n      canvasApi.preventUnhandled.start();\n    };\n\n    return combine(\n      monitorForExternal({\n        canMonitor: containsFilesOrUri,\n        onDragStart: () => {\n          preventUnhandledStart();\n          $monitorDragState.set(POTENTIAL);\n          handleBuilderOnDrop.cancel();\n          preventUnhandledStop.cancel();\n        },\n        onDrop: () => {\n          handleBuilderOnDrop();\n          preventUnhandledStop();\n        },\n      }),\n      canvasApi.monitorForExternal({\n        canMonitor: containsFilesOrUri,\n        onDragStart: () => {\n          preventUnhandledStart();\n          $monitorCanvasDragState.set(POTENTIAL);\n          handleCanvasOnDrop.cancel();\n          preventUnhandledStop.cancel();\n        },\n        onDrop: () => {\n          handleCanvasOnDrop();\n          preventUnhandledStop();\n        },\n      }),\n      () => {\n        return () => {\n          handleBuilderOnDrop.cancel();\n          preventUnhandledStop.cancel();\n\n          preventUnhandled.stop();\n          canvasApi.preventUnhandled.stop();\n          $monitorDragState.set(IDLE);\n          $monitorCanvasDragState.set(IDLE);\n        };\n      }\n    );\n  }, [handleBuilderOnDrop, handleCanvasOnDrop, preventUnhandledStop, refresh]);\n\n  return null;\n};\n\nexport const useExternalDragStateEffect = (\n  callback: (state: ExternalMonitorDragState) => void\n) => {\n  const handleCallback = useEffectEvent(callback);\n\n  useEffect(() => {\n    return $externalDragState.subscribe(handleCallback);\n  }, []);\n};\n\nconst dropCount = atom(0);\n\nexport const registerDrop = () => {\n  dropCount.set(dropCount.get() + 1);\n};\n\nexport const useOnDropEffect = (callback: () => void) => {\n  const handleCallback = useEffectEvent(callback);\n\n  useEffect(() => {\n    return dropCount.listen(handleCallback);\n  }, []);\n};\n\nexport const isBlockedByBackdrop = (element: Element) => {\n  const elementRect = getAllElementsBoundingBox([element]);\n  const centerX = elementRect.left + elementRect.width / 2;\n  const centerY = elementRect.top + elementRect.height / 2;\n\n  // Get the element directly under the center of the target element\n  const topElement = document.elementFromPoint(centerX, centerY);\n  const isNotBlocked = element.contains(topElement) || topElement === element;\n\n  return false === isNotBlocked;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/index.ts",
    "content": "export { useAssets } from \"./use-assets\";\nexport * from \"./delete-assets\";\nexport { uploadAssets } from \"./upload-assets\";\nexport * from \"./types\";\nexport * from \"./separator\";\nexport * from \"./assets-shell\";\nexport * from \"./asset-upload\";\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/not-found.tsx",
    "content": "import { Flex, Text } from \"@webstudio-is/design-system\";\n\nexport const NotFound = () => {\n  return (\n    <Flex align=\"center\" justify=\"center\" css={{ height: 100 }}>\n      <Text>No matching assets</Text>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/separator.tsx",
    "content": "import {\n  Separator as SeparatorPrimitive,\n  styled,\n} from \"@webstudio-is/design-system\";\nimport { theme } from \"@webstudio-is/design-system\";\n\nexport const Separator = styled(SeparatorPrimitive, {\n  marginTop: theme.spacing[3],\n  marginBottom: theme.spacing[5],\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/types.ts",
    "content": "import type { Asset } from \"@webstudio-is/sdk\";\n\ntype PreviewAsset = Pick<\n  Asset,\n  \"name\" | \"filename\" | \"id\" | \"format\" | \"description\" | \"type\"\n>;\n\nexport type UploadedAssetContainer = {\n  status: \"uploaded\";\n  asset: Asset;\n};\n\nexport type UploadingAssetContainer = {\n  status: \"uploading\";\n  objectURL: string;\n  asset: PreviewAsset;\n};\n\n/**\n * Assets that can be shown in the UI\n */\nexport type AssetContainer = UploadedAssetContainer | UploadingAssetContainer;\n\nexport type AssetActionResponse = {\n  uploadedAssets?: Array<Asset>;\n  deletedAssets?: Array<Asset>;\n  errors?: string;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/upload-assets.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { __testing__ } from \"./upload-assets\";\n\nconst { deduplicateAssetName } = __testing__;\n\ndescribe(\"upload-assets\", () => {\n  describe(\"deduplicateAssetName\", () => {\n    test(\"returns original name when no duplicates exist\", () => {\n      const existingNames = new Set([\"other-file.png\", \"another-file.jpg\"]);\n      const result = deduplicateAssetName(\"unique-file.png\", existingNames);\n      expect(result).toBe(\"unique-file.png\");\n    });\n\n    test(\"adds suffix when duplicate exists\", () => {\n      const existingNames = new Set([\"duplicate.png\"]);\n      const result = deduplicateAssetName(\"duplicate.png\", existingNames);\n      expect(result).toBe(\"duplicate_1.png\");\n    });\n\n    test(\"increments suffix for multiple duplicates\", () => {\n      const existingNames = new Set([\"file.png\", \"file_1.png\", \"file_2.png\"]);\n      const result = deduplicateAssetName(\"file.png\", existingNames);\n      expect(result).toBe(\"file_3.png\");\n    });\n\n    test(\"handles names without extension\", () => {\n      const existingNames = new Set<string>();\n      const result = deduplicateAssetName(\"no-extension\", existingNames);\n      expect(result).toBe(\"no-extension\");\n    });\n\n    test(\"handles empty existing names set\", () => {\n      const existingNames = new Set<string>();\n      const result = deduplicateAssetName(\"file.jpg\", existingNames);\n      expect(result).toBe(\"file.jpg\");\n    });\n\n    test(\"handles complex file extensions\", () => {\n      const existingNames = new Set([\"archive.tar.gz\"]);\n      const result = deduplicateAssetName(\"archive.tar.gz\", existingNames);\n      expect(result).toBe(\"archive.tar_1.gz\");\n    });\n\n    test(\"finds first available suffix with gaps\", () => {\n      const existingNames = new Set([\"file.png\", \"file_2.png\", \"file_3.png\"]);\n      const result = deduplicateAssetName(\"file.png\", existingNames);\n      expect(result).toBe(\"file_1.png\");\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/upload-assets.tsx",
    "content": "import warnOnce from \"warn-once\";\nimport invariant from \"tiny-invariant\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport type { AssetType } from \"@webstudio-is/asset-uploader\";\nimport { Box, toast, css, theme } from \"@webstudio-is/design-system\";\nimport { sanitizeS3Key } from \"@webstudio-is/asset-uploader\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport { restAssetsUploadPath, restAssetsPath } from \"~/shared/router-utils\";\nimport { fetch } from \"~/shared/fetch.client\";\nimport type { AssetActionResponse } from \"~/builder/shared/assets\";\nimport {\n  $assets,\n  $authToken,\n  $project,\n  $uploadingFilesDataStore,\n  type UploadingFileData,\n} from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { onNextTransactionComplete } from \"~/shared/sync/project-queue\";\nimport { invalidateAssets } from \"~/shared/resources\";\nimport {\n  formatAssetName,\n  getFileName,\n  getMimeType,\n  getSha256Hash,\n  getSha256HashOfFile,\n} from \"./asset-utils\";\n\nconst safeDeleteAssets = (assetIds: Asset[\"id\"][], projectId: string) => {\n  const currentProjectId = $project.get()?.id;\n\n  if (currentProjectId !== projectId) {\n    toast.error(\"Project has been changed, files will not be uploaded\");\n    // Can cause data corruption\n    return;\n  }\n\n  serverSyncStore.createTransaction([$assets], (assets) => {\n    for (const assetId of assetIds) {\n      assets.delete(assetId);\n    }\n  });\n\n  onNextTransactionComplete(() => {\n    invalidateAssets();\n  });\n};\n\nconst safeSetAsset = (asset: Asset, projectId: string) => {\n  const currentProjectId = $project.get()?.id;\n\n  if (currentProjectId !== projectId) {\n    toast.error(\"Project has been changed, files will not be uploaded\");\n    // Can cause data corrupiton\n    return;\n  }\n\n  serverSyncStore.createTransaction([$assets], (assets) => {\n    assets.set(asset.id, asset);\n  });\n\n  onNextTransactionComplete(() => {\n    invalidateAssets();\n  });\n};\n\nconst getFilesData = async <T extends File | URL>(\n  type: AssetType,\n  filesOrUrls: T[]\n): Promise<UploadingFileData[]> => {\n  const filesData: UploadingFileData[] = [];\n  for (const fileOrUrl of filesOrUrls) {\n    if (fileOrUrl instanceof File) {\n      const assetId = await getSha256HashOfFile(fileOrUrl);\n      filesData.push({\n        source: \"file\" as const,\n        assetId: assetId,\n        type,\n        file: fileOrUrl,\n        objectURL: URL.createObjectURL(fileOrUrl),\n      });\n      continue;\n    }\n\n    const assetId = await getSha256Hash(fileOrUrl.href);\n    filesData.push({\n      source: \"url\" as const,\n      assetId,\n      type,\n      url: fileOrUrl.href,\n      objectURL: fileOrUrl.href,\n    });\n  }\n\n  return filesData;\n};\n\nconst addUploadingFilesData = (filesData: UploadingFileData[]) => {\n  const uploadingFilesData = $uploadingFilesDataStore.get();\n  $uploadingFilesDataStore.set([...uploadingFilesData, ...filesData]);\n};\n\nconst deleteUploadingFileData = (id: UploadingFileData[\"assetId\"]) => {\n  const uploadingFilesData = $uploadingFilesDataStore.get();\n  $uploadingFilesDataStore.set(\n    uploadingFilesData.filter((fileData) => fileData.assetId !== id)\n  );\n};\n\nconst getVideoDimensions = async (file: File) => {\n  return new Promise<{ width: number; height: number }>((resolve, reject) => {\n    const url = URL.createObjectURL(file);\n    const vid = document.createElement(\"video\");\n    vid.preload = \"metadata\";\n    vid.src = url;\n\n    vid.onloadedmetadata = () => {\n      URL.revokeObjectURL(url);\n      resolve({ width: vid.videoWidth, height: vid.videoHeight });\n    };\n    vid.onerror = () => {\n      URL.revokeObjectURL(url);\n      reject(new Error(\"Invalid video file\"));\n    };\n  });\n};\n\nconst deduplicateAssetName = (name: string, existingNames: Set<string>) => {\n  // eslint-disable-next-line no-constant-condition\n  for (let index = 0; true; index += 1) {\n    const suffix = index === 0 ? \"\" : `_${index}`;\n    const lastDotAt = name.lastIndexOf(\".\");\n    if (lastDotAt === -1) {\n      return name;\n    }\n    const basename = name.slice(0, lastDotAt);\n    const ext = name.slice(lastDotAt);\n    const nameWithSuffix = basename + suffix + ext;\n    if (!existingNames.has(nameWithSuffix)) {\n      return nameWithSuffix;\n    }\n  }\n};\n\nconst uploadAsset = async ({\n  authToken,\n  projectId,\n  fileOrUrl,\n  assetType,\n  onCompleted,\n  onError,\n}: {\n  authToken: undefined | string;\n  projectId: string;\n  fileOrUrl: File | URL;\n  assetType: AssetType;\n  onCompleted: (data: AssetActionResponse) => void;\n  onError: (error: string) => void;\n}) => {\n  try {\n    const mimeType = getMimeType(fileOrUrl);\n    const fileName = getFileName(fileOrUrl);\n\n    const metaFormData = new FormData();\n    metaFormData.append(\"projectId\", projectId);\n    metaFormData.append(\"type\", assetType);\n    // sanitizeS3Key here is just because of https://github.com/remix-run/remix/issues/4443\n    // should be removed after fix\n    const existingNames = new Set<string>();\n    for (const asset of $assets.get().values()) {\n      existingNames.add(formatAssetName(asset));\n    }\n    metaFormData.append(\n      \"filename\",\n      deduplicateAssetName(sanitizeS3Key(fileName), existingNames)\n    );\n\n    const authHeaders = new Headers();\n    if (authToken !== undefined) {\n      authHeaders.set(\"x-auth-token\", authToken);\n    }\n\n    const metaResponse = await fetch(restAssetsPath(), {\n      method: \"POST\",\n      body: metaFormData,\n      headers: authHeaders,\n    });\n\n    const metaData: { name: string } | { errors: string } =\n      await metaResponse.json();\n\n    if (\"errors\" in metaData) {\n      throw Error(metaData.errors);\n    }\n\n    const body =\n      fileOrUrl instanceof File\n        ? fileOrUrl\n        : JSON.stringify({ url: fileOrUrl.href });\n\n    const headers = new Headers(authHeaders);\n\n    if (fileOrUrl instanceof URL) {\n      headers.set(\"Content-Type\", \"application/json\");\n    }\n\n    let width = undefined;\n    let height = undefined;\n\n    if (mimeType.startsWith(\"video/\") && fileOrUrl instanceof File) {\n      const videoSize = await getVideoDimensions(fileOrUrl);\n      width = videoSize.width;\n      height = videoSize.height;\n    }\n\n    const uploadResponse = await fetch(\n      restAssetsUploadPath({ name: metaData.name, width, height }),\n      {\n        method: \"POST\",\n        body,\n        headers,\n      }\n    );\n\n    const uploadData: AssetActionResponse = await uploadResponse.json();\n\n    if (\"errors\" in uploadData) {\n      throw Error(uploadData.errors);\n    }\n\n    onCompleted(uploadData);\n  } catch (error) {\n    if (error instanceof Error) {\n      onError(error.message);\n    }\n  }\n};\n\nconst handleAfterSubmit = (\n  assetId: string,\n  data: AssetActionResponse,\n  projectId: string\n) => {\n  warnOnce(\n    data.uploadedAssets?.length !== 1,\n    \"Expected exactly 1 uploaded asset\"\n  );\n\n  const uploadedAsset = data.uploadedAssets?.[0];\n\n  if (uploadedAsset === undefined) {\n    warnOnce(true, \"An uploaded asset is undefined\");\n    toast.error(\"Could not upload an asset\");\n    safeDeleteAssets([assetId], projectId);\n    return;\n  }\n\n  // update store with new asset and set current id\n  safeSetAsset({ ...uploadedAsset, id: assetId }, projectId);\n};\n\nconst imageWidth = css({\n  maxWidth: \"100%\",\n});\n\nconst ToastImageInfo = ({ objectURL }: { objectURL: string }) => {\n  return (\n    <Box css={{ width: theme.spacing[18] }}>\n      <Image\n        className={imageWidth()}\n        src={objectURL}\n        optimize={false}\n        width={64}\n        loader={wsImageLoader}\n      />\n    </Box>\n  );\n};\n\nconst processingQueue: [\n  filesData: UploadingFileData[],\n  projectId: string,\n  authToken: string | undefined,\n][] = [];\n\nconst processUpload = async (\n  filesData: UploadingFileData[],\n  projectId: string,\n  authToken: string | undefined\n) => {\n  processingQueue.push([filesData, projectId, authToken]);\n\n  if (processingQueue.length > 1) {\n    return;\n  }\n\n  while (processingQueue.length > 0) {\n    const [filesData, projectId, authToken] = processingQueue.shift()!;\n\n    const currentProjectId = $project.get()?.id;\n    if (currentProjectId !== projectId) {\n      toast.error(\"Project has been changed, files will not be uploaded\");\n      // Can cause data corrupiton\n      continue;\n    }\n\n    for (const fileData of filesData) {\n      const assetId = fileData.assetId;\n\n      if ($assets.get().has(assetId)) {\n        toast.info(\"Asset already exists\", {\n          icon: <ToastImageInfo objectURL={fileData.objectURL} />,\n        });\n\n        deleteUploadingFileData(assetId);\n        continue;\n      }\n\n      await uploadAsset({\n        authToken,\n        projectId,\n        fileOrUrl:\n          fileData.source === \"file\" ? fileData.file : new URL(fileData.url),\n        assetType: fileData.type,\n        onCompleted: (data) => {\n          URL.revokeObjectURL(fileData.objectURL);\n          deleteUploadingFileData(assetId);\n          handleAfterSubmit(assetId, data, projectId);\n        },\n        onError: (error) => {\n          deleteUploadingFileData(assetId);\n          toast.error(error, {\n            icon: <ToastImageInfo objectURL={fileData.objectURL} />,\n          });\n\n          safeDeleteAssets([assetId], projectId);\n        },\n      });\n    }\n  }\n};\n\nexport const uploadAssets = async <T extends File | URL>(\n  type: AssetType,\n  filesOrUrls: T[]\n): Promise<Map<T, string>> => {\n  const projectId = $project.get()?.id;\n  const authToken = $authToken.get();\n  if (projectId === undefined) {\n    return new Map();\n  }\n\n  const filesData = await getFilesData(type, filesOrUrls);\n\n  // Filter out duplicates inside filesData\n  const uniqFilesDataMap = new Map(\n    filesData.map((fileData) => [fileData.assetId, fileData])\n  );\n\n  // Filter out duplicates existing in assets or uploading files\n  const existingIds = [\n    ...$assets.get().keys(),\n    ...$uploadingFilesDataStore.get().map((fileData) => fileData.assetId),\n  ];\n\n  for (const existingAssetId of existingIds) {\n    if (uniqFilesDataMap.has(existingAssetId)) {\n      const fileData = uniqFilesDataMap.get(existingAssetId)!;\n      uniqFilesDataMap.delete(existingAssetId);\n      toast.info(\"Asset already exists\", {\n        icon: <ToastImageInfo objectURL={fileData.objectURL} />,\n      });\n    }\n  }\n\n  const uniqFilesData = [...uniqFilesDataMap.values()];\n\n  addUploadingFilesData(uniqFilesData);\n\n  processUpload(uniqFilesData, projectId, authToken);\n\n  const res = new Map();\n\n  for (let i = 0; i < filesData.length; ++i) {\n    const fileOrUrl = filesOrUrls[i];\n    const fileData = filesData[i];\n\n    invariant(\n      fileOrUrl instanceof URL ||\n        (fileOrUrl instanceof File &&\n          fileData.source === \"file\" &&\n          fileData.file === fileOrUrl)\n    );\n    invariant(\n      fileOrUrl instanceof File ||\n        (fileOrUrl instanceof URL &&\n          fileData.source === \"url\" &&\n          fileData.url === fileOrUrl.href)\n    );\n\n    res.set(filesOrUrls[i], filesData[i].assetId);\n  }\n\n  return res;\n};\n\nexport const __testing__ = {\n  deduplicateAssetName,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/use-assets.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { AssetContainer, UploadedAssetContainer } from \"./types\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport { __testing__ } from \"./use-assets\";\n\nconst { filterByType } = __testing__;\n\ndescribe(\"use-assets\", () => {\n  describe(\"filterByType\", () => {\n    const createImageAsset = (): Asset => ({\n      id: \"image-id\",\n      type: \"image\",\n      name: \"image-name\",\n      format: \"png\",\n      size: 1000,\n      meta: { width: 100, height: 100 },\n      createdAt: \"2024-01-01\",\n      projectId: \"project-id\",\n    });\n\n    const createFontAsset = (): Asset => ({\n      id: \"font-id\",\n      type: \"font\",\n      name: \"font-name\",\n      format: \"woff2\",\n      size: 1000,\n      meta: { family: \"TestFont\", style: \"normal\", weight: 400 },\n      createdAt: \"2024-01-01\",\n      projectId: \"project-id\",\n    });\n\n    const createFileAsset = (): Asset => ({\n      id: \"file-id\",\n      type: \"file\",\n      name: \"file-name\",\n      format: \"pdf\",\n      size: 1000,\n      meta: {},\n      createdAt: \"2024-01-01\",\n      projectId: \"project-id\",\n    });\n\n    const createUploadedContainer = (asset: Asset): UploadedAssetContainer => ({\n      status: \"uploaded\",\n      asset,\n    });\n\n    test(\"returns all containers when type is undefined\", () => {\n      const containers: AssetContainer[] = [\n        createUploadedContainer(createImageAsset()),\n        createUploadedContainer(createFontAsset()),\n        createUploadedContainer(createFileAsset()),\n      ];\n\n      const result = filterByType(containers, undefined);\n\n      expect(result).toEqual(containers);\n      expect(result).toHaveLength(3);\n    });\n\n    test(\"filters containers by image type\", () => {\n      const imageContainer = createUploadedContainer(createImageAsset());\n      const fontContainer = createUploadedContainer(createFontAsset());\n      const fileContainer = createUploadedContainer(createFileAsset());\n      const containers: AssetContainer[] = [\n        imageContainer,\n        fontContainer,\n        fileContainer,\n      ];\n\n      const result = filterByType(containers, \"image\");\n\n      expect(result).toEqual([imageContainer]);\n      expect(result).toHaveLength(1);\n    });\n\n    test(\"filters containers by font type\", () => {\n      const imageContainer = createUploadedContainer(createImageAsset());\n      const fontContainer = createUploadedContainer(createFontAsset());\n      const containers: AssetContainer[] = [imageContainer, fontContainer];\n\n      const result = filterByType(containers, \"font\");\n\n      expect(result).toEqual([fontContainer]);\n      expect(result).toHaveLength(1);\n    });\n\n    test(\"returns empty array when no containers match type\", () => {\n      const imageContainer = createUploadedContainer(createImageAsset());\n      const containers: AssetContainer[] = [imageContainer];\n\n      const result = filterByType(containers, \"font\");\n\n      expect(result).toEqual([]);\n      expect(result).toHaveLength(0);\n    });\n\n    test(\"returns empty array when given empty array\", () => {\n      const result = filterByType([], \"image\");\n\n      expect(result).toEqual([]);\n      expect(result).toHaveLength(0);\n    });\n\n    test(\"filters multiple containers of same type\", () => {\n      const imageContainer1 = createUploadedContainer(createImageAsset());\n      const imageAsset2: Asset = {\n        ...createImageAsset(),\n        id: \"image-id-2\",\n      };\n      const imageContainer2 = createUploadedContainer(imageAsset2);\n      const fontContainer = createUploadedContainer(createFontAsset());\n      const containers: AssetContainer[] = [\n        imageContainer1,\n        fontContainer,\n        imageContainer2,\n      ];\n\n      const result = filterByType(containers, \"image\");\n\n      expect(result).toHaveLength(2);\n      expect(result).toEqual([imageContainer1, imageContainer2]);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/assets/use-assets.tsx",
    "content": "import { useMemo } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport { $assets, $uploadingFilesDataStore } from \"~/shared/nano-states\";\nimport type {\n  AssetContainer,\n  UploadedAssetContainer,\n  UploadingAssetContainer,\n} from \"./types\";\nimport { uploadingFileDataToAsset } from \"./asset-utils\";\n\nconst $assetContainers = computed(\n  [$assets, $uploadingFilesDataStore],\n  (assets, uploadingFilesData) => {\n    const uploadingContainers: UploadingAssetContainer[] = [];\n\n    for (const uploadingFile of uploadingFilesData) {\n      uploadingContainers.push({\n        status: \"uploading\",\n        objectURL: uploadingFile.objectURL,\n        asset: uploadingFileDataToAsset(uploadingFile),\n      });\n    }\n\n    const uploadedContainers: UploadedAssetContainer[] = [];\n\n    for (const asset of assets.values()) {\n      uploadedContainers.push({\n        status: \"uploaded\",\n        asset,\n      });\n    }\n\n    // sort newest uploaded assets first\n    uploadedContainers.sort(\n      (leftContainer, rightContainer) =>\n        new Date(rightContainer.asset.createdAt).getTime() -\n        new Date(leftContainer.asset.createdAt).getTime()\n    );\n\n    // put uploading assets first\n    return [...uploadingContainers, ...uploadedContainers];\n  }\n);\n\nconst filterByType = (\n  assetContainers: AssetContainer[],\n  type: Asset[\"type\"] | undefined\n) => {\n  if (type === undefined) {\n    return assetContainers;\n  }\n  return assetContainers.filter((assetContainer) => {\n    return assetContainer.asset.type === type;\n  });\n};\n\nexport const useAssets = (type?: Asset[\"type\"]) => {\n  const assetContainers = useStore($assetContainers);\n\n  const assetsByType = useMemo(() => {\n    return filterByType(assetContainers, type);\n  }, [assetContainers, type]);\n\n  return {\n    /**\n     * Already loaded assets or assets that are being uploaded\n     */\n    assetContainers: assetsByType,\n  };\n};\n\nexport const __testing__ = {\n  filterByType,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/binding-popover.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { encodeDataSourceVariable } from \"@webstudio-is/sdk\";\nimport { evaluateExpressionWithinScope } from \"./binding-popover\";\n\ntest(\"evaluateExpressionWithinScope works\", () => {\n  const variableName = \"jsonVariable\";\n  const encVariableName = encodeDataSourceVariable(variableName);\n  const variableValue = 1;\n\n  expect(\n    evaluateExpressionWithinScope(`${encVariableName} + ${encVariableName}`, {\n      [encVariableName]: variableValue,\n    })\n  ).toEqual(2);\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/binding-popover.tsx",
    "content": "import {\n  type ButtonHTMLAttributes,\n  forwardRef,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  DotIcon,\n  InfoCircleIcon,\n  PlusIcon,\n  ResetIcon,\n  TrashIcon,\n} from \"@webstudio-is/icons\";\nimport {\n  Box,\n  Button,\n  CssValueListArrowFocus,\n  CssValueListItem,\n  DialogTitleActions,\n  DialogClose,\n  DialogTitle,\n  Flex,\n  FloatingPanel,\n  Label,\n  ScrollArea,\n  SmallIconButton,\n  Text,\n  Tooltip,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport {\n  decodeDataSourceVariable,\n  getExpressionIdentifiers,\n  lintExpression,\n} from \"@webstudio-is/sdk\";\nimport { $dataSourceVariables, $isDesignMode } from \"~/shared/nano-states\";\nimport {\n  computeExpression,\n  encodeDataVariableName,\n} from \"~/shared/data-variables\";\nimport {\n  ExpressionEditor,\n  formatValuePreview,\n  type EditorApi,\n} from \"./expression-editor\";\n\n/**\n * Check if a value is a primitive that can be safely stringified.\n * Allows: string, number, boolean, null, undefined\n * Rejects: object, array, function, symbol\n */\nexport const isPrimitiveValue = (value: unknown): boolean => {\n  if (value === null || value === undefined) {\n    return true;\n  }\n  const type = typeof value;\n  return type === \"string\" || type === \"number\" || type === \"boolean\";\n};\n\n/**\n * Generate a validation error message for non-primitive values.\n * @param label - The control label (e.g., \"Title\", \"URL\")\n * @returns Error message or undefined if value is valid\n */\nexport const validatePrimitiveValue = (\n  value: unknown,\n  label: string\n): string | undefined => {\n  if (!isPrimitiveValue(value)) {\n    return `${label} expects a primitive value (string, number, boolean, null, or undefined), not an object, array, or function`;\n  }\n};\n\nexport const evaluateExpressionWithinScope = (\n  expression: string,\n  scope: Record<string, unknown>\n) => {\n  const variables = new Map<string, unknown>();\n  for (const [name, value] of Object.entries(scope)) {\n    const decodedName = decodeDataSourceVariable(name);\n    if (decodedName) {\n      variables.set(decodedName, value);\n    }\n  }\n\n  return computeExpression(expression, variables);\n};\n\nconst BindingPanel = ({\n  scope,\n  aliases,\n  valueError,\n  value,\n  onChange,\n  onSave,\n}: {\n  scope: Record<string, unknown>;\n  aliases: Map<string, string>;\n  valueError?: string;\n  value: string;\n  onChange: () => void;\n  onSave: (value: string, invalid: boolean) => void;\n}) => {\n  const editorApiRef = useRef<undefined | EditorApi>(undefined);\n  const [expression, setExpression] = useState(value);\n  const usedIdentifiers = useMemo(\n    () => getExpressionIdentifiers(value),\n    [value]\n  );\n  const [errorsCount, setErrorsCount] = useState<number>(0);\n  const [touched, setTouched] = useState(false);\n  const scopeEntries = Object.entries(scope);\n\n  const validate = (expression: string) => {\n    const diagnostics = lintExpression({\n      expression,\n      availableVariables: new Set(aliases.keys()),\n    });\n    // prevent saving expression only with syntax error\n    const errors = diagnostics.filter((item) => item.severity === \"error\");\n    setErrorsCount(errors.length);\n  };\n\n  const updateExpression = (newExpression: string) => {\n    setExpression(newExpression);\n    onChange();\n    validate(newExpression);\n  };\n\n  return (\n    <ScrollArea\n      css={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        width: theme.spacing[30],\n      }}\n    >\n      <Box css={{ paddingBottom: theme.spacing[5] }}>\n        <Flex gap=\"1\" css={{ padding: theme.panel.padding }}>\n          <Text variant=\"labels\">Variables</Text>\n          <Tooltip\n            variant=\"wrapped\"\n            content={\n              \"Click on the available variables in this scope to insert them into the Expression Editor.\"\n            }\n          >\n            <InfoCircleIcon tabIndex={0} />\n          </Tooltip>\n        </Flex>\n        {scopeEntries.length === 0 && (\n          <Flex justify=\"center\" align=\"center\" css={{ py: theme.spacing[5] }}>\n            <Text variant=\"labels\" align=\"center\">\n              No variables available\n            </Text>\n          </Flex>\n        )}\n        <CssValueListArrowFocus>\n          {scopeEntries.map(([identifier, value], index) => {\n            const name = aliases.get(identifier);\n            const label =\n              value === undefined\n                ? name\n                : `${name}: ${formatValuePreview(value)}`;\n            return (\n              <CssValueListItem\n                key={identifier}\n                id={identifier}\n                index={index}\n                label={<Label truncate>{label}</Label>}\n                // mark all variables used in expression as selected\n                active={usedIdentifiers.has(identifier)}\n                // convert variable to expression\n                onClick={() => {\n                  if (name) {\n                    const nameIdentifier = encodeDataVariableName(name);\n                    editorApiRef.current?.replaceSelection(nameIdentifier);\n                  }\n                }}\n                // expression editor blur is fired after pointer down even\n                // preventing it allows to not trigger validation\n                // and flickering error tooltip\n                onPointerDown={(event) => {\n                  event.preventDefault();\n                }}\n              />\n            );\n          })}\n        </CssValueListArrowFocus>\n      </Box>\n      <Flex gap=\"1\" css={{ padding: theme.panel.padding }}>\n        <Text variant=\"labels\">Expression editor</Text>\n        <Tooltip\n          variant=\"wrapped\"\n          content={\n            <Text>\n              Use JavaScript syntax to access variables along with comparison\n              and arithmetic operators.\n              <br />\n              Use the dot notation to access nested object values:\n              <Text variant=\"mono\">Variable.nested.value</Text>\n            </Text>\n          }\n        >\n          <InfoCircleIcon tabIndex={0} />\n        </Tooltip>\n      </Flex>\n      <Box css={{ padding: theme.panel.padding, pt: 0 }}>\n        <ExpressionEditor\n          editorApiRef={editorApiRef}\n          scope={scope}\n          aliases={aliases}\n          color={\n            (touched && errorsCount > 0) || valueError !== undefined\n              ? \"error\"\n              : undefined\n          }\n          autoFocus={true}\n          value={expression}\n          onChange={(value) => {\n            updateExpression(value);\n            setTouched(false);\n          }}\n          onChangeComplete={() => {\n            onSave(expression, errorsCount > 0);\n            setTouched(true);\n          }}\n        />\n      </Box>\n    </ScrollArea>\n  );\n};\n\nconst bindingOpacityProperty = \"--ws-binding-opacity\";\n\nexport const BindingControl = ({ children }: { children: ReactNode }) => {\n  return (\n    <Box\n      css={{\n        position: \"relative\",\n        \"&:hover\": { [bindingOpacityProperty]: 1 },\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nexport type BindingVariant = \"default\" | \"bound\" | \"overwritten\";\n\nconst BindingButton = forwardRef<\n  HTMLButtonElement,\n  ButtonHTMLAttributes<HTMLButtonElement> & {\n    variant: BindingVariant;\n    error?: string;\n    value: string;\n  }\n>(({ variant, error, value, ...props }, ref) => {\n  const expanded = props[\"aria-expanded\"];\n  const overwrittenMessage =\n    variant === \"overwritten\" ? (\n      <Flex direction=\"column\" gap=\"2\" css={{ maxWidth: theme.spacing[28] }}>\n        <Text>Bound variable is overwritten with temporary value</Text>\n        <Button\n          color=\"dark\"\n          prefix={<ResetIcon />}\n          css={{ flexGrow: 1 }}\n          onClick={() => {\n            const potentialVariableId = decodeDataSourceVariable(value);\n            const dataSourceVariables = new Map($dataSourceVariables.get());\n            if (potentialVariableId !== undefined) {\n              dataSourceVariables.delete(potentialVariableId);\n              $dataSourceVariables.set(dataSourceVariables);\n            }\n          }}\n        >\n          Reset value\n        </Button>\n      </Flex>\n    ) : undefined;\n  const tooltipContent = error ?? overwrittenMessage;\n  return (\n    // prevent giving content to tooltip when popover is open\n    // to avoid button remounting and popover flickering\n    // when switch between valid and error value\n    <Tooltip content={expanded ? undefined : tooltipContent} delayDuration={0}>\n      <SmallIconButton\n        ref={ref}\n        data-variant={variant}\n        bleed={false}\n        css={{\n          // hide by default\n          opacity: `var(${bindingOpacityProperty}, 0)`,\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          boxSizing: \"border-box\",\n          padding: 2,\n          // Because of the InputErrorsTooltip, we need to set zIndex to 1 (as InputErrorsTooltip needs an additional position relative wrapper)\n          zIndex: 1,\n          transform: \"translate(-50%, -50%) scale(1)\",\n          transition: \"transform 60ms, opacity 0ms 60ms\",\n          // https://easings.net/#easeInOutSine\n          transitionTimingFunction: \"cubic-bezier(0.37, 0, 0.63, 1)\",\n          \"--dot-display\": \"block\",\n          \"--plus-display\": \"none\",\n          \"&[data-variant=bound], &[data-variant=overwritten]\": {\n            opacity: 1,\n          },\n          \"&:hover, &:focus-visible, &[aria-expanded=true]\": {\n            // always show when interacted with\n            opacity: 1,\n            transform: `translate(-50%, -50%) scale(1.5)`,\n            \"--dot-display\": \"none\",\n            \"--plus-display\": \"block\",\n          },\n          \"&:disabled\": {\n            display: \"none\",\n          },\n        }}\n        {...props}\n        icon={\n          <Box\n            css={{\n              width: 12,\n              height: 12,\n              borderRadius: \"50%\",\n              backgroundColor: theme.colors.backgroundStyleSourceToken,\n              display: \"flex\",\n              justifyContent: \"center\",\n              alignItems: \"center\",\n              \"&[data-variant=bound]\": {\n                backgroundColor: theme.colors.backgroundStyleSourceLocal,\n              },\n              \"&[data-variant=overwritten]\": {\n                backgroundColor: theme.colors.borderOverwrittenMain,\n              },\n              \"&[data-variant=error]\": {\n                backgroundColor: theme.colors.backgroundDestructiveMain,\n              },\n            }}\n            data-variant={error ? \"error\" : variant}\n          >\n            <DotIcon\n              size={7}\n              style={{ display: `var(--dot-display)`, color: \"white\" }}\n            />\n            <PlusIcon\n              size={8}\n              style={{ display: `var(--plus-display)`, color: \"white\" }}\n            />\n          </Box>\n        }\n      />\n    </Tooltip>\n  );\n});\nBindingButton.displayName = \"BindingButton\";\n\nexport const BindingPopover = ({\n  scope,\n  aliases,\n  variant,\n  validate,\n  value,\n  onChange,\n  onRemove,\n}: {\n  scope: Record<string, unknown>;\n  aliases: Map<string, string>;\n  variant: BindingVariant;\n  validate?: (value: unknown) => undefined | string;\n  value: string;\n  onChange: (newValue: string) => void;\n  onRemove: (evaluatedValue: unknown) => void;\n}) => {\n  const [isOpen, onOpenChange] = useState(false);\n  const hasUnsavedChange = useRef<boolean>(false);\n  const preventedClosing = useRef<boolean>(false);\n  const isDesignMode = useStore($isDesignMode);\n\n  if (!isDesignMode) {\n    return;\n  }\n\n  const valueError = validate?.(evaluateExpressionWithinScope(value, scope));\n  return (\n    <FloatingPanel\n      placement=\"left-start\"\n      open={isOpen}\n      onOpenChange={(newOpen) => {\n        // handle special case for popover close\n        if (newOpen === false) {\n          // prevent saving when changes are not saved or validated\n          if (hasUnsavedChange.current) {\n            // schedule closing after saving\n            preventedClosing.current = true;\n            return;\n          }\n          preventedClosing.current = false;\n        }\n        onOpenChange(newOpen);\n      }}\n      title={\n        <DialogTitle\n          suffix={\n            <DialogTitleActions>\n              <Tooltip content=\"Reset binding\" side=\"bottom\">\n                {/* automatically close popover when remove expression */}\n                <DialogClose>\n                  <Button\n                    aria-label=\"Reset binding\"\n                    prefix={<TrashIcon />}\n                    color=\"ghost\"\n                    disabled={variant === \"default\"}\n                    onClick={(event) => {\n                      event.preventDefault();\n                      // inline variables and close dialog\n                      const evaluatedValue = evaluateExpressionWithinScope(\n                        value,\n                        scope\n                      );\n\n                      onRemove(evaluatedValue);\n                      preventedClosing.current = false;\n                      hasUnsavedChange.current = false;\n                      onOpenChange(false);\n                    }}\n                  />\n                </DialogClose>\n              </Tooltip>\n              <DialogClose />\n            </DialogTitleActions>\n          }\n        >\n          Binding\n        </DialogTitle>\n      }\n      content={\n        <BindingPanel\n          scope={scope}\n          aliases={aliases}\n          valueError={valueError}\n          value={value}\n          onChange={() => {\n            hasUnsavedChange.current = true;\n          }}\n          onSave={(value, invalid) => {\n            // avoid saving without changes\n            if (hasUnsavedChange.current === false) {\n              return;\n            }\n            // let user see the error and let close popover after\n            hasUnsavedChange.current = false;\n            if (invalid) {\n              return;\n            }\n            // save value and close popover\n            onChange(value);\n            if (preventedClosing.current) {\n              preventedClosing.current = false;\n              onOpenChange(false);\n            }\n          }}\n        />\n      }\n    >\n      <BindingButton variant={variant} error={valueError} value={value} />\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/calc-canvas-width.test.ts",
    "content": "import { test, expect, describe, beforeEach } from \"vitest\";\nimport { calcCanvasWidth, setCanvasWidth } from \"./calc-canvas-width\";\nimport { $workspaceRect, $canvasWidth } from \"~/builder/shared/nano-states\";\nimport { $breakpoints } from \"~/shared/sync/data-stores\";\n\n// Helper to create a DOMRect-like object for testing\nconst createRect = (width: number, height: number): DOMRect => ({\n  width,\n  height,\n  x: 0,\n  y: 0,\n  top: 0,\n  right: width,\n  bottom: height,\n  left: 0,\n  toJSON: () => ({\n    width,\n    height,\n    x: 0,\n    y: 0,\n    top: 0,\n    right: width,\n    bottom: height,\n    left: 0,\n  }),\n});\n\nconst breakpoints = [\n  { id: \"0\", label: \"Base\" },\n  { id: \"1\", label: \"Tablet\", maxWidth: 991 },\n  { id: \"2\", label: \"Mobile landscape\", maxWidth: 767 },\n  { id: \"3\", label: \"Mobile portrait\", maxWidth: 479 },\n  { id: \"4\", label: \"Large\", minWidth: 1280 },\n  { id: \"5\", label: \"Extra Large\", minWidth: 1440 },\n];\n\ndescribe(\"base breakpoint\", () => {\n  test(\"without canvas width\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [{ id: \"0\", label: \"Base\" }];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(workspaceWidth);\n  });\n\n  test(\"canvas width bigger than workspace\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [{ id: \"0\", label: \"Base\" }];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n        canvasWidth: 1200,\n      })\n    ).toStrictEqual(workspaceWidth);\n  });\n\n  test(\"canvas width smaller than workspace\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [{ id: \"0\", label: \"Base\" }];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n        canvasWidth: 900,\n      })\n    ).toStrictEqual(workspaceWidth);\n  });\n\n  test(\"keep current canvas width when there is min-width breakpoint\", () => {\n    const workspaceWidth = 1200;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n        canvasWidth: 1000,\n      })\n    ).toStrictEqual(1000);\n  });\n\n  test(\"keep a custom canvas width when there is no min-width breakpoint\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Tablet\", maxWidth: 991 },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n        canvasWidth: 993,\n      })\n    ).toStrictEqual(993);\n  });\n\n  test(\"using maximum available workspace while not entering any of the breakpoints\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(workspaceWidth);\n  });\n\n  test(\"workspace is smaller than the base breakpoint range, so we need to use minimum available\", () => {\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth: 500,\n      })\n    ).toStrictEqual(992);\n  });\n\n  test(\"min-width only\", () => {\n    const workspaceWidth = 1000;\n\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"4\", label: \"Large\", minWidth: 1280 },\n      { id: \"5\", label: \"Extra Large\", minWidth: 1440 },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(workspaceWidth);\n  });\n\n  test(\"max-width only\", () => {\n    const workspaceWidth = 1000;\n\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Tablet\", maxWidth: 991 },\n      { id: \"2\", label: \"Mobile landscape\", maxWidth: 767 },\n      { id: \"3\", label: \"Mobile portrait\", maxWidth: 479 },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(workspaceWidth);\n  });\n});\n\ndescribe(\"other breakpoints\", () => {\n  test(\"tablet\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n      })\n    ).toStrictEqual(768);\n  });\n\n  test(\"tablet - keep the custom width\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n        canvasWidth: 993,\n      })\n    ).toStrictEqual(993);\n  });\n\n  test(\"mobile landscape\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[2],\n        workspaceWidth,\n      })\n    ).toStrictEqual(480);\n  });\n\n  test(\"mobile portrait\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[3],\n        workspaceWidth,\n      })\n    ).toStrictEqual(320);\n  });\n\n  test(\"large\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[4],\n        workspaceWidth,\n      })\n    ).toStrictEqual(1280);\n  });\n\n  test(\"extra large\", () => {\n    const workspaceWidth = 1000;\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[5],\n        workspaceWidth,\n      })\n    ).toStrictEqual(1440);\n  });\n\n  test(\"0 min-width\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [{ id: \"0\", label: \"x\", minWidth: 0 }];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(320);\n  });\n\n  test(\"low min-width\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [{ id: \"0\", label: \"x\", minWidth: 123 }];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(123);\n  });\n\n  test(\"no previous max-width\", () => {\n    const workspaceWidth = 1000;\n    const breakpoints = [{ id: \"0\", label: \"x\", maxWidth: 100 }];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(320);\n  });\n});\n\ndescribe(\"custom condition breakpoints\", () => {\n  test(\"returns undefined for custom condition breakpoint\", () => {\n    const workspaceWidth = 1200;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Portrait\", condition: \"orientation:portrait\" },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"returns undefined for hover condition\", () => {\n    const workspaceWidth = 1200;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Hover\", condition: \"hover:hover\" },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"returns undefined for prefers-color-scheme\", () => {\n    const workspaceWidth = 1200;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Dark\", condition: \"prefers-color-scheme:dark\" },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"returns undefined regardless of custom canvasWidth for condition breakpoints\", () => {\n    const workspaceWidth = 1200;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Portrait\", condition: \"orientation:portrait\" },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n        canvasWidth: 800, // Should be ignored\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"base breakpoint ignores custom condition breakpoints when calculating width\", () => {\n    const workspaceWidth = 1200;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Tablet\", minWidth: 768 },\n      { id: \"2\", label: \"Portrait\", condition: \"orientation:portrait\" },\n      { id: \"3\", label: \"Hover\", condition: \"hover:hover\" },\n    ];\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(767); // minWidth - 1, ignoring condition breakpoints\n  });\n\n  test(\"handles mix of width-based and condition breakpoints\", () => {\n    const workspaceWidth = 1200;\n    const breakpoints = [\n      { id: \"0\", label: \"Base\" },\n      { id: \"1\", label: \"Tablet\", minWidth: 768 },\n      { id: \"2\", label: \"Portrait\", condition: \"orientation:portrait\" },\n      { id: \"3\", label: \"Desktop\", minWidth: 1024 },\n      { id: \"4\", label: \"Hover\", condition: \"hover:hover\" },\n    ];\n\n    // Width-based breakpoint works normally\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[1],\n        workspaceWidth,\n      })\n    ).toStrictEqual(768);\n\n    // Condition breakpoint returns undefined\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[2],\n        workspaceWidth,\n      })\n    ).toBeUndefined();\n\n    // Base calculates correctly ignoring condition breakpoints\n    expect(\n      calcCanvasWidth({\n        breakpoints,\n        selectedBreakpoint: breakpoints[0],\n        workspaceWidth,\n      })\n    ).toStrictEqual(767);\n  });\n});\n\ndescribe(\"setCanvasWidth\", () => {\n  beforeEach(() => {\n    // Reset stores before each test\n    $canvasWidth.set(0);\n    $breakpoints.set(new Map());\n    $workspaceRect.set(undefined);\n  });\n\n  test(\"returns false when workspaceRect is undefined\", () => {\n    const breakpoints = new Map([[\"0\", { id: \"0\", label: \"Base\" }]]);\n    $breakpoints.set(breakpoints);\n    $workspaceRect.set(undefined);\n\n    expect(setCanvasWidth(\"0\")).toBe(false);\n  });\n\n  test(\"returns false when selectedBreakpoint is not found\", () => {\n    $workspaceRect.set(createRect(1000, 800));\n    $breakpoints.set(new Map([[\"0\", { id: \"0\", label: \"Base\" }]]));\n\n    expect(setCanvasWidth(\"nonexistent\")).toBe(false);\n  });\n\n  test(\"sets canvas width for base breakpoint\", () => {\n    const workspaceWidth = 1200;\n    $workspaceRect.set(createRect(workspaceWidth, 800));\n    $breakpoints.set(new Map([[\"0\", { id: \"0\", label: \"Base\" }]]));\n\n    const result = setCanvasWidth(\"0\");\n\n    expect(result).toBe(true);\n    expect($canvasWidth.get()).toBe(workspaceWidth);\n  });\n\n  test(\"sets canvas width for max-width breakpoint\", () => {\n    $workspaceRect.set(createRect(1200, 800));\n    $breakpoints.set(\n      new Map([\n        [\"0\", { id: \"0\", label: \"Base\" }],\n        [\"1\", { id: \"1\", label: \"Tablet\", maxWidth: 991 }],\n        [\"2\", { id: \"2\", label: \"Mobile\", maxWidth: 767 }],\n      ])\n    );\n\n    const result = setCanvasWidth(\"1\");\n\n    expect(result).toBe(true);\n    expect($canvasWidth.get()).toBe(768); // maxWidth of previous breakpoint + 1\n  });\n\n  test(\"sets canvas width for min-width breakpoint\", () => {\n    $workspaceRect.set(createRect(1000, 800));\n    $breakpoints.set(\n      new Map([\n        [\"0\", { id: \"0\", label: \"Base\" }],\n        [\"1\", { id: \"1\", label: \"Large\", minWidth: 1280 }],\n      ])\n    );\n\n    const result = setCanvasWidth(\"1\");\n\n    expect(result).toBe(true);\n    expect($canvasWidth.get()).toBe(1280);\n  });\n\n  test(\"updates canvas width when called multiple times\", () => {\n    $workspaceRect.set(createRect(1500, 800));\n    $breakpoints.set(\n      new Map([\n        [\"0\", { id: \"0\", label: \"Base\" }],\n        [\"1\", { id: \"1\", label: \"Tablet\", maxWidth: 991 }],\n        [\"2\", { id: \"2\", label: \"Mobile\", maxWidth: 767 }],\n      ])\n    );\n\n    setCanvasWidth(\"1\");\n    expect($canvasWidth.get()).toBe(768); // Previous breakpoint maxWidth (767) + 1\n\n    setCanvasWidth(\"0\");\n    expect($canvasWidth.get()).toBe(1500);\n  });\n\n  test(\"returns undefined for custom condition breakpoint in setCanvasWidth\", () => {\n    $workspaceRect.set(createRect(1200, 800));\n    $breakpoints.set(\n      new Map([\n        [\"0\", { id: \"0\", label: \"Base\" }],\n        [\n          \"1\",\n          { id: \"1\", label: \"Portrait\", condition: \"orientation:portrait\" },\n        ],\n      ])\n    );\n\n    const result = setCanvasWidth(\"1\");\n\n    expect(result).toBe(true);\n    expect($canvasWidth.get()).toBeUndefined(); // Undefined for condition breakpoints\n  });\n\n  test(\"returns undefined for hover condition breakpoint in setCanvasWidth\", () => {\n    $workspaceRect.set(createRect(1500, 800));\n    $breakpoints.set(\n      new Map([\n        [\"0\", { id: \"0\", label: \"Base\" }],\n        [\"1\", { id: \"1\", label: \"Hover\", condition: \"hover:hover\" }],\n      ])\n    );\n\n    const result = setCanvasWidth(\"1\");\n\n    expect(result).toBe(true);\n    expect($canvasWidth.get()).toBeUndefined();\n  });\n});\n\n// Note: useSetCanvasWidth is a React hook with complex side effects (multiple store subscriptions\n// and cleanup logic). It's primarily tested through integration tests and component tests rather\n// than unit tests, as it orchestrates calls to setCanvasWidth which is fully unit tested above.\n"
  },
  {
    "path": "apps/builder/app/builder/shared/calc-canvas-width.ts",
    "content": "import { useEffect } from \"react\";\nimport { compareMedia } from \"@webstudio-is/css-engine\";\nimport type { Breakpoint } from \"@webstudio-is/sdk\";\nimport {\n  groupBreakpoints,\n  isBaseBreakpoint,\n  minCanvasWidth,\n} from \"~/shared/breakpoints-utils\";\nimport { $workspaceRect, $canvasWidth } from \"~/builder/shared/nano-states\";\nimport {\n  $breakpoints,\n  $isPreviewMode,\n  $selectedBreakpoint,\n} from \"~/shared/nano-states\";\n\nconst defaultWidth = 320;\n\n// We want to know if user resized the canvas to a custom value.\nconst isCustomCanvasWidth = (\n  breakpoints: Array<Breakpoint>,\n  selectedBreakpoint: Breakpoint,\n  canvasWidth?: number\n) => {\n  if (canvasWidth === undefined) {\n    return false;\n  }\n  const hasMinWidth = breakpoints.some(\n    (breakpoint) => breakpoint.minWidth !== undefined\n  );\n\n  // Only in case of base breakpoint and whithout min-width breakpoint, we ignore the custom width,\n  // because we want it to go full viewport width\n  if (isBaseBreakpoint(selectedBreakpoint) && hasMinWidth === false) {\n    return false;\n  }\n\n  for (const breakpoint of breakpoints) {\n    if (\n      breakpoint.minWidth !== undefined &&\n      canvasWidth === breakpoint.minWidth\n    ) {\n      return false;\n    }\n    if (\n      breakpoint.maxWidth !== undefined &&\n      canvasWidth === breakpoint.maxWidth + 1\n    ) {\n      return false;\n    }\n  }\n  return true;\n};\n\n// Find a canvas width that is within the selected breakpoint's range, but is at it's minimum.\n// Goal is to allow user to consistently know the direction they want to resize to after they switched to a breakpoint.\n// In this case they will want to always increase the width after switching.\nexport const calcCanvasWidth = ({\n  breakpoints,\n  selectedBreakpoint,\n  workspaceWidth,\n  canvasWidth,\n}: {\n  breakpoints: Array<Breakpoint>;\n  selectedBreakpoint: Breakpoint;\n  workspaceWidth: number;\n  canvasWidth?: number;\n}) => {\n  // Condition-based breakpoints (e.g., orientation:portrait, hover:hover) don't have width ranges\n  // Return undefined so the width will be defined by current canvas width\n  if (selectedBreakpoint.condition !== undefined) {\n    return;\n  }\n\n  // When user has resized the canvas to a custom value, we want to keep it.\n  if (isCustomCanvasWidth(breakpoints, selectedBreakpoint, canvasWidth)) {\n    return canvasWidth;\n  }\n\n  // Finding the canvas width when user selects base breakpoint is a bit more complicated.\n  // We want to find the lowest possible size that is bigger than all max breakpoints and smaller than all min breakpoints.\n  // Note: it is still possible to get intersecting min and max breakpoints.\n  if (isBaseBreakpoint(selectedBreakpoint)) {\n    // Base is the only breakpoint\n    if (breakpoints.length === 1) {\n      return workspaceWidth;\n    }\n\n    const grouped = groupBreakpoints(breakpoints);\n    // Only consider width-based breakpoints for canvas sizing\n    const filtered = grouped.widthBased.filter(({ minWidth, maxWidth }) => {\n      // We don't want to grow the canvas beyond the workspace width.\n      if (minWidth && minWidth < workspaceWidth) {\n        return true;\n      }\n      // Max width can not be smaller than the minimum canvas width.\n      if (maxWidth && maxWidth > minCanvasWidth) {\n        return true;\n      }\n    });\n    let lowestMinWidth = filtered\n      .filter(({ minWidth }) => minWidth !== undefined)\n      .at(-1)?.minWidth;\n\n    lowestMinWidth =\n      lowestMinWidth === undefined ? workspaceWidth : lowestMinWidth - 1;\n\n    let highestMaxWidth = filtered\n      .filter(({ maxWidth }) => maxWidth !== undefined)\n      .at(0)?.maxWidth;\n\n    highestMaxWidth =\n      highestMaxWidth === undefined ? minCanvasWidth : highestMaxWidth + 1;\n\n    return Math.max(lowestMinWidth, highestMaxWidth);\n  }\n\n  if (selectedBreakpoint.minWidth !== undefined) {\n    if (selectedBreakpoint.minWidth === 0) {\n      return defaultWidth;\n    }\n    return selectedBreakpoint.minWidth;\n  }\n\n  const sorted = breakpoints\n    .filter((breakpoint) => breakpoint.maxWidth !== undefined)\n    .sort(compareMedia)\n    .reverse();\n\n  const index = sorted.findIndex(\n    (breakpoint) => breakpoint.id === selectedBreakpoint.id\n  );\n  const previousBreakpointMaxWidth = sorted[index - 1]?.maxWidth;\n  if (previousBreakpointMaxWidth === undefined) {\n    return defaultWidth;\n  }\n  return previousBreakpointMaxWidth + 1;\n};\n\nexport const setCanvasWidth = (breakpointId: Breakpoint[\"id\"]) => {\n  const workspaceRect = $workspaceRect.get();\n  const breakpoints = $breakpoints.get();\n  const selectedBreakpoint = breakpoints.get(breakpointId);\n\n  if (workspaceRect === undefined || selectedBreakpoint === undefined) {\n    return false;\n  }\n\n  const width = calcCanvasWidth({\n    breakpoints: Array.from(breakpoints.values()),\n    selectedBreakpoint,\n    workspaceWidth: workspaceRect.width,\n  });\n\n  $canvasWidth.set(width);\n  return true;\n};\n\n/**\n *  Update canvas width initially and on breakpoint change\n **/\nexport const useSetCanvasWidth = () => {\n  useEffect(() => {\n    const update = () => {\n      const selectedBreakpoint = $selectedBreakpoint.get();\n      if (selectedBreakpoint) {\n        // When there is selected breakpoint, we want to find the smallest possible size\n        // that is bigger than any max-width breakpoints and smaller than any min-width breakpoints.\n        // When on base breakpoint it will be the biggest possible but smaller than the workspace.\n        setCanvasWidth(selectedBreakpoint.id);\n      }\n    };\n\n    const unsubscribeBreakpoints = $breakpoints.subscribe(update);\n    const unsubscribeRect = $workspaceRect.listen(update);\n    const unsubscribeIsPreviewMode = $isPreviewMode.listen((isPreviewMode) => {\n      if (isPreviewMode) {\n        update();\n      }\n    });\n    const unsubscribeSelectedBreakpoint = $selectedBreakpoint.listen(\n      (selectedBreakpoint) => {\n        // This will set initial width of the canvas once the initial selected breakpoint is known.\n        if (selectedBreakpoint) {\n          update();\n          // We can unsubscribe right away as we only need this once.\n          unsubscribeSelectedBreakpoint();\n        }\n      }\n    );\n\n    return () => {\n      unsubscribeBreakpoints();\n      unsubscribeRect();\n      unsubscribeIsPreviewMode();\n      unsubscribeSelectedBreakpoint();\n    };\n  }, []);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/client-settings/index.ts",
    "content": "export * from \"./settings\";\n"
  },
  {
    "path": "apps/builder/app/builder/shared/client-settings/settings.ts",
    "content": "import { atom } from \"nanostores\";\nimport { z } from \"zod\";\nimport { sidebarPanelNames } from \"~/builder/sidebar-left/types\";\n\nconst Settings = z.object({\n  navigatorLayout: z.enum([\"docked\", \"undocked\"]).default(\"undocked\"),\n  stylePanelMode: z.enum([\"default\", \"focus\", \"advanced\"]).default(\"default\"),\n  sidebarPanelWidths: z\n    .record(z.enum(sidebarPanelNames), z.number())\n    .default({}),\n});\n\nexport type Settings = z.infer<typeof Settings>;\n\nconst defaultSettings = Settings.parse({});\n\nconst namespace = \"__webstudio_user_settings__\";\n\nconst read = (): Settings => {\n  let settingsString;\n  try {\n    settingsString = localStorage.getItem(namespace);\n  } catch {\n    // We don't need to handle this one.\n  }\n\n  if (settingsString == null) {\n    return defaultSettings;\n  }\n\n  try {\n    return Settings.parse(JSON.parse(settingsString));\n  } catch (error) {\n    if (error instanceof Error) {\n      console.error({\n        message: \"Bad user settings in local storage\",\n        extras: {\n          error: error.message,\n        },\n      });\n    }\n  }\n  return defaultSettings;\n};\n\nconst write = (settings: Settings) => {\n  localStorage.setItem(namespace, JSON.stringify(settings));\n};\n\nconst initialSettings = read();\n\nexport const $settings = atom<Settings>(initialSettings);\n\nexport const setSetting = <Name extends keyof Settings>(\n  name: Name,\n  value: Settings[Name]\n) => {\n  const settings = $settings.get();\n  if (settings[name] === value) {\n    return;\n  }\n  const nextSettings = { ...settings, [name]: value };\n  $settings.set(nextSettings);\n  write(nextSettings);\n};\n\nexport const getSetting = <Name extends keyof Settings>(name: Name) => {\n  return $settings.get()[name];\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/collapsible-section.stories.tsx",
    "content": "import { Flex, StorySection, Text } from \"@webstudio-is/design-system\";\nimport {\n  CollapsibleSectionRoot,\n  CollapsibleSection,\n  CollapsibleSectionWithAddButton,\n  CollapsibleProvider,\n} from \"./collapsible-section\";\n\nexport default {\n  title: \"Builder/Shared/Collapsible section\",\n  component: CollapsibleSection,\n};\n\nexport const CollapsibleSections = () => (\n  <StorySection title=\"Collapsible Sections\">\n    <Flex direction=\"row\" gap=\"9\">\n      <Flex direction=\"column\" css={{ width: 240 }}>\n        <Text variant=\"labels\" css={{ padding: 8 }}>\n          Independent sections\n        </Text>\n        <CollapsibleSectionRoot label=\"Root section\">\n          <Text>Content inside a root section.</Text>\n        </CollapsibleSectionRoot>\n\n        <CollapsibleSection label=\"Default section\">\n          <Text>This section uses persisted open/close state.</Text>\n        </CollapsibleSection>\n\n        <CollapsibleSectionWithAddButton\n          label=\"With add button\"\n          onAdd={() => {}}\n          hasItems\n        >\n          <Text>Section with an add button in the title.</Text>\n        </CollapsibleSectionWithAddButton>\n\n        <CollapsibleSectionWithAddButton\n          label=\"Empty (no items)\"\n          onAdd={() => {}}\n          hasItems={false}\n        >\n          <Text>This won&apos;t appear because hasItems is false.</Text>\n        </CollapsibleSectionWithAddButton>\n      </Flex>\n\n      <CollapsibleProvider accordion initialOpen=\"First\">\n        <Flex direction=\"column\" css={{ width: 240 }}>\n          <Text variant=\"labels\" css={{ padding: 8 }}>\n            Accordion mode\n          </Text>\n          <CollapsibleSection label=\"First\">\n            <Text>First section content.</Text>\n          </CollapsibleSection>\n          <CollapsibleSection label=\"Second\">\n            <Text>Second section content.</Text>\n          </CollapsibleSection>\n          <CollapsibleSection label=\"Third\">\n            <Text>Third section content.</Text>\n          </CollapsibleSection>\n        </Flex>\n      </CollapsibleProvider>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/shared/collapsible-section.tsx",
    "content": "import { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Flex,\n  Collapsible,\n  SectionTitle,\n  SectionTitleLabel,\n  SectionTitleButton,\n  Separator,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { PlusIcon } from \"@webstudio-is/icons\";\nimport type { Simplify } from \"type-fest\";\n\ntype Label = string;\n\ntype State = {\n  [label: string]: boolean;\n};\n\n// Preserves the open/close state even when component gets unmounted\nconst $state = atom<State>({});\n\ntype HandleOpenState = (\n  label: Label,\n  isOpenForced?: boolean\n) => [boolean, (value: boolean) => void];\n\nconst CollapsibleSectionContext = createContext<\n  | undefined\n  | {\n      handleOpenState: HandleOpenState;\n    }\n>(undefined);\n\nexport const CollapsibleProvider = ({\n  children,\n  accordion,\n  initialOpen,\n}: {\n  children: ReactNode;\n  accordion?: boolean;\n  initialOpen?: Label;\n}) => {\n  const state = useStore($state);\n  useEffect(() => {\n    const nextState = { ...$state.get() };\n\n    if (initialOpen === \"*\") {\n      for (const key in nextState) {\n        nextState[key] = true;\n      }\n      $state.set(nextState);\n      return;\n    }\n\n    if (accordion) {\n      for (const key in nextState) {\n        nextState[key] = false;\n      }\n    }\n\n    if (initialOpen) {\n      nextState[initialOpen] = true;\n    }\n    $state.set(nextState);\n  }, [accordion, initialOpen]);\n\n  const handleOpenState: HandleOpenState = useCallback(\n    (label, isOpenForced) => {\n      const nextState = { ...state };\n      if (nextState[label] === undefined) {\n        nextState[label] = accordion ? false : true;\n      }\n\n      const setIsOpen = (isOpen: boolean) => {\n        if (accordion) {\n          for (const key in nextState) {\n            nextState[key] = false;\n          }\n        }\n        nextState[label] = isOpen;\n        $state.set(nextState);\n      };\n\n      return [isOpenForced ?? nextState[label], setIsOpen];\n    },\n    [state, accordion]\n  );\n\n  return (\n    <CollapsibleSectionContext.Provider value={{ handleOpenState }}>\n      {children}\n    </CollapsibleSectionContext.Provider>\n  );\n};\n\nexport const useOpenState: HandleOpenState = (label, isOpen) => {\n  const context = useContext(CollapsibleSectionContext);\n  const localState = useState(isOpen ?? true);\n  if (context === undefined) {\n    return localState;\n  }\n  return context.handleOpenState(label, isOpen);\n};\n\ntype CollapsibleSectionBaseProps = {\n  trigger?: ReactNode;\n  children: ReactNode;\n  fullWidth?: boolean;\n  label?: string;\n  isOpen?: boolean;\n  onOpenChange?: (value: boolean) => void;\n};\n\nexport const CollapsibleSectionRoot = ({\n  label,\n  trigger,\n  children,\n  fullWidth = false,\n  isOpen,\n  onOpenChange,\n}: CollapsibleSectionBaseProps) => {\n  return (\n    <Collapsible.Root defaultOpen open={isOpen} onOpenChange={onOpenChange}>\n      <Collapsible.Trigger asChild>\n        {trigger ?? (\n          <SectionTitle>\n            <SectionTitleLabel>{label ?? \"\"}</SectionTitleLabel>\n          </SectionTitle>\n        )}\n      </Collapsible.Trigger>\n\n      <Collapsible.Content asChild>\n        <Flex\n          gap=\"2\"\n          direction=\"column\"\n          css={{\n            pb: theme.panel.paddingBlock,\n            px: fullWidth ? 0 : theme.panel.paddingInline,\n            paddingTop: 0,\n            \"&:empty\": { display: \"none\" },\n          }}\n        >\n          {children}\n        </Flex>\n      </Collapsible.Content>\n      <Separator />\n    </Collapsible.Root>\n  );\n};\n\ntype CollapsibleSectionProps = Simplify<\n  Omit<CollapsibleSectionBaseProps, \"onOpenChange\"> & {\n    label: Label;\n  }\n>;\n\nexport const CollapsibleSection = (props: CollapsibleSectionProps) => {\n  const { label, trigger, children, fullWidth } = props;\n  const [isOpen, setIsOpen] = useOpenState(label, props.isOpen);\n  return (\n    <CollapsibleSectionRoot\n      label={label}\n      trigger={trigger}\n      fullWidth={fullWidth}\n      isOpen={isOpen}\n      onOpenChange={setIsOpen}\n    >\n      {children}\n    </CollapsibleSectionRoot>\n  );\n};\n\nexport const CollapsibleSectionWithAddButton = ({\n  onAdd,\n  hasItems = true,\n  ...props\n}: Omit<CollapsibleSectionProps, \"trigger\" | \"categoryProps\"> & {\n  onAdd?: () => void;\n\n  /**\n   * If set to `true`, dots aren't shown,\n   * but still affects how isOpen is treated and whether onAdd is called on open.\n   */\n  hasItems?: boolean | ComponentProps<typeof SectionTitle>[\"dots\"];\n}) => {\n  const { label, children } = props;\n  const [isOpen, setIsOpen] = useOpenState(label, props.isOpen);\n\n  const isEmpty =\n    hasItems === false || (Array.isArray(hasItems) && hasItems.length === 0);\n\n  // If it's open but empty, we want it to look as closed\n  const isOpenFinal = isOpen && isEmpty === false;\n\n  return (\n    <CollapsibleSectionRoot\n      label={label}\n      fullWidth={false}\n      isOpen={isOpenFinal}\n      onOpenChange={(nextIsOpen) => {\n        setIsOpen(nextIsOpen);\n        if (isEmpty) {\n          onAdd?.();\n        }\n      }}\n      trigger={\n        <SectionTitle\n          dots={Array.isArray(hasItems) ? hasItems : []}\n          suffix={\n            onAdd ? (\n              <SectionTitleButton\n                prefix={<PlusIcon />}\n                onClick={() => {\n                  if (isOpenFinal === false) {\n                    setIsOpen(true);\n                  }\n                  onAdd();\n                }}\n              />\n            ) : undefined\n          }\n        >\n          <SectionTitleLabel>{props.label}</SectionTitleLabel>\n        </SectionTitle>\n      }\n    >\n      {children}\n    </CollapsibleSectionRoot>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/commands.ts",
    "content": "import { toast } from \"@webstudio-is/design-system\";\nimport type { WebstudioFragment } from \"@webstudio-is/sdk\";\nimport {\n  isAutoGridPlacement,\n  resetGridChildPlacement,\n} from \"~/builder/features/style-panel/sections/layout/shared/grid-utils\";\nimport { createCommandsEmitter, type Command } from \"~/shared/commands-emitter\";\nimport {\n  $editingItemSelector,\n  $isDesignMode,\n  toggleBuilderMode,\n  $project,\n} from \"~/shared/nano-states\";\n\n// Declare command for type safety\ndeclare module \"~/shared/pubsub\" {\n  interface CommandRegistry {\n    focusStyleSourceInput: undefined;\n  }\n}\n\nimport {\n  $breakpointsMenuView,\n  selectBreakpointByOrder,\n} from \"~/shared/breakpoints\";\nimport {\n  updateWebstudioData,\n  unwrapInstance,\n  deleteSelectedInstance,\n  extractWebstudioFragment,\n  insertWebstudioFragmentAt,\n  insertWebstudioFragmentCopy,\n} from \"~/shared/instance-utils\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { $publisher } from \"~/shared/pubsub\";\nimport {\n  $activeInspectorPanel,\n  $publishDialog,\n  setActiveSidebarPanel,\n  toggleActiveSidebarPanel,\n} from \"./nano-states\";\nimport { $selectedInstancePath, selectInstance } from \"~/shared/awareness\";\nimport { openCommandPanel } from \"../features/command-panel\";\nimport { showWrapComponentsList } from \"../features/command-panel/groups/wrap-group\";\nimport { showConvertComponentsList } from \"../features/command-panel/groups/convert-group\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport { getSetting, setSetting } from \"./client-settings\";\nimport { findAvailableVariables } from \"~/shared/data-variables\";\nimport { generateFragmentFromHtml } from \"~/shared/html\";\nimport { generateFragmentFromTailwind } from \"~/shared/tailwind/tailwind\";\nimport { denormalizeSrcProps } from \"~/shared/copy-paste/asset-upload\";\nimport { isSyncIdle } from \"~/shared/sync/project-queue\";\nimport { openDeleteUnusedTokensDialog } from \"~/builder/shared/style-source-actions\";\nimport { openDeleteUnusedDataVariablesDialog } from \"~/builder/shared/data-variable-utils\";\nimport { openDeleteUnusedCssVariablesDialog } from \"~/builder/shared/css-variable-utils\";\nimport { openDeleteUnusedAssetsDialog } from \"~/builder/shared/asset-manager/delete-unused-assets\";\nimport { openKeyboardShortcutsDialog } from \"~/builder/features/keyboard-shortcuts-dialog\";\nimport {\n  copyInstance,\n  emitPaste,\n  cutInstance,\n} from \"~/shared/copy-paste/init-copy-paste\";\nimport { toggleInstanceShow } from \"~/shared/instance-utils\";\n\nconst makeBreakpointCommand = <CommandName extends string>(\n  name: CommandName,\n  number: number\n): Command<CommandName> => ({\n  name,\n  hidden: true,\n  defaultHotkeys: [`${number}`],\n  disableOnInputLikeControls: true,\n  handler: () => {\n    selectBreakpointByOrder(number);\n  },\n});\n\nexport const { emitCommand, subscribeCommands } = createCommandsEmitter({\n  source: \"builder\",\n  externalCommands: [\n    \"editInstanceText\",\n    \"formatBold\",\n    \"formatItalic\",\n    \"formatSuperscript\",\n    \"formatSubscript\",\n    \"formatLink\",\n    \"formatSpan\",\n    \"formatClear\",\n  ],\n  commands: [\n    // system\n\n    {\n      name: \"cancelCurrentDrag\",\n      label: \"Deselect\",\n      description: \"Cancel drag or deselect\",\n      hidden: true,\n      category: \"General\",\n      defaultHotkeys: [\"escape\"],\n      // radix check event.defaultPrevented before invoking callbacks\n      preventDefault: false,\n      handler: () => {\n        const { publish } = $publisher.get();\n        publish?.({ type: \"cancelCurrentDrag\" });\n      },\n    },\n    {\n      name: \"clickCanvas\",\n      description: \"Click on canvas\",\n      hidden: true,\n      handler: () => {\n        $breakpointsMenuView.set(undefined);\n        setActiveSidebarPanel(\"auto\");\n      },\n    },\n\n    // ui\n\n    {\n      name: \"togglePreviewMode\",\n      description: \"Preview mode\",\n      category: \"Top bar\",\n      defaultHotkeys: [\"meta+shift+p\", \"ctrl+shift+p\"],\n      handler: () => {\n        setActiveSidebarPanel(\"auto\");\n        toggleBuilderMode(\"preview\");\n      },\n    },\n    {\n      name: \"toggleDesignMode\",\n      description: \"Toggle design mode\",\n      category: \"Top bar\",\n      defaultHotkeys: [\"meta+shift+d\", \"ctrl+shift+d\"],\n      handler: () => {\n        setActiveSidebarPanel(\"auto\");\n        toggleBuilderMode(\"design\");\n      },\n    },\n    {\n      name: \"toggleContentMode\",\n      description: \"Toggle content mode\",\n      category: \"Top bar\",\n      defaultHotkeys: [\"meta+shift+c\", \"ctrl+shift+c\"],\n      handler: () => {\n        setActiveSidebarPanel(\"auto\");\n        toggleBuilderMode(\"content\");\n      },\n    },\n    {\n      name: \"openBreakpointsMenu\",\n      description: \"Manage responsive breakpoints\",\n      handler: () => {\n        $breakpointsMenuView.set(\"initial\");\n      },\n    },\n    {\n      name: \"openPublishDialog\",\n      description: \"Deploy your project\",\n      category: \"Top bar\",\n      defaultHotkeys: [\"shift+P\"],\n      handler: () => {\n        $publishDialog.set(\"publish\");\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"openExportDialog\",\n      description: \"Export project code\",\n      category: \"General\",\n      defaultHotkeys: [\"shift+E\"],\n      handler: () => {\n        $publishDialog.set(\"export\");\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"toggleComponentsPanel\",\n      description: \"Toggle components panel\",\n      category: \"Panels\",\n      defaultHotkeys: [\"a\"],\n      handler: () => {\n        if ($isDesignMode.get() === false) {\n          builderApi.toast.info(\n            \"Components panel is only available in design mode.\"\n          );\n          return;\n        }\n        toggleActiveSidebarPanel(\"components\");\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"toggleNavigatorPanel\",\n      description: \"Toggle navigator panel\",\n      category: \"Panels\",\n      defaultHotkeys: [\"z\"],\n      handler: () => {\n        toggleActiveSidebarPanel(\"navigator\");\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"openStylePanel\",\n      description: \"Open style panel\",\n      category: \"Panels\",\n      defaultHotkeys: [\"s\"],\n      handler: () => {\n        if ($isDesignMode.get() === false) {\n          builderApi.toast.info(\n            \"Style panel is only available in design mode.\"\n          );\n          return;\n        }\n        $activeInspectorPanel.set(\"style\");\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"focusStyleSources\",\n      description: \"Focus style sources input\",\n      category: \"Style panel\",\n      defaultHotkeys: [\"meta+enter\", \"ctrl+enter\"],\n      handler: () => {\n        if ($isDesignMode.get() === false) {\n          builderApi.toast.info(\n            \"Style panel is only available in design mode.\"\n          );\n          return;\n        }\n        $activeInspectorPanel.set(\"style\");\n        requestAnimationFrame(() => {\n          emitCommand(\"focusStyleSourceInput\");\n        });\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"focusStyleSourceInput\",\n      description: \"Focus style source input\",\n      hidden: true,\n      handler: () => {\n        // This command is handled by the style panel component\n        // It's emitted by openStylePanel command\n      },\n    },\n    {\n      name: \"toggleStylePanelFocusMode\",\n      description: \"Toggle style panel focus mode\",\n      category: \"Style panel\",\n      defaultHotkeys: [\"alt+shift+s\"],\n      handler: () => {\n        setSetting(\n          \"stylePanelMode\",\n          getSetting(\"stylePanelMode\") === \"focus\" ? \"default\" : \"focus\"\n        );\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"toggleStylePanelAdvancedMode\",\n      description: \"Toggle style panel advanced mode\",\n      category: \"Style panel\",\n      defaultHotkeys: [\"alt+shift+a\"],\n      handler: () => {\n        setSetting(\n          \"stylePanelMode\",\n          getSetting(\"stylePanelMode\") === \"advanced\" ? \"default\" : \"advanced\"\n        );\n      },\n      disableOnInputLikeControls: true,\n    },\n    {\n      name: \"openSettingsPanel\",\n      description: \"Open settings panel\",\n      category: \"Panels\",\n      defaultHotkeys: [\"d\"],\n      handler: () => {\n        $activeInspectorPanel.set(\"settings\");\n      },\n      disableOnInputLikeControls: true,\n    },\n    makeBreakpointCommand(\"selectBreakpoint1\", 1),\n    makeBreakpointCommand(\"selectBreakpoint2\", 2),\n    makeBreakpointCommand(\"selectBreakpoint3\", 3),\n    makeBreakpointCommand(\"selectBreakpoint4\", 4),\n    makeBreakpointCommand(\"selectBreakpoint5\", 5),\n    makeBreakpointCommand(\"selectBreakpoint6\", 6),\n    makeBreakpointCommand(\"selectBreakpoint7\", 7),\n    makeBreakpointCommand(\"selectBreakpoint8\", 8),\n    makeBreakpointCommand(\"selectBreakpoint9\", 9),\n    {\n      name: \"copy\",\n      description: \"Copy selected instance\",\n      category: \"Navigator\",\n      handler: copyInstance,\n    },\n    {\n      name: \"paste\",\n      description: \"Paste copied instance\",\n      category: \"Navigator\",\n      handler: emitPaste,\n    },\n    {\n      name: \"cut\",\n      description: \"Cut selected instance\",\n      category: \"Navigator\",\n      handler: cutInstance,\n    },\n    {\n      name: \"toggleShow\",\n      description: \"Toggle instance visibility\",\n      category: \"Navigator\",\n      handler: () => {\n        const instancePath = $selectedInstancePath.get();\n        if (instancePath?.[0]) {\n          toggleInstanceShow(instancePath[0].instance.id);\n        }\n      },\n    },\n    {\n      name: \"deleteInstanceBuilder\",\n      label: \"Delete Instance\",\n      description: \"Delete selected instance\",\n      category: \"Navigator\",\n      defaultHotkeys: [\"backspace\", \"delete\"],\n      // See \"deleteInstanceCanvas\" for details on why the command is separated for the canvas and builder.\n      disableHotkeyOutsideApp: true,\n      disableOnInputLikeControls: true,\n      handler: deleteSelectedInstance,\n    },\n    {\n      name: \"duplicateInstance\",\n      description: \"Duplicate selected instance\",\n      category: \"Navigator\",\n      defaultHotkeys: [\"meta+d\", \"ctrl+d\"],\n      handler: () => {\n        const project = $project.get();\n        if (project === undefined) {\n          return;\n        }\n        if ($isDesignMode.get() === false) {\n          builderApi.toast.info(\"Duplicating is only allowed in design mode.\");\n          return;\n        }\n        const instancePath = $selectedInstancePath.get();\n        // global root or body are selected\n        if (instancePath === undefined || instancePath.length === 1) {\n          return;\n        }\n        const [selectedItem, parentItem] = instancePath;\n\n        updateWebstudioData((data) => {\n          const fragment = extractWebstudioFragment(\n            data,\n            selectedItem.instance.id\n          );\n          const { newInstanceIds } = insertWebstudioFragmentCopy({\n            data,\n            fragment,\n            availableVariables: findAvailableVariables({\n              ...data,\n              startingInstanceId: parentItem.instanceSelector[0],\n            }),\n            projectId: project.id,\n          });\n          const newRootInstanceId = newInstanceIds.get(\n            selectedItem.instance.id\n          );\n          if (newRootInstanceId === undefined) {\n            return;\n          }\n\n          // When the original child is auto-placed in a grid, ensure the\n          // duplicate is also auto-placed to prevent overlapping items.\n          // Manually positioned children keep their exact grid position.\n          if (\n            isAutoGridPlacement({\n              styles: data.styles,\n              styleSources: data.styleSources,\n              styleSourceSelections: data.styleSourceSelections,\n              instanceId: selectedItem.instance.id,\n            })\n          ) {\n            resetGridChildPlacement({\n              styles: data.styles,\n              styleSources: data.styleSources,\n              styleSourceSelections: data.styleSourceSelections,\n              instanceId: newRootInstanceId,\n            });\n          }\n\n          const parentInstance = data.instances.get(parentItem.instance.id);\n          if (parentInstance === undefined) {\n            return;\n          }\n          // put after current instance\n          const indexWithinChildren = parentInstance.children.findIndex(\n            (child) =>\n              child.type === \"id\" && child.value === selectedItem.instance.id\n          );\n          const position = indexWithinChildren + 1;\n          parentInstance.children.splice(position, 0, {\n            type: \"id\",\n            value: newRootInstanceId,\n          });\n          // select new instance\n          selectInstance([newRootInstanceId, ...parentItem.instanceSelector]);\n        });\n      },\n    },\n    {\n      name: \"editInstanceLabel\",\n      description: \"Edit instance label\",\n      category: \"Navigator\",\n      defaultHotkeys: [\"meta+e\", \"ctrl+e\"],\n      handler: () => {\n        const instancePath = $selectedInstancePath.get();\n        if (instancePath === undefined) {\n          return;\n        }\n        const [selectedItem] = instancePath;\n        $editingItemSelector.set(selectedItem.instanceSelector);\n      },\n    },\n    {\n      name: \"wrap\",\n      label: \"Wrap\",\n      description: \"Wrap\",\n      category: \"Navigator\",\n      defaultHotkeys: [\"meta+alt+g\", \"ctrl+alt+g\"],\n      keepCommandPanelOpen: true,\n      handler: () => {\n        showWrapComponentsList();\n      },\n    },\n    {\n      name: \"unwrap\",\n      description: \"Remove parent wrapper\",\n      category: \"Navigator\",\n      defaultHotkeys: [\"meta+shift+g\", \"ctrl+shift+g\"],\n      handler: () => unwrapInstance(),\n    },\n    {\n      name: \"convert\",\n      label: \"Convert\",\n      description: \"Convert component\",\n      category: \"Navigator\",\n      keepCommandPanelOpen: true,\n      handler: () => {\n        showConvertComponentsList();\n      },\n    },\n\n    {\n      name: \"pasteTailwind\",\n      label: \"Paste HTML with Tailwind classes\",\n      description: \"Convert Tailwind to CSS\",\n      handler: async () => {\n        const html = await navigator.clipboard.readText();\n        const parseResult = generateFragmentFromHtml(html);\n        const { skippedSelectors } = parseResult;\n        let fragment: WebstudioFragment = parseResult;\n        fragment = await denormalizeSrcProps(fragment);\n        fragment = await generateFragmentFromTailwind(fragment);\n        const result = insertWebstudioFragmentAt(fragment);\n        if (skippedSelectors.length > 0) {\n          builderApi.toast.info(\n            `Skipped nested selectors (no matching elements): ${skippedSelectors.join(\", \")}`\n          );\n        }\n        return result;\n      },\n    },\n\n    // history\n\n    {\n      name: \"undo\",\n      description: \"Undo last action\",\n      category: \"General\",\n      // safari use meta+z to reopen closed tabs, here added ctrl as alternative\n      defaultHotkeys: [\"meta+z\", \"ctrl+z\"],\n      disableOnInputLikeControls: true,\n      handler: () => {\n        serverSyncStore.undo();\n      },\n    },\n    {\n      name: \"redo\",\n      description: \"Redo last action\",\n      category: \"General\",\n      // safari use meta+z to reopen closed tabs, here added ctrl as alternative\n      defaultHotkeys: [\"meta+shift+z\", \"ctrl+shift+z\"],\n      disableOnInputLikeControls: true,\n      handler: () => {\n        serverSyncStore.redo();\n      },\n    },\n\n    {\n      name: \"save\",\n      description: \"Save project\",\n      category: \"General\",\n      defaultHotkeys: [\"meta+s\", \"ctrl+s\"],\n      handler: async () => {\n        toast.dismiss(\"save-success\");\n        try {\n          await isSyncIdle();\n          toast.success(\"Project saved successfully\", { id: \"save-success\" });\n        } catch (error) {\n          if (error instanceof Error) {\n            toast.error(error.message);\n          }\n        }\n      },\n    },\n\n    {\n      name: \"openCommandPanel\",\n      description: \"Open command panel\",\n      category: \"General\",\n      defaultHotkeys: [\"meta+k\", \"ctrl+k\"],\n      handler: () => {\n        if ($isDesignMode.get()) {\n          openCommandPanel();\n        }\n      },\n    },\n\n    {\n      name: \"deleteUnusedTokens\",\n      label: \"Delete unused tokens\",\n      description: \"Remove unused tokens\",\n      handler: () => {\n        openDeleteUnusedTokensDialog();\n      },\n    },\n\n    {\n      name: \"findDuplicateTokens\",\n      label: \"Find duplicate tokens\",\n      description: \"Find tokens with identical styles or names\",\n      handler: () => {\n        // Import needed to avoid circular dependency\n        import(\n          \"~/builder/features/command-panel/groups/duplicate-tokens-group\"\n        ).then(({ showDuplicateTokensView }) => {\n          showDuplicateTokensView();\n        });\n      },\n    },\n\n    {\n      name: \"deleteUnusedDataVariables\",\n      label: \"Delete unused data variables\",\n      description: \"Remove unused data variables\",\n      handler: () => {\n        openDeleteUnusedDataVariablesDialog();\n      },\n    },\n\n    {\n      name: \"deleteUnusedCssVariables\",\n      label: \"Delete unused CSS variables\",\n      description: \"Remove unused CSS variables\",\n      handler: () => {\n        openDeleteUnusedCssVariablesDialog();\n      },\n    },\n\n    {\n      name: \"deleteUnusedAssets\",\n      label: \"Delete unused assets\",\n      description: \"Remove unused assets\",\n      handler: () => {\n        openDeleteUnusedAssetsDialog();\n      },\n    },\n\n    {\n      name: \"openKeyboardShortcuts\",\n      description: \"View keyboard shortcuts\",\n      category: \"General\",\n      defaultHotkeys: [\"shift+?\"],\n      disableOnInputLikeControls: true,\n      handler: () => {\n        openKeyboardShortcutsDialog();\n      },\n    },\n  ],\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/add-style-input.tsx",
    "content": "import { forwardRef, useRef, useState, type KeyboardEvent } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport {\n  Box,\n  ComboboxAnchor,\n  ComboboxContent,\n  ComboboxItemDescription,\n  ComboboxListbox,\n  ComboboxListboxItem,\n  ComboboxRoot,\n  ComboboxScrollArea,\n  InputField,\n  NestedInputButton,\n  Text,\n  theme,\n  useCombobox,\n} from \"@webstudio-is/design-system\";\nimport {\n  propertiesData,\n  shorthandProperties,\n  keywordValues,\n  propertyDescriptions,\n  parseCssValue,\n} from \"@webstudio-is/css-data\";\nimport {\n  cssWideKeywords,\n  generateStyleMap,\n  mergeStyles,\n  toValue,\n  type CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport { composeEventHandlers } from \"~/shared/event-utils\";\nimport {\n  deleteProperty,\n  setProperty,\n} from \"~/builder/features/style-panel/shared/use-style-data\";\nimport { $availableVariables } from \"~/builder/features/style-panel/shared/model\";\nimport { parseStyleInput } from \"./parse-style-input\";\nimport { validateCssVariableName } from \"~/builder/shared/css-variable-utils\";\nimport { toast } from \"@webstudio-is/design-system\";\n\ntype SearchItem = {\n  property: string;\n  label: string;\n  value?: string;\n  key: string;\n};\n\nconst getNewPropertyDescription = (item: null | SearchItem) => {\n  let description: string | undefined = \"Add CSS property.\";\n  if (item?.property.startsWith(\"--\")) {\n    description = \"Create CSS variable.\";\n  }\n  if (item && propertyDescriptions[item.property]) {\n    description = propertyDescriptions[item.property];\n  }\n  return <Box css={{ width: theme.spacing[28] }}>{description}</Box>;\n};\n\nconst getAutocompleteItems = () => {\n  const autoCompleteItems: SearchItem[] = [];\n  for (const varValue of $availableVariables.get()) {\n    const property = `--${varValue.value}`;\n    autoCompleteItems.push({\n      // consider additional properties to be custom properties\n      key: property,\n      property,\n      label: property,\n    });\n  }\n  for (const property in propertiesData) {\n    autoCompleteItems.push({\n      // Allow matching \"gr te co\" -> \"grid-template-columns\"\n      key: property.replaceAll(\"-\", \" \"),\n      property,\n      label: property,\n    });\n  }\n\n  for (const property of shorthandProperties) {\n    autoCompleteItems.push({\n      // Allow matching \"gr te co\" -> \"grid-template-columns\"\n      key: property.replaceAll(\"-\", \" \"),\n      property,\n      label: property,\n    });\n  }\n\n  const ignoreValues = new Set([...cssWideKeywords, ...keywordValues.color]);\n\n  for (const [property, values] of Object.entries(keywordValues)) {\n    for (const value of values ?? []) {\n      if (ignoreValues.has(value)) {\n        continue;\n      }\n      autoCompleteItems.push({\n        // Allow matching \"gr te co\" -> \"grid-template-columns\"\n        key: `${property.replaceAll(\"-\", \" \")} ${value}`,\n        property,\n        value,\n        label: `${property}: ${value}`,\n      });\n    }\n  }\n\n  autoCompleteItems.sort((a, b) =>\n    Intl.Collator().compare(a.property, b.property)\n  );\n\n  return autoCompleteItems;\n};\n\nconst matchOrSuggestToCreate = (search: string, items: Array<SearchItem>) => {\n  const searchWithSpaces = search.startsWith(\"--\")\n    ? search\n    : search.replaceAll(\"-\", \" \");\n  const matched = matchSorter(items, searchWithSpaces, {\n    keys: [\"key\"],\n  });\n\n  // Limit the array to 100 elements\n  matched.length = Math.min(matched.length, 100);\n\n  if (matched.length === 0) {\n    const parsedStyleMap = parseStyleInput(search);\n    const styleMap = mergeStyles(parsedStyleMap);\n\n    // When parsedStyles is more than one, user entered a shorthand.\n    // We will suggest to insert their shorthand first.\n    if (styleMap.size > 1) {\n      matched.push({\n        key: \"\",\n        property: search,\n        label: `Create \"${search}\"`,\n      });\n    }\n    // Now we will suggest to insert each longhand separately.\n    for (const [property, value] of styleMap) {\n      matched.push({\n        key: \"\",\n        property,\n        value: toValue(value),\n        label: `Create \"${generateStyleMap(new Map([[property, value]]))}\"`,\n      });\n    }\n  }\n\n  return matched;\n};\n\n/**\n * Advanced search control supports following interactions\n *\n * - find property\n * - create custom property\n * - submit css declarations\n * - paste css declarations\n *\n */\nexport const AddStyleInput = forwardRef<\n  HTMLInputElement,\n  {\n    onClose: () => void;\n    onSubmit: (css: string) => void;\n    onFocus: () => void;\n    onBlur: () => void;\n  }\n>(({ onClose, onSubmit, onFocus, onBlur }, forwardedRef) => {\n  const [item, setItem] = useState<SearchItem>({\n    property: \"\",\n    label: \"\",\n    key: \"\",\n  });\n  const highlightedItemRef = useRef<SearchItem>();\n\n  const combobox = useCombobox<SearchItem>({\n    getItems: getAutocompleteItems,\n    itemToString: (item) => item?.label ?? \"\",\n    value: item,\n    getItemProps: () => ({ text: \"sentence\" }),\n    match: matchOrSuggestToCreate,\n    onChange: (input) => {\n      return setItem({\n        property: input ?? \"\",\n        label: input ?? \"\",\n        key: input ?? \"\",\n      });\n    },\n    onItemSelect: (item) => {\n      clear();\n      // When there is no value, property can be:\n      // - property without value: gap\n      // - declaration with value: gap: 10px\n      // - block: gap: 10px; margin: 20px;\n      if (item.value === undefined) {\n        return onSubmit(item.property);\n      }\n      onSubmit(`${item.property}: ${item.value}`);\n    },\n    onItemHighlight: (item) => {\n      const previousHighlightedItem = highlightedItemRef.current;\n      if (item?.value === undefined && previousHighlightedItem) {\n        deleteProperty(previousHighlightedItem.property as CssProperty, {\n          isEphemeral: true,\n        });\n        highlightedItemRef.current = undefined;\n        return;\n      }\n\n      if (item?.value) {\n        const value = parseCssValue(item.property as CssProperty, item.value);\n        setProperty(item.property as CssProperty)(value, {\n          isEphemeral: true,\n        });\n        highlightedItemRef.current = item;\n      }\n    },\n  });\n\n  const descriptionItem = combobox.items[combobox.highlightedIndex];\n  const description = getNewPropertyDescription(descriptionItem);\n  const descriptions = combobox.items.map(getNewPropertyDescription);\n  const inputProps = combobox.getInputProps();\n\n  const clear = () => {\n    setItem({ property: \"\", label: \"\", key: \"\" });\n  };\n\n  const handleEnter = (event: KeyboardEvent) => {\n    if (event.key === \"Enter\") {\n      const property = item.property.split(\":\")[0].trim();\n\n      // Validate CSS variable names before creating them\n      if (property.startsWith(\"--\")) {\n        const error = validateCssVariableName(property);\n        if (error) {\n          // For duplicate variables, show warning and continue updating the values\n          if (error.type === \"duplicate\") {\n            toast.warn(\n              `CSS variable \"${property}\" already exists. Its value will be updated.`\n            );\n          } else {\n            // Show error via toast and block submission\n            toast.error(error.message);\n            return;\n          }\n        }\n      }\n\n      clear();\n      onSubmit(item.property);\n    }\n  };\n\n  const handleEscape = (event: KeyboardEvent) => {\n    if (event.key === \"Escape\") {\n      clear();\n      onClose();\n    }\n  };\n\n  const handleDelete = (event: KeyboardEvent) => {\n    // When user hits backspace and there is nothing in the input - we hide the input\n    if (event.key === \"Backspace\" && combobox.inputValue === \"\") {\n      clear();\n      onClose();\n    }\n  };\n\n  const handleKeyDown = composeEventHandlers([\n    inputProps.onKeyDown,\n    handleEnter,\n    handleEscape,\n    handleDelete,\n  ]);\n\n  const handleBlur = composeEventHandlers([\n    inputProps.onBlur,\n    () => {\n      // When user clicks on a combobox item, input will receive blur event,\n      // but we don't want that to be handled upstream because input may get hidden without click getting handled.\n      if (combobox.isOpen === false) {\n        onBlur();\n      }\n    },\n  ]);\n\n  return (\n    <ComboboxRoot open={combobox.isOpen}>\n      <div {...combobox.getComboboxProps()}>\n        <ComboboxAnchor>\n          <InputField\n            {...inputProps}\n            onFocus={onFocus}\n            onBlur={handleBlur}\n            inputRef={forwardedRef}\n            onKeyDown={handleKeyDown}\n            placeholder=\"Add styles\"\n            suffix={<NestedInputButton {...combobox.getToggleButtonProps()} />}\n          />\n        </ComboboxAnchor>\n        <ComboboxContent>\n          <ComboboxListbox {...combobox.getMenuProps()}>\n            <ComboboxScrollArea>\n              {combobox.items.map((item, index) => (\n                <ComboboxListboxItem\n                  {...combobox.getItemProps({ item, index })}\n                  key={index}\n                >\n                  <Text variant=\"labels\" truncate css={{ maxWidth: \"25ch\" }}>\n                    {item.label}\n                  </Text>\n                </ComboboxListboxItem>\n              ))}\n            </ComboboxScrollArea>\n            {description && (\n              <ComboboxItemDescription descriptions={descriptions}>\n                {description}\n              </ComboboxItemDescription>\n            )}\n          </ComboboxListbox>\n        </ComboboxContent>\n      </div>\n    </ComboboxRoot>\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/css-editor-context-menu.tsx",
    "content": "import { useRef, type ReactNode } from \"react\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuTrigger,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport {\n  generateStyleMap,\n  mergeStyles,\n  toValue,\n  type CssProperty,\n  type CssStyleMap,\n} from \"@webstudio-is/css-engine\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\n\nexport const copyAttribute = \"data-declaration\";\n\nexport const CssEditorContextMenu = ({\n  children,\n  foundProperties,\n  declarations,\n  onPaste,\n  onDeleteProperty,\n  onDeleteAllDeclarations,\n}: {\n  children: ReactNode;\n  foundProperties: Array<CssProperty>;\n  declarations: Array<ComputedStyleDecl>;\n  onPaste: (cssText: string) => void;\n  onDeleteProperty: (property: CssProperty) => void;\n  onDeleteAllDeclarations: (styleMap: CssStyleMap) => void;\n}) => {\n  const lastClickedProperty = useRef<string>();\n\n  const handlePaste = () => {\n    navigator.clipboard.readText().then(onPaste);\n  };\n\n  // Gets all currently visible declarations based on what's in the search or filters.\n  const getAllDeclarations = () => {\n    // We want to only copy properties that are currently in front of the user.\n    // That includes search or any future filters.\n    const currentStyleMap: CssStyleMap = new Map();\n\n    for (const styleDecl of declarations) {\n      const isEmpty = toValue(styleDecl.cascadedValue) === \"\";\n      if (foundProperties.includes(styleDecl.property) && isEmpty === false) {\n        currentStyleMap.set(styleDecl.property, styleDecl.cascadedValue);\n      }\n    }\n    return currentStyleMap;\n  };\n\n  const handleCopyAll = () => {\n    const styleMap = getAllDeclarations();\n    const css = generateStyleMap(mergeStyles(styleMap));\n    navigator.clipboard.writeText(css);\n  };\n\n  const handleCopy = () => {\n    const property = lastClickedProperty.current as CssProperty;\n\n    if (property === undefined) {\n      return;\n    }\n    const styleDecl = declarations.find(\n      (styleDecl) => styleDecl.property === property\n    );\n\n    if (styleDecl === undefined) {\n      return;\n    }\n\n    const css = generateStyleMap(\n      new Map([[styleDecl.property, styleDecl.cascadedValue]])\n    );\n    navigator.clipboard.writeText(css);\n  };\n\n  const handleDelete = () => {\n    const property = lastClickedProperty.current as CssProperty;\n    const styleDecl = declarations.find(\n      (styleDecl) => styleDecl.property === property\n    );\n    if (styleDecl === undefined) {\n      return;\n    }\n    onDeleteProperty(property);\n  };\n\n  const handleDeleteAllDeclarations = () => {\n    const styleMap = getAllDeclarations();\n    onDeleteAllDeclarations(styleMap);\n  };\n\n  return (\n    <ContextMenu>\n      <ContextMenuTrigger\n        asChild\n        onPointerDown={(event) => {\n          if (!(event.target instanceof HTMLElement)) {\n            return;\n          }\n          const property =\n            event.target\n              .closest<HTMLElement>(`[${copyAttribute}]`)\n              ?.getAttribute(copyAttribute) ?? undefined;\n\n          lastClickedProperty.current = property;\n        }}\n      >\n        {children}\n      </ContextMenuTrigger>\n      <ContextMenuContent css={{ width: theme.spacing[25] }}>\n        <ContextMenuItem onSelect={handleCopy}>\n          Copy declaration\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={handleCopyAll}>\n          Copy all declarations\n        </ContextMenuItem>\n        <ContextMenuItem onSelect={handlePaste}>\n          Paste declarations\n        </ContextMenuItem>\n        <ContextMenuItem destructive onSelect={handleDelete}>\n          Delete declaration\n        </ContextMenuItem>\n        <ContextMenuItem destructive onSelect={handleDeleteAllDeclarations}>\n          Delete all declarations\n        </ContextMenuItem>\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/css-editor.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport { CssEditor as CssEditorComponent } from \"./css-editor\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\n\nexport const CSSEditor = () => {\n  const declarations = [\n    [\n      \"background-image\",\n      {\n        type: \"layers\",\n        value: [{ type: \"keyword\", value: \"none\" }],\n      },\n    ],\n    [\"accent-color\", { type: \"keyword\", value: \"red\" }],\n    [\"align-content\", { type: \"keyword\", value: \"normal\" }],\n    [\"opacity\", { type: \"unit\", unit: \"number\", value: 11.2 }],\n  ].map(([property, value]) => {\n    return {\n      property,\n      source: { name: \"local\" },\n      cascadedValue: value,\n      computedValue: value,\n      usedValue: value,\n    } as ComputedStyleDecl;\n  });\n\n  return (\n    <StorySection title=\"CSS Editor\">\n      <CssEditorComponent\n        declarations={declarations}\n        onDeleteProperty={() => undefined}\n        onSetProperty={() => () => undefined}\n        onAddDeclarations={() => undefined}\n        onDeleteAllDeclarations={() => undefined}\n      />\n    </StorySection>\n  );\n};\n\nexport default {\n  title: \"Style panel/CSS Editor\",\n  component: CSSEditor,\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/css-editor.tsx",
    "content": "import { mergeRefs } from \"@react-aria/utils\";\nimport * as colorjs from \"colorjs.io/fn\";\nimport {\n  memo,\n  useEffect,\n  useRef,\n  useState,\n  type ChangeEvent,\n  type ComponentProps,\n  type RefObject,\n} from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport {\n  Box,\n  Flex,\n  Label,\n  SearchField,\n  Separator,\n  Text,\n  theme,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport {\n  camelCaseProperty,\n  keywordValues,\n  propertiesData,\n  propertyDescriptions,\n} from \"@webstudio-is/css-data\";\nimport {\n  toValue,\n  type CssProperty,\n  type CssStyleMap,\n} from \"@webstudio-is/css-engine\";\n// @todo all style panel stuff needs to be moved to shared and/or decoupled from style panel\nimport { CssValueInputContainer } from \"../../features/style-panel/shared/css-value-input\";\nimport { $availableVariables } from \"../../features/style-panel/shared/model\";\nimport { PropertyInfo } from \"../../features/style-panel/property-label\";\nimport { ColorPickerPopover } from \"@webstudio-is/design-system\";\nimport { useClientSupports } from \"~/shared/client-supports\";\nimport { CssEditorContextMenu, copyAttribute } from \"./css-editor-context-menu\";\nimport { AddStyleInput } from \"./add-style-input\";\nimport { parseStyleInput } from \"./parse-style-input\";\nimport type {\n  DeleteProperty,\n  SetProperty,\n} from \"../../features/style-panel/shared/use-style-data\";\nimport type { ComputedStyleDecl } from \"~/shared/style-object-model\";\n\n// Used to indent the values when they are on the next line. This way its easier to see\n// where the property ends and value begins, especially in case of presets.\nconst initialIndentation = `20px`;\n\nconst AdvancedPropertyLabel = ({\n  styleDecl,\n  onReset,\n  onDeleteProperty,\n}: {\n  styleDecl: ComputedStyleDecl;\n  onReset?: () => void;\n  onDeleteProperty: DeleteProperty;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n\n  const description =\n    propertyDescriptions[camelCaseProperty(styleDecl.property)];\n\n  return (\n    <Tooltip\n      open={isOpen}\n      onOpenChange={setIsOpen}\n      // prevent closing tooltip on content click\n      onPointerDown={(event) => event.preventDefault()}\n      triggerProps={{\n        onClick: (event) => {\n          if (event.altKey) {\n            event.preventDefault();\n            onDeleteProperty(styleDecl.property);\n            onReset?.();\n            return;\n          }\n          setIsOpen(true);\n        },\n      }}\n      content={\n        <PropertyInfo\n          title={\n            styleDecl.property.startsWith(\"--\")\n              ? \"CSS Variable\"\n              : styleDecl.property\n          }\n          description={description}\n          styles={[styleDecl]}\n          onReset={() => {\n            onDeleteProperty(styleDecl.property);\n            setIsOpen(false);\n            onReset?.();\n          }}\n          resetType=\"delete\"\n          link={propertiesData[styleDecl.property]?.mdnUrl}\n        />\n      }\n    >\n      <Label\n        color={styleDecl.source.name}\n        text=\"mono\"\n        css={{\n          backgroundColor: \"transparent\",\n          marginLeft: `-${initialIndentation}`,\n        }}\n      >\n        {styleDecl.property}\n      </Label>\n    </Tooltip>\n  );\n};\n\nconst AdvancedPropertyValue = ({\n  styleDecl,\n  onDeleteProperty,\n  onSetProperty,\n  onChangeComplete,\n  onReset,\n  inputRef: inputRefProp,\n}: {\n  styleDecl: ComputedStyleDecl;\n  onDeleteProperty: DeleteProperty;\n  onSetProperty: SetProperty;\n  onChangeComplete: ComponentProps<\n    typeof CssValueInputContainer\n  >[\"onChangeComplete\"];\n  onReset: ComponentProps<typeof CssValueInputContainer>[\"onReset\"];\n  inputRef?: RefObject<HTMLInputElement>;\n}) => {\n  const inputRef = useRef<HTMLInputElement>(null);\n  let isColor = false;\n  try {\n    colorjs.parse(toValue(styleDecl.usedValue));\n    isColor = true;\n  } catch {\n    isColor = false;\n  }\n\n  return (\n    <CssValueInputContainer\n      inputRef={mergeRefs(inputRef, inputRefProp)}\n      variant=\"chromeless\"\n      text=\"mono\"\n      fieldSizing=\"content\"\n      prefix={\n        isColor && (\n          <ColorPickerPopover\n            value={styleDecl.usedValue}\n            onChange={(styleValue) => {\n              const options = { isEphemeral: true, listed: true };\n              if (styleValue) {\n                onSetProperty(styleDecl.property)(styleValue, options);\n              } else {\n                onDeleteProperty(styleDecl.property, options);\n              }\n            }}\n            onChangeComplete={(styleValue) => {\n              onSetProperty(styleDecl.property)(styleValue, { listed: true });\n            }}\n          />\n        )\n      }\n      property={styleDecl.property}\n      styleSource={styleDecl.source.name}\n      getOptions={() => [\n        ...(keywordValues[styleDecl.property] ?? []).map((value) => ({\n          type: \"keyword\" as const,\n          value,\n        })),\n        ...$availableVariables.get(),\n      ]}\n      value={styleDecl.cascadedValue}\n      onUpdate={(styleValue, options) => {\n        if (\n          styleValue.type === \"keyword\" &&\n          styleValue.value.startsWith(\"--\")\n        ) {\n          onSetProperty(styleDecl.property)(\n            { type: \"var\", value: styleValue.value.slice(2) },\n            { ...options, listed: true }\n          );\n        } else {\n          onSetProperty(styleDecl.property)(styleValue, {\n            ...options,\n            listed: true,\n          });\n        }\n      }}\n      onDelete={(options) => onDeleteProperty(styleDecl.property, options)}\n      onChangeComplete={onChangeComplete}\n      onReset={onReset}\n    />\n  );\n};\n\n/**\n * The Advanced section in the Style Panel on </> Global root has performance issues.\n * To fix this, we skip rendering properties not visible in the viewport using the contentvisibilityautostatechange event,\n * and the contentVisibility and containIntrinsicSize CSS properties.\n */\nconst LazyRender = ({ children }: ComponentProps<\"div\">) => {\n  const visibilityChangeEventSupported = useClientSupports(\n    () => \"oncontentvisibilityautostatechange\" in document.body\n  );\n  const ref = useRef<HTMLDivElement>(null);\n  const [isVisible, setIsVisible] = useState(!visibilityChangeEventSupported);\n\n  useEffect(() => {\n    if (!visibilityChangeEventSupported) {\n      return;\n    }\n\n    if (ref.current == null) {\n      return;\n    }\n\n    const controller = new AbortController();\n\n    ref.current.addEventListener(\n      \"contentvisibilityautostatechange\",\n      (event) => {\n        setIsVisible(!event.skipped);\n      },\n      {\n        signal: controller.signal,\n      }\n    );\n\n    return () => {\n      controller.abort();\n    };\n  }, [visibilityChangeEventSupported]);\n\n  return (\n    <div\n      ref={ref}\n      style={{\n        display: \"inline-block\",\n        contentVisibility: \"auto\",\n        // https://developer.mozilla.org/en-US/docs/Web/CSS/contain-intrinsic-size\n        // containIntrinsicSize is used to set the default size of an element before any content is loaded.\n        // This helps in preventing layout shifts and provides a better user experience by maintaining a consistent layout.\n        // It also affects the contentvisibilityautostatechange event to be called properly,\n        // with \"auto\" it will call it with skipped false for all initial elements.\n        // 44px is the height of the property row with 2 lines of text. This value can be adjusted slightly.\n        containIntrinsicSize: \"auto 44px\",\n      }}\n    >\n      {isVisible ? children : undefined}\n    </div>\n  );\n};\n\nconst AdvancedDeclarationLonghand = memo(\n  ({\n    styleDecl,\n    onChangeComplete,\n    onDeleteProperty,\n    onSetProperty,\n    onReset,\n    valueInputRef,\n    indentation = initialIndentation,\n  }: {\n    styleDecl: ComputedStyleDecl;\n    indentation?: string;\n    onReset?: () => void;\n    onSetProperty: SetProperty;\n    onDeleteProperty: DeleteProperty;\n    onChangeComplete?: ComponentProps<\n      typeof CssValueInputContainer\n    >[\"onChangeComplete\"];\n    valueInputRef?: RefObject<HTMLInputElement>;\n  }) => {\n    return (\n      <Flex\n        css={{ paddingLeft: indentation }}\n        wrap=\"wrap\"\n        align=\"center\"\n        justify=\"start\"\n        {...{ [copyAttribute]: styleDecl.property }}\n      >\n        <AdvancedPropertyLabel\n          styleDecl={styleDecl}\n          onReset={onReset}\n          onDeleteProperty={onDeleteProperty}\n        />\n        <Text\n          variant=\"mono\"\n          // Improves the visual separation of value from the property.\n          css={{\n            textIndent: \"-0.5ch\",\n            fontWeight: \"bold\",\n          }}\n        >\n          :\n        </Text>\n        <AdvancedPropertyValue\n          styleDecl={styleDecl}\n          onChangeComplete={onChangeComplete}\n          onReset={onReset}\n          onDeleteProperty={onDeleteProperty}\n          onSetProperty={onSetProperty}\n          inputRef={valueInputRef}\n        />\n      </Flex>\n    );\n  }\n);\n\nexport const CssEditor = ({\n  onDeleteProperty,\n  onSetProperty,\n  onAddDeclarations,\n  onDeleteAllDeclarations,\n  declarations,\n  showSearch = true,\n  virtualize = true,\n  propertiesPosition = \"bottom\",\n  recentProperties = [],\n  showAddStyleInput,\n  onToggleAddStyleInput,\n}: {\n  declarations: Array<ComputedStyleDecl>;\n  onDeleteProperty: DeleteProperty;\n  onSetProperty: SetProperty;\n  onAddDeclarations: (styleMap: CssStyleMap) => void;\n  onDeleteAllDeclarations: (styleMap: CssStyleMap) => void;\n  showSearch?: boolean;\n  propertiesPosition?: \"top\" | \"bottom\";\n  virtualize?: boolean;\n  recentProperties?: Array<CssProperty>;\n  showAddStyleInput?: boolean;\n  onToggleAddStyleInput?: (show: boolean) => void;\n}) => {\n  const addPropertyInputRef = useRef<HTMLInputElement>(null);\n  const lastRecentValueInputRef = useRef<HTMLInputElement>(null);\n  const lastRegularValueInputRef = useRef<HTMLInputElement>(null);\n  const searchInputRef = useRef<HTMLInputElement>(null);\n  const [searchProperties, setSearchProperties] =\n    useState<Array<CssProperty>>();\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (showAddStyleInput) {\n      addPropertyInputRef.current?.focus();\n    }\n  }, [showAddStyleInput]);\n\n  const declarationsMap = new Map(\n    declarations.map((decl) => [decl.property, decl])\n  );\n\n  const advancedProperties = declarations.map(({ property }) => property);\n\n  const currentProperties =\n    searchProperties ??\n    advancedProperties.filter(\n      (property) => recentProperties.includes(property) === false\n    );\n\n  const showRecentProperties =\n    recentProperties.length > 0 && searchProperties === undefined;\n\n  const handleInsertStyles = (cssText: string) => {\n    const styleMap = parseStyleInput(cssText);\n    if (styleMap.size === 0) {\n      return new Map();\n    }\n    onAddDeclarations(styleMap);\n    return styleMap;\n  };\n\n  const handleAbortSearch = () => {\n    if (containerRef.current) {\n      containerRef.current.style.minHeight = \"auto\";\n    }\n    setSearchProperties(undefined);\n  };\n\n  const handleSearch = (event: ChangeEvent<HTMLInputElement>) => {\n    const search = event.target.value.trim().replaceAll(\"-\", \" \");\n\n    if (search === \"\") {\n      return handleAbortSearch();\n    }\n    // This keeps container height big enough to avoid scroll position jumping around while user types.\n    if (containerRef.current) {\n      containerRef.current.style.height = `${window.innerHeight}px`;\n      // Min height is needed as long as the search is active.\n      containerRef.current.style.minHeight = `${window.innerHeight}px`;\n      requestAnimationFrame(() => {\n        // We can't keep it permanently because we need user to see all of the content.\n        // Fixed height only needed temporarily while react rerenders the tree to avoid jumps.\n        if (containerRef.current) {\n          containerRef.current.style.height = \"auto\";\n        }\n      });\n    }\n\n    const styles = declarations.map(({ property, cascadedValue }) => {\n      return {\n        key: `${property.replaceAll(\"-\", \" \")} ${toValue(cascadedValue)}`,\n        property,\n      };\n    });\n\n    const matched = matchSorter(styles, search, {\n      keys: [\"key\"],\n    }).map(({ property }) => property);\n\n    setSearchProperties(matched);\n  };\n\n  const afterChangingStyles = () => {\n    onToggleAddStyleInput?.(false);\n    requestAnimationFrame(() => {\n      // We are either focusing the last value input from the recent list if available or the search input.\n      const element =\n        lastRecentValueInputRef.current ??\n        lastRegularValueInputRef.current ??\n        searchInputRef.current;\n\n      element?.focus();\n      element?.select();\n    });\n  };\n\n  const handleDeleteProperty: DeleteProperty = (property, options = {}) => {\n    onDeleteProperty(property, options);\n    if (options.isEphemeral === true) {\n      return;\n    }\n    setSearchProperties(\n      searchProperties?.filter((searchProperty) => searchProperty !== property)\n    );\n  };\n\n  const handleDeleteAllDeclarations = (styleMap: CssStyleMap) => {\n    setSearchProperties(\n      searchProperties?.filter(\n        (searchProperty) => styleMap.has(searchProperty) === false\n      )\n    );\n    onDeleteAllDeclarations(styleMap);\n  };\n\n  const recentPropertiesAndAddStyleInput = (\n    <Flex\n      direction=\"column\"\n      css={{ paddingInline: theme.panel.paddingInline, gap: 2 }}\n    >\n      {showRecentProperties &&\n        recentProperties.map((property) => {\n          const styleDecl = declarationsMap.get(property);\n          if (styleDecl === undefined) {\n            return;\n          }\n          return (\n            <AdvancedDeclarationLonghand\n              styleDecl={styleDecl}\n              key={property}\n              valueInputRef={lastRecentValueInputRef}\n              onChangeComplete={(event) => {\n                if (event.type === \"enter\") {\n                  onToggleAddStyleInput?.(true);\n                }\n              }}\n              onReset={afterChangingStyles}\n              onDeleteProperty={handleDeleteProperty}\n              onSetProperty={onSetProperty}\n            />\n          );\n        })}\n      <Box\n        css={\n          showAddStyleInput\n            ? { paddingTop: theme.spacing[3] }\n            : // We hide it visually so you can tab into it to get shown.\n              { overflow: \"hidden\", height: 0 }\n        }\n      >\n        <AddStyleInput\n          onSubmit={(cssText: string) => {\n            const styles = handleInsertStyles(cssText);\n            if (styles.size > 0) {\n              afterChangingStyles();\n            }\n          }}\n          onClose={afterChangingStyles}\n          onFocus={() => {\n            onToggleAddStyleInput?.(true);\n          }}\n          onBlur={() => {\n            onToggleAddStyleInput?.(false);\n          }}\n          ref={addPropertyInputRef}\n        />\n      </Box>\n    </Flex>\n  );\n\n  return (\n    <Box css={{ isolation: \"isolate\" }}>\n      {showSearch && (\n        <Box\n          css={{\n            padding: theme.panel.padding,\n            position: \"sticky\",\n            top: 0,\n            background: theme.colors.backgroundPanel,\n            zIndex: 1,\n          }}\n        >\n          <SearchField\n            inputRef={searchInputRef}\n            onChange={handleSearch}\n            onAbort={handleAbortSearch}\n          />\n        </Box>\n      )}\n      <CssEditorContextMenu\n        onPaste={handleInsertStyles}\n        onDeleteProperty={handleDeleteProperty}\n        onDeleteAllDeclarations={handleDeleteAllDeclarations}\n        declarations={declarations}\n        foundProperties={\n          searchProperties ?? [...recentProperties, ...currentProperties]\n        }\n      >\n        <Flex gap=\"1\" direction=\"column\">\n          {propertiesPosition === \"bottom\" && (\n            <>\n              {recentPropertiesAndAddStyleInput}\n              {showRecentProperties && <Separator />}\n            </>\n          )}\n          <Flex\n            direction=\"column\"\n            css={{\n              paddingInline: theme.panel.paddingInline,\n              gap: 2,\n            }}\n            ref={containerRef}\n          >\n            {currentProperties.map((property) => {\n              const styleDecl = declarationsMap.get(property);\n              if (styleDecl === undefined) {\n                return;\n              }\n\n              const declarationElement = (\n                <AdvancedDeclarationLonghand\n                  styleDecl={styleDecl}\n                  onDeleteProperty={handleDeleteProperty}\n                  onSetProperty={onSetProperty}\n                  valueInputRef={lastRegularValueInputRef}\n                  key={property}\n                />\n              );\n              // When using it in keyframes with layout where delcarations are on top of add input button,\n              // we need to focus last added value, but the logic for waiting for the last rendered event would add up complexity.\n              // For now we just manually avoid virtualization in this layout.\n              if (virtualize) {\n                return (\n                  <LazyRender key={property}>{declarationElement}</LazyRender>\n                );\n              }\n              return declarationElement;\n            })}\n          </Flex>\n          {propertiesPosition === \"top\" && (\n            <>\n              {showRecentProperties && <Separator />}\n              {recentPropertiesAndAddStyleInput}\n            </>\n          )}\n        </Flex>\n      </CssEditorContextMenu>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/index.ts",
    "content": "export { CssEditor } from \"./css-editor\";\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/parse-style-input.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { parseStyleInput } from \"./parse-style-input\";\n\ndescribe(\"parseStyleInput\", () => {\n  test(\"parses custom property\", () => {\n    const result = parseStyleInput(\"--custom-color\");\n    expect(result).toEqual(\n      new Map([[\"--custom-color\", { type: \"unparsed\", value: \"\" }]])\n    );\n  });\n\n  test(\"parses longhand property\", () => {\n    const result = parseStyleInput(\"color\");\n    expect(result).toEqual(\n      new Map([[\"color\", { type: \"keyword\", value: \"unset\" }]])\n    );\n  });\n\n  test(\"parses shorthand property\", () => {\n    const result = parseStyleInput(\"margin\");\n    expect(result).toEqual(\n      new Map([\n        [\"margin-top\", { type: \"keyword\", value: \"unset\" }],\n        [\"margin-right\", { type: \"keyword\", value: \"unset\" }],\n        [\"margin-bottom\", { type: \"keyword\", value: \"unset\" }],\n        [\"margin-left\", { type: \"keyword\", value: \"unset\" }],\n      ])\n    );\n  });\n\n  test(\"trims whitespace\", () => {\n    const result = parseStyleInput(\"  color  \");\n    expect(result).toEqual(\n      new Map([[\"color\", { type: \"keyword\", value: \"unset\" }]])\n    );\n  });\n\n  test(\"handles unparsable regular property\", () => {\n    const result = parseStyleInput(\"notapro perty\");\n    expect(result).toEqual(new Map());\n  });\n\n  test(\"converts unknown property to custom property assuming user forgot to add --\", () => {\n    const result = parseStyleInput(\"notaproperty\");\n    expect(result).toEqual(\n      new Map([[\"--notaproperty\", { type: \"unparsed\", value: \"\" }]])\n    );\n  });\n\n  test(\"parses single property-value pair\", () => {\n    const result = parseStyleInput(\"color: red\");\n    expect(result).toEqual(\n      new Map([[\"color\", { type: \"keyword\", value: \"red\" }]])\n    );\n  });\n\n  test(\"parses multiple property-value pairs\", () => {\n    const result = parseStyleInput(\"color: red; display: block\");\n    expect(result).toEqual(\n      new Map([\n        [\"color\", { type: \"keyword\", value: \"red\" }],\n        [\"display\", { type: \"keyword\", value: \"block\" }],\n      ])\n    );\n  });\n\n  test(\"parses multiple property-value pairs, one is invalid\", () => {\n    const result = parseStyleInput(\"color: red; somethinginvalid: block\");\n    expect(result).toEqual(\n      new Map([\n        [\"color\", { type: \"keyword\", value: \"red\" }],\n        [\"--somethinginvalid\", { type: \"unparsed\", value: \"block\" }],\n      ])\n    );\n  });\n\n  test(\"parses custom property with value\", () => {\n    const result = parseStyleInput(\"--custom-color: red\");\n    expect(result).toEqual(\n      new Map([[\"--custom-color\", { type: \"unparsed\", value: \"red\" }]])\n    );\n  });\n\n  test(\"handles malformed style block\", () => {\n    const result = parseStyleInput(\"color: red; invalid;\");\n    expect(result).toEqual(\n      new Map([[\"color\", { type: \"keyword\", value: \"red\" }]])\n    );\n  });\n\n  test(\"output property with invalid value\", () => {\n    const result = parseStyleInput(\"rotate: z 0;\");\n    expect(result).toEqual(\n      new Map([[\"rotate\", { type: \"invalid\", value: \"z 0\" }]])\n    );\n  });\n\n  test(\"preserves -webkit-text-stroke as shorthand property, not as CSS variable\", () => {\n    const result = parseStyleInput(\"-webkit-text-stroke: 1px red\");\n    expect(result).toEqual(\n      new Map([\n        [\n          \"-webkit-text-stroke\",\n          {\n            type: \"tuple\",\n            value: [\n              { type: \"unit\", unit: \"px\", value: 1 },\n              { type: \"keyword\", value: \"red\" },\n            ],\n          },\n        ],\n      ])\n    );\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-editor/parse-style-input.ts",
    "content": "import {\n  propertiesData,\n  parseCss,\n  shorthandProperties,\n} from \"@webstudio-is/css-data\";\nimport { type CssProperty, type CssStyleMap } from \"@webstudio-is/css-engine\";\nimport { lexer } from \"css-tree\";\n\n// When user provides only a property name, we need to make it `property:;` to be able to parse it.\nconst ensureValue = (css: string) => {\n  css = css.trim();\n\n  // Is it a custom property \"--foo\"?\n  if (css.startsWith(\"--\") && lexer.match(\"<custom-ident>\", css).matched) {\n    return `${css}:;`;\n  }\n  // Is it a known longhand property?\n  if (propertiesData[css as CssProperty]) {\n    return `${css}:;`;\n  }\n  // Is it a known shorthand property?\n  if (\n    shorthandProperties.includes(css as (typeof shorthandProperties)[number])\n  ) {\n    return `${css}:;`;\n  }\n  // Is it a custom property without dashes \"--foo\"?\n  if (lexer.match(\"<custom-ident>\", `--${css}`).matched) {\n    return `--${css}:;`;\n  }\n\n  return css;\n};\n\n/**\n * Does several attempts to parse:\n * - Custom property \"--foo\"\n * - Known regular property \"color\"\n * - Custom property without -- (user forgot to add)\n * - Custom property and value: --foo: red\n * - Property and value: color: red\n * - Multiple properties: color: red; background: blue\n */\nexport const parseStyleInput = (css: string): CssStyleMap => {\n  css = ensureValue(css);\n  const styles = parseCss(`selector{${css}}`);\n  const styleMap: CssStyleMap = new Map();\n  for (const { property, value } of styles) {\n    if (property.startsWith(\"--\")) {\n      styleMap.set(property, value);\n      continue;\n    }\n    // somethingunknown: red; -> --somethingunknown: red;\n    // but keep known shorthands like -webkit-text-stroke as-is\n    if (\n      propertiesData[property] === undefined &&\n      shorthandProperties.includes(\n        property as (typeof shorthandProperties)[number]\n      ) === false\n    ) {\n      styleMap.set(`--${property}`, value);\n      continue;\n    }\n    // @todo This should be returning { type: \"guaranteedInvalid\" }\n    styleMap.set(property, value);\n  }\n  return styleMap;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-variable-utils.test.tsx",
    "content": "import { test, expect, describe } from \"vitest\";\nimport { atom } from \"nanostores\";\nimport type { StyleDecl, Styles, Prop } from \"@webstudio-is/sdk\";\nimport type { StyleProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { parseCssValue } from \"@webstudio-is/css-data\";\nimport { $styles } from \"~/shared/sync/data-stores\";\nimport {\n  findCssVariableUsagesByInstance,\n  validateCssVariableName,\n  performCssVariableRename,\n  updateVarReferencesInProps,\n} from \"./css-variable-utils\";\n\n// Mock the nano-states module\nconst mockStyles = atom<Styles>(new Map());\n\n// Replace the actual store with our mock\nObject.defineProperty($styles, \"get\", {\n  value: () => mockStyles.get(),\n});\n\n// Helper to create a StyleDecl\nconst createStyleDecl = (\n  styleSourceId: string,\n  breakpointId: string,\n  property: string,\n  value: string\n): StyleDecl => ({\n  styleSourceId,\n  breakpointId,\n  property: property as StyleProperty,\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  value: parseCssValue(property as any, value),\n});\n\ndescribe(\"validateCssVariableName\", () => {\n  test(\"returns required error for empty name\", () => {\n    mockStyles.set(new Map());\n\n    const error = validateCssVariableName(\"\");\n    expect(error).toEqual({\n      type: \"required\",\n      message: \"CSS variable name cannot be empty\",\n    });\n  });\n\n  test(\"returns required error for whitespace-only name\", () => {\n    mockStyles.set(new Map());\n\n    const error = validateCssVariableName(\"   \");\n    expect(error).toEqual({\n      type: \"required\",\n      message: \"CSS variable name cannot be empty\",\n    });\n  });\n\n  test(\"returns invalid error for name without --\", () => {\n    mockStyles.set(new Map());\n\n    const error = validateCssVariableName(\"my-color\");\n    expect(error).toEqual({\n      type: \"invalid\",\n      message: 'CSS variable name must start with \"--\"',\n    });\n  });\n\n  test(\"returns undefined for valid unique name\", () => {\n    const styles = new Map([\n      [\n        \"local1:base:--existing::\",\n        createStyleDecl(\"local1\", \"base\", \"--existing\", \"red\"),\n      ],\n    ]);\n    mockStyles.set(styles);\n\n    const error = validateCssVariableName(\"--new-color\");\n    expect(error).toBeUndefined();\n  });\n\n  test(\"returns duplicate error for existing name\", () => {\n    const styles = new Map([\n      [\n        \"local1:base:--my-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--my-color\", \"red\"),\n      ],\n    ]);\n    mockStyles.set(styles);\n\n    const error = validateCssVariableName(\"--my-color\");\n    expect(error).toEqual({\n      type: \"duplicate\",\n      message: 'CSS variable \"--my-color\" already exists',\n    });\n  });\n\n  test(\"allows renaming variable to same name\", () => {\n    const styles = new Map([\n      [\n        \"local1:base:--my-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--my-color\", \"red\"),\n      ],\n    ]);\n    mockStyles.set(styles);\n\n    const error = validateCssVariableName(\"--my-color\", \"--my-color\");\n    expect(error).toBeUndefined();\n  });\n});\n\ndescribe(\"findCssVariableUsagesByInstance\", () => {\n  test(\"does not track definitions\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--my-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--my-color\", \"red\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should not track definitions, only var() references\n    expect(counts.size).toBe(0);\n    expect(instances.size).toBe(0);\n  });\n\n  test(\"tracks var() references\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n      [\n        \"box2\",\n        {\n          instanceId: \"box2\",\n          values: [\"local2\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary-color\", \"blue\"),\n      ],\n      [\n        \"local2:base:color::\",\n        createStyleDecl(\"local2\", \"base\", \"color\", \"var(--primary-color)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.size).toBe(1);\n    expect(counts.get(\"--primary-color\")).toBe(1);\n    expect(instances.get(\"--primary-color\")).toEqual(new Set([\"box2\"]));\n  });\n\n  test(\"tracks only var() references not definitions\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n      [\n        \"box2\",\n        {\n          instanceId: \"box2\",\n          values: [\"local2\"],\n        },\n      ],\n      [\n        \"box3\",\n        {\n          instanceId: \"box3\",\n          values: [\"local3\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      // box1 defines --color\n      [\n        \"local1:base:--color::\",\n        createStyleDecl(\"local1\", \"base\", \"--color\", \"red\"),\n      ],\n      // box2 uses --color\n      [\n        \"local2:base:backgroundColor::\",\n        createStyleDecl(\"local2\", \"base\", \"backgroundColor\", \"var(--color)\"),\n      ],\n      // box3 defines and uses --color\n      [\n        \"local3:base:--color::\",\n        createStyleDecl(\"local3\", \"base\", \"--color\", \"blue\"),\n      ],\n      [\n        \"local3:base:color::\",\n        createStyleDecl(\"local3\", \"base\", \"color\", \"var(--color)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.size).toBe(1);\n    expect(counts.get(\"--color\")).toBe(2);\n    // Only box2 and box3 use var(--color), box1 only defines it\n    expect(instances.get(\"--color\")).toEqual(new Set([\"box2\", \"box3\"]));\n  });\n\n  test(\"handles multiple variables\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color::\",\n        createStyleDecl(\"local1\", \"base\", \"--color\", \"red\"),\n      ],\n      [\n        \"local1:base:--size::\",\n        createStyleDecl(\"local1\", \"base\", \"--size\", \"16px\"),\n      ],\n      [\n        \"local1:base:fontSize::\",\n        createStyleDecl(\"local1\", \"base\", \"fontSize\", \"var(--size)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.size).toBe(1);\n    // --color is only defined, not used\n    expect(counts.get(\"--color\")).toBeUndefined();\n    // --size is used via var()\n    expect(counts.get(\"--size\")).toBe(1);\n    expect(instances.get(\"--size\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"distinguishes between definition and usage\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"definer\",\n        {\n          instanceId: \"definer\",\n          values: [\"definerLocal\"],\n        },\n      ],\n      [\n        \"user\",\n        {\n          instanceId: \"user\",\n          values: [\"userLocal\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      // definer instance defines the variable\n      [\n        \"definerLocal:base:--theme-color::\",\n        createStyleDecl(\"definerLocal\", \"base\", \"--theme-color\", \"blue\"),\n      ],\n      // user instance uses the variable\n      [\n        \"userLocal:base:color::\",\n        createStyleDecl(\"userLocal\", \"base\", \"color\", \"var(--theme-color)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.size).toBe(1);\n    expect(counts.get(\"--theme-color\")).toBe(1);\n    // Should only track the user, not the definer\n    expect(instances.get(\"--theme-color\")).toEqual(new Set([\"user\"]));\n    expect(instances.get(\"--theme-color\")?.has(\"definer\")).toBe(false);\n  });\n\n  test(\"counts multiple references on same instance\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--spacing::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing\", \"16px\"),\n      ],\n      // Same instance uses the variable three times\n      [\n        \"local1:base:marginTop::\",\n        createStyleDecl(\"local1\", \"base\", \"marginTop\", \"var(--spacing)\"),\n      ],\n      [\n        \"local1:base:marginBottom::\",\n        createStyleDecl(\"local1\", \"base\", \"marginBottom\", \"var(--spacing)\"),\n      ],\n      [\n        \"local1:base:padding::\",\n        createStyleDecl(\"local1\", \"base\", \"padding\", \"var(--spacing)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.size).toBe(1);\n    // Should count all 3 usages, not just 1\n    expect(counts.get(\"--spacing\")).toBe(3);\n    // But only 1 instance\n    expect(instances.get(\"--spacing\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"handles unparsed values with var()\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    // parseCssValue returns unparsed type for calc() expressions\n    // Now we DO track var() inside unparsed strings\n    const styles = new Map([\n      [\n        \"local1:base:--spacing::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing\", \"16px\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\"local1\", \"base\", \"width\", \"calc(var(--spacing) * 2)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should track var() references inside unparsed calc() expressions\n    expect(counts.size).toBe(1);\n    expect(counts.get(\"--spacing\")).toBe(1);\n    expect(instances.get(\"--spacing\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"tracks var() inside calc() function\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--test1::\",\n        createStyleDecl(\"local1\", \"base\", \"--test1\", \"10px\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\"local1\", \"base\", \"width\", \"calc(var(--test1))\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should track var() references inside calc() expressions\n    expect(counts.size).toBe(1);\n    expect(counts.get(\"--test1\")).toBe(1);\n    expect(instances.get(\"--test1\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"tracks var() inside complex calc() with multiple variables\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--spacing::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing\", \"10px\"),\n      ],\n      [\n        \"local1:base:--multiplier::\",\n        createStyleDecl(\"local1\", \"base\", \"--multiplier\", \"2\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"width\",\n          \"calc(var(--spacing) * var(--multiplier))\"\n        ),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.size).toBe(2);\n    expect(counts.get(\"--spacing\")).toBe(1);\n    expect(counts.get(\"--multiplier\")).toBe(1);\n    expect(instances.get(\"--spacing\")).toEqual(new Set([\"box1\"]));\n    expect(instances.get(\"--multiplier\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"tracks var() with fallback values\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary-color\", \"red\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"color\",\n          \"var(--primary-color, blue)\"\n        ),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--primary-color\")).toBe(1);\n    expect(instances.get(\"--primary-color\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"tracks var() with nested fallbacks\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary\", \"blue\"),\n      ],\n      [\n        \"local1:base:--secondary::\",\n        createStyleDecl(\"local1\", \"base\", \"--secondary\", \"green\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"color\",\n          \"var(--primary, var(--secondary, red))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--primary\")).toBe(1);\n    expect(counts.get(\"--secondary\")).toBe(1);\n  });\n\n  test(\"tracks var() with CSS variable as fallback (var(--primary, --fallback))\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary\", \"blue\"),\n      ],\n      [\n        \"local1:base:--fallback::\",\n        createStyleDecl(\"local1\", \"base\", \"--fallback\", \"red\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"color\",\n          \"var(--primary, --fallback)\"\n        ),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Both the primary and fallback variable should be tracked\n    expect(counts.get(\"--primary\")).toBe(1);\n    expect(counts.get(\"--fallback\")).toBe(1);\n    expect(instances.get(\"--primary\")).toEqual(new Set([\"box1\"]));\n    expect(instances.get(\"--fallback\")).toEqual(new Set([\"box1\"]));\n  });\n\n  test(\"tracks multiple var() with variable fallbacks in same value\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color1::\",\n        createStyleDecl(\"local1\", \"base\", \"--color1\", \"red\"),\n      ],\n      [\n        \"local1:base:--color2::\",\n        createStyleDecl(\"local1\", \"base\", \"--color2\", \"blue\"),\n      ],\n      [\n        \"local1:base:--fallback1::\",\n        createStyleDecl(\"local1\", \"base\", \"--fallback1\", \"yellow\"),\n      ],\n      [\n        \"local1:base:--fallback2::\",\n        createStyleDecl(\"local1\", \"base\", \"--fallback2\", \"green\"),\n      ],\n      [\n        \"local1:base:background::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"background\",\n          \"linear-gradient(var(--color1, --fallback1), var(--color2, --fallback2))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--color1\")).toBe(1);\n    expect(counts.get(\"--color2\")).toBe(1);\n    expect(counts.get(\"--fallback1\")).toBe(1);\n    expect(counts.get(\"--fallback2\")).toBe(1);\n  });\n\n  test(\"tracks deeply nested var() with variable fallbacks\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--first::\",\n        createStyleDecl(\"local1\", \"base\", \"--first\", \"value\"),\n      ],\n      [\n        \"local1:base:--second::\",\n        createStyleDecl(\"local1\", \"base\", \"--second\", \"value\"),\n      ],\n      [\n        \"local1:base:--third::\",\n        createStyleDecl(\"local1\", \"base\", \"--third\", \"value\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"color\",\n          \"var(--first, var(--second, --third))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--first\")).toBe(1);\n    expect(counts.get(\"--second\")).toBe(1);\n    expect(counts.get(\"--third\")).toBe(1);\n  });\n\n  test(\"tracks variables in gradient\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--angle::\",\n        createStyleDecl(\"local1\", \"base\", \"--angle\", \"45deg\"),\n      ],\n      [\n        \"local1:base:--color1::\",\n        createStyleDecl(\"local1\", \"base\", \"--color1\", \"red\"),\n      ],\n      [\n        \"local1:base:--color2::\",\n        createStyleDecl(\"local1\", \"base\", \"--color2\", \"blue\"),\n      ],\n      [\n        \"local1:base:backgroundImage::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"backgroundImage\",\n          \"linear-gradient(var(--angle), var(--color1), var(--color2))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--angle\")).toBe(1);\n    expect(counts.get(\"--color1\")).toBe(1);\n    expect(counts.get(\"--color2\")).toBe(1);\n  });\n\n  test(\"tracks variables with hyphens and numbers\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--spacing-level-1::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing-level-1\", \"10px\"),\n      ],\n      [\n        \"local1:base:padding::\",\n        createStyleDecl(\"local1\", \"base\", \"padding\", \"var(--spacing-level-1)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--spacing-level-1\")).toBe(1);\n  });\n\n  test(\"does not track variable definitions as usages\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary-color\", \"blue\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Defining --primary-color should not count as using it\n    expect(counts.size).toBe(0);\n  });\n\n  test(\"tracks var() in HTML Embed code\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary-color\", \"blue\"),\n      ],\n      [\n        \"local1:base:--bg-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--bg-color\", \"white\"),\n      ],\n    ]);\n\n    const props = new Map([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"box1\",\n          type: \"string\" as const,\n          name: \"code\",\n          value: `<style>\n          .my-class {\n            color: var(--primary-color);\n            background: var(--bg-color);\n          }\n        </style>`,\n        },\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props,\n    });\n\n    expect(counts.get(\"--primary-color\")).toBe(1);\n    expect(counts.get(\"--bg-color\")).toBe(1);\n    expect(instances.get(\"--primary-color\")?.has(\"box1\")).toBe(true);\n    expect(instances.get(\"--bg-color\")?.has(\"box1\")).toBe(true);\n  });\n});\n\ndescribe(\"performCssVariableRename\", () => {\n  test(\"updates property declarations\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--my-color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--my-color\" as StyleProperty,\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--my-color\",\n      \"--new-color\"\n    );\n\n    expect(result.size).toBe(1);\n    const [decl] = Array.from(result.values());\n    expect(decl.property).toBe(\"--new-color\");\n    expect(decl.value).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"updates var() references\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--my-color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--my-color\" as StyleProperty,\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: { type: \"var\", value: \"my-color\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--my-color\",\n      \"--new-color\"\n    );\n\n    expect(result.size).toBe(2);\n    const colorDecl = Array.from(result.values()).find(\n      (d) => d.property === \"color\"\n    );\n    expect(colorDecl?.value).toEqual({ type: \"var\", value: \"new-color\" });\n  });\n\n  test(\"updates nested var() references\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:transform\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"transform\" as StyleProperty,\n          value: {\n            type: \"function\",\n            name: \"rotate\",\n            args: {\n              type: \"var\",\n              value: \"rotation-angle\",\n            },\n          } as StyleValue,\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--rotation-angle\",\n      \"--angle\"\n    );\n\n    const transformDecl = Array.from(result.values())[0];\n    expect(transformDecl.value).toMatchObject({\n      type: \"function\",\n      name: \"rotate\",\n      args: {\n        type: \"var\",\n        value: \"angle\",\n      },\n    });\n  });\n\n  test(\"updates array of var() references\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:boxShadow\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"boxShadow\" as StyleProperty,\n          value: {\n            type: \"tuple\",\n            value: [\n              { type: \"var\", value: \"shadow-x\" },\n              { type: \"var\", value: \"shadow-y\" },\n              { type: \"var\", value: \"shadow-color\" },\n            ],\n          },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--shadow-color\",\n      \"--color-shadow\"\n    );\n\n    const shadowDecl = Array.from(result.values())[0];\n    expect(shadowDecl.value).toMatchObject({\n      type: \"tuple\",\n      value: [\n        { type: \"var\", value: \"shadow-x\" },\n        { type: \"var\", value: \"shadow-y\" },\n        { type: \"var\", value: \"color-shadow\" },\n      ],\n    });\n  });\n\n  test(\"updates var() with keyword fallback (fallback unchanged)\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: {\n            type: \"var\",\n            value: \"primary\",\n            fallback: { type: \"keyword\", value: \"blue\" },\n          },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--primary\",\n      \"--primary-color\"\n    );\n\n    const colorDecl = Array.from(result.values())[0];\n    expect(colorDecl.value).toMatchObject({\n      type: \"var\",\n      value: \"primary-color\",\n      fallback: { type: \"keyword\", value: \"blue\" },\n    });\n  });\n\n  test(\"updates primary variable when it has a fallback\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: {\n            type: \"var\",\n            value: \"old-primary\",\n            fallback: { type: \"keyword\", value: \"blue\" },\n          } as StyleValue,\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--old-primary\",\n      \"--new-primary\"\n    );\n\n    const colorDecl = Array.from(result.values())[0];\n    expect(colorDecl.value).toMatchObject({\n      type: \"var\",\n      value: \"new-primary\",\n      fallback: { type: \"keyword\", value: \"blue\" },\n    });\n  });\n\n  test(\"updates var() primary value when it has a keyword fallback\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: {\n            type: \"var\",\n            value: \"theme-color\",\n            fallback: { type: \"keyword\", value: \"blue\" },\n          },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--theme-color\",\n      \"--brand-color\"\n    );\n\n    const colorDecl = Array.from(result.values())[0];\n    expect(colorDecl.value).toMatchObject({\n      type: \"var\",\n      value: \"brand-color\",\n      fallback: { type: \"keyword\", value: \"blue\" },\n    });\n  });\n\n  test(\"handles multiple declarations of same variable\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId1:base:--my-var\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId1\",\n          property: \"--my-var\" as StyleProperty,\n          value: { type: \"keyword\", value: \"value1\" },\n        },\n      ],\n      [\n        \"styleSourceId2:base:--my-var\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId2\",\n          property: \"--my-var\" as StyleProperty,\n          value: { type: \"keyword\", value: \"value2\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(styles, \"--my-var\", \"--renamed\");\n\n    expect(result.size).toBe(2);\n    const decls = Array.from(result.values());\n    expect(decls.every((d) => d.property === \"--renamed\")).toBe(true);\n  });\n\n  test(\"does not modify unrelated variables\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--my-color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--my-color\" as StyleProperty,\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:--other-color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--other-color\" as StyleProperty,\n          value: { type: \"keyword\", value: \"blue\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: { type: \"var\", value: \"other-color\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--my-color\",\n      \"--new-color\"\n    );\n\n    expect(result.size).toBe(3);\n    const otherColorDecl = Array.from(result.values()).find(\n      (d) => d.property === \"--other-color\"\n    );\n    expect(otherColorDecl?.property).toBe(\"--other-color\");\n\n    const colorDecl = Array.from(result.values()).find(\n      (d) => d.property === \"color\"\n    );\n    expect(colorDecl?.value).toEqual({ type: \"var\", value: \"other-color\" });\n  });\n\n  test(\"handles values with no references\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--my-color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--my-color\" as StyleProperty,\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:fontSize\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"fontSize\" as StyleProperty,\n          value: { type: \"unit\", value: 16, unit: \"px\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--my-color\",\n      \"--new-color\"\n    );\n\n    expect(result.size).toBe(2);\n    const fontSizeDecl = Array.from(result.values()).find(\n      (d) => d.property === \"fontSize\"\n    );\n    // Value should be unchanged\n    expect(fontSizeDecl?.value).toEqual({\n      type: \"unit\",\n      value: 16,\n      unit: \"px\",\n    });\n  });\n});\n\ndescribe(\"renameCssVariable\", () => {\n  test(\"updates var() references in HTML Embed code\", () => {\n    const props = new Map([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"embed1\",\n          name: \"code\",\n          type: \"string\" as const,\n          value: `<style>\n  .custom { \n    color: var(--theme-color);\n    background: var(--theme-color, blue);\n  }\n</style>`,\n        },\n      ],\n    ]);\n\n    const oldProperty = \"--theme-color\";\n    const newProperty = \"--brand-color\";\n\n    // Simulate the rename logic for props\n    const regex = new RegExp(\n      `var\\\\(\\\\s*${oldProperty.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\b`,\n      \"g\"\n    );\n    const prop = props.get(\"prop1\");\n    const updatedValue = prop!.value.replace(regex, `var(${newProperty}`);\n\n    // Verify both occurrences were replaced\n    expect(updatedValue).toContain(\"var(--brand-color)\");\n    expect(updatedValue).not.toContain(\"var(--theme-color)\");\n    expect(updatedValue).toContain(\"var(--brand-color, blue)\");\n\n    // Verify the original had the old name\n    expect(prop!.value).toContain(\"var(--theme-color)\");\n  });\n});\n\ndescribe(\"updateVarReferencesInProps\", () => {\n  test(\"updates single var() reference in HTML Embed code\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: '<div style=\"color: var(--primary)\">Text</div>',\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(\n      props,\n      \"--primary\",\n      \"--main-color\"\n    );\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\n      '<div style=\"color: var(--main-color)\">Text</div>'\n    );\n    expect(prop!.value).not.toContain(\"--primary\");\n  });\n\n  test(\"updates multiple occurrences in same prop\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"var(--color) and var(--color, red)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--color\", \"--new-color\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"var(--new-color) and var(--new-color, red)\");\n    expect(prop!.value).not.toContain(\"var(--color\");\n  });\n\n  test(\"preserves fallback values\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"var(--bg, #fff)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--bg\", \"--background\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"var(--background, #fff)\");\n  });\n\n  test(\"updates var() with CSS variable fallback\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"color: var(--primary, --fallback);\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--fallback\", \"--default\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"color: var(--primary, --default);\");\n  });\n\n  test(\"updates primary in var() with CSS variable fallback\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"color: var(--primary, --fallback);\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--primary\", \"--main\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"color: var(--main, --fallback);\");\n  });\n\n  test(\"updates multiple variables in var() with multiple fallbacks\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value:\n            \"background: linear-gradient(var(--color1, --fb1), var(--color2, --fb2));\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--color1\", \"--primary\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\n      \"background: linear-gradient(var(--primary, --fb1), var(--color2, --fb2));\"\n    );\n  });\n\n  test(\"updates nested var() with variable fallbacks\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"color: var(--first, var(--second, --third));\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--second\", \"--backup\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"color: var(--first, var(--backup, --third));\");\n  });\n\n  test(\"handles special regex characters in variable name\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"var(--color-$special)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(\n      props,\n      \"--color-$special\",\n      \"--normal\"\n    );\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"var(--normal)\");\n  });\n\n  test(\"does not update when no matches found\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"var(--other)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--primary\", \"--main\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"var(--other)\");\n  });\n\n  test(\"ignores non-code props\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"className\",\n          type: \"string\",\n          value: \"var(--color)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--color\", \"--new-color\");\n    const prop = result.get(\"prop1\");\n\n    // Should not be updated because name is not \"code\"\n    expect(prop!.value).toBe(\"var(--color)\");\n  });\n\n  test(\"handles empty props map\", () => {\n    const props = new Map<string, Prop>();\n\n    const result = updateVarReferencesInProps(props, \"--primary\", \"--main\");\n\n    expect(result.size).toBe(0);\n  });\n\n  test(\"handles prop with empty value\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--color\", \"--new-color\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\"\");\n  });\n\n  test(\"handles whitespace variations in var()\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"var( --spacing ) and var(  --spacing, 10px)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(\n      props,\n      \"--spacing\",\n      \"--new-spacing\"\n    );\n    const prop = result.get(\"prop1\");\n\n    // Simple regex replacement preserves whitespace\n    expect(prop!.value).toBe(\n      \"var( --new-spacing ) and var(  --new-spacing, 10px)\"\n    );\n  });\n\n  test(\"uses lookahead to avoid partial matches\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \"var(--color) and var(--color-dark)\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--color\", \"--primary\");\n    const prop = result.get(\"prop1\");\n\n    // Only --color should be updated, not --color-dark (lookahead ensures complete match)\n    expect(prop!.value).toBe(\"var(--primary) and var(--color-dark)\");\n  });\n});\n\ndescribe(\"findCssVariableUsagesByInstance - edge cases and malformed syntax\", () => {\n  test(\"tracks variables with extra whitespace\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--spacing::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing\", \"10px\"),\n      ],\n      [\n        \"local1:base:padding::\",\n        createStyleDecl(\"local1\", \"base\", \"padding\", \"var( --spacing )\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--spacing\")).toBe(1);\n  });\n\n  test(\"tracks variables without spaces after var(\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color::\",\n        createStyleDecl(\"local1\", \"base\", \"--color\", \"red\"),\n      ],\n      [\n        \"local1:base:backgroundColor::\",\n        createStyleDecl(\"local1\", \"base\", \"backgroundColor\", \"var(--color)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--color\")).toBe(1);\n  });\n\n  test(\"tracks variables in uppercase VAR()\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--size::\",\n        createStyleDecl(\"local1\", \"base\", \"--size\", \"20px\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\"local1\", \"base\", \"width\", \"VAR(--size)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--size\")).toBe(1);\n  });\n\n  test(\"tracks variables with underscores\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--my_variable::\",\n        createStyleDecl(\"local1\", \"base\", \"--my_variable\", \"value\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--my_variable)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--my_variable\")).toBe(1);\n  });\n\n  test(\"tracks variables in nested calc expressions\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--base::\",\n        createStyleDecl(\"local1\", \"base\", \"--base\", \"10px\"),\n      ],\n      [\n        \"local1:base:--multiplier::\",\n        createStyleDecl(\"local1\", \"base\", \"--multiplier\", \"2\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"width\",\n          \"calc(calc(var(--base) * var(--multiplier)) + 5px)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--base\")).toBe(1);\n    expect(counts.get(\"--multiplier\")).toBe(1);\n  });\n\n  test(\"tracks variables in clamp()\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--min::\",\n        createStyleDecl(\"local1\", \"base\", \"--min\", \"10px\"),\n      ],\n      [\n        \"local1:base:--preferred::\",\n        createStyleDecl(\"local1\", \"base\", \"--preferred\", \"5vw\"),\n      ],\n      [\n        \"local1:base:--max::\",\n        createStyleDecl(\"local1\", \"base\", \"--max\", \"50px\"),\n      ],\n      [\n        \"local1:base:fontSize::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"fontSize\",\n          \"clamp(var(--min), var(--preferred), var(--max))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--min\")).toBe(1);\n    expect(counts.get(\"--preferred\")).toBe(1);\n    expect(counts.get(\"--max\")).toBe(1);\n  });\n\n  test(\"tracks variables in min() and max()\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--size-a::\",\n        createStyleDecl(\"local1\", \"base\", \"--size-a\", \"100px\"),\n      ],\n      [\n        \"local1:base:--size-b::\",\n        createStyleDecl(\"local1\", \"base\", \"--size-b\", \"50vw\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"width\",\n          \"min(var(--size-a), var(--size-b))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--size-a\")).toBe(1);\n    expect(counts.get(\"--size-b\")).toBe(1);\n  });\n\n  test(\"tracks variables in rgba()\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--red::\",\n        createStyleDecl(\"local1\", \"base\", \"--red\", \"255\"),\n      ],\n      [\n        \"local1:base:--alpha::\",\n        createStyleDecl(\"local1\", \"base\", \"--alpha\", \"0.5\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"color\",\n          \"rgba(var(--red), 0, 0, var(--alpha))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--red\")).toBe(1);\n    expect(counts.get(\"--alpha\")).toBe(1);\n  });\n\n  test(\"tracks variables in transform functions\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--rotate::\",\n        createStyleDecl(\"local1\", \"base\", \"--rotate\", \"45deg\"),\n      ],\n      [\n        \"local1:base:--scale::\",\n        createStyleDecl(\"local1\", \"base\", \"--scale\", \"1.5\"),\n      ],\n      [\n        \"local1:base:transform::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"transform\",\n          \"rotate(var(--rotate)) scale(var(--scale))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--rotate\")).toBe(1);\n    expect(counts.get(\"--scale\")).toBe(1);\n  });\n\n  test(\"tracks variables in filter functions\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--blur-amount::\",\n        createStyleDecl(\"local1\", \"base\", \"--blur-amount\", \"5px\"),\n      ],\n      [\n        \"local1:base:filter::\",\n        createStyleDecl(\"local1\", \"base\", \"filter\", \"blur(var(--blur-amount))\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--blur-amount\")).toBe(1);\n  });\n\n  test(\"tracks variables in url() with concatenation\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--image-path::\",\n        createStyleDecl(\"local1\", \"base\", \"--image-path\", \"/images/bg.jpg\"),\n      ],\n      [\n        \"local1:base:backgroundImage::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"backgroundImage\",\n          \"url(var(--image-path))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--image-path\")).toBe(1);\n  });\n\n  test(\"tracks variables with very long names\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const longVarName =\n      \"--this-is-a-very-long-variable-name-with-many-hyphens-and-words\";\n\n    const styles = new Map([\n      [\n        `local1:base:${longVarName}::`,\n        createStyleDecl(\"local1\", \"base\", longVarName, \"value\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", `var(${longVarName})`),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(longVarName)).toBe(1);\n  });\n\n  test(\"tracks variables in custom properties with var()\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--base-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--base-color\", \"blue\"),\n      ],\n      [\n        \"local1:base:--derived-color::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"--derived-color\",\n          \"var(--base-color)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--base-color\")).toBe(1);\n  });\n\n  test(\"tracks same variable used multiple times in one value\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--spacing::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing\", \"10px\"),\n      ],\n      [\n        \"local1:base:padding::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"padding\",\n          \"var(--spacing) var(--spacing) var(--spacing) var(--spacing)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should count as 1 usage since it's the same property\n    expect(counts.get(\"--spacing\")).toBe(1);\n  });\n\n  test(\"handles variables that look like numbers\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--100::\",\n        createStyleDecl(\"local1\", \"base\", \"--100\", \"value\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--100)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--100\")).toBe(1);\n  });\n\n  test(\"tracks variables with emoji (technically valid CSS)\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--😀::\",\n        createStyleDecl(\"local1\", \"base\", \"--😀\", \"happy\"),\n      ],\n      [\n        \"local1:base:content::\",\n        createStyleDecl(\"local1\", \"base\", \"content\", \"var(--😀)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--😀\")).toBe(1);\n  });\n\n  test(\"tracks variables in grid-template-* properties\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--col-width::\",\n        createStyleDecl(\"local1\", \"base\", \"--col-width\", \"1fr\"),\n      ],\n      [\n        \"local1:base:gridTemplateColumns::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"gridTemplateColumns\",\n          \"var(--col-width) var(--col-width) var(--col-width)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--col-width\")).toBe(1);\n  });\n\n  test(\"tracks variables in animation properties\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--duration::\",\n        createStyleDecl(\"local1\", \"base\", \"--duration\", \"2s\"),\n      ],\n      [\n        \"local1:base:animationDuration::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"animationDuration\",\n          \"var(--duration)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--duration\")).toBe(1);\n  });\n\n  test(\"tracks variables in box-shadow with multiple shadows\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--shadow-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--shadow-color\", \"rgba(0,0,0,0.2)\"),\n      ],\n      [\n        \"local1:base:boxShadow::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"boxShadow\",\n          \"0 2px 4px var(--shadow-color), 0 4px 8px var(--shadow-color)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--shadow-color\")).toBe(1);\n  });\n\n  test(\"tracks variables in incorrect syntax (missing closing paren)\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color::\",\n        createStyleDecl(\"local1\", \"base\", \"--color\", \"red\"),\n      ],\n      [\n        \"local1:base:backgroundColor::\",\n        // Malformed - missing closing paren\n        createStyleDecl(\"local1\", \"base\", \"backgroundColor\", \"var(--color\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should still track it because we're just searching for the string\n    expect(counts.get(\"--color\")).toBe(1);\n  });\n\n  test(\"tracks variables in string concatenation syntax (invalid but user might try)\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--prefix::\",\n        createStyleDecl(\"local1\", \"base\", \"--prefix\", \"hello\"),\n      ],\n      [\n        \"local1:base:content::\",\n        // Invalid syntax but user might try it\n        createStyleDecl(\"local1\", \"base\", \"content\", \"var(--prefix) + world\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--prefix\")).toBe(1);\n  });\n\n  test(\"tracks variables with spaces in names (technically invalid)\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--my color::\",\n        createStyleDecl(\"local1\", \"base\", \"--my color\", \"red\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--my color)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should track it even though it's invalid CSS\n    expect(counts.get(\"--my color\")).toBe(1);\n  });\n\n  test(\"distinguishes between similar variable names\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color::\",\n        createStyleDecl(\"local1\", \"base\", \"--color\", \"red\"),\n      ],\n      [\n        \"local1:base:--color-dark::\",\n        createStyleDecl(\"local1\", \"base\", \"--color-dark\", \"darkred\"),\n      ],\n      [\n        \"local1:base:--color-darker::\",\n        createStyleDecl(\"local1\", \"base\", \"--color-darker\", \"maroon\"),\n      ],\n      [\n        \"local1:base:backgroundColor::\",\n        createStyleDecl(\"local1\", \"base\", \"backgroundColor\", \"var(--color)\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--color-dark)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--color\")).toBe(1);\n    expect(counts.get(\"--color-dark\")).toBe(1);\n    expect(counts.get(\"--color-darker\")).toBeUndefined();\n  });\n});\n\ndescribe(\"Word boundary edge cases - no-space contexts\", () => {\n  test(\"tracks variable followed by percentage without space\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--opacity::\",\n        createStyleDecl(\"local1\", \"base\", \"--opacity\", \"50\"),\n      ],\n      [\n        \"local1:base:background::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"background\",\n          \"linear-gradient(90deg, red var(--opacity)%)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--opacity\")).toBe(1);\n  });\n\n  test(\"tracks variable in tight arithmetic without spaces\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--size::\",\n        createStyleDecl(\"local1\", \"base\", \"--size\", \"100px\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\"local1\", \"base\", \"width\", \"calc(var(--size)-10px)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--size\")).toBe(1);\n  });\n\n  test(\"tracks variable followed by closing paren then another function\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--blur::\",\n        createStyleDecl(\"local1\", \"base\", \"--blur\", \"5px\"),\n      ],\n      [\n        \"local1:base:--brightness::\",\n        createStyleDecl(\"local1\", \"base\", \"--brightness\", \"1.2\"),\n      ],\n      [\n        \"local1:base:filter::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"filter\",\n          \"blur(var(--blur))brightness(var(--brightness))\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--blur\")).toBe(1);\n    expect(counts.get(\"--brightness\")).toBe(1);\n  });\n\n  test(\"tracks variable in malformed concatenation\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--value::\",\n        createStyleDecl(\"local1\", \"base\", \"--value\", \"10\"),\n      ],\n      [\n        \"local1:base:content::\",\n        // Malformed but user might try it\n        createStyleDecl(\"local1\", \"base\", \"content\", \"var(--value)px\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--value\")).toBe(1);\n  });\n});\n\ndescribe(\"Renaming with potential interference\", () => {\n  test(\"renames --color to --color-new without breaking\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--color\" as StyleProperty,\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:backgroundColor\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"backgroundColor\" as StyleProperty,\n          value: { type: \"var\", value: \"color\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(styles, \"--color\", \"--color-new\");\n\n    const bgDecl = Array.from(result.values()).find(\n      (d) => d.property === \"backgroundColor\"\n    );\n    expect(bgDecl?.value).toEqual({ type: \"var\", value: \"color-new\" });\n\n    const colorDef = Array.from(result.values()).find(\n      (d) => d.property === \"--color-new\"\n    );\n    expect(colorDef).toBeDefined();\n    expect(colorDef?.value).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"renames --x to --xx without affecting --xxx\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--x\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--x\" as StyleProperty,\n          value: { type: \"keyword\", value: \"value1\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:--xxx\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--xxx\" as StyleProperty,\n          value: { type: \"keyword\", value: \"value2\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:width\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"width\" as StyleProperty,\n          value: { type: \"var\", value: \"x\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:height\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"height\" as StyleProperty,\n          value: { type: \"var\", value: \"xxx\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(styles, \"--x\", \"--xx\");\n\n    const widthDecl = Array.from(result.values()).find(\n      (d) => d.property === \"width\"\n    );\n    expect(widthDecl?.value).toEqual({ type: \"var\", value: \"xx\" });\n\n    const heightDecl = Array.from(result.values()).find(\n      (d) => d.property === \"height\"\n    );\n    // Should NOT be affected\n    expect(heightDecl?.value).toEqual({ type: \"var\", value: \"xxx\" });\n  });\n\n  test(\"handles variable in both var type and unparsed string\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--spacing\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--spacing\" as StyleProperty,\n          value: { type: \"keyword\", value: \"10px\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:padding\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"padding\" as StyleProperty,\n          value: { type: \"var\", value: \"spacing\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:width\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"width\" as StyleProperty,\n          value: {\n            type: \"unparsed\",\n            value: \"calc(100% - var(--spacing))\",\n          },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--spacing\",\n      \"--new-spacing\"\n    );\n\n    const paddingDecl = Array.from(result.values()).find(\n      (d) => (d.property as string) === \"padding\"\n    );\n    expect(paddingDecl?.value).toEqual({ type: \"var\", value: \"new-spacing\" });\n\n    const widthDecl = Array.from(result.values()).find(\n      (d) => (d.property as string) === \"width\"\n    );\n    expect(widthDecl?.value).toEqual({\n      type: \"unparsed\",\n      value: \"calc(100% - var(--new-spacing))\",\n    });\n  });\n});\n\ndescribe(\"HTML Embed edge cases\", () => {\n  test(\"tracks variables in minified CSS\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary\", \"blue\"),\n      ],\n      [\n        \"local1:base:--secondary::\",\n        createStyleDecl(\"local1\", \"base\", \"--secondary\", \"red\"),\n      ],\n    ]);\n\n    const props = new Map([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"box1\",\n          type: \"string\" as const,\n          name: \"code\",\n          value:\n            \"<style>.a{color:var(--primary);background:var(--secondary)}</style>\",\n        },\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props,\n    });\n\n    expect(counts.get(\"--primary\")).toBe(1);\n    expect(counts.get(\"--secondary\")).toBe(1);\n  });\n\n  test(\"tracks variables in inline styles within HTML\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--text-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--text-color\", \"navy\"),\n      ],\n      [\n        \"local1:base:--bg-color::\",\n        createStyleDecl(\"local1\", \"base\", \"--bg-color\", \"white\"),\n      ],\n    ]);\n\n    const props = new Map([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"box1\",\n          type: \"string\" as const,\n          name: \"code\",\n          value: `\n            <div style=\"color: var(--text-color)\">\n              <span style=\"background: var(--bg-color)\">Text</span>\n            </div>\n          `,\n        },\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props,\n    });\n\n    expect(counts.get(\"--text-color\")).toBe(1);\n    expect(counts.get(\"--bg-color\")).toBe(1);\n  });\n\n  test(\"tracks variables with multiple HTML elements\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--spacing::\",\n        createStyleDecl(\"local1\", \"base\", \"--spacing\", \"1rem\"),\n      ],\n    ]);\n\n    const props = new Map([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"box1\",\n          type: \"string\" as const,\n          name: \"code\",\n          value: `\n            <div style=\"margin: var(--spacing)\">\n              <p style=\"padding: var(--spacing)\">Content</p>\n              <span style=\"gap: var(--spacing)\">More</span>\n            </div>\n          `,\n        },\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props,\n    });\n\n    // Should count as 1 usage (same instance, even if multiple occurrences)\n    expect(counts.get(\"--spacing\")).toBe(1);\n  });\n\n  test(\"updates variables in minified CSS\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value: \".x{color:var(--old);background:var(--old)}\",\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--old\", \"--new\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\".x{color:var(--new);background:var(--new)}\");\n  });\n\n  test(\"updates variables in inline styles\", () => {\n    const props = new Map<string, Prop>([\n      [\n        \"prop1\",\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"code\",\n          type: \"string\",\n          value:\n            '<div style=\"color: var(--theme)\"><span style=\"background: var(--theme)\">Text</span></div>',\n        },\n      ],\n    ]);\n\n    const result = updateVarReferencesInProps(props, \"--theme\", \"--brand\");\n    const prop = result.get(\"prop1\");\n\n    expect(prop!.value).toBe(\n      '<div style=\"color: var(--brand)\"><span style=\"background: var(--brand)\">Text</span></div>'\n    );\n  });\n});\n\ndescribe(\"Deeply nested structures\", () => {\n  test(\"preserves deeply nested var in layers > function > tuple\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:filter\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"filter\" as StyleProperty,\n          value: {\n            type: \"layers\",\n            value: [\n              {\n                type: \"function\",\n                name: \"drop-shadow\",\n                args: {\n                  type: \"tuple\",\n                  value: [\n                    { type: \"unit\", value: 0, unit: \"px\" },\n                    { type: \"unit\", value: 0, unit: \"px\" },\n                    { type: \"var\", value: \"shadow-blur\" },\n                    { type: \"var\", value: \"shadow-color\" },\n                  ],\n                },\n              } as StyleValue,\n            ],\n          } as StyleValue,\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--shadow-blur\",\n      \"--blur-amount\"\n    );\n\n    const filterDecl = Array.from(result.values())[0];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const filterValue = filterDecl.value as any;\n\n    expect(filterValue.type).toBe(\"layers\");\n    expect(filterValue.value[0].type).toBe(\"function\");\n    expect(filterValue.value[0].args.type).toBe(\"tuple\");\n    expect(filterValue.value[0].args.value[2]).toEqual({\n      type: \"var\",\n      value: \"blur-amount\",\n    });\n    expect(filterValue.value[0].args.value[3]).toEqual({\n      type: \"var\",\n      value: \"shadow-color\",\n    });\n  });\n\n  test(\"handles shadow with var in all fields\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:boxShadow\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"boxShadow\" as StyleProperty,\n          value: {\n            type: \"layers\",\n            value: [\n              {\n                type: \"shadow\",\n                offsetX: { type: \"var\", value: \"offset-x\" },\n                offsetY: { type: \"var\", value: \"offset-y\" },\n                blur: { type: \"var\", value: \"blur-size\" },\n                spread: { type: \"var\", value: \"spread-size\" },\n                color: { type: \"var\", value: \"shadow-color\" },\n              },\n            ],\n          } as StyleValue,\n        },\n      ],\n    ]);\n\n    // Test renaming each field independently\n    let result = performCssVariableRename(styles, \"--offset-x\", \"--x\");\n    let shadowDecl = Array.from(result.values())[0];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    let shadowValue = (shadowDecl.value as any).value[0];\n    expect(shadowValue.offsetX).toEqual({ type: \"var\", value: \"x\" });\n\n    result = performCssVariableRename(styles, \"--blur-size\", \"--blur\");\n    shadowDecl = Array.from(result.values())[0];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    shadowValue = (shadowDecl.value as any).value[0];\n    expect(shadowValue.blur).toEqual({ type: \"var\", value: \"blur\" });\n\n    result = performCssVariableRename(styles, \"--shadow-color\", \"--color\");\n    shadowDecl = Array.from(result.values())[0];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    shadowValue = (shadowDecl.value as any).value[0];\n    expect(shadowValue.color).toEqual({ type: \"var\", value: \"color\" });\n  });\n\n  test(\"preserves 5-level nested structure\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:filter\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"filter\" as StyleProperty,\n          value: {\n            type: \"layers\",\n            value: [\n              {\n                type: \"function\",\n                name: \"drop-shadow\",\n                args: {\n                  type: \"tuple\",\n                  value: [\n                    {\n                      type: \"function\",\n                      name: \"calc\",\n                      args: {\n                        type: \"unparsed\",\n                        value: \"var(--deep-var) * 2\",\n                      },\n                    } as StyleValue,\n                  ],\n                },\n              } as StyleValue,\n            ],\n          } as StyleValue,\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--deep-var\",\n      \"--renamed-deep\"\n    );\n\n    const filterDecl = Array.from(result.values())[0];\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const filterValue = filterDecl.value as any;\n\n    expect(filterValue.value[0].args.value[0].args.value).toBe(\n      \"var(--renamed-deep) * 2\"\n    );\n  });\n});\n\ndescribe(\"Special character variable names\", () => {\n  test(\"handles variables with special regex chars in complex values\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color-$special::\",\n        createStyleDecl(\"local1\", \"base\", \"--color-$special\", \"red\"),\n      ],\n      [\n        \"local1:base:width::\",\n        createStyleDecl(\n          \"local1\",\n          \"base\",\n          \"width\",\n          \"calc(var(--color-$special) * 2)\"\n        ),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--color-$special\")).toBe(1);\n  });\n\n  test(\"renames variables with special regex chars\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--var-$test\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--var-$test\" as StyleProperty,\n          value: { type: \"keyword\", value: \"value\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: { type: \"var\", value: \"var-$test\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--var-$test\",\n      \"--var-$new\"\n    );\n\n    const colorDecl = Array.from(result.values()).find(\n      (d) => d.property === \"color\"\n    );\n    expect(colorDecl?.value).toEqual({ type: \"var\", value: \"var-$new\" });\n  });\n\n  test(\"handles variables with dots\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--version-1.0::\",\n        createStyleDecl(\"local1\", \"base\", \"--version-1.0\", \"value\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--version-1.0)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    expect(counts.get(\"--version-1.0\")).toBe(1);\n  });\n\n  test(\"handles variables with parentheses in name\", () => {\n    const styles = new Map<string, StyleDecl>([\n      [\n        \"styleSourceId:base:--var(test)\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"--var(test)\" as StyleProperty,\n          value: { type: \"keyword\", value: \"value\" },\n        },\n      ],\n      [\n        \"styleSourceId:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"styleSourceId\",\n          property: \"color\" as StyleProperty,\n          value: { type: \"var\", value: \"var(test)\" },\n        },\n      ],\n    ]);\n\n    const result = performCssVariableRename(\n      styles,\n      \"--var(test)\",\n      \"--var-test\"\n    );\n\n    const colorDecl = Array.from(result.values()).find(\n      (d) => d.property === \"color\"\n    );\n    expect(colorDecl?.value).toEqual({ type: \"var\", value: \"var-test\" });\n  });\n});\n\ndescribe(\"Performance and optimization validation\", () => {\n  test(\"only tracks defined variables, not undefined ones\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--defined::\",\n        createStyleDecl(\"local1\", \"base\", \"--defined\", \"value\"),\n      ],\n      [\n        \"local1:base:color::\",\n        // References undefined variable\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--undefined)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Should not track --undefined since it's not defined\n    expect(counts.get(\"--undefined\")).toBeUndefined();\n    expect(counts.size).toBe(0);\n  });\n\n  test(\"breaks early when finding referenced variables\", () => {\n    // This test validates the optimization in $referencedCssVariables\n    const styleSourceSelections = new Map([\n      [\n        \"box1\",\n        {\n          instanceId: \"box1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--used::\",\n        createStyleDecl(\"local1\", \"base\", \"--used\", \"value\"),\n      ],\n      [\n        \"local1:base:color1::\",\n        createStyleDecl(\"local1\", \"base\", \"color1\", \"var(--used)\"),\n      ],\n      [\n        \"local1:base:color2::\",\n        // Another reference to same variable - should not increase processing\n        createStyleDecl(\"local1\", \"base\", \"color2\", \"var(--used)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Both usages should be counted\n    expect(counts.get(\"--used\")).toBe(2);\n  });\n});\n\ndescribe(\"$unusedCssVariables and deleteUnusedCssVariables\", () => {\n  test(\"identifies unused variables correctly\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"root\",\n        {\n          instanceId: \"root\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--used::\",\n        createStyleDecl(\"local1\", \"base\", \"--used\", \"red\"),\n      ],\n      [\n        \"local1:base:--unused::\",\n        createStyleDecl(\"local1\", \"base\", \"--unused\", \"blue\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--used)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // --used should have 1 usage, --unused should have 0\n    expect(counts.get(\"--used\")).toBe(1);\n    expect(counts.get(\"--unused\")).toBeUndefined();\n  });\n\n  test(\"only considers variables associated with instances\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"root\",\n        {\n          instanceId: \"root\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    // orphaned-source is not in styleSourceSelections\n    const styles = new Map([\n      [\n        \"local1:base:--visible::\",\n        createStyleDecl(\"local1\", \"base\", \"--visible\", \"red\"),\n      ],\n      [\n        \"orphaned-source:base:--orphaned::\",\n        createStyleDecl(\"orphaned-source\", \"base\", \"--orphaned\", \"blue\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // --visible should be tracked (no usages, so undefined)\n    // --orphaned should not be tracked at all\n    expect(counts.has(\"--visible\")).toBe(false);\n    expect(counts.has(\"--orphaned\")).toBe(false);\n  });\n\n  test(\"detects variables used in HTML Embed code props\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"root\",\n        {\n          instanceId: \"root\",\n          values: [\"local1\"],\n        },\n      ],\n      [\n        \"embed\",\n        {\n          instanceId: \"embed\",\n          values: [\"local2\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--used-in-embed::\",\n        createStyleDecl(\"local1\", \"base\", \"--used-in-embed\", \"red\"),\n      ],\n      [\n        \"local1:base:--unused::\",\n        createStyleDecl(\"local1\", \"base\", \"--unused\", \"blue\"),\n      ],\n    ]);\n\n    const props = new Map([\n      [\n        \"embed-code\",\n        {\n          id: \"embed-code\",\n          instanceId: \"embed\",\n          type: \"string\" as const,\n          name: \"code\",\n          value: \"<style>.test { color: var(--used-in-embed); }</style>\",\n        },\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props,\n    });\n\n    // --used-in-embed should have 1 usage\n    expect(counts.get(\"--used-in-embed\")).toBe(1);\n    expect(counts.get(\"--unused\")).toBeUndefined();\n  });\n\n  test(\"handles variables on root instance\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"root\",\n        {\n          instanceId: \"root\",\n          values: [\"root-local\"],\n        },\n      ],\n      [\n        \"child\",\n        {\n          instanceId: \"child\",\n          values: [\"child-local\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"root-local:base:--root-var::\",\n        createStyleDecl(\"root-local\", \"base\", \"--root-var\", \"red\"),\n      ],\n      [\n        \"child-local:base:color::\",\n        createStyleDecl(\"child-local\", \"base\", \"color\", \"var(--root-var)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // --root-var should be used by child\n    expect(counts.get(\"--root-var\")).toBe(1);\n  });\n\n  test(\"distinguishes between similar variable names\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"root\",\n        {\n          instanceId: \"root\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--color::\",\n        createStyleDecl(\"local1\", \"base\", \"--color\", \"red\"),\n      ],\n      [\n        \"local1:base:--color-dark::\",\n        createStyleDecl(\"local1\", \"base\", \"--color-dark\", \"darkred\"),\n      ],\n      [\n        \"local1:base:background::\",\n        // Uses --color but not --color-dark\n        createStyleDecl(\"local1\", \"base\", \"background\", \"var(--color)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // --color should be used, --color-dark should not\n    expect(counts.get(\"--color\")).toBe(1);\n    expect(counts.get(\"--color-dark\")).toBeUndefined();\n  });\n\n  test(\"handles multiple instances defining the same variable\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"instance1\",\n        {\n          instanceId: \"instance1\",\n          values: [\"local1\"],\n        },\n      ],\n      [\n        \"instance2\",\n        {\n          instanceId: \"instance2\",\n          values: [\"local2\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--shared::\",\n        createStyleDecl(\"local1\", \"base\", \"--shared\", \"value1\"),\n      ],\n      [\n        \"local2:base:--shared::\",\n        createStyleDecl(\"local2\", \"base\", \"--shared\", \"value2\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--shared)\"),\n      ],\n    ]);\n\n    const { counts, instances } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // --shared should be used once\n    expect(counts.get(\"--shared\")).toBe(1);\n    // Usage is from instance1\n    expect(instances.get(\"--shared\")).toEqual(new Set([\"instance1\"]));\n  });\n\n  test(\"variables used only within same instance are counted\", () => {\n    const styleSourceSelections = new Map([\n      [\n        \"instance1\",\n        {\n          instanceId: \"instance1\",\n          values: [\"local1\"],\n        },\n      ],\n    ]);\n\n    const styles = new Map([\n      [\n        \"local1:base:--primary::\",\n        createStyleDecl(\"local1\", \"base\", \"--primary\", \"blue\"),\n      ],\n      [\n        \"local1:base:color::\",\n        createStyleDecl(\"local1\", \"base\", \"color\", \"var(--primary)\"),\n      ],\n    ]);\n\n    const { counts } = findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props: new Map(),\n    });\n\n    // Variable defined and used in same instance\n    expect(counts.get(\"--primary\")).toBe(1);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/css-variable-utils.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { atom, computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n  Button,\n  Text,\n  Flex,\n  theme,\n  InputField,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport type {\n  Instance,\n  StyleDecl,\n  StyleSourceSelections,\n  Props,\n} from \"@webstudio-is/sdk\";\nimport type {\n  StyleValue,\n  StyleProperty,\n  CustomProperty,\n} from \"@webstudio-is/css-engine\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport {\n  $styles,\n  $styleSourceSelections,\n  $props,\n} from \"~/shared/sync/data-stores\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { getStyleDeclKey } from \"@webstudio-is/sdk\";\n\nconst $isDeleteUnusedCssVariablesDialogOpen = atom(false);\n\nexport const openDeleteUnusedCssVariablesDialog = () => {\n  $isDeleteUnusedCssVariablesDialogOpen.set(true);\n};\n\n// Utility: Escape regex special characters\nconst escapeRegex = (str: string): string => {\n  return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n};\n\n// Utility: Create regex to match variable name with word boundary\n// This avoids matching --color inside --color-dark\n// Uses negative lookahead (?![\\\\w-]) to ensure variable name doesn't continue\n// with word characters or hyphens, preventing partial matches.\nconst createVarNameRegex = (varName: string): RegExp => {\n  return new RegExp(`${escapeRegex(varName)}(?![\\\\w-])`, \"g\");\n};\n\n// Traverse a StyleValue to find all var() references\nconst findVarReferences = (value: StyleValue, varName: string): boolean => {\n  const valueStr = toValue(value);\n  return createVarNameRegex(varName).test(valueStr);\n};\n\n// Find all CSS variable references in HTML Embed code props\nconst findVarReferencesInProps = (props: Props, varName: string): boolean => {\n  const regex = createVarNameRegex(varName);\n  for (const prop of props.values()) {\n    if (prop.type === \"string\" && prop.name === \"code\" && prop.value) {\n      if (regex.test(prop.value)) {\n        return true;\n      }\n    }\n  }\n  return false;\n};\n\n// Find CSS variable usage counts (how many times each variable is referenced via var())\nexport const findCssVariableUsagesByInstance = ({\n  styleSourceSelections,\n  styles,\n  props,\n}: {\n  styleSourceSelections: StyleSourceSelections;\n  styles: Map<string, StyleDecl>;\n  props: Props;\n}): {\n  counts: Map<string, number>;\n  instances: Map<string, Set<Instance[\"id\"]>>;\n} => {\n  const usageCounts = new Map<string, number>();\n  const usageInstances = new Map<string, Set<Instance[\"id\"]>>();\n\n  // Track which style sources belong to which instances\n  const instancesByStyleSource = new Map<string, Instance[\"id\"]>();\n  for (const { instanceId, values } of styleSourceSelections.values()) {\n    for (const styleSourceId of values) {\n      instancesByStyleSource.set(styleSourceId, instanceId);\n    }\n  }\n\n  // Helper to add a var reference\n  const addVarReference = (varName: string, instanceId: Instance[\"id\"]) => {\n    const count = usageCounts.get(varName) ?? 0;\n    usageCounts.set(varName, count + 1);\n\n    let instances = usageInstances.get(varName);\n    if (instances === undefined) {\n      instances = new Set();\n      usageInstances.set(varName, instances);\n    }\n    instances.add(instanceId);\n  };\n\n  // Collect all defined variables\n  // Performance optimization: Only check variables that are defined\n  // instead of searching for all possible var() patterns.\n  // This is O(defined_vars × styles) instead of O(all_patterns × styles).\n  const definedVariables = new Set<string>();\n  for (const styleDecl of styles.values()) {\n    if (styleDecl.property.startsWith(\"--\")) {\n      definedVariables.add(styleDecl.property);\n    }\n  }\n\n  // Track CSS variable references in StyleDecl values\n  for (const styleDecl of styles.values()) {\n    const instanceId = instancesByStyleSource.get(styleDecl.styleSourceId);\n    if (!instanceId) {\n      continue;\n    }\n\n    // Check each defined variable if it's used in this style\n    for (const varName of definedVariables) {\n      if (findVarReferences(styleDecl.value, varName)) {\n        addVarReference(varName, instanceId);\n      }\n    }\n  }\n\n  // Track CSS variable references in HTML Embed code props\n  for (const varName of definedVariables) {\n    if (findVarReferencesInProps(props, varName)) {\n      // Find which instance this belongs to\n      for (const prop of props.values()) {\n        if (prop.type === \"string\" && prop.name === \"code\" && prop.value) {\n          const regex = createVarNameRegex(varName);\n          if (regex.test(prop.value)) {\n            addVarReference(varName, prop.instanceId);\n          }\n        }\n      }\n    }\n  }\n\n  return { counts: usageCounts, instances: usageInstances };\n};\n\nconst $cssVariableUsageData = computed(\n  [$styleSourceSelections, $styles, $props],\n  (styleSourceSelections, styles, props) => {\n    return findCssVariableUsagesByInstance({\n      styleSourceSelections,\n      styles,\n      props,\n    });\n  }\n);\n\nexport const $usedCssVariablesInInstances = computed(\n  $cssVariableUsageData,\n  (data) => data.counts\n);\n\nexport const $cssVariableInstancesByVariable = computed(\n  $cssVariableUsageData,\n  (data) => data.instances\n);\n\n// Get all defined CSS variables (unique properties that start with --)\nexport const $definedCssVariables = computed($styles, (styles) => {\n  const definedVariables = new Set<CustomProperty>();\n  for (const styleDecl of styles.values()) {\n    if (styleDecl.property.startsWith(\"--\")) {\n      definedVariables.add(styleDecl.property as CustomProperty);\n    }\n  }\n  return definedVariables;\n});\n\n// Map CSS variables to the instances where they are defined\nexport const $cssVariableDefinitionsByVariable = computed(\n  [$styleSourceSelections, $styles],\n  (styleSourceSelections, styles) => {\n    const definitionsByVariable = new Map<string, Set<Instance[\"id\"]>>();\n\n    // Build map of styleSourceId to instanceId\n    const instancesByStyleSource = new Map<string, Instance[\"id\"]>();\n    for (const { instanceId, values } of styleSourceSelections.values()) {\n      for (const styleSourceId of values) {\n        instancesByStyleSource.set(styleSourceId, instanceId);\n      }\n    }\n\n    // Find all CSS variable definitions\n    for (const styleDecl of styles.values()) {\n      if (styleDecl.property.startsWith(\"--\")) {\n        const instanceId = instancesByStyleSource.get(styleDecl.styleSourceId);\n        if (instanceId) {\n          let instances = definitionsByVariable.get(styleDecl.property);\n          if (instances === undefined) {\n            instances = new Set();\n            definitionsByVariable.set(styleDecl.property, instances);\n          }\n          instances.add(instanceId);\n        }\n      }\n    }\n\n    return definitionsByVariable;\n  }\n);\n\n// Get all referenced CSS variables (from both styles and HTML Embed code props)\nexport const $referencedCssVariables = computed(\n  [$styles, $props],\n  (styles, props) => {\n    const referencedVariables = new Set<string>();\n\n    // Collect all defined variables first\n    const definedVariables = new Set<string>();\n    for (const styleDecl of styles.values()) {\n      if (styleDecl.property.startsWith(\"--\")) {\n        definedVariables.add(styleDecl.property);\n      }\n    }\n\n    // Check which defined variables are referenced\n    for (const varName of definedVariables) {\n      // Check in styles\n      // Performance optimization: We only need to know IF a variable is used (boolean),\n      // not HOW MANY times, so we break on first match.\n      for (const styleDecl of styles.values()) {\n        if (findVarReferences(styleDecl.value, varName)) {\n          referencedVariables.add(varName);\n          break;\n        }\n      }\n\n      // Check in props if not already found\n      if (\n        !referencedVariables.has(varName) &&\n        findVarReferencesInProps(props, varName)\n      ) {\n        referencedVariables.add(varName);\n      }\n    }\n\n    return referencedVariables;\n  }\n);\n\n// Get all unused CSS variables (defined in instances but not referenced)\nexport const $unusedCssVariables = computed(\n  [$cssVariableDefinitionsByVariable, $referencedCssVariables],\n  (definitionsByVariable, referencedVariables) => {\n    const unusedVariables = new Set<string>();\n    for (const varName of definitionsByVariable.keys()) {\n      if (!referencedVariables.has(varName)) {\n        unusedVariables.add(varName);\n      }\n    }\n    return unusedVariables;\n  }\n);\n\ntype CssVariableError =\n  | { type: \"required\"; message: string }\n  | { type: \"invalid\"; message: string }\n  | { type: \"duplicate\"; message: string };\n\nexport const validateCssVariableName = (\n  name: string,\n  currentProperty?: string\n): CssVariableError | undefined => {\n  // Check if name is required\n  if (name.trim().length === 0) {\n    return {\n      type: \"required\",\n      message: \"CSS variable name cannot be empty\",\n    };\n  }\n\n  // Ensure name starts with --\n  if (!name.startsWith(\"--\")) {\n    return {\n      type: \"invalid\",\n      message: 'CSS variable name must start with \"--\"',\n    };\n  }\n\n  // Check for duplicates within the same style source + breakpoint + state\n  const styles = $styles.get();\n  for (const styleDecl of styles.values()) {\n    if (\n      styleDecl.property === name &&\n      styleDecl.property !== currentProperty &&\n      styleDecl.property.startsWith(\"--\")\n    ) {\n      return {\n        type: \"duplicate\",\n        message: `CSS variable \"${name}\" already exists`,\n      };\n    }\n  }\n};\n\n// Update all var() references in a StyleValue\nconst updateVarReferences = (\n  value: StyleValue,\n  oldProperty: string,\n  newProperty: string\n): StyleValue => {\n  // For simple replacement, use JSON stringify/parse approach\n  // since toValue produces CSS string which is harder to parse back\n  let valueStr = JSON.stringify(value);\n\n  // Handle two patterns:\n  // 1. In var type objects: \"value\":\"variable-name\" (without --)\n  //    StyleValue stores var without -- prefix in \"value\" field\n  // 2. In literal strings: \"--variable-name\" (with --)\n  //    Unparsed strings contain full --variable-name syntax\n  // Both patterns are needed because StyleValue can contain both structured\n  // var types and unparsed CSS strings with var() references\n\n  // Strip -- prefix for var type replacement\n  const oldVarName = oldProperty.startsWith(\"--\")\n    ? oldProperty.slice(2)\n    : oldProperty;\n  const newVarName = newProperty.startsWith(\"--\")\n    ? newProperty.slice(2)\n    : newProperty;\n\n  // Replace in var type value fields: \"value\":\"old-name\" -> \"value\":\"new-name\"\n  // Use word boundary to avoid replacing \"color\" in \"color-dark\"\n  const varTypeRegex = new RegExp(\n    `(\"value\":\")${escapeRegex(oldVarName)}(?![\\\\w-])`,\n    \"g\"\n  );\n  valueStr = valueStr.replace(varTypeRegex, `$1${newVarName}`);\n\n  // Also replace literal --variable-name in unparsed strings\n  valueStr = valueStr.replace(createVarNameRegex(oldProperty), newProperty);\n\n  return JSON.parse(valueStr) as StyleValue;\n};\n\n// Core rename logic without transaction wrapper (for testing)\nexport const performCssVariableRename = (\n  styles: Map<string, StyleDecl>,\n  oldProperty: string,\n  newProperty: string\n): Map<string, StyleDecl> => {\n  const updatedStyles = new Map(styles);\n\n  // Update all StyleDecl with the old property name\n  const styleDeclsToUpdate: Array<{ key: string; decl: StyleDecl }> = [];\n  for (const [key, styleDecl] of updatedStyles) {\n    if (styleDecl.property === oldProperty) {\n      styleDeclsToUpdate.push({ key, decl: styleDecl });\n    }\n  }\n\n  for (const { key, decl } of styleDeclsToUpdate) {\n    updatedStyles.delete(key);\n    const newDecl = { ...decl, property: newProperty as StyleProperty };\n    updatedStyles.set(getStyleDeclKey(newDecl), newDecl);\n  }\n\n  // Update all var() references in StyleValue\n  const styleDeclsToUpdateRefs: Array<{ key: string; decl: StyleDecl }> = [];\n  for (const [key, styleDecl] of updatedStyles) {\n    styleDeclsToUpdateRefs.push({ key, decl: styleDecl });\n  }\n\n  for (const { key, decl } of styleDeclsToUpdateRefs) {\n    const newValue = updateVarReferences(decl.value, oldProperty, newProperty);\n    if (newValue !== decl.value) {\n      updatedStyles.set(key, { ...decl, value: newValue });\n    }\n  }\n\n  return updatedStyles;\n};\n\n// Update var() references in HTML Embed code props (pure function for testing)\nexport const updateVarReferencesInProps = (\n  props: Props,\n  oldProperty: string,\n  newProperty: string\n): Props => {\n  const updatedProps = new Map(props);\n  const regex = createVarNameRegex(oldProperty);\n\n  for (const [key, prop] of updatedProps) {\n    if (prop.type === \"string\" && prop.name === \"code\" && prop.value) {\n      const updatedValue = prop.value.replace(regex, newProperty);\n      if (updatedValue !== prop.value) {\n        updatedProps.set(key, { ...prop, value: updatedValue });\n      }\n    }\n  }\n\n  return updatedProps;\n};\n\nexport const renameCssVariable = (\n  oldProperty: string,\n  newProperty: string\n): CssVariableError | undefined => {\n  const validationError = validateCssVariableName(newProperty, oldProperty);\n  if (validationError) {\n    return validationError;\n  }\n\n  serverSyncStore.createTransaction([$styles, $props], (styles, props) => {\n    const updatedStyles = performCssVariableRename(\n      styles,\n      oldProperty,\n      newProperty\n    );\n\n    // Clear and repopulate the styles map\n    styles.clear();\n    for (const [key, value] of updatedStyles) {\n      styles.set(key, value);\n    }\n\n    // Update var() references in HTML Embed code props\n    const updatedProps = updateVarReferencesInProps(\n      props,\n      oldProperty,\n      newProperty\n    );\n    props.clear();\n    for (const [key, value] of updatedProps) {\n      props.set(key, value);\n    }\n  });\n\n  return;\n};\n\nexport const deleteUnusedCssVariables = () => {\n  const unusedVariables = $unusedCssVariables.get();\n\n  if (unusedVariables.size === 0) {\n    return 0;\n  }\n\n  // Delete all unused variable declarations\n  serverSyncStore.createTransaction([$styles], (styles) => {\n    const keysToDelete: string[] = [];\n\n    for (const [key, styleDecl] of styles) {\n      if (unusedVariables.has(styleDecl.property)) {\n        keysToDelete.push(key);\n      }\n    }\n\n    for (const key of keysToDelete) {\n      styles.delete(key);\n    }\n  });\n\n  return unusedVariables.size;\n};\n\ntype DeleteCssVariableDialogProps = {\n  cssVariable?: { property: string };\n  onClose: () => void;\n  onConfirm: (property: string) => void;\n};\n\nexport const DeleteCssVariableDialog = ({\n  cssVariable,\n  onClose,\n  onConfirm,\n}: DeleteCssVariableDialogProps) => {\n  return (\n    <Dialog\n      open={cssVariable !== undefined}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          // Prevent command panel from handling keyboard events\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete confirmation</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Text>{`Delete CSS variable \"${cssVariable?.property}\" from the project?`}</Text>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <Button\n              color=\"destructive\"\n              onClick={() => {\n                onConfirm(cssVariable!.property);\n                onClose();\n              }}\n            >\n              Delete\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ntype RenameCssVariableDialogProps = {\n  cssVariable?: { property: string };\n  onClose: () => void;\n  onConfirm: (oldProperty: string, newProperty: string) => void;\n};\n\nexport const RenameCssVariableDialog = ({\n  cssVariable,\n  onClose,\n  onConfirm,\n}: RenameCssVariableDialogProps) => {\n  const [name, setName] = useState(\"\");\n  const [error, setError] = useState<string>();\n\n  // Reset name and clear error when cssVariable changes\n  useEffect(() => {\n    if (cssVariable?.property !== undefined) {\n      setName(cssVariable.property);\n      setError(undefined);\n    }\n  }, [cssVariable?.property]);\n\n  const handleConfirm = () => {\n    const renameError = renameCssVariable(cssVariable!.property, name);\n    if (renameError) {\n      setError(renameError.message);\n      return;\n    }\n    onConfirm(cssVariable!.property, name);\n    onClose();\n  };\n\n  return (\n    <Dialog\n      open={cssVariable !== undefined}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          // Prevent command panel from handling keyboard events\n          event.stopPropagation();\n          if (event.key === \"Enter\" && !error) {\n            handleConfirm();\n          }\n        }}\n      >\n        <DialogTitle>Rename CSS Variable</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Flex direction=\"column\" gap=\"1\">\n            <InputField\n              value={name}\n              onChange={(event) => {\n                setName(event.target.value);\n                setError(undefined);\n              }}\n              color={error ? \"error\" : undefined}\n            />\n            {error && (\n              <Text color=\"destructive\" variant=\"monoBold\">\n                {error}\n              </Text>\n            )}\n          </Flex>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <Button color=\"primary\" onClick={handleConfirm}>\n              Rename\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst DeleteUnusedCssVariablesDialogContent = ({\n  onClose,\n}: {\n  onClose: () => void;\n}) => {\n  const unusedVariables = useStore($unusedCssVariables);\n  // Convert Set to Array for display\n  const unusedVariablesArray = Array.from(unusedVariables);\n\n  return (\n    <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n      {unusedVariablesArray.length === 0 ? (\n        <Text>There are no unused CSS variables to delete.</Text>\n      ) : (\n        <>\n          <Text>\n            Delete {unusedVariablesArray.length} unused CSS{\" \"}\n            {unusedVariablesArray.length === 1 ? \"variable\" : \"variables\"} from\n            the project?\n          </Text>\n          <Text\n            variant=\"mono\"\n            css={{\n              maxHeight: 200,\n              overflowY: \"auto\",\n              backgroundColor: theme.colors.backgroundPanel,\n              borderRadius: theme.borderRadius[4],\n              wordBreak: \"break-word\",\n            }}\n          >\n            {unusedVariablesArray.join(\", \")}\n          </Text>\n        </>\n      )}\n      <Flex direction=\"rowReverse\" gap=\"2\">\n        {unusedVariablesArray.length > 0 && (\n          <Button\n            color=\"destructive\"\n            autoFocus\n            onClick={() => {\n              const deletedCount = deleteUnusedCssVariables();\n              onClose();\n              if (deletedCount === 0) {\n                toast.info(\"No unused CSS variables to delete\");\n              } else {\n                toast.success(\n                  `Deleted ${deletedCount} unused CSS ${deletedCount === 1 ? \"variable\" : \"variables\"}`\n                );\n              }\n            }}\n          >\n            Delete\n          </Button>\n        )}\n        <DialogClose>\n          <Button color=\"ghost\">\n            {unusedVariablesArray.length > 0 ? \"Cancel\" : \"Close\"}\n          </Button>\n        </DialogClose>\n      </Flex>\n    </Flex>\n  );\n};\n\nexport const DeleteUnusedCssVariablesDialog = () => {\n  const open = useStore($isDeleteUnusedCssVariablesDialogOpen);\n  const handleClose = () => {\n    $isDeleteUnusedCssVariablesDialogOpen.set(false);\n  };\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          handleClose();\n        }\n      }}\n    >\n      <DialogContent\n        width={400}\n        onKeyDown={(event) => {\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete unused CSS variables</DialogTitle>\n        <DeleteUnusedCssVariablesDialogContent onClose={handleClose} />\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/data-variable-utils.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport { atom } from \"nanostores\";\nimport { $dataSources } from \"~/shared/sync/data-stores\";\nimport { validateDataVariableName } from \"./data-variable-utils\";\nimport type { DataSources, DataSource } from \"@webstudio-is/sdk\";\n\n// Mock the nano-states module\nconst mockDataSources = atom<DataSources>(new Map());\n\n// Replace the actual store with our mock\nObject.defineProperty($dataSources, \"get\", {\n  value: () => mockDataSources.get(),\n});\n\n// Helper to create a minimal variable data source for testing\nconst createVariable = (\n  id: string,\n  name: string,\n  scopeInstanceId?: string\n): DataSource => ({\n  id,\n  scopeInstanceId,\n  name,\n  type: \"variable\",\n  value: { type: \"string\", value: \"\" },\n});\n\ntest(\"validateDataVariableName returns required error for empty name\", () => {\n  mockDataSources.set(new Map());\n\n  const error = validateDataVariableName(\"\");\n  expect(error?.type).toBe(\"required\");\n  expect(error?.message).toBe(\"Variable name is required\");\n});\n\ntest(\"validateDataVariableName returns required error for whitespace-only name\", () => {\n  mockDataSources.set(new Map());\n\n  const error = validateDataVariableName(\"   \");\n  expect(error?.type).toBe(\"required\");\n});\n\ntest(\"validateDataVariableName returns undefined for valid unique name\", () => {\n  mockDataSources.set(\n    new Map([[\"var1\", createVariable(\"var1\", \"existingVariable\", \"instance1\")]])\n  );\n\n  const error = validateDataVariableName(\"newVariable\");\n  expect(error).toBeUndefined();\n});\n\ntest(\"validateDataVariableName returns duplicate error when name exists on same instance\", () => {\n  mockDataSources.set(\n    new Map([\n      [\"var1\", createVariable(\"var1\", \"myVariable\", \"instance1\")],\n      [\"var2\", createVariable(\"var2\", \"otherVariable\", \"instance1\")],\n    ])\n  );\n\n  // Creating a new variable with existing name on same instance\n  const error = validateDataVariableName(\"myVariable\", \"var2\");\n  expect(error?.type).toBe(\"duplicate\");\n});\n\ntest(\"validateDataVariableName allows same name on different instances\", () => {\n  mockDataSources.set(\n    new Map([\n      [\"var1\", createVariable(\"var1\", \"myVariable\", \"instance1\")],\n      [\"var2\", createVariable(\"var2\", \"otherVariable\", \"instance2\")],\n    ])\n  );\n\n  // Creating a variable on instance2 with name that exists on instance1\n  const error = validateDataVariableName(\"myVariable\", \"var2\");\n  expect(error).toBeUndefined();\n});\n\ntest(\"validateDataVariableName allows renaming variable to same name\", () => {\n  mockDataSources.set(\n    new Map([[\"var1\", createVariable(\"var1\", \"myVariable\", \"instance1\")]])\n  );\n\n  // Renaming var1 to its current name\n  const error = validateDataVariableName(\"myVariable\", \"var1\");\n  expect(error).toBeUndefined();\n});\n\ntest(\"validateDataVariableName returns duplicate error when renaming to existing name on same instance\", () => {\n  mockDataSources.set(\n    new Map([\n      [\"var1\", createVariable(\"var1\", \"firstVariable\", \"instance1\")],\n      [\"var2\", createVariable(\"var2\", \"secondVariable\", \"instance1\")],\n    ])\n  );\n\n  // Renaming var2 to var1's name on same instance\n  const error = validateDataVariableName(\"firstVariable\", \"var2\");\n  expect(error?.type).toBe(\"duplicate\");\n});\n\ntest(\"validateDataVariableName ignores non-variable data sources\", () => {\n  mockDataSources.set(\n    new Map([\n      [\"var1\", createVariable(\"var1\", \"myVariable\", \"instance1\")],\n      [\n        \"resource1\",\n        {\n          id: \"resource1\",\n          scopeInstanceId: \"instance1\",\n          name: \"myResource\",\n          type: \"resource\",\n          resourceId: \"res1\",\n        },\n      ],\n    ])\n  );\n\n  // Creating a variable with same name as resource should be allowed\n  const error = validateDataVariableName(\"myResource\");\n  expect(error).toBeUndefined();\n});\n\ntest(\"validateDataVariableName validates for new variables without variableId\", () => {\n  mockDataSources.set(\n    new Map([[\"var1\", createVariable(\"var1\", \"existingVariable\", \"instance1\")]])\n  );\n\n  // Creating a new variable (no variableId) with existing name on same instance\n  const error = validateDataVariableName(\n    \"existingVariable\",\n    undefined,\n    \"instance1\"\n  );\n  expect(error?.type).toBe(\"duplicate\");\n});\n\ntest(\"validateDataVariableName handles undefined scopeInstanceId correctly\", () => {\n  mockDataSources.set(\n    new Map([[\"var1\", createVariable(\"var1\", \"globalVariable\", undefined)]])\n  );\n\n  // Creating a variable with same name but undefined scope\n  const error = validateDataVariableName(\"globalVariable\");\n  expect(error?.type).toBe(\"duplicate\");\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/data-variable-utils.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { atom, computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { toast } from \"@webstudio-is/design-system\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n  Flex,\n  Text,\n  Button,\n  theme,\n  InputField,\n} from \"@webstudio-is/design-system\";\nimport type { DataSource, Instance } from \"@webstudio-is/sdk\";\nimport { ROOT_INSTANCE_ID } from \"@webstudio-is/sdk\";\nimport {\n  $pages,\n  $instances,\n  $props,\n  $dataSources,\n  $resources,\n} from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { findVariableUsagesByInstance } from \"~/shared/data-variables\";\n\nconst $isDeleteUnusedDataVariablesDialogOpen = atom(false);\n\nexport const openDeleteUnusedDataVariablesDialog = () => {\n  $isDeleteUnusedDataVariablesDialogOpen.set(true);\n};\n\nexport type DataVariableError = {\n  type: \"required\" | \"duplicate\";\n  message: string;\n};\n\n/**\n * Computed store that tracks which instances use each variable\n * Returns a Map of variable ID to Set of instance IDs\n */\nexport const $usedVariablesInInstances = computed(\n  [$pages, $instances, $props, $dataSources, $resources],\n  (pages, instances, props, dataSources, resources) => {\n    return findVariableUsagesByInstance({\n      startingInstanceId: ROOT_INSTANCE_ID,\n      pages,\n      instances,\n      props,\n      dataSources,\n      resources,\n    });\n  }\n);\n\ntype DeleteDataVariableDialogProps = {\n  variable?: { id: DataSource[\"id\"]; name: string; usages: number };\n  onClose: () => void;\n  onConfirm: (variableId: DataSource[\"id\"]) => void;\n};\n\nexport const DeleteDataVariableDialog = ({\n  variable,\n  onClose,\n  onConfirm,\n}: DeleteDataVariableDialogProps) => {\n  return (\n    <Dialog\n      open={variable !== undefined}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          // Prevent command panel from handling keyboard events\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete confirmation</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Text>\n            {variable &&\n              (variable.usages > 0\n                ? `Delete \"${variable.name}\" variable from the project? It is used in ${variable.usages} ${variable.usages === 1 ? \"expression\" : \"expressions\"}.`\n                : `Delete \"${variable.name}\" variable from the project?`)}\n          </Text>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <Button\n              color=\"destructive\"\n              onClick={() => {\n                onConfirm(variable!.id);\n                onClose();\n              }}\n            >\n              Delete\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const deleteUnusedDataVariables = () => {\n  const dataSources = $dataSources.get();\n  const usedVariablesInInstances = $usedVariablesInInstances.get();\n  const unusedVariableIds: DataSource[\"id\"][] = [];\n\n  for (const dataSource of dataSources.values()) {\n    if (dataSource.type === \"variable\") {\n      const usages = usedVariablesInInstances.get(dataSource.id);\n      if (usages === undefined || usages.size === 0) {\n        unusedVariableIds.push(dataSource.id);\n      }\n    }\n  }\n\n  if (unusedVariableIds.length === 0) {\n    return 0;\n  }\n\n  serverSyncStore.createTransaction(\n    [$dataSources, $resources],\n    (dataSources, resources) => {\n      for (const variableId of unusedVariableIds) {\n        const dataSource = dataSources.get(variableId);\n        // Cleanup resource when variable is deleted\n        if (dataSource?.type === \"resource\") {\n          resources.delete(dataSource.resourceId);\n        }\n        dataSources.delete(variableId);\n      }\n    }\n  );\n\n  return unusedVariableIds.length;\n};\n\nexport const validateDataVariableName = (\n  name: string,\n  variableId?: DataSource[\"id\"],\n  scopeInstanceId?: Instance[\"id\"]\n): DataVariableError | undefined => {\n  if (name.trim().length === 0) {\n    return {\n      type: \"required\",\n      message: \"Variable name is required\",\n    };\n  }\n\n  const dataSources = $dataSources.get();\n  const currentVariable = variableId ? dataSources.get(variableId) : undefined;\n  const actualScopeInstanceId =\n    scopeInstanceId ??\n    (currentVariable?.type === \"variable\"\n      ? currentVariable.scopeInstanceId\n      : undefined);\n\n  for (const dataSource of dataSources.values()) {\n    if (\n      dataSource.type === \"variable\" &&\n      dataSource.scopeInstanceId === actualScopeInstanceId &&\n      dataSource.name === name &&\n      dataSource.id !== variableId\n    ) {\n      return {\n        type: \"duplicate\",\n        message: \"Name is already used by another variable on this instance\",\n      };\n    }\n  }\n};\n\nexport const renameDataVariable = (\n  id: DataSource[\"id\"],\n  name: string\n): DataVariableError | undefined => {\n  const validationError = validateDataVariableName(name, id);\n  if (validationError) {\n    return validationError;\n  }\n\n  serverSyncStore.createTransaction([$dataSources], (dataSources) => {\n    const dataSource = dataSources.get(id);\n    if (dataSource?.type === \"variable\") {\n      dataSource.name = name;\n    }\n  });\n};\n\ntype RenameDataVariableDialogProps = {\n  variable?: { id: DataSource[\"id\"]; name: string };\n  onClose: () => void;\n  onConfirm: (variableId: DataSource[\"id\"], newName: string) => void;\n};\n\nexport const RenameDataVariableDialog = ({\n  variable,\n  onClose,\n  onConfirm,\n}: RenameDataVariableDialogProps) => {\n  const [name, setName] = useState(\"\");\n  const [error, setError] = useState<string>();\n\n  // Reset name and clear error when variable changes\n  useEffect(() => {\n    if (variable?.name !== undefined) {\n      setName(variable.name);\n      setError(undefined);\n    }\n  }, [variable?.id, variable?.name]);\n\n  const handleConfirm = () => {\n    const renameError = renameDataVariable(variable!.id, name);\n    if (renameError) {\n      setError(renameError.message);\n      return;\n    }\n    onConfirm(variable!.id, name);\n    onClose();\n  };\n\n  return (\n    <Dialog\n      open={variable !== undefined}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          // Prevent command panel from handling keyboard events\n          event.stopPropagation();\n          if (event.key === \"Enter\" && !error) {\n            handleConfirm();\n          }\n        }}\n      >\n        <DialogTitle>Rename variable</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Flex direction=\"column\" gap=\"1\">\n            <InputField\n              value={name}\n              onChange={(event) => {\n                setName(event.target.value);\n                setError(undefined);\n              }}\n              color={error ? \"error\" : undefined}\n            />\n            {error && (\n              <Text color=\"destructive\" variant=\"monoBold\">\n                {error}\n              </Text>\n            )}\n          </Flex>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <Button color=\"primary\" onClick={handleConfirm}>\n              Rename\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const DeleteUnusedDataVariablesDialog = () => {\n  const open = useStore($isDeleteUnusedDataVariablesDialogOpen);\n  const usedVariablesInInstances = useStore($usedVariablesInInstances);\n  const dataSources = useStore($dataSources);\n\n  const handleClose = () => {\n    $isDeleteUnusedDataVariablesDialogOpen.set(false);\n  };\n\n  const unusedVariables: Array<{ id: string; name: string }> = [];\n  for (const dataSource of dataSources.values()) {\n    if (dataSource.type === \"variable\") {\n      const usages = usedVariablesInInstances.get(dataSource.id);\n      if (usages === undefined || usages.size === 0) {\n        unusedVariables.push({ id: dataSource.id, name: dataSource.name });\n      }\n    }\n  }\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          handleClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete unused data variables</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          {unusedVariables.length === 0 ? (\n            <Text>There are no unused data variables to delete.</Text>\n          ) : (\n            <>\n              <Text>\n                Delete {unusedVariables.length} unused data{\" \"}\n                {unusedVariables.length === 1 ? \"variable\" : \"variables\"} from\n                the project?\n              </Text>\n              <Text\n                variant=\"mono\"\n                css={{\n                  maxHeight: 200,\n                  overflowY: \"auto\",\n                  backgroundColor: theme.colors.backgroundPanel,\n                  borderRadius: theme.borderRadius[4],\n                  wordBreak: \"break-word\",\n                }}\n              >\n                {unusedVariables.map((variable) => variable.name).join(\", \")}\n              </Text>\n            </>\n          )}\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            {unusedVariables.length > 0 && (\n              <Button\n                color=\"destructive\"\n                autoFocus\n                onClick={() => {\n                  const deletedCount = deleteUnusedDataVariables();\n                  handleClose();\n                  if (deletedCount === 0) {\n                    toast.info(\"No unused data variables to delete\");\n                  } else {\n                    toast.success(\n                      `Deleted ${deletedCount} unused data ${deletedCount === 1 ? \"variable\" : \"variables\"}`\n                    );\n                  }\n                }}\n              >\n                Delete\n              </Button>\n            )}\n            <DialogClose>\n              <Button color=\"ghost\">\n                {unusedVariables.length > 0 ? \"Cancel\" : \"Close\"}\n              </Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/expression-editor.stories.tsx",
    "content": "import type { Meta, StoryObj } from \"@storybook/react\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { ExpressionEditor as ExpressionEditorComponent } from \"./expression-editor\";\nimport { useState } from \"react\";\n\nexport default {\n  title: \"Expression Editor\",\n  component: ExpressionEditorComponent,\n} satisfies Meta;\n\nconst scope = {\n  $ws$dataSource$123: {\n    long: \"!s fb skffsjdfksjdlkjslkkjlkj sjf lsdjsskljl kjsf\",\n    array: [],\n    number: 0,\n    boolean: true,\n    object: { param: \"value\" },\n  },\n  $ws$dataSource$321: { my: \"god\" },\n  $ws$dataSource$computed: [\n    // 0-11 keys\n    { \"with space\": 0 },\n    { \"0_numeric\": 0 },\n    0,\n    0,\n    0,\n    0,\n    0,\n    0,\n    0,\n    0,\n    0,\n    0,\n  ],\n};\nconst aliases = new Map<string, string>([\n  [\"$ws$dataSource$123\", \"Hello world\"],\n  [\"$ws$dataSource$321\", \"oh\"],\n  [\"$ws$dataSource$computed\", \"computed\"],\n]);\n\nconst ExpressionStory = () => {\n  const [value, setValue] = useState(\"$ws$dataSource$123.world\");\n  return (\n    <ExpressionEditorComponent\n      scope={scope}\n      aliases={aliases}\n      value={value}\n      onChange={setValue}\n      onChangeComplete={setValue}\n    />\n  );\n};\n\nexport const ExpressionEditor: StoryObj = {\n  render: () => (\n    <StorySection title=\"Expression Editor\">\n      <p>\n        {`Start typing \"h\" or \"o\" or press \"Tab\" to start variables completion`}\n      </p>\n      <ExpressionStory />\n    </StorySection>\n  ),\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/expression-editor.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { generateCompletionOptions } from \"./expression-editor\";\n\ndescribe(\"generateCompletionOptions\", () => {\n  describe(\"object properties\", () => {\n    test(\"returns object properties with preview values\", () => {\n      const target = { name: \"John\", age: 30, email: \"john@example.com\" };\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      expect(options).toEqual([\n        { label: \"name\", detail: `\"John\"` },\n        { label: \"age\", detail: \"30\" },\n        { label: \"email\", detail: `\"john@example.com\"` },\n      ]);\n    });\n\n    test(\"returns all properties regardless of pathName (filtering happens in CodeMirror layer)\", () => {\n      const target = { name: \"John\", age: 30, email: \"john@example.com\" };\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"na\",\n        pathLength: 1,\n      });\n\n      // generateCompletionOptions doesn't filter - that's done by matchSorter in scopeCompletionSource\n      expect(options).toEqual([\n        { label: \"name\", detail: `\"John\"` },\n        { label: \"age\", detail: \"30\" },\n        { label: \"email\", detail: `\"john@example.com\"` },\n      ]);\n    });\n\n    test(\"handles properties with special characters\", () => {\n      const target = { \"prop with spaces\": \"value\", \"123\": \"numeric key\" };\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      // Object.entries order for number-like keys may vary\n      const labels = options.map((o) => o.label);\n      expect(labels).toContain(\"prop with spaces\");\n      expect(labels).toContain(\"123\");\n    });\n  });\n\n  describe(\"array properties and methods\", () => {\n    test(\"returns array indices, length property, and methods\", () => {\n      const target = [\"apple\", \"banana\", \"cherry\"];\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      // Should include indices, length, and all array methods\n      const labels = options.map((o) => o.label);\n      expect(labels).toContain(\"0\");\n      expect(labels).toContain(\"1\");\n      expect(labels).toContain(\"2\");\n      expect(labels).toContain(\"length\");\n      expect(labels).toContain(\"slice()\");\n\n      // Check specific values\n      const lengthOption = options.find((o) => o.label === \"length\");\n      expect(lengthOption).toEqual({\n        label: \"length\",\n        detail: \"3\",\n        type: \"property\",\n      });\n    });\n\n    test(\"filters length property by pathName\", () => {\n      const target = [1, 2, 3, 4, 5];\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"len\",\n        pathLength: 1,\n      });\n\n      expect(options).toContainEqual({\n        label: \"length\",\n        detail: \"5\",\n        type: \"property\",\n      });\n    });\n  });\n\n  describe(\"string methods\", () => {\n    test(\"returns string methods when target is a string\", () => {\n      const target = \"hello world\";\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      const methodLabels = options.map((o) => o.label);\n      expect(methodLabels).toContain(\"toUpperCase()\");\n      expect(methodLabels).toContain(\"toLowerCase()\");\n      expect(methodLabels).toContain(\"replace()\");\n    });\n\n    test(\"filters string methods by pathName\", () => {\n      const target = \"test string\";\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"upper\",\n        pathLength: 1,\n      });\n\n      // Should include both toUpperCase and toLocaleUpperCase\n      const labels = options.map((o) => o.label);\n      expect(labels).toContain(\"toUpperCase()\");\n      expect(labels).toContain(\"toLocaleUpperCase()\");\n    });\n\n    test(\"does not return string methods for non-string targets\", () => {\n      const target = { name: \"test\" };\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"upper\",\n        pathLength: 1,\n      });\n\n      // Should only return object properties, no string methods\n      const methodOptions = options.filter((o) => o.type === \"method\");\n      expect(methodOptions).toEqual([]);\n    });\n  });\n\n  describe(\"pathLength === 0 (top-level variables)\", () => {\n    test(\"does not include methods for top-level variables\", () => {\n      const target = \"hello\";\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 0,\n      });\n\n      expect(options).toEqual([]);\n    });\n\n    test(\"returns array indices for top-level arrays (but no methods)\", () => {\n      const target = [1, 2, 3];\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 0,\n      });\n\n      // At pathLength === 0, we get array indices but no length/methods\n      expect(options).toEqual([\n        { label: \"0\", detail: \"1\" },\n        { label: \"1\", detail: \"2\" },\n        { label: \"2\", detail: \"3\" },\n      ]);\n    });\n  });\n\n  describe(\"non-object targets\", () => {\n    test(\"returns empty array for null\", () => {\n      const options = generateCompletionOptions({\n        target: null,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      expect(options).toEqual([]);\n    });\n\n    test(\"returns empty array for undefined\", () => {\n      const options = generateCompletionOptions({\n        target: undefined,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      expect(options).toEqual([]);\n    });\n\n    test(\"returns empty array for primitive numbers\", () => {\n      const options = generateCompletionOptions({\n        target: 42,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      expect(options).toEqual([]);\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"handles empty object\", () => {\n      const options = generateCompletionOptions({\n        target: {},\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      expect(options).toEqual([]);\n    });\n\n    test(\"handles empty array\", () => {\n      const options = generateCompletionOptions({\n        target: [],\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      // Empty array has length and all array methods\n      const labels = options.map((o) => o.label);\n      expect(labels).toContain(\"length\");\n      expect(labels).toContain(\"slice()\");\n\n      const lengthOption = options.find((o) => o.label === \"length\");\n      expect(lengthOption).toEqual({\n        label: \"length\",\n        detail: \"0\",\n        type: \"property\",\n      });\n    });\n\n    test(\"handles nested values in preview\", () => {\n      const target = {\n        nested: { inner: \"value\" },\n        arr: [1, 2, 3],\n      };\n      const options = generateCompletionOptions({\n        target,\n        pathName: \"\",\n        pathLength: 1,\n      });\n\n      // formatValuePreview returns \"JSON\" for complex objects\n      expect(options).toEqual([\n        { label: \"nested\", detail: \"JSON\" },\n        { label: \"arr\", detail: \"JSON\" },\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/expression-editor.tsx",
    "content": "import { useEffect, useMemo, type RefObject } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport type { SyntaxNode } from \"@lezer/common\";\nimport { Facet, RangeSetBuilder } from \"@codemirror/state\";\nimport {\n  type DecorationSet,\n  type ViewUpdate,\n  Decoration,\n  WidgetType,\n  ViewPlugin,\n  keymap,\n  EditorView,\n  tooltips,\n} from \"@codemirror/view\";\nimport { bracketMatching, syntaxTree } from \"@codemirror/language\";\nimport { linter } from \"@codemirror/lint\";\nimport {\n  type Completion,\n  type CompletionSource,\n  autocompletion,\n  closeBrackets,\n  closeBracketsKeymap,\n  completionKeymap,\n  CompletionContext,\n  insertCompletionText,\n  pickedCompletion,\n} from \"@codemirror/autocomplete\";\nimport { javascript } from \"@codemirror/lang-javascript\";\nimport { textVariants, css, rawTheme } from \"@webstudio-is/design-system\";\nimport {\n  decodeDataVariableId,\n  lintExpression,\n  allowedStringMethods,\n  allowedArrayMethods,\n} from \"@webstudio-is/sdk\";\nimport {\n  EditorContent,\n  EditorDialog,\n  EditorDialogButton,\n  EditorDialogControl,\n  type EditorApi,\n} from \"~/shared/code-editor-base\";\nimport {\n  decodeDataVariableName,\n  encodeDataVariableName,\n  restoreExpressionVariables,\n  unsetExpressionVariables,\n} from \"~/shared/data-variables\";\n\nexport type { EditorApi };\n\nexport const formatValue = (value: unknown) => {\n  try {\n    if (Array.isArray(value)) {\n      // format arrays as multiline\n      return JSON.stringify(value, null, 2);\n    }\n    if (typeof value === \"object\" && value !== null) {\n      // format objects with parentheses to enforce correct\n      // syntax highlighting as expression instead of block\n      return `(${JSON.stringify(value, null, 2)})`;\n    }\n    return JSON.stringify(value);\n  } catch {\n    // show nothing when value is invalid\n    return \"\";\n  }\n};\n\nexport const formatValuePreview = (value: unknown) => {\n  if (typeof value === \"string\") {\n    return JSON.stringify(value);\n  }\n  if (Number.isNaN(value)) {\n    return \"nan\";\n  }\n  if (typeof value === \"number\") {\n    return value.toString();\n  }\n  if (typeof value === \"boolean\") {\n    return value.toString();\n  }\n  if (value === undefined) {\n    return \"\";\n  }\n  if (value === null) {\n    return \"null\";\n  }\n  return \"JSON\";\n};\n\ntype Scope = Record<string, unknown>;\n\ntype Aliases = Map<string, string>;\n\nconst VariablesData = Facet.define<{\n  scope: Scope;\n  aliases: Aliases;\n}>();\n\n// completion based on\n// https://github.com/codemirror/lang-javascript/blob/4dcee95aee9386fd2c8ad55f93e587b39d968489/src/complete.ts\n\nconst Identifier = /^[\\p{L}$][\\p{L}\\p{N}$]*$/u;\n\nconst pathFor = (\n  read: (node: SyntaxNode) => string,\n  member: SyntaxNode,\n  name: string\n) => {\n  const path: string[] = [];\n  // traverse from current node to the root variable\n  for (;;) {\n    const object = member.firstChild;\n    if (object?.name === \"VariableName\") {\n      path.push(read(object));\n      return { path: path.reverse(), name };\n    }\n    if (object?.name === \"MemberExpression\") {\n      // MemberExpression(SyntaxNode PropertyName)\n      if (object.lastChild?.name === \"PropertyName\") {\n        path.push(read(object.lastChild!));\n        member = object;\n        continue;\n      }\n      // MemberExpression(SyntaxNode [ SyntaxNode ])\n      if (object.lastChild?.name === \"]\") {\n        const computed = object.lastChild.prevSibling;\n        if (computed?.name === \"Number\") {\n          path.push(read(computed));\n          member = object;\n          continue;\n        }\n        if (computed?.name === \"String\") {\n          // trim quotes from string literal\n          path.push(read(computed).slice(1, -1));\n          member = object;\n          continue;\n        }\n      }\n    }\n    // unexpected case\n    break;\n  }\n};\n\n/// Helper function for defining JavaScript completion sources. It\n/// returns the completable name and object path for a completion\n/// context, or undefined if no name/property completion should happen at\n/// that position. For example, when completing after `a.b.c` it will\n/// return `{path: [\"a\", \"b\"], name: \"c\"}`. When completing after `x`\n/// it will return `{path: [], name: \"x\"}`. When not in a property or\n/// name, it will return undefined if `context.explicit` is false, and\n/// `{path: [], name: \"\"}` otherwise.\nconst completionPath = (\n  context: CompletionContext\n): { path: string[]; name: string } | undefined => {\n  const read = (node: SyntaxNode) =>\n    context.state.doc.sliceString(node.from, node.to);\n  const inner = syntaxTree(context.state).resolveInner(context.pos, -1);\n  // suggest global variable name when user start completion explicitly\n  if (inner.name === \"Script\") {\n    if (context.explicit) {\n      return { path: [], name: \"\" };\n    }\n    return;\n  }\n  // complete variable name when start entering\n  if (inner.name === \"VariableName\") {\n    return { path: [], name: read(inner) };\n  }\n  // suggest property name when enter `object.`\n  if (inner.name === \".\" && inner.parent?.name === \"MemberExpression\") {\n    return pathFor(read, inner.parent, \"\");\n  }\n  // complete property when enter \"object.prope\"\n  if (\n    inner.name === \"PropertyName\" &&\n    inner.parent?.name === \"MemberExpression\"\n  ) {\n    return pathFor(read, inner.parent, read(inner));\n  }\n  return;\n};\n\n/**\n * Generate completion options for object properties and array/string methods.\n * Exported for testing.\n */\nexport const generateCompletionOptions = ({\n  target,\n  pathName,\n  pathLength,\n}: {\n  target: unknown;\n  pathName: string;\n  pathLength: number;\n}): Array<{ label: string; detail: string; type?: string }> => {\n  const options: Array<{ label: string; detail: string; type?: string }> = [];\n\n  // Add object properties\n  if (typeof target === \"object\" && target !== null) {\n    for (const [name, value] of Object.entries(target)) {\n      options.push({\n        label: name,\n        detail: formatValuePreview(value),\n      });\n    }\n  }\n\n  // Add string/array methods and properties for nested paths\n  if (pathLength > 0) {\n    const isString = typeof target === \"string\";\n    const isArray = Array.isArray(target);\n\n    if (isString) {\n      for (const method of allowedStringMethods) {\n        if (method.toLowerCase().includes(pathName.toLowerCase())) {\n          options.push({\n            label: `${method}()`,\n            detail: \"string method\",\n            type: \"method\",\n          });\n        }\n      }\n    }\n\n    if (isArray) {\n      // Add length property\n      if (\"length\".toLowerCase().includes(pathName.toLowerCase())) {\n        options.push({\n          label: \"length\",\n          detail: `${target.length}`,\n          type: \"property\",\n        });\n      }\n\n      for (const method of allowedArrayMethods) {\n        if (method.toLowerCase().includes(pathName.toLowerCase())) {\n          options.push({\n            label: `${method}()`,\n            detail: \"array method\",\n            type: \"method\",\n          });\n        }\n      }\n    }\n  }\n\n  return options;\n};\n\n// Defines a completion source that completes from the given scope\n// object (for example `globalThis`). Will enter properties\n// of the object when completing properties on a directly-named path.\nconst scopeCompletionSource: CompletionSource = (context) => {\n  const [{ scope }] = context.state.facet(VariablesData);\n  const path = completionPath(context);\n  if (path === undefined) {\n    return null;\n  }\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  let target: any = scope;\n  for (const step of path.path) {\n    target = target?.[step];\n    if (target == null) {\n      return null;\n    }\n  }\n\n  // Generate base completion options using exported function\n  const baseOptions = generateCompletionOptions({\n    target,\n    pathName: path.name,\n    pathLength: path.path.length,\n  });\n\n  // Convert to CodeMirror Completion format with apply functions\n  let options: Completion[] = baseOptions.map((option) => {\n    const name = option.label;\n    return {\n      label: name,\n      displayLabel: decodeDataVariableName(name),\n      detail: option.detail,\n      type: option.type,\n      apply: (view, completion, from, to) => {\n        const textToInsert = name;\n        // complete valid js identifier or top level variable without quotes\n        if (Identifier.test(textToInsert) || path.path.length === 0) {\n          // complete with dot\n          view.dispatch({\n            ...insertCompletionText(view.state, textToInsert, from, to),\n            annotations: pickedCompletion.of(completion),\n          });\n        } else {\n          // complete with computed member expression\n          view.dispatch({\n            ...insertCompletionText(\n              view.state,\n              // `param with spaces` -> [\"param with spaces\"]\n              // `0` -> [0]\n              `[${/^\\d+$/.test(textToInsert) ? textToInsert : JSON.stringify(textToInsert)}]`,\n              // remove dot when autocomplete computed member expression\n              // variable.\n              // variable[\"name\"]\n              from - 1,\n              to\n            ),\n            annotations: pickedCompletion.of(completion),\n          });\n        }\n      },\n    };\n  });\n\n  // Sort options if target is an object\n  if (typeof target === \"object\" && target !== null) {\n    options = matchSorter(options, path.name, {\n      keys: [(option) => option.displayLabel ?? option.label],\n      baseSort: (left, right) => {\n        const leftName = left.item.label;\n        const rightName = right.item.label;\n        const leftIndex = Number(leftName);\n        const rightIndex = Number(rightName);\n        // sort string fields\n        if (Number.isNaN(leftIndex) || Number.isNaN(rightIndex)) {\n          return leftName.localeCompare(rightName);\n        }\n        // sort indexes if both numbers\n        return leftIndex - rightIndex;\n      },\n    });\n  }\n\n  return {\n    from: context.pos - path.name.length,\n    filter: false,\n    options,\n  };\n};\n\n/**\n * Highlight variables and replace their $ws$dataSource$name like labels\n * with user names\n *\n * https://codemirror.net/examples/decoration/#atomic-ranges\n */\n\nclass VariableWidget extends WidgetType {\n  text: string;\n  constructor(text: string) {\n    super();\n    this.text = text;\n  }\n  toDOM(): HTMLElement {\n    const span = document.createElement(\"span\");\n    span.style.backgroundColor = \"rgba(24, 119, 232, 0.2)\";\n    span.textContent = this.text;\n    return span;\n  }\n}\n\nconst getVariableDecorations = (view: EditorView) => {\n  const builder = new RangeSetBuilder<Decoration>();\n  syntaxTree(view.state).iterate({\n    from: 0,\n    to: view.state.doc.length,\n    enter: (node) => {\n      if (node.name === \"VariableName\") {\n        const [{ scope }] = view.state.facet(VariablesData);\n        const identifier = view.state.doc.sliceString(node.from, node.to);\n        const variableName = decodeDataVariableName(identifier);\n        if (identifier in scope) {\n          builder.add(\n            node.from,\n            node.to,\n            Decoration.replace({\n              widget: new VariableWidget(variableName!),\n            })\n          );\n        }\n      }\n    },\n  });\n  return builder.finish();\n};\n\nconst variablesPlugin = ViewPlugin.fromClass(\n  class {\n    decorations: DecorationSet;\n    constructor(view: EditorView) {\n      this.decorations = getVariableDecorations(view);\n    }\n    update(update: ViewUpdate) {\n      if (update.docChanged) {\n        this.decorations = getVariableDecorations(update.view);\n      }\n    }\n  },\n  {\n    decorations: (instance) => instance.decorations,\n    provide: (plugin) =>\n      EditorView.atomicRanges.of((view) => {\n        return view.plugin(plugin)?.decorations || Decoration.none;\n      }),\n  }\n);\n\nconst emptyScope: Scope = {};\nconst emptyAliases: Aliases = new Map();\n\nconst wrapperStyle = css({\n  // 1 line is 16px\n  // set and max 20 lines\n  \"--ws-code-editor-max-height\": \"320px\",\n});\n\nconst linterTooltipTheme = EditorView.theme({\n  \".cm-tooltip:has(.cm-tooltip-lint)\": {\n    backgroundColor: \"transparent\",\n    borderWidth: 0,\n    paddingTop: rawTheme.spacing[5],\n    paddingBottom: rawTheme.spacing[5],\n    pointerEvents: \"none\",\n  },\n  \".cm-tooltip-lint\": {\n    backgroundColor: rawTheme.colors.backgroundTooltipMain,\n    color: rawTheme.colors.foregroundContrastMain,\n    borderRadius: rawTheme.borderRadius[7],\n    padding: rawTheme.spacing[5],\n  },\n  \".cm-tooltip-lint .cm-diagnostic\": {\n    borderWidth: 0,\n    padding: 0,\n    margin: 0,\n    ...textVariants.regular,\n  },\n});\n\nconst expressionLinter = linter((view) => {\n  const [{ scope }] = view.state.facet(VariablesData);\n  return lintExpression({\n    expression: view.state.doc.toString(),\n    availableVariables: new Set(Object.keys(scope)),\n  });\n});\n\nexport const ExpressionEditor = ({\n  editorApiRef,\n  scope = emptyScope,\n  aliases = emptyAliases,\n  color,\n  autoFocus = false,\n  readOnly = false,\n  value,\n  onChange,\n  onChangeComplete,\n}: {\n  editorApiRef?: RefObject<undefined | EditorApi>;\n  /**\n   * object with variables and their data to autocomplete\n   */\n  scope?: Scope;\n  /**\n   * variable aliases to show instead of $ws$dataSource$id\n   */\n  aliases?: Aliases;\n  color?: \"error\";\n  autoFocus?: boolean;\n  readOnly?: boolean;\n  value: string;\n  onChange: (value: string) => void;\n  onChangeComplete: (value: string) => void;\n}) => {\n  const { nameById, idByName } = useMemo(() => {\n    const nameById = new Map();\n    const idByName = new Map();\n    for (const [identifier, name] of aliases) {\n      const id = decodeDataVariableId(identifier);\n      if (id) {\n        nameById.set(id, name);\n        idByName.set(name, id);\n      }\n    }\n    return { nameById, idByName };\n  }, [aliases]);\n  const expressionWithUnsetVariables = useMemo(() => {\n    return unsetExpressionVariables({\n      expression: value,\n      unsetNameById: nameById,\n    });\n  }, [value, nameById]);\n  const scopeWithUnsetVariables = useMemo(() => {\n    const newScope: typeof scope = {};\n    for (const [identifier, value] of Object.entries(scope)) {\n      const name = aliases.get(identifier);\n      if (name) {\n        newScope[encodeDataVariableName(name)] = value;\n      }\n    }\n    return newScope;\n  }, [scope, aliases]);\n  const aliasesWithUnsetVariables = useMemo(() => {\n    const newAliases: typeof aliases = new Map();\n    for (const [_identifier, name] of aliases) {\n      newAliases.set(encodeDataVariableName(name), name);\n    }\n    return newAliases;\n  }, [aliases]);\n\n  const extensions = useMemo(\n    () => [\n      bracketMatching(),\n      closeBrackets(),\n      javascript({}),\n      VariablesData.of({\n        scope: scopeWithUnsetVariables,\n        aliases: aliasesWithUnsetVariables,\n      }),\n      // render autocomplete in body\n      // to prevent popover scroll overflow\n      tooltips({ parent: document.body }),\n      autocompletion({\n        override: [scopeCompletionSource],\n        icons: false,\n      }),\n      variablesPlugin,\n      keymap.of([...closeBracketsKeymap, ...completionKeymap]),\n      expressionLinter,\n      linterTooltipTheme,\n    ],\n    [scopeWithUnsetVariables, aliasesWithUnsetVariables]\n  );\n\n  // prevent clicking on autocomplete options propagating to body\n  // and closing dialogs and popovers\n  useEffect(() => {\n    const handlePointerDown = (event: PointerEvent) => {\n      if (\n        event.target instanceof HTMLElement &&\n        event.target.closest(\".cm-tooltip-autocomplete\")\n      ) {\n        event.stopPropagation();\n      }\n    };\n    const options = { capture: true };\n    document.addEventListener(\"pointerdown\", handlePointerDown, options);\n    return () => {\n      document.removeEventListener(\"pointerdown\", handlePointerDown, options);\n    };\n  }, []);\n\n  const content = (\n    <EditorContent\n      editorApiRef={editorApiRef}\n      extensions={extensions}\n      invalid={color === \"error\"}\n      readOnly={readOnly}\n      autoFocus={autoFocus}\n      value={expressionWithUnsetVariables}\n      onChange={(newValue: string) => {\n        const expressionWithRestoredVariables = restoreExpressionVariables({\n          expression: newValue,\n          maskedIdByName: idByName,\n        });\n        onChange(expressionWithRestoredVariables);\n      }}\n      onChangeComplete={(newValue: string) => {\n        const expressionWithRestoredVariables = restoreExpressionVariables({\n          expression: newValue,\n          maskedIdByName: idByName,\n        });\n        onChangeComplete(expressionWithRestoredVariables);\n      }}\n    />\n  );\n\n  return (\n    <div className={wrapperStyle.toString()}>\n      <EditorDialogControl>\n        {content}\n        <EditorDialog title=\"Expression Editor\" content={content}>\n          <EditorDialogButton />\n        </EditorDialog>\n      </EditorDialogControl>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/fonts-manager/fonts-manager.tsx",
    "content": "import {\n  DeprecatedList,\n  DeprecatedListItem,\n  useDeprecatedList,\n  theme,\n  useSearchFieldKeys,\n  findNextListItemIndex,\n  Tooltip,\n  Text,\n  rawTheme,\n  Link,\n  Flex,\n} from \"@webstudio-is/design-system\";\nimport {\n  AssetsShell,\n  deleteAssets,\n  Separator,\n  useAssets,\n} from \"~/builder/shared/assets\";\nimport { useMemo, useState } from \"react\";\nimport { useMenu } from \"./item-menu\";\nimport { CheckMarkIcon, InfoCircleIcon } from \"@webstudio-is/icons\";\nimport {\n  type Item,\n  filterIdsByFamily,\n  filterItems,\n  groupItemsByType,\n  toItems,\n} from \"./item-utils\";\nimport type { FontFamilyValue } from \"@webstudio-is/css-engine\";\n\nconst useLogic = ({ onChange, value }: FontsManagerProps) => {\n  const { assetContainers } = useAssets(\"font\");\n  const [selectedIndex, setSelectedIndex] = useState(-1);\n  const fontItems = useMemo(() => toItems(assetContainers), [assetContainers]);\n\n  const searchProps = useSearchFieldKeys({\n    onMove({ direction }) {\n      if (direction === \"current\") {\n        handleChangeCurrent(selectedIndex);\n        return;\n      }\n      const nextIndex = findNextListItemIndex(\n        selectedIndex,\n        groupedItems.length,\n        direction\n      );\n      setSelectedIndex(nextIndex);\n    },\n  });\n\n  const filteredItems = useMemo(\n    () =>\n      searchProps.value === \"\"\n        ? fontItems\n        : filterItems(searchProps.value, fontItems),\n    [fontItems, searchProps.value]\n  );\n\n  const { uploadedItems, systemItems, groupedItems } = useMemo(\n    () => groupItemsByType(filteredItems),\n    [filteredItems]\n  );\n\n  const currentIndex = useMemo(() => {\n    return groupedItems.findIndex((item) => item.label === value?.value[0]);\n  }, [groupedItems, value]);\n\n  const handleChangeCurrent = (nextCurrentIndex: number) => {\n    const item = groupedItems[nextCurrentIndex];\n    if (item !== undefined) {\n      onChange(item.label);\n    }\n  };\n\n  const { getItemProps, getListProps } = useDeprecatedList({\n    items: groupedItems,\n    selectedIndex,\n    currentIndex,\n    onSelect: setSelectedIndex,\n    onChangeCurrent: handleChangeCurrent,\n  });\n\n  const handleDelete = (index: number) => {\n    const family = groupedItems[index].label;\n    const ids = filterIdsByFamily(family, assetContainers);\n    deleteAssets(ids);\n    if (index === currentIndex) {\n      onChange(undefined);\n    }\n  };\n\n  return {\n    groupedItems,\n    uploadedItems,\n    systemItems,\n    selectedIndex,\n    handleDelete,\n    handleSelect: setSelectedIndex,\n    getItemProps,\n    getListProps,\n    searchProps,\n  };\n};\n\ntype FontsManagerProps = {\n  value?: FontFamilyValue;\n  onChange: (value?: string) => void;\n};\n\nexport const FontsManager = ({ value, onChange }: FontsManagerProps) => {\n  const {\n    groupedItems,\n    uploadedItems,\n    systemItems,\n    handleDelete,\n    handleSelect,\n    selectedIndex,\n    getListProps,\n    getItemProps,\n    searchProps,\n  } = useLogic({ onChange, value });\n\n  const listProps = getListProps();\n  const { render: renderMenu, isOpen: isMenuOpen } = useMenu({\n    selectedIndex,\n    onSelect: handleSelect,\n    onDelete: handleDelete,\n  });\n\n  const renderItem = (item: Item, index: number) => {\n    const { key, ...itemProps } = getItemProps({ index });\n    return (\n      <DeprecatedListItem\n        {...itemProps}\n        key={key}\n        prefix={itemProps.current ? <CheckMarkIcon size={12} /> : undefined}\n        suffix={\n          item.type === \"uploaded\" ? (\n            renderMenu(index)\n          ) : itemProps.state === \"selected\" && item.description ? (\n            <Tooltip\n              variant=\"wrapped\"\n              content={\n                <Flex\n                  direction=\"column\"\n                  gap=\"2\"\n                  css={{ maxWidth: theme.spacing[28] }}\n                >\n                  <Text variant=\"titles\">{item.label}</Text>\n                  <Text\n                    variant=\"monoBold\"\n                    color=\"moreSubtle\"\n                    userSelect=\"text\"\n                    css={{\n                      whiteSpace: \"break-spaces\",\n                      cursor: \"text\",\n                    }}\n                  >\n                    {`font-family: ${item.stack.join(\", \")};`}\n                  </Text>\n                  <Text>{item.description}</Text>\n                </Flex>\n              }\n            >\n              <InfoCircleIcon\n                tabIndex={0}\n                color={rawTheme.colors.foregroundSubtle}\n              />\n            </Tooltip>\n          ) : undefined\n        }\n      >\n        {item.label}\n      </DeprecatedListItem>\n    );\n  };\n\n  return (\n    <AssetsShell\n      searchProps={searchProps}\n      type=\"font\"\n      isEmpty={groupedItems.length === 0}\n    >\n      <DeprecatedList\n        {...listProps}\n        onBlur={(event) => {\n          if (isMenuOpen === false) {\n            listProps.onBlur(event);\n          }\n        }}\n      >\n        {uploadedItems.length !== 0 && (\n          <DeprecatedListItem state=\"disabled\">{\"Uploaded\"}</DeprecatedListItem>\n        )}\n        {uploadedItems.map(renderItem)}\n        {systemItems.length !== 0 && (\n          <>\n            {uploadedItems.length !== 0 && <Separator />}\n            <DeprecatedListItem\n              state=\"disabled\"\n              suffix={\n                <Tooltip\n                  variant=\"wrapped\"\n                  content={\n                    <Text>\n                      {\n                        \"System font stack CSS organized by typeface classification for every modern OS. No downloading, no layout shifts, no flashes— just instant renders. Learn more about \"\n                      }\n                      <Link\n                        href=\"https://github.com/system-fonts/modern-font-stacks\"\n                        target=\"_blank\"\n                        color=\"inherit\"\n                        variant=\"inherit\"\n                      >\n                        modern font stacks\n                      </Link>\n                      .\n                    </Text>\n                  }\n                >\n                  <InfoCircleIcon\n                    tabIndex={0}\n                    color={rawTheme.colors.foregroundSubtle}\n                    style={{ pointerEvents: \"auto\" }}\n                  />\n                </Tooltip>\n              }\n            >\n              System\n            </DeprecatedListItem>\n          </>\n        )}\n        {systemItems.map((item, index) =>\n          renderItem(item, index + uploadedItems.length)\n        )}\n      </DeprecatedList>\n    </AssetsShell>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/fonts-manager/index.ts",
    "content": "export * from \"./fonts-manager\";\nexport { toItems } from \"./item-utils\";\n"
  },
  {
    "path": "apps/builder/app/builder/shared/fonts-manager/item-menu.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  Text,\n  styled,\n  Tooltip,\n  SmallIconButton,\n} from \"@webstudio-is/design-system\";\nimport { EllipsesIcon } from \"@webstudio-is/icons\";\nimport { type FocusEventHandler, useState, useRef, useEffect } from \"react\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport { $authPermit } from \"~/shared/nano-states\";\n\nconst MenuButton = styled(SmallIconButton, {\n  \"&:hover, &:focus-visible\": {\n    color: theme.colors.foregroundMain,\n  },\n});\n\ntype ItemMenuProps = {\n  onDelete: () => void;\n  onOpenChange: (open: boolean) => void;\n  onFocusTrigger?: FocusEventHandler;\n  onBlurTrigger?: FocusEventHandler;\n};\n\nconst ItemMenu = ({\n  onDelete,\n  onOpenChange,\n  onFocusTrigger,\n  onBlurTrigger,\n}: ItemMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const isMounted = useRef(false);\n\n  useEffect(() => {\n    isMounted.current = true;\n    return () => {\n      isMounted.current = false;\n    };\n  }, []);\n\n  const authPermit = useStore($authPermit);\n\n  const isDeleteDisabled = authPermit === \"view\";\n  const tooltipContent = isDeleteDisabled\n    ? \"View mode. You can't delete assets.\"\n    : undefined;\n\n  return (\n    <DropdownMenu\n      open={isOpen}\n      onOpenChange={(open) => {\n        // Apparently onOpenChange can be called after component was unmounted and results in warning.\n        // Use case: deleting list item removes the item itself while menu is open.\n        if (isMounted.current) {\n          setIsOpen(open);\n        }\n        onOpenChange(open);\n      }}\n    >\n      <DropdownMenuTrigger asChild>\n        <MenuButton\n          aria-label=\"Font menu\"\n          onFocus={onFocusTrigger}\n          onBlur={onBlurTrigger}\n          tabIndex={0}\n          onClick={(event) => {\n            // Prevent setting the current font to the item.\n            event.stopPropagation();\n          }}\n          icon={<EllipsesIcon />}\n        />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\">\n        <Tooltip side=\"bottom\" content={tooltipContent}>\n          <DropdownMenuItem\n            disabled={isDeleteDisabled}\n            onClick={(event) => {\n              // Prevent setting the current font to the item.\n              event.stopPropagation();\n            }}\n            onSelect={() => {\n              onDelete();\n            }}\n          >\n            <Text>Delete font</Text>\n          </DropdownMenuItem>\n        </Tooltip>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\ntype UseMenu = {\n  selectedIndex: number;\n  onSelect: (index: number) => void;\n  onDelete: (index: number) => void;\n};\n\nexport const useMenu = ({ selectedIndex, onSelect, onDelete }: UseMenu) => {\n  const openMenu = useRef(-1);\n  const focusedMenuTrigger = useRef(-1);\n\n  const render = (index: number) => {\n    const show =\n      selectedIndex === index ||\n      openMenu.current === index ||\n      focusedMenuTrigger.current === index;\n\n    if (show === false) {\n      return;\n    }\n\n    return (\n      <ItemMenu\n        onOpenChange={(open) => {\n          openMenu.current = open === true ? index : -1;\n          onSelect(index);\n        }}\n        onDelete={() => {\n          onDelete(index);\n        }}\n        onFocusTrigger={() => {\n          focusedMenuTrigger.current = index;\n          onSelect(-1);\n        }}\n        onBlurTrigger={() => {\n          focusedMenuTrigger.current = -1;\n        }}\n      />\n    );\n  };\n\n  return {\n    render,\n    isOpen: openMenu.current !== -1,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/fonts-manager/item-utils.ts",
    "content": "import { SYSTEM_FONTS } from \"@webstudio-is/fonts\";\nimport { matchSorter } from \"match-sorter\";\nimport type { AssetContainer } from \"../assets\";\n\nexport type Item = {\n  label: string;\n  type: \"uploaded\" | \"system\";\n  description?: string;\n  stack: Array<string>;\n};\n\nexport const toItems = (\n  assetContainers: Array<AssetContainer>\n): Array<Item> => {\n  // We can have 2+ assets with the same family name, so we use a map to dedupe.\n  const uploaded = new Map();\n  for (const assetContainer of assetContainers) {\n    if (assetContainer.status !== \"uploaded\") {\n      continue;\n    }\n\n    const { asset } = assetContainer;\n    // @todo need to teach ts the right type from useAssets\n    if (\"meta\" in asset && \"family\" in asset.meta) {\n      uploaded.set(asset.meta.family, {\n        label: asset.meta.family,\n        type: \"uploaded\",\n      });\n    }\n  }\n\n  const system = [];\n  for (const [label, config] of SYSTEM_FONTS) {\n    system.push({\n      label,\n      type: \"system\",\n      description: config.description,\n      stack: config.stack,\n    });\n  }\n  return [...uploaded.values(), ...system];\n};\n\nexport const filterIdsByFamily = (\n  family: string,\n  assetContainers: Array<AssetContainer>\n) => {\n  // One family may have multiple assets for different formats, so we need to find them all.\n  return assetContainers\n    .filter((assetContainer) => {\n      if (assetContainer.status !== \"uploaded\") {\n        return false;\n      }\n      const { asset } = assetContainer;\n      // @todo need to teach TS the right type from useAssets\n      return (\n        \"meta\" in asset &&\n        \"family\" in asset.meta &&\n        asset.meta.family === family\n      );\n    })\n    .map((assetContainer) => assetContainer.asset!.id);\n};\n\nexport const groupItemsByType = (items: Array<Item>) => {\n  const uploadedItems = items.filter((item) => item.type === \"uploaded\");\n  const systemItems = items.filter((item) => item.type === \"system\");\n  const groupedItems = [...uploadedItems, ...systemItems];\n  return { uploadedItems, systemItems, groupedItems };\n};\n\nexport const filterItems = (search: string, items: Array<Item>) => {\n  return matchSorter(items, search, {\n    keys: [(item) => item.label],\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/inert-handlers.ts",
    "content": "import { useCallback } from \"react\";\nimport { canvasApi } from \"~/shared/canvas-api\";\nimport { $textEditingInstanceSelector } from \"~/shared/nano-states\";\n\nexport const skipInertHandlersAttribute = \"data-ws-skip-inert-handlers\";\n\nexport const useInertHandlers = () => {\n  /**\n   * Prevents Lexical text editor from stealing focus during rendering.\n   * Sets the inert attribute on the canvas body element and disables the text editor.\n   *\n   * This must be done synchronously to avoid the following issue:\n   *\n   * 1. Text editor is in edit state.\n   * 2. User focuses on the builder (e.g., clicks any input).\n   * 3. The text editor blur event triggers, causing a rerender on data change (data saved in onBlur).\n   * 4. Text editor rerenders, stealing focus from the builder.\n   * 5. Inert attribute is set asynchronously, but focus is already lost.\n   *\n   * Synchronous focusing and setInert prevent the text editor from focusing on render.\n   * This cannot be handled inside the canvas because the text editor toolbar is in the builder and focus events in the canvas should be ignored.\n   *\n   * Use onPointerDown instead of onFocus because Radix focus lock triggers on text edit blur\n   * before the focusin event when editing text inside a Radix dialog.\n   */\n  const onPointerDown = useCallback((event: React.PointerEvent) => {\n    if (\n      event.target instanceof Element &&\n      event.target.closest(`[${skipInertHandlersAttribute}]`)\n    ) {\n      return;\n    }\n\n    // Ignore toolbar focus events. See the onFocus handler in text-toolbar.tsx\n    if (false === event.defaultPrevented) {\n      canvasApi.setInert();\n      $textEditingInstanceSelector.set(undefined);\n    }\n  }, []);\n\n  /**\n   * Prevent Radix from stealing focus during editing in the style sources\n   * For example, when the user select or create new style source item inside a dialog.\n   */\n  const onKeyDown = useCallback((event: React.KeyboardEvent) => {\n    if (\n      event.target instanceof Element &&\n      event.target.closest(`[${skipInertHandlersAttribute}]`)\n    ) {\n      return;\n    }\n\n    if (event.target instanceof HTMLInputElement) {\n      canvasApi.setInert();\n    }\n  }, []);\n\n  /**\n   * Prevent Radix from stealing focus during editing in the settings panel.\n   * For example, when the user modifies the text content of an H1 element inside a dialog.\n   */\n  const onInput = useCallback((event: React.FormEvent<HTMLDivElement>) => {\n    if (\n      event.target instanceof Element &&\n      event.target.closest(`[${skipInertHandlersAttribute}]`)\n    ) {\n      return;\n    }\n\n    canvasApi.setInert();\n  }, []);\n\n  return {\n    onInput,\n    onKeyDown,\n    onPointerDown,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/instance-context-menu.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuItemRightSlot,\n  ContextMenuSeparator,\n  ContextMenuTrigger,\n  theme,\n  Kbd,\n  Box,\n} from \"@webstudio-is/design-system\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\nimport { emitCommand } from \"./commands\";\nimport {\n  $selectedInstancePath,\n  $selectedPage,\n  getInstanceKey,\n} from \"~/shared/awareness\";\nimport { canUnwrapInstance } from \"~/shared/instance-utils\";\nimport { $propValuesByInstanceSelector } from \"~/shared/nano-states\";\nimport { ROOT_INSTANCE_ID } from \"@webstudio-is/sdk\";\nimport type { InstancePath } from \"~/shared/awareness\";\n\nconst getMenuPermissions = (instancePath: InstancePath | undefined) => {\n  const instanceId = instancePath?.[0]?.instance.id;\n  const rootInstanceId = $selectedPage.get()?.rootInstanceId;\n\n  const isRoot = instanceId === ROOT_INSTANCE_ID;\n  const isBody = instanceId === rootInstanceId;\n  const isRootOrBody = isRoot || isBody;\n  const canUnwrap = instancePath ? canUnwrapInstance(instancePath) : false;\n\n  return {\n    canCopy: !isRootOrBody,\n    canPaste: !isRoot,\n    canCut: !isRootOrBody,\n    canDuplicate: !isRootOrBody,\n    canHide: !isRoot,\n    canRename: !isRoot,\n    canWrap: !isRootOrBody,\n    canUnwrap,\n    canConvert: !isRootOrBody,\n    canDelete: !isRootOrBody,\n  };\n};\n\nexport const MenuItems = () => {\n  const instancePath = useStore($selectedInstancePath);\n  const propValues = useStore($propValuesByInstanceSelector);\n\n  const instanceSelector = instancePath?.[0]?.instanceSelector;\n\n  const show = instanceSelector\n    ? Boolean(\n        propValues.get(getInstanceKey(instanceSelector))?.get(showAttribute) ??\n          true\n      )\n    : true;\n\n  const permissions = getMenuPermissions(instancePath);\n\n  return (\n    <Box css={{ width: theme.spacing[28] }}>\n      <ContextMenuItem\n        disabled={!permissions.canCopy}\n        onSelect={() => {\n          emitCommand(\"copy\");\n        }}\n      >\n        Copy\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"c\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canPaste}\n        onSelect={() => {\n          emitCommand(\"paste\");\n        }}\n      >\n        Paste\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"v\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canCut}\n        onSelect={() => {\n          emitCommand(\"cut\");\n        }}\n      >\n        Cut\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"x\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canDuplicate}\n        onSelect={() => {\n          emitCommand(\"duplicateInstance\");\n        }}\n      >\n        Duplicate\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"d\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuSeparator />\n      <ContextMenuItem\n        disabled={!permissions.canHide}\n        onSelect={() => {\n          emitCommand(\"toggleShow\");\n        }}\n      >\n        {show ? \"Hide\" : \"Show\"}\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canRename}\n        onSelect={() => {\n          emitCommand(\"editInstanceLabel\");\n        }}\n      >\n        Rename\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"e\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canWrap}\n        onSelect={() => {\n          emitCommand(\"wrap\");\n        }}\n      >\n        Wrap\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"alt\", \"g\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canUnwrap}\n        onSelect={() => {\n          emitCommand(\"unwrap\");\n        }}\n      >\n        Unwrap\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"shift\", \"g\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        disabled={!permissions.canConvert}\n        onSelect={() => {\n          emitCommand(\"convert\");\n        }}\n      >\n        Convert\n      </ContextMenuItem>\n      <ContextMenuSeparator />\n      <ContextMenuItem\n        onSelect={() => {\n          emitCommand(\"focusStyleSources\");\n        }}\n      >\n        Add token\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"meta\", \"enter\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuItem\n        onSelect={() => {\n          emitCommand(\"openSettingsPanel\");\n        }}\n      >\n        Open settings\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"d\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n      <ContextMenuSeparator />\n      <ContextMenuItem\n        disabled={!permissions.canDelete}\n        destructive\n        onSelect={() => {\n          emitCommand(\"deleteInstanceBuilder\");\n        }}\n      >\n        Delete\n        <ContextMenuItemRightSlot>\n          <Kbd value={[\"delete\"]} />\n        </ContextMenuItemRightSlot>\n      </ContextMenuItem>\n    </Box>\n  );\n};\n\nexport const InstanceContextMenu = ({ children }: { children: ReactNode }) => {\n  return (\n    <ContextMenu>\n      <ContextMenuTrigger\n        asChild\n        onPointerDown={(event) => {\n          if (!(event.target instanceof HTMLElement)) {\n            return;\n          }\n          event.target.closest<HTMLElement>(\"[data-tree-button]\")?.click();\n        }}\n      >\n        {children}\n      </ContextMenuTrigger>\n      <ContextMenuContent>\n        <MenuItems />\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/instance-label.tsx",
    "content": "import type { HtmlTags } from \"html-tags\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  BlockquoteIcon,\n  BodyIcon,\n  BoldIcon,\n  BoxIcon,\n  BracesIcon,\n  ButtonElementIcon,\n  CalendarIcon,\n  FormIcon,\n  FormTextAreaIcon,\n  FormTextFieldIcon,\n  HeadingIcon,\n  ImageIcon,\n  ItemIcon,\n  LabelIcon,\n  LinkIcon,\n  ListIcon,\n  ListItemIcon,\n  MinusIcon,\n  SelectIcon,\n  SubscriptIcon,\n  SuperscriptIcon,\n  TextAlignLeftIcon,\n  TextItalicIcon,\n} from \"@webstudio-is/icons/svg\";\nimport {\n  elementComponent,\n  parseComponentName,\n  ROOT_INSTANCE_ID,\n  type Instance,\n} from \"@webstudio-is/sdk\";\nimport { $instances } from \"~/shared/sync/data-stores\";\nimport { $registeredComponentMetas } from \"~/shared/nano-states\";\nimport { humanizeString } from \"~/shared/string-utils\";\n\nconst htmlIcons: Record<string, undefined | string> = {\n  // typography\n  h1: HeadingIcon,\n  h2: HeadingIcon,\n  h3: HeadingIcon,\n  h4: HeadingIcon,\n  h5: HeadingIcon,\n  h6: HeadingIcon,\n  p: TextAlignLeftIcon,\n  blockquote: BlockquoteIcon,\n  code: BracesIcon,\n  ul: ListIcon,\n  ol: ListIcon,\n  li: ListItemIcon,\n  hr: MinusIcon,\n  // rich text\n  b: BoldIcon,\n  strong: BoldIcon,\n  i: TextItalicIcon,\n  em: TextItalicIcon,\n  sub: SubscriptIcon,\n  sup: SuperscriptIcon,\n  a: LinkIcon,\n  // form\n  form: FormIcon,\n  textarea: FormTextAreaIcon,\n  button: ButtonElementIcon,\n  input: FormTextFieldIcon,\n  label: LabelIcon,\n  select: SelectIcon,\n  option: ItemIcon,\n  // misc\n  body: BodyIcon,\n  time: CalendarIcon,\n  img: ImageIcon,\n} satisfies Partial<Record<HtmlTags, undefined | string>>;\n\ntype InstanceLike = {\n  component: string;\n  label?: string;\n  tag?: string;\n};\n\ntype Props = {\n  size?: number | string;\n  instance: InstanceLike;\n  icon?: string;\n};\n\nexport const InstanceIcon = ({ size = 16, instance, icon }: Props) => {\n  const metas = useStore($registeredComponentMetas);\n  const meta = metas.get(instance.component);\n  // element component should be treated as div when no tag specified\n  const elementTag =\n    instance.component === elementComponent ? \"div\" : undefined;\n  const tag =\n    instance.tag ?? elementTag ?? Object.keys(meta?.presetStyle ?? {})[0];\n  const computedIcon = icon ?? meta?.icon ?? htmlIcons[tag] ?? BoxIcon;\n  return (\n    <div\n      style={{ width: size, height: size }}\n      dangerouslySetInnerHTML={{ __html: computedIcon }}\n    />\n  );\n};\n\nconst getLabelFromComponentName = (component: Instance[\"component\"]) => {\n  const [_namespace, componentName] = parseComponentName(component);\n  return humanizeString(componentName);\n};\n\nexport const getInstanceLabel = (\n  instanceOrInstanceId: InstanceLike | string\n): string => {\n  if (typeof instanceOrInstanceId === \"string\") {\n    if (instanceOrInstanceId === ROOT_INSTANCE_ID) {\n      return \"Root\";\n    }\n    const instance = $instances.get().get(instanceOrInstanceId);\n    if (instance) {\n      return getInstanceLabel(instance);\n    }\n    return \"Unknown\";\n  }\n\n  if (instanceOrInstanceId.label) {\n    return instanceOrInstanceId.label;\n  }\n  if (\n    instanceOrInstanceId.component === elementComponent &&\n    instanceOrInstanceId.tag\n  ) {\n    return `<${instanceOrInstanceId.tag}>`;\n  }\n  const meta = $registeredComponentMetas\n    .get()\n    .get(instanceOrInstanceId.component);\n  return (\n    meta?.label || getLabelFromComponentName(instanceOrInstanceId.component)\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/loading.stories.tsx",
    "content": "import { Flex, StorySection, Text, theme } from \"@webstudio-is/design-system\";\nimport { Loading as LoadingComponent, LoadingBackground } from \"./loading\";\n\nexport default {\n  title: \"Builder/Shared/Loading\",\n  component: LoadingComponent,\n};\n\nexport const Loading = () => (\n  <StorySection title=\"Loading\">\n    <Flex direction=\"column\" gap=\"5\">\n      <Text variant=\"labels\">Loading at 30%</Text>\n      <Flex\n        css={{\n          position: \"relative\",\n          height: 200,\n          background: theme.colors.backgroundTopbar,\n        }}\n      >\n        <LoadingComponent\n          state={{\n            state: \"loading\",\n            progress: 30,\n            readyStates: new Map([\n              [\"dataLoadingState\", false],\n              [\"selectedInstanceRenderState\", false],\n              [\"canvasIframeState\", false],\n            ]),\n          }}\n        />\n      </Flex>\n\n      <Text variant=\"labels\">Loading at 80%</Text>\n      <Flex\n        css={{\n          position: \"relative\",\n          height: 200,\n          background: theme.colors.backgroundTopbar,\n        }}\n      >\n        <LoadingComponent\n          state={{\n            state: \"loading\",\n            progress: 80,\n            readyStates: new Map([\n              [\"dataLoadingState\", true],\n              [\"selectedInstanceRenderState\", true],\n              [\"canvasIframeState\", false],\n            ]),\n          }}\n        />\n      </Flex>\n\n      <Text variant=\"labels\">Background overlay (visible)</Text>\n      <Flex css={{ position: \"relative\", height: 100 }}>\n        <LoadingBackground show />\n      </Flex>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/shared/loading.tsx",
    "content": "import { useState } from \"react\";\nimport { Box, Flex, Progress, theme } from \"@webstudio-is/design-system\";\nimport { WebstudioIcon } from \"@webstudio-is/icons\";\nimport { useInterval } from \"~/shared/hook-utils/use-interval\";\n\nexport const LoadingBackground = ({\n  show,\n  onTransitionEnd,\n}: {\n  show: boolean;\n  onTransitionEnd?: () => void;\n}) => {\n  const [transitionEnded, setTransitionEnded] = useState(false);\n\n  if (transitionEnded) {\n    return;\n  }\n\n  return (\n    <Box\n      css={{\n        position: \"absolute\",\n        inset: 0,\n        transitionDuration: \"300ms\",\n        pointerEvents: \"none\",\n        transitionProperty: \"opacity\",\n        backgroundColor: theme.colors.backgroundTopbar,\n        opacity: show ? 1 : 0,\n        isolation: \"isolate\",\n      }}\n      onTransitionEnd={() => {\n        setTransitionEnded(true);\n        onTransitionEnd?.();\n      }}\n    />\n  );\n};\n\ntype LoadingState = {\n  state: \"ready\" | \"loading\";\n  progress: number;\n  readyStates: Map<\n    \"dataLoadingState\" | \"selectedInstanceRenderState\" | \"canvasIframeState\",\n    boolean\n  >;\n};\n\nexport const Loading = ({ state }: { state: LoadingState }) => {\n  const [fakeProgress, setFakeProgress] = useState(state.progress);\n  const [transitionEnded, setTransitionEnded] = useState(false);\n\n  useInterval((intervalId) => {\n    setFakeProgress((previousFakeValue) => {\n      // Makeing sure fake value is not higher than real value to prevent jumping back\n      let nextFakeValue = Math.max(previousFakeValue + 1, state.progress);\n      // Make sure fake value is not lower than 10 to avoid showing empty progress.\n      nextFakeValue = Math.max(nextFakeValue, 10);\n      // Making sure fake value can't get bigger than 100, now that it reached 100 we can stop faking it.\n      if (nextFakeValue >= 100) {\n        clearInterval(intervalId);\n        return previousFakeValue;\n      }\n      return nextFakeValue;\n    });\n  }, 100);\n\n  if (state.state === \"ready\" && transitionEnded) {\n    return;\n  }\n\n  return (\n    <Flex\n      justify=\"center\"\n      css={{\n        position: \"absolute\",\n        inset: 0,\n        top: theme.spacing[15],\n        pointerEvents: \"none\",\n        zIndex: 0,\n      }}\n    >\n      <LoadingBackground\n        show={state.state !== \"ready\"}\n        onTransitionEnd={() => {\n          setTransitionEnded(true);\n        }}\n      />\n      {state.state !== \"ready\" && (\n        <Flex\n          direction=\"column\"\n          align=\"center\"\n          justify=\"center\"\n          gap=\"3\"\n          css={{ isolation: \"isolate\" }}\n        >\n          <WebstudioIcon size={60} />\n          <Progress value={fakeProgress} />\n        </Flex>\n      )}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/nano-states.ts",
    "content": "import { atom, computed } from \"nanostores\";\nimport {\n  $isPreviewMode,\n  $selectedInstanceRenderState,\n} from \"~/shared/nano-states/misc\";\nimport { $canvasIframeState } from \"~/shared/nano-states/canvas\";\nimport type { SidebarPanelName } from \"~/builder/sidebar-left/types\";\nimport { $settings, getSetting } from \"./client-settings\";\n\nexport const $isShareDialogOpen = atom<boolean>(false);\n\nexport const $publishDialog = atom<\"none\" | \"publish\" | \"export\">(\"none\");\n\nexport const $canvasWidth = atom<number | undefined>();\n\nexport const $isCloneDialogOpen = atom<boolean>(false);\n\nexport const $canvasRect = atom<DOMRect | undefined>();\n\nexport const $workspaceRect = atom<DOMRect | undefined>();\n\nexport const $canvasScrollbarSize = atom<\n  { width: number; height: number } | undefined\n>();\n\nexport const $scale = computed(\n  [$canvasWidth, $workspaceRect],\n  (canvasWidth, workspaceRect) => {\n    if (\n      canvasWidth === undefined ||\n      workspaceRect === undefined ||\n      canvasWidth <= workspaceRect.width\n    ) {\n      return 100;\n    }\n    return Number.parseFloat(\n      ((workspaceRect.width / canvasWidth) * 100).toFixed(2)\n    );\n  }\n);\n\nexport const $clampingRect = computed(\n  [$workspaceRect, $canvasRect, $canvasScrollbarSize, $scale],\n  (workspaceRect, canvasRect, canvasScrollbarSize, scale) => {\n    if (\n      workspaceRect === undefined ||\n      canvasRect === undefined ||\n      canvasScrollbarSize === undefined\n    ) {\n      return;\n    }\n\n    const scrollbarWidthScaled = Math.round(\n      (canvasScrollbarSize.width * scale) / 100\n    );\n\n    const scrollbarHeightScaled = Math.round(\n      (canvasScrollbarSize.height * scale) / 100\n    );\n\n    if (canvasRect.width >= workspaceRect.width) {\n      return {\n        left: 0,\n        top: 0,\n        width: workspaceRect.width - scrollbarWidthScaled,\n        height: workspaceRect.height - scrollbarHeightScaled,\n      };\n    }\n\n    return {\n      left: 0,\n      top: 0,\n      width: canvasRect.width - scrollbarWidthScaled,\n      height: canvasRect.height - scrollbarHeightScaled,\n    };\n  }\n);\n\nexport const $activeInspectorPanel = atom<\"style\" | \"settings\">(\"style\");\n\nexport const $dataLoadingState = atom<\"idle\" | \"loading\" | \"loaded\">(\"idle\");\n\nexport const $loadingState = computed(\n  [$dataLoadingState, $selectedInstanceRenderState, $canvasIframeState],\n  (dataLoadingState, selectedInstanceRenderState, canvasIframeState) => {\n    const readyStates = new Map<\n      \"dataLoadingState\" | \"selectedInstanceRenderState\" | \"canvasIframeState\",\n      boolean\n    >([\n      [\"dataLoadingState\", dataLoadingState === \"loaded\"],\n      [\n        \"selectedInstanceRenderState\",\n        selectedInstanceRenderState !== \"pending\",\n      ],\n      [\"canvasIframeState\", canvasIframeState === \"ready\"],\n    ]);\n\n    const readyCount = Array.from(readyStates.values()).filter(Boolean).length;\n    const progress = Math.round((readyCount / readyStates.size) * 100);\n    const state: \"ready\" | \"loading\" =\n      readyCount === readyStates.size ? \"ready\" : \"loading\";\n\n    return { state, progress, readyStates };\n  }\n);\n\n// Only used internally to avoid directly setting the value without using setActiveSidebarPanel.\nconst $activeSidebarPanel_ = atom<SidebarPanelName | undefined>();\n\nexport const $activeSidebarPanel = computed(\n  [$activeSidebarPanel_, $isPreviewMode, $loadingState, $settings],\n  (currentPanel, isPreviewMode, loadingState, { navigatorLayout }) => {\n    if (loadingState.state !== \"ready\") {\n      return \"none\";\n    }\n    if (isPreviewMode) {\n      return currentPanel === \"pages\" ? \"pages\" : \"none\";\n    }\n    if (currentPanel === undefined) {\n      return navigatorLayout === \"undocked\" ? \"navigator\" : \"none\";\n    }\n    return currentPanel;\n  }\n);\n\n/**\n * auto shows default panel when sidepanel is undocked and hides when docked\n */\nexport const setActiveSidebarPanel = (nextPanel: \"auto\" | SidebarPanelName) => {\n  const currentPanel = $activeSidebarPanel.get();\n  // - When navigator is open, user is trying to close the navigator.\n  // - Navigator is closed, user is trying to close some other panel, and if navigator is undocked, it needs to be opened.\n  if (nextPanel === \"none\") {\n    if (currentPanel === \"navigator\") {\n      $activeSidebarPanel_.set(\"none\");\n      return;\n    }\n    if (getSetting(\"navigatorLayout\") === \"undocked\") {\n      $activeSidebarPanel_.set(\"navigator\");\n      return;\n    }\n  }\n  $activeSidebarPanel_.set(nextPanel === \"auto\" ? undefined : nextPanel);\n};\n\nexport const toggleActiveSidebarPanel = (panel: SidebarPanelName) => {\n  const currentPanel = $activeSidebarPanel.get();\n  setActiveSidebarPanel(panel === currentPanel ? \"none\" : panel);\n};\n\nexport const $remoteDialog = atom<{ title: string; url: string } | undefined>();\n\n// Track which grid track (column/row) is being edited in the style panel\nexport type GridEditingTrack = {\n  type: \"column\" | \"row\";\n  index: number;\n};\n\nexport const $gridEditingTrack = atom<GridEditingTrack | undefined>(undefined);\n\n// Track which grid area is being edited in the style panel\nexport type GridEditingArea = {\n  columnStart: number;\n  columnEnd: number;\n  rowStart: number;\n  rowEnd: number;\n};\n\nexport const $gridEditingArea = atom<GridEditingArea | undefined>(undefined);\n\n// Whether a grid-related section is visible in the style panel.\n// Grid guides are shown on the canvas when true.\nexport const $isStylePanelGridVisible = atom(false);\n"
  },
  {
    "path": "apps/builder/app/builder/shared/relative-time.stories.tsx",
    "content": "import { Flex, StorySection, Text } from \"@webstudio-is/design-system\";\nimport { RelativeTime as RelativeTimeComponent } from \"./relative-time\";\n\nexport default {\n  title: \"Builder/Shared/Relative Time\",\n  component: RelativeTimeComponent,\n};\n\nexport const RelativeTime = () => {\n  const now = Date.now();\n  const times = [\n    { label: \"Just now\", date: new Date(now - 10_000) },\n    { label: \"5 minutes ago\", date: new Date(now - 5 * 60_000) },\n    { label: \"1 hour ago\", date: new Date(now - 60 * 60_000) },\n    { label: \"Yesterday\", date: new Date(now - 24 * 60 * 60_000) },\n    { label: \"Last week\", date: new Date(now - 7 * 24 * 60 * 60_000) },\n  ];\n\n  return (\n    <StorySection title=\"Relative Time\">\n      <Flex direction=\"column\" gap=\"2\">\n        {times.map(({ label, date }) => (\n          <Flex key={label} gap=\"3\" align=\"center\">\n            <Text variant=\"labels\" css={{ width: 120 }}>\n              {label}:\n            </Text>\n            <Text>\n              <RelativeTimeComponent time={date} />\n            </Text>\n          </Flex>\n        ))}\n      </Flex>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/relative-time.tsx",
    "content": "import useRelativeTime from \"@nkzw/use-relative-time\";\n\nexport const RelativeTime = ({ time }: { time: Date }) => {\n  return useRelativeTime(time.getTime());\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/style-source-actions.tsx",
    "content": "import { useState, useEffect } from \"react\";\nimport { atom, computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n  Button,\n  Text,\n  Flex,\n  theme,\n  InputField,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport type { Instance, StyleSource } from \"@webstudio-is/sdk\";\nimport {\n  $styleSources,\n  $styleSourceSelections,\n  $styles,\n  $selectedStyleSources,\n  $selectedStyleState,\n} from \"~/shared/nano-states\";\nimport {\n  deleteStyleSourceMutable,\n  findUnusedTokens,\n  deleteStyleSourcesMutable,\n  validateAndRenameStyleSource,\n  renameStyleSourceMutable,\n  type RenameStyleSourceError,\n} from \"~/shared/style-source-utils\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { $selectedInstance } from \"~/shared/awareness\";\n\n// Re-export the type for convenience\nexport type { RenameStyleSourceError };\n\nconst $isDeleteUnusedTokensDialogOpen = atom(false);\n\nexport const openDeleteUnusedTokensDialog = () => {\n  $isDeleteUnusedTokensDialogOpen.set(true);\n};\n\nexport const $styleSourceUsages = computed(\n  $styleSourceSelections,\n  (styleSourceSelections) => {\n    const styleSourceUsages = new Map<StyleSource[\"id\"], Set<Instance[\"id\"]>>();\n    for (const { instanceId, values } of styleSourceSelections.values()) {\n      for (const styleSourceId of values) {\n        let usages = styleSourceUsages.get(styleSourceId);\n        if (usages === undefined) {\n          usages = new Set();\n          styleSourceUsages.set(styleSourceId, usages);\n        }\n        usages.add(instanceId);\n      }\n    }\n    return styleSourceUsages;\n  }\n);\n\nconst deselectMatchingStyleSource = (styleSourceId: StyleSource[\"id\"]) => {\n  const instanceId = $selectedInstance.get()?.id;\n  if (instanceId === undefined) {\n    return;\n  }\n  const selectedStyleSources = new Map($selectedStyleSources.get());\n  if (selectedStyleSources.get(instanceId) === styleSourceId) {\n    selectedStyleSources.delete(instanceId);\n    $selectedStyleSources.set(selectedStyleSources);\n    $selectedStyleState.set(undefined);\n  }\n};\n\nexport const deleteStyleSource = (styleSourceId: StyleSource[\"id\"]) => {\n  serverSyncStore.createTransaction(\n    [$styleSources, $styleSourceSelections, $styles],\n    (styleSources, styleSourceSelections, styles) => {\n      deleteStyleSourceMutable({\n        styleSourceId,\n        styleSources,\n        styleSourceSelections,\n        styles,\n      });\n    }\n  );\n  // reset selected style source if necessary\n  deselectMatchingStyleSource(styleSourceId);\n};\n\nexport const deleteUnusedTokens = () => {\n  const styleSources = $styleSources.get();\n  const styleSourceUsages = $styleSourceUsages.get();\n  const unusedTokenIds = findUnusedTokens({ styleSources, styleSourceUsages });\n\n  if (unusedTokenIds.length === 0) {\n    return 0;\n  }\n\n  serverSyncStore.createTransaction(\n    [$styleSources, $styleSourceSelections, $styles],\n    (styleSources, styleSourceSelections, styles) => {\n      deleteStyleSourcesMutable({\n        styleSourceIds: unusedTokenIds,\n        styleSources,\n        styleSourceSelections,\n        styles,\n      });\n    }\n  );\n\n  return unusedTokenIds.length;\n};\n\nexport const renameStyleSource = (\n  id: StyleSource[\"id\"],\n  name: string\n): RenameStyleSourceError | undefined => {\n  const styleSources = $styleSources.get();\n  const validationError = validateAndRenameStyleSource({\n    id,\n    name,\n    styleSources,\n  });\n  if (validationError) {\n    return validationError;\n  }\n  serverSyncStore.createTransaction([$styleSources], (styleSources) => {\n    renameStyleSourceMutable({ id, name, styleSources });\n  });\n};\n\ntype DeleteStyleSourceDialogProps = {\n  styleSource?: { id: StyleSource[\"id\"]; name: string };\n  onClose: () => void;\n  onConfirm: (styleSourceId: StyleSource[\"id\"]) => void;\n};\n\nexport const DeleteStyleSourceDialog = ({\n  styleSource,\n  onClose,\n  onConfirm,\n}: DeleteStyleSourceDialogProps) => {\n  return (\n    <Dialog\n      open={styleSource !== undefined}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          // Prevent command panel from handling keyboard events\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete confirmation</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Text>{`Delete \"${styleSource?.name}\" token from the project including all of its styles?`}</Text>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <Button\n              color=\"destructive\"\n              onClick={() => {\n                onConfirm(styleSource!.id);\n                onClose();\n              }}\n            >\n              Delete\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\ntype RenameStyleSourceDialogProps = {\n  styleSource?: { id: StyleSource[\"id\"]; name: string };\n  onClose: () => void;\n  onConfirm: (styleSourceId: StyleSource[\"id\"], newName: string) => void;\n};\n\nexport const RenameStyleSourceDialog = ({\n  styleSource,\n  onClose,\n  onConfirm,\n}: RenameStyleSourceDialogProps) => {\n  const [name, setName] = useState(\"\");\n  const [error, setError] = useState<string>();\n\n  // Reset name and clear error when styleSource changes\n  useEffect(() => {\n    if (styleSource?.name !== undefined) {\n      setName(styleSource.name);\n      setError(undefined);\n    }\n  }, [styleSource?.id, styleSource?.name]);\n\n  const handleConfirm = () => {\n    const renameError = renameStyleSource(styleSource!.id, name);\n    if (renameError) {\n      if (renameError.type === \"minlength\") {\n        setError(\"Token name cannot be empty\");\n      } else if (renameError.type === \"duplicate\") {\n        setError(\"A token with this name already exists\");\n      }\n      return;\n    }\n    onConfirm(styleSource!.id, name);\n    onClose();\n  };\n\n  return (\n    <Dialog\n      open={styleSource !== undefined}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          // Prevent command panel from handling keyboard events\n          event.stopPropagation();\n          if (event.key === \"Enter\" && !error) {\n            handleConfirm();\n          }\n        }}\n      >\n        <DialogTitle>Rename token</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Flex direction=\"column\" gap=\"1\">\n            <InputField\n              value={name}\n              onChange={(event) => {\n                setName(event.target.value);\n                setError(undefined);\n              }}\n              color={error ? \"error\" : undefined}\n            />\n            {error && (\n              <Text color=\"destructive\" variant=\"monoBold\">\n                {error}\n              </Text>\n            )}\n          </Flex>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <Button color=\"primary\" onClick={handleConfirm}>\n              Rename\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const DeleteUnusedTokensDialog = () => {\n  const open = useStore($isDeleteUnusedTokensDialogOpen);\n  const styleSourceUsages = useStore($styleSourceUsages);\n  const styleSources = useStore($styleSources);\n\n  const handleClose = () => {\n    $isDeleteUnusedTokensDialogOpen.set(false);\n  };\n\n  const unusedTokenIds = findUnusedTokens({ styleSources, styleSourceUsages });\n  const unusedTokens: Array<{ id: string; name: string }> = unusedTokenIds\n    .map((id) => {\n      const styleSource = styleSources.get(id);\n      return styleSource?.type === \"token\"\n        ? { id, name: styleSource.name }\n        : null;\n    })\n    .filter((token): token is { id: string; name: string } => token !== null);\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          handleClose();\n        }\n      }}\n    >\n      <DialogContent\n        onKeyDown={(event) => {\n          event.stopPropagation();\n        }}\n      >\n        <DialogTitle>Delete unused tokens</DialogTitle>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          {unusedTokens.length === 0 ? (\n            <Text>There are no unused tokens to delete.</Text>\n          ) : (\n            <>\n              <Text>\n                Delete {unusedTokens.length} unused{\" \"}\n                {unusedTokens.length === 1 ? \"token\" : \"tokens\"} from the\n                project?\n              </Text>\n              <Text\n                variant=\"mono\"\n                css={{\n                  maxHeight: 200,\n                  overflowY: \"auto\",\n                  backgroundColor: theme.colors.backgroundPanel,\n                  borderRadius: theme.borderRadius[4],\n                  wordBreak: \"break-word\",\n                }}\n              >\n                {unusedTokens.map((token) => token.name).join(\", \")}\n              </Text>\n            </>\n          )}\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            {unusedTokens.length > 0 && (\n              <Button\n                color=\"destructive\"\n                autoFocus\n                onClick={() => {\n                  const deletedCount = deleteUnusedTokens();\n                  handleClose();\n                  if (deletedCount === 0) {\n                    toast.info(\"No unused tokens to delete\");\n                  } else {\n                    toast.success(\n                      `Deleted ${deletedCount} unused ${deletedCount === 1 ? \"token\" : \"tokens\"}`\n                    );\n                  }\n                }}\n              >\n                Delete\n              </Button>\n            )}\n            <DialogClose>\n              <Button color=\"ghost\">\n                {unusedTokens.length > 0 ? \"Cancel\" : \"Close\"}\n              </Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/topbar-layout.stories.tsx",
    "content": "import {\n  Button,\n  Flex,\n  rawTheme,\n  StorySection,\n  theme,\n  ToolbarButton,\n  Text,\n  buttonStyle,\n  Link,\n} from \"@webstudio-is/design-system\";\nimport {\n  CloudIcon,\n  OfflineIcon,\n  ShieldIcon,\n  WebstudioIcon,\n} from \"@webstudio-is/icons\";\nimport { $queueStatus } from \"~/shared/sync/project-queue\";\nimport { $authPermit } from \"~/shared/nano-states\";\nimport { SyncStatus } from \"~/builder/features/sync-status\";\nimport { ViewMode } from \"~/builder/features/view-mode\";\nimport { TopbarLayout } from \"./topbar-layout\";\n\nexport default {\n  title: \"Topbar layout\",\n  component: TopbarLayout,\n};\n\nconst MenuPlaceholder = () => (\n  <ToolbarButton aria-label=\"Menu\">\n    <WebstudioIcon size={22} />\n  </ToolbarButton>\n);\n\nconst PagePlaceholder = () => (\n  <ToolbarButton css={{ paddingInline: theme.panel.paddingInline }}>\n    <Text truncate css={{ maxWidth: theme.spacing[24] }}>\n      Home\n    </Text>\n  </ToolbarButton>\n);\n\nconst BreakpointsPlaceholder = () => (\n  <Flex align=\"center\" gap=\"1\">\n    <ToolbarButton>Base</ToolbarButton>\n  </Flex>\n);\n\nconst SyncErrorIndicator = () => (\n  <Flex\n    align=\"center\"\n    justify=\"center\"\n    css={{ height: theme.spacing[\"15\"] }}\n    shrink={false}\n  >\n    <OfflineIcon color={rawTheme.colors.foregroundDestructive} />\n  </Flex>\n);\n\nconst ViewModeIndicator = () => (\n  <Flex\n    align=\"center\"\n    justify=\"center\"\n    css={{ height: theme.spacing[\"15\"] }}\n    shrink={false}\n  >\n    <CloudIcon color={rawTheme.colors.backgroundAlertMain} />\n  </Flex>\n);\n\nconst SafeModeIndicator = () => (\n  <ToolbarButton variant=\"subtle\">\n    <ShieldIcon stroke={rawTheme.colors.foregroundDestructive} />\n  </ToolbarButton>\n);\n\nexport const TopbarLayouts = () => {\n  $queueStatus.set({ status: \"failed\" });\n  $authPermit.set(\"view\");\n  return (\n    <StorySection title=\"Topbar Layouts\">\n      <Flex direction=\"column\" gap=\"5\">\n        <Text variant=\"labels\">Default</Text>\n        <TopbarLayout\n          menu={<MenuPlaceholder />}\n          left={<PagePlaceholder />}\n          center={<BreakpointsPlaceholder />}\n          right={\n            <>\n              <Button color=\"gradient\">Share</Button>\n              <Button color=\"positive\">Publish</Button>\n            </>\n          }\n        />\n        <Text variant=\"labels\">With indicators</Text>\n        <TopbarLayout\n          menu={<MenuPlaceholder />}\n          left={<PagePlaceholder />}\n          center={<BreakpointsPlaceholder />}\n          right={\n            <>\n              <SafeModeIndicator />\n              <ViewModeIndicator />\n              <SyncErrorIndicator />\n              <Button color=\"gradient\">Share</Button>\n              <Button color=\"positive\">Publish</Button>\n              <Link\n                data-state=\"auto\"\n                className={buttonStyle({ color: \"positive\" })}\n                color=\"contrast\"\n                href=\"#\"\n                underline=\"none\"\n              >\n                Clone\n              </Link>\n            </>\n          }\n        />\n        <Text variant=\"labels\">Menu only</Text>\n        <TopbarLayout menu={<MenuPlaceholder />} />\n        <Text variant=\"labels\">Sync failed</Text>\n        <TopbarLayout\n          menu={<MenuPlaceholder />}\n          left={<PagePlaceholder />}\n          center={<BreakpointsPlaceholder />}\n          right={\n            <>\n              <SyncStatus />\n              <Button color=\"gradient\">Share</Button>\n              <Button color=\"positive\">Publish</Button>\n            </>\n          }\n        />\n        <Text variant=\"labels\">View mode</Text>\n        <TopbarLayout\n          menu={<MenuPlaceholder />}\n          left={<PagePlaceholder />}\n          center={<BreakpointsPlaceholder />}\n          right={\n            <>\n              <ViewMode />\n              <Button color=\"gradient\">Share</Button>\n              <Button color=\"positive\">Publish</Button>\n            </>\n          }\n        />\n      </Flex>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/topbar-layout.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport {\n  theme,\n  css,\n  Flex,\n  Toolbar,\n  ToolbarToggleGroup,\n  type CSS,\n} from \"@webstudio-is/design-system\";\n\nconst topbarContainerStyle = css({\n  position: \"relative\",\n  display: \"flex\",\n  justifyContent: \"space-between\",\n  background: theme.colors.backgroundTopbar,\n  height: theme.spacing[15],\n  paddingRight: theme.panel.paddingInline,\n  color: theme.colors.foregroundContrastMain,\n});\n\ntype TopbarLayoutProps = {\n  menu: ReactNode;\n  left?: ReactNode;\n  center?: ReactNode;\n  right?: ReactNode;\n  loading?: ReactNode;\n  css?: CSS;\n};\n\nexport const TopbarLayout = ({\n  menu,\n  left,\n  center,\n  right,\n  loading,\n  css,\n}: TopbarLayoutProps) => (\n  <nav className={topbarContainerStyle({ css })}>\n    <Flex css={{ flexBasis: \"20%\" }}>\n      <Flex grow={false} shrink={false}>\n        {menu}\n      </Flex>\n      {left && <Flex align=\"center\">{left}</Flex>}\n    </Flex>\n    <Flex justify=\"center\">{center}</Flex>\n    <Toolbar>\n      <ToolbarToggleGroup\n        type=\"single\"\n        css={{\n          isolation: \"isolate\",\n          justifyContent: \"flex-end\",\n          gap: theme.spacing[5],\n          flexShrink: 0,\n        }}\n      >\n        {right}\n      </ToolbarToggleGroup>\n    </Toolbar>\n    {loading}\n  </nav>\n);\n"
  },
  {
    "path": "apps/builder/app/builder/shared/topbar.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  theme,\n  ToolbarButton,\n  Text,\n  type CSS,\n  Tooltip,\n  Kbd,\n} from \"@webstudio-is/design-system\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { $pages } from \"~/shared/sync/data-stores\";\nimport { $editingPageId } from \"~/shared/nano-states\";\n\nimport { ShareButton } from \"~/builder/features/share\";\nimport { PublishButton } from \"~/builder/features/publish\";\nimport { SyncStatus } from \"~/builder/features/sync-status\";\nimport { Menu } from \"~/builder/features/menu\";\nimport { BreakpointsContainer } from \"~/builder/features/breakpoints\";\nimport { ViewMode } from \"~/builder/features/view-mode\";\nimport { AddressBarPopover } from \"~/builder/features/address-bar\";\nimport { toggleActiveSidebarPanel } from \"~/builder/shared/nano-states\";\nimport type { ReactNode } from \"react\";\nimport { CloneButton } from \"~/builder/features/clone\";\nimport { $selectedPage } from \"~/shared/awareness\";\nimport { BuilderModeDropDown } from \"~/builder/features/builder-mode\";\nimport { SafeModeButton } from \"~/builder/features/safe-mode\";\nimport { TopbarLayout } from \"./topbar-layout\";\n\nconst PagesButton = () => {\n  const page = useStore($selectedPage);\n  if (page === undefined) {\n    return;\n  }\n\n  return (\n    <Tooltip\n      content={\n        <Text>\n          {\"Pages or page settings \"}\n          <Kbd value={[\"alt\", \"click\"]} color=\"moreSubtle\" />\n        </Text>\n      }\n    >\n      <ToolbarButton\n        css={{ paddingInline: theme.panel.paddingInline }}\n        aria-label=\"Toggle Pages\"\n        onClick={(event) => {\n          $editingPageId.set(event.altKey ? page.id : undefined);\n          toggleActiveSidebarPanel(\"pages\");\n        }}\n        tabIndex={0}\n      >\n        <Text truncate css={{ maxWidth: theme.spacing[24] }}>\n          {page.name}\n        </Text>\n      </ToolbarButton>\n    </Tooltip>\n  );\n};\n\ntype TopbarProps = {\n  project: Project;\n  loading: ReactNode;\n  css: CSS;\n};\n\nexport const Topbar = ({ project, css, loading }: TopbarProps) => {\n  const pages = useStore($pages);\n  return (\n    <TopbarLayout\n      css={css}\n      menu={<Menu />}\n      left={\n        pages ? (\n          <>\n            <PagesButton />\n            <AddressBarPopover />\n          </>\n        ) : undefined\n      }\n      center={<BreakpointsContainer />}\n      right={\n        <>\n          <SafeModeButton />\n          <ViewMode />\n          <SyncStatus />\n          <BuilderModeDropDown />\n          <ShareButton projectId={project.id} />\n          <PublishButton projectId={project.id} />\n          <CloneButton />\n        </>\n      }\n      loading={loading}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/url-pattern.test.ts",
    "content": "import { expect, test, describe } from \"vitest\";\nimport {\n  compilePathnamePattern,\n  isPathnamePattern,\n  matchPathnamePattern,\n  tokenizePathnamePattern,\n  validatePathnamePattern,\n} from \"./url-pattern\";\nimport { VALID_URLPATTERN_PATHS } from \"@webstudio-is/sdk/router-paths.test\";\n\n/**\n * These tests use the shared test data from @webstudio-is/sdk to ensure\n * URLPattern matching is consistent with schema validation.\n */\ndescribe(\"Shared router path tests - URLPattern matching\", () => {\n  describe(\"all valid paths can be used as URLPattern patterns\", () => {\n    test.each(VALID_URLPATTERN_PATHS)(\"accepts pattern: %s\", (pattern) => {\n      // Pattern should be valid for URLPattern\n      // Some patterns may have validation errors due to our custom rules\n      // but they should still work with URLPattern itself\n      expect(() => matchPathnamePattern(pattern, pattern)).not.toThrow();\n    });\n  });\n\n  describe(\"all valid static paths can be matched exactly\", () => {\n    // Filter out patterns (those with : or *)\n    const staticPaths = VALID_URLPATTERN_PATHS.filter(\n      (p) => !p.includes(\":\") && !p.includes(\"*\")\n    );\n\n    test.each(staticPaths)(\"matches exactly: %s\", (path) => {\n      const result = matchPathnamePattern(path, path);\n      expect(result).toEqual({});\n    });\n  });\n});\n\ntest(\"decode matched params\", () => {\n  expect(matchPathnamePattern(\"/blog/:slug\", \"/blog/привет\")).toEqual({\n    slug: \"привет\",\n  });\n  expect(\n    matchPathnamePattern(\n      \"/blog/:slug\",\n      \"/blog/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82\"\n    )\n  ).toEqual({\n    slug: \"привет\",\n  });\n});\n\ndescribe(\"URLPattern Unicode/non-Latin character support\", () => {\n  // These tests verify that the URLPattern-based router properly handles\n  // non-Latin characters in redirect paths, which is important for\n  // international websites (Chinese, Japanese, Korean, etc.)\n\n  describe(\"exact path matching with non-Latin characters\", () => {\n    test(\"matches Chinese Simplified paths\", () => {\n      expect(matchPathnamePattern(\"/关于我们\", \"/关于我们\")).toEqual({});\n      expect(matchPathnamePattern(\"/产品/手机\", \"/产品/手机\")).toEqual({});\n    });\n\n    test(\"matches Chinese Traditional paths\", () => {\n      expect(matchPathnamePattern(\"/關於我們\", \"/關於我們\")).toEqual({});\n      expect(matchPathnamePattern(\"/港聞\", \"/港聞\")).toEqual({});\n    });\n\n    test(\"matches Japanese paths (Hiragana, Katakana, Kanji)\", () => {\n      expect(matchPathnamePattern(\"/こんにちは\", \"/こんにちは\")).toEqual({});\n      expect(matchPathnamePattern(\"/カテゴリ\", \"/カテゴリ\")).toEqual({});\n      expect(matchPathnamePattern(\"/日本語\", \"/日本語\")).toEqual({});\n    });\n\n    test(\"matches Korean paths (Hangul)\", () => {\n      expect(matchPathnamePattern(\"/한국어\", \"/한국어\")).toEqual({});\n      expect(matchPathnamePattern(\"/블로그/포스트\", \"/블로그/포스트\")).toEqual(\n        {}\n      );\n    });\n\n    test(\"matches Cyrillic paths\", () => {\n      expect(matchPathnamePattern(\"/о-нас\", \"/о-нас\")).toEqual({});\n      expect(matchPathnamePattern(\"/блог/статья\", \"/блог/статья\")).toEqual({});\n    });\n\n    test(\"matches Arabic paths\", () => {\n      expect(matchPathnamePattern(\"/مرحبا\", \"/مرحبا\")).toEqual({});\n    });\n\n    test(\"matches Hebrew paths\", () => {\n      expect(matchPathnamePattern(\"/שלום\", \"/שלום\")).toEqual({});\n    });\n\n    test(\"matches Greek paths\", () => {\n      expect(matchPathnamePattern(\"/σχετικά\", \"/σχετικά\")).toEqual({});\n    });\n\n    test(\"matches European diacritics\", () => {\n      expect(matchPathnamePattern(\"/über-uns\", \"/über-uns\")).toEqual({});\n      expect(matchPathnamePattern(\"/café\", \"/café\")).toEqual({});\n      expect(matchPathnamePattern(\"/niño\", \"/niño\")).toEqual({});\n    });\n  });\n\n  describe(\"dynamic segments with non-Latin characters\", () => {\n    test(\"captures Chinese characters in :slug parameter\", () => {\n      expect(matchPathnamePattern(\"/:slug\", \"/关于我们\")).toEqual({\n        slug: \"关于我们\",\n      });\n    });\n\n    test(\"captures Japanese characters in :slug parameter\", () => {\n      expect(matchPathnamePattern(\"/blog/:slug\", \"/blog/日本語\")).toEqual({\n        slug: \"日本語\",\n      });\n    });\n\n    test(\"captures Korean characters in :slug parameter\", () => {\n      expect(\n        matchPathnamePattern(\"/:category/:post\", \"/블로그/포스트\")\n      ).toEqual({\n        category: \"블로그\",\n        post: \"포스트\",\n      });\n    });\n  });\n\n  describe(\"wildcard patterns with non-Latin characters\", () => {\n    test(\"matches wildcard with Chinese paths\", () => {\n      expect(matchPathnamePattern(\"/blog/*\", \"/blog/中文/测试\")).toEqual({\n        0: \"中文/测试\",\n      });\n    });\n\n    test(\"matches wildcard with Japanese paths\", () => {\n      expect(\n        matchPathnamePattern(\"/カテゴリ/*\", \"/カテゴリ/記事/詳細\")\n      ).toEqual({\n        0: \"記事/詳細\",\n      });\n    });\n  });\n\n  describe(\"URL-encoded vs literal matching\", () => {\n    test(\"matches URL-encoded paths and decodes them\", () => {\n      // %E6%B8%AF%E8%81%9E is URL-encoded 港聞\n      expect(matchPathnamePattern(\"/:slug\", \"/%E6%B8%AF%E8%81%9E\")).toEqual({\n        slug: \"港聞\",\n      });\n    });\n\n    test(\"matches mixed Latin and non-Latin paths\", () => {\n      expect(matchPathnamePattern(\"/blog/关于\", \"/blog/关于\")).toEqual({});\n      expect(matchPathnamePattern(\"/news/:slug\", \"/news/港聞\")).toEqual({\n        slug: \"港聞\",\n      });\n    });\n  });\n});\n\ntest(\"check pathname is pattern\", () => {\n  expect(isPathnamePattern(\"/:name\")).toEqual(true);\n  expect(isPathnamePattern(\"/:slug*\")).toEqual(true);\n  expect(isPathnamePattern(\"/:id?\")).toEqual(true);\n  expect(isPathnamePattern(\"/*\")).toEqual(true);\n\n  expect(isPathnamePattern(\"\")).toEqual(false);\n  expect(isPathnamePattern(\"/\")).toEqual(false);\n  expect(isPathnamePattern(\"/blog\")).toEqual(false);\n  expect(isPathnamePattern(\"/blog/post-name\")).toEqual(false);\n});\n\ntest(\"tokenize named params in pathname pattern\", () => {\n  expect(tokenizePathnamePattern(\"/blog/:id\")).toEqual([\n    { type: \"fragment\", value: \"/blog/\" },\n    { type: \"param\", name: \"id\", optional: false, splat: false },\n  ]);\n  expect(tokenizePathnamePattern(\"/blog/:slug*\")).toEqual([\n    { type: \"fragment\", value: \"/blog/\" },\n    { type: \"param\", name: \"slug\", optional: false, splat: true },\n  ]);\n  expect(tokenizePathnamePattern(\"/blog/:name?\")).toEqual([\n    { type: \"fragment\", value: \"/blog/\" },\n    { type: \"param\", name: \"name\", optional: true, splat: false },\n  ]);\n  expect(tokenizePathnamePattern(\"/blog/*\")).toEqual([\n    { type: \"fragment\", value: \"/blog/\" },\n    { type: \"param\", name: \"0\", optional: false, splat: true },\n  ]);\n});\n\ntest(\"tokenize complex pathname pattern\", () => {\n  expect(tokenizePathnamePattern(\"/blog/:date/:slug*\")).toEqual([\n    { type: \"fragment\", value: \"/blog/\" },\n    { type: \"param\", name: \"date\", optional: false, splat: false },\n    { type: \"fragment\", value: \"/\" },\n    { type: \"param\", name: \"slug\", optional: false, splat: true },\n  ]);\n});\n\ntest(\"tokenize trailing fragment\", () => {\n  expect(tokenizePathnamePattern(\"/blog/:name/profile\")).toEqual([\n    { type: \"fragment\", value: \"/blog/\" },\n    { type: \"param\", name: \"name\", optional: false, splat: false },\n    { type: \"fragment\", value: \"/profile\" },\n  ]);\n});\n\ntest(\"tokenize pathname without params\", () => {\n  expect(tokenizePathnamePattern(\"/blog/post\")).toEqual([\n    { type: \"fragment\", value: \"/blog/post\" },\n  ]);\n});\n\ntest(\"tokenize empty pathname\", () => {\n  expect(tokenizePathnamePattern(\"\")).toEqual([\n    { type: \"fragment\", value: \"\" },\n  ]);\n});\n\ntest(\"compile pathname pattern with named values\", () => {\n  expect(\n    compilePathnamePattern(tokenizePathnamePattern(\"/blog/:id/:date\"), {\n      id: \"my-id\",\n      date: \"my-date\",\n    })\n  ).toEqual(\"/blog/my-id/my-date\");\n});\n\ntest(\"compile pathname pattern with named values\", () => {\n  expect(\n    compilePathnamePattern(tokenizePathnamePattern(\"/blog/:id/:date\"), {\n      id: \"my-id\",\n      date: \"my-date\",\n    })\n  ).toEqual(\"/blog/my-id/my-date\");\n});\n\ntest(\"compile pathname pattern with named wildcard\", () => {\n  expect(\n    compilePathnamePattern(tokenizePathnamePattern(\"/blog/:slug*/:date*\"), {\n      slug: \"my-slug\",\n      date: \"my-date\",\n    })\n  ).toEqual(\"/blog/my-slug/my-date\");\n});\n\ntest(\"compile pathname pattern with indexed wildcard\", () => {\n  expect(\n    compilePathnamePattern(tokenizePathnamePattern(\"/blog/*/:slug*/*\"), {\n      // use random order\n      1: \"one\",\n      slug: \"my-slug\",\n      0: \"zero\",\n    })\n  ).toEqual(\"/blog/zero/my-slug/one\");\n});\n\ntest(\"compile pathname pattern with many indexed wildcards (more than 10)\", () => {\n  expect(\n    compilePathnamePattern(\n      tokenizePathnamePattern(\"/blog/*/*/*/*/*/*/*/*/*/*/*/*\"),\n      {\n        // use random order\n        11: \"eleven\",\n        10: \"ten\",\n        9: \"nine\",\n        8: \"eight\",\n        7: \"seven\",\n        6: \"six\",\n        5: \"five\",\n        4: \"four\",\n        3: \"three\",\n        2: \"two\",\n        1: \"one\",\n        0: \"zero\",\n      }\n    )\n  ).toEqual(\n    \"/blog/zero/one/two/three/four/five/six/seven/eight/nine/ten/eleven\"\n  );\n});\n\ntest(\"collapse empty values\", () => {\n  expect(\n    compilePathnamePattern(tokenizePathnamePattern(\"/blog/*/:slug\"), {\n      // use random order\n      slug: \"\",\n      0: \"\",\n    })\n  ).toEqual(\"/blog//\");\n});\n\ntest(\"collapse optional values with preceding slash\", () => {\n  expect(\n    compilePathnamePattern(tokenizePathnamePattern(\"/blog/:slug?/:name?\"), {\n      slug: \"\",\n    })\n  ).toEqual(\"/blog\");\n});\n\ntest(\"validate invalid pattern\", () => {\n  expect(validatePathnamePattern(\"/:name*?\")).toEqual([\n    `Invalid path pattern '/:name*?'`,\n  ]);\n});\n\ntest(\"validate named groups\", () => {\n  expect(validatePathnamePattern(\"/:name\")).toEqual([]);\n  expect(validatePathnamePattern(\"/:name/last\")).toEqual([]);\n});\n\ntest(\"validate named groups with optional modifier\", () => {\n  expect(validatePathnamePattern(\"/:name?\")).toEqual([]);\n  expect(validatePathnamePattern(\"/:name?/last\")).toEqual([]);\n});\n\ntest('validate \"one or more\" named group modifier', () => {\n  expect(validatePathnamePattern(\"/:name+/:slug+\")).toEqual([\n    \"Dynamic parameters ':name+', ':slug+' shouldn't have the + modifier.\",\n  ]);\n});\n\ntest('validate \"zero or more\" named group modifier', () => {\n  expect(validatePathnamePattern(\"/:name*\")).toEqual([]);\n  expect(validatePathnamePattern(\"/:name*/:another*/:slug*\")).toEqual([\n    \"':name*', ':another*' should end the path.\",\n  ]);\n});\n\ntest(\"validate wildcard groups\", () => {\n  expect(validatePathnamePattern(\"/*\")).toEqual([]);\n  expect(validatePathnamePattern(\"/*?/*?\")).toEqual([\n    `Optional wildcard '*?' is not allowed.`,\n  ]);\n  expect(validatePathnamePattern(\"/*/*\")).toEqual([\n    `Wildcard '*' should end the path.`,\n  ]);\n  expect(validatePathnamePattern(\"/*/last\")).toEqual([\n    `Wildcard '*' should end the path.`,\n  ]);\n  expect(validatePathnamePattern(\"/*?/*/last\")).toEqual([\n    `Optional wildcard '*?' is not allowed.`,\n    `Wildcard '*' should end the path.`,\n  ]);\n});\n\ntest(\"forbid wildcard group with static parts before\", () => {\n  expect(validatePathnamePattern(\"/blog-*\")).toEqual([\n    `Static parts cannot be mixed with dynamic parameters at 'blog-*'.`,\n  ]);\n});\n\ntest(`forbid named group with \"zero or more\" modifier and static parts before`, () => {\n  expect(validatePathnamePattern(\"/blog-:slug*\")).toEqual([\n    `Static parts cannot be mixed with dynamic parameters at 'blog-:slug*'.`,\n  ]);\n});\n\ntest(`forbid named group with static parts before or after`, () => {\n  expect(validatePathnamePattern(\"/prefix-:id\")).toEqual([\n    `Static parts cannot be mixed with dynamic parameters at 'prefix-:id'.`,\n  ]);\n  expect(validatePathnamePattern(\"/:id-suffix\")).toEqual([\n    `Static parts cannot be mixed with dynamic parameters at ':id-suffix'.`,\n  ]);\n  expect(validatePathnamePattern(\"/prefix-:id-suffix\")).toEqual([\n    `Static parts cannot be mixed with dynamic parameters at 'prefix-:id-suffix'.`,\n  ]);\n});\n\ntest(`? should be allowed in named groups only`, () => {\n  expect(validatePathnamePattern(\"/name?\")).toEqual([\n    `Optional parameter indicator ? must be at the end of the named parameter. Correct usage: /:param?`,\n  ]);\n});\n"
  },
  {
    "path": "apps/builder/app/builder/shared/url-pattern.ts",
    "content": "import { matchPathnameParams } from \"@webstudio-is/sdk\";\nimport { URLPattern } from \"urlpattern-polyfill\";\n\nexport { isPathnamePattern } from \"@webstudio-is/sdk\";\n\nconst baseUrl = \"http://url\";\n\nconst tryDecode = (encoded: string) => {\n  try {\n    return decodeURIComponent(encoded);\n  } catch {\n    return encoded;\n  }\n};\n\nexport const matchPathnamePattern = (pattern: string, pathname: string) => {\n  try {\n    const groups = new URLPattern({ pathname: pattern }).exec({ pathname })\n      ?.pathname.groups;\n    if (groups) {\n      const decodedGroups: Record<string, undefined | string> = {};\n      for (const [name, value] of Object.entries(groups)) {\n        if (value) {\n          decodedGroups[name] = tryDecode(value);\n        }\n      }\n      return decodedGroups;\n    }\n  } catch {\n    // empty block\n  }\n};\n\n// allowed syntax\n// :name - group without modifiers\n// :name? - group with optional modifier\n// :name* - group with zero or more modifier in the end\n// * - wildcard group in the end\n\ntype Token =\n  | { type: \"fragment\"; value: string }\n  | { type: \"param\"; name: string; optional: boolean; splat: boolean };\n\nexport const tokenizePathnamePattern = (pathname: string) => {\n  const tokens: Token[] = [];\n  let lastCursor = 0;\n  let lastWildcard = -1;\n\n  for (const match of matchPathnameParams(pathname)) {\n    const cursor = match.index ?? 0;\n    if (lastCursor < cursor) {\n      tokens.push({\n        type: \"fragment\",\n        value: pathname.slice(lastCursor, cursor),\n      });\n    }\n    lastCursor = cursor + match[0].length;\n    if (match.groups?.name) {\n      const optional = match.groups.modifier === \"?\";\n      const splat = match.groups.modifier === \"*\";\n\n      tokens.push({ type: \"param\", name: match.groups.name, optional, splat });\n    }\n    if (match.groups?.wildcard) {\n      lastWildcard += 1;\n      tokens.push({\n        type: \"param\",\n        name: lastWildcard.toString(),\n        splat: true,\n        optional: false,\n      });\n    }\n  }\n  if (lastCursor < pathname.length || tokens.length === 0) {\n    tokens.push({\n      type: \"fragment\",\n      value: pathname.slice(lastCursor),\n    });\n  }\n  return tokens;\n};\n\nexport const compilePathnamePattern = (\n  tokens: Token[],\n  values: Record<string, undefined | string>\n) => {\n  let compiledPathname = \"\";\n  for (const token of tokens) {\n    if (token.type === \"fragment\") {\n      compiledPathname += token.value;\n    }\n    if (token.type === \"param\") {\n      const value = values[token.name] ?? \"\";\n      // remove preceding slash\n      if (token.optional && value.length === 0) {\n        compiledPathname = compiledPathname.slice(0, -1);\n      }\n      compiledPathname += value;\n    }\n  }\n  return compiledPathname;\n};\n\nexport const validatePathnamePattern = (pathname: string) => {\n  try {\n    new URLPattern(pathname, baseUrl);\n  } catch {\n    return [`Invalid path pattern '${pathname}'`];\n  }\n\n  const messages: string[] = [];\n\n  // fobid :name+ everywhere\n  const namedGroupsWithPlus = Array.from(pathname.matchAll(/:\\w+\\+/g)).flat();\n  if (namedGroupsWithPlus.length > 0) {\n    const list = namedGroupsWithPlus.map((item) => `'${item}'`).join(\", \");\n    messages.push(`Dynamic parameters ${list} shouldn't have the + modifier.`);\n  }\n\n  // :name* in the middle\n  const namedGroupsWithAsterisk = Array.from(\n    // skip matching in the end of string\n    pathname.matchAll(/:\\w+\\*(?!$)/g)\n  ).flat();\n  if (namedGroupsWithAsterisk.length > 0) {\n    const list = namedGroupsWithAsterisk.map((item) => `'${item}'`).join(\", \");\n    messages.push(`${list} should end the path.`);\n  }\n\n  // *? everywhere\n  const wildcardGroupsWithQuestion = Array.from(\n    pathname.matchAll(/\\*\\?/g)\n  ).flat();\n  if (wildcardGroupsWithQuestion.length > 0) {\n    messages.push(`Optional wildcard '*?' is not allowed.`);\n  }\n\n  // * in the middle\n  const wildcardGroups = Array.from(\n    // skip matching with :name before *\n    // skip matching with ? after *\n    // skip matching with * in the end\n    pathname.matchAll(/(?<!:\\w+)\\*(?!\\?)(?!$)/g)\n  ).flat();\n  if (wildcardGroups.length > 0) {\n    messages.push(`Wildcard '*' should end the path.`);\n  }\n\n  // show segment errors only when syntax is valid\n  if (messages.length > 0) {\n    return messages;\n  }\n\n  for (const segment of pathname.split(\"/\")) {\n    const group = segment.match(/(?<group>:\\w+(\\*|\\?)?)/)?.groups?.group;\n    if (group) {\n      if (group.length !== segment.length) {\n        messages.push(\n          `Static parts cannot be mixed with dynamic parameters at '${segment}'.`\n        );\n      }\n    } else if (segment.includes(\"*\")) {\n      if (segment.length > 1) {\n        messages.push(\n          `Static parts cannot be mixed with dynamic parameters at '${segment}'.`\n        );\n      }\n    } else if (segment.includes(\"?\")) {\n      messages.push(\n        `Optional parameter indicator ? must be at the end of the named parameter. Correct usage: /:param?`\n      );\n    }\n  }\n\n  return messages;\n};\n"
  },
  {
    "path": "apps/builder/app/builder/shared/use-disable-context-menu.ts",
    "content": "import { useEffect } from \"react\";\n\n/**\n * Disables the default browser context menu throughout the application,\n * except for interactive elements like links, inputs, textareas, etc.\n *\n * Users expect to see Webstudio's custom context menu when right-clicking,\n * not the browser's default menu. This hook prevents confusion by ensuring\n * only the application's context menu appears, while still allowing the\n * browser menu for interactive elements where it's useful (e.g., copy/paste\n * in inputs, open link in new tab).\n */\nexport const useDisableContextMenu = () => {\n  useEffect(() => {\n    const handleContextMenu = (event: MouseEvent) => {\n      const target = event.target as HTMLElement;\n\n      // Allow context menu for interactive elements\n      const interactiveSelectors = [\n        \"input\",\n        \"textarea\",\n        \"select\",\n        \"a[href]\",\n        '[contenteditable=\"true\"]',\n        \"pre\",\n      ];\n\n      const isInteractive = interactiveSelectors.some((selector) =>\n        target.closest(selector)\n      );\n\n      if (!isInteractive) {\n        event.preventDefault();\n      }\n    };\n\n    document.addEventListener(\"contextmenu\", handleContextMenu);\n    return () => {\n      document.removeEventListener(\"contextmenu\", handleContextMenu);\n    };\n  }, []);\n};\n"
  },
  {
    "path": "apps/builder/app/builder/sidebar-left/sidebar-left.tsx",
    "content": "import { useRef, useState, type ReactNode } from \"react\";\nimport { Kbd, rawTheme, Text } from \"@webstudio-is/design-system\";\nimport { useSubscribe, type Publish } from \"~/shared/pubsub\";\nimport {\n  $dragAndDropState,\n  $isContentMode,\n  $isPreviewMode,\n} from \"~/shared/nano-states\";\nimport { Flex } from \"@webstudio-is/design-system\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport {\n  ExtensionIcon,\n  HelpIcon,\n  ImageIcon,\n  NavigatorIcon,\n  PageIcon,\n  PlusIcon,\n  type IconComponent,\n} from \"@webstudio-is/icons\";\nimport { HelpCenter } from \"../features/help/help-center\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  $activeSidebarPanel,\n  setActiveSidebarPanel,\n  toggleActiveSidebarPanel,\n} from \"~/builder/shared/nano-states\";\nimport {\n  SidebarButton,\n  SidebarTabs,\n  SidebarTabsContent,\n  SidebarTabsList,\n  SidebarTabsTrigger,\n} from \"./sidebar-tabs\";\nimport {\n  ExternalDragDropMonitor,\n  POTENTIAL,\n  isBlockedByBackdrop,\n  useOnDropEffect,\n  useExternalDragStateEffect,\n} from \"~/builder/shared/assets/drag-monitor\";\nimport { getSetting, setSetting } from \"~/builder/shared/client-settings\";\nimport { ComponentsPanel } from \"~/builder/features/components\";\nimport { PagesPanel } from \"~/builder/features/pages\";\nimport { NavigatorPanel } from \"~/builder/features/navigator\";\nimport { AssetsPanel } from \"~/builder/features/assets\";\nimport { MarketplacePanel } from \"~/builder/features/marketplace\";\nimport type { SidebarPanelName } from \"./types\";\n\nconst none = { Panel: () => null };\n\nconst HelpTabTrigger = () => {\n  const [helpIsOpen, setHelpIsOpen] = useState(false);\n  return (\n    <HelpCenter onOpenChange={setHelpIsOpen}>\n      <HelpCenter.Trigger asChild>\n        <SidebarButton\n          label=\"Learn Webstudio or ask for help\"\n          data-state={helpIsOpen ? \"active\" : undefined}\n        >\n          <HelpIcon size={rawTheme.spacing[10]} />\n        </SidebarButton>\n      </HelpCenter.Trigger>\n    </HelpCenter>\n  );\n};\n\ntype PanelConfig = {\n  name: SidebarPanelName;\n  label: ReactNode;\n  Icon: IconComponent;\n  Panel: (props: { publish: Publish; onClose: () => void }) => ReactNode;\n  visibility?: {\n    content?: boolean; // if set, controls visibility in edit mode, if not the panel is visible\n    // Probably other modes\n  };\n};\n\nconst isPanelVisible = (\n  panel: Pick<PanelConfig, \"visibility\">,\n  {\n    isPreviewMode,\n    isContentMode,\n  }: { isPreviewMode: boolean; isContentMode: boolean }\n) => {\n  if (isPreviewMode) {\n    return false;\n  }\n\n  const { visibility } = panel;\n\n  // If visibility is not defined, the panel is always visible\n  if (visibility === undefined) {\n    return true;\n  }\n\n  if (isContentMode) {\n    // If visibility.edit is not defined, the panel is visible\n    return visibility.content ?? true;\n  }\n\n  return true;\n};\n\nconst panels: PanelConfig[] = [\n  {\n    name: \"components\",\n    label: (\n      <Text>\n        Components&nbsp;&nbsp;\n        <Kbd value={[\"A\"]} color=\"moreSubtle\" />\n      </Text>\n    ),\n    Icon: PlusIcon,\n    Panel: ComponentsPanel,\n    visibility: {\n      content: false,\n    },\n  },\n  {\n    name: \"pages\",\n    label: \"Pages\",\n    Icon: PageIcon,\n    Panel: PagesPanel,\n  },\n  {\n    name: \"navigator\",\n    label: (\n      <Text>\n        Navigator&nbsp;&nbsp;\n        <Kbd value={[\"z\"]} color=\"moreSubtle\" />\n      </Text>\n    ),\n    Icon: NavigatorIcon,\n    Panel: NavigatorPanel,\n  },\n  {\n    name: \"assets\",\n    label: \"Assets\",\n    Icon: ImageIcon,\n    Panel: AssetsPanel,\n  },\n  {\n    name: \"marketplace\",\n    label: \"Marketplace\",\n    Icon: ExtensionIcon,\n    Panel: MarketplacePanel,\n    visibility: {\n      content: false,\n    },\n  },\n];\n\nconst setSidebarPanelWidth = (panelName: string, width: number) => {\n  const widths = getSetting(\"sidebarPanelWidths\");\n  setSetting(\"sidebarPanelWidths\", { ...widths, [panelName]: width });\n};\n\nconst getSidebarPanelWidth = (panelName: SidebarPanelName) => {\n  if (panelName === \"none\") {\n    return theme.sizes.sidebarWidth;\n  }\n  const width = getSetting(\"sidebarPanelWidths\")[panelName];\n  if (width === undefined) {\n    return theme.sizes.sidebarWidth;\n  }\n  return width + \"px\";\n};\n\ntype SidebarLeftProps = {\n  publish: Publish;\n};\n\nexport const SidebarLeft = ({ publish }: SidebarLeftProps) => {\n  const activePanel = useStore($activeSidebarPanel);\n  const dragAndDropState = useStore($dragAndDropState);\n  const { Panel } = panels.find((item) => item.name === activePanel) ?? none;\n  const isPreviewMode = useStore($isPreviewMode);\n  const isContentMode = useStore($isContentMode);\n  const tabsWrapperRef = useRef<HTMLDivElement>(null);\n  const returnTabRef = useRef<SidebarPanelName | undefined>(undefined);\n\n  useSubscribe(\"dragEnd\", () => {\n    setActiveSidebarPanel(\"auto\");\n  });\n\n  useOnDropEffect(() => {\n    const element = tabsWrapperRef.current;\n\n    if (element == null) {\n      return;\n    }\n\n    if (isBlockedByBackdrop(element)) {\n      return;\n    }\n\n    returnTabRef.current = undefined;\n  });\n\n  useExternalDragStateEffect((state) => {\n    if (state !== POTENTIAL) {\n      if (returnTabRef.current !== undefined) {\n        setActiveSidebarPanel(returnTabRef.current);\n      }\n      returnTabRef.current = undefined;\n      return;\n    }\n\n    const element = tabsWrapperRef.current;\n\n    if (element == null) {\n      return;\n    }\n\n    if (isBlockedByBackdrop(element)) {\n      return;\n    }\n\n    returnTabRef.current = activePanel;\n    // Save prevous state\n    setActiveSidebarPanel(\"assets\");\n  });\n\n  const modes = { isContentMode, isPreviewMode };\n\n  return (\n    <SidebarTabs\n      activationMode=\"manual\"\n      value={activePanel}\n      orientation=\"vertical\"\n    >\n      {\n        // In preview mode, we don't show left sidebar, but we want to allow pages panel to be open in the preview mode.\n        // This way user can switch pages without exiting preview mode.\n      }\n      {isPreviewMode === false && (\n        <Flex\n          grow\n          direction=\"column\"\n          css={{ borderRight: `1px solid ${theme.colors.borderMain}` }}\n        >\n          <ExternalDragDropMonitor />\n          <div ref={tabsWrapperRef} style={{ display: \"contents\" }}>\n            <SidebarTabsList>\n              {panels\n                .filter((panel) => isPanelVisible(panel, modes))\n                .map(({ name, Icon, label }) => {\n                  return (\n                    <SidebarTabsTrigger\n                      key={name}\n                      label={label}\n                      value={name}\n                      onClick={() => {\n                        toggleActiveSidebarPanel(name);\n                      }}\n                    >\n                      <Icon size={rawTheme.spacing[10]} />\n                    </SidebarTabsTrigger>\n                  );\n                })}\n            </SidebarTabsList>\n          </div>\n\n          <HelpTabTrigger />\n        </Flex>\n      )}\n\n      <SidebarTabsContent\n        value={activePanel === \"none\" ? \"\" : activePanel}\n        onResize={({ width }) => {\n          if (activePanel !== \"none\") {\n            setSidebarPanelWidth(activePanel, width);\n          }\n        }}\n        onKeyDown={(event) => {\n          if (event.key === \"Escape\") {\n            setActiveSidebarPanel(\"none\");\n          }\n        }}\n        resizable\n        css={{\n          \"--sidebar-left-panel-width\": `${getSidebarPanelWidth(activePanel)}`,\n          width: \"var(--sidebar-left-panel-width)\",\n          minWidth: theme.sizes.sidebarWidth,\n          maxWidth: theme.spacing[35],\n          // We need the node to be rendered but hidden\n          // to keep receiving the drag events.\n          visibility:\n            dragAndDropState.isDragging &&\n            dragAndDropState.dragPayload?.origin === \"panel\" &&\n            getSetting(\"navigatorLayout\") !== \"undocked\"\n              ? \"hidden\"\n              : \"visible\",\n        }}\n      >\n        <Flex\n          css={{\n            position: \"relative\",\n            height: \"100%\",\n            flexGrow: 1,\n            background: theme.colors.backgroundPanel,\n          }}\n          direction=\"column\"\n        >\n          <Panel\n            publish={publish}\n            onClose={() => setActiveSidebarPanel(\"none\")}\n          />\n        </Flex>\n      </SidebarTabsContent>\n    </SidebarTabs>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/sidebar-left/sidebar-tabs.tsx",
    "content": "import {\n  Box,\n  Tabs,\n  TabsContent,\n  TabsList,\n  TabsTrigger,\n  Tooltip,\n  css,\n  focusRingStyle,\n  styled,\n  theme,\n  useResize,\n  type CSS,\n} from \"@webstudio-is/design-system\";\nimport {\n  forwardRef,\n  useEffect,\n  useRef,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\n\nexport const SidebarTabs = styled(Tabs, {\n  display: \"flex\",\n  flexDirection: \"column\",\n  alignItems: \"center\",\n  position: \"relative\",\n  boxSizing: \"border-box\",\n  flexGrow: 1,\n});\n\nconst triggerFocusRing = focusRingStyle();\n\nconst buttonStyle = css({\n  position: \"relative\",\n  boxSizing: \"border-box\",\n  flexShrink: 0,\n  display: \"flex\",\n  size: theme.spacing[15],\n  m: 0,\n  userSelect: \"none\",\n  outline: \"none\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  color: theme.colors.foregroundIconMain,\n  backgroundColor: theme.colors.backgroundPanel,\n  border: \"none\",\n  \"&:focus-visible\": triggerFocusRing,\n  \"@hover\": {\n    \"&:hover\": {\n      backgroundColor: theme.colors.backgroundHover,\n    },\n  },\n\n  '&[data-state=\"active\"]': {\n    backgroundColor: theme.colors.backgroundHover,\n  },\n});\n\nexport const SidebarButton = forwardRef<\n  HTMLButtonElement,\n  ComponentProps<\"button\"> & { label: string }\n>(({ label, ...props }, ref) => {\n  return (\n    <Tooltip side=\"right\" content={label}>\n      <button\n        {...props}\n        ref={ref}\n        aria-label={label}\n        className={buttonStyle()}\n      ></button>\n    </Tooltip>\n  );\n});\n\nexport const SidebarTabsTrigger = forwardRef<\n  HTMLButtonElement,\n  ComponentProps<typeof TabsTrigger> & { label: ReactNode | string }\n>(({ label, children, ...props }, ref) => {\n  return (\n    <Tooltip side=\"right\" content={label}>\n      <Box>\n        <TabsTrigger\n          {...props}\n          ref={ref}\n          aria-label={typeof label === \"string\" ? label : undefined}\n          className={buttonStyle()}\n        >\n          {children}\n        </TabsTrigger>\n      </Box>\n    </Tooltip>\n  );\n});\n\nexport const SidebarTabsList = styled(TabsList, {\n  boxSizing: \"border-box\",\n  flexShrink: 0,\n  display: \"flex\",\n  flexDirection: \"column\",\n  alignItems: \"center\",\n  outline: \"none\",\n  flexGrow: 1,\n  backgroundColor: theme.colors.backgroundPanel,\n});\n\nconst sidebarTabsContentStyle = css({\n  flexGrow: 1,\n  position: \"absolute\",\n  top: 0,\n  left: \"100%\",\n  height: \"100%\",\n  backgroundColor: theme.colors.backgroundPanel,\n  outline: \"none\",\n  // Drawing border this way to ensure content still has full width, avoid subpixels and give layout round numbers\n  \"&::after\": {\n    content: \"''\",\n    position: \"absolute\",\n    top: 0,\n    right: 0,\n    bottom: 0,\n    width: 1,\n    background: theme.colors.borderMain,\n  },\n  variants: {\n    resizable: {\n      true: {\n        overflow: \"auto\",\n        resize: \"horizontal\",\n      },\n    },\n  },\n});\n\ntype SidebarTabsContentProps = Omit<\n  ComponentProps<typeof TabsContent>,\n  \"onResize\"\n> & {\n  css?: CSS;\n  resizable?: boolean;\n  onResize?: (size: { width: number; height: number }) => void;\n};\n\nexport const SidebarTabsContent = ({\n  resizable,\n  css,\n  onResize,\n  ...props\n}: SidebarTabsContentProps) => {\n  const onResizeRef = useRef(onResize);\n  onResizeRef.current = onResize;\n\n  const [element, setElement] = useResize({\n    onResizeEnd: (entries) => {\n      if (entries[0] && onResizeRef.current) {\n        onResizeRef.current({\n          width: entries[0].contentRect.width,\n          height: entries[0].contentRect.height,\n        });\n      }\n      element?.style.removeProperty(\"width\");\n    },\n  });\n\n  useEffect(() => {\n    if (element && onResizeRef.current) {\n      const rect = element.getBoundingClientRect();\n      onResizeRef.current({ width: rect.width, height: rect.height });\n    }\n  }, [element]);\n\n  return (\n    <TabsContent\n      {...props}\n      ref={resizable ? setElement : undefined}\n      className={sidebarTabsContentStyle({ css, resizable })}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/builder/sidebar-left/types.ts",
    "content": "export const sidebarPanelNames = [\n  \"assets\",\n  \"components\",\n  \"navigator\",\n  \"pages\",\n  \"marketplace\",\n] as const;\n\nexport type SidebarPanelName = (typeof sidebarPanelNames)[number] | \"none\";\n"
  },
  {
    "path": "apps/builder/app/canvas/canvas.tsx",
    "content": "import { useMemo, useEffect, useState, useLayoutEffect, useRef } from \"react\";\nimport { ErrorBoundary, type FallbackProps } from \"react-error-boundary\";\nimport { useStore } from \"@nanostores/react\";\nimport { type Instances, coreMetas } from \"@webstudio-is/sdk\";\nimport { coreTemplates } from \"@webstudio-is/sdk/core-templates\";\nimport type { Components } from \"@webstudio-is/react-sdk\";\nimport { wsImageLoader, wsVideoLoader } from \"@webstudio-is/image\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport * as baseComponents from \"@webstudio-is/sdk-components-react\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport { hooks as baseComponentHooks } from \"@webstudio-is/sdk-components-react/hooks\";\nimport * as baseComponentTemplates from \"@webstudio-is/sdk-components-react/templates\";\nimport * as animationComponents from \"@webstudio-is/sdk-components-animation\";\nimport * as animationComponentMetas from \"@webstudio-is/sdk-components-animation/metas\";\nimport * as animationTemplates from \"@webstudio-is/sdk-components-animation/templates\";\nimport { hooks as animationComponentHooks } from \"@webstudio-is/sdk-components-animation/hooks\";\nimport * as radixComponents from \"@webstudio-is/sdk-components-react-radix\";\nimport * as radixComponentMetas from \"@webstudio-is/sdk-components-react-radix/metas\";\nimport * as radixTemplates from \"@webstudio-is/sdk-components-react-radix/templates\";\nimport { hooks as radixComponentHooks } from \"@webstudio-is/sdk-components-react-radix/hooks\";\nimport { ErrorMessage } from \"~/shared/error\";\nimport { $publisher, publish } from \"~/shared/pubsub\";\nimport {\n  registerContainers,\n  serverSyncStore,\n  useCanvasStore,\n} from \"~/shared/sync/sync-stores\";\nimport {\n  GlobalStyles,\n  subscribeStyles,\n  mountStyles,\n  manageDesignModeStyles,\n  manageContentEditModeStyles,\n} from \"./shared/styles\";\nimport {\n  WebstudioComponentCanvas,\n  WebstudioComponentPreview,\n} from \"./features/webstudio-component\";\nimport {\n  $assets,\n  $pages,\n  $instances,\n  registerComponentLibrary,\n  $registeredComponents,\n  subscribeComponentHooks,\n  $isPreviewMode,\n  $isDesignMode,\n  $isContentMode,\n  subscribeModifierKeys,\n  assetBaseUrl,\n  $breakpoints,\n} from \"~/shared/nano-states\";\nimport { useDragAndDrop } from \"./shared/use-drag-drop\";\nimport {\n  initCopyPaste,\n  initCopyPasteForContentEditMode,\n} from \"~/shared/copy-paste/init-copy-paste\";\nimport { inflateInstance, subscribeInflator } from \"./inflator\";\nimport { useWindowResizeDebounced } from \"~/shared/dom-hooks\";\nimport { subscribeInstanceSelection } from \"./instance-selection\";\nimport { subscribeInstanceHovering } from \"./instance-hovering\";\nimport { useHashLinkSync } from \"~/shared/pages\";\nimport { useMount } from \"~/shared/hook-utils/use-mount\";\nimport { subscribeInterceptedEvents } from \"./interceptor\";\nimport { subscribeCommands } from \"~/canvas/shared/commands\";\nimport { updateCollaborativeInstanceRect } from \"./collaborative-instance\";\nimport { initCanvasApi } from \"~/shared/canvas-api\";\nimport { subscribeFontLoadingDone } from \"./shared/font-weight-support\";\nimport { subscribeSelected } from \"./instance-selected\";\nimport { subscribeGridGuidesOnSelected } from \"./grid-guide-utils\";\nimport { subscribeScrollNewInstanceIntoView } from \"./shared/scroll-new-instance-into-view\";\nimport { $selectedPage } from \"~/shared/awareness\";\nimport { createInstanceElement } from \"./elements\";\nimport { subscribeScrollbarSize } from \"./scrollbar-width\";\nimport { compareMedia } from \"@webstudio-is/css-engine\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport { useDebounceEffect } from \"@webstudio-is/design-system\";\nimport { subscribeInstanceContextMenu } from \"./instance-context-menu\";\n\nregisterContainers();\n\nconst FallbackComponent = ({ error, resetErrorBoundary }: FallbackProps) => {\n  // try to recover from error when webstudio data is changed again\n  useEffect(() => {\n    return serverSyncStore.subscribe(resetErrorBoundary);\n  }, [resetErrorBoundary]);\n  return (\n    // body is required to prevent breaking collapsed instances logic\n    <body>\n      <ErrorMessage\n        error={{\n          message: error instanceof Error ? error.message : \"Unknown error\",\n          status: 500,\n        }}\n      />\n    </body>\n  );\n};\n\nconst handleError = (error: unknown) => {\n  if (error instanceof Error) {\n    builderApi.toast.error(error.message);\n    return;\n  }\n\n  builderApi.toast.error(`Unknown error: ${String(error)}`);\n  console.error(error);\n};\n\nconst useElementsTree = (components: Components, instances: Instances) => {\n  const isSafeMode = builderApi.isSafeMode();\n  const page = useStore($selectedPage);\n  const isPreviewMode = useStore($isPreviewMode);\n  const breakpointsMap = useStore($breakpoints);\n  const rootInstanceId = page?.rootInstanceId ?? \"\";\n\n  if (typeof window === \"undefined\") {\n    // @todo remove after https://github.com/webstudio-is/webstudio/issues/1313 now its needed to be sure that no leaks exists\n\n    console.info({\n      $assets: $assets.get().size,\n      $pages: $pages.get()?.pages.length ?? 0,\n      $instances: $instances.get().size,\n    });\n  }\n\n  const breakpoints = useMemo(\n    () => [...breakpointsMap.values()].sort(compareMedia),\n    [breakpointsMap]\n  );\n\n  return useMemo(() => {\n    return (\n      <ReactSdkContext.Provider\n        value={{\n          renderer: isPreviewMode ? \"preview\" : \"canvas\",\n          isSafeMode,\n          assetBaseUrl,\n          imageLoader: wsImageLoader,\n          videoLoader: wsVideoLoader,\n          resources: {},\n          breakpoints,\n          // error reporting\n          onError: handleError,\n        }}\n      >\n        {createInstanceElement({\n          instances,\n          instanceId: rootInstanceId,\n          instanceSelector: [rootInstanceId],\n          Component: isPreviewMode\n            ? WebstudioComponentPreview\n            : WebstudioComponentCanvas,\n          components,\n        })}\n      </ReactSdkContext.Provider>\n    );\n  }, [\n    instances,\n    rootInstanceId,\n    components,\n    isPreviewMode,\n    breakpoints,\n    isSafeMode,\n  ]);\n};\n\nconst DesignMode = () => {\n  const debounceEffect = useDebounceEffect();\n  const ref = useRef<undefined | Instances>(undefined);\n\n  useDragAndDrop();\n\n  useEffect(() => {\n    const abortController = new AbortController();\n    subscribeScrollNewInstanceIntoView(\n      debounceEffect,\n      ref,\n      abortController.signal\n    );\n    const unsubscribeSelected = subscribeSelected(debounceEffect);\n    const unsubscribeGridGuides = subscribeGridGuidesOnSelected();\n    return () => {\n      unsubscribeSelected();\n      unsubscribeGridGuides();\n      abortController.abort();\n    };\n  }, [debounceEffect]);\n\n  useEffect(() => {\n    const abortController = new AbortController();\n    const options = { signal: abortController.signal };\n    // We need to initialize this in both canvas and builder,\n    // because the events will fire in either one, depending on where the focus is\n    // @todo we need to forward the events from canvas to builder and avoid importing this\n    // in both places\n    initCopyPaste(options);\n    manageDesignModeStyles(options);\n    subscribeScrollbarSize(options);\n    updateCollaborativeInstanceRect(options);\n    subscribeInstanceSelection(options);\n    subscribeInstanceHovering(options);\n    subscribeFontLoadingDone(options);\n    subscribeModifierKeys(options);\n    return () => {\n      abortController.abort();\n    };\n  }, []);\n  return null;\n};\n\nconst ContentEditMode = () => {\n  const debounceEffect = useDebounceEffect();\n  const ref = useRef<undefined | Instances>(undefined);\n\n  useEffect(() => {\n    const abortController = new AbortController();\n    subscribeScrollNewInstanceIntoView(\n      debounceEffect,\n      ref,\n      abortController.signal\n    );\n    const unsubscribeSelected = subscribeSelected(debounceEffect);\n    const unsubscribeGridGuides = subscribeGridGuidesOnSelected();\n    return () => {\n      unsubscribeSelected();\n      unsubscribeGridGuides();\n      abortController.abort();\n    };\n  }, [debounceEffect]);\n\n  useEffect(() => {\n    const abortController = new AbortController();\n    const options = { signal: abortController.signal };\n    manageContentEditModeStyles(options);\n    subscribeScrollbarSize(options);\n    subscribeInstanceSelection(options);\n    subscribeInstanceHovering(options);\n    subscribeFontLoadingDone(options);\n    initCopyPasteForContentEditMode(options);\n    subscribeModifierKeys(options);\n    return () => {\n      abortController.abort();\n    };\n  }, []);\n  return null;\n};\n\nexport const Canvas = () => {\n  useCanvasStore();\n  const isDesignMode = useStore($isDesignMode);\n  const isContentMode = useStore($isContentMode);\n\n  useMount(() => {\n    registerComponentLibrary({\n      components: {},\n      metas: coreMetas,\n      templates: coreTemplates,\n    });\n    registerComponentLibrary({\n      components: baseComponents,\n      metas: baseComponentMetas,\n      hooks: baseComponentHooks,\n      templates: baseComponentTemplates,\n    });\n    registerComponentLibrary({\n      namespace: \"@webstudio-is/sdk-components-react-radix\",\n      components: radixComponents,\n      metas: radixComponentMetas,\n      hooks: radixComponentHooks,\n      templates: radixTemplates,\n    });\n    registerComponentLibrary({\n      namespace: \"@webstudio-is/sdk-components-animation\",\n      components: animationComponents,\n      metas: animationComponentMetas,\n      hooks: animationComponentHooks,\n      templates: animationTemplates,\n    });\n  });\n\n  useMount(initCanvasApi);\n\n  useLayoutEffect(() => {\n    mountStyles();\n  }, []);\n\n  useEffect(subscribeStyles, []);\n\n  useEffect(subscribeComponentHooks, []);\n\n  useEffect(subscribeCommands, []);\n\n  useEffect(() => {\n    $publisher.set({ publish });\n  }, []);\n\n  const selectedPage = useStore($selectedPage);\n\n  useEffect(() => {\n    const rootInstanceId = selectedPage?.rootInstanceId;\n    if (rootInstanceId !== undefined) {\n      inflateInstance(rootInstanceId);\n    }\n  });\n\n  useWindowResizeDebounced(() => {\n    const rootInstanceId = selectedPage?.rootInstanceId;\n    if (rootInstanceId !== undefined) {\n      inflateInstance(rootInstanceId);\n    }\n  });\n\n  useEffect(subscribeInflator, []);\n\n  useHashLinkSync();\n\n  useEffect(subscribeInterceptedEvents, []);\n\n  useEffect(subscribeInstanceContextMenu, []);\n\n  const components = useStore($registeredComponents);\n  const instances = useStore($instances);\n  const elements = useElementsTree(components, instances);\n\n  const [isInitialized, setInitialized] = useState(false);\n  useEffect(() => {\n    setInitialized(true);\n  }, []);\n\n  if (components.size === 0 || instances.size === 0) {\n    return;\n  }\n\n  return (\n    <>\n      <GlobalStyles />\n      {/* catch all errors in rendered components */}\n      <ErrorBoundary FallbackComponent={FallbackComponent}>\n        {elements}\n      </ErrorBoundary>\n      {\n        // Call hooks after render to ensure effects are last.\n        // Helps improve outline calculations as all styles are then applied.\n      }\n      {isDesignMode && isInitialized && <DesignMode />}\n      {isContentMode && isInitialized && <ContentEditMode />}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/collaborative-instance.ts",
    "content": "import {\n  getAllElementsBoundingBox,\n  getVisibleElementsByInstanceSelector,\n} from \"~/shared/dom-utils\";\nimport {\n  $collaborativeInstanceSelector,\n  $collaborativeInstanceRect,\n} from \"~/shared/nano-states\";\n\nexport const updateCollaborativeInstanceRect = ({\n  signal,\n}: {\n  signal: AbortSignal;\n}) => {\n  let frameHandler: number = -1;\n  let elements: HTMLElement[] = [];\n\n  const frameLoop = () => {\n    const newRect = getAllElementsBoundingBox(elements);\n    const prevRect = $collaborativeInstanceRect.get();\n\n    if (\n      newRect.x !== prevRect?.x ||\n      newRect.y !== prevRect?.y ||\n      newRect.width !== prevRect?.width ||\n      newRect.height !== prevRect?.height\n    ) {\n      $collaborativeInstanceRect.set(newRect);\n    }\n\n    frameHandler = requestAnimationFrame(frameLoop);\n  };\n\n  const unsubscribe = $collaborativeInstanceSelector.subscribe((selector) => {\n    if (selector === undefined) {\n      cancelAnimationFrame(frameHandler);\n      $collaborativeInstanceRect.set(undefined);\n      elements = [];\n      return;\n    }\n\n    elements = getVisibleElementsByInstanceSelector(selector);\n\n    if (elements.length > 0) {\n      cancelAnimationFrame(frameHandler);\n      frameLoop();\n    }\n  });\n\n  signal.addEventListener(\"abort\", () => {\n    unsubscribe();\n    cancelAnimationFrame(frameHandler);\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/elements.tsx",
    "content": "import {\n  Fragment,\n  type ForwardRefExoticComponent,\n  type JSX,\n  type RefAttributes,\n  type RefObject,\n} from \"react\";\nimport type { Instance, Instances } from \"@webstudio-is/sdk\";\nimport type { Components } from \"@webstudio-is/react-sdk\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\n\nexport type WebstudioComponentProps = {\n  instance: Instance;\n  instanceSelector: Instance[\"id\"][];\n  components: Components;\n};\n\nexport const createInstanceElement = ({\n  instances,\n  instanceId,\n  instanceSelector,\n  Component,\n  components,\n  ref,\n}: {\n  instances: Instances;\n  instanceId: Instance[\"id\"];\n  instanceSelector: InstanceSelector;\n  Component: ForwardRefExoticComponent<\n    WebstudioComponentProps & RefAttributes<HTMLElement>\n  >;\n  components: Components;\n  ref?: RefObject<HTMLElement>;\n}) => {\n  const instance = instances.get(instanceId);\n  if (instance === undefined) {\n    return null;\n  }\n  return (\n    <Component\n      ref={ref}\n      key={instance.id}\n      instance={instance}\n      instanceSelector={instanceSelector}\n      components={components}\n    />\n  );\n};\n\nconst renderText = (text: string): Array<JSX.Element> => {\n  const lines = text.split(\"\\n\");\n  return lines.map((line, index) => (\n    <Fragment key={index}>\n      {line}\n      {index < lines.length - 1 && <br />}\n    </Fragment>\n  ));\n};\n\nexport const createInstanceChildrenElements = ({\n  instances,\n  instanceSelector,\n  children,\n  Component,\n  components,\n}: {\n  instances: Instances;\n  instanceSelector: InstanceSelector;\n  children: Instance[\"children\"][0][];\n  Component: ForwardRefExoticComponent<\n    WebstudioComponentProps & RefAttributes<HTMLElement>\n  >;\n  components: Components;\n}) => {\n  const elements = children.map((child) => {\n    if (child.type === \"text\") {\n      return renderText(child.value);\n    }\n    if (child.type === \"expression\") {\n      return;\n    }\n    if (child.type === \"id\") {\n      return createInstanceElement({\n        instances,\n        instanceId: child.value,\n        instanceSelector: [child.value, ...instanceSelector],\n        Component,\n        components,\n      });\n    }\n\n    child satisfies never;\n  });\n  // let empty children be coalesced with fallback\n  if (elements.length === 0) {\n    return;\n  }\n  return elements;\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/features/build-mode/block-template.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  selectorIdAttribute,\n  type AnyComponent,\n  type WebstudioComponentSystemProps,\n} from \"@webstudio-is/react-sdk\";\nimport * as React from \"react\";\nimport { $isDesignMode, $selectedInstanceSelector } from \"~/shared/nano-states\";\n\nexport const BlockTemplate = React.forwardRef<\n  HTMLDivElement,\n  WebstudioComponentSystemProps & { children: React.ReactNode }\n>(({ ...props }, ref) => {\n  const isDesignMode = useStore($isDesignMode);\n  const selectedInstanceSelector = useStore($selectedInstanceSelector);\n  const templateInstanceStringSelector = props[selectorIdAttribute];\n\n  if (!isDesignMode) {\n    return;\n  }\n\n  if (selectedInstanceSelector === undefined) {\n    return;\n  }\n\n  if (selectedInstanceSelector.length === 0) {\n    return;\n  }\n\n  const selectedSelector = selectedInstanceSelector.join(\",\");\n\n  // Exclude all selected ancestors\n  if (!selectedSelector.endsWith(templateInstanceStringSelector)) {\n    return;\n  }\n\n  const childrenCount = React.Children.count(props.children);\n\n  return (\n    <div\n      style={{ display: childrenCount === 0 ? \"block\" : \"contents\" }}\n      ref={ref}\n      {...props}\n    />\n  );\n}) as AnyComponent;\n"
  },
  {
    "path": "apps/builder/app/canvas/features/build-mode/block.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { blockTemplateComponent } from \"@webstudio-is/sdk\";\nimport {\n  idAttribute,\n  selectorIdAttribute,\n  type AnyComponent,\n  type WebstudioComponentSystemProps,\n} from \"@webstudio-is/react-sdk\";\n\nimport * as React from \"react\";\nimport {\n  $instances,\n  $isDesignMode,\n  $isPreviewMode,\n  $selectedInstanceSelector,\n} from \"~/shared/nano-states\";\n\nexport const Block = React.forwardRef<\n  HTMLDivElement,\n  { children: React.ReactNode } & WebstudioComponentSystemProps\n>(({ children, ...props }, ref) => {\n  const instances = useStore($instances);\n  const isDesignMode = useStore($isDesignMode);\n  const isPreviewMode = useStore($isPreviewMode);\n  const instanceId = props[idAttribute];\n  const instance = instances.get(instanceId);\n  const selectedInstanceSelector = useStore($selectedInstanceSelector);\n\n  const childArray = React.Children.toArray(children).filter((child) =>\n    React.isValidElement(child)\n  );\n\n  if (instance === undefined) {\n    return <div>Content Block instance is undefined</div>;\n  }\n\n  const templateInstanceId = instance.children.find(\n    (child) =>\n      child.type === \"id\" &&\n      instances.get(child.value)?.component === blockTemplateComponent\n  )?.value;\n\n  if (templateInstanceId === undefined) {\n    return <div>Content Block template child is not found</div>;\n  }\n\n  const templateInstance = instances.get(templateInstanceId);\n\n  if (templateInstance === undefined) {\n    return <div>Content Block template instance is not found</div>;\n  }\n\n  if (isDesignMode) {\n    if (selectedInstanceSelector !== undefined) {\n      const selectedSelector = selectedInstanceSelector.join(\",\");\n      // If any template child is selected then render only template\n      const stringSelector = props[selectorIdAttribute];\n      const templateSelector = `${templateInstanceId},${stringSelector}`;\n\n      if (selectedSelector.endsWith(templateSelector)) {\n        return (\n          <div style={{ display: \"contents\" }} ref={ref} {...props}>\n            {childArray.filter((child) => {\n              const { instanceSelector } = child.props;\n\n              return instanceSelector[0] === templateInstanceId;\n            })}\n          </div>\n        );\n      }\n    }\n  }\n\n  const hasContent = childArray.length > 1;\n  const hasTemplates = templateInstance.children.length > 0;\n\n  if (!isDesignMode && !hasContent && !hasTemplates) {\n    return <></>;\n  }\n\n  const editableBlockStyle = hasContent ? { display: \"contents\" } : {};\n\n  return (\n    <div ref={ref} style={editableBlockStyle} {...props}>\n      {childArray}\n      {hasContent || isPreviewMode ? null : (\n        <div>Editable block you can edit</div>\n      )}\n    </div>\n  );\n}) as AnyComponent;\n"
  },
  {
    "path": "apps/builder/app/canvas/features/text-editor/index.ts",
    "content": "export { TextEditor } from \"./text-editor\";\n"
  },
  {
    "path": "apps/builder/app/canvas/features/text-editor/interop.test.tsx",
    "content": "import { test, expect } from \"vitest\";\nimport { createHeadlessEditor } from \"@lexical/headless\";\nimport { LinkNode } from \"@lexical/link\";\nimport { $, renderData, renderTemplate, ws } from \"@webstudio-is/template\";\nimport { $convertToLexical, $convertToUpdates, type Refs } from \"./interop\";\n\nconst { instances } = renderData(\n  <$.Body ws:id=\"bodyId\">\n    <$.Box ws:id=\"emptyBoxId\"></$.Box>\n    <$.Box ws:id=\"textBoxId\">\n      Hello{\"\\n\"}\n      <$.Bold ws:id=\"boldId\">\n        <$.Italic ws:id=\"italicId\">world</$.Italic>\n      </$.Bold>\n      {\"\\n\"}\n      <$.Span ws:id=\"spanId\">and</$.Span>\n      {\"\\n\"}\n      <$.RichTextLink ws:id=\"linkId\" href=\"/my-url\">\n        other realms\n      </$.RichTextLink>\n    </$.Box>\n    <ws.element ws:tag=\"div\" ws:id=\"textElementId\">\n      Hello{\"\\n\"}\n      <ws.element ws:tag=\"b\" ws:id=\"boldElementId\">\n        <ws.element ws:tag=\"i\" ws:id=\"italicElementId\">\n          world\n        </ws.element>\n      </ws.element>\n      {\"\\n\"}\n      <ws.element ws:tag=\"span\" ws:id=\"spanElementId\">\n        and\n      </ws.element>\n      {\"\\n\"}\n      <ws.element ws:tag=\"a\" ws:id=\"linkElementId\" href=\"/my-url\">\n        other realms\n      </ws.element>\n    </ws.element>\n  </$.Body>\n);\n\nconst expectedState = {\n  root: expect.objectContaining({\n    type: \"root\",\n    children: [\n      expect.objectContaining({\n        type: \"paragraph\",\n        children: [\n          expect.objectContaining({\n            type: \"text\",\n            format: 0,\n            style: \"\",\n            text: \"Hello\",\n          }),\n          expect.objectContaining({ type: \"linebreak\" }),\n          expect.objectContaining({\n            type: \"text\",\n            format: 3,\n            style: \"\",\n            text: \"world\",\n          }),\n          expect.objectContaining({ type: \"linebreak\" }),\n          expect.objectContaining({\n            type: \"text\",\n            format: 0,\n            style: \"--style-node-trigger: 1;\",\n            text: \"and\",\n          }),\n          expect.objectContaining({ type: \"linebreak\" }),\n          expect.objectContaining({\n            type: \"link\",\n            format: \"\",\n            rel: null,\n            target: null,\n            title: null,\n            url: \"\",\n            children: [\n              expect.objectContaining({\n                type: \"text\",\n                format: 0,\n                style: \"\",\n                text: \"other realms\",\n              }),\n            ],\n          }),\n        ],\n      }),\n    ],\n  }),\n};\n\ntest(\"convert legacy instances to lexical\", async () => {\n  const refs: Refs = new Map();\n  const editor = createHeadlessEditor({\n    nodes: [LinkNode],\n  });\n  await new Promise<void>((resolve) => {\n    editor.update(\n      () => {\n        $convertToLexical(instances, \"textBoxId\", refs);\n      },\n      { onUpdate: resolve }\n    );\n  });\n  expect(editor.getEditorState().toJSON()).toEqual(expectedState);\n  expect(refs).toEqual(\n    new Map([\n      [\"4:bold\", \"boldId\"],\n      [\"4:italic\", \"italicId\"],\n      [\"6:span\", \"spanId\"],\n      [\"8\", \"linkId\"],\n    ])\n  );\n});\n\ntest(\"convert element instances to lexical\", async () => {\n  const refs: Refs = new Map();\n  const editor = createHeadlessEditor({\n    nodes: [LinkNode],\n  });\n  await new Promise<void>((resolve) => {\n    editor.update(\n      () => {\n        $convertToLexical(instances, \"textElementId\", refs);\n      },\n      { onUpdate: resolve }\n    );\n  });\n  expect(editor.getEditorState().toJSON()).toEqual(expectedState);\n  expect(refs).toEqual(\n    new Map([\n      [\"13:bold\", \"boldElementId\"],\n      [\"13:italic\", \"italicElementId\"],\n      [\"15:span\", \"spanElementId\"],\n      [\"17\", \"linkElementId\"],\n    ])\n  );\n});\n\ntest(\"convert lexical to element instances updates\", async () => {\n  const refs: Refs = new Map();\n  const editor = createHeadlessEditor({\n    nodes: [LinkNode],\n  });\n  await new Promise<void>((resolve) => {\n    editor.update(\n      () => {\n        $convertToLexical(instances, \"textElementId\", refs);\n      },\n      { onUpdate: resolve }\n    );\n  });\n  const treeRootInstance = instances.get(\"textElementId\");\n  if (treeRootInstance === undefined) {\n    throw Error(\"Tree root instance should be in test data\");\n  }\n  const updates = editor.getEditorState().read(() => {\n    return $convertToUpdates(treeRootInstance, refs, new Map());\n  });\n  expect(updates).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"textElementId\">\n        Hello{\"\\n\"}\n        <ws.element ws:tag=\"b\" ws:id=\"boldElementId\">\n          <ws.element ws:tag=\"i\" ws:id=\"italicElementId\">\n            world\n          </ws.element>\n        </ws.element>\n        {\"\\n\"}\n        <ws.element ws:tag=\"span\" ws:id=\"spanElementId\">\n          and\n        </ws.element>\n        {\"\\n\"}\n        <ws.element ws:tag=\"a\" ws:id=\"linkElementId\" href=\"/my-url\">\n          other realms\n        </ws.element>\n      </ws.element>\n    ).instances\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/features/text-editor/interop.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport {\n  type TextNode,\n  type ElementNode,\n  $getRoot,\n  $createTextNode,\n  $createParagraphNode,\n  $createLineBreakNode,\n  $isTextNode,\n  $isElementNode,\n  $isParagraphNode,\n  $isLineBreakNode,\n} from \"lexical\";\nimport { $createLinkNode, $isLinkNode } from \"@lexical/link\";\nimport {\n  elementComponent,\n  type Instance,\n  type Instances,\n} from \"@webstudio-is/sdk\";\nimport { $isSpanNode, $setNodeSpan } from \"./toolbar-connector\";\n\n// Map<nodeKey, instanceId>\nexport type Refs = Map<string, string>;\n\nconst legacyLexicalFormats = [\n  [\"bold\", \"Bold\"],\n  [\"italic\", \"Italic\"],\n  [\"superscript\", \"Superscript\"],\n  [\"subscript\", \"Subscript\"],\n] as const;\n\nconst elementLexicalFormats = [\n  [\"bold\", \"b\"],\n  [\"italic\", \"i\"],\n  [\"superscript\", \"sup\"],\n  [\"subscript\", \"sub\"],\n] as const;\n\nconst $writeUpdates = (\n  node: ElementNode,\n  instanceChildren: Instance[\"children\"],\n  instancesList: Instance[],\n  refs: Refs,\n  newLinkKeyToInstanceId: Refs\n) => {\n  const children = node.getChildren();\n  for (const child of children) {\n    if ($isParagraphNode(child)) {\n      $writeUpdates(\n        child,\n        instanceChildren,\n        instancesList,\n        refs,\n        newLinkKeyToInstanceId\n      );\n    }\n    if ($isLineBreakNode(child)) {\n      instanceChildren.push({ type: \"text\", value: \"\\n\" });\n    }\n    if ($isLinkNode(child)) {\n      const key = child.getKey();\n      const id = refs.get(key) ?? newLinkKeyToInstanceId.get(key) ?? nanoid();\n      refs.set(key, id);\n      instanceChildren.push({\n        type: \"id\",\n        value: id,\n      });\n      const childChildren: Instance[\"children\"] = [];\n      $writeUpdates(\n        child,\n        childChildren,\n        instancesList,\n        refs,\n        newLinkKeyToInstanceId\n      );\n      instancesList.push({\n        type: \"instance\",\n        id,\n        component: elementComponent,\n        tag: \"a\",\n        children: childChildren,\n      });\n    }\n    if ($isTextNode(child)) {\n      // support nesting bold into italic and vice versa\n      // considering lexical represents both as single node\n      // and add ref suffix to distinct styling on one node key\n      const text = child.getTextContent();\n      let parentUpdates = instanceChildren;\n      if ($isSpanNode(child)) {\n        // prematurely generate span id to select it right after applying\n        const key = `${child.getKey()}:span`;\n        const id = refs.get(key) ?? nanoid();\n        refs.set(key, id);\n        const childChildren: Instance[\"children\"] = [];\n        instancesList.push({\n          type: \"instance\",\n          id,\n          component: elementComponent,\n          tag: \"span\",\n          children: childChildren,\n        });\n        parentUpdates.push({ type: \"id\", value: id });\n        parentUpdates = childChildren;\n      }\n      // convert all lexical formats\n      for (const [format, tag] of elementLexicalFormats) {\n        if (child.hasFormat(format)) {\n          const key = `${child.getKey()}:${format}`;\n          const id = refs.get(key) ?? nanoid();\n          refs.set(key, id);\n          const childInstance: Instance = {\n            type: \"instance\",\n            id,\n            component: elementComponent,\n            tag,\n            children: [],\n          };\n          instancesList.push(childInstance);\n          parentUpdates.push({ type: \"id\", value: id });\n          parentUpdates = childInstance.children;\n        }\n      }\n      parentUpdates.push({ type: \"text\", value: text });\n    }\n  }\n};\n\nexport const $convertToUpdates = (\n  treeRootInstance: Instance,\n  refs: Refs,\n  newLinkKeyToInstanceId: Refs\n) => {\n  const treeRootInstanceChildren: Instance[\"children\"] = [];\n  const instancesList: Instance[] = [\n    {\n      ...treeRootInstance,\n      children: treeRootInstanceChildren,\n    },\n  ];\n  const root = $getRoot();\n  $writeUpdates(\n    root,\n    treeRootInstanceChildren,\n    instancesList,\n    refs,\n    newLinkKeyToInstanceId\n  );\n  return instancesList;\n};\n\nconst $writeLexical = (\n  parent: ElementNode | TextNode,\n  children: Instance[\"children\"],\n  instances: Instances,\n  refs: Refs\n) => {\n  for (const child of children) {\n    if (child.type === \"text\") {\n      // convert text\n      if (child.value === \"\\n\" && $isElementNode(parent)) {\n        const lineBreakNode = $createLineBreakNode();\n        parent.append(lineBreakNode);\n        continue;\n      }\n      if ($isTextNode(parent)) {\n        parent.setTextContent(child.value);\n      } else {\n        const textNode = $createTextNode(child.value);\n        parent.append(textNode);\n      }\n      continue;\n    }\n\n    const instance = instances.get(child.value);\n    if (instance === undefined) {\n      continue;\n    }\n\n    // convert instances\n    const isLinkInstance =\n      instance.component === \"RichTextLink\" ||\n      (instance.component === elementComponent && instance.tag === \"a\");\n    if (isLinkInstance && $isElementNode(parent)) {\n      const linkNode = $createLinkNode(\"\");\n      refs.set(linkNode.getKey(), instance.id);\n      parent.append(linkNode);\n      $writeLexical(linkNode, instance.children, instances, refs);\n    }\n    if (\n      instance.component === \"Span\" ||\n      (instance.component === elementComponent && instance.tag === \"span\")\n    ) {\n      let textNode;\n      if ($isTextNode(parent)) {\n        textNode = parent;\n      } else {\n        textNode = $createTextNode(\"\");\n        parent.append(textNode);\n      }\n      $setNodeSpan(textNode);\n      refs.set(`${textNode.getKey()}:span`, instance.id);\n      $writeLexical(textNode, instance.children, instances, refs);\n    }\n    // convert all lexical formats\n    for (const [format, component] of legacyLexicalFormats) {\n      if (instance.component === component) {\n        let textNode;\n        if ($isTextNode(parent)) {\n          textNode = parent;\n        } else {\n          textNode = $createTextNode(\"\");\n          parent.append(textNode);\n        }\n        textNode.toggleFormat(format);\n        refs.set(`${textNode.getKey()}:${format}`, instance.id);\n        $writeLexical(textNode, instance.children, instances, refs);\n      }\n    }\n    // convert all lexical formats\n    for (const [format, tag] of elementLexicalFormats) {\n      if (instance.component === elementComponent && instance.tag === tag) {\n        let textNode;\n        if ($isTextNode(parent)) {\n          textNode = parent;\n        } else {\n          textNode = $createTextNode(\"\");\n          parent.append(textNode);\n        }\n        textNode.toggleFormat(format);\n        refs.set(`${textNode.getKey()}:${format}`, instance.id);\n        $writeLexical(textNode, instance.children, instances, refs);\n      }\n    }\n  }\n};\n\nexport const $convertToLexical = (\n  instances: Instances,\n  rootInstanceId: Instance[\"id\"],\n  refs: Refs\n) => {\n  const root = $getRoot();\n  const p = $createParagraphNode();\n  root.append(p);\n  const rootInstance = instances.get(rootInstanceId);\n  if (rootInstance) {\n    $writeLexical(p, rootInstance.children, instances, refs);\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/features/text-editor/text-editor.stories.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { ContentEditable } from \"@lexical/react/LexicalContentEditable\";\nimport type { StoryFn, Meta } from \"@storybook/react\";\nimport { action } from \"@storybook/addon-actions\";\nimport { Box, Button, Flex, StorySection } from \"@webstudio-is/design-system\";\nimport { theme } from \"@webstudio-is/design-system\";\nimport type { Instance, Instances, Props } from \"@webstudio-is/sdk\";\nimport { $, renderData } from \"@webstudio-is/template\";\nimport {\n  $instances,\n  $pages,\n  $registeredComponentMetas,\n  $textEditingInstanceSelector,\n  $textToolbar,\n} from \"~/shared/nano-states\";\nimport { TextEditor } from \"./text-editor\";\nimport { emitCommand, subscribeCommands } from \"~/canvas/shared/commands\";\nimport { $awareness } from \"~/shared/awareness\";\n\nexport default {\n  component: TextEditor,\n  title: \"Canvas/Text editor\",\n} satisfies Meta<typeof TextEditor>;\n\nconst createInstancePair = (\n  id: Instance[\"id\"],\n  component: string,\n  children: Instance[\"children\"]\n): [Instance[\"id\"], Instance] => {\n  return [\n    id,\n    {\n      type: \"instance\",\n      id,\n      component,\n      children,\n    },\n  ];\n};\n\nconst instances: Instances = new Map([\n  createInstancePair(\"1\", \"Text\", [\n    { type: \"text\", value: \"Paragraph you can edit Blabla \" },\n    { type: \"id\", value: \"2\" },\n    { type: \"id\", value: \"3\" },\n    { type: \"id\", value: \"5\" },\n  ]),\n  createInstancePair(\"2\", \"Bold\", [\n    { type: \"text\", value: \"Very Very very bold text \" },\n  ]),\n  createInstancePair(\"3\", \"Bold\", [{ type: \"id\", value: \"4\" }]),\n  createInstancePair(\"4\", \"Italic\", [\n    { type: \"text\", value: \"And Bold Small with small italic\" },\n  ]),\n  createInstancePair(\"5\", \"Bold\", [\n    { type: \"text\", value: \" la la la subtext\" },\n  ]),\n]);\n\nconst props: Props = new Map();\n\nexport const Basic: StoryFn<typeof TextEditor> = () => {\n  const state = useStore($textToolbar);\n\n  useEffect(subscribeCommands, []);\n\n  return (\n    <StorySection title=\"Basic\">\n      <div>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: state?.isBold ? \"bold\" : \"normal\" }}\n          onClick={(event) => {\n            event.preventDefault();\n            emitCommand(\"formatBold\");\n          }}\n        >\n          Bold\n        </button>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: state?.isItalic ? \"bold\" : \"normal\" }}\n          onClick={() => emitCommand(\"formatItalic\")}\n        >\n          Italic\n        </button>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: state?.isSuperscript ? \"bold\" : \"normal\" }}\n          onClick={() => emitCommand(\"formatSuperscript\")}\n        >\n          Superscript\n        </button>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: state?.isSubscript ? \"bold\" : \"normal\" }}\n          onClick={() => emitCommand(\"formatSubscript\")}\n        >\n          Subscript\n        </button>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: state?.isLink ? \"bold\" : \"normal\" }}\n          onClick={() => emitCommand(\"formatLink\")}\n        >\n          Link\n        </button>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: state?.isSpan ? \"bold\" : \"normal\" }}\n          onClick={() => emitCommand(\"formatSpan\")}\n        >\n          Span\n        </button>\n        <button\n          disabled={state == null}\n          style={{ fontWeight: \"normal\" }}\n          onClick={() => emitCommand(\"formatClear\")}\n        >\n          Clear\n        </button>\n        <Box\n          css={{\n            \"& > div\": {\n              padding: `0 ${theme.spacing[5]}`,\n              border: \"1px solid #999\",\n              color: \"black\",\n            },\n          }}\n        >\n          <TextEditor\n            rootInstanceSelector={[\"1\"]}\n            instances={instances}\n            props={props}\n            contentEditable={<ContentEditable />}\n            onChange={action(\"onChange\")}\n            onSelectInstance={(instanceId) =>\n              console.info(\"select instance\", instanceId)\n            }\n          />\n        </Box>\n      </div>\n    </StorySection>\n  );\n};\n\nexport const CursorPositioning: StoryFn<typeof TextEditor> = () => {\n  const textEditingInstanceSelector = useStore($textEditingInstanceSelector);\n\n  return (\n    <StorySection title=\"Cursor Positioning\">\n      <Box\n        css={{\n          width: 300,\n          \"& > div\": {\n            padding: 40,\n            backgroundColor: textEditingInstanceSelector\n              ? \"unset\"\n              : \"rgba(0,0,0,0.1)\",\n          },\n          border: \"1px solid #999\",\n          color: \"black\",\n          \" *\": {\n            outline: \"none\",\n          },\n        }}\n        onClick={(event) => {\n          if (textEditingInstanceSelector !== undefined) {\n            return;\n          }\n          $textEditingInstanceSelector.set({\n            selector: [\"1\"],\n            reason: \"click\",\n            mouseX: event.clientX,\n            mouseY: event.clientY,\n          });\n        }}\n      >\n        {textEditingInstanceSelector && (\n          <TextEditor\n            rootInstanceSelector={[\"1\"]}\n            instances={instances}\n            props={props}\n            contentEditable={<ContentEditable />}\n            onChange={action(\"onChange\")}\n            onSelectInstance={(instanceId) =>\n              console.info(\"select instance\", instanceId)\n            }\n          />\n        )}\n\n        {!textEditingInstanceSelector && (\n          <div>\n            <span>Paragraph you can edit Blabla </span>\n            <strong>Very Very very bold text </strong>\n            <strong>\n              <i>And Bold Small with small italic</i>\n            </strong>\n            <strong> la la la subtext</strong>\n          </div>\n        )}\n      </Box>\n      <br />\n      <div>\n        <i>Click on text above, see cursor position and start editing text</i>\n      </div>\n      {textEditingInstanceSelector && (\n        <Button\n          onClick={() => {\n            $textEditingInstanceSelector.set(undefined);\n          }}\n        >\n          Reset\n        </Button>\n      )}\n    </StorySection>\n  );\n};\n\nexport const CursorPositioningUpDown: StoryFn<typeof TextEditor> = () => {\n  const [{ instances }, setState] = useState(() => {\n    $pages.set({\n      folders: [],\n      homePage: {\n        id: \"homePageId\",\n        rootInstanceId: \"bodyId\",\n        meta: {},\n        path: \"\",\n        title: \"\",\n        name: \"\",\n      },\n      pages: [\n        {\n          id: \"pageId\",\n          rootInstanceId: \"bodyId\",\n          path: \"\",\n          title: \"\",\n          name: \"\",\n          meta: {},\n        },\n      ],\n    });\n\n    $awareness.set({ pageId: \"pageId\" });\n\n    $registeredComponentMetas.set(\n      new Map([\n        [\"Box\", { icon: \"icon\" }],\n        [\"Bold\", { icon: \"icon\" }],\n      ])\n    );\n\n    return renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxAId\">\n          Hello world <$.Bold ws:id=\"boldA\">Hello world</$.Bold> Hello world\n          world Hello worldsdsdj skdk ls dk jslkdjklsjdkl sdk jskdj ksjd lksdj\n          dsj\n        </$.Box>\n        <$.Box ws:id=\"boxBId\">\n          Let it be Let it be <$.Bold ws:id=\"boldB\">Let it be Let</$.Bold> Let\n          it be Let it be Let it be Let it be Let it be Let it be\n        </$.Box>\n      </$.Body>\n    );\n  });\n\n  useEffect(() => {\n    $instances.set(instances);\n  }, [instances]);\n\n  const textEditingInstanceSelector = useStore($textEditingInstanceSelector);\n\n  return (\n    <StorySection title=\"Cursor Positioning Up Down\">\n      <Flex\n        gap={2}\n        direction={\"column\"}\n        css={{\n          width: 500,\n          \"& > div > div\": {\n            padding: 5,\n            border: \"1px solid #999\",\n          },\n          \"& *[aria-readonly]\": {\n            backgroundColor: \"rgba(0,0,0,0.02)\",\n          },\n          \"& strong\": {\n            fontSize: \"1.5em\",\n          },\n\n          color: \"black\",\n          \" *\": {\n            outline: \"none\",\n          },\n        }}\n      >\n        <div style={{ display: \"contents\" }} data-ws-selector=\"boxAId,bodyId\">\n          <TextEditor\n            key={textEditingInstanceSelector?.selector[0] ?? \"\"}\n            editable={\n              textEditingInstanceSelector === undefined ||\n              textEditingInstanceSelector?.selector[0] === \"boxAId\"\n            }\n            rootInstanceSelector={[\"boxAId\", \"bodyId\"]}\n            instances={instances}\n            props={props}\n            contentEditable={<ContentEditable />}\n            onChange={(data) => {\n              setState((prev) => {\n                for (const instance of data) {\n                  prev.instances.set(instance.id, instance);\n                }\n                return prev;\n              });\n            }}\n            onSelectInstance={(instanceId) =>\n              console.info(\"select instance\", instanceId)\n            }\n          />\n        </div>\n\n        <div\n          style={{ display: \"contents\" }}\n          data-ws-selector=\"boxBId,bodyId\"\n          data-ws-collapsed=\"true\"\n        >\n          <TextEditor\n            key={textEditingInstanceSelector?.selector[0] ?? \"\"}\n            editable={textEditingInstanceSelector?.selector[0] === \"boxBId\"}\n            rootInstanceSelector={[\"boxBId\", \"bodyId\"]}\n            instances={instances}\n            props={props}\n            contentEditable={<ContentEditable />}\n            onChange={(data) => {\n              setState((prev) => {\n                for (const instance of data) {\n                  prev.instances.set(instance.id, instance);\n                }\n                return prev;\n              });\n            }}\n            onSelectInstance={(instanceId) =>\n              console.info(\"select instance\", instanceId)\n            }\n          />\n        </div>\n      </Flex>\n      <br />\n      <i>Use arrows to move between editors, clicks are not working</i>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/features/text-editor/text-editor.tsx",
    "content": "import * as colorjs from \"colorjs.io/fn\";\nimport {\n  useState,\n  useEffect,\n  useLayoutEffect,\n  useCallback,\n  useRef,\n  type JSX,\n} from \"react\";\nimport {\n  KEY_ENTER_COMMAND,\n  INSERT_LINE_BREAK_COMMAND,\n  COMMAND_PRIORITY_EDITOR,\n  RootNode,\n  ElementNode,\n  $createLineBreakNode,\n  $getSelection,\n  $isRangeSelection,\n  type EditorState,\n  $isLineBreakNode,\n  COMMAND_PRIORITY_LOW,\n  $setSelection,\n  $getRoot,\n  $isTextNode,\n  $isElementNode,\n  KEY_ARROW_DOWN_COMMAND,\n  KEY_ARROW_UP_COMMAND,\n  KEY_ARROW_RIGHT_COMMAND,\n  KEY_ARROW_LEFT_COMMAND,\n  $createRangeSelection,\n  COMMAND_PRIORITY_CRITICAL,\n  $getNearestNodeFromDOMNode,\n  // eslint-disable-next-line camelcase\n  $normalizeSelection__EXPERIMENTAL,\n  type LexicalEditor,\n  type SerializedEditorState,\n  $createTextNode,\n  KEY_DOWN_COMMAND,\n  COMMAND_PRIORITY_NORMAL,\n  type NodeKey,\n  $getNodeByKey,\n  SELECTION_CHANGE_COMMAND,\n  $selectAll,\n} from \"lexical\";\nimport { LinkNode } from \"@lexical/link\";\nimport { LexicalComposer } from \"@lexical/react/LexicalComposer\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport { RichTextPlugin } from \"@lexical/react/LexicalRichTextPlugin\";\nimport { LexicalErrorBoundary } from \"@lexical/react/LexicalErrorBoundary\";\nimport { HistoryPlugin } from \"@lexical/react/LexicalHistoryPlugin\";\nimport { LinkPlugin } from \"@lexical/react/LexicalLinkPlugin\";\n\nimport { nanoid } from \"nanoid\";\nimport { createRegularStyleSheet } from \"@webstudio-is/css-engine\";\nimport type { Instance, Instances, Props } from \"@webstudio-is/sdk\";\nimport {\n  inflatedAttribute,\n  idAttribute,\n  selectorIdAttribute,\n} from \"@webstudio-is/react-sdk\";\nimport { isDescendantOrSelf, type InstanceSelector } from \"~/shared/tree-utils\";\nimport { ToolbarConnectorPlugin } from \"./toolbar-connector\";\nimport { type Refs, $convertToLexical, $convertToUpdates } from \"./interop\";\nimport { useEffectEvent } from \"~/shared/hook-utils/effect-event\";\nimport {\n  deleteInstanceMutable,\n  findAllEditableInstanceSelector,\n  updateWebstudioData,\n} from \"~/shared/instance-utils\";\nimport {\n  $blockChildOutline,\n  $hoveredInstanceOutline,\n  $hoveredInstanceSelector,\n  $instances,\n  $registeredComponentMetas,\n  $selectedInstanceSelector,\n  $textEditingInstanceSelector,\n  $textEditorContextMenu,\n  execTextEditorContextMenuCommand,\n  findBlockChildSelector,\n  findTemplates,\n} from \"~/shared/nano-states\";\nimport {\n  getElementByInstanceSelector,\n  getVisibleElementsByInstanceSelector,\n} from \"~/shared/dom-utils\";\nimport deepEqual from \"fast-deep-equal\";\nimport { inflateInstance } from \"~/canvas/inflator\";\nimport {\n  $selectedPage,\n  addTemporaryInstance,\n  getInstancePath,\n  selectInstance,\n} from \"~/shared/awareness\";\nimport { shallowEqual } from \"shallow-equal\";\nimport {\n  insertListItemAt,\n  insertTemplateAt,\n} from \"~/builder/features/workspace/canvas-tools/outline/block-utils\";\nimport { richTextPlaceholders } from \"~/shared/content-model\";\n\nconst BindInstanceToNodePlugin = ({\n  refs,\n  rootInstanceSelector,\n}: {\n  refs: Refs;\n  rootInstanceSelector: InstanceSelector;\n}) => {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    for (const [nodeKey, instanceId] of refs) {\n      // extract key from stored key:style format\n      const [key] = nodeKey.split(\":\");\n      const element = editor.getElementByKey(key);\n      if (element) {\n        element.setAttribute(idAttribute, instanceId);\n        // We set id + root selector here, for simplicity\n        // This solves hover behavior during mouseMove for editable child outline\n        // @todo: A normal selector must be used, but it would require significantly more code to detect the tree structure.\n        element.setAttribute(\n          selectorIdAttribute,\n          [instanceId, ...rootInstanceSelector].join(\",\")\n        );\n      }\n    }\n  }, [editor, refs, rootInstanceSelector]);\n  return null;\n};\n\n/**\n * In case of text color is near transparent, make caret visible with color animation between #666 and #999\n */\nconst CaretColorPlugin = () => {\n  const [editor] = useLexicalComposerContext();\n  const caretClassName = useState(() => `a${nanoid()}`)[0];\n\n  useEffect(() => {\n    const rootElement = editor.getRootElement();\n\n    if (rootElement === null) {\n      return;\n    }\n\n    const elementColor = window.getComputedStyle(rootElement).color;\n\n    let isLightBackground = false;\n    try {\n      const color = colorjs.parse(elementColor);\n      const alpha = color.alpha ?? 1;\n      isLightBackground = alpha < 0.1;\n    } catch {\n      // If we can't parse the color, assume it's not light\n    }\n\n    if (isLightBackground) {\n      // Apply caret color with animated color\n      const sheet = createRegularStyleSheet({ name: \"text-editor-caret\" });\n\n      // Animation on cursor needed to make it visible on any background\n      sheet.addPlaintextRule(`\n\n        @keyframes ${caretClassName}-keyframes {\n          from {caret-color: #666;}\n          to {caret-color: #999;}\n        }\n\n        .${caretClassName} {\n          animation-name: ${caretClassName}-keyframes;\n          animation-duration: 0.5s;\n          animation-iteration-count: infinite;\n          animation-direction: alternate;\n        }\n      `);\n\n      rootElement.classList.add(caretClassName);\n      sheet.render();\n\n      return () => {\n        rootElement.classList.remove(caretClassName);\n        sheet.unmount();\n      };\n    }\n  }, [caretClassName, editor]);\n\n  return null;\n};\n\nconst isChrome = () =>\n  navigator.userAgentData?.brands.some(\n    (brand) => brand.brand === \"Google Chrome\"\n  );\n\nconst OnChangeOnBlurPlugin = ({\n  onChange,\n}: {\n  onChange: (editorState: EditorState, reason: \"blur\" | \"unmount\") => void;\n}) => {\n  const [editor] = useLexicalComposerContext();\n  const handleChange = useEffectEvent(onChange);\n\n  useEffect(\n    () => () => {\n      // Ensures editable content is saved if no blur event occurs before unmount.\n      // This can happen in Firefox and Safari.\n      // To reproduce: create a Content Block, edit a paragraph, then type `/` and select Heading or Paragraph from the menu.\n      // Without this, changes may be lost on unmount in FF and Safari.\n\n      if (isChrome()) {\n        // Fixes an issue in DEV MODE where, if text is center-aligned inside Flex/Grid,\n        // the code below causes Chrome to scroll the editable text block to the center of the view.\n        return;\n      }\n      // The issue is related to React’s development mode.\n      // When we set the initial selection in the Editor, we disable Lexical’s internal\n      // scrolling using the update operation tag tag: \"skip-scroll-into-view\".\n      // The problem is that a read operation forces all pending update operations to commit,\n      // and for some reason, this forced commit does not respect tags.\n      // In React’s development mode, useEffect runs twice, which causes scrollIntoView\n      // to be called during the first read.\n      // To prevent this, we disconnect the editor from the DOM\n      // by setting editor._rootElement = null;.\n      // This makes Lexical assume it’s in headless mode,\n      // preventing it from executing DOM operations.\n      editor._rootElement = null;\n\n      // Safari and FF support as no blur event is triggered in some cases\n      editor.read(() => {\n        handleChange(editor.getEditorState(), \"unmount\");\n      });\n    },\n    [editor]\n  );\n\n  useEffect(() => {\n    const handleBlur = () => {\n      // force read to get the latest state\n      editor.read(() => {\n        handleChange(editor.getEditorState(), \"blur\");\n      });\n    };\n\n    // https://github.com/facebook/lexical/blob/867d449b2a6497ff9b1fbdbd70724c74a1044d8b/packages/lexical-react/src/LexicalNodeEventPlugin.ts#L59C12-L67C8\n    return editor.registerRootListener((rootElement, prevRootElement) => {\n      rootElement?.addEventListener(\"blur\", handleBlur);\n      prevRootElement?.removeEventListener(\"blur\", handleBlur);\n    });\n  }, [editor]);\n\n  return null;\n};\n\nconst getNodeKeyFromDOMNode = (\n  dom: Node,\n  editor: LexicalEditor\n): NodeKey | undefined => {\n  const prop = `__lexicalKey_${editor._key}`;\n  return (dom as Node & Record<typeof prop, NodeKey | undefined>)[prop];\n};\n\nconst LinkSelectionPlugin = ({\n  rootInstanceSelector,\n  registerNewLink,\n}: {\n  rootInstanceSelector: InstanceSelector;\n  registerNewLink: (key: NodeKey, instanceId: string) => void;\n}) => {\n  const [editor] = useLexicalComposerContext();\n  const [preservedSelection] = useState(rootInstanceSelector);\n\n  useEffect(() => {\n    if (!editor.isEditable()) {\n      return;\n    }\n\n    const removeUpdateListener = editor.registerUpdateListener(\n      ({ editorState }) => {\n        editorState.read(() => {\n          const selectedInstanceSelector = $selectedInstanceSelector.get();\n\n          if (selectedInstanceSelector === undefined) {\n            return;\n          }\n\n          if (\n            !isDescendantOrSelf(selectedInstanceSelector, preservedSelection)\n          ) {\n            return;\n          }\n\n          const selection = $getSelection();\n          if (!$isRangeSelection(selection)) {\n            return false;\n          }\n          const key = selection.anchor.getNode().getKey();\n\n          const elt = editor.getElementByKey(key);\n          let link = elt?.closest(`a[${selectorIdAttribute}]`);\n          const newLink = elt?.closest(`a`);\n\n          while (newLink != null && link == null) {\n            // new link detected\n\n            // https://github.com/facebook/lexical/blob/b7fa4cf673869dac0c2e0c1fe667e71e72ff6adb/packages/lexical/src/LexicalUtils.ts#L465\n            const key = getNodeKeyFromDOMNode(newLink, editor);\n            if (key === undefined) {\n              console.error(\"Key not found for node\", newLink);\n              break;\n            }\n\n            // Register new link\n            const instanceId = nanoid();\n\n            newLink.setAttribute(idAttribute, instanceId);\n            // We set id + root selector here, for simplicity\n            // This solves hover behavior during mouseMove for editable child outline\n            // @todo: A normal selector must be used, but it would require significantly more code to detect the tree structure.\n            newLink.setAttribute(\n              selectorIdAttribute,\n              [instanceId, ...rootInstanceSelector].join(\",\")\n            );\n\n            registerNewLink(key, instanceId);\n\n            link = newLink;\n\n            break;\n          }\n\n          if (link == null) {\n            if (\n              shallowEqual(preservedSelection, $selectedInstanceSelector.get())\n            ) {\n              return false;\n            }\n\n            selectInstance(preservedSelection);\n\n            return false;\n          }\n\n          const selectorAttribute = link\n            .getAttribute(selectorIdAttribute)\n            ?.split(\",\");\n\n          if (selectorAttribute === undefined) {\n            return false;\n          }\n\n          if (\n            shallowEqual(selectorAttribute, $selectedInstanceSelector.get())\n          ) {\n            return false;\n          }\n\n          selectInstance(selectorAttribute);\n        });\n      }\n    );\n\n    return () => {\n      removeUpdateListener();\n    };\n  }, [editor, preservedSelection, registerNewLink, rootInstanceSelector]);\n\n  return null;\n};\n\nconst RemoveParagaphsPlugin = () => {\n  const [editor] = useLexicalComposerContext();\n\n  // register own commands before RichTextPlugin\n  // to stop propagation\n  useLayoutEffect(() => {\n    const removeCommand = editor.registerCommand(\n      KEY_ENTER_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        event?.preventDefault();\n        // returns true which stops propagation\n        return editor.dispatchCommand(INSERT_LINE_BREAK_COMMAND, false);\n      },\n      COMMAND_PRIORITY_EDITOR\n    );\n\n    // merge pasted paragraphs into single one\n    // and separate lines with line breaks\n    const removeNodeTransform = editor.registerNodeTransform(\n      RootNode,\n      (node) => {\n        // merge paragraphs into first with line breaks between\n        if (node.getChildrenSize() > 1) {\n          const children = node.getChildren();\n          let first;\n          for (let index = 0; index < children.length; index += 1) {\n            const paragraph = children[index];\n            // With default configuration root contains only paragraphs.\n            // Lexical converts headings to paragraphs on paste for example.\n            // So he we just check root children which are all paragraphs.\n            if (paragraph instanceof ElementNode) {\n              if (index === 0) {\n                first = paragraph;\n              } else if (first) {\n                first.append($createLineBreakNode());\n                for (const child of paragraph.getChildren()) {\n                  first.append(child);\n                }\n                paragraph.remove();\n              }\n            }\n          }\n        }\n      }\n    );\n\n    return () => {\n      removeCommand();\n      removeNodeTransform();\n    };\n  }, [editor]);\n\n  return null;\n};\n\nconst isSelectionLastNode = () => {\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection)) {\n    return false;\n  }\n\n  const rootNode = $getRoot();\n  const lastNode = rootNode.getLastDescendant();\n  const anchor = selection.anchor;\n\n  if ($isLineBreakNode(lastNode)) {\n    const anchorNode = anchor.getNode();\n    return (\n      $isElementNode(anchorNode) &&\n      anchorNode.getLastDescendant() === lastNode &&\n      anchor.offset === anchorNode.getChildrenSize()\n    );\n  } else if ($isTextNode(lastNode)) {\n    return (\n      anchor.offset === lastNode.getTextContentSize() &&\n      anchor.getNode() === lastNode\n    );\n  } else if ($isElementNode(lastNode)) {\n    return (\n      anchor.offset === lastNode.getChildrenSize() &&\n      anchor.getNode() === lastNode\n    );\n  }\n\n  return false;\n};\n\nconst isSelectionFirstNode = () => {\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection)) {\n    return false;\n  }\n\n  const rootNode = $getRoot();\n  const firstNode = rootNode.getFirstDescendant();\n  const anchor = selection.anchor;\n\n  if ($isLineBreakNode(firstNode)) {\n    const anchorNode = anchor.getNode();\n    return (\n      $isElementNode(anchorNode) &&\n      anchorNode.getFirstDescendant() === firstNode &&\n      anchor.offset === 0\n    );\n  } else if ($isTextNode(firstNode)) {\n    return anchor.offset === 0 && anchor.getNode() === firstNode;\n  } else if ($isElementNode(firstNode)) {\n    return anchor.offset === 0 && anchor.getNode() === firstNode;\n  }\n\n  return false;\n};\n\nconst getDomSelectionRect = () => {\n  const domSelection = window.getSelection();\n  if (!domSelection || !domSelection.focusNode) {\n    return;\n  }\n\n  // Get current line position\n  const range = domSelection.getRangeAt(0);\n\n  // The cursor position at the beginning of a line is technically associated with both:\n  // The end of the previous line\n  // The beginning of the current line\n  // Select the rectangle for the current line. It typically appears as the last rect in the list.\n  const rects = range.getClientRects();\n  const currentRect = rects[rects.length - 1] ?? undefined;\n\n  return currentRect;\n};\n\nconst getVerticalIntersectionRatio = (rectA: DOMRect, rectB: DOMRect) => {\n  const topIntersection = Math.max(rectA.top, rectB.top);\n  const bottomIntersection = Math.min(rectA.bottom, rectB.bottom);\n  const intersectionHeight = Math.max(0, bottomIntersection - topIntersection);\n  const minHeight = Math.min(rectA.height, rectB.height);\n  return minHeight === 0 ? 0 : intersectionHeight / minHeight;\n};\n\nconst caretFromPoint = (\n  x: number,\n  y: number\n): null | {\n  offset: number;\n  node: Node;\n} => {\n  if (typeof document.caretRangeFromPoint !== \"undefined\") {\n    const range = document.caretRangeFromPoint(x, y);\n    if (range === null) {\n      return null;\n    }\n    return {\n      node: range.startContainer,\n      offset: range.startOffset,\n    };\n  } else if (typeof document.caretPositionFromPoint !== \"undefined\") {\n    const range = document.caretPositionFromPoint(x, y);\n    if (range === null) {\n      return null;\n    }\n    return {\n      node: range.offsetNode,\n      offset: range.offset,\n    };\n  } else {\n    // Gracefully handle IE\n    return null;\n  }\n};\n\n/**\n * Select all TEXT nodes inside editor root, then find the top and bottom rects\n */\nconst getTopBottomRects = (\n  editor: LexicalEditor\n): [topRects: DOMRect[], bottomRects: DOMRect[]] => {\n  const rootElement = editor.getElementByKey($getRoot().getKey());\n  if (rootElement == null) {\n    return [[], []];\n  }\n\n  const walker = document.createTreeWalker(\n    rootElement,\n    NodeFilter.SHOW_TEXT,\n    null\n  );\n\n  const allRects: DOMRect[] = [];\n\n  while (walker.nextNode()) {\n    const range = document.createRange();\n    range.selectNodeContents(walker.currentNode);\n    const rects = range.getClientRects();\n    allRects.push(...Array.from(rects));\n  }\n\n  if (allRects.length === 0) {\n    return [[], []];\n  }\n\n  const topRect = Array.from(allRects).sort((a, b) => a.top - b.top)[0];\n\n  const bottomRect = Array.from(allRects).sort(\n    (a, b) => b.bottom - a.bottom\n  )[0];\n\n  const topRects = allRects.filter(\n    (rect) => getVerticalIntersectionRatio(rect, topRect) > 0.5\n  );\n  const bottomRects = allRects.filter(\n    (rect) => getVerticalIntersectionRatio(rect, bottomRect) > 0.5\n  );\n\n  return [topRects, bottomRects];\n};\n\nconst InitCursorPlugin = () => {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    if (!editor.isEditable()) {\n      return;\n    }\n\n    editor.update(\n      () => {\n        const textEditingInstanceSelector = $textEditingInstanceSelector.get();\n        if (textEditingInstanceSelector === undefined) {\n          return;\n        }\n\n        const { reason } = textEditingInstanceSelector;\n\n        if (reason === undefined) {\n          return;\n        }\n\n        if (reason === \"click\") {\n          const { mouseX, mouseY } = textEditingInstanceSelector;\n\n          const eventRange = caretFromPoint(mouseX, mouseY);\n\n          if (eventRange !== null) {\n            const { offset: domOffset, node: domNode } = eventRange;\n            const node = $getNearestNodeFromDOMNode(domNode);\n\n            if (node !== null) {\n              const selection = $createRangeSelection();\n              if ($isTextNode(node)) {\n                selection.anchor.set(node.getKey(), domOffset, \"text\");\n                selection.focus.set(node.getKey(), domOffset, \"text\");\n                const normalizedSelection =\n                  $normalizeSelection__EXPERIMENTAL(selection);\n\n                $setSelection(normalizedSelection);\n                return;\n              }\n            }\n\n            if (domNode instanceof Element) {\n              const rect = domNode.getBoundingClientRect();\n              if (mouseX > rect.right) {\n                const selection = $getRoot().selectEnd();\n                $setSelection(selection);\n                return;\n              }\n            }\n          }\n        }\n\n        while (reason === \"down\" || reason === \"up\") {\n          const { cursorX } = textEditingInstanceSelector;\n\n          const [topRects, bottomRects] = getTopBottomRects(editor);\n\n          // Smoodge the cursor a little to the left and right to find the nearest text node\n          const smoodgeOffsets = [1, 2, 4];\n          const maxOffset = Math.max(...smoodgeOffsets);\n\n          const rects = reason === \"down\" ? topRects : bottomRects;\n\n          rects.sort((a, b) => a.left - b.left);\n\n          const rectWithText = rects.find(\n            (rect, index) =>\n              rect.left - (index === 0 ? maxOffset : 0) <= cursorX &&\n              cursorX <=\n                rect.right + (index === rects.length - 1 ? maxOffset : 0)\n          );\n\n          if (rectWithText === undefined) {\n            break;\n          }\n\n          const newCursorY = rectWithText.top + rectWithText.height / 2;\n\n          const eventRanges = [caretFromPoint(cursorX, newCursorY)];\n          for (const offset of smoodgeOffsets) {\n            eventRanges.push(caretFromPoint(cursorX - offset, newCursorY));\n            eventRanges.push(caretFromPoint(cursorX + offset, newCursorY));\n          }\n\n          for (const eventRange of eventRanges) {\n            if (eventRange === null) {\n              continue;\n            }\n\n            const { offset: domOffset, node: domNode } = eventRange;\n            const node = $getNearestNodeFromDOMNode(domNode);\n\n            if (node !== null && $isTextNode(node)) {\n              const selection = $createRangeSelection();\n              selection.anchor.set(node.getKey(), domOffset, \"text\");\n              selection.focus.set(node.getKey(), domOffset, \"text\");\n              const normalizedSelection =\n                $normalizeSelection__EXPERIMENTAL(selection);\n              $setSelection(normalizedSelection);\n\n              return;\n            }\n          }\n\n          break;\n        }\n\n        if (\n          reason === \"down\" ||\n          reason === \"right\" ||\n          reason === \"enter\" ||\n          reason === \"click\"\n        ) {\n          const firstNode = $getRoot().getFirstDescendant();\n\n          if (firstNode === null) {\n            return;\n          }\n\n          if ($isTextNode(firstNode)) {\n            const selection = $createRangeSelection();\n            selection.anchor.set(firstNode.getKey(), 0, \"text\");\n            selection.focus.set(firstNode.getKey(), 0, \"text\");\n            $setSelection(selection);\n          }\n\n          if ($isElementNode(firstNode)) {\n            // e.g. Box is empty\n            const selection = $createRangeSelection();\n            selection.anchor.set(firstNode.getKey(), 0, \"element\");\n            selection.focus.set(firstNode.getKey(), 0, \"element\");\n            $setSelection(selection);\n          }\n\n          if ($isLineBreakNode(firstNode)) {\n            // e.g. Box contains 2+ empty lines\n            const selection = $createRangeSelection();\n            $setSelection(selection);\n          }\n\n          return;\n        }\n\n        if (reason === \"up\" || reason === \"left\") {\n          const selection = $createRangeSelection();\n          const lastNode = $getRoot().getLastDescendant();\n\n          if (lastNode === null) {\n            return;\n          }\n\n          if ($isTextNode(lastNode)) {\n            const contentSize = lastNode.getTextContentSize();\n            selection.anchor.set(lastNode.getKey(), contentSize, \"text\");\n            selection.focus.set(lastNode.getKey(), contentSize, \"text\");\n            $setSelection(selection);\n          }\n\n          if ($isElementNode(lastNode)) {\n            // e.g. Box is empty\n            const selection = $createRangeSelection();\n            selection.anchor.set(lastNode.getKey(), 0, \"element\");\n            selection.focus.set(lastNode.getKey(), 0, \"element\");\n            $setSelection(selection);\n          }\n\n          if ($isLineBreakNode(lastNode)) {\n            // e.g. Box contains 2+ empty lines\n            const parent = lastNode.getParent();\n            if ($isElementNode(parent)) {\n              const selection = $createRangeSelection();\n              selection.anchor.set(\n                parent.getKey(),\n                parent.getChildrenSize(),\n                \"element\"\n              );\n              selection.focus.set(\n                parent.getKey(),\n                parent.getChildrenSize(),\n                \"element\"\n              );\n              $setSelection(selection);\n            }\n          }\n\n          return;\n        }\n        if (reason === \"new\") {\n          $selectAll();\n          return;\n        }\n\n        reason satisfies never;\n      },\n      {\n        // We are controlling scroll ourself in instance-selected.ts see updateScroll.\n        // Without skipping we are getting side effects of composition in scrollBy, scrollIntoView calls\n        tag: \"skip-scroll-into-view\",\n      }\n    );\n  }, [editor]);\n\n  return null;\n};\n\ntype HandleNextParams =\n  | {\n      reason: \"up\" | \"down\";\n      cursorX: number;\n    }\n  | {\n      reason: \"right\" | \"left\";\n    };\n\ntype SwitchBlockPluginProps = {\n  onNext: (editorState: EditorState, params: HandleNextParams) => void;\n};\n\nconst isSingleCursorSelection = () => {\n  const selection = $getSelection();\n\n  if (!$isRangeSelection(selection)) {\n    return false;\n  }\n  const isCaret =\n    selection.anchor.offset === selection.focus.offset &&\n    selection.anchor.key === selection.focus.key;\n\n  if (!isCaret) {\n    return false;\n  }\n\n  return true;\n};\n\nconst SwitchBlockPlugin = ({ onNext }: SwitchBlockPluginProps) => {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    // The right arrow key should move the cursor to the next block only if it is at the end of the current block.\n    return editor.registerCommand(\n      KEY_ARROW_RIGHT_COMMAND,\n      (event) => {\n        if (!isSingleCursorSelection()) {\n          return false;\n        }\n\n        const isLast = isSelectionLastNode();\n\n        if (isLast) {\n          const state = editor.getEditorState();\n          onNext(state, { reason: \"right\" });\n          event?.preventDefault();\n          return true;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_LOW\n    );\n  }, [editor, onNext]);\n\n  useEffect(() => {\n    // The left arrow key should move the cursor to the previous block only if it is at the start of the current block.\n    return editor.registerCommand(\n      KEY_ARROW_LEFT_COMMAND,\n      (event) => {\n        if (!isSingleCursorSelection()) {\n          return false;\n        }\n\n        const isFirst = isSelectionFirstNode();\n\n        if (isFirst) {\n          const state = editor.getEditorState();\n          onNext(state, { reason: \"left\" });\n          event?.preventDefault();\n          return true;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_LOW\n    );\n  }, [editor, onNext]);\n\n  useEffect(() => {\n    // The down arrow key should move the cursor to the next block if:\n    // - it is at the end of the current block\n    // - the cursor is at the last line of the current block\n    return editor.registerCommand(\n      KEY_ARROW_DOWN_COMMAND,\n      (event) => {\n        if (!isSingleCursorSelection()) {\n          return false;\n        }\n\n        const isLast = isSelectionLastNode();\n\n        const rect = getDomSelectionRect();\n\n        if (isLast) {\n          const state = editor.getEditorState();\n          onNext(state, { reason: \"down\", cursorX: rect?.x ?? 0 });\n          event?.preventDefault();\n          return true;\n        }\n\n        // Check if the cursor is inside a rectangle on the last line\n        if (rect === undefined) {\n          return false;\n        }\n\n        const rootNode = $getRoot();\n        const lastNode = rootNode.getLastDescendant();\n        if ($isLineBreakNode(lastNode)) {\n          return false;\n        }\n\n        const [, lineRects] = getTopBottomRects(editor);\n\n        const cursorY = rect.y + rect.height / 2;\n\n        if (\n          lineRects.some(\n            (lineRect) =>\n              lineRect.left <= rect.x &&\n              rect.x <= lineRect.right &&\n              lineRect.top <= cursorY &&\n              cursorY <= lineRect.bottom\n          )\n        ) {\n          const state = editor.getEditorState();\n          onNext(state, { reason: \"down\", cursorX: rect?.x ?? 0 });\n          event?.preventDefault();\n          return true;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL\n    );\n  }, [editor, onNext]);\n\n  useEffect(() => {\n    // The up arrow key should move the cursor to the previous block if:\n    // - it is at the start of the current block\n    // - the cursor is at the first line of the current block\n    return editor.registerCommand(\n      KEY_ARROW_UP_COMMAND,\n      (event) => {\n        if (!isSingleCursorSelection()) {\n          return false;\n        }\n\n        const isFirst = isSelectionFirstNode();\n        const rect = getDomSelectionRect();\n\n        if (isFirst) {\n          const state = editor.getEditorState();\n          onNext(state, { reason: \"up\", cursorX: rect?.x ?? 0 });\n          event?.preventDefault();\n          return true;\n        }\n\n        if (rect === undefined) {\n          return false;\n        }\n\n        const rootNode = $getRoot();\n        const lastNode = rootNode.getFirstDescendant();\n\n        if ($isLineBreakNode(lastNode)) {\n          return false;\n        }\n\n        const [lineRects] = getTopBottomRects(editor);\n\n        const cursorY = rect.y + rect.height / 2;\n\n        if (\n          lineRects.some(\n            (lineRect) =>\n              lineRect.left <= rect.x &&\n              rect.x <= lineRect.right &&\n              lineRect.top <= cursorY &&\n              cursorY <= lineRect.bottom\n          )\n        ) {\n          const state = editor.getEditorState();\n          onNext(state, { reason: \"up\", cursorX: rect?.x ?? 0 });\n          event?.preventDefault();\n          return true;\n        }\n\n        // Lexical has a bug where the cursor sometimes stops moving up.\n        // Slight adjustments fix this issue.\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n        selection.modify(\"move\", false, \"character\");\n        selection.modify(\"move\", true, \"character\");\n\n        return false;\n      },\n      COMMAND_PRIORITY_CRITICAL\n    );\n  }, [editor, onNext]);\n\n  return null;\n};\n\ntype ContextMenuParams = {\n  cursorRect: DOMRect;\n};\n\ntype RichTextContentPluginProps = {\n  rootInstanceSelector: InstanceSelector;\n  onOpen: (\n    editorState: EditorState,\n    params: undefined | ContextMenuParams\n  ) => void;\n  onNext: (editorState: EditorState, params: HandleNextParams) => void;\n};\n\nconst RichTextContentPlugin = (props: RichTextContentPluginProps) => {\n  const [templates] = useState(() =>\n    findTemplates(props.rootInstanceSelector, $instances.get())\n  );\n\n  if (templates === undefined) {\n    return;\n  }\n\n  if (templates.length === 0) {\n    return;\n  }\n\n  return <RichTextContentPluginInternal {...props} templates={templates} />;\n};\n\nconst getTag = (instanceId: Instance[\"id\"]) => {\n  const instances = $instances.get();\n  const metas = $registeredComponentMetas.get();\n  const instance = instances.get(instanceId);\n  if (instance === undefined) {\n    return;\n  }\n  const meta = metas.get(instance.component);\n  const tags = Object.keys(meta?.presetStyle ?? {});\n  return instance.tag ?? tags[0];\n};\n\nconst RichTextContentPluginInternal = ({\n  rootInstanceSelector,\n  onOpen,\n  templates,\n  onNext,\n}: RichTextContentPluginProps & {\n  templates: [instance: Instance, instanceSelector: InstanceSelector][];\n}) => {\n  const [editor] = useLexicalComposerContext();\n  const [preservedSelection] = useState(rootInstanceSelector);\n\n  const handleOpen = useEffectEvent(onOpen);\n\n  useEffect(() => {\n    if (!editor.isEditable()) {\n      return;\n    }\n\n    let menuState: \"closed\" | \"opening\" | \"opened\" = \"closed\";\n\n    let slashNodeKey: NodeKey | undefined = undefined;\n\n    const closeMenu = () => {\n      if (menuState === \"closed\") {\n        return;\n      }\n\n      menuState = \"closed\";\n\n      handleOpen(editor.getEditorState(), undefined);\n\n      if (slashNodeKey === undefined) {\n        return;\n      }\n\n      const node = $getNodeByKey(slashNodeKey);\n\n      if ($isTextNode(node)) {\n        node.setStyle(\"\");\n      }\n\n      const selectedInstanceSelector = $selectedInstanceSelector.get();\n\n      const isSelectionInSameComponent = selectedInstanceSelector\n        ? isDescendantOrSelf(selectedInstanceSelector, preservedSelection)\n        : false;\n\n      if (!isSelectionInSameComponent) {\n        node?.remove();\n\n        // Delete current\n        if ($getRoot().getTextContentSize() === 0) {\n          const blockChildSelector =\n            findBlockChildSelector(rootInstanceSelector);\n\n          if (blockChildSelector) {\n            updateWebstudioData((data) => {\n              deleteInstanceMutable(\n                data,\n                getInstancePath(rootInstanceSelector, data.instances)\n              );\n            });\n          }\n        }\n      }\n\n      // if selection changed, remove the slash node\n\n      const selection = $getSelection();\n\n      if (!$isRangeSelection(selection)) {\n        return;\n      }\n\n      selection.setStyle(\"\");\n    };\n\n    const unsubscibeSelectionChange = editor.registerCommand(\n      SELECTION_CHANGE_COMMAND,\n      () => {\n        if (menuState !== \"opened\") {\n          return false;\n        }\n\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          closeMenu();\n          return false;\n        }\n\n        if (selection.anchor.key !== slashNodeKey) {\n          closeMenu();\n          return false;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_LOW\n    );\n\n    const unsubscibeKeyDown = editor.registerCommand(\n      KEY_DOWN_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n\n        if (event.key === \"Backspace\" || event.key === \"Delete\") {\n          if ($getRoot().getTextContentSize() === 0) {\n            const tag = getTag(rootInstanceSelector[0]);\n            if (tag === \"li\") {\n              onNext(editor.getEditorState(), { reason: \"left\" });\n              const parentInstanceSelector = rootInstanceSelector.slice(1);\n              const parentInstance = $instances\n                .get()\n                .get(parentInstanceSelector[0]);\n              const isLastChild = parentInstance?.children.length === 1;\n              updateWebstudioData((data) => {\n                deleteInstanceMutable(\n                  data,\n                  getInstancePath(\n                    isLastChild ? parentInstanceSelector : rootInstanceSelector,\n                    data.instances\n                  )\n                );\n              });\n              event.preventDefault();\n              return true;\n            }\n\n            const blockChildSelector =\n              findBlockChildSelector(rootInstanceSelector);\n\n            if (blockChildSelector) {\n              onNext(editor.getEditorState(), { reason: \"left\" });\n\n              updateWebstudioData((data) => {\n                deleteInstanceMutable(\n                  data,\n                  getInstancePath(blockChildSelector, data.instances)\n                );\n              });\n\n              event.preventDefault();\n              return true;\n            }\n          }\n        }\n\n        if (menuState === \"closed\") {\n          if (event.key === \"Enter\" && !event.shiftKey) {\n            // Custom logic if we are editing list item\n            const tag = getTag(rootInstanceSelector[0]);\n            if (tag === \"li\" && $getRoot().getTextContentSize() > 0) {\n              // Instead of creating block component we need to add a new list item\n              insertListItemAt(rootInstanceSelector);\n              event.preventDefault();\n              return true;\n            }\n\n            // Check if it pressed on the last line, last symbol\n\n            const allowedTags = [\"p\", \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"];\n\n            for (const tag of allowedTags) {\n              const templateSelector = templates.find(\n                ([instance]) => getTag(instance.id) === tag\n              )?.[1];\n\n              if (templateSelector === undefined) {\n                continue;\n              }\n\n              /*\n              @todo Split logic idea\n              // clone root node then\n\n              // getPreviousSibling\n              const removeNextSiblings = (node: LexicalNode) => {\n                let current: LexicalNode | null = node;\n                while (current) {\n                  const next = current.getNextSibling();\n                  if (next) {\n                    next.remove();\n                    continue;\n                  }\n                  // Move up to parent and continue removing siblings\n\n                  current = current.getParent();\n\n                  if ($isRootNode(current)) {\n                    break;\n                  }\n                }\n              };\n\n              const anchorNode = selection.anchor.getNode();\n              const anchorOffset = selection.anchor.offset;\n\n              if (!$isTextNode(anchorNode)) {\n                continue;\n              }\n              anchorNode.splitText(anchorOffset);\n              removeNextSiblings(anchorNode);\n\n              */\n\n              insertTemplateAt(templateSelector, rootInstanceSelector, false);\n\n              if (tag === \"li\" && $getRoot().getTextContentSize() === 0) {\n                const parentInstanceSelector = rootInstanceSelector.slice(1);\n                const parentInstance = $instances\n                  .get()\n                  .get(parentInstanceSelector[0]);\n\n                const isLastChild = parentInstance?.children.length === 1;\n\n                // Pressing Enter within an empty list item deletes the empty item\n                updateWebstudioData((data) => {\n                  deleteInstanceMutable(\n                    data,\n                    getInstancePath(\n                      isLastChild\n                        ? parentInstanceSelector\n                        : rootInstanceSelector,\n                      data.instances\n                    )\n                  );\n                });\n              }\n\n              event.preventDefault();\n              return true;\n            }\n          }\n        }\n\n        if (menuState === \"opened\") {\n          if (event.key === \"Escape\") {\n            closeMenu();\n            event.preventDefault();\n            return true;\n          }\n\n          if (event.key === \" \") {\n            closeMenu();\n          }\n\n          if (event.key === \"/\") {\n            closeMenu();\n          }\n\n          if (event.key === \"Enter\") {\n            execTextEditorContextMenuCommand({\n              type: \"enter\",\n            });\n\n            event.preventDefault();\n            return true;\n          }\n\n          if (event.key === \"ArrowUp\") {\n            execTextEditorContextMenuCommand({\n              type: \"selectPrevious\",\n            });\n\n            event.preventDefault();\n            return true;\n          }\n\n          if (event.key === \"ArrowDown\") {\n            execTextEditorContextMenuCommand({\n              type: \"selectNext\",\n            });\n\n            event.preventDefault();\n            return true;\n          }\n        }\n\n        if (menuState === \"closed\") {\n          if (event.key !== \"/\") {\n            return false;\n          }\n\n          const slashNode = $createTextNode(\"/\");\n          slashNodeKey = slashNode.getKey();\n          menuState = \"opening\";\n\n          slashNode.setStyle(\"background-color: rgba(127, 127, 127, 0.2);\");\n          selection.setStyle(\"background-color: rgba(127, 127, 127, 0.2);\");\n          selection.insertNodes([slashNode]);\n\n          event.preventDefault();\n          return true;\n        }\n\n        return false;\n      },\n      COMMAND_PRIORITY_EDITOR\n    );\n\n    const closeMenuWithUpdate = () => {\n      if (menuState === \"closed\") {\n        return;\n      }\n\n      editor.update(() => {\n        closeMenu();\n      });\n    };\n\n    const unsubscribeUpdateListener = editor.registerUpdateListener(\n      ({ editorState }) => {\n        if (menuState === \"opened\") {\n          editorState.read(() => {\n            if (slashNodeKey === undefined) {\n              closeMenu();\n              return;\n            }\n            const node = $getNodeByKey(slashNodeKey);\n\n            if (node === null) {\n              closeMenuWithUpdate();\n              return;\n            }\n            const content = node.getTextContent();\n\n            const filter = content.slice(1);\n\n            execTextEditorContextMenuCommand({\n              type: \"filter\",\n              value: filter,\n            });\n          });\n        }\n\n        if (menuState === \"opening\") {\n          editorState.read(() => {\n            if (slashNodeKey === undefined) {\n              closeMenu();\n              return;\n            }\n\n            const slashNode = editor.getElementByKey(slashNodeKey);\n\n            if (slashNode === null) {\n              closeMenu();\n              return;\n            }\n\n            const rect = slashNode.getBoundingClientRect();\n\n            menuState = \"opened\";\n\n            handleOpen(editor.getEditorState(), {\n              cursorRect: rect,\n            });\n          });\n        }\n      }\n    );\n\n    const unsubscribeBlurListener = editor.registerRootListener(\n      (rootElement, prevRootElement) => {\n        rootElement?.addEventListener(\"blur\", closeMenuWithUpdate);\n        prevRootElement?.removeEventListener(\"blur\", closeMenuWithUpdate);\n      }\n    );\n\n    return () => {\n      unsubscibeKeyDown();\n      unsubscribeUpdateListener();\n      unsubscibeSelectionChange();\n      unsubscribeBlurListener();\n      // Safari and FF support as no blur event is triggered in some cases\n      closeMenuWithUpdate();\n    };\n  }, [editor, onNext, preservedSelection, rootInstanceSelector, templates]);\n\n  return null;\n};\n\nconst onError = (error: Error) => {\n  throw error;\n};\n\ntype TextEditorProps = {\n  rootInstanceSelector: InstanceSelector;\n  instances: Instances;\n  props: Props;\n  contentEditable: JSX.Element;\n  editable?: boolean;\n  onChange: (instancesList: Instance[]) => void;\n  onSelectInstance: (instanceId: Instance[\"id\"]) => void;\n};\n\nconst mod = (n: number, m: number) => {\n  return ((n % m) + m) % m;\n};\n\nconst InitialJSONStatePlugin = ({\n  onInitialState,\n}: {\n  onInitialState: (json: SerializedEditorState) => void;\n}) => {\n  const [editor] = useLexicalComposerContext();\n  const handleInitialState = useEffectEvent(onInitialState);\n\n  useEffect(() => {\n    handleInitialState(editor.getEditorState().toJSON());\n  }, [editor]);\n\n  return null;\n};\n\n/**\n * Removes link nodes and converts them to text nodes inside <a> elements.\n * Solves the issue with pasting from external sources that contain links.\n */\nconst LinkSanitizePlugin = (): null => {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    const rootElement = editor.getRootElement();\n    if (rootElement === null) {\n      return;\n    }\n\n    if (!(rootElement instanceof HTMLAnchorElement)) {\n      return;\n    }\n\n    return editor.registerNodeTransform(LinkNode, (linkNode) => {\n      linkNode.insertBefore($createTextNode(linkNode.getTextContent()));\n      linkNode.remove();\n    });\n  }, [editor]);\n\n  return null;\n};\n\nconst AnyKeyDownPlugin = ({\n  onKeyDown,\n}: {\n  onKeyDown: (event: KeyboardEvent) => void;\n}) => {\n  const [editor] = useLexicalComposerContext();\n\n  useEffect(() => {\n    return editor.registerCommand(\n      KEY_DOWN_COMMAND,\n      (event) => {\n        const selection = $getSelection();\n        if (!$isRangeSelection(selection)) {\n          return false;\n        }\n\n        onKeyDown(event);\n        return false;\n      },\n      COMMAND_PRIORITY_NORMAL\n    );\n  }, [editor, onKeyDown]);\n\n  return null;\n};\n\nexport const TextEditor = ({\n  rootInstanceSelector: rootInstanceSelectorUnstable,\n  instances,\n  props,\n  contentEditable,\n  editable,\n  onChange,\n  onSelectInstance,\n}: TextEditorProps) => {\n  const [rootInstanceSelector] = useState(() => rootInstanceSelectorUnstable);\n  // class names must be started with letter so we add a prefix\n  const [paragraphClassName] = useState(() => `a${nanoid()}`);\n  const [italicClassName] = useState(() => `a${nanoid()}`);\n  const lastSavedStateJsonRef = useRef<SerializedEditorState | null>(null);\n  const [newLinkKeyToInstanceId] = useState(() => new Map());\n\n  const handleChange = useEffectEvent(\n    (editorState: EditorState, reason: \"blur\" | \"unmount\" | \"next\") => {\n      editorState.read(() => {\n        const treeRootInstance = instances.get(rootInstanceSelector[0]);\n        if (treeRootInstance) {\n          const jsonState = editorState.toJSON();\n          if (deepEqual(jsonState, lastSavedStateJsonRef.current)) {\n            inflateInstance(rootInstanceSelector[0], false);\n            return;\n          }\n\n          onChange(\n            $convertToUpdates(treeRootInstance, refs, newLinkKeyToInstanceId)\n          );\n          newLinkKeyToInstanceId.clear();\n          lastSavedStateJsonRef.current = jsonState;\n        }\n\n        inflateInstance(rootInstanceSelector[0], false);\n      });\n\n      const textEditingSelector = $textEditingInstanceSelector.get()?.selector;\n      if (textEditingSelector === undefined) {\n        return;\n      }\n\n      if (reason === \"blur\") {\n        if (shallowEqual(textEditingSelector, rootInstanceSelector)) {\n          $textEditingInstanceSelector.set(undefined);\n        }\n      }\n    }\n  );\n\n  useLayoutEffect(() => {\n    const sheet = createRegularStyleSheet({ name: \"text-editor\" });\n\n    // reset paragraph styles and make it work inside <a>\n    sheet.addPlaintextRule(`\n      .${paragraphClassName} { display: inline-block; margin: 0; }\n    `);\n\n    // fixes the bug on canvas that cursor is not shown on empty elements\n    sheet.addPlaintextRule(`\n      .${paragraphClassName}:has(br):not(:has(:not(br))) { min-width: 1px; }\n    `);\n\n    /// set italic style for bold italic combination on the same element\n    sheet.addPlaintextRule(`\n      .${italicClassName} { font-style: italic; }\n    `);\n    sheet.render();\n    return () => {\n      sheet.unmount();\n    };\n  }, [paragraphClassName, italicClassName]);\n\n  // store references separately because lexical nodes\n  // cannot store custom data\n  // Map<nodeKey, Instance>\n  const [refs] = useState<Refs>(() => new Map());\n  const initialConfig = {\n    namespace: \"WsTextEditor\",\n    theme: {\n      paragraph: paragraphClassName,\n      text: {\n        italic: italicClassName,\n      },\n    },\n    editable,\n    editorState: () => {\n      const [rootInstanceId] = rootInstanceSelector;\n      // text editor is unmounted when change properties in side panel\n      // so assume new nodes don't need to preserve instance id\n      // and store only initial references\n      $convertToLexical(instances, rootInstanceId, refs);\n    },\n    nodes: [LinkNode],\n    onError,\n  };\n\n  const handleNext = useEffectEvent(\n    (state: EditorState, args: HandleNextParams) => {\n      const rootInstanceId = $selectedPage.get()?.rootInstanceId;\n      const metas = $registeredComponentMetas.get();\n\n      if (rootInstanceId === undefined) {\n        return;\n      }\n\n      const editableInstanceSelectors: InstanceSelector[] = [];\n      findAllEditableInstanceSelector({\n        instanceSelector: [rootInstanceId],\n        instances,\n        props,\n        metas,\n        results: editableInstanceSelectors,\n      });\n\n      const currentIndex = editableInstanceSelectors.findIndex(\n        (instanceSelector) => {\n          return (\n            instanceSelector[0] === rootInstanceSelector[0] &&\n            instanceSelector.join(\",\") === rootInstanceSelector.join(\",\")\n          );\n        }\n      );\n\n      if (currentIndex === -1) {\n        return;\n      }\n\n      for (let i = 1; i < editableInstanceSelectors.length; i++) {\n        const nextIndex =\n          args.reason === \"down\" || args.reason === \"right\"\n            ? mod(currentIndex + i, editableInstanceSelectors.length)\n            : mod(currentIndex - i, editableInstanceSelectors.length);\n\n        const nextSelector = editableInstanceSelectors[nextIndex];\n\n        const nextInstance = instances.get(nextSelector[0]);\n        if (nextInstance === undefined) {\n          continue;\n        }\n\n        const hasExpressionChildren = nextInstance.children.some(\n          (child) => child.type === \"expression\"\n        );\n\n        // opinionated: Skip if binded (double click is working)\n        if (hasExpressionChildren) {\n          continue;\n        }\n\n        // Skip invisible elements\n        if (getVisibleElementsByInstanceSelector(nextSelector).length === 0) {\n          continue;\n        }\n\n        const instance = instances.get(nextSelector[0]);\n\n        if (instance === undefined) {\n          continue;\n        }\n        const meta = metas.get(instance.component);\n        const tags = Object.keys(meta?.presetStyle ?? {});\n        const tag = instance.tag ?? tags[0];\n\n        // opinionated: Non-collapsed elements without children can act as spacers (they have size for some reason).\n        if (\n          // Components with pseudo-elements (e.g., ::marker) that prevent content from collapsing\n          richTextPlaceholders.has(tag) === false &&\n          instance?.children.length === 0\n        ) {\n          const elt = getElementByInstanceSelector(nextSelector);\n\n          if (elt === undefined) {\n            continue;\n          }\n\n          if (!elt.hasAttribute(inflatedAttribute)) {\n            continue;\n          }\n        }\n\n        handleChange(state, \"next\");\n\n        $textEditingInstanceSelector.set({\n          selector: nextSelector,\n          ...args,\n        });\n\n        selectInstance(nextSelector);\n\n        break;\n      }\n    }\n  );\n\n  const handleAnyKeydown = useCallback((event: KeyboardEvent) => {\n    // Skip alt as Block outline depends on Alt key press\n    if (event.key === \"Alt\") {\n      return;\n    }\n\n    $blockChildOutline.set(undefined);\n    $hoveredInstanceOutline.set(undefined);\n    $hoveredInstanceSelector.set(undefined);\n  }, []);\n\n  const registerNewLink = useCallback(\n    (key: NodeKey, instanceId: string) => {\n      newLinkKeyToInstanceId.set(key, instanceId);\n      addTemporaryInstance({\n        id: instanceId,\n        component: \"RichTextLink\",\n        type: \"instance\",\n        children: [],\n      });\n    },\n    [newLinkKeyToInstanceId]\n  );\n\n  const handleContextMenuOpen = useCallback(\n    (_editorState: EditorState, params: undefined | ContextMenuParams) => {\n      $textEditorContextMenu.set(params);\n    },\n    []\n  );\n\n  return (\n    <LexicalComposer initialConfig={initialConfig}>\n      <RemoveParagaphsPlugin />\n      <CaretColorPlugin />\n      <ToolbarConnectorPlugin\n        onSelectNode={(nodeKey) => {\n          const instanceId = refs.get(`${nodeKey}:span`);\n          if (instanceId !== undefined) {\n            onSelectInstance(instanceId);\n          }\n        }}\n      />\n      <BindInstanceToNodePlugin\n        refs={refs}\n        rootInstanceSelector={rootInstanceSelector}\n      />\n      <RichTextPlugin\n        ErrorBoundary={LexicalErrorBoundary}\n        contentEditable={contentEditable}\n      />\n      <LinkPlugin />\n\n      <LinkSanitizePlugin />\n      <HistoryPlugin />\n\n      <SwitchBlockPlugin onNext={handleNext} />\n      <RichTextContentPlugin\n        onOpen={handleContextMenuOpen}\n        rootInstanceSelector={rootInstanceSelector}\n        onNext={handleNext}\n      />\n      <OnChangeOnBlurPlugin onChange={handleChange} />\n      <InitCursorPlugin />\n      <LinkSelectionPlugin\n        rootInstanceSelector={rootInstanceSelector}\n        registerNewLink={registerNewLink}\n      />\n      <AnyKeyDownPlugin onKeyDown={handleAnyKeydown} />\n      <InitialJSONStatePlugin\n        onInitialState={(json) => {\n          lastSavedStateJsonRef.current = json;\n        }}\n      />\n    </LexicalComposer>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/features/text-editor/toolbar-connector.tsx",
    "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  type RangeSelection,\n  type TextNode,\n  type LexicalEditor,\n  $getSelection,\n  $isRangeSelection,\n  $isTextNode,\n  SELECTION_CHANGE_COMMAND,\n  COMMAND_PRIORITY_LOW,\n  type TextFormatType,\n  createCommand,\n  COMMAND_PRIORITY_EDITOR,\n} from \"lexical\";\nimport { $getNearestNodeOfType } from \"@lexical/utils\";\nimport { $patchStyleText } from \"@lexical/selection\";\nimport { LinkNode } from \"@lexical/link\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport { $textToolbar } from \"~/shared/nano-states\";\nimport { subscribeScrollState } from \"~/canvas/shared/scroll-state\";\n\nlet activeEditor: undefined | LexicalEditor;\n\nexport const getActiveEditor = () => {\n  return activeEditor;\n};\n\nexport const TOGGLE_SPAN_COMMAND = createCommand<void>();\nexport const CLEAR_FORMAT_COMMAND = createCommand<void>();\n\nconst spanTriggerName = \"--style-node-trigger\";\n\nexport const $isSpanNode = (node: TextNode) => {\n  return node.getStyle().includes(spanTriggerName);\n};\n\nexport const $setNodeSpan = (node: TextNode) => {\n  return node.setStyle(`${spanTriggerName}: 1;`);\n};\n\nconst $getSpanNodes = (selection: RangeSelection) => {\n  const nodes = selection.getNodes();\n  const spans: TextNode[] = [];\n  // check each TextNode within selection for existing span nodes\n  for (const node of nodes) {\n    if ($isTextNode(node) && $isSpanNode(node)) {\n      spans.push(node);\n    }\n  }\n  return spans;\n};\n\nconst $toggleSpan = () => {\n  const selection = $getSelection();\n  if ($isRangeSelection(selection)) {\n    const spans = $getSpanNodes(selection);\n    if (spans.length === 0) {\n      // lexical creates separate text node when style property do not match\n      $patchStyleText(selection, {\n        [spanTriggerName]: \"1\",\n      });\n    } else {\n      // clear span nodes style\n      for (const node of spans) {\n        node.setStyle(\"\");\n      }\n    }\n  }\n};\n\nconst $clearText = () => {\n  const selection = $getSelection();\n  if ($isRangeSelection(selection)) {\n    // split nodes by selection and mark with style\n    $patchStyleText(selection, {\n      \"--clear-selection-trigger\": \"1\",\n    });\n    // recompute selection to get new splitted nodes\n    const newSelection = $getSelection();\n    if ($isRangeSelection(newSelection)) {\n      // update both nodes and selection\n      newSelection.format = 0;\n      for (const node of selection.getNodes()) {\n        if ($isTextNode(node)) {\n          node.setFormat(0);\n          node.setStyle(\"\");\n        }\n      }\n    }\n  }\n};\n\nconst $isSelectedLink = (selection: RangeSelection) => {\n  const [selectedNode] = selection.getNodes();\n  return $getNearestNodeOfType(selectedNode, LinkNode) != null;\n};\n\nexport const hasSelectionFormat = (formatType: TextFormatType | \"link\") => {\n  return activeEditor?.getEditorState().read(() => {\n    const selection = $getSelection();\n    if ($isRangeSelection(selection)) {\n      if (formatType === \"link\") {\n        return $isSelectedLink(selection);\n      }\n      return selection.hasFormat(formatType);\n    }\n  });\n};\n\nconst getSelectionClienRect = () => {\n  const nativeSelection = window.getSelection();\n  if (nativeSelection === null) {\n    return;\n  }\n\n  if (nativeSelection.rangeCount === 0) {\n    return;\n  }\n\n  const domRange = nativeSelection.getRangeAt(0);\n  return domRange.getBoundingClientRect();\n};\n\nconst ToolbarConnectorPluginInternal = ({\n  onSelectNode,\n}: {\n  onSelectNode: (nodeKey: string) => void;\n}) => {\n  const [editor] = useLexicalComposerContext();\n\n  const isMouseDownRef = useRef(false);\n  // control toolbar state on data or selection updates\n  const updateToolbar = useCallback(() => {\n    const selection = $getSelection();\n    if (\n      $isRangeSelection(selection) &&\n      selection.getTextContent().length !== 0 &&\n      isMouseDownRef.current === false\n    ) {\n      const selectionRect = getSelectionClienRect();\n      const isBold = selection.hasFormat(\"bold\");\n      const isItalic = selection.hasFormat(\"italic\");\n      const isSuperscript = selection.hasFormat(\"superscript\");\n      const isSubscript = selection.hasFormat(\"subscript\");\n      const isLink = $isSelectedLink(selection);\n      const isSpan = $getSpanNodes(selection).length !== 0;\n      $textToolbar.set({\n        selectionRect,\n        isBold,\n        isItalic,\n        isSuperscript,\n        isSubscript,\n        isLink,\n        isSpan,\n      });\n    } else {\n      $textToolbar.set(undefined);\n    }\n  }, []);\n\n  useEffect(() => {\n    return subscribeScrollState({\n      onScrollStart: () => {\n        // hide toolbar on scroll start preserving all data\n        const textToolbar = $textToolbar.get();\n        if (textToolbar) {\n          $textToolbar.set({\n            ...textToolbar,\n            selectionRect: undefined,\n          });\n        }\n      },\n      onScrollEnd: () => {\n        // restore toolbar with new position\n        const textToolbar = $textToolbar.get();\n        if (textToolbar) {\n          $textToolbar.set({\n            ...textToolbar,\n            selectionRect: getSelectionClienRect(),\n          });\n        }\n      },\n    });\n  }, []);\n\n  // prevent showing toolbar when select with mouse\n  useEffect(() => {\n    const onMouseDown = () => {\n      isMouseDownRef.current = true;\n    };\n    const onMouseUp = () => {\n      isMouseDownRef.current = false;\n      const editorState = editor.getEditorState();\n      editorState.read(() => {\n        updateToolbar();\n      });\n    };\n    document.addEventListener(\"mousedown\", onMouseDown);\n    document.addEventListener(\"mouseup\", onMouseUp);\n    return () => {\n      document.removeEventListener(\"mousedown\", onMouseDown);\n      document.removeEventListener(\"mouseup\", onMouseUp);\n    };\n  }, [editor, updateToolbar]);\n\n  useEffect(() => {\n    // hide toolbar when editor is unmounted\n    return () => {\n      $textToolbar.set(undefined);\n    };\n  }, []);\n\n  useEffect(() => {\n    return editor.registerCommand(\n      SELECTION_CHANGE_COMMAND,\n      () => {\n        updateToolbar();\n        return false;\n      },\n      COMMAND_PRIORITY_LOW\n    );\n  }, [editor, updateToolbar]);\n\n  useEffect(() => {\n    return editor.registerUpdateListener(({ editorState }) => {\n      editorState.read(() => {\n        updateToolbar();\n      });\n    });\n  }, [editor, updateToolbar]);\n\n  useEffect(() => {\n    return editor.registerCommand(\n      TOGGLE_SPAN_COMMAND,\n      () => {\n        editor.update(\n          () => {\n            $toggleSpan();\n          },\n          {\n            onUpdate: () => {\n              const editorState = editor.getEditorState();\n              editorState.read(() => {\n                const selection = $getSelection();\n                if ($isRangeSelection(selection)) {\n                  const spans = $getSpanNodes(selection);\n                  if (spans.length !== 0) {\n                    const [node] = spans;\n                    onSelectNode(node.getKey());\n                  }\n                }\n              });\n            },\n          }\n        );\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR\n    );\n  }, [editor, onSelectNode]);\n\n  useEffect(() => {\n    return editor.registerCommand(\n      CLEAR_FORMAT_COMMAND,\n      () => {\n        editor.update(() => {\n          $clearText();\n        });\n        return true;\n      },\n      COMMAND_PRIORITY_EDITOR\n    );\n  }, [editor]);\n\n  return null;\n};\n\nexport const ToolbarConnectorPlugin = ({\n  onSelectNode,\n}: {\n  onSelectNode: (nodeKey: string) => void;\n}) => {\n  const [editor] = useLexicalComposerContext();\n  const [hasRootElement, setHasRootElement] = useState(false);\n\n  useEffect(() => {\n    const rootElement = editor.getRootElement();\n\n    /**\n     * We don't set root element for VisuallyHidden nodes\n     * and need to prevent Toolbar events in this case\n     */\n    if (rootElement === null) {\n      return;\n    }\n\n    setHasRootElement(true);\n\n    activeEditor = editor;\n  }, [editor]);\n\n  if (hasRootElement === false) {\n    return null;\n  }\n\n  return <ToolbarConnectorPluginInternal onSelectNode={onSelectNode} />;\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/features/webstudio-component/index.ts",
    "content": "export * from \"./webstudio-component\";\n"
  },
  {
    "path": "apps/builder/app/canvas/features/webstudio-component/webstudio-component.test.tsx",
    "content": "/**\n * @vitest-environment jsdom\n */\nimport { describe, test, expect } from \"vitest\";\nimport { __testing__ } from \"./webstudio-component\";\n\nconst { computeComponentKey } = __testing__;\n\ndescribe(\"computeComponentKey - key generation logic\", () => {\n  test(\"prioritizes assetId over other props\", () => {\n    expect(\n      computeComponentKey({\n        $webstudio$canvasOnly$assetId: \"asset-123\",\n        src: \"/image.jpg\",\n        defaultValue: \"default\",\n      })\n    ).toBe(\"asset-123\");\n  });\n\n  test(\"falls back to defaultValue when no assetId\", () => {\n    expect(\n      computeComponentKey({\n        src: \"/image.jpg\",\n        defaultValue: \"default-value\",\n      })\n    ).toBe(\"default-value\");\n  });\n\n  test(\"uses src when no assetId or defaultValue\", () => {\n    expect(\n      computeComponentKey({\n        src: \"/path/to/video.mp4\",\n      })\n    ).toBe(\"/path/to/video.mp4\");\n  });\n\n  test(\"returns undefined when no relevant props\", () => {\n    expect(computeComponentKey({})).toBeUndefined();\n  });\n\n  test(\"handles null and undefined values\", () => {\n    expect(\n      computeComponentKey({\n        src: null,\n        defaultValue: undefined,\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"coerces defaultValue to string\", () => {\n    expect(computeComponentKey({ defaultValue: 42 })).toBe(\"42\");\n    expect(computeComponentKey({ defaultValue: true })).toBe(\"true\");\n    expect(computeComponentKey({ defaultValue: 0 })).toBe(\"0\");\n  });\n\n  test(\"coerces src to string\", () => {\n    expect(computeComponentKey({ src: \"string-src\" })).toBe(\"string-src\");\n    expect(computeComponentKey({ src: 123 })).toBe(\"123\");\n    expect(computeComponentKey({ src: undefined })).toBeUndefined();\n  });\n\n  test(\"different assetIds produce different keys\", () => {\n    const key1 = computeComponentKey({\n      $webstudio$canvasOnly$assetId: \"asset-123\",\n      src: \"/image.jpg\",\n    });\n    const key2 = computeComponentKey({\n      $webstudio$canvasOnly$assetId: \"asset-456\",\n      src: \"/image.jpg\",\n    });\n\n    expect(key1).not.toBe(key2);\n  });\n\n  test(\"different src values produce different keys\", () => {\n    const key1 = computeComponentKey({ src: \"/assets/video1.mp4\" });\n    const key2 = computeComponentKey({ src: \"/assets/video2.mp4\" });\n\n    expect(key1).not.toBe(key2);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/features/webstudio-component/webstudio-component.tsx",
    "content": "import {\n  useEffect,\n  forwardRef,\n  type ForwardedRef,\n  useRef,\n  useLayoutEffect,\n  useMemo,\n  Fragment,\n  type ReactNode,\n  type JSX,\n} from \"react\";\nimport { $getSelection, $isRangeSelection } from \"lexical\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport type {\n  Instance,\n  Instances,\n  Prop,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport {\n  findTreeInstanceIds,\n  collectionComponent,\n  descendantComponent,\n  blockComponent,\n  blockTemplateComponent,\n  getIndexesWithinAncestors,\n  elementComponent,\n} from \"@webstudio-is/sdk\";\nimport { indexProperty, tagProperty } from \"@webstudio-is/sdk/runtime\";\nimport {\n  idAttribute,\n  componentAttribute,\n  showAttribute,\n  selectorIdAttribute,\n  type AnyComponent,\n  textContentAttribute,\n  standardAttributesToReactProps,\n  getCollectionEntries,\n} from \"@webstudio-is/react-sdk\";\nimport { rawTheme } from \"@webstudio-is/design-system\";\nimport { Input, Select, Textarea } from \"@webstudio-is/sdk-components-react\";\n\nconst computeComponentKey = (props: Record<string, unknown>) => {\n  const assetId = props.$webstudio$canvasOnly$assetId;\n  const src = props.src;\n  const defaultValue = props.defaultValue;\n\n  return (\n    (typeof assetId === \"string\" ? assetId : undefined) ??\n    (defaultValue != null ? String(defaultValue) : undefined) ??\n    (src != null ? String(src) : undefined)\n  );\n};\n\nexport const __testing__ = { computeComponentKey };\n\nimport {\n  $propValuesByInstanceSelectorWithMemoryProps,\n  getIndexedInstanceId,\n  $instances,\n  $registeredComponentMetas,\n  $selectedInstanceRenderState,\n  findBlockSelector,\n  $props,\n} from \"~/shared/nano-states\";\nimport { $textEditingInstanceSelector } from \"~/shared/nano-states\";\nimport {\n  type InstanceSelector,\n  areInstanceSelectorsEqual,\n} from \"~/shared/tree-utils\";\nimport { inflateInstance } from \"~/canvas/inflator\";\nimport { getIsVisuallyHidden } from \"~/shared/visually-hidden\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { TextEditor } from \"../text-editor\";\nimport {\n  $selectedPage,\n  getInstanceKey,\n  selectInstance,\n} from \"~/shared/awareness\";\nimport {\n  createInstanceChildrenElements,\n  type WebstudioComponentProps,\n} from \"~/canvas/elements\";\nimport { Block } from \"../build-mode/block\";\nimport { BlockTemplate } from \"../build-mode/block-template\";\nimport {\n  editablePlaceholderAttribute,\n  editingPlaceholderVariable,\n} from \"~/canvas/shared/styles\";\nimport { richTextPlaceholders } from \"~/shared/content-model\";\n\nconst ContentEditable = ({\n  placeholder,\n  renderComponentWithRef,\n}: {\n  placeholder: string | undefined;\n  renderComponentWithRef: (\n    elementRef: ForwardedRef<HTMLElement>\n  ) => JSX.Element;\n}) => {\n  const [editor] = useLexicalComposerContext();\n\n  const ref = useRef<HTMLElement>(null);\n\n  /**\n   * useLayoutEffect to be sure that editor plugins on useEffect would have access to rootElement\n   */\n  useLayoutEffect(() => {\n    const rootElement = ref.current;\n\n    if (rootElement == null) {\n      return;\n    }\n\n    if (getIsVisuallyHidden(rootElement)) {\n      return;\n    }\n\n    if (rootElement.tagName === \"A\") {\n      if (window.getComputedStyle(rootElement).display === \"inline-flex\") {\n        // Issue: <a> tag doesn't work with inline-flex when the cursor is at the start or end of the text.\n        // Solution: Inline-flex is not supported by Lexical. Use \"inline\" during editing.\n        rootElement.style.display = \"inline\";\n      }\n    }\n\n    // Issue: <button> with contentEditable does not allow pressing space.\n    // Solution: Add space on space keydown.\n    const abortController = new AbortController();\n    if (rootElement.closest(\"button\")) {\n      rootElement.addEventListener(\n        \"keydown\",\n        (event) => {\n          if (event.code === \"Space\") {\n            editor.update(() => {\n              const selection = $getSelection();\n\n              if ($isRangeSelection(selection)) {\n                selection.insertText(\" \");\n              }\n            });\n\n            event.preventDefault();\n          }\n        },\n        { signal: abortController.signal }\n      );\n\n      // Some controls like Tab and TabTrigger intercept arrow keys for navigation.\n      // Prevent propagation to avoid conflicts with Lexical's default behavior.\n      rootElement.addEventListener(\n        \"keydown\",\n        (event) => {\n          if ([\"ArrowLeft\", \"ArrowRight\"].includes(event.code)) {\n            event.stopPropagation();\n          }\n        },\n        { signal: abortController.signal }\n      );\n    }\n\n    rootElement.contentEditable = \"true\";\n\n    editor.setRootElement(rootElement);\n\n    // Must be done after 'setRootElement' to avoid Lexical's default behavior\n    // white-space affects \"text-wrap\", remove it and use \"white-space-collapse\" instead\n    rootElement.style.removeProperty(\"white-space\");\n    rootElement.style.setProperty(\"white-space-collapse\", \"pre-wrap\");\n\n    if (placeholder !== undefined) {\n      rootElement.style.setProperty(\n        editingPlaceholderVariable,\n        `'${placeholder.replaceAll(\"'\", \"\\\\'\")}'`\n      );\n    }\n\n    return () => {\n      abortController.abort();\n    };\n  }, [editor, placeholder]);\n\n  return renderComponentWithRef(ref);\n};\n\nconst ErrorStub = forwardRef<\n  HTMLDivElement,\n  {\n    children?: ReactNode;\n  }\n>((props, ref) => {\n  return (\n    <div\n      {...props}\n      ref={ref}\n      style={{\n        padding: rawTheme.spacing[5],\n        border: `1px solid ${rawTheme.colors.borderDestructiveMain}`,\n        color: rawTheme.colors.foregroundDestructive,\n      }}\n    />\n  );\n});\nErrorStub.displayName = \"ErrorStub\";\n\nconst MissingComponentStub = forwardRef<\n  HTMLDivElement,\n  { children?: ReactNode }\n>((props, ref) => {\n  return (\n    <ErrorStub ref={ref} {...props}>\n      Component {props[componentAttribute as never]} does not exist\n    </ErrorStub>\n  );\n});\nMissingComponentStub.displayName = \"MissingComponentStub\";\n\nconst InvalidCollectionDataStub = forwardRef<\n  HTMLDivElement,\n  { children?: ReactNode }\n>((props, ref) => {\n  return (\n    <ErrorStub ref={ref} {...props}>\n      The Collection component requires an array in the data property. When\n      binding external data, it is likely that the array is nested somewhere\n      within, and you need to provide the correct path in the binding.{\" \"}\n      <a\n        style={{ color: \"inherit\" }}\n        target=\"_blank\"\n        href=\"https://docs.webstudio.is/university/core-components/collection#whats-an-array\"\n        // avoid preventing click by events interceptor\n        onClickCapture={(event) => event.stopPropagation()}\n      >\n        Learn more\n      </a>\n    </ErrorStub>\n  );\n});\nInvalidCollectionDataStub.displayName = \"InvalidCollectionDataStub\";\n\nconst DroppableComponentStub = forwardRef<\n  HTMLDivElement,\n  { children?: ReactNode }\n>((props, ref) => {\n  return (\n    <div {...props} ref={ref} style={{ display: \"block\" }}>\n      {/* explicitly specify undefined to override passed children */}\n      {undefined}\n    </div>\n  );\n});\nDroppableComponentStub.displayName = \"DroppableComponentStub\";\n\n// this utility is temporary solution to compute instance selectors\n// for rich text subtree which cannot have slots so its safe to traverse ancestors\n// until editor instance is reached\n//\n// once all lexical formats are replaced with elmenents it should be\n// straightforward to compute selectors from lexical tree\nconst getInstanceSelector = (\n  instances: Instances,\n  rootInstanceSelector: InstanceSelector,\n  instanceId: Instance[\"id\"]\n) => {\n  const parentInstancesById = new Map<Instance[\"id\"], Instance[\"id\"]>();\n  for (const instance of instances.values()) {\n    for (const child of instance.children) {\n      if (child.type === \"id\") {\n        parentInstancesById.set(child.value, instance.id);\n      }\n    }\n  }\n  const selector: InstanceSelector = [];\n  let currentInstanceId: undefined | Instance[\"id\"] = instanceId;\n  while (currentInstanceId) {\n    selector.push(currentInstanceId);\n    currentInstanceId = parentInstancesById.get(currentInstanceId);\n    if (currentInstanceId === rootInstanceSelector[0]) {\n      return [...selector, ...rootInstanceSelector];\n    }\n  }\n  return;\n};\n\nconst $indexesWithinAncestors = computed(\n  [$registeredComponentMetas, $instances, $selectedPage],\n  (metas, instances, page) => {\n    return getIndexesWithinAncestors(\n      metas,\n      instances,\n      page ? [page.rootInstanceId] : []\n    );\n  }\n);\n\nconst useInstanceProps = (instanceSelector: InstanceSelector) => {\n  const instanceKey = getInstanceKey(instanceSelector);\n  const [instanceId] = instanceSelector;\n  const $instancePropsObject = useMemo(() => {\n    return computed(\n      [\n        $propValuesByInstanceSelectorWithMemoryProps,\n        $instances,\n        $indexesWithinAncestors,\n        $registeredComponentMetas,\n      ],\n      (\n        propValuesByInstanceSelector,\n        instances,\n        indexesWithinAncestors,\n        metas\n      ) => {\n        const instancePropsObject: Record<Prop[\"name\"], unknown> = {};\n        const instance = instances.get(instanceId);\n        const tag = instance?.tag;\n        if (tag !== undefined) {\n          instancePropsObject[tagProperty] = tag;\n        }\n        const meta = metas.get(instance?.component ?? \"\");\n        const hasTags = Object.keys(meta?.presetStyle ?? {}).length > 0;\n        const index = indexesWithinAncestors.get(instanceId);\n        if (index !== undefined) {\n          instancePropsObject[indexProperty] = index.toString();\n        }\n        const instanceProps = propValuesByInstanceSelector.get(instanceKey);\n        if (instanceProps) {\n          for (const [name, value] of instanceProps) {\n            let propName = name;\n            // convert html attribute only when component has tags\n            // and does not specify own property with this name\n            if (hasTags && !meta?.props?.[propName]) {\n              propName = standardAttributesToReactProps[propName] ?? propName;\n            }\n            instancePropsObject[propName] = value;\n          }\n        }\n        return instancePropsObject;\n      }\n    );\n  }, [instanceKey, instanceId]);\n  const instancePropsObject = useStore($instancePropsObject);\n  return instancePropsObject;\n};\n\nconst existingElements = new Set<string>();\n\n/**\n * We are identifying newly created instances like Tooltips and ensuring the calculation of 'inflated' elements.\n */\nconst useInflateOnNewElement = (instanceId: Instance[\"id\"]) => {\n  useEffect(() => {\n    if (existingElements.has(instanceId) === false) {\n      inflateInstance(instanceId);\n    }\n\n    existingElements.add(instanceId);\n    return () => {\n      existingElements.delete(instanceId);\n    };\n  }, [instanceId]);\n};\n\n/**\n * We combine Radix's implicit event handlers with user-defined ones,\n * such as onClick or onSubmit. For instance, a Button within\n * a TooltipTrigger receives an onClick handler from the TooltipTrigger.\n * We might also need an additional onClick handler on the Button for other\n * purposes (setting variable).\n **/\nconst mergeProps = (\n  // here we assume all on* props are callbacks\n  // cast to avoid extra checks\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  restProps: Record<string, any>,\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  instanceProps: Record<string, any>,\n  callbackStrategy: \"merge\" | \"delete\"\n) => {\n  // merge props into single object\n  const props = { ...restProps, ...instanceProps };\n  for (const propName of Object.keys(props)) {\n    const restPropValue = restProps[propName];\n    const instancePropValue = instanceProps[propName];\n\n    const isHandler = /^on[A-Z]/.test(propName);\n    if (isHandler === false) {\n      continue;\n    }\n    // combine handlers for preview\n    if (callbackStrategy === \"merge\") {\n      props[propName] = (...args: unknown[]) => {\n        restPropValue?.(...args);\n        instancePropValue?.(...args);\n      };\n    }\n    // delete all handlers from canvas mode\n    if (callbackStrategy === \"delete\") {\n      delete props[propName];\n    }\n  }\n  return props;\n};\n\nconst getTextContent = (instanceProps: Record<string, unknown>) => {\n  const value = instanceProps[textContentAttribute];\n  // serialize objects and let react render literal types\n  if (typeof value === \"object\" && value !== null) {\n    return String(value);\n  }\n  return value as ReactNode;\n};\n\nconst getEditableComponentPlaceholder = (\n  instance: Instance,\n  instanceSelector: InstanceSelector,\n  instances: Instances,\n  metas: Map<string, WsComponentMeta>,\n  mode: \"editing\" | \"editable\"\n) => {\n  const meta = metas.get(instance.component);\n  const tags = Object.keys(meta?.presetStyle ?? {});\n  const tag = instance.tag ?? tags[0];\n  const placeholder = richTextPlaceholders.get(tag);\n  if (placeholder === undefined) {\n    return;\n  }\n  const isContentBlockChild =\n    undefined !== findBlockSelector(instanceSelector, instances);\n  // The paragraph contains only an \"editing\" placeholder within the content block.\n  if (tag === \"p\" && isContentBlockChild && mode === \"editing\") {\n    return \"Write something or press '/' for commands...\";\n  }\n  return placeholder;\n};\n\nexport const WebstudioComponentCanvas = forwardRef<\n  HTMLElement,\n  WebstudioComponentProps\n>(({ instance, instanceSelector, components, ...restProps }, ref) => {\n  const instanceId = instance.id;\n  const instances = useStore($instances);\n  const allProps = useStore($props);\n  const metas = useStore($registeredComponentMetas);\n\n  const textEditingInstanceSelector = useStore($textEditingInstanceSelector);\n\n  const { [showAttribute]: show = true, ...instanceProps } =\n    useInstanceProps(instanceSelector);\n\n  const children =\n    getTextContent(instanceProps) ??\n    createInstanceChildrenElements({\n      instances,\n      instanceSelector,\n      children: instance.children,\n      Component: WebstudioComponentCanvas,\n      components,\n    });\n  /**\n   * Prevents edited element from having a size of 0 on the first render.\n   * Directly using `children` in Text Edit\n   * conflicts with React due to lexical node changes.\n   */\n  const initialContentEditableContent = useRef(children);\n\n  useInflateOnNewElement(instanceId);\n\n  // this assumes presence of `useStore($selectedInstanceSelector)` above\n  // we rely on root re-rendering after selected instance changes\n  useEffect(() => {\n    // 1 means root\n    if (instanceSelector.length === 1) {\n      // If by the time root is rendered,\n      // no selected instance renders and sets state to \"mounted\",\n      // then it's clear that selected instance will not render at all, so we set it to \"notMounted\"\n      if ($selectedInstanceRenderState.get() === \"pending\") {\n        $selectedInstanceRenderState.set(\"notMounted\");\n      }\n    }\n  });\n\n  if (show === false) {\n    return <></>;\n  }\n\n  let Component: string | AnyComponent =\n    components.get(instance.component) ??\n    (MissingComponentStub as AnyComponent);\n\n  if (instance.component === elementComponent) {\n    Component = instance.tag ?? \"div\";\n    // replace to enable uncontrolled state\n    if (Component === \"input\") {\n      Component = Input as AnyComponent;\n    }\n    if (Component === \"textarea\") {\n      Component = Textarea as AnyComponent;\n    }\n    if (Component === \"select\") {\n      Component = Select as AnyComponent;\n    }\n  }\n\n  if (instance.component === collectionComponent) {\n    const originalData = instanceProps.data;\n    if (originalData && instance.children.length > 0) {\n      const entries = getCollectionEntries(originalData);\n      if (entries.length > 0) {\n        return entries.map(([key]) => (\n          <Fragment key={key}>\n            {createInstanceChildrenElements({\n              instances,\n              instanceSelector: [\n                getIndexedInstanceId(instance.id, key),\n                ...instanceSelector,\n              ],\n              children: instance.children,\n              Component: WebstudioComponentCanvas,\n              components,\n            })}\n          </Fragment>\n        ));\n      }\n    }\n    Component = DroppableComponentStub as AnyComponent;\n  }\n\n  if (instance.component === descendantComponent) {\n    return <></>;\n  }\n\n  if (instance.component === blockComponent) {\n    Component = Block;\n  }\n\n  if (instance.component === blockTemplateComponent) {\n    Component = BlockTemplate;\n  }\n\n  const mergedProps = mergeProps(restProps, instanceProps, \"delete\");\n\n  const props: {\n    [componentAttribute]: string;\n    [idAttribute]: string;\n    [selectorIdAttribute]: string;\n  } & Record<string, unknown> = {\n    ...mergedProps,\n    // current props should override bypassed from parent\n    // important for data-ws-* props\n    tabIndex: 0,\n    [selectorIdAttribute]: instanceSelector.join(\",\"),\n    [componentAttribute]: instance.component,\n    [idAttribute]: instance.id,\n    [editablePlaceholderAttribute]: getEditableComponentPlaceholder(\n      instance,\n      instanceSelector,\n      instances,\n      metas,\n      \"editable\"\n    ),\n  };\n\n  // React ignores defaultValue changes after first render.\n  // Key prop forces re-creation to reflect updates on canvas.\n  // Also use assetId to recreate component when asset changes (e.g., deleted, replaced)\n  // For expressions that resolve to asset URLs (via assets resource), use the src value itself\n  const key = computeComponentKey(props);\n\n  const instanceElement = (\n    <>\n      <Component key={key} {...props} ref={ref}>\n        {children}\n      </Component>\n    </>\n  );\n\n  if (\n    areInstanceSelectorsEqual(\n      textEditingInstanceSelector?.selector,\n      instanceSelector\n    ) === false\n  ) {\n    initialContentEditableContent.current = children;\n    return instanceElement;\n  }\n\n  return (\n    <TextEditor\n      rootInstanceSelector={instanceSelector}\n      instances={instances}\n      props={allProps}\n      contentEditable={\n        <ContentEditable\n          placeholder={getEditableComponentPlaceholder(\n            instance,\n            instanceSelector,\n            instances,\n            metas,\n            \"editing\"\n          )}\n          renderComponentWithRef={(elementRef) => (\n            <Component {...props} ref={mergeRefs(ref, elementRef)}>\n              {initialContentEditableContent.current}\n            </Component>\n          )}\n        />\n      }\n      onChange={(instancesList) => {\n        serverSyncStore.createTransaction([$instances], (instances) => {\n          const deletedTreeIds = findTreeInstanceIds(instances, instance.id);\n          for (const updatedInstance of instancesList) {\n            instances.set(updatedInstance.id, updatedInstance);\n            // exclude reused instances\n            deletedTreeIds.delete(updatedInstance.id);\n          }\n          for (const instanceId of deletedTreeIds) {\n            instances.delete(instanceId);\n          }\n        });\n      }}\n      onSelectInstance={(instanceId) => {\n        const instances = $instances.get();\n        const newSelectedSelector = getInstanceSelector(\n          instances,\n          instanceSelector,\n          instanceId\n        );\n        $textEditingInstanceSelector.set(undefined);\n        selectInstance(newSelectedSelector);\n      }}\n    />\n  );\n});\n\nexport const WebstudioComponentPreview = forwardRef<\n  HTMLElement,\n  WebstudioComponentProps\n>(({ instance, instanceSelector, components, ...restProps }, ref) => {\n  const instances = useStore($instances);\n  const { [showAttribute]: show = true, ...instanceProps } =\n    useInstanceProps(instanceSelector);\n  const props = {\n    ...mergeProps(restProps, instanceProps, \"merge\"),\n    [idAttribute]: instance.id,\n    [componentAttribute]: instance.component,\n    [selectorIdAttribute]: instanceSelector.join(\",\"),\n  };\n  if (show === false) {\n    return <></>;\n  }\n\n  if (instance.component === collectionComponent) {\n    const originalData = instanceProps.data;\n    if (originalData && instance.children.length > 0) {\n      const entries = getCollectionEntries(originalData);\n      if (entries.length > 0) {\n        return entries.map(([key]) => (\n          <Fragment key={key}>\n            {createInstanceChildrenElements({\n              instances,\n              instanceSelector: [\n                getIndexedInstanceId(instance.id, key),\n                ...instanceSelector,\n              ],\n              children: instance.children,\n              Component: WebstudioComponentPreview,\n              components,\n            })}\n          </Fragment>\n        ));\n      }\n    }\n  }\n\n  if (instance.component === descendantComponent) {\n    return <></>;\n  }\n\n  let Component: undefined | string | AnyComponent = components.get(\n    instance.component\n  );\n\n  if (instance.component === elementComponent) {\n    Component = instance.tag ?? \"div\";\n    // replace to enable uncontrolled state\n    if (Component === \"input\") {\n      Component = Input as AnyComponent;\n    }\n    if (Component === \"textarea\") {\n      Component = Textarea as AnyComponent;\n    }\n    if (Component === \"select\") {\n      Component = Select as AnyComponent;\n    }\n  }\n\n  if (instance.component === blockComponent) {\n    Component = Block;\n  }\n\n  if (instance.component === blockTemplateComponent) {\n    Component = BlockTemplate;\n  }\n\n  if (Component === undefined) {\n    return <></>;\n  }\n  return (\n    <Component {...props} ref={ref}>\n      {getTextContent(instanceProps) ??\n        createInstanceChildrenElements({\n          instances,\n          instanceSelector,\n          children: instance.children,\n          Component: WebstudioComponentPreview,\n          components,\n        })}\n    </Component>\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/grid-guide-utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { __testing__ } from \"./grid-guide-utils\";\n\nconst { findImplicitStart } = __testing__;\n\ndescribe(\"findImplicitStart\", () => {\n  test(\"returns trackCount when all tracks are explicit\", () => {\n    // All tracks are regular sizes, no 1.25px\n    expect(findImplicitStart(\"100px 200px 300px\", 3)).toBe(3);\n  });\n\n  test(\"detects implicit tracks at the end\", () => {\n    // Two explicit tracks followed by one implicit (1.25px)\n    expect(findImplicitStart(\"100px 200px 1.25px\", 3)).toBe(2);\n  });\n\n  test(\"detects implicit tracks at the start\", () => {\n    // Negative line numbers can push implicit tracks before explicit ones\n    expect(findImplicitStart(\"1.25px 100px 200px\", 3)).toBe(0);\n  });\n\n  test(\"detects multiple implicit tracks\", () => {\n    expect(findImplicitStart(\"100px 1.25px 1.25px\", 3)).toBe(1);\n  });\n\n  test(\"returns trackCount for empty template\", () => {\n    expect(findImplicitStart(\"none\", 0)).toBe(0);\n  });\n\n  test(\"returns trackCount for single explicit track\", () => {\n    expect(findImplicitStart(\"100px\", 1)).toBe(1);\n  });\n\n  test(\"handles fractional explicit sizes near probe value\", () => {\n    // 1.5px should not trigger implicit detection\n    expect(findImplicitStart(\"1.5px 100px\", 2)).toBe(2);\n  });\n\n  test(\"handles exact probe value\", () => {\n    expect(findImplicitStart(\"1.25px\", 1)).toBe(0);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/grid-guide-utils.ts",
    "content": "import {\n  $instances,\n  $isResizingCanvas,\n  $stylesIndex,\n  $propValuesByInstanceSelectorWithMemoryProps,\n} from \"~/shared/nano-states\";\nimport { $gridCellData, type GridCellData } from \"~/shared/nano-states\";\nimport { subscribeScrollState } from \"~/canvas/shared/scroll-state\";\nimport { subscribeWindowResize } from \"~/shared/dom-hooks\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\nimport { $awareness } from \"~/shared/awareness\";\nimport { getElementByInstanceSelector } from \"~/shared/dom-utils\";\nimport { parseGridTemplateTrackList } from \"@webstudio-is/css-data\";\nimport { doNotTrackMutation } from \"~/shared/dom-utils\";\n\nconst hideGridGuides = () => {\n  $gridCellData.set(undefined);\n};\n\nconst MAX_TRACKS = 20;\n\n// Read resolved CSS strings from getComputedStyle. No DOM probing needed —\n// the builder grid guides mirror the grid via a child div that faithfully\n// reproduces the canvas element's CSS (including user transforms).\n// A parent wrapper handles our scale + translate positioning.\n// Inflation is handled at the track level (minmax in inflator.ts), so\n// resolved templates are always non-zero for inflated grids.\n\n// CSS properties synced to the builder overlay mirror div.\n// To support a new property, add it here — no type or builder changes needed.\nconst mirrorProperties = [\n  // Grid layout\n  \"display\",\n  \"grid-template-columns\",\n  \"grid-template-rows\",\n  \"grid-template-areas\",\n  \"column-gap\",\n  \"row-gap\",\n  \"justify-content\",\n  \"justify-items\",\n  \"align-content\",\n  \"align-items\",\n  // Box model\n  \"width\",\n  \"height\",\n  \"box-sizing\",\n  \"padding-top\",\n  \"padding-right\",\n  \"padding-bottom\",\n  \"padding-left\",\n  \"border-top-width\",\n  \"border-right-width\",\n  \"border-bottom-width\",\n  \"border-left-width\",\n  \"border-top-style\",\n  \"border-right-style\",\n  \"border-bottom-style\",\n  \"border-left-style\",\n  // Direction\n  \"direction\",\n  // Transforms\n  \"transform\",\n  \"transform-origin\",\n  \"scale\",\n  \"rotate\",\n  \"translate\",\n];\n\n// Sentinel value for the implicit track probe. Implicit (auto-generated)\n// tracks will resolve to this size while explicit tracks keep their original\n// resolved size. 1.25px is unlikely to appear in authored CSS.\nconst IMPLICIT_PROBE_SIZE = 1.25;\n\n/**\n * Find the index where implicit tracks begin for one axis.\n * Parses the resolved template track list from a clone where\n * grid-auto-columns/rows are set to IMPLICIT_PROBE_SIZE px.\n * Returns the 0-based index of the first implicit track, or\n * trackCount when all tracks are explicit.\n */\nconst findImplicitStart = (\n  resolvedTemplate: string,\n  trackCount: number\n): number => {\n  const tracks = parseGridTemplateTrackList(resolvedTemplate);\n  for (let i = 0; i < tracks.length; i++) {\n    const size = parseFloat(tracks[i].value);\n    if (Math.abs(size - IMPLICIT_PROBE_SIZE) < 0.01) {\n      return i;\n    }\n  }\n  return trackCount;\n};\n\n/**\n * Probe implicit track boundaries by creating a hidden clone of the grid\n * with grid-auto-columns/rows set to 1.25px. Implicit tracks resolve to\n * 1.25px while explicit tracks retain their authored size.\n */\nconst probeImplicitTracks = (\n  gridElement: HTMLElement,\n  columnCount: number,\n  rowCount: number\n): { implicitColumnStart: number; implicitRowStart: number } => {\n  const clone = gridElement.cloneNode(false) as HTMLElement;\n  doNotTrackMutation(clone);\n  clone.style.cssText = window.getComputedStyle(gridElement).cssText;\n  clone.style.position = \"fixed\";\n  clone.style.visibility = \"hidden\";\n  clone.style.pointerEvents = \"none\";\n  clone.style.gridAutoColumns = `${IMPLICIT_PROBE_SIZE}px`;\n  clone.style.gridAutoRows = `${IMPLICIT_PROBE_SIZE}px`;\n\n  // Copy child placeholders so the browser generates the same implicit tracks\n  for (const child of gridElement.children) {\n    const placeholder = document.createElement(\"div\");\n    const childStyle = window.getComputedStyle(child);\n    placeholder.style.gridColumnStart = childStyle.gridColumnStart;\n    placeholder.style.gridColumnEnd = childStyle.gridColumnEnd;\n    placeholder.style.gridRowStart = childStyle.gridRowStart;\n    placeholder.style.gridRowEnd = childStyle.gridRowEnd;\n    placeholder.style.order = childStyle.order;\n    doNotTrackMutation(placeholder);\n    clone.appendChild(placeholder);\n  }\n\n  document.body.appendChild(clone);\n  const probeStyle = window.getComputedStyle(clone);\n  const implicitColumnStart = findImplicitStart(\n    probeStyle.gridTemplateColumns,\n    columnCount\n  );\n  const implicitRowStart = findImplicitStart(\n    probeStyle.gridTemplateRows,\n    rowCount\n  );\n  document.body.removeChild(clone);\n\n  return { implicitColumnStart, implicitRowStart };\n};\n\nconst computeGridCells = (\n  gridElement: HTMLElement,\n  instanceId: string\n): GridCellData | undefined => {\n  const computedStyle = window.getComputedStyle(gridElement);\n  const display = computedStyle.display;\n\n  if (display !== \"grid\" && display !== \"inline-grid\") {\n    return;\n  }\n\n  const columnTracks = parseGridTemplateTrackList(\n    computedStyle.gridTemplateColumns\n  );\n  const rowTracks = parseGridTemplateTrackList(computedStyle.gridTemplateRows);\n\n  const columnCount = Math.min(Math.max(1, columnTracks.length), MAX_TRACKS);\n  const rowCount = Math.min(Math.max(1, rowTracks.length), MAX_TRACKS);\n\n  const untransformedWidth = gridElement.offsetWidth;\n  const untransformedHeight = gridElement.offsetHeight;\n\n  if (untransformedWidth < 1 || untransformedHeight < 1) {\n    return;\n  }\n\n  const bcr = gridElement.getBoundingClientRect();\n\n  // Build cssText from a whitelist of properties that affect grid layout\n  // and visual appearance. Adding a new property = one line here.\n  const parts: string[] = [];\n  for (const prop of mirrorProperties) {\n    const value = computedStyle.getPropertyValue(prop);\n    if (value) {\n      parts.push(`${prop}:${value}`);\n    }\n  }\n  // Override border-color to transparent — we need border space but not color\n  parts.push(\"border-color:transparent\");\n\n  const { implicitColumnStart, implicitRowStart } = probeImplicitTracks(\n    gridElement,\n    columnCount,\n    rowCount\n  );\n\n  return {\n    instanceId,\n    columnCount,\n    rowCount,\n    bcr: { top: bcr.top, left: bcr.left },\n    untransformedWidth,\n    untransformedHeight,\n    resolvedCssText: parts.join(\";\"),\n    implicitColumnStart,\n    implicitRowStart,\n  };\n};\n\nconst findGridContainer = (\n  instanceSelector: Readonly<InstanceSelector>\n): { element: HTMLElement; instanceId: string } | undefined => {\n  // Check each ancestor in the selector\n  for (let i = 0; i < instanceSelector.length; i++) {\n    const ancestorSelector = instanceSelector.slice(i);\n    const element = getElementByInstanceSelector(ancestorSelector);\n\n    if (element) {\n      const computedStyle = window.getComputedStyle(element);\n      const display = computedStyle.display;\n\n      if (display === \"grid\" || display === \"inline-grid\") {\n        return { element, instanceId: instanceSelector[i] };\n      }\n    }\n  }\n\n  return undefined;\n};\n\nconst subscribeGridGuides = (\n  selectedInstanceSelector: Readonly<InstanceSelector>\n) => {\n  if (selectedInstanceSelector.length === 0) {\n    hideGridGuides();\n    return;\n  }\n\n  let rafId = 0;\n\n  // Re-evaluate findGridContainer on every update so that the overlay\n  // appears as soon as an element becomes a grid (e.g. display changed\n  // from block → grid) without requiring a re-selection.\n  const updateGridGuides = () => {\n    if ($isResizingCanvas.get()) {\n      hideGridGuides();\n      return;\n    }\n\n    const gridInfo = findGridContainer(selectedInstanceSelector);\n    if (!gridInfo) {\n      hideGridGuides();\n      return;\n    }\n\n    const cellData = computeGridCells(gridInfo.element, gridInfo.instanceId);\n    $gridCellData.set(cellData);\n  };\n\n  // Schedule an update after styles are applied to the DOM.\n  // The stylesheet renderer uses rAF, so we need rAF→rAF to ensure\n  // both the CSS is written and layout is recalculated.\n  const scheduleUpdate = () => {\n    cancelAnimationFrame(rafId);\n    rafId = requestAnimationFrame(() => {\n      rafId = requestAnimationFrame(() => {\n        updateGridGuides();\n      });\n    });\n  };\n\n  // Initial computation\n  updateGridGuides();\n\n  const unsubscribeStylesIndex = $stylesIndex.subscribe(() => {\n    scheduleUpdate();\n  });\n\n  const unsubscribeInstances = $instances.subscribe(() => {\n    scheduleUpdate();\n  });\n\n  const unsubscribePropValues =\n    $propValuesByInstanceSelectorWithMemoryProps.subscribe(() => {\n      scheduleUpdate();\n    });\n\n  const unsubscribeIsResizing = $isResizingCanvas.subscribe((isResizing) => {\n    if (isResizing) {\n      hideGridGuides();\n    } else {\n      updateGridGuides();\n    }\n  });\n\n  const unsubscribeScrollState = subscribeScrollState({\n    onScrollStart: hideGridGuides,\n    onScrollEnd: updateGridGuides,\n  });\n\n  const unsubscribeWindowResize = subscribeWindowResize({\n    onResizeStart: hideGridGuides,\n    onResizeEnd: updateGridGuides,\n  });\n\n  return () => {\n    cancelAnimationFrame(rafId);\n    hideGridGuides();\n    unsubscribeStylesIndex();\n    unsubscribeInstances();\n    unsubscribePropValues();\n    unsubscribeIsResizing();\n    unsubscribeScrollState();\n    unsubscribeWindowResize();\n  };\n};\n\nexport const subscribeGridGuidesOnSelected = () => {\n  let previousSelectedInstance: readonly string[] | undefined = undefined;\n  let unsubscribeGridGuides = () => {};\n\n  const unsubscribe = $awareness.subscribe((awareness) => {\n    const instanceSelector = awareness?.instanceSelector;\n    if (instanceSelector !== previousSelectedInstance) {\n      unsubscribeGridGuides();\n      unsubscribeGridGuides =\n        subscribeGridGuides(instanceSelector ?? []) ?? (() => {});\n      previousSelectedInstance = instanceSelector;\n    }\n  });\n\n  return () => {\n    unsubscribe();\n    unsubscribeGridGuides();\n  };\n};\n\nexport const __testing__ = {\n  findImplicitStart,\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/index.client.ts",
    "content": "export * from \"./canvas\";\n"
  },
  {
    "path": "apps/builder/app/canvas/inflator.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { Breakpoint, StyleDecl, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { __testing__ } from \"./inflator\";\n\nconst {\n  getInstanceSize,\n  getInflationState,\n  buildAncestorSelector,\n  extractInstanceIdsFromChanges,\n} = __testing__;\n\nconst px = (value: number): StyleValue => ({\n  type: \"unit\",\n  value,\n  unit: \"px\",\n});\n\nconst createStyleDecl = (overrides: Partial<StyleDecl> = {}): StyleDecl => ({\n  styleSourceId: \"ss-1\",\n  breakpointId: \"base\",\n  property: \"color\",\n  value: { type: \"keyword\", value: \"red\" },\n  ...overrides,\n});\n\ndescribe(\"getInstanceSize\", () => {\n  const baseBreakpoints = new Map<string, Breakpoint>([\n    [\"base\", { id: \"base\", label: \"Base\" }],\n  ]);\n\n  const callGetInstanceSize = (\n    overrides: Partial<Parameters<typeof getInstanceSize>[0]> = {}\n  ) =>\n    getInstanceSize({\n      instanceId: \"inst-1\",\n      tagName: \"div\",\n      metas: new Map(),\n      breakpoints: baseBreakpoints,\n      selectedBreakpointId: \"base\",\n      stylesByInstanceId: new Map(),\n      instances: new Map(),\n      ...overrides,\n    });\n\n  const noSize = { width: undefined, height: undefined };\n\n  test(\"returns undefined when no selected breakpoint\", () => {\n    expect(callGetInstanceSize({ selectedBreakpointId: undefined })).toEqual(\n      noSize\n    );\n  });\n\n  test(\"returns undefined when instance has no styles or preset\", () => {\n    expect(callGetInstanceSize()).toEqual(noSize);\n  });\n\n  test(\"reads width and height from preset styles\", () => {\n    const metas = new Map<string, WsComponentMeta>([\n      [\n        \"Box\",\n        {\n          presetStyle: {\n            div: [\n              { property: \"width\", value: px(100) },\n              { property: \"height\", value: px(50) },\n            ],\n          },\n        },\n      ],\n    ]);\n    expect(\n      callGetInstanceSize({\n        metas,\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n      })\n    ).toEqual({ width: 100, height: 50 });\n  });\n\n  test(\"ignores preset styles with a state selector\", () => {\n    const metas = new Map<string, WsComponentMeta>([\n      [\n        \"Box\",\n        {\n          presetStyle: {\n            div: [{ state: \":hover\", property: \"width\", value: px(200) }],\n          },\n        },\n      ],\n    ]);\n    expect(\n      callGetInstanceSize({\n        metas,\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n      })\n    ).toEqual(noSize);\n  });\n\n  test(\"instance styles override preset styles\", () => {\n    const metas = new Map<string, WsComponentMeta>([\n      [\n        \"Box\",\n        {\n          presetStyle: {\n            div: [{ property: \"width\", value: px(100) }],\n          },\n        },\n      ],\n    ]);\n    expect(\n      callGetInstanceSize({\n        metas,\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n        stylesByInstanceId: new Map([\n          [\"inst-1\", [createStyleDecl({ property: \"width\", value: px(250) })]],\n        ]),\n      })\n    ).toEqual({ width: 250, height: undefined });\n  });\n\n  test(\"non-unit value (auto) results in undefined\", () => {\n    expect(\n      callGetInstanceSize({\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n        stylesByInstanceId: new Map([\n          [\n            \"inst-1\",\n            [\n              createStyleDecl({\n                property: \"width\",\n                value: { type: \"keyword\", value: \"auto\" },\n              }),\n            ],\n          ],\n        ]),\n      })\n    ).toEqual(noSize);\n  });\n\n  test(\"cascades breakpoints from smallest to selected\", () => {\n    const breakpoints = new Map<string, Breakpoint>([\n      [\"base\", { id: \"base\", label: \"Base\" }],\n      [\"tablet\", { id: \"tablet\", label: \"Tablet\", maxWidth: 991 }],\n      [\"mobile\", { id: \"mobile\", label: \"Mobile\", maxWidth: 479 }],\n    ]);\n    expect(\n      callGetInstanceSize({\n        breakpoints,\n        selectedBreakpointId: \"tablet\",\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n        stylesByInstanceId: new Map([\n          [\n            \"inst-1\",\n            [\n              createStyleDecl({\n                breakpointId: \"base\",\n                property: \"width\",\n                value: px(100),\n              }),\n              createStyleDecl({\n                breakpointId: \"tablet\",\n                property: \"width\",\n                value: px(200),\n              }),\n              createStyleDecl({\n                breakpointId: \"mobile\",\n                property: \"width\",\n                value: px(300),\n              }),\n            ],\n          ],\n        ]),\n      })\n    ).toEqual({ width: 200, height: undefined });\n  });\n\n  test(\"stops cascade at selected breakpoint\", () => {\n    const breakpoints = new Map<string, Breakpoint>([\n      [\"base\", { id: \"base\", label: \"Base\" }],\n      [\"tablet\", { id: \"tablet\", label: \"Tablet\", maxWidth: 991 }],\n      [\"mobile\", { id: \"mobile\", label: \"Mobile\", maxWidth: 479 }],\n    ]);\n    expect(\n      callGetInstanceSize({\n        breakpoints,\n        selectedBreakpointId: \"base\",\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n        stylesByInstanceId: new Map([\n          [\n            \"inst-1\",\n            [\n              createStyleDecl({\n                breakpointId: \"mobile\",\n                property: \"width\",\n                value: px(300),\n              }),\n            ],\n          ],\n        ]),\n      })\n    ).toEqual(noSize);\n  });\n\n  test(\"skips preset when tagName is undefined\", () => {\n    const metas = new Map<string, WsComponentMeta>([\n      [\n        \"Box\",\n        {\n          presetStyle: {\n            div: [{ property: \"width\", value: px(100) }],\n          },\n        },\n      ],\n    ]);\n    expect(\n      callGetInstanceSize({\n        tagName: undefined,\n        metas,\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n      })\n    ).toEqual(noSize);\n  });\n\n  test(\"ignores instance styles with a state selector\", () => {\n    expect(\n      callGetInstanceSize({\n        instances: new Map([[\"inst-1\", { component: \"Box\" }]]),\n        stylesByInstanceId: new Map([\n          [\n            \"inst-1\",\n            [\n              createStyleDecl({\n                state: \":hover\",\n                property: \"width\",\n                value: px(999),\n              }),\n            ],\n          ],\n        ]),\n      })\n    ).toEqual(noSize);\n  });\n});\n\ndescribe(\"getInflationState\", () => {\n  const call = (overrides: Partial<Parameters<typeof getInflationState>[0]>) =>\n    getInflationState({\n      offsetWidth: 100,\n      offsetHeight: 100,\n      explicitWidth: undefined,\n      explicitHeight: undefined,\n      ...overrides,\n    });\n\n  test(\"no inflation when element has size\", () => {\n    expect(call({})).toBe(\"\");\n  });\n\n  test(\"inflate width only\", () => {\n    expect(call({ offsetWidth: 0 })).toBe(\"w\");\n  });\n\n  test(\"inflate height only\", () => {\n    expect(call({ offsetHeight: 0 })).toBe(\"h\");\n  });\n\n  test(\"inflate both dimensions\", () => {\n    expect(call({ offsetWidth: 0, offsetHeight: 0 })).toBe(\"wh\");\n  });\n\n  test(\"explicit width prevents inflation\", () => {\n    expect(call({ offsetWidth: 0, explicitWidth: 100 })).toBe(\"\");\n  });\n\n  test(\"explicit height prevents inflation\", () => {\n    expect(call({ offsetHeight: 0, explicitHeight: 50 })).toBe(\"\");\n  });\n\n  test(\"both explicit sizes prevent inflation\", () => {\n    expect(\n      call({\n        offsetWidth: 0,\n        offsetHeight: 0,\n        explicitWidth: 100,\n        explicitHeight: 50,\n      })\n    ).toBe(\"\");\n  });\n\n  test(\"partial explicit: inflate only height\", () => {\n    expect(call({ offsetWidth: 0, offsetHeight: 0, explicitWidth: 100 })).toBe(\n      \"h\"\n    );\n  });\n\n  test(\"explicit 0 still prevents inflation\", () => {\n    expect(\n      call({\n        offsetWidth: 0,\n        offsetHeight: 0,\n        explicitWidth: 0,\n        explicitHeight: 0,\n      })\n    ).toBe(\"\");\n  });\n});\n\ndescribe(\"buildAncestorSelector\", () => {\n  test(\"returns body when :has() is not supported\", () => {\n    expect(\n      buildAncestorSelector({\n        instanceIds: [\"id-1\", \"id-2\"],\n        hasSelectorSupport: false,\n      })\n    ).toBe(\"body\");\n  });\n\n  test(\"single instance id\", () => {\n    expect(\n      buildAncestorSelector({ instanceIds: [\"id-1\"], hasSelectorSupport: true })\n    ).toBe('[data-ws-id]:has([data-ws-id=\"id-1\"])');\n  });\n\n  test(\"multiple instance ids\", () => {\n    expect(\n      buildAncestorSelector({\n        instanceIds: [\"id-1\", \"id-2\"],\n        hasSelectorSupport: true,\n      })\n    ).toBe('[data-ws-id]:has([data-ws-id=\"id-1\"]):has([data-ws-id=\"id-2\"])');\n  });\n\n  test(\"empty array with :has() support\", () => {\n    expect(\n      buildAncestorSelector({ instanceIds: [], hasSelectorSupport: true })\n    ).toBe(\"[data-ws-id]\");\n  });\n\n  test(\"empty array without :has() support\", () => {\n    expect(\n      buildAncestorSelector({ instanceIds: [], hasSelectorSupport: false })\n    ).toBe(\"body\");\n  });\n});\n\ndescribe(\"extractInstanceIdsFromChanges\", () => {\n  const sss = (patches: Array<{ value?: { instanceId?: unknown } }>) => ({\n    namespace: \"styleSourceSelections\" as const,\n    patches,\n  });\n\n  test(\"empty changes\", () => {\n    expect(extractInstanceIdsFromChanges([])).toEqual([]);\n  });\n\n  test(\"extracts ids from styleSourceSelections\", () => {\n    expect(\n      extractInstanceIdsFromChanges([\n        sss([{ value: { instanceId: \"a\" } }, { value: { instanceId: \"b\" } }]),\n      ])\n    ).toEqual([\"a\", \"b\"]);\n  });\n\n  test(\"ignores other namespaces\", () => {\n    expect(\n      extractInstanceIdsFromChanges([\n        { namespace: \"styles\", patches: [{ value: { instanceId: \"a\" } }] },\n      ])\n    ).toEqual([]);\n  });\n\n  test(\"skips patches without instanceId or with non-string instanceId\", () => {\n    expect(\n      extractInstanceIdsFromChanges([\n        sss([\n          { value: {} },\n          { value: { instanceId: undefined } },\n          { value: { instanceId: 123 } },\n          { value: { instanceId: \"valid\" } },\n        ]),\n      ])\n    ).toEqual([\"valid\"]);\n  });\n\n  test(\"combines ids across multiple changes\", () => {\n    expect(\n      extractInstanceIdsFromChanges([\n        sss([{ value: { instanceId: \"a\" } }]),\n        { namespace: \"styles\", patches: [{ value: { instanceId: \"skip\" } }] },\n        sss([{ value: { instanceId: \"b\" } }]),\n      ])\n    ).toEqual([\"a\", \"b\"]);\n  });\n\n  test(\"handles patches without value property\", () => {\n    expect(\n      extractInstanceIdsFromChanges([\n        sss([\n          {} as { value?: { instanceId?: unknown } },\n          { value: { instanceId: \"a\" } },\n        ]),\n      ])\n    ).toEqual([\"a\"]);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/inflator.ts",
    "content": "import htmlTags, { voidHtmlTags, type HtmlTags } from \"html-tags\";\nimport { inflatedAttribute, idAttribute } from \"@webstudio-is/react-sdk\";\nimport { compareMedia, type StyleValue } from \"@webstudio-is/css-engine\";\nimport { parseGridTemplateTrackList } from \"@webstudio-is/css-data\";\nimport type { Breakpoint, StyleDecl, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport {\n  $breakpoints,\n  $instances,\n  $registeredComponentMetas,\n  $stylesIndex,\n} from \"~/shared/nano-states\";\nimport { $selectedBreakpoint } from \"~/shared/nano-states\";\nimport { $selectedPage } from \"~/shared/awareness\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { doNotTrackMutation } from \"~/shared/dom-utils\";\n\nconst isHtmlTag = (tag: string): tag is HtmlTags =>\n  htmlTags.includes(tag as HtmlTags);\n\nconst instanceIdSet = new Set<string>();\n\nlet rafHandle: number;\n\n// Padding in px added to collapsed elements to give them visible size on canvas.\n// Grid containers multiply this by their track count so each virtual cell\n// gets this amount of space.\nexport const INFLATE_PADDING = 75;\n\n// Marker attribute for grid containers with per-track minmax wrapping.\n// Separate from inflatedAttribute so dashed-outline CSS doesn't apply\n// to grids that aren't fully collapsed — only their tracks are inflated.\nconst gridInflatedAttribute = \"data-ws-grid-inflated\";\n\n// Do not add inflation paddings for replaced elements as at the moment we add them they don't have real size\n// https://developer.mozilla.org/en-US/docs/Web/CSS/Replaced_element\nconst replacedHtmlElements = [\"iframe\", \"video\", \"embed\", \"img\"];\n\n// Do not add inflation paddings for void elements\n// https://developer.mozilla.org/en-US/docs/Glossary/Void_element\nconst skipElementsSet = new Set([...voidHtmlTags, ...replacedHtmlElements]);\n\nconst isSelectorSupported = (selector: string) => {\n  try {\n    return Boolean(document.querySelector(selector));\n  } catch {\n    return false;\n  }\n};\n\n/**\n * Determine explicit width/height for an instance from preset + breakpoint styles.\n * Returns `{ width, height }` where each is a number if explicitly set via a unit value,\n * or `undefined` if not set or set to a non-unit value (e.g., `auto`).\n *\n * Pure function — accepts all data as parameters for testability.\n */\nconst getInstanceSize = ({\n  instanceId,\n  tagName,\n  metas,\n  breakpoints,\n  selectedBreakpointId,\n  stylesByInstanceId,\n  instances,\n}: {\n  instanceId: string;\n  tagName: HtmlTags | undefined;\n  metas: Map<string, WsComponentMeta>;\n  breakpoints: Map<string, Breakpoint>;\n  selectedBreakpointId: string | undefined;\n  stylesByInstanceId: Map<string, StyleDecl[]>;\n  instances: Map<string, { component: string }>;\n}): { width: number | undefined; height: number | undefined } => {\n  if (selectedBreakpointId === undefined) {\n    return { width: undefined, height: undefined };\n  }\n\n  let widthValue: undefined | StyleValue;\n  let heightValue: undefined | StyleValue;\n\n  const component = instances.get(instanceId)?.component;\n  if (component && tagName) {\n    const presetStyles = metas.get(component)?.presetStyle?.[tagName] ?? [];\n    for (const styleDecl of presetStyles) {\n      if (styleDecl.state === undefined) {\n        if (styleDecl.property === \"width\") {\n          widthValue = styleDecl.value;\n        }\n        if (styleDecl.property === \"height\") {\n          heightValue = styleDecl.value;\n        }\n      }\n    }\n  }\n\n  const sortedBreakpoints = Array.from(breakpoints.values()).sort(compareMedia);\n  const matchingBreakpoints: string[] = [];\n  for (const breakpoint of sortedBreakpoints) {\n    matchingBreakpoints.push(breakpoint.id);\n    if (breakpoint.id === selectedBreakpointId) {\n      break;\n    }\n  }\n\n  const instanceStyles = stylesByInstanceId.get(instanceId);\n  if (instanceStyles) {\n    for (const breakpointId of matchingBreakpoints) {\n      for (const styleDecl of instanceStyles) {\n        if (\n          styleDecl.breakpointId === breakpointId &&\n          styleDecl.state === undefined\n        ) {\n          if (styleDecl.property === \"width\") {\n            widthValue = styleDecl.value;\n          }\n          if (styleDecl.property === \"height\") {\n            heightValue = styleDecl.value;\n          }\n        }\n      }\n    }\n  }\n\n  return {\n    width: widthValue?.type === \"unit\" ? widthValue.value : undefined,\n    height: heightValue?.type === \"unit\" ? heightValue.value : undefined,\n  };\n};\n\n/**\n * Determine the inflation state for an element based on its actual dimensions\n * and whether explicit sizes are set. Returns `\"w\"`, `\"h\"`, `\"wh\"`, or `\"\"`.\n *\n * Pure function for testability.\n */\nconst getInflationState = ({\n  offsetWidth,\n  offsetHeight,\n  explicitWidth,\n  explicitHeight,\n}: {\n  offsetWidth: number;\n  offsetHeight: number;\n  explicitWidth: number | undefined;\n  explicitHeight: number | undefined;\n}): string => {\n  const inflateWidth =\n    offsetWidth === 0 && explicitWidth === undefined ? \"w\" : \"\";\n  const inflateHeight =\n    offsetHeight === 0 && explicitHeight === undefined ? \"h\" : \"\";\n  return inflateWidth + inflateHeight;\n};\n\n/**\n * Build a CSS selector to find the common ancestor of a set of instance IDs.\n * Falls back to `\"body\"` when `:has()` is not supported.\n *\n * Pure function for testability.\n */\nconst buildAncestorSelector = ({\n  instanceIds,\n  hasSelectorSupport,\n}: {\n  instanceIds: string[];\n  hasSelectorSupport: boolean;\n}): string => {\n  if (!hasSelectorSupport) {\n    return \"body\";\n  }\n  return `[${idAttribute}]${instanceIds\n    .map((instanceId) => `:has([${idAttribute}=\"${instanceId}\"])`)\n    .join(\"\")}`;\n};\n\n/**\n * Extract instanceIds from server sync store changes that affect style source selections.\n *\n * Pure function for testability.\n */\nconst extractInstanceIdsFromChanges = (\n  changes: Array<{\n    namespace: string;\n    patches: Array<{ value?: { instanceId?: unknown } }>;\n  }>\n): string[] => {\n  const ids: string[] = [];\n  for (const change of changes) {\n    if (change.namespace === \"styleSourceSelections\") {\n      for (const patch of change.patches) {\n        const instanceId = patch.value?.instanceId;\n        if (typeof instanceId === \"string\") {\n          ids.push(instanceId);\n        }\n      }\n    }\n  }\n  return ids;\n};\n\nconst MAX_SIZE_TO_USE_OPTIMIZATION = 50;\n\nconst findFirstNonContentsParent = (element: Element) => {\n  let parent = element.parentElement;\n\n  while (parent) {\n    const computedStyle = window.getComputedStyle(parent);\n    const isHidden = parent.getAttribute(\"hidden\") !== null;\n\n    if (computedStyle.display !== \"contents\" && !isHidden) {\n      return parent;\n    }\n\n    parent = parent.parentElement;\n  }\n\n  return undefined;\n};\n\nconst applyInflation = () => {\n  const rootInstanceId = $selectedPage.get()?.rootInstanceId;\n\n  // Below algorithm quickly finds the common ancestor of all elements with an instanceId.\n  // However, for a large number of elements, it's more efficient to calculate from the root.\n  // In almost all cases, if instanceIdsSet >= MAX_SIZE_TO_USE_OPTIMIZATION, it likely includes all elements.\n  const instanceIds =\n    instanceIdSet.size < MAX_SIZE_TO_USE_OPTIMIZATION\n      ? Array.from(instanceIdSet)\n      : rootInstanceId !== undefined\n        ? [rootInstanceId]\n        : [];\n\n  instanceIdSet.clear();\n  if (instanceIds.length === 0) {\n    return;\n  }\n\n  const hasSelectorSupport = isSelectorSupported(\":has(body)\");\n\n  const elementSelector = buildAncestorSelector({\n    instanceIds,\n    hasSelectorSupport,\n  });\n\n  const elements: Element[] = [];\n\n  // Element itself or last common ancestor or body\n  const baseElement =\n    Array.from(document.querySelectorAll(elementSelector)).pop() ??\n    document.body;\n  elements.push(baseElement);\n\n  const descendants = baseElement.querySelectorAll(`[${idAttribute}]`);\n  elements.push(...descendants);\n\n  const elementsToRecalculate: HTMLElement[] = [];\n  const gridContainers = new Set<HTMLElement>();\n  const parentsWithAbsoluteChildren = new Map<HTMLElement, number>();\n\n  for (const element of elements) {\n    if (!(element instanceof HTMLElement)) {\n      continue;\n    }\n\n    if (skipElementsSet.has(element.tagName.toLowerCase())) {\n      // Images should not collapse, and have a fallback.\n      // The issue that unloaded image has 0 width and height until explicitly set,\n      // so at the moment new Image added we detect it as collapsed.\n      // Skip it for now.\n      continue;\n    }\n\n    const elementStyle = window.getComputedStyle(element);\n\n    // Collect grid containers for per-track inflation (step 5).\n    // Runs regardless of child count — even grids with children need\n    // their tracks wrapped to prevent empty cells from collapsing.\n    if (\n      elementStyle.display === \"grid\" ||\n      elementStyle.display === \"inline-grid\"\n    ) {\n      gridContainers.add(element);\n    }\n\n    // Find all Leaf like elements\n    // Leaf like elements are elements that have no children or all children are absolute or fixed\n    // Excluding hidden elements without size\n    if (element.childElementCount === 0) {\n      if (element.offsetParent !== null) {\n        elementsToRecalculate.push(element);\n      }\n\n      if (elementStyle.position === \"fixed\") {\n        elementsToRecalculate.push(element);\n      }\n    }\n\n    const parentElement = findFirstNonContentsParent(element);\n\n    if (parentElement) {\n      if (\n        elementStyle.position === \"absolute\" ||\n        elementStyle.position === \"fixed\" ||\n        element.offsetParent == null // collapsed or none\n      ) {\n        parentsWithAbsoluteChildren.set(\n          parentElement,\n          parentsWithAbsoluteChildren.get(parentElement) ?? 0\n        );\n      } else {\n        parentsWithAbsoluteChildren.set(parentElement, 1);\n      }\n    }\n  }\n\n  for (const [element, value] of parentsWithAbsoluteChildren.entries()) {\n    // HTML is not part of webstudio data so skip to not process\n    if (element.tagName === \"HTML\") {\n      continue;\n    }\n\n    // All children are absolute or fixed\n    if (value === 0) {\n      elementsToRecalculate.push(element);\n    }\n  }\n\n  // If most elements are inflated at the next step, scrollHeight becomes equal to clientHeight,\n  // which resets the scroll position. To prevent this, we set the document's height to the current scrollHeight\n  // to preserve the scroll position.\n  // use nested element to avoid full page repaint and freeze on big projects\n  let inflatorElement = document.querySelector(\n    \"#ws-inflator\"\n  ) as null | HTMLElement;\n  if (!inflatorElement) {\n    inflatorElement = document.createElement(\"div\") as HTMLElement;\n    inflatorElement.style.position = \"absolute\";\n    inflatorElement.style.top = \"0px\";\n    inflatorElement.style.left = \"0px\";\n    inflatorElement.style.right = \"0px\";\n    inflatorElement.setAttribute(\"id\", \"ws-inflator\");\n    inflatorElement.setAttribute(\"hidden\", \"true\");\n    // Mark that we are in the process of recalculating inflated elements\n    // to avoid infinite loop of mutations\n    doNotTrackMutation(inflatorElement);\n    document.documentElement.appendChild(inflatorElement);\n  }\n  inflatorElement.removeAttribute(\"hidden\");\n  inflatorElement.style.height = `${document.documentElement.scrollHeight}px`;\n\n  // Now combine all operations in batches.\n\n  // 1. Remove all inflated attributes and grid inflation overrides.\n  const clearInflation = (el: Element) => {\n    const wasInflated = el.hasAttribute(inflatedAttribute);\n    const wasGridInflated = el.hasAttribute(gridInflatedAttribute);\n    el.removeAttribute(inflatedAttribute);\n    el.removeAttribute(gridInflatedAttribute);\n    if (el instanceof HTMLElement) {\n      el.style.removeProperty(\"--ws-inflate-h\");\n      el.style.removeProperty(\"--ws-inflate-w\");\n      if (wasInflated || wasGridInflated) {\n        el.style.removeProperty(\"grid-template-columns\");\n        el.style.removeProperty(\"grid-template-rows\");\n      }\n    }\n  };\n  if (baseElement.parentElement) {\n    clearInflation(baseElement.parentElement);\n  }\n  clearInflation(baseElement);\n  for (const element of baseElement.querySelectorAll(\n    `[${inflatedAttribute}], [${gridInflatedAttribute}]`\n  )) {\n    clearInflation(element);\n  }\n\n  // 2. Read stores once for all elements\n  const metas = $registeredComponentMetas.get();\n  const breakpoints = $breakpoints.get();\n  const selectedBreakpoint = $selectedBreakpoint.get();\n  const { stylesByInstanceId } = $stylesIndex.get();\n  const allInstances = $instances.get();\n  const selectedBreakpointId = selectedBreakpoint?.id;\n\n  // 3. Calculate inflation state\n  const inflatedElements = new Map<HTMLElement, string>();\n\n  for (const element of elementsToRecalculate) {\n    const elementInstanceId = element.getAttribute(idAttribute);\n\n    if (elementInstanceId === null) {\n      // Not a webstudio controlled element, like popover portal\n      continue;\n    }\n\n    const tagName = element.tagName.toLowerCase();\n\n    const elementSize = getInstanceSize({\n      instanceId: elementInstanceId,\n      tagName: isHtmlTag(tagName) ? tagName : undefined,\n      metas,\n      breakpoints,\n      selectedBreakpointId,\n      stylesByInstanceId,\n      instances: allInstances,\n    });\n\n    // Grid containers with no children may have non-zero offsetHeight/offsetWidth\n    // purely from gaps even though all tracks collapsed to 0px.\n    // Detect this case and treat the dimension as 0 so inflation kicks in.\n    let { offsetWidth, offsetHeight } = element;\n    const elementComputedStyle = window.getComputedStyle(element);\n    if (\n      (elementComputedStyle.display === \"grid\" ||\n        elementComputedStyle.display === \"inline-grid\") &&\n      element.childElementCount === 0\n    ) {\n      const rows = parseGridTemplateTrackList(\n        elementComputedStyle.gridTemplateRows\n      );\n      const cols = parseGridTemplateTrackList(\n        elementComputedStyle.gridTemplateColumns\n      );\n      if (rows.length > 0 && rows.every((t) => t.value === \"0px\")) {\n        offsetHeight = 0;\n      }\n      if (cols.length > 0 && cols.every((t) => t.value === \"0px\")) {\n        offsetWidth = 0;\n      }\n    }\n\n    const state = getInflationState({\n      offsetWidth,\n      offsetHeight,\n      explicitWidth: elementSize.width,\n      explicitHeight: elementSize.height,\n    });\n\n    if (state) {\n      inflatedElements.set(element, state);\n    }\n  }\n\n  // 4. Add inflated attributes for collapsed elements (dashed outline + padding).\n  for (const [element, value] of inflatedElements.entries()) {\n    element.setAttribute(inflatedAttribute, value);\n  }\n\n  // 5. Per-track grid inflation.\n  // Only inflate tracks that resolved to 0px (empty/collapsed).\n  // Skip inflation on axes that use auto-fit — the browser intentionally\n  // collapses unused auto-fit tracks to 0px and they should stay hidden.\n  for (const element of gridContainers) {\n    const instanceId = element.getAttribute(idAttribute);\n    const instanceStyles = instanceId\n      ? stylesByInstanceId.get(instanceId)\n      : undefined;\n\n    const hasAutoFit = (property: string) =>\n      instanceStyles?.some(\n        (s) =>\n          s.property === property &&\n          s.value.type === \"unparsed\" &&\n          s.value.value.includes(\"auto-fit\")\n      ) ?? false;\n\n    const computedStyle = window.getComputedStyle(element);\n\n    const cols = parseGridTemplateTrackList(computedStyle.gridTemplateColumns);\n    if (\n      !hasAutoFit(\"gridTemplateColumns\") &&\n      cols.length > 0 &&\n      cols.some((t) => t.value === \"0px\")\n    ) {\n      element.style.gridTemplateColumns = cols\n        .map((t) =>\n          t.value === \"0px\" ? `minmax(${INFLATE_PADDING}px, 0px)` : t.value\n        )\n        .join(\" \");\n    }\n\n    const rows = parseGridTemplateTrackList(computedStyle.gridTemplateRows);\n    if (\n      !hasAutoFit(\"gridTemplateRows\") &&\n      rows.length > 0 &&\n      rows.some((t) => t.value === \"0px\")\n    ) {\n      element.style.gridTemplateRows = rows\n        .map((t) =>\n          t.value === \"0px\" ? `minmax(${INFLATE_PADDING}px, 0px)` : t.value\n        )\n        .join(\" \");\n    }\n\n    element.setAttribute(gridInflatedAttribute, \"\");\n  }\n\n  inflatorElement.setAttribute(\"hidden\", \"true\");\n};\n\n/**\n * When we add elements or edit element styles, a situation arises where an element can collapse\n * to 0 width, 0 height, or both. We need the user to be able to select such elements anyway.\n * We find the minimum set of elements that would otherwise collapse, set the inflatedAttribute\n * on them, and style helpers add padding to prevent collapsing.\n **/\nexport const inflateInstance = (instanceId: string, syncExec = false) => {\n  instanceIdSet.add(instanceId);\n\n  cancelAnimationFrame(rafHandle);\n\n  if (syncExec) {\n    applyInflation();\n    return;\n  }\n\n  rafHandle = requestAnimationFrame(() => {\n    applyInflation();\n  });\n};\n\n/**\n * For optimisation reasons try to extract instanceId of changed elements from pubsub.\n * In that case we just check the subtree of parent/common ancestor of changed elements\n * to find elements that need inflation.\n **/\nexport const subscribeInflator = () => {\n  return serverSyncStore.subscribe((_transactionId, changes) => {\n    const ids = extractInstanceIdsFromChanges(changes);\n    for (const id of ids) {\n      inflateInstance(id);\n    }\n  });\n};\n\nexport const __testing__ = {\n  getInstanceSize,\n  getInflationState,\n  buildAncestorSelector,\n  extractInstanceIdsFromChanges,\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/instance-context-menu.ts",
    "content": "import { selectorIdAttribute } from \"@webstudio-is/react-sdk\";\nimport {\n  $instanceContextMenu,\n  $textEditingInstanceSelector,\n} from \"~/shared/nano-states\";\n\nexport const subscribeInstanceContextMenu = () => {\n  const handleContextMenu = (event: MouseEvent) => {\n    // Allow native context menu when editing text content\n    if ($textEditingInstanceSelector.get() !== undefined) {\n      return;\n    }\n\n    const target = event.target as HTMLElement;\n    const element = target.closest(`[${selectorIdAttribute}]`);\n    const selectorId = element?.getAttribute(selectorIdAttribute);\n\n    if (selectorId) {\n      event.preventDefault();\n      const instanceSelector = selectorId.split(\",\");\n      $instanceContextMenu.set({\n        position: { x: event.clientX, y: event.clientY },\n        instanceSelector,\n      });\n    }\n  };\n\n  document.addEventListener(\"contextmenu\", handleContextMenu);\n\n  return () => {\n    document.removeEventListener(\"contextmenu\", handleContextMenu);\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/instance-hovering.ts",
    "content": "import { idAttribute } from \"@webstudio-is/react-sdk\";\nimport {\n  $blockChildOutline,\n  $hoveredInstanceSelector,\n  $instances,\n  $textEditingInstanceSelector,\n  findBlockChildSelector,\n} from \"~/shared/nano-states\";\nimport { $hoveredInstanceOutline } from \"~/shared/nano-states\";\nimport {\n  getAllElementsBoundingBox,\n  getVisibleElementsByInstanceSelector,\n  getInstanceSelectorFromElement,\n} from \"~/shared/dom-utils\";\nimport { subscribeScrollState } from \"./shared/scroll-state\";\nimport { isDescendantOrSelf, type InstanceSelector } from \"~/shared/tree-utils\";\nimport { $awareness } from \"~/shared/awareness\";\n\ntype TimeoutId = undefined | ReturnType<typeof setTimeout>;\n\nexport const subscribeInstanceHovering = ({\n  signal,\n}: {\n  signal: AbortSignal;\n}) => {\n  let hoveredElement: undefined | Element = undefined;\n  let updateOnMouseMove = false;\n  let isScrolling = false;\n\n  let mouseOutTimeoutId: TimeoutId = undefined;\n\n  const eventOptions = { passive: true, signal };\n\n  window.addEventListener(\n    \"mouseover\",\n    (event) => {\n      if (event.target instanceof Element) {\n        const element = event.target.closest(`[${idAttribute}]`) ?? undefined;\n        if (element !== undefined) {\n          clearTimeout(mouseOutTimeoutId);\n          // store hovered element locally to update outline when scroll ends\n          hoveredElement = element;\n          updateOnMouseMove = true;\n        }\n      }\n    },\n    eventOptions\n  );\n\n  const updateEditableOutline = () => {\n    // We want the hover outline to appear only if a mouse or trackpad action caused it, not from keyboard navigation.\n    // Otherwise, when we leave the Lexical editor using the keyboard,\n    // the mouseover event triggers on elements created after Lexical loses focus.\n    // This causes an outline to appear on the element under the now-invisible mouse pointer\n    // (as the browser hides the pointer on blur), creating some visual distraction.\n    if (hoveredElement !== undefined) {\n      const instanceSelector = getInstanceSelectorFromElement(hoveredElement);\n      if (instanceSelector) {\n        if (updateOnMouseMove) {\n          $hoveredInstanceSelector.set(instanceSelector);\n          updateEditableChildOutline(instanceSelector);\n        } else {\n          const textSelector = $textEditingInstanceSelector.get()?.selector;\n\n          // We need to update the editable child's outline even if the mouseover event is not triggered.\n          // This can happen if the user enters text editing mode, presses any key, and then moves the mouse.\n          // In this case, the mouseover event does not occur, but we still need to show the editable child's outline.\n          if (\n            textSelector &&\n            isDescendantOrSelf(instanceSelector, textSelector) && // optimisation\n            $blockChildOutline.get() === undefined\n          ) {\n            updateEditableChildOutline(instanceSelector);\n          }\n        }\n      }\n    }\n    updateOnMouseMove = false;\n  };\n\n  window.addEventListener(\n    \"click\",\n    () => {\n      // Fixes the bug if initial editable instance is empty and has collapsed paddings\n      setTimeout(updateEditableOutline, 0);\n    },\n    eventOptions\n  );\n\n  window.addEventListener(\"mousemove\", updateEditableOutline, eventOptions);\n\n  window.addEventListener(\n    \"mouseout\",\n    () => {\n      updateOnMouseMove = false;\n      hoveredElement = undefined;\n\n      mouseOutTimeoutId = setTimeout(() => {\n        $blockChildOutline.set(undefined);\n        $hoveredInstanceSelector.set(undefined);\n        $hoveredInstanceOutline.set(undefined);\n      }, 100);\n\n      // Fixes the bug, that new hover occures during timeout\n      const unsubscribe = $hoveredInstanceSelector.listen(() => {\n        clearTimeout(mouseOutTimeoutId);\n        unsubscribe();\n      });\n    },\n    eventOptions\n  );\n\n  const updateEditableChildOutline = (instanceSelector: InstanceSelector) => {\n    if (isScrolling) {\n      return;\n    }\n\n    if (instanceSelector.length === 0) {\n      return;\n    }\n\n    const blockChildSelector = findBlockChildSelector(instanceSelector);\n\n    if (blockChildSelector === undefined) {\n      $blockChildOutline.set(undefined);\n      return;\n    }\n\n    const blockChildElements =\n      getVisibleElementsByInstanceSelector(blockChildSelector);\n    const blockChildRect = getAllElementsBoundingBox(blockChildElements);\n\n    if (blockChildRect === undefined) {\n      $blockChildOutline.set(undefined);\n      return;\n    }\n\n    $blockChildOutline.set({\n      selector: blockChildSelector,\n      rect: blockChildRect,\n    });\n  };\n\n  const updateHoveredRect = (instanceSelector: Readonly<InstanceSelector>) => {\n    if (isScrolling) {\n      return;\n    }\n\n    if (instanceSelector.length === 0) {\n      return;\n    }\n\n    const elements = getVisibleElementsByInstanceSelector(instanceSelector);\n\n    if (elements.length === 0) {\n      return;\n    }\n\n    const [instanceId] = instanceSelector;\n    const instances = $instances.get();\n    const instance = instances.get(instanceId);\n    if (instance === undefined) {\n      return;\n    }\n\n    $hoveredInstanceOutline.set({\n      instanceId: instance.id,\n      rect: getAllElementsBoundingBox(elements),\n    });\n  };\n\n  // remove hover outline when scroll starts\n  // and show it with new rect when scroll ends\n  const unsubscribeScrollState = subscribeScrollState({\n    onScrollStart() {\n      isScrolling = true;\n      $hoveredInstanceOutline.set(undefined);\n      $blockChildOutline.set(undefined);\n    },\n    onScrollEnd() {\n      isScrolling = false;\n      if (hoveredElement !== undefined) {\n        const instanceSelector = getInstanceSelectorFromElement(hoveredElement);\n\n        if (instanceSelector === undefined) {\n          return;\n        }\n\n        updateHoveredRect(instanceSelector);\n      }\n    },\n  });\n\n  // update rect whenever hovered instance is changed\n  const unsubscribeHoveredInstanceId = $hoveredInstanceSelector.subscribe(\n    (instanceSelector) => {\n      if (instanceSelector) {\n        updateHoveredRect(instanceSelector);\n      } else {\n        $hoveredInstanceOutline.set(undefined);\n      }\n    }\n  );\n\n  // selected instance selection can change hovered instance outlines (example Block/Template/Child)\n  const usubscribeSelectedInstanceSelector = $awareness.subscribe(() => {\n    const instanceSelector = $hoveredInstanceSelector.get();\n    if (instanceSelector) {\n      updateHoveredRect(instanceSelector);\n    } else {\n      $hoveredInstanceOutline.set(undefined);\n    }\n  });\n\n  signal.addEventListener(\"abort\", () => {\n    unsubscribeScrollState();\n    clearTimeout(mouseOutTimeoutId);\n    unsubscribeHoveredInstanceId();\n    usubscribeSelectedInstanceSelector();\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/instance-selected.ts",
    "content": "import { shallowEqual } from \"shallow-equal\";\nimport warnOnce from \"warn-once\";\nimport type { Instance } from \"@webstudio-is/sdk\";\nimport type { CssProperty, UnitValue } from \"@webstudio-is/css-engine\";\nimport { propertiesData } from \"@webstudio-is/css-data\";\nimport { selectorIdAttribute } from \"@webstudio-is/react-sdk\";\nimport { subscribeWindowResize } from \"~/shared/dom-hooks\";\nimport {\n  $isResizingCanvas,\n  $selectedInstanceSizes,\n  $selectedInstanceRenderState,\n  $stylesIndex,\n  $instances,\n  $propValuesByInstanceSelectorWithMemoryProps,\n  $styles,\n  $selectedInstanceStates,\n  $styleSourceSelections,\n  type UnitSizes,\n  type PropertySizes,\n} from \"~/shared/nano-states\";\nimport {\n  getAllElementsBoundingBox,\n  getVisibleElementsByInstanceSelector,\n  getAllElementsByInstanceSelector,\n  scrollIntoView,\n  hasDoNotTrackMutationRecord,\n} from \"~/shared/dom-utils\";\nimport { subscribeScrollState } from \"~/canvas/shared/scroll-state\";\nimport { $selectedInstanceOutline } from \"~/shared/nano-states\";\nimport { inflateInstance } from \"~/canvas/inflator\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\nimport { $awareness } from \"~/shared/awareness\";\n\nconst setOutline = (instanceId: Instance[\"id\"], elements: HTMLElement[]) => {\n  $selectedInstanceOutline.set({\n    instanceId,\n    rect: getAllElementsBoundingBox(elements),\n  });\n};\n\nconst hideOutline = () => {\n  $selectedInstanceOutline.set(undefined);\n};\n\nconst calculateUnitSizes = (element: HTMLElement): UnitSizes => {\n  // Based on this https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions/8876069#8876069\n  // this is crossbrowser way to get viewport sizes vw vh in px\n  const vw =\n    Math.max(document.documentElement.clientWidth, window.innerWidth) / 100;\n  const vh =\n    Math.max(document.documentElement.clientHeight, window.innerHeight) / 100;\n\n  // em in px is equal to current computed style for font size\n  const em = Number.parseFloat(getComputedStyle(element).fontSize);\n\n  // rem in px is equal to root computed style for font size\n  const rem = Number.parseFloat(\n    getComputedStyle(document.documentElement).fontSize\n  );\n\n  // we create a node with 1ch width, measure it in px and remove it\n  const node = document.createElement(\"div\");\n  node.style.width = \"1ch\";\n  node.style.position = \"absolute\";\n  element.appendChild(node);\n  const ch = Number.parseFloat(getComputedStyle(node).width);\n  element.removeChild(node);\n\n  return {\n    ch, // 1ch in pixels\n    vw, // 1vw in pixels\n    vh, // 1vh in pixels\n    em, // 1em in pixels\n    rem, // 1rem in pixels\n    px: 1, // always 1, simplifies conversions and types, i.e valueTo = valueFrom * unitSizes[from] / unitSizes[to]\n  };\n};\n\nconst calculatePropertySizes = (element: HTMLElement) => {\n  const computedStyle = getComputedStyle(element);\n  const propertySizes: PropertySizes = {};\n  for (const property in propertiesData) {\n    try {\n      const propertyValue = computedStyle.getPropertyValue(property);\n      const value = CSSStyleValue.parse(property, propertyValue);\n      if (value instanceof CSSUnitValue) {\n        propertySizes[property as CssProperty] = {\n          type: \"unit\",\n          // px | number | percent etc\n          unit:\n            value.unit === \"percent\" ? \"%\" : (value.unit as UnitValue[\"unit\"]),\n          value: value.value,\n        };\n      }\n    } catch (error) {\n      // failed with unknown property like -moz-osx-font-smoothing\n      // also firefox does not support CSSStyleValue\n    }\n  }\n  return propertySizes;\n};\n\nconst subscribeSelectedInstance = (\n  selectedInstanceSelector: Readonly<InstanceSelector>,\n  debounceEffect: (callback: () => void) => void\n) => {\n  if (selectedInstanceSelector.length === 0) {\n    return;\n  }\n\n  const instanceId = selectedInstanceSelector[0];\n\n  let visibleElements = getVisibleElementsByInstanceSelector(\n    selectedInstanceSelector\n  );\n\n  const updateScroll = () => {\n    const bbox = getAllElementsBoundingBox(visibleElements);\n    if (visibleElements.length === 0) {\n      return;\n    }\n    scrollIntoView(visibleElements[0], bbox);\n  };\n\n  const updateElements = () => {\n    visibleElements = getVisibleElementsByInstanceSelector(\n      selectedInstanceSelector\n    );\n  };\n\n  const updateInflation = () => {\n    if (visibleElements.length === 0) {\n      return;\n    }\n\n    for (const element of visibleElements) {\n      const selectorId = element.getAttribute(selectorIdAttribute);\n      if (selectorId === null) {\n        continue;\n      }\n\n      const instanceSelector = selectorId.split(\",\");\n      if (instanceSelector.length === 0) {\n        continue;\n      }\n\n      inflateInstance(instanceSelector[0], false);\n    }\n\n    // Synchronously execute inflateInstance to calculate right outline\n    // This fixes an issue, when new element outline was calculated before inflation calculations\n    inflateInstance(instanceId, true);\n  };\n\n  updateInflation();\n\n  const showOutline = () => {\n    if ($isResizingCanvas.get()) {\n      return;\n    }\n    setOutline(instanceId, visibleElements);\n  };\n  // effect close to rendered element also catches dnd remounts\n  // so actual state is always provided here\n  showOutline();\n\n  const updateStores = () => {\n    const elements = getAllElementsByInstanceSelector(selectedInstanceSelector);\n\n    if (elements.length === 0) {\n      return;\n    }\n\n    const [element] = elements;\n    // trigger style recomputing every time instance styles are changed\n    const unitSizes = calculateUnitSizes(element);\n    const propertySizes = calculatePropertySizes(element);\n    $selectedInstanceSizes.set({ unitSizes, propertySizes });\n\n    const availableStates = new Set<string>();\n    const instanceStyleSourceIds = new Set(\n      $styleSourceSelections.get().get(instanceId)?.values\n    );\n    const styles = $styles.get();\n    for (const styleDecl of styles.values()) {\n      if (\n        instanceStyleSourceIds.has(styleDecl.styleSourceId) &&\n        styleDecl.state\n      ) {\n        availableStates.add(styleDecl.state);\n      }\n    }\n    const activeStates = new Set<string>();\n    for (const state of availableStates) {\n      try {\n        // pseudo classes like :open or :current are not supported in .matches method\n        if (element.matches(state)) {\n          activeStates.add(state);\n        }\n      } catch {\n        warnOnce(true, `state selector \"${state}\" is invalid`);\n      }\n    }\n\n    if (\n      !shallowEqual(activeStates.keys(), $selectedInstanceStates.get().keys())\n    ) {\n      $selectedInstanceStates.set(activeStates);\n    }\n  };\n\n  let updateStoreTimeouHandle: undefined | ReturnType<typeof setTimeout>;\n\n  const update = () => {\n    debounceEffect(() => {\n      updateElements();\n      // Disconnect before inflation so the observer doesn't see\n      // the inline style changes inflation makes on the parent element.\n      // updateObservers() reconnects after.\n      mutationObserver.disconnect();\n      // Having hover etc, element can have no size because of that\n      // Newly created element can have 0 size\n      updateInflation();\n      // contentRect has wrong x/y values for absolutely positioned element.\n      // getBoundingClientRect is used instead.\n      showOutline();\n\n      updateStores();\n\n      // Having that elements can be changed (i.e. div => address tag change, observe again)\n      updateObservers();\n    });\n  };\n\n  // update scroll state\n  updateScroll();\n\n  // Lightweight update\n  const updateOutline: MutationCallback = (mutationRecords) => {\n    if (hasDoNotTrackMutationRecord(mutationRecords)) {\n      return;\n    }\n\n    showOutline();\n  };\n\n  const resizeObserver = new ResizeObserver(update);\n\n  const mutationHandler: MutationCallback = (mutationRecords) => {\n    if (hasDoNotTrackMutationRecord(mutationRecords)) {\n      return;\n    }\n\n    update();\n  };\n\n  // detect movement of the element within same parent\n  // React prevent remount when key stays the same\n  // `attributes: true` fixes issues with popups after trigger text editing\n  // that cause radix to incorrectly set content in a wrong position at first render\n  const mutationObserver = new MutationObserver(mutationHandler);\n\n  const updateObservers = () => {\n    for (const element of visibleElements) {\n      resizeObserver.observe(element);\n\n      const parent = element?.parentElement;\n\n      if (parent) {\n        mutationObserver.observe(parent, {\n          childList: true,\n          attributes: true,\n          attributeOldValue: true,\n          attributeFilter: [\"style\", \"class\"],\n        });\n      }\n    }\n  };\n\n  const bodyStyleMutationObserver = new MutationObserver(updateOutline);\n\n  // previewStyle variables\n  bodyStyleMutationObserver.observe(document.body, {\n    attributes: true,\n    attributeOldValue: true,\n    attributeFilter: [\"style\", \"class\"],\n  });\n\n  updateObservers();\n\n  const unsubscribe$stylesIndex = $stylesIndex.subscribe(update);\n  const unsubscribe$instances = $instances.subscribe(update);\n  const unsubscribePropValuesStore =\n    $propValuesByInstanceSelectorWithMemoryProps.subscribe(update);\n\n  const unsubscribeIsResizingCanvas = $isResizingCanvas.subscribe(\n    (isResizing) => {\n      if (isResizing && $selectedInstanceOutline.get()) {\n        return hideOutline();\n      }\n      showOutline();\n    }\n  );\n\n  const unsubscribeScrollState = subscribeScrollState({\n    onScrollStart() {\n      hideOutline();\n    },\n    onScrollEnd() {\n      showOutline();\n    },\n  });\n\n  const unsubscribeWindowResize = subscribeWindowResize({\n    onResizeStart() {\n      hideOutline();\n    },\n    onResizeEnd() {\n      showOutline();\n    },\n  });\n\n  $selectedInstanceRenderState.set(\"mounted\");\n\n  return () => {\n    clearTimeout(updateStoreTimeouHandle);\n    hideOutline();\n    $selectedInstanceRenderState.set(\"notMounted\");\n    resizeObserver.disconnect();\n    mutationObserver.disconnect();\n    bodyStyleMutationObserver.disconnect();\n    unsubscribeIsResizingCanvas();\n    unsubscribeScrollState();\n    unsubscribeWindowResize();\n    unsubscribe$stylesIndex();\n    unsubscribe$instances();\n    unsubscribePropValuesStore();\n  };\n};\n\nexport const subscribeSelected = (\n  debounceEffect: (callback: () => void) => void\n) => {\n  let previousSelectedInstance: readonly string[] | undefined = undefined;\n  let unsubscribeSelectedInstance = () => {};\n\n  const unsubscribe = $awareness.subscribe((awareness) => {\n    const instanceSelector = awareness?.instanceSelector;\n    if (instanceSelector !== previousSelectedInstance) {\n      unsubscribeSelectedInstance();\n      unsubscribeSelectedInstance =\n        subscribeSelectedInstance(instanceSelector ?? [], debounceEffect) ??\n        (() => {});\n      previousSelectedInstance = instanceSelector;\n    }\n  });\n\n  return () => {\n    unsubscribe();\n    unsubscribeSelectedInstance();\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/instance-selection.ts",
    "content": "import { getInstanceSelectorFromElement } from \"~/shared/dom-utils\";\nimport {\n  $hoveredInstanceOutline,\n  $hoveredInstanceSelector,\n  $instances,\n  $isContentMode,\n  $props,\n  $registeredComponentMetas,\n} from \"~/shared/nano-states\";\nimport { $textEditingInstanceSelector } from \"~/shared/nano-states\";\nimport { emitCommand } from \"./shared/commands\";\nimport { shallowEqual } from \"shallow-equal\";\nimport { $awareness, selectInstance } from \"~/shared/awareness\";\nimport { findClosestRichText } from \"~/shared/content-model\";\n\nconst isElementBeingEdited = (element: Element) => {\n  if (element.closest(\"[contenteditable=true]\")) {\n    return true;\n  }\n\n  return false;\n};\n\nconst handleSelect = (event: MouseEvent) => {\n  const element = event.target;\n\n  if (!(element instanceof Element)) {\n    return;\n  }\n\n  if (isElementBeingEdited(element)) {\n    return;\n  }\n\n  const instanceSelector = getInstanceSelectorFromElement(element);\n\n  if (instanceSelector === undefined) {\n    return;\n  }\n\n  // Prevent unnecessary updates (2 clicks are registered before a double click)\n  if ($textEditingInstanceSelector.get() !== undefined) {\n    $textEditingInstanceSelector.set(undefined);\n  }\n\n  // Prevent unnecessary updates (2 clicks are registered before a double click)\n  if (!shallowEqual(instanceSelector, $awareness.get()?.instanceSelector)) {\n    selectInstance(instanceSelector);\n  }\n};\n\nconst handleEdit = (event: MouseEvent) => {\n  const element = event.target;\n\n  if (!(element instanceof Element)) {\n    return;\n  }\n\n  if (isElementBeingEdited(element)) {\n    return;\n  }\n\n  const instanceSelector = getInstanceSelectorFromElement(element);\n\n  if (instanceSelector === undefined) {\n    return;\n  }\n\n  const instances = $instances.get();\n\n  let editableInstanceSelector = findClosestRichText({\n    instanceSelector,\n    instances,\n    props: $props.get(),\n    metas: $registeredComponentMetas.get(),\n  });\n\n  // Do not allow edit bindable text instances with expression children in Content Mode\n  if (editableInstanceSelector !== undefined && $isContentMode.get()) {\n    const instance = instances.get(editableInstanceSelector[0]);\n    if (instance === undefined) {\n      return false;\n    }\n\n    const hasExpressionChildren = instance.children.some(\n      (child) => child.type === \"expression\"\n    );\n\n    if (hasExpressionChildren) {\n      editableInstanceSelector = undefined;\n    }\n  }\n\n  if (editableInstanceSelector === undefined) {\n    // Handle non-editable instances in Content Mode:\n\n    // Reset editing state when clicking from an editable text to a non-editable instance\n    if ($textEditingInstanceSelector.get() !== undefined) {\n      $textEditingInstanceSelector.set(undefined);\n    }\n\n    // Select the instance when no editable parent is found\n    if (!shallowEqual(instanceSelector, $awareness.get()?.instanceSelector)) {\n      selectInstance(instanceSelector);\n    }\n\n    return;\n  }\n\n  // Avoid redundant selection if the instance is already selected\n  if (\n    !shallowEqual($awareness.get()?.instanceSelector, editableInstanceSelector)\n  ) {\n    selectInstance(editableInstanceSelector);\n  }\n\n  $hoveredInstanceOutline.set(undefined);\n  $hoveredInstanceSelector.set(undefined);\n\n  $textEditingInstanceSelector.set({\n    selector: editableInstanceSelector,\n    reason: \"click\",\n    mouseX: event.clientX,\n    mouseY: event.clientY,\n  });\n};\n\nexport const subscribeInstanceSelection = ({\n  signal,\n}: {\n  signal: AbortSignal;\n}) => {\n  addEventListener(\n    \"click\",\n    (event) => {\n      emitCommand(\"clickCanvas\");\n\n      if ($isContentMode.get()) {\n        handleEdit(event);\n        return;\n      }\n\n      handleSelect(event);\n    },\n    { passive: true, signal }\n  );\n\n  addEventListener(\n    \"dblclick\",\n    (event) => {\n      handleEdit(event);\n    },\n    { passive: true, signal }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/interceptor.ts",
    "content": "import { getPagePath, isAbsoluteUrl } from \"@webstudio-is/sdk\";\nimport {\n  compilePathnamePattern,\n  matchPathnamePattern,\n  tokenizePathnamePattern,\n} from \"~/builder/shared/url-pattern\";\nimport { $selectedPage, selectPage } from \"~/shared/awareness\";\nimport {\n  $isPreviewMode,\n  $pages,\n  $selectedPageHash,\n} from \"~/shared/nano-states\";\nimport { $currentSystem, updateCurrentSystem } from \"~/shared/system\";\nimport { comparePatterns } from \"./shared/routing-priority\";\n\nconst getSelectedPagePathname = () => {\n  const pages = $pages.get();\n  const page = $selectedPage.get();\n  if (page && pages) {\n    const tokens = tokenizePathnamePattern(getPagePath(page.id, pages));\n    const system = $currentSystem.get();\n    return compilePathnamePattern(tokens, system.params);\n  }\n};\n\nconst switchPageAndUpdateSystem = (href: string, formData?: FormData) => {\n  const pages = $pages.get();\n  if (pages === undefined) {\n    return;\n  }\n  // preserve pathname when not specified in href/action\n  if (href === \"\" || href.startsWith(\"?\")) {\n    const pathname = getSelectedPagePathname();\n    if (pathname) {\n      href = `${pathname}${href}`;\n    }\n  }\n  // preserve also search params when navigate with hash\n  if (href.startsWith(\"#\")) {\n    const pathname = getSelectedPagePathname();\n    if (pathname) {\n      const system = $currentSystem.get();\n      const searchParams = new URLSearchParams(\n        system.search as Record<string, string>\n      );\n      href = `${pathname}?${searchParams}${href}`;\n    }\n  }\n  const pageHref = new URL(href, \"https://any-valid.url\");\n  // sort pages before matching to not depend on order of page creation\n  const sortedPages = [pages.homePage, ...pages.pages].toSorted(\n    (leftPage, rightPage) => comparePatterns(leftPage.path, rightPage.path)\n  );\n  for (const page of sortedPages) {\n    const pagePath = getPagePath(page.id, pages);\n    const params = matchPathnamePattern(pagePath, pageHref.pathname);\n    if (params) {\n      // populate search params with form data values if available\n      if (formData) {\n        for (const [key, value] of formData.entries()) {\n          pageHref.searchParams.set(key, value.toString());\n        }\n      }\n      const search = Object.fromEntries(pageHref.searchParams);\n      $selectedPageHash.set({ hash: pageHref.hash });\n      selectPage(page.id);\n      updateCurrentSystem({ params, search });\n      break;\n    }\n  }\n};\n\nexport const subscribeInterceptedEvents = () => {\n  const handleClick = (event: MouseEvent) => {\n    if (!(event.target instanceof Element)) {\n      return;\n    }\n    const isPreviewMode = $isPreviewMode.get();\n\n    // Prevent forwarding the click event on an input element when the associated label has a \"for\" attribute\n    // and prevent checkbox or radio inputs changing when clicked\n    if (event.target.closest(\"label[for]\") || event.target.closest(\"input\")) {\n      if (isPreviewMode) {\n        return;\n      }\n      event.preventDefault();\n    }\n\n    const a = event.target.closest(\"a\");\n    if (a) {\n      if (isPreviewMode) {\n        // use attribute instead of a.href to get raw unresolved value\n        const href = a.getAttribute(\"href\") ?? \"\";\n        if (isAbsoluteUrl(href)) {\n          window.open(href, \"_blank\");\n          // relative paths can be safely downloaded\n        } else if (a.hasAttribute(\"download\")) {\n          return;\n        } else {\n          switchPageAndUpdateSystem(href);\n        }\n        event.preventDefault();\n        return;\n      }\n      event.preventDefault();\n    }\n    // prevent invoking submit with buttons in canvas mode\n    // because form with prevented submit still invokes validation\n    if (event.target.closest(\"button\")) {\n      if (isPreviewMode) {\n        return;\n      }\n      event.preventDefault();\n    }\n  };\n\n  const handlePointerDown = (event: PointerEvent) => {\n    if (!(event.target instanceof Element)) {\n      return;\n    }\n    const isPreviewMode = $isPreviewMode.get();\n\n    if (event.target.closest(\"select\")) {\n      if (isPreviewMode) {\n        return;\n      }\n      event.preventDefault();\n    }\n  };\n\n  const handleSubmit = (event: SubmitEvent) => {\n    if ($isPreviewMode.get()) {\n      const form =\n        event.target instanceof HTMLFormElement ? event.target : undefined;\n      if (form === undefined) {\n        return;\n      }\n      // use attribute instead of form.action to get raw unresolved value\n      // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-fs-action\n      const action = form.getAttribute(\"action\") ?? \"\";\n      // lower case just for safety\n      const method = form.method.toLowerCase();\n      if (method === \"get\" && isAbsoluteUrl(action) === false) {\n        switchPageAndUpdateSystem(action, new FormData(form));\n      }\n    }\n    // prevent submitting the form when clicking a button type submit\n    event.preventDefault();\n  };\n\n  const handleKeydown = (event: KeyboardEvent) => {\n    if (!(event.target instanceof Element)) {\n      return;\n    }\n    if ($isPreviewMode.get()) {\n      return;\n    }\n    if (\n      event.target instanceof HTMLInputElement ||\n      event.target instanceof HTMLTextAreaElement\n    ) {\n      // prevent typing in inputs only in canvas mode\n      event.preventDefault();\n    }\n  };\n\n  // Note: Event handlers behave unexpectedly when used inside a dialog component.\n  // In Dialogs, React intercepts and processes events before they reach our handlers.\n  // To ensure consistent behavior across all components, we're using event capturing.\n  // This allows us to intercept events before React gets a chance to handle them.\n  document.documentElement.addEventListener(\"click\", handleClick, {\n    capture: true,\n  });\n  document.documentElement.addEventListener(\"submit\", handleSubmit, {\n    capture: true,\n  });\n\n  document.documentElement.addEventListener(\"keydown\", handleKeydown);\n\n  document.documentElement.addEventListener(\"pointerdown\", handlePointerDown);\n\n  return () => {\n    document.documentElement.removeEventListener(\n      \"pointerdown\",\n      handlePointerDown\n    );\n    document.documentElement.removeEventListener(\"click\", handleClick, {\n      capture: true,\n    });\n    document.documentElement.removeEventListener(\"submit\", handleSubmit, {\n      capture: true,\n    });\n    document.documentElement.removeEventListener(\"keydown\", handleKeydown);\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/scrollbar-width.ts",
    "content": "import { shallowEqual } from \"shallow-equal\";\nimport { $canvasScrollbarSize } from \"~/builder/shared/nano-states\";\n\nexport const subscribeScrollbarSize = ({ signal }: { signal: AbortSignal }) => {\n  const getScrollbarSize = () => ({\n    width: window.innerWidth - document.documentElement.clientWidth,\n    height: window.innerHeight - document.documentElement.clientHeight,\n  });\n\n  $canvasScrollbarSize.set(getScrollbarSize());\n\n  const observer = new ResizeObserver(() => {\n    const newSize = getScrollbarSize();\n    if (!shallowEqual($canvasScrollbarSize.get(), newSize)) {\n      $canvasScrollbarSize.set(newSize);\n    }\n  });\n\n  observer.observe(document.documentElement);\n\n  signal.addEventListener(\n    \"abort\",\n    () => {\n      observer.disconnect();\n    },\n    { once: true }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/commands.ts",
    "content": "import { FORMAT_TEXT_COMMAND } from \"lexical\";\nimport { TOGGLE_LINK_COMMAND } from \"@lexical/link\";\nimport { createCommandsEmitter } from \"~/shared/commands-emitter\";\nimport { getElementByInstanceSelector } from \"~/shared/dom-utils\";\nimport { findAllEditableInstanceSelector } from \"~/shared/instance-utils\";\nimport {\n  $instances,\n  $props,\n  $registeredComponentMetas,\n  $selectedInstanceSelector,\n  $textEditingInstanceSelector,\n  $textToolbar,\n} from \"~/shared/nano-states\";\nimport {\n  CLEAR_FORMAT_COMMAND,\n  TOGGLE_SPAN_COMMAND,\n  getActiveEditor,\n  hasSelectionFormat,\n} from \"../features/text-editor/toolbar-connector\";\nimport { selectInstance } from \"~/shared/awareness\";\nimport { isDescendantOrSelf, type InstanceSelector } from \"~/shared/tree-utils\";\nimport { deleteSelectedInstance } from \"~/shared/instance-utils\";\nimport { findClosestRichText } from \"~/shared/content-model\";\n\nexport const { emitCommand, subscribeCommands } = createCommandsEmitter({\n  source: \"canvas\",\n  externalCommands: [\"clickCanvas\"],\n  commands: [\n    {\n      name: \"deleteInstanceCanvas\",\n      hidden: true,\n      defaultHotkeys: [\"backspace\", \"delete\"],\n      preventDefault: false,\n      disableHotkeyOutsideApp: true,\n      // We are not disabling \"Backspace\" or \"Delete\" on the canvas. This is the main reason we have separate functions: deleteInstanceCanvas and deleteInstanceBuilder.\n      disableOnInputLikeControls: false,\n      handler: deleteSelectedInstance,\n    },\n\n    {\n      name: \"editInstanceText\",\n      hidden: true,\n      defaultHotkeys: [\"enter\"],\n      disableOnInputLikeControls: true,\n      // builder invokes command with custom hotkey setup\n      disableHotkeyOutsideApp: true,\n      handler: () => {\n        const selectedInstanceSelector = $selectedInstanceSelector.get();\n        if (selectedInstanceSelector === undefined) {\n          return;\n        }\n\n        if (\n          isDescendantOrSelf(\n            $textEditingInstanceSelector.get()?.selector ?? [],\n            selectedInstanceSelector\n          )\n        ) {\n          // already in text editing mode\n          return;\n        }\n\n        let editableInstanceSelector = findClosestRichText({\n          instanceSelector: selectedInstanceSelector,\n          instances: $instances.get(),\n          props: $props.get(),\n          metas: $registeredComponentMetas.get(),\n        });\n\n        if (editableInstanceSelector === undefined) {\n          const selectors: InstanceSelector[] = [];\n\n          findAllEditableInstanceSelector({\n            instanceSelector: selectedInstanceSelector,\n            instances: $instances.get(),\n            props: $props.get(),\n            metas: $registeredComponentMetas.get(),\n            results: selectors,\n          });\n\n          if (selectors.length === 0) {\n            $textEditingInstanceSelector.set(undefined);\n            return;\n          }\n\n          editableInstanceSelector = selectors[0];\n        }\n\n        const element = getElementByInstanceSelector(editableInstanceSelector);\n        if (element === undefined) {\n          return;\n        }\n        // When an event is triggered from the Builder,\n        // the canvas element may be unfocused, so it's important to focus the element on the canvas.\n        element.focus();\n\n        selectInstance(editableInstanceSelector);\n\n        $textEditingInstanceSelector.set({\n          selector: editableInstanceSelector,\n          reason: \"enter\",\n        });\n      },\n    },\n\n    {\n      name: \"escapeSelection\",\n      hidden: true,\n      defaultHotkeys: [\"escape\"],\n      disableOnInputLikeControls: true,\n      // reset selection for canvas, but not for the builder\n      disableHotkeyOutsideApp: true,\n      handler: () => {\n        const selectedInstanceSelector = $selectedInstanceSelector.get();\n        const textEditingInstanceSelector = $textEditingInstanceSelector.get();\n        const textToolbar = $textToolbar.get();\n\n        // close text toolbar first without exiting text editing mode\n        if (textToolbar) {\n          $textToolbar.set(undefined);\n          return;\n        }\n\n        // exit text editing mode first without unselecting instance\n        if (textEditingInstanceSelector) {\n          $textEditingInstanceSelector.set(undefined);\n          return;\n        }\n\n        if (selectedInstanceSelector) {\n          // unselect both instance and style source\n          selectInstance(undefined);\n          return;\n        }\n      },\n    },\n\n    {\n      name: \"formatBold\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        editor?.dispatchCommand(FORMAT_TEXT_COMMAND, \"bold\");\n        // refocus editor on the next frame\n        // otherwise it sometimes is left on toolbar button\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n    {\n      name: \"formatItalic\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        editor?.dispatchCommand(FORMAT_TEXT_COMMAND, \"italic\");\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n    {\n      name: \"formatSuperscript\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        editor?.dispatchCommand(FORMAT_TEXT_COMMAND, \"superscript\");\n        // remove subscript if superscript is added\n        if (hasSelectionFormat(\"subscript\")) {\n          editor?.dispatchCommand(FORMAT_TEXT_COMMAND, \"subscript\");\n        }\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n    {\n      name: \"formatSubscript\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        editor?.dispatchCommand(FORMAT_TEXT_COMMAND, \"subscript\");\n        // remove superscript if subscript is added\n        if (hasSelectionFormat(\"superscript\")) {\n          editor?.dispatchCommand(FORMAT_TEXT_COMMAND, \"superscript\");\n        }\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n    {\n      name: \"formatLink\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        if (hasSelectionFormat(\"link\")) {\n          editor?.dispatchCommand(TOGGLE_LINK_COMMAND, null);\n        } else {\n          editor?.dispatchCommand(TOGGLE_LINK_COMMAND, \"https://\");\n        }\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n    {\n      name: \"formatSpan\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        editor?.dispatchCommand(TOGGLE_SPAN_COMMAND, undefined);\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n    {\n      name: \"formatClear\",\n      hidden: true,\n      handler: () => {\n        const editor = getActiveEditor();\n        editor?.dispatchCommand(CLEAR_FORMAT_COMMAND, undefined);\n        requestAnimationFrame(() => editor?.focus());\n      },\n    },\n  ],\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/font-weight-support.ts",
    "content": "import type { FontWeight } from \"@webstudio-is/fonts\";\nimport { $detectedFontsWeights } from \"~/shared/nano-states\";\n\nconst fontWeights: Array<FontWeight> = [\n  \"100\",\n  \"200\",\n  \"300\",\n  \"400\",\n  \"500\",\n  \"600\",\n  \"700\",\n  \"800\",\n  \"900\",\n];\n\n// Render a font on canvas in each width and compare the amount of inked pixels.\nconst testFontWeights = (fontFamily: string) => {\n  const canvas = document.createElement(\"canvas\");\n  canvas.width = 200;\n  canvas.height = 20;\n  const context = canvas.getContext(\"2d\", { willReadFrequently: true });\n  const supportedWeights: Array<FontWeight> = [\"400\"];\n\n  if (context === null) {\n    return supportedWeights;\n  }\n\n  const weightWidthMap: Map<FontWeight, number> = new Map();\n\n  // Function to render text with a given font weight and measure \"inked\" pixels\n  // We can't just compare the text width using `context.measureText(..).width api` because many monospace\n  // fonts have the same width for all weights.\n  const measureInkCoverage = (fontWeight: string) => {\n    context.clearRect(0, 0, canvas.width, canvas.height);\n    context.font = `${fontWeight} 16px ${fontFamily}`;\n    context.fillText(\"abcdefgsw1234567890\", 0, 16);\n\n    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);\n    const pixels = imageData.data;\n\n    let inkedPixels = 0;\n    // Loop through all pixel data and count non-transparent pixels.\n    for (let index = 3; index < pixels.length; index += 4) {\n      if (pixels[index] > 0) {\n        inkedPixels++;\n      }\n    }\n\n    return inkedPixels;\n  };\n\n  for (const testWeight of fontWeights) {\n    weightWidthMap.set(\n      testWeight as FontWeight,\n      measureInkCoverage(testWeight)\n    );\n  }\n\n  const compare = (weights: Array<FontWeight>) => {\n    const supported: Array<FontWeight> = [];\n    for (let index = 0; index < weights.length; index++) {\n      const referenceWeight = weights[index];\n      const testWeight = weights[index + 1];\n      const referenceWidth = weightWidthMap.get(referenceWeight);\n      const testWidth = weightWidthMap.get(testWeight);\n      // If next width is the same as the previous one, it means the weight is not supported\n      if (testWeight && testWidth !== referenceWidth) {\n        supported.push(testWeight);\n      }\n    }\n    return supported;\n  };\n\n  // 400 is the baseline that's always supported, from there we test in both directions\n  // each step must result in a different width to be considered supported\n  // if for e.g. 400-300 is compared and has not change in width, it means 300 is not supported\n  // then we can then compare 300-200 and so on.\n  supportedWeights.push(...compare([\"400\", \"300\", \"200\", \"100\"]));\n  supportedWeights.push(...compare([\"400\", \"500\", \"600\", \"700\", \"800\", \"900\"]));\n\n  return supportedWeights.sort();\n};\n\nexport const subscribeFontLoadingDone = ({\n  signal,\n}: {\n  signal: AbortSignal;\n}) => {\n  document.fonts.addEventListener(\n    \"loadingdone\",\n    () => {\n      const cache = new Map($detectedFontsWeights.get());\n      // We need to re-detect all fonts because we don't know which fonts were loaded.\n      for (const [stack] of cache) {\n        const supportedWeights = testFontWeights(stack);\n        cache.set(stack, supportedWeights);\n      }\n      $detectedFontsWeights.set(cache);\n    },\n    { signal }\n  );\n};\n\nexport const detectSupportedFontWeights = (stack: string) => {\n  // Delaying it to potentially have less work done in the previous frame.\n  requestAnimationFrame(() => {\n    const cache = new Map($detectedFontsWeights.get());\n    // Detecting immediately in case its a font that is already loaded, otheriwese\n    // it will be detected correctly when the font is loaded and in the meantime the detected\n    // value may be incorrect due to the fallback font.\n    const supportedWeights = testFontWeights(stack);\n    cache.set(stack, supportedWeights);\n    $detectedFontsWeights.set(cache);\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/inert.ts",
    "content": "let resetTimeoutHandle: number | undefined = undefined;\n\nconst resetAutoDisposeInert = () => {\n  if (resetTimeoutHandle === undefined) {\n    return;\n  }\n  document.body.removeAttribute(\"inert\");\n  clearTimeout(resetTimeoutHandle);\n  resetTimeoutHandle = undefined;\n};\n\nlet lastPointerEventTime = Date.now();\n// 1000 ms is a reasonable time for the preview to reset.\n// Anyway should never happen after user has finished preview changes (can happen during preview changes)\nconst AUTO_DISPOSE_INERT_TIMEOUT = 1000;\n\n// A brief delay to ensure mutation observers within the focus scope are activated by the preview changes.\nconst DISPOSE_INERT_TIMEOUT = 300;\n\nconst PREVENT_INERT_TIMEOUT = 100;\n\nconst setAutoDisposeInert = (timeout: number) => {\n  // Some events in the builder can occur after clicking on the canvas (e.g., blur on an input field).\n  // In such cases, we should prevent 'inert' from being set and allow the selection to complete.\n  if (Date.now() - lastPointerEventTime < PREVENT_INERT_TIMEOUT) {\n    return;\n  }\n\n  document.body.setAttribute(\"inert\", \"true\");\n\n  // To prevent a completely non-interactive canvas due to edge cases,\n  // make sure to clean up preview changes if preview styles fail to reset correctly.\n  clearTimeout(resetTimeoutHandle);\n\n  resetTimeoutHandle = window.setTimeout(resetAutoDisposeInert, timeout);\n};\n\n/**\n * Controls (e.g., radix focus scope) may inadvertently shift focus from inputs.\n * Example: When the user modifies styles or content in the settings panel, the use of a mutation observer with Radix causes the focus to shift to the Radix dialog.\n * Currently, there's no way to block focus shifts inside iframes (see https:*github.com/w3c/webappsec-permissions-policy/issues/273 for future updates).\n * Workaround: use the `inert` attribute on iframe body to prevent focus changes.\n */\nexport const setInert = () => setAutoDisposeInert(AUTO_DISPOSE_INERT_TIMEOUT);\nexport const resetInert = () => setAutoDisposeInert(DISPOSE_INERT_TIMEOUT);\n\n// window.self !== window.top means we are on canvas\nif (typeof window !== \"undefined\" && window.self !== window.top) {\n  window.addEventListener(\"pointerdown\", () => {\n    lastPointerEventTime = Date.now();\n  });\n}\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/routing-priority.test.ts",
    "content": "import { test, expect } from \"vitest\";\nimport { comparePatterns } from \"./routing-priority\";\n\ntest(\"sort top-level patterns\", () => {\n  const patterns = [\"/foo*\", \"/:foo\", \"/foo\"];\n  const expected = [\"/foo\", \"/:foo\", \"/foo*\"];\n  expect(patterns.toSorted(comparePatterns)).toEqual(expected);\n});\n\ntest(\"sort static paths\", () => {\n  const patterns = [\"/a/z\", \"/a/b/c\", \"/a/c\", \"/a/b\"];\n  const expected = [\"/a/b\", \"/a/c\", \"/a/z\", \"/a/b/c\"];\n  expect(patterns.toSorted(comparePatterns)).toEqual(expected);\n});\n\ntest(\"sort mixed static, dynamic, spread at multiple levels\", () => {\n  const patterns = [\n    \"/foo\",\n    \"/:id\",\n    \"/bar*\",\n    \"/foo/bar\",\n    \"/foo/:id\",\n    \"/foo/bar*\",\n  ];\n  const expected = [\n    // static first-segment\n    \"/foo\",\n    \"/foo/bar\",\n    \"/foo/:id\",\n    \"/foo/bar*\",\n    // dynamic then spread at top level\n    \"/:id\",\n    \"/bar*\",\n  ];\n  expect(patterns.toSorted(comparePatterns)).toEqual(expected);\n});\n\ntest(\"sort deeply nested mixed segments\", () => {\n  const patterns = [\"/u/bar\", \"/u/:id\", \"/u/bar/b\", \"/u/:id/c\", \"/u/bar/*\"];\n  const expected = [\n    // static second-segment\n    \"/u/bar\",\n    \"/u/bar/b\",\n    \"/u/bar/*\",\n    // dynamic second-segment\n    \"/u/:id\",\n    \"/u/:id/c\",\n  ];\n  expect(patterns.toSorted(comparePatterns)).toEqual(expected);\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/routing-priority.ts",
    "content": "const STATIC = 1;\nconst DYNAMIC = 2;\nconst SPREAD = 3;\n\nconst getSegmentScore = (segment: string) => {\n  // give spread the least priority\n  if (segment.endsWith(\"*\")) {\n    return SPREAD;\n  }\n  // sort dynamic segments before splat\n  if (segment.startsWith(\":\")) {\n    return DYNAMIC;\n  }\n  // sort static routes before dynamic routes\n  return STATIC;\n};\n\nexport const comparePatterns = (leftPattern: string, rightPattern: string) => {\n  const leftSegments = leftPattern.split(\"/\");\n  const rightSegments = rightPattern.split(\"/\");\n  const commonLength = Math.min(leftSegments.length, rightSegments.length);\n\n  // compare each segment first\n  for (let index = 0; index < commonLength; index++) {\n    const leftScore = getSegmentScore(leftSegments[index]);\n    const rightScore = getSegmentScore(rightSegments[index]);\n    if (leftScore !== rightScore) {\n      return leftScore - rightScore;\n    }\n  }\n\n  // compare amount of segments\n  const leftLength = leftSegments.length;\n  const rightLength = rightSegments.length;\n  if (leftLength !== rightLength) {\n    return leftLength - rightLength;\n  }\n\n  // sort alphabetically\n  return leftPattern.localeCompare(rightPattern);\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/scroll-new-instance-into-view.ts",
    "content": "import { idAttribute } from \"@webstudio-is/react-sdk\";\nimport type { Instances } from \"@webstudio-is/sdk\";\nimport { $instances } from \"~/shared/sync/data-stores\";\n\n/**\n * When a new instance is added to the canvas, scroll it into view.\n */\nexport const subscribeScrollNewInstanceIntoView = (\n  debounceEffect: (callback: () => void) => void,\n  previousInstances: { current?: Instances },\n  signal: AbortSignal\n) => {\n  const unsubscribe = $instances.subscribe((instances) => {\n    if (previousInstances.current === undefined) {\n      previousInstances.current = instances;\n      return;\n    }\n    let newInstanceId: string | undefined;\n    for (const [id] of instances) {\n      if (previousInstances.current.has(id)) {\n        continue;\n      }\n      newInstanceId = id;\n      break;\n    }\n    if (newInstanceId === undefined) {\n      return;\n    }\n    previousInstances.current = instances;\n    debounceEffect(() => {\n      const element = document.querySelector(\n        `[${idAttribute}=\"${newInstanceId}\"]`\n      );\n      element?.scrollIntoView({\n        behavior: \"smooth\",\n        block: \"nearest\",\n      });\n    });\n  });\n  signal.addEventListener(\"abort\", unsubscribe);\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/scroll-state.ts",
    "content": "import { createNanoEvents } from \"nanoevents\";\n\n// Using a JS emitter to avoid overhead with subscribing scroll event directly on the DOM by many listeners\nconst emitter = createNanoEvents<{\n  scrollStart: () => void;\n  scroll: () => void;\n  scrollEnd: () => void;\n}>();\n\nif (typeof window === \"object\") {\n  const eventOptions = {\n    passive: true,\n    capture: true,\n  };\n\n  let timeoutId = 0;\n  let isScrolling = false;\n\n  window.addEventListener(\n    \"scroll\",\n    () => {\n      if (isScrolling === false) {\n        emitter.emit(\"scrollStart\");\n      }\n      emitter.emit(\"scroll\");\n      isScrolling = true;\n      clearTimeout(timeoutId);\n      timeoutId = window.setTimeout(() => {\n        if (isScrolling === false) {\n          return;\n        }\n        isScrolling = false;\n        emitter.emit(\"scrollEnd\");\n      }, 150);\n    },\n    eventOptions\n  );\n}\n\ntype UseScrollState = {\n  onScroll?: () => void;\n  onScrollStart?: () => void;\n  onScrollEnd?: () => void;\n};\n\nconst noop = () => {};\n\n/**\n * Scroll state abstraction that can handle a lot of subscribers well.\n * Potentially could add rate limiting and actual scroll top/left values.\n */\nexport const subscribeScrollState = ({\n  onScroll = noop,\n  onScrollStart = noop,\n  onScrollEnd = noop,\n}: UseScrollState) => {\n  const unsubscribeScrollStart = emitter.on(\"scrollStart\", onScrollStart);\n  const unsubscribeScroll = emitter.on(\"scroll\", onScroll);\n  const unsubscribeScrollEnd = emitter.on(\"scrollEnd\", onScrollEnd);\n\n  return () => {\n    unsubscribeScrollStart();\n    unsubscribeScroll();\n    unsubscribeScrollEnd();\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/styles.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport type { Instance, StyleDecl } from \"@webstudio-is/sdk\";\nimport {\n  descendantComponent,\n  ROOT_INSTANCE_ID,\n  rootComponent,\n} from \"@webstudio-is/sdk\";\nimport {\n  createRegularStyleSheet,\n  FakeStyleElement,\n} from \"@webstudio-is/css-engine\";\n\n// polyfill CSS.escape for Node environment\nimport \"css.escape\";\n\nimport { __testing__ } from \"./styles\";\n\nconst {\n  getEphemeralProperty,\n  getInstanceSelector,\n  getPresetStyleSelector,\n  computeDescendantSelectors,\n  computeInstanceStyles,\n  computeEditableCursorRules,\n  computeStylesDiff,\n  toDeclarationParams,\n  toVarValue,\n  hasExpressionChildren,\n  renderStateStyles,\n  simulateConditionBreakpoints,\n} = __testing__;\n\nconst createTestSheet = () =>\n  createRegularStyleSheet({ element: new FakeStyleElement() });\n\nconst mockTransformValue = () => undefined;\n\n// Simple passthrough - just return the style value\nconst mockToStyleValue = (styleDecl: Pick<StyleDecl, \"value\">) =>\n  styleDecl.value;\n\nconst createStyleDecl = (overrides: Partial<StyleDecl> = {}): StyleDecl => ({\n  styleSourceId: \"style-source-1\",\n  breakpointId: \"base\",\n  property: \"color\",\n  value: { type: \"keyword\", value: \"red\" },\n  ...overrides,\n});\n\ndescribe(\"renderStateStyles\", () => {\n  test(\"clears sheet when instanceStyles is undefined\", () => {\n    const sheet = createTestSheet();\n    sheet.addMediaRule(\"base\");\n    const rule = sheet.addNestingRule('[data-ws-id=\"old\"]');\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    renderStateStyles({\n      instanceStyles: undefined,\n      sheet,\n      transformValue: mockTransformValue,\n      toStyleValue: mockToStyleValue,\n    });\n    expect(sheet.cssText).toBe(\"\");\n  });\n\n  describe(\"pseudo-elements keep their selector\", () => {\n    test(\"::before styles apply to ::before selector\", () => {\n      const sheet = createTestSheet();\n      renderStateStyles({\n        instanceStyles: {\n          instanceId: \"box\",\n          selectedState: \"::before\",\n          breakpoints: [{ id: \"base\", label: \"Base\" }],\n          styles: [\n            createStyleDecl({\n              property: \"content\",\n              value: { type: \"keyword\", value: '\"\"' },\n            }),\n          ],\n        },\n        sheet,\n        transformValue: mockTransformValue,\n        toStyleValue: mockToStyleValue,\n      });\n      expect(sheet.cssText).toContain(\"::before\");\n      expect(sheet.cssText).toContain('content: \"\"');\n    });\n\n    test(\"::after styles apply to ::after selector\", () => {\n      const sheet = createTestSheet();\n      renderStateStyles({\n        instanceStyles: {\n          instanceId: \"box\",\n          selectedState: \"::after\",\n          breakpoints: [{ id: \"base\", label: \"Base\" }],\n          styles: [\n            createStyleDecl({\n              property: \"content\",\n              value: { type: \"keyword\", value: '\"\"' },\n            }),\n          ],\n        },\n        sheet,\n        transformValue: mockTransformValue,\n        toStyleValue: mockToStyleValue,\n      });\n      expect(sheet.cssText).toContain(\"::after\");\n    });\n\n    test(\"::placeholder keeps selector\", () => {\n      const sheet = createTestSheet();\n      renderStateStyles({\n        instanceStyles: {\n          instanceId: \"input\",\n          selectedState: \"::placeholder\",\n          breakpoints: [{ id: \"base\", label: \"Base\" }],\n          styles: [createStyleDecl()],\n        },\n        sheet,\n        transformValue: mockTransformValue,\n        toStyleValue: mockToStyleValue,\n      });\n      expect(sheet.cssText).toContain(\"::placeholder\");\n    });\n  });\n\n  describe(\"pseudo-classes render without state for preview\", () => {\n    test(\":hover styles apply without :hover selector\", () => {\n      const sheet = createTestSheet();\n      renderStateStyles({\n        instanceStyles: {\n          instanceId: \"button\",\n          selectedState: \":hover\",\n          breakpoints: [{ id: \"base\", label: \"Base\" }],\n          styles: [\n            createStyleDecl({\n              property: \"backgroundColor\",\n              value: { type: \"keyword\", value: \"blue\" },\n            }),\n          ],\n        },\n        sheet,\n        transformValue: mockTransformValue,\n        toStyleValue: mockToStyleValue,\n      });\n      expect(sheet.cssText).not.toContain(\":hover\");\n      expect(sheet.cssText).toContain(\"background-color: blue\");\n    });\n\n    test(\":focus styles apply without :focus selector\", () => {\n      const sheet = createTestSheet();\n      renderStateStyles({\n        instanceStyles: {\n          instanceId: \"input\",\n          selectedState: \":focus\",\n          breakpoints: [{ id: \"base\", label: \"Base\" }],\n          styles: [createStyleDecl()],\n        },\n        sheet,\n        transformValue: mockTransformValue,\n        toStyleValue: mockToStyleValue,\n      });\n      expect(sheet.cssText).not.toContain(\":focus\");\n    });\n\n    test(\":active styles apply without :active selector\", () => {\n      const sheet = createTestSheet();\n      renderStateStyles({\n        instanceStyles: {\n          instanceId: \"button\",\n          selectedState: \":active\",\n          breakpoints: [{ id: \"base\", label: \"Base\" }],\n          styles: [createStyleDecl()],\n        },\n        sheet,\n        transformValue: mockTransformValue,\n        toStyleValue: mockToStyleValue,\n      });\n      expect(sheet.cssText).not.toContain(\":active\");\n    });\n  });\n\n  test(\"creates nesting rule with correct instance selector\", () => {\n    const sheet = createTestSheet();\n    renderStateStyles({\n      instanceStyles: {\n        instanceId: \"my-instance-123\",\n        selectedState: \":hover\",\n        breakpoints: [{ id: \"base\", label: \"Base\" }],\n        styles: [createStyleDecl()],\n      },\n      sheet,\n      transformValue: mockTransformValue,\n      toStyleValue: mockToStyleValue,\n    });\n    expect(sheet.cssText).toContain('[data-ws-id=\"my-instance-123\"]');\n  });\n\n  test(\"renders multiple style declarations\", () => {\n    const sheet = createTestSheet();\n    renderStateStyles({\n      instanceStyles: {\n        instanceId: \"box\",\n        selectedState: \"::before\",\n        breakpoints: [{ id: \"base\", label: \"Base\" }],\n        styles: [\n          createStyleDecl({\n            property: \"content\",\n            value: { type: \"keyword\", value: '\"\"' },\n          }),\n          createStyleDecl({\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          }),\n          createStyleDecl({\n            property: \"fontSize\",\n            value: { type: \"unit\", unit: \"px\", value: 16 },\n          }),\n        ],\n      },\n      sheet,\n      transformValue: mockTransformValue,\n      toStyleValue: mockToStyleValue,\n    });\n    expect(sheet.cssText).toContain('content: \"\"');\n    expect(sheet.cssText).toContain(\"color: red\");\n    expect(sheet.cssText).toContain(\"font-size: 16px\");\n  });\n});\n\ndescribe(\"getEphemeralProperty\", () => {\n  test(\"generates property name from styleSourceId, state, and property\", () => {\n    expect(\n      getEphemeralProperty({\n        styleSourceId: \"source-1\",\n        state: \":hover\",\n        property: \"color\",\n      })\n    ).toBe(\"--source-1-:hover-color\");\n  });\n\n  test(\"handles missing state as empty string\", () => {\n    expect(\n      getEphemeralProperty({\n        styleSourceId: \"source-1\",\n        property: \"backgroundColor\",\n      })\n    ).toBe(\"--source-1--backgroundColor\");\n  });\n\n  test(\"handles pseudo-element state\", () => {\n    expect(\n      getEphemeralProperty({\n        styleSourceId: \"abc\",\n        state: \"::before\",\n        property: \"content\",\n      })\n    ).toBe(\"--abc-::before-content\");\n  });\n});\n\ndescribe(\"computeDescendantSelectors\", () => {\n  const createInstance = (\n    id: string,\n    component: string,\n    children: Instance[\"children\"] = []\n  ): Instance => ({\n    type: \"instance\",\n    id,\n    component,\n    children,\n  });\n\n  const createProp = (\n    id: string,\n    instanceId: string,\n    name: string,\n    value: string\n  ) => ({\n    id,\n    instanceId,\n    name,\n    type: \"string\" as const,\n    value,\n  });\n\n  test(\"returns empty map when no descendant components\", () => {\n    const instances = new Map([\n      [\"box\", createInstance(\"box\", \"Box\", [{ type: \"id\", value: \"text\" }])],\n      [\"text\", createInstance(\"text\", \"Text\")],\n    ]);\n    const props = new Map();\n    expect(computeDescendantSelectors(instances, props)).toEqual(new Map());\n  });\n\n  test(\"computes selector for descendant component with parent and selector prop\", () => {\n    const instances = new Map([\n      [\n        \"parent\",\n        createInstance(\"parent\", \"Box\", [{ type: \"id\", value: \"descendant\" }]),\n      ],\n      [\"descendant\", createInstance(\"descendant\", descendantComponent)],\n    ]);\n    const props = new Map([\n      [\"prop-1\", createProp(\"prop-1\", \"descendant\", \"selector\", \" > div\")],\n    ]);\n    const result = computeDescendantSelectors(instances, props);\n    expect(result.get(\"descendant\")).toBe('[data-ws-id=\"parent\"] > div');\n  });\n\n  test(\"ignores descendant without selector prop\", () => {\n    const instances = new Map([\n      [\n        \"parent\",\n        createInstance(\"parent\", \"Box\", [{ type: \"id\", value: \"descendant\" }]),\n      ],\n      [\"descendant\", createInstance(\"descendant\", descendantComponent)],\n    ]);\n    const props = new Map();\n    const result = computeDescendantSelectors(instances, props);\n    expect(result.has(\"descendant\")).toBe(false);\n  });\n\n  test(\"ignores descendant without parent\", () => {\n    const instances = new Map([\n      [\"descendant\", createInstance(\"descendant\", descendantComponent)],\n    ]);\n    const props = new Map([\n      [\"prop-1\", createProp(\"prop-1\", \"descendant\", \"selector\", \" > div\")],\n    ]);\n    const result = computeDescendantSelectors(instances, props);\n    expect(result.has(\"descendant\")).toBe(false);\n  });\n\n  test(\"handles multiple descendants\", () => {\n    const instances = new Map([\n      [\n        \"parent\",\n        createInstance(\"parent\", \"Box\", [\n          { type: \"id\", value: \"desc1\" },\n          { type: \"id\", value: \"desc2\" },\n        ]),\n      ],\n      [\"desc1\", createInstance(\"desc1\", descendantComponent)],\n      [\"desc2\", createInstance(\"desc2\", descendantComponent)],\n    ]);\n    const props = new Map([\n      [\"p1\", createProp(\"p1\", \"desc1\", \"selector\", \" .item\")],\n      [\"p2\", createProp(\"p2\", \"desc2\", \"selector\", \" a\")],\n    ]);\n    const result = computeDescendantSelectors(instances, props);\n    expect(result.get(\"desc1\")).toBe('[data-ws-id=\"parent\"] .item');\n    expect(result.get(\"desc2\")).toBe('[data-ws-id=\"parent\"] a');\n  });\n});\n\ndescribe(\"computeInstanceStyles\", () => {\n  const createInstance = (id: string): Instance => ({\n    type: \"instance\",\n    id,\n    component: \"Box\",\n    children: [],\n  });\n\n  test(\"returns undefined when selectedInstance is undefined\", () => {\n    expect(\n      computeInstanceStyles({\n        selectedInstance: undefined,\n        selectedStyleState: \":hover\",\n        breakpoints: new Map(),\n        styleSourceSelections: new Map(),\n        styles: new Map(),\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"returns undefined when selectedStyleState is undefined\", () => {\n    expect(\n      computeInstanceStyles({\n        selectedInstance: createInstance(\"box\"),\n        selectedStyleState: undefined,\n        breakpoints: new Map(),\n        styleSourceSelections: new Map(),\n        styles: new Map(),\n      })\n    ).toBeUndefined();\n  });\n\n  test(\"filters styles by selected state and instance style sources\", () => {\n    const result = computeInstanceStyles({\n      selectedInstance: createInstance(\"box\"),\n      selectedStyleState: \":hover\",\n      breakpoints: new Map([[\"base\", { id: \"base\", label: \"Base\" }]]),\n      styleSourceSelections: new Map([\n        [\"box\", { values: [\"local\", \"token-1\"] }],\n      ]),\n      styles: new Map([\n        [\n          \"key1\",\n          createStyleDecl({\n            styleSourceId: \"local\",\n            breakpointId: \"base\",\n            state: \":hover\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          }),\n        ],\n        [\n          \"key2\",\n          createStyleDecl({\n            styleSourceId: \"local\",\n            breakpointId: \"base\",\n            property: \"fontSize\",\n            value: { type: \"unit\", unit: \"px\", value: 16 },\n          }),\n        ],\n        [\n          \"key3\",\n          createStyleDecl({\n            styleSourceId: \"other\",\n            breakpointId: \"base\",\n            state: \":hover\",\n            property: \"marginTop\",\n            value: { type: \"unit\", unit: \"px\", value: 0 },\n          }),\n        ],\n      ]),\n    });\n    expect(result).toEqual({\n      instanceId: \"box\",\n      selectedState: \":hover\",\n      breakpoints: [{ id: \"base\", label: \"Base\" }],\n      styles: [\n        {\n          styleSourceId: \"local\",\n          breakpointId: \"base\",\n          state: \":hover\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n    });\n  });\n\n  test(\"includes styles from multiple style sources\", () => {\n    const result = computeInstanceStyles({\n      selectedInstance: createInstance(\"box\"),\n      selectedStyleState: \"::before\",\n      breakpoints: new Map([[\"base\", { id: \"base\", label: \"Base\" }]]),\n      styleSourceSelections: new Map([\n        [\"box\", { values: [\"local\", \"token-1\"] }],\n      ]),\n      styles: new Map([\n        [\n          \"key1\",\n          createStyleDecl({\n            styleSourceId: \"local\",\n            breakpointId: \"base\",\n            state: \"::before\",\n            property: \"content\",\n            value: { type: \"keyword\", value: '\"\"' },\n          }),\n        ],\n        [\n          \"key2\",\n          createStyleDecl({\n            styleSourceId: \"token-1\",\n            breakpointId: \"base\",\n            state: \"::before\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"blue\" },\n          }),\n        ],\n      ]),\n    });\n    expect(result?.styles).toHaveLength(2);\n  });\n});\n\ndescribe(\"computeEditableCursorRules\", () => {\n  const createInstance = (\n    id: string,\n    children: Instance[\"children\"] = []\n  ): Instance => ({\n    id,\n    type: \"instance\",\n    component: \"Box\",\n    children,\n  });\n\n  test(\"returns empty array when no selectors\", () => {\n    const result = computeEditableCursorRules([], new Map());\n    expect(result).toEqual([]);\n  });\n\n  test(\"generates cursor rule for editable instances\", () => {\n    const instances = new Map<string, Instance>([\n      [\"inst-1\", createInstance(\"inst-1\")],\n      [\"inst-2\", createInstance(\"inst-2\")],\n    ]);\n    const selectors = [[\"inst-1\"], [\"inst-2\"]] as [string][];\n    const result = computeEditableCursorRules(selectors, instances);\n    expect(result).toHaveLength(1);\n    expect(result[0]).toContain('[data-ws-id=\"inst-1\"]');\n    expect(result[0]).toContain('[data-ws-id=\"inst-2\"]');\n    expect(result[0]).toContain(\"cursor: text\");\n  });\n\n  test(\"excludes instances with expression children\", () => {\n    const instances = new Map<string, Instance>([\n      [\"inst-1\", createInstance(\"inst-1\")],\n      [\n        \"inst-2\",\n        createInstance(\"inst-2\", [{ type: \"expression\", value: \"someExpr\" }]),\n      ],\n    ]);\n    const selectors = [[\"inst-1\"], [\"inst-2\"]] as [string][];\n    const result = computeEditableCursorRules(selectors, instances);\n    expect(result).toHaveLength(1);\n    expect(result[0]).toContain('[data-ws-id=\"inst-1\"]');\n    expect(result[0]).not.toContain('[data-ws-id=\"inst-2\"]');\n  });\n\n  test(\"excludes instances not found in map\", () => {\n    const instances = new Map<string, Instance>([\n      [\"inst-1\", createInstance(\"inst-1\")],\n    ]);\n    const selectors = [[\"inst-1\"], [\"inst-missing\"]] as [string][];\n    const result = computeEditableCursorRules(selectors, instances);\n    expect(result).toHaveLength(1);\n    expect(result[0]).toContain('[data-ws-id=\"inst-1\"]');\n    expect(result[0]).not.toContain(\"inst-missing\");\n  });\n\n  test(\"chunks selectors into groups of 20\", () => {\n    const instances = new Map<string, Instance>();\n    const selectors: [string][] = [];\n    for (let i = 0; i < 25; i++) {\n      const id = `inst-${i}`;\n      instances.set(id, createInstance(id));\n      selectors.push([id]);\n    }\n    const result = computeEditableCursorRules(selectors, instances);\n    expect(result).toHaveLength(2);\n  });\n\n  test(\"skips empty chunks\", () => {\n    const instances = new Map<string, Instance>();\n    // All instances have expression children, so all chunks are empty\n    for (let i = 0; i < 5; i++) {\n      const id = `inst-${i}`;\n      instances.set(\n        id,\n        createInstance(id, [{ type: \"expression\", value: \"expr\" }])\n      );\n    }\n    const selectors = Array.from(instances.keys()).map((id) => [id]) as [\n      string,\n    ][];\n    const result = computeEditableCursorRules(selectors, instances);\n    expect(result).toEqual([]);\n  });\n});\n\ndescribe(\"computeStylesDiff\", () => {\n  const style1 = createStyleDecl({\n    styleSourceId: \"source-1\",\n    breakpointId: \"base\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"red\" },\n  });\n  const style2 = createStyleDecl({\n    styleSourceId: \"source-2\",\n    breakpointId: \"base\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"flex\" },\n  });\n  const mockTransform = () => undefined;\n\n  test(\"returns all styles as added when prev is empty\", () => {\n    const styles = new Map([\n      [\"key1\", style1],\n      [\"key2\", style2],\n    ]);\n    const result = computeStylesDiff({\n      styles,\n      transformValue: mockTransform,\n      prevStylesSet: new Set(),\n      prevTransformValue: mockTransform,\n    });\n    expect(result.addedStyles.size).toBe(2);\n    expect(result.deletedStyles.size).toBe(0);\n    expect(result.nextPrevStylesSet.size).toBe(2);\n  });\n\n  test(\"returns removed styles as deleted\", () => {\n    const prevStyles = new Set([style1, style2]);\n    const styles = new Map([[\"key1\", style1]]);\n    const result = computeStylesDiff({\n      styles,\n      transformValue: mockTransform,\n      prevStylesSet: prevStyles,\n      prevTransformValue: mockTransform,\n    });\n    expect(result.addedStyles.size).toBe(0);\n    expect(result.deletedStyles.size).toBe(1);\n    expect(result.deletedStyles.has(style2)).toBe(true);\n  });\n\n  test(\"returns new styles as added and removed as deleted\", () => {\n    const style3 = createStyleDecl({\n      styleSourceId: \"source-3\",\n      breakpointId: \"base\",\n      property: \"width\",\n      value: { type: \"unit\", value: 100, unit: \"px\" },\n    });\n    const prevStyles = new Set([style1, style2]);\n    const styles = new Map([\n      [\"key1\", style1],\n      [\"key3\", style3],\n    ]);\n    const result = computeStylesDiff({\n      styles,\n      transformValue: mockTransform,\n      prevStylesSet: prevStyles,\n      prevTransformValue: mockTransform,\n    });\n    expect(result.addedStyles.size).toBe(1);\n    expect(result.addedStyles.has(style3)).toBe(true);\n    expect(result.deletedStyles.size).toBe(1);\n    expect(result.deletedStyles.has(style2)).toBe(true);\n  });\n\n  test(\"invalidates cache when transformValue changes\", () => {\n    const newTransform = () => undefined;\n    const prevStyles = new Set([style1]);\n    const styles = new Map([\n      [\"key1\", style1],\n      [\"key2\", style2],\n    ]);\n    const result = computeStylesDiff({\n      styles,\n      transformValue: newTransform,\n      prevStylesSet: prevStyles,\n      prevTransformValue: mockTransform, // different reference\n    });\n    // All styles are \"added\" because cache was invalidated\n    expect(result.addedStyles.size).toBe(2);\n    expect(result.deletedStyles.size).toBe(0);\n  });\n\n  test(\"no changes when styles are identical\", () => {\n    const prevStyles = new Set([style1, style2]);\n    const styles = new Map([\n      [\"key1\", style1],\n      [\"key2\", style2],\n    ]);\n    const result = computeStylesDiff({\n      styles,\n      transformValue: mockTransform,\n      prevStylesSet: prevStyles,\n      prevTransformValue: mockTransform,\n    });\n    expect(result.addedStyles.size).toBe(0);\n    expect(result.deletedStyles.size).toBe(0);\n  });\n});\n\ndescribe(\"toDeclarationParams\", () => {\n  test(\"converts StyleDecl to declaration params\", () => {\n    const styleDecl = createStyleDecl({\n      breakpointId: \"tablet\",\n      state: \":hover\",\n      property: \"backgroundColor\",\n    });\n    const result = toDeclarationParams(styleDecl);\n    expect(result).toEqual({\n      breakpoint: \"tablet\",\n      selector: \":hover\",\n      property: \"backgroundColor\",\n    });\n  });\n\n  test(\"converts undefined state to empty string\", () => {\n    const styleDecl = createStyleDecl({\n      breakpointId: \"base\",\n      state: undefined,\n      property: \"color\",\n    });\n    const result = toDeclarationParams(styleDecl);\n    expect(result).toEqual({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"color\",\n    });\n  });\n\n  test(\"handles pseudo-element state\", () => {\n    const styleDecl = createStyleDecl({\n      breakpointId: \"base\",\n      state: \"::before\",\n      property: \"content\",\n    });\n    const result = toDeclarationParams(styleDecl);\n    expect(result).toEqual({\n      breakpoint: \"base\",\n      selector: \"::before\",\n      property: \"content\",\n    });\n  });\n});\n\ndescribe(\"getInstanceSelector\", () => {\n  test(\"returns :root for ROOT_INSTANCE_ID\", () => {\n    expect(getInstanceSelector(ROOT_INSTANCE_ID)).toBe(\":root\");\n  });\n\n  test(\"returns attribute selector for regular instance ID\", () => {\n    expect(getInstanceSelector(\"inst-123\")).toBe('[data-ws-id=\"inst-123\"]');\n  });\n\n  test(\"returns attribute selector for empty string\", () => {\n    expect(getInstanceSelector(\"\")).toBe('[data-ws-id=\"\"]');\n  });\n});\n\ndescribe(\"getPresetStyleSelector\", () => {\n  test(\"returns :root for rootComponent\", () => {\n    expect(getPresetStyleSelector(rootComponent, \"div\")).toBe(\":root\");\n  });\n\n  test(\"returns :where selector for regular component\", () => {\n    expect(getPresetStyleSelector(\"Box\", \"div\")).toBe(\n      'div:where([data-ws-component=\"Box\"])'\n    );\n  });\n\n  test(\"uses tag in selector\", () => {\n    expect(getPresetStyleSelector(\"Button\", \"button\")).toBe(\n      'button:where([data-ws-component=\"Button\"])'\n    );\n  });\n\n  test(\"handles custom component names\", () => {\n    expect(getPresetStyleSelector(\"MyCustomComponent\", \"span\")).toBe(\n      'span:where([data-ws-component=\"MyCustomComponent\"])'\n    );\n  });\n});\n\ndescribe(\"hasExpressionChildren\", () => {\n  const createInstance = (\n    id: string,\n    children: Instance[\"children\"] = []\n  ): Instance => ({\n    id,\n    type: \"instance\",\n    component: \"Box\",\n    children,\n  });\n\n  test(\"returns false for instance with no children\", () => {\n    expect(hasExpressionChildren(createInstance(\"box\"))).toBe(false);\n  });\n\n  test(\"returns false for instance with only id children\", () => {\n    expect(\n      hasExpressionChildren(\n        createInstance(\"box\", [\n          { type: \"id\", value: \"child-1\" },\n          { type: \"id\", value: \"child-2\" },\n        ])\n      )\n    ).toBe(false);\n  });\n\n  test(\"returns false for instance with text children\", () => {\n    expect(\n      hasExpressionChildren(\n        createInstance(\"box\", [{ type: \"text\", value: \"hello\" }])\n      )\n    ).toBe(false);\n  });\n\n  test(\"returns true for instance with expression child\", () => {\n    expect(\n      hasExpressionChildren(\n        createInstance(\"box\", [{ type: \"expression\", value: \"someExpr\" }])\n      )\n    ).toBe(true);\n  });\n\n  test(\"returns true when expression is mixed with other children\", () => {\n    expect(\n      hasExpressionChildren(\n        createInstance(\"box\", [\n          { type: \"text\", value: \"hello\" },\n          { type: \"expression\", value: \"expr\" },\n          { type: \"id\", value: \"child\" },\n        ])\n      )\n    ).toBe(true);\n  });\n});\n\ndescribe(\"toVarValue\", () => {\n  test(\"returns var type with escaped property name\", () => {\n    const result = toVarValue(\n      createStyleDecl({\n        styleSourceId: \"source-1\",\n        state: \":hover\",\n        property: \"color\",\n      }),\n      mockTransformValue\n    );\n    expect(result.type).toBe(\"var\");\n    expect(result.value).toBe(CSS.escape(\"source-1-:hover-color\"));\n  });\n\n  test(\"uses style decl value as fallback by default\", () => {\n    const result = toVarValue(\n      createStyleDecl({\n        value: { type: \"keyword\", value: \"red\" },\n      }),\n      mockTransformValue\n    );\n    expect(result.fallback).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"uses explicit fallback when provided\", () => {\n    const result = toVarValue(\n      createStyleDecl({\n        value: { type: \"keyword\", value: \"red\" },\n      }),\n      mockTransformValue,\n      { type: \"keyword\", value: \"blue\" }\n    );\n    expect(result.fallback).toEqual({ type: \"keyword\", value: \"blue\" });\n  });\n\n  test(\"handles missing state\", () => {\n    const result = toVarValue(\n      createStyleDecl({\n        styleSourceId: \"local\",\n        state: undefined,\n        property: \"display\",\n      }),\n      mockTransformValue\n    );\n    expect(result.value).toBe(CSS.escape(\"local--display\"));\n  });\n\n  test(\"handles pseudo-element state\", () => {\n    const result = toVarValue(\n      createStyleDecl({\n        styleSourceId: \"abc\",\n        state: \"::before\",\n        property: \"content\",\n      }),\n      mockTransformValue\n    );\n    expect(result.value).toBe(CSS.escape(\"abc-::before-content\"));\n  });\n});\n\ndescribe(\"simulateConditionBreakpoints\", () => {\n  const base = { id: \"base\", label: \"Base\" };\n  const dark = {\n    id: \"dark\",\n    label: \"Dark\",\n    condition: \"prefers-color-scheme:dark\",\n  };\n  const light = {\n    id: \"light\",\n    label: \"Light\",\n    condition: \"prefers-color-scheme:light\",\n  };\n  const tablet = { id: \"tablet\", label: \"Tablet\", maxWidth: 991 };\n  const portrait = {\n    id: \"portrait\",\n    label: \"Portrait\",\n    condition: \"orientation:portrait\",\n  };\n\n  test(\"passes breakpoints through when no condition is selected\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark, tablet],\n      selectedBreakpoint: base,\n    });\n    expect(result).toEqual(\n      new Map([\n        [\"base\", base],\n        [\"dark\", dark],\n        [\"tablet\", tablet],\n      ])\n    );\n  });\n\n  test(\"simulates matching condition as all\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark, light],\n      selectedBreakpoint: dark,\n    });\n    expect(result.get(\"dark\")).toEqual({ mediaType: \"all\" });\n  });\n\n  test(\"simulates non-matching same-feature condition as not all\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark, light],\n      selectedBreakpoint: dark,\n    });\n    expect(result.get(\"light\")).toEqual({ mediaType: \"not all\" });\n  });\n\n  test(\"does not affect base breakpoint\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark, light],\n      selectedBreakpoint: dark,\n    });\n    expect(result.get(\"base\")).toEqual(base);\n  });\n\n  test(\"does not affect unrelated condition breakpoints\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark, portrait],\n      selectedBreakpoint: dark,\n    });\n    expect(result.get(\"portrait\")).toEqual(portrait);\n  });\n\n  test(\"does not affect width-based breakpoints\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark, tablet],\n      selectedBreakpoint: dark,\n    });\n    expect(result.get(\"tablet\")).toEqual(tablet);\n  });\n\n  test(\"handles undefined selected breakpoint\", () => {\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, dark],\n      selectedBreakpoint: undefined,\n    });\n    expect(result).toEqual(\n      new Map([\n        [\"base\", base],\n        [\"dark\", dark],\n      ])\n    );\n  });\n\n  test(\"handles breakpoint with empty string condition\", () => {\n    const emptyCondition = { id: \"empty\", label: \"Empty\", condition: \"\" };\n    const result = simulateConditionBreakpoints({\n      breakpoints: [base, emptyCondition, dark],\n      selectedBreakpoint: dark,\n    });\n    // Empty condition doesn't parse, so it passes through unchanged\n    expect(result.get(\"empty\")).toEqual(emptyCondition);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/styles.ts",
    "content": "import { useLayoutEffect } from \"react\";\nimport { computed } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Instance,\n  ROOT_INSTANCE_ID,\n  getStyleDeclKey,\n  descendantComponent,\n  rootComponent,\n  type Breakpoint,\n  type StyleDecl,\n  type StyleSourceSelection,\n  createImageValueTransformer,\n  addFontRules,\n} from \"@webstudio-is/sdk\";\nimport { inflatedAttribute, idAttribute } from \"@webstudio-is/react-sdk\";\nimport { INFLATE_PADDING } from \"~/canvas/inflator\";\nimport { isPseudoElement, parseMediaCondition } from \"@webstudio-is/css-data\";\nimport {\n  StyleValue,\n  type MediaRuleOptions,\n  type StyleSheetRegular,\n  type TransformValue,\n  type VarValue,\n  createRegularStyleSheet,\n  hyphenateProperty,\n  toValue,\n  toVarFallback,\n} from \"@webstudio-is/css-engine\";\nimport {\n  $assets,\n  $breakpoints,\n  $instances,\n  $props,\n  $registeredComponentMetas,\n  $selectedBreakpoint,\n  $selectedInstanceSelector,\n  $selectedStyleState,\n  $styleSourceSelections,\n  $styles,\n  assetBaseUrl,\n} from \"~/shared/nano-states\";\nimport { setDifference } from \"~/shared/shim\";\nimport { $ephemeralStyles } from \"../stores\";\nimport { canvasApi } from \"~/shared/canvas-api\";\nimport { $selectedInstance, $selectedPage } from \"~/shared/awareness\";\nimport { findAllEditableInstanceSelector } from \"~/shared/instance-utils\";\nimport type { InstanceSelector } from \"~/shared/tree-utils\";\nimport { getAllElementsByInstanceSelector } from \"~/shared/dom-utils\";\nimport { createComputedStyleDeclStore } from \"~/builder/features/style-panel/shared/model\";\n\nconst userSheet = createRegularStyleSheet({ name: \"user-styles\" });\nconst stateSheet = createRegularStyleSheet({ name: \"state-styles\" });\nconst helpersSheet = createRegularStyleSheet({ name: \"helpers\" });\nconst fontsAndDefaultsSheet = createRegularStyleSheet({\n  name: \"fonts-and-defaults\",\n});\nconst presetSheet = createRegularStyleSheet({ name: \"preset-styles\" });\n\n/**\n * maintain order of rendered stylesheets\n * should be invoked before any subscription\n */\nexport const mountStyles = () => {\n  fontsAndDefaultsSheet.render();\n  presetSheet.render();\n  userSheet.render();\n  stateSheet.render();\n  helpersSheet.render();\n};\n\nexport const editablePlaceholderAttribute = \"data-ws-editable-placeholder\";\n// @todo replace with modern typed attr() when supported in all browsers\n// see the second edge case\n// https://developer.mozilla.org/en-US/docs/Web/CSS/attr#backwards_compatibility\nexport const editingPlaceholderVariable = \"--ws-editing-placeholder\";\n\nconst hasExpressionChildren = (instance: Instance) =>\n  instance.children.some((child) => child.type === \"expression\");\n\nconst helperStylesShared = [\n  // Display a placeholder text for elements that are editable but currently empty\n  `:is([${editablePlaceholderAttribute}]):empty::before {\n    content: attr(${editablePlaceholderAttribute});\n    opacity: 0.3;\n  }\n  `,\n\n  // Display a placeholder text for elements that are editing but empty (Lexical adds p>br children)\n  `:is([${editablePlaceholderAttribute}])[contenteditable] > p:only-child:has(br:only-child) {\n    position: relative;\n    display: block;\n    &:after {\n      content: var(${editingPlaceholderVariable});\n      position: absolute;\n      left: 0;\n      right: 0;\n      top: 0;\n      /* Ensures placeholder text is visible even in narrow containers */\n      min-width: 100px;\n      opacity: 0.3;\n    }\n  }\n  `,\n\n  // Using :where allows to prevent increasing specificity, so that helper is overwritten by user styles.\n  `[${idAttribute}]:where([${inflatedAttribute}]:not(body)) {\n    outline: 1px dashed rgba(0,0,0,0.7);\n    outline-offset: -1px;\n    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.7);\n  }`,\n  // Has no width, will collapse\n  // Ensure the TextEditor element doesn't uncollapse unintentionally:\n  // 1. Some TextEditor elements might appear empty for a few cycles after creation. (depends on existance of initial child)\n  // 2. If detected as empty, the collapsing algorithm could wrongly uncollapse them.\n  // 3. We prevent this by excluding elements with `data-lexical-editor`.\n  // 4. This rule is used here and not in collapsing detection because `data-lexical-editor` might not be set instantly.\n  //    Mistakes in collapsing will be corrected in the next cycle.\n  // Grid containers use track-level minmax() inflation (applied inline by\n  // inflator.ts) instead of padding, so we skip padding for elements that\n  // have inline grid-template-columns or grid-template-rows (i.e. grids).\n  // Non-grid elements fall back to the default INFLATE_PADDING.\n  `[${idAttribute}]:where(:not(body):not([data-lexical-editor]):not([style*=\"grid-template\"])[${inflatedAttribute}=\"w\"]) {\n    padding-right: ${INFLATE_PADDING}px;\n  }`,\n  // Has no height, will collapse\n  `[${idAttribute}]:where(:not(body):not([data-lexical-editor]):not([style*=\"grid-template\"])[${inflatedAttribute}=\"h\"]) {\n    padding-top: ${INFLATE_PADDING}px;\n  }`,\n  // Has no width or height, will collapse\n  `[${idAttribute}]:where(:not(body):not([data-lexical-editor]):not([style*=\"grid-template\"])[${inflatedAttribute}=\"wh\"]) {\n    padding-right: ${INFLATE_PADDING}px;\n    padding-top: ${INFLATE_PADDING}px;\n  }`,\n  `[${idAttribute}][contenteditable], [${idAttribute}]:focus {\n    outline: 0;\n  }`,\n  `[${idAttribute}][contenteditable] {\n    box-shadow: 0 0 0 4px rgb(36 150 255 / 20%)\n  }`,\n];\n\n// Common user-select and cursor rules for canvas modes\nconst helperStylesUserSelect = [\n  // When double clicking into an element to edit text, it should not select the word.\n  `[${idAttribute}] {\n    user-select: none;\n    /* Safari */\n    -webkit-user-select: none;\n    cursor: default;\n  }`,\n  `[${idAttribute}][contenteditable] {\n    /* Safari */\n    cursor: initial;\n  }`,\n];\n\n// Helper styles on for canvas in design mode\n// - Only instances that would collapse without helper should receive helper\n// - Helper is removed when any CSS property is changed on that instance that would prevent collapsing, so that helper is not needed\n// - Helper doesn't show on the preview or publish\n// - Helper goes away if an instance inserted as a child\n// - There is no need to set padding-right or padding-bottom if you just need a small div with a defined or layout-based size, as soon as div is not collapsing, helper should not apply\n// - Padding will be only added on the side that would collapse otherwise\n//\n// For example when I add a div, it is a block element, it grows automatically full width but has 0 height, in this case spacing helper with padidng-top: 50px should apply, so that it doesn't collapse.\n// If user sets `height: 100px` or does anything that would give it a height - we remove the helper padding right away, so user can actually see the height they set\n//\n// In other words we prevent elements from collapsing when they have 0 height or width by making them non-zero on canvas, but then we remove those paddings as soon as element doesn't collapse.\nconst helperStyles = [...helperStylesUserSelect, ...helperStylesShared];\n\n// Find all editable elements and set cursor text inside\nconst helperStylesContentEdit = [\n  ...helperStylesUserSelect,\n  ...helperStylesShared,\n];\n\nconst subscribeDesignModeHelperStyles = () => {\n  helpersSheet.setAttribute(\"media\", \"all\");\n\n  for (const style of helperStyles) {\n    helpersSheet.addPlaintextRule(style);\n  }\n  helpersSheet.render();\n\n  return () => {\n    helpersSheet.clear();\n    helpersSheet.render();\n  };\n};\n\n/**\n * Compute CSS rules for editable elements in content edit mode.\n * Groups selectors into chunks to avoid overly long :is() selectors.\n */\nconst computeEditableCursorRules = (\n  editableInstanceSelectors: InstanceSelector[],\n  instances: Map<Instance[\"id\"], Instance>\n) => {\n  const rules: string[] = [];\n  // 20 is arbitrary but keeps selector length reasonable\n  const chunkSize = 20;\n  for (let i = 0; i < editableInstanceSelectors.length; i += chunkSize) {\n    const chunk = editableInstanceSelectors\n      .slice(i, i + chunkSize)\n      .filter((selector) => {\n        const instance = instances.get(selector[0]);\n        if (instance === undefined) {\n          return false;\n        }\n        // Instances with expression children are not directly editable\n        return hasExpressionChildren(instance) === false;\n      });\n    if (chunk.length === 0) {\n      continue;\n    }\n    const selectors = chunk.map(\n      (selector) => `[${idAttribute}=\"${selector[0]}\"]`\n    );\n    rules.push(\n      `:is(${selectors.join(\", \")}), :is(${selectors.join(\", \")}) a { cursor: text; }`\n    );\n  }\n  return rules;\n};\n\nconst subscribeContentEditModeHelperStyles = () => {\n  const renderHelperStyles = () => {\n    helpersSheet.clear();\n    helpersSheet.setAttribute(\"media\", \"all\");\n\n    for (const style of helperStylesContentEdit) {\n      helpersSheet.addPlaintextRule(style);\n    }\n\n    // Show text cursor on all editable elements (including links and buttons)\n    // to indicate they are editable in the content editor mode\n    //\n    // @todo Consider setting cursor: pointer on non-editable elements by default\n    // to better distinguish clickable vs editable elements, needs more investigation\n    const rootInstanceId = $selectedPage.get()?.rootInstanceId;\n    if (rootInstanceId !== undefined) {\n      const editableInstanceSelectors: InstanceSelector[] = [];\n      const instances = $instances.get();\n\n      findAllEditableInstanceSelector({\n        instanceSelector: [rootInstanceId],\n        instances,\n        props: $props.get(),\n        metas: $registeredComponentMetas.get(),\n        results: editableInstanceSelectors,\n      });\n\n      for (const rule of computeEditableCursorRules(\n        editableInstanceSelectors,\n        instances\n      )) {\n        helpersSheet.addPlaintextRule(rule);\n      }\n    }\n\n    helpersSheet.render();\n  };\n\n  renderHelperStyles();\n\n  const requestIdleCallbackFn =\n    globalThis.requestIdleCallback ?? requestAnimationFrame;\n  const cancelIdleCallbackFn =\n    globalThis.cancelIdleCallback ?? cancelAnimationFrame;\n\n  let idleId: number;\n  const renderHelperStylesIdle = () => {\n    cancelIdleCallbackFn(idleId);\n    idleId = requestIdleCallbackFn(renderHelperStyles);\n  };\n\n  const unsubscribeInstances = $instances.listen(renderHelperStylesIdle);\n  const unsubscribeSelectedPage = $selectedPage.listen(renderHelperStylesIdle);\n\n  return () => {\n    unsubscribeInstances();\n    unsubscribeSelectedPage();\n    helpersSheet.clear();\n    helpersSheet.render();\n  };\n};\n\n// keep stable transformValue in store\n// to preserve cache in css engine\nconst $transformValue = computed($assets, (assets) =>\n  createImageValueTransformer(assets, {\n    assetBaseUrl,\n  })\n);\n\nconst getEphemeralProperty = (\n  styleDecl: Pick<StyleDecl, \"styleSourceId\" | \"state\" | \"property\">\n) => {\n  const { styleSourceId, state = \"\", property } = styleDecl;\n  return `--${styleSourceId}-${state}-${property}`;\n};\n\n// wrap normal style value with var(--namespace, value) to support ephemeral styles updates\n// between all token usages\nconst toVarValue = (\n  styleDecl: StyleDecl,\n  transformValue: TransformValue,\n  fallback?: StyleValue\n): VarValue => {\n  return {\n    type: \"var\",\n    // var style value is relying on name without leading \"--\"\n    // escape complex selectors in state like \":hover\"\n    // setProperty and removeProperty escape automatically\n    value: CSS.escape(getEphemeralProperty(styleDecl).slice(2)),\n    fallback: fallback\n      ? toVarFallback(fallback, transformValue)\n      : toVarFallback(styleDecl.value, transformValue),\n  };\n};\n\nconst computeDescendantSelectors = <\n  P extends { instanceId: string; name: string; type: string; value?: unknown },\n>(\n  instances: Map<Instance[\"id\"], Instance>,\n  props: Map<string, P>\n) => {\n  const parentIdByInstanceId = new Map<Instance[\"id\"], Instance[\"id\"]>();\n  const descendantInstanceIds: Instance[\"id\"][] = [];\n  for (const instance of instances.values()) {\n    if (instance.component === descendantComponent) {\n      descendantInstanceIds.push(instance.id);\n    }\n    for (const child of instance.children) {\n      if (child.type === \"id\") {\n        parentIdByInstanceId.set(child.value, instance.id);\n      }\n    }\n  }\n  const descendantSelectorByInstanceId = new Map<Instance[\"id\"], string>();\n  for (const prop of props.values()) {\n    if (prop.name === \"selector\" && prop.type === \"string\") {\n      descendantSelectorByInstanceId.set(prop.instanceId, prop.value as string);\n    }\n  }\n  const descendantSelectors = new Map<Instance[\"id\"], string>();\n  for (const instanceId of descendantInstanceIds) {\n    const parentId = parentIdByInstanceId.get(instanceId);\n    const selector = descendantSelectorByInstanceId.get(instanceId);\n    if (parentId && selector) {\n      descendantSelectors.set(\n        instanceId,\n        `[${idAttribute}=\"${parentId}\"]${selector}`\n      );\n    }\n  }\n  return descendantSelectors;\n};\n\n/**\n * Convert StyleDecl to declaration params for sheet operations.\n */\nconst toDeclarationParams = (styleDecl: StyleDecl) => ({\n  breakpoint: styleDecl.breakpointId,\n  selector: styleDecl.state ?? \"\",\n  property: styleDecl.property,\n});\n\n/**\n * Compute added and deleted styles by comparing current styles with previous.\n * Returns new prevStylesSet to track for next diff.\n */\nconst computeStylesDiff = ({\n  styles,\n  transformValue,\n  prevStylesSet,\n  prevTransformValue,\n}: {\n  styles: Map<string, StyleDecl>;\n  transformValue: TransformValue;\n  prevStylesSet: Set<StyleDecl>;\n  prevTransformValue: TransformValue | undefined;\n}) => {\n  // invalidate styles cache when assets are changed\n  let effectivePrevStyles = prevStylesSet;\n  if (prevTransformValue !== transformValue) {\n    effectivePrevStyles = new Set();\n  }\n  const stylesSet = new Set(styles.values());\n  const addedStyles = setDifference(stylesSet, effectivePrevStyles);\n  const deletedStyles = setDifference(effectivePrevStyles, stylesSet);\n  return { addedStyles, deletedStyles, nextPrevStylesSet: stylesSet };\n};\n\n/**\n * Convert instance ID to CSS selector.\n * ROOT_INSTANCE_ID maps to \":root\", others to attribute selector.\n */\nconst getInstanceSelector = (instanceId: string) =>\n  instanceId === ROOT_INSTANCE_ID\n    ? \":root\"\n    : `[${idAttribute}=\"${instanceId}\"]`;\n\n/**\n * Convert component and tag to preset CSS selector.\n * rootComponent maps to \":root\", others use :where() for low specificity.\n */\nconst getPresetStyleSelector = (component: string, tag: string) =>\n  component === rootComponent\n    ? \":root\"\n    : `${tag}:where([data-ws-component=\"${component}\"])`;\n\nconst $descendantSelectors = computed(\n  [$instances, $props],\n  computeDescendantSelectors\n);\n\n/**\n * Transform breakpoint options to simulate condition-based breakpoints.\n * When a condition breakpoint is selected (e.g. prefers-color-scheme:dark),\n * breakpoints with the same feature+value render as base (always apply),\n * same feature but different value render as \"not all\" (never apply),\n * and all others are unchanged.\n */\nconst simulateConditionBreakpoints = ({\n  breakpoints,\n  selectedBreakpoint,\n}: {\n  breakpoints: Iterable<Breakpoint>;\n  selectedBreakpoint: Breakpoint | undefined;\n}): Map<string, MediaRuleOptions> => {\n  const result = new Map<string, MediaRuleOptions>();\n  const simulated = selectedBreakpoint?.condition\n    ? parseMediaCondition(selectedBreakpoint.condition)\n    : undefined;\n  for (const breakpoint of breakpoints) {\n    if (simulated && breakpoint.condition) {\n      const parsed = parseMediaCondition(breakpoint.condition);\n      if (parsed?.feature === simulated.feature) {\n        const mediaType = parsed.value === simulated.value ? \"all\" : \"not all\";\n        result.set(breakpoint.id, { mediaType });\n        continue;\n      }\n    }\n    result.set(breakpoint.id, breakpoint);\n  }\n  return result;\n};\n\n/**\n * track new or deleted styles and style source selections items\n * and update style sheet accordingly\n */\nexport const subscribeStyles = () => {\n  let animationFrameId: undefined | number;\n\n  const renderUserSheetInTheNextFrame = () => {\n    if (animationFrameId) {\n      cancelAnimationFrame(animationFrameId);\n    }\n    animationFrameId = requestAnimationFrame(() => {\n      userSheet.setTransformer($transformValue.get());\n      userSheet.render();\n    });\n  };\n\n  const unsubscribeBreakpoints = computed(\n    [$breakpoints, $selectedBreakpoint],\n    (breakpoints, selectedBreakpoint) =>\n      [breakpoints, selectedBreakpoint] as const\n  ).subscribe(([breakpoints, selectedBreakpoint]) => {\n    const mediaRules = simulateConditionBreakpoints({\n      breakpoints: breakpoints.values(),\n      selectedBreakpoint,\n    });\n    for (const [id, options] of mediaRules) {\n      userSheet.addMediaRule(id, options);\n    }\n    renderUserSheetInTheNextFrame();\n  });\n\n  const unsubscribeTransformValue = $transformValue.subscribe(() => {\n    renderUserSheetInTheNextFrame();\n  });\n\n  // add/delete declarations in mixins\n  let prevStylesSet = new Set<StyleDecl>();\n  let prevTransformValue: undefined | TransformValue;\n  // track value transformer to properly serialize var() fallback as unparsed\n  // before it was managed css engine but here toValue is invoked by styles renderer directly\n  const unsubscribeStyles = computed(\n    [$styles, $transformValue],\n    (styles, transformValue) => [styles, transformValue] as const\n  ).subscribe(([styles, transformValue]) => {\n    const { addedStyles, deletedStyles, nextPrevStylesSet } = computeStylesDiff(\n      {\n        styles,\n        transformValue,\n        prevStylesSet,\n        prevTransformValue,\n      }\n    );\n    prevTransformValue = transformValue;\n    prevStylesSet = nextPrevStylesSet;\n    // delete before adding declarations by the same key\n    for (const styleDecl of deletedStyles) {\n      const rule = userSheet.addMixinRule(styleDecl.styleSourceId);\n      rule.deleteDeclaration(toDeclarationParams(styleDecl));\n    }\n    for (const styleDecl of addedStyles) {\n      const rule = userSheet.addMixinRule(styleDecl.styleSourceId);\n      rule.setDeclaration({\n        ...toDeclarationParams(styleDecl),\n        value: toVarValue(styleDecl, transformValue),\n      });\n    }\n    renderUserSheetInTheNextFrame();\n  });\n\n  // apply mixins to nesting rules\n  let prevSelectionsSet = new Set<StyleSourceSelection>();\n  const unsubscribeStyleSourceSelections = $styleSourceSelections.subscribe(\n    (styleSourceSelections) => {\n      const selectionsSet = new Set(styleSourceSelections.values());\n      const addedSelections = setDifference(selectionsSet, prevSelectionsSet);\n      prevSelectionsSet = selectionsSet;\n      for (const { instanceId, values } of addedSelections) {\n        const rule = userSheet.addNestingRule(getInstanceSelector(instanceId));\n        rule.applyMixins(values);\n      }\n      renderUserSheetInTheNextFrame();\n    }\n  );\n\n  const unsubscribeDescendantSelectors = $descendantSelectors.subscribe(\n    (descendantSelectors) => {\n      let selectorsUpdated = false;\n      for (const [instanceId, descendantSelector] of descendantSelectors) {\n        // access descendant component rule\n        // and change its selector to parent id + selector prop\n        const key = `[${idAttribute}=\"${instanceId}\"]`;\n        const rule = userSheet.addNestingRule(key);\n        // invalidate only when necessary\n        if (rule.getSelector() !== descendantSelector) {\n          selectorsUpdated = true;\n          rule.setSelector(descendantSelector);\n        }\n      }\n      if (selectorsUpdated) {\n        renderUserSheetInTheNextFrame();\n      }\n    }\n  );\n\n  return () => {\n    unsubscribeBreakpoints();\n    unsubscribeStyles();\n    unsubscribeStyleSourceSelections();\n    unsubscribeDescendantSelectors();\n    unsubscribeTransformValue();\n    if (animationFrameId) {\n      cancelAnimationFrame(animationFrameId);\n    }\n  };\n};\n\nexport const manageContentEditModeStyles = ({\n  signal,\n}: {\n  signal: AbortSignal;\n}) => {\n  const unsubscribePreviewMode = subscribeContentEditModeHelperStyles();\n  signal.addEventListener(\"abort\", () => {\n    unsubscribePreviewMode();\n  });\n};\n\nexport const manageDesignModeStyles = ({ signal }: { signal: AbortSignal }) => {\n  const unsubscribeStateStyles = subscribeStateStyles();\n  const unsubscribeEphemeralStyle = subscribeEphemeralStyle();\n  const unsubscribePreviewMode = subscribeDesignModeHelperStyles();\n  signal.addEventListener(\"abort\", () => {\n    unsubscribeStateStyles();\n    unsubscribeEphemeralStyle();\n    unsubscribePreviewMode();\n  });\n};\n\nexport const GlobalStyles = () => {\n  const assets = useStore($assets);\n  const metas = useStore($registeredComponentMetas);\n\n  useLayoutEffect(() => {\n    fontsAndDefaultsSheet.clear();\n    addFontRules({\n      sheet: fontsAndDefaultsSheet,\n      assets,\n      assetBaseUrl,\n    });\n    fontsAndDefaultsSheet.render();\n  }, [assets]);\n\n  useLayoutEffect(() => {\n    presetSheet.clear();\n    presetSheet.addMediaRule(\"presets\");\n    for (const [component, meta] of metas) {\n      for (const [tag, styles] of Object.entries(meta.presetStyle ?? {})) {\n        const rule = presetSheet.addNestingRule(\n          getPresetStyleSelector(component, tag)\n        );\n        for (const declaration of styles) {\n          rule.setDeclaration({\n            breakpoint: \"presets\",\n            selector: declaration.state ?? \"\",\n            property: declaration.property,\n            value: declaration.value,\n          });\n        }\n      }\n    }\n    presetSheet.render();\n  }, [metas]);\n\n  return null;\n};\n\nconst computeInstanceStyles = ({\n  selectedInstance,\n  selectedStyleState,\n  breakpoints,\n  styleSourceSelections,\n  styles,\n}: {\n  selectedInstance: Instance | undefined;\n  selectedStyleState: string | undefined;\n  breakpoints: Map<string, Breakpoint>;\n  styleSourceSelections: Map<string, { values: string[] }>;\n  styles: Map<string, StyleDecl>;\n}) => {\n  if (selectedInstance === undefined || selectedStyleState === undefined) {\n    return;\n  }\n  const styleSources = new Set(\n    styleSourceSelections.get(selectedInstance.id)?.values\n  );\n  const instanceStyles: StyleDecl[] = [];\n  for (const styleDecl of styles.values()) {\n    if (\n      styleDecl.state === selectedStyleState &&\n      styleSources.has(styleDecl.styleSourceId)\n    ) {\n      instanceStyles.push(styleDecl);\n    }\n  }\n  return {\n    instanceId: selectedInstance.id,\n    selectedState: selectedStyleState,\n    breakpoints: Array.from(breakpoints.values()),\n    styles: instanceStyles,\n  };\n};\n\nconst $instanceStyles = computed(\n  [\n    $selectedInstance,\n    $selectedStyleState,\n    $breakpoints,\n    $styleSourceSelections,\n    $styles,\n  ],\n  (\n    selectedInstance,\n    selectedStyleState,\n    breakpoints,\n    styleSourceSelections,\n    styles\n  ) =>\n    computeInstanceStyles({\n      selectedInstance,\n      selectedStyleState,\n      breakpoints,\n      styleSourceSelections,\n      styles,\n    })\n);\n\n/**\n * Render state styles to a stylesheet.\n *\n * For pseudo-classes like :hover, render without state so users can preview\n * the styles without triggering the state.\n * For pseudo-elements like ::before, keep the selector so styles apply\n * to the pseudo-element, not the parent.\n */\nconst renderStateStyles = ({\n  instanceStyles,\n  sheet,\n  transformValue,\n  toStyleValue,\n}: {\n  instanceStyles: ReturnType<typeof $instanceStyles.get>;\n  sheet: StyleSheetRegular;\n  transformValue: TransformValue;\n  toStyleValue: (\n    styleDecl: StyleDecl,\n    transformValue: TransformValue\n  ) => StyleValue;\n}) => {\n  sheet.clear();\n  if (instanceStyles === undefined) {\n    sheet.render();\n    return;\n  }\n  const { instanceId, selectedState, breakpoints, styles } = instanceStyles;\n  for (const breakpoint of breakpoints) {\n    sheet.addMediaRule(breakpoint.id, breakpoint);\n  }\n  const selector = `[${idAttribute}=\"${instanceId}\"]`;\n  const rule = sheet.addNestingRule(selector);\n  const stateSelector = isPseudoElement(selectedState) ? selectedState : \"\";\n  for (const styleDecl of styles) {\n    rule.setDeclaration({\n      breakpoint: styleDecl.breakpointId,\n      selector: stateSelector,\n      property: styleDecl.property,\n      value: toStyleValue(styleDecl, transformValue),\n    });\n  }\n  sheet.setTransformer(transformValue);\n  sheet.render();\n};\n\n/**\n * render currently selected state styles as stateless\n * in separate sheet and clear when state is not selected\n */\nconst subscribeStateStyles = () => {\n  return $instanceStyles.subscribe((instanceStyles) => {\n    renderStateStyles({\n      instanceStyles,\n      sheet: stateSheet,\n      transformValue: $transformValue.get(),\n      toStyleValue: (styleDecl, transformValue) =>\n        toVarValue(styleDecl, transformValue),\n    });\n  });\n};\n\nconst subscribeEphemeralStyle = () => {\n  // track custom properties added on previous ephemeral update\n  const appliedEphemeralDeclarations = new Map<\n    string,\n    [StyleDecl, HTMLElement[]]\n  >();\n\n  return $ephemeralStyles.subscribe((ephemeralStyles) => {\n    const instance = $selectedInstance.get();\n    const instanceSelector = $selectedInstanceSelector.get();\n\n    if (instance === undefined || instanceSelector === undefined) {\n      return;\n    }\n\n    // reset ephemeral styles\n    if (ephemeralStyles.length === 0) {\n      canvasApi.resetInert();\n\n      for (const [\n        styleDecl,\n        elements,\n      ] of appliedEphemeralDeclarations.values()) {\n        document.documentElement.style.removeProperty(\n          getEphemeralProperty(styleDecl)\n        );\n\n        for (const element of elements) {\n          element.style.removeProperty(getEphemeralProperty(styleDecl));\n        }\n      }\n      userSheet.setTransformer($transformValue.get());\n      userSheet.render();\n      appliedEphemeralDeclarations.clear();\n    }\n\n    // add ephemeral styles\n    if (ephemeralStyles.length > 0) {\n      canvasApi.setInert();\n      const selector = getInstanceSelector(instance.id);\n      const rule = userSheet.addNestingRule(selector);\n      let ephemeralSheetUpdated = false;\n      for (const styleDecl of ephemeralStyles) {\n        // update custom property\n        document.documentElement.style.setProperty(\n          getEphemeralProperty(styleDecl),\n          toValue(styleDecl.value, $transformValue.get())\n        );\n\n        // We need to apply the custom property to the selected element as well.\n        // Otherwise, variables defined on it will not be visible on documentElement.\n        const elements = getAllElementsByInstanceSelector(instanceSelector);\n        for (const element of elements) {\n          element.style.setProperty(\n            getEphemeralProperty(styleDecl),\n            toValue(styleDecl.value, $transformValue.get())\n          );\n        }\n\n        // Lazily add a rule to the user stylesheet (it might not be created yet if no styles have been added to the instance property).\n        const styleDeclKey = getStyleDeclKey(styleDecl);\n        if (appliedEphemeralDeclarations.has(styleDeclKey) === false) {\n          ephemeralSheetUpdated = true;\n\n          const mixinRule = userSheet.addMixinRule(styleDecl.styleSourceId);\n\n          // Use the actual style value as a fallback (non-ephemeral); see the \"Lazy\" comment above.\n          const computedStyleDecl = createComputedStyleDeclStore(\n            hyphenateProperty(styleDecl.property)\n          ).get();\n\n          const value = toVarValue(\n            styleDecl,\n            $transformValue.get(),\n            computedStyleDecl.cascadedValue\n          );\n\n          mixinRule.setDeclaration({\n            ...toDeclarationParams(styleDecl),\n            value,\n          });\n\n          rule.addMixin(styleDecl.styleSourceId);\n\n          // When editing state styles (e.g., :hover), also render var() in stateSheet\n          // so ephemeral updates are visible in the state preview\n          if (styleDecl.state !== undefined) {\n            const stateRule = stateSheet.addNestingRule(selector);\n            // For pseudo-elements (::before, ::after), keep the selector so styles\n            // apply to the pseudo-element. For pseudo-classes (:hover, :focus),\n            // render without state so users can preview without triggering the state.\n            const stateSelector = isPseudoElement(styleDecl.state)\n              ? styleDecl.state\n              : \"\";\n            stateRule.setDeclaration({\n              breakpoint: styleDecl.breakpointId,\n              selector: stateSelector,\n              property: styleDecl.property,\n              value,\n            });\n          }\n        }\n        appliedEphemeralDeclarations.set(styleDeclKey, [styleDecl, elements]);\n      }\n      // avoid stylesheet rerendering on every ephemeral update\n      if (ephemeralSheetUpdated) {\n        userSheet.render();\n        stateSheet.render();\n      }\n    }\n  });\n};\n\nexport const __testing__ = {\n  getEphemeralProperty,\n  getInstanceSelector,\n  getPresetStyleSelector,\n  computeDescendantSelectors,\n  computeInstanceStyles,\n  computeEditableCursorRules,\n  computeStylesDiff,\n  toDeclarationParams,\n  toVarValue,\n  hasExpressionChildren,\n  renderStateStyles,\n  simulateConditionBreakpoints,\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/use-drag-drop.ts",
    "content": "import { useLayoutEffect, useRef } from \"react\";\nimport { elementComponent, type Instance } from \"@webstudio-is/sdk\";\nimport {\n  type Point,\n  useAutoScroll,\n  useDrag,\n  useDrop,\n  computeIndicatorPlacement,\n} from \"@webstudio-is/design-system\";\nimport {\n  $dragAndDropState,\n  $instances,\n  $props,\n  $registeredComponentMetas,\n  type ItemDropTarget,\n} from \"~/shared/nano-states\";\nimport { publish, useSubscribe } from \"~/shared/pubsub\";\nimport {\n  getComponentTemplateData,\n  insertWebstudioElementAt,\n  insertWebstudioFragmentAt,\n  reparentInstance,\n} from \"~/shared/instance-utils\";\nimport {\n  getElementByInstanceSelector,\n  getInstanceIdFromElement,\n  getInstanceSelectorFromElement,\n} from \"~/shared/dom-utils\";\nimport {\n  type InstanceSelector,\n  areInstanceSelectorsEqual,\n} from \"~/shared/tree-utils\";\nimport { findClosestInstanceMatchingFragment } from \"~/shared/matcher\";\nimport {\n  findClosestContainer,\n  findClosestRichText,\n  isTreeSatisfyingContentModel,\n} from \"~/shared/content-model\";\n\ndeclare module \"~/shared/pubsub\" {\n  export interface PubsubMap {\n    dragEnd: DragEndPayload;\n    dragMove: DragMovePayload;\n    dragStart: DragStartPayload;\n    dropTargetChange: undefined | ItemDropTarget;\n    cancelCurrentDrag: undefined;\n  }\n}\n\ntype Origin = \"canvas\" | \"panel\";\n\nexport type DragStartPayload =\n  | { origin: Origin; type: \"insert\"; dragComponent: Instance[\"component\"] }\n  | {\n      origin: Origin;\n      type: \"reparent\";\n      dragInstanceSelector: InstanceSelector;\n    };\n\nexport type DragEndPayload = {\n  isCanceled: boolean;\n};\n\nexport type DragMovePayload = { canvasCoordinates: Point };\n\nconst findClosestDroppableInstanceSelector = (\n  instanceSelector: InstanceSelector,\n  dragPayload: DragStartPayload\n) => {\n  const instances = $instances.get();\n  const props = $props.get();\n  const metas = $registeredComponentMetas.get();\n\n  // prevent dropping anything into non containers like image\n  instanceSelector = findClosestContainer({\n    metas,\n    props,\n    instances,\n    instanceSelector,\n  });\n  let droppableIndex = -1;\n  if (dragPayload?.type === \"insert\") {\n    // allow dropping element into any container\n    if (dragPayload.dragComponent === elementComponent) {\n      droppableIndex = 0;\n    } else {\n      const fragment = getComponentTemplateData(dragPayload.dragComponent);\n      if (fragment) {\n        droppableIndex = findClosestInstanceMatchingFragment({\n          instances,\n          props,\n          metas,\n          instanceSelector,\n          fragment,\n        });\n      }\n    }\n  }\n  if (dragPayload?.type === \"reparent\") {\n    const dropInstanceSelector = [\n      dragPayload.dragInstanceSelector[0],\n      ...instanceSelector,\n    ];\n    const matches = isTreeSatisfyingContentModel({\n      instances,\n      props,\n      metas,\n      instanceSelector: dropInstanceSelector,\n    });\n    droppableIndex = matches ? 0 : -1;\n  }\n\n  if (droppableIndex === -1) {\n    return;\n  }\n  const droppableInstanceSelector = instanceSelector.slice(droppableIndex);\n  return droppableInstanceSelector;\n};\n\nconst initialState: {\n  dropTarget: ItemDropTarget | undefined;\n  dragPayload: DragStartPayload | undefined;\n} = {\n  dropTarget: undefined,\n  dragPayload: undefined,\n};\n\nconst sharedDropOptions = {\n  getValidChildren: (parent: Element) => {\n    return Array.from(parent.children).filter(\n      (child) => getInstanceIdFromElement(child) !== undefined\n    );\n  },\n};\n\nexport const useDragAndDrop = () => {\n  const state = useRef({ ...initialState });\n\n  const autoScrollHandlers = useAutoScroll({ fullscreen: true });\n\n  const dropHandlers = useDrop<InstanceSelector>({\n    ...sharedDropOptions,\n\n    elementToData(element) {\n      const instanceSelector = getInstanceSelectorFromElement(element);\n      if (instanceSelector === undefined) {\n        return false;\n      }\n      return instanceSelector;\n    },\n\n    // This must be fast, it can be called multiple times per pointer move\n    swapDropTarget(dropTarget) {\n      const { dragPayload } = state.current;\n\n      if (dropTarget === undefined || dragPayload === undefined) {\n        return;\n      }\n\n      const dropInstanceSelector = dropTarget.data;\n\n      const newDropInstanceSelector = dropInstanceSelector.slice();\n      if (dropTarget.area !== \"center\") {\n        newDropInstanceSelector.shift();\n      }\n\n      // Don't allow to drop inside drag item or any of its children\n      if (dragPayload.type === \"reparent\") {\n        const [dragInstanceId] = dragPayload.dragInstanceSelector;\n        const dragInstanceIndex =\n          newDropInstanceSelector.indexOf(dragInstanceId);\n        if (dragInstanceIndex !== -1) {\n          newDropInstanceSelector.splice(0, dragInstanceIndex + 1);\n        }\n      }\n\n      const droppableInstanceSelector = findClosestDroppableInstanceSelector(\n        newDropInstanceSelector,\n        dragPayload\n      );\n      if (droppableInstanceSelector === undefined) {\n        return;\n      }\n\n      if (\n        areInstanceSelectorsEqual(\n          dropInstanceSelector,\n          droppableInstanceSelector\n        )\n      ) {\n        return dropTarget;\n      }\n\n      const element = getElementByInstanceSelector(droppableInstanceSelector);\n      if (element === undefined) {\n        return;\n      }\n\n      return { data: droppableInstanceSelector, element };\n    },\n\n    onDropTargetChange(dropTarget) {\n      publish({\n        type: \"dropTargetChange\",\n        payload:\n          dropTarget === undefined\n            ? undefined\n            : {\n                placement: dropTarget.placement,\n                indexWithinChildren: dropTarget.indexWithinChildren,\n                itemSelector: dropTarget.data,\n              },\n      });\n    },\n  });\n\n  const dragHandlers = useDrag<InstanceSelector>({\n    elementToData(element) {\n      const instanceSelector = getInstanceSelectorFromElement(element);\n      if (instanceSelector === undefined) {\n        return false;\n      }\n      // cannot drag while editing text\n      if (element.closest(\"[contenteditable=true]\")) {\n        return false;\n      }\n      // When trying to drag an instance inside editor, drag the editor instead\n      return (\n        findClosestRichText({\n          instanceSelector,\n          instances: $instances.get(),\n          props: $props.get(),\n          metas: $registeredComponentMetas.get(),\n        }) ?? instanceSelector\n      );\n    },\n\n    onStart({ data: dragInstanceSelector }) {\n      publish({\n        type: \"dragStart\",\n        payload: {\n          type: \"reparent\",\n          origin: \"canvas\",\n          dragInstanceSelector,\n        },\n      });\n    },\n    onMove: (point) => {\n      publish({\n        type: \"dragMove\",\n        payload: { canvasCoordinates: point },\n      });\n    },\n    onEnd({ isCanceled }) {\n      publish({\n        type: \"dragEnd\",\n        payload: { isCanceled },\n      });\n    },\n  });\n\n  // We have to use useLayoutEffect to setup the refs\n  // because we want to use <body> as a root.\n  // We prefer useLayoutEffect over useEffect\n  // because it's closer in the life cycle to when React noramlly calls the \"ref\" callbacks.\n  useLayoutEffect(() => {\n    dropHandlers.rootRef(document.documentElement);\n    dragHandlers.rootRef(document.documentElement);\n    window.addEventListener(\"scroll\", dropHandlers.handleScroll);\n\n    return () => {\n      dropHandlers.rootRef(null);\n      dragHandlers.rootRef(null);\n      window.removeEventListener(\"scroll\", dropHandlers.handleScroll);\n    };\n  }, [dragHandlers, dropHandlers, autoScrollHandlers]);\n\n  useSubscribe(\"cancelCurrentDrag\", () => {\n    dragHandlers.cancelCurrentDrag();\n  });\n\n  // Handle drag from the panel\n  // ================================================================\n\n  useSubscribe(\"dragStart\", (dragPayload) => {\n    state.current.dragPayload = dragPayload;\n    autoScrollHandlers.setEnabled(true);\n    dropHandlers.handleStart();\n  });\n\n  useSubscribe(\"dragMove\", ({ canvasCoordinates }) => {\n    dropHandlers.handleMove(canvasCoordinates);\n    autoScrollHandlers.handleMove(canvasCoordinates);\n  });\n\n  useSubscribe(\"dropTargetChange\", (dropTarget) => {\n    state.current.dropTarget = dropTarget;\n    if (dropTarget === undefined) {\n      $dragAndDropState.set({\n        ...$dragAndDropState.get(),\n        placementIndicator: undefined,\n      });\n      return;\n    }\n    const element = getElementByInstanceSelector(dropTarget.itemSelector);\n    if (element === undefined) {\n      return;\n    }\n    $dragAndDropState.set({\n      ...$dragAndDropState.get(),\n      placementIndicator: computeIndicatorPlacement({\n        ...sharedDropOptions,\n        element,\n        placement: dropTarget.placement,\n      }),\n    });\n  });\n\n  useSubscribe(\"dragEnd\", ({ isCanceled }) => {\n    dropHandlers.handleEnd({ isCanceled });\n    autoScrollHandlers.setEnabled(false);\n    const { dropTarget, dragPayload } = state.current;\n\n    if (dropTarget && dragPayload && isCanceled === false) {\n      const insertable = {\n        parentSelector: dropTarget.itemSelector,\n        position: dropTarget.indexWithinChildren,\n      };\n      if (dragPayload.type === \"insert\") {\n        if (dragPayload.dragComponent === elementComponent) {\n          insertWebstudioElementAt(insertable);\n        } else {\n          const fragment = getComponentTemplateData(dragPayload.dragComponent);\n          if (fragment) {\n            insertWebstudioFragmentAt(fragment, insertable);\n          }\n        }\n      }\n      if (dragPayload.type === \"reparent\") {\n        reparentInstance(dragPayload.dragInstanceSelector, insertable);\n      }\n    }\n\n    state.current = { ...initialState };\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/shared/use-pointer-outline.ts",
    "content": "import { useEffect, useRef } from \"react\";\nimport type { Point } from \"@webstudio-is/design-system\";\n\n// Draw a point where we think the pointer is to visualize if calculations are based on the right position\n// Only needed for debugging.\nexport const usePointerOutline = () => {\n  const ref = useRef<undefined | HTMLDivElement>(undefined);\n\n  useEffect(() => {\n    const div = document.createElement(\"div\");\n    div.style.cssText = `\n      position: absolute;\n      width: 5px;\n      height: 5px;\n      background: red;\n    `;\n    document.body.appendChild(div);\n    ref.current = div;\n    return () => {\n      document.body.removeChild(div);\n      ref.current = undefined;\n    };\n  }, []);\n\n  return (offset: Point) => {\n    if (ref.current === undefined) {\n      return;\n    }\n    ref.current.style.left = `${offset.x}px`;\n    ref.current.style.top = `${offset.y}px`;\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/canvas/stores.ts",
    "content": "import { atom } from \"nanostores\";\nimport type { StyleDecl } from \"@webstudio-is/sdk\";\n\nexport const $ephemeralStyles = atom<Array<StyleDecl>>([]);\n"
  },
  {
    "path": "apps/builder/app/dashboard/dashboard.stories.tsx",
    "content": "import type { StoryFn } from \"@storybook/react\";\nimport type { JSX } from \"react\";\nimport { createMemoryRouter, RouterProvider } from \"react-router-dom\";\nimport {\n  Box,\n  Flex,\n  StorySection,\n  Text,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { Dashboard, DashboardSetup } from \"./dashboard\";\nimport { Card as CardComponent, CardContent, CardFooter } from \"./shared/card\";\nimport { ThumbnailWithAbbr, ThumbnailLinkWithAbbr } from \"./shared/thumbnail\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\n\nexport default {\n  title: \"Dashboard\",\n  component: Dashboard,\n};\n\nconst user = {\n  id: \"0\",\n  createdAt: new Date().toString(),\n  email: null,\n  image: null,\n  username: \"Taylor\",\n  teamId: null,\n  provider: \"github\",\n  projectsTags: [],\n};\n\nconst createRouter = (element: JSX.Element, path: string, current?: string) =>\n  createMemoryRouter([{ path, element }], {\n    initialEntries: [current ?? path],\n  });\n\nconst userPlanFeatures: UserPlanFeatures = {\n  allowAdditionalPermissions: false,\n  allowDynamicData: false,\n  allowContentMode: false,\n  allowStagingPublish: false,\n  maxContactEmails: 0,\n  maxDomainsAllowedPerUser: 0,\n  maxPublishesAllowedPerUser: 1,\n  purchases: [],\n};\n\nconst projects = [\n  {\n    id: \"0\",\n    createdAt: new Date().toString(),\n    title: \"My Project\",\n    domain: \"domain.com\",\n    userId: \"\",\n    isDeleted: false,\n    isPublished: false,\n    latestBuild: null,\n    previewImageAsset: null,\n    previewImageAssetId: \"\",\n    latestBuildVirtual: null,\n    marketplaceApprovalStatus: \"UNLISTED\" as const,\n    tags: [],\n    domainsVirtual: [],\n  } as DashboardProject,\n];\n\nconst data = {\n  user,\n  templates: projects,\n  userPlanFeatures,\n  publisherHost: \"https://wstd.work\",\n  projects,\n};\n\nexport const Welcome: StoryFn<typeof Dashboard> = () => {\n  const router = createRouter(\n    <>\n      <DashboardSetup data={{ ...data, projects: [] }} />\n      <Dashboard />\n    </>,\n    \"/dashboard/templates\"\n  );\n  return (\n    <StorySection title=\"Welcome\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n\nexport const Projects: StoryFn<typeof Dashboard> = () => {\n  const router = createRouter(\n    <>\n      <DashboardSetup data={data} />\n      <Dashboard />\n    </>,\n    \"/dashboard\"\n  );\n  return (\n    <StorySection title=\"Projects\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n\nexport const Templates: StoryFn<typeof Dashboard> = () => {\n  const router = createRouter(\n    <>\n      <DashboardSetup data={data} />\n      <Dashboard />\n    </>,\n    \"/dashboard/templates\"\n  );\n  return (\n    <StorySection title=\"Templates\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n\nexport const Search: StoryFn<typeof Dashboard> = () => {\n  const router = createRouter(\n    <>\n      <DashboardSetup data={data} />\n      <Dashboard />\n    </>,\n    \"/dashboard/search\",\n    \"/dashboard/search?q=my\"\n  );\n\n  return (\n    <StorySection title=\"Search\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n\nexport const SearchNothingFound: StoryFn<typeof Dashboard> = () => {\n  const router = createRouter(\n    <>\n      <DashboardSetup data={data} />\n      <Dashboard />\n    </>,\n    \"/dashboard/search\",\n    \"/dashboard/search?q=notfound\"\n  );\n  return (\n    <StorySection title=\"Search Nothing Found\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n\nexport const Card = () => (\n  <StorySection title=\"Card\">\n    <Flex gap=\"3\" wrap=\"wrap\" align=\"start\">\n      <Box css={{ width: theme.spacing[30] }}>\n        <CardComponent>\n          <CardContent\n            css={{ background: theme.colors.brandBackgroundProjectCardFront }}\n          />\n          <CardFooter>\n            <Text truncate>My project</Text>\n          </CardFooter>\n        </CardComponent>\n      </Box>\n      <Box css={{ width: theme.spacing[30] }}>\n        <CardComponent aria-selected={true}>\n          <CardContent\n            css={{ background: theme.colors.brandBackgroundProjectCardFront }}\n          />\n          <CardFooter>\n            <Text truncate>Selected project</Text>\n          </CardFooter>\n        </CardComponent>\n      </Box>\n      {[\"Project Alpha\", \"My Website\", \"Landing Page\"].map((title) => (\n        <Box key={title} css={{ width: theme.spacing[30] }}>\n          <CardComponent>\n            <CardContent\n              css={{ background: theme.colors.brandBackgroundProjectCardFront }}\n            />\n            <CardFooter>\n              <Text truncate>{title}</Text>\n            </CardFooter>\n          </CardComponent>\n        </Box>\n      ))}\n    </Flex>\n  </StorySection>\n);\n\nexport const Thumbnails = () => (\n  <StorySection title=\"Thumbnails\">\n    <Flex gap=\"3\">\n      <ThumbnailWithAbbr title=\"My Next Project\" onClick={() => {}} />\n      <ThumbnailLinkWithAbbr title=\"Landing Page\" to=\"#\" />\n      <ThumbnailWithAbbr title=\"Portfolio\" onClick={() => {}} />\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/dashboard/dashboard.tsx",
    "content": "import { useEffect, useState, type ReactNode } from \"react\";\nimport {\n  Flex,\n  List,\n  ListItem,\n  Text,\n  TooltipProvider,\n  Toaster,\n  css,\n  globalCss,\n  theme,\n  PanelBanner,\n  Link,\n  buttonStyle,\n} from \"@webstudio-is/design-system\";\nimport { BodyIcon, ExtensionIcon } from \"@webstudio-is/icons\";\nimport { NavLink, useLocation, useRevalidator } from \"@remix-run/react\";\nimport { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport { CloneProjectDialog } from \"~/shared/clone-project\";\nimport { dashboardPath } from \"~/shared/router-utils\";\nimport { CollapsibleSection } from \"~/builder/shared/collapsible-section\";\nimport { ProfileMenu } from \"./profile-menu\";\nimport { Projects } from \"./projects/projects\";\nimport { Templates } from \"./templates/templates\";\nimport { Header } from \"./shared/layout\";\nimport { help } from \"~/shared/help\";\nimport { SearchResults } from \"./search/search-results\";\nimport type { DashboardData } from \"./shared/types\";\nimport { Search } from \"./search/search-field\";\n\nconst globalStyles = globalCss({\n  body: {\n    margin: 0,\n  },\n});\n\nconst CloneProject = ({\n  projectToClone,\n}: {\n  projectToClone: DashboardData[\"projectToClone\"];\n}) => {\n  const location = useLocation();\n  const [isOpen, setIsOpen] = useState(projectToClone !== undefined);\n  const { revalidate } = useRevalidator();\n\n  useEffect(() => {\n    const searchParams = new URLSearchParams(location.search);\n    const cloneProjectAuthToken = searchParams.get(\"projectToCloneAuthToken\");\n    if (cloneProjectAuthToken === null) {\n      return;\n    }\n\n    // Use the native history API to remove query parameters without reloading the page data\n    const currentState = window.history.state;\n    window.history.replaceState(currentState, \"\", location.pathname);\n  }, [location.search, location.pathname]);\n\n  if (projectToClone !== undefined) {\n    return (\n      <CloneProjectDialog\n        isOpen={isOpen}\n        onOpenChange={setIsOpen}\n        project={{\n          id: projectToClone.id,\n          title: projectToClone.title,\n        }}\n        authToken={projectToClone.authToken}\n        onCreate={() => {\n          revalidate();\n        }}\n      />\n    );\n  }\n};\n\nconst sidebarLinkStyle = css({\n  all: \"unset\",\n  cursor: \"pointer\",\n  display: \"flex\",\n  alignItems: \"center\",\n  gap: theme.spacing[5],\n  height: theme.spacing[13],\n  paddingInline: theme.panel.paddingInline,\n  outline: \"none\",\n  \"&:focus-visible, &:hover\": {\n    background: theme.colors.backgroundHover,\n  },\n  \"&[aria-current=page]\": {\n    background: theme.colors.backgroundItemCurrent,\n    color: theme.colors.foregroundMain,\n  },\n});\n\nconst NavigationItems = ({\n  items,\n}: {\n  items: Array<{\n    to: string;\n    prefix: ReactNode;\n    children: string;\n    target?: string;\n  }>;\n}) => {\n  return (\n    <List style={{ padding: 0, margin: 0 }}>\n      {items.map((item, index) => {\n        return (\n          <ListItem asChild index={index} key={index}>\n            <NavLink\n              to={item.to}\n              end\n              target={item.target}\n              className={sidebarLinkStyle()}\n            >\n              {item.prefix}\n              <Text variant=\"labels\" color=\"main\">\n                {item.children}\n              </Text>\n            </NavLink>\n          </ListItem>\n        );\n      })}\n    </List>\n  );\n};\n\nconst $data = atom<DashboardData | undefined>();\n\nexport const DashboardSetup = ({ data }: { data: DashboardData }) => {\n  $data.set(data);\n  globalStyles();\n  return null;\n};\n\nconst getView = (pathname: string, hasProjects: boolean) => {\n  if (pathname === dashboardPath(\"search\")) {\n    return \"search\";\n  }\n\n  if (hasProjects === false) {\n    return \"welcome\";\n  }\n\n  if (pathname === dashboardPath(\"templates\")) {\n    return \"templates\";\n  }\n  return \"projects\";\n};\n\nexport const Dashboard = () => {\n  const data = useStore($data);\n  const location = useLocation();\n\n  if (data === undefined) {\n    return null;\n  }\n\n  const {\n    user,\n    userPlanFeatures,\n    publisherHost,\n    projectToClone,\n    projects,\n    templates,\n  } = data;\n  const hasProjects = projects.length > 0;\n  const view = getView(location.pathname, hasProjects);\n\n  return (\n    <TooltipProvider>\n      <Flex css={{ height: \"100vh\" }}>\n        <Flex\n          as=\"aside\"\n          align=\"stretch\"\n          direction=\"column\"\n          shrink={false}\n          css={{\n            width: theme.sizes.sidebarWidth,\n            borderRight: `1px solid ${theme.colors.borderMain}`,\n            position: \"sticky\",\n            top: 0,\n          }}\n        >\n          <Header variant=\"aside\">\n            <ProfileMenu user={user} userPlanFeatures={userPlanFeatures} />\n          </Header>\n          <Flex\n            direction=\"column\"\n            gap=\"3\"\n            css={{\n              paddingInline: theme.spacing[7],\n              paddingBottom: theme.spacing[7],\n            }}\n          >\n            <Search />\n          </Flex>\n          <nav>\n            <CollapsibleSection label=\"Workspace\" fullWidth>\n              <NavigationItems\n                items={\n                  view === \"welcome\" || hasProjects === false\n                    ? [\n                        {\n                          to: dashboardPath(),\n                          prefix: <ExtensionIcon />,\n                          children: \"Welcome\",\n                        },\n                      ]\n                    : [\n                        {\n                          to: dashboardPath(\"projects\"),\n                          prefix: <BodyIcon />,\n                          children: \"Projects\",\n                        },\n                        {\n                          to: dashboardPath(\"templates\"),\n                          prefix: <ExtensionIcon />,\n                          children: \"Starter templates\",\n                        },\n                      ]\n                }\n              />\n            </CollapsibleSection>\n            <CollapsibleSection label=\"Help & support\" fullWidth>\n              <NavigationItems\n                items={help.map((item) => ({\n                  to: item.url,\n                  target: \"_blank\",\n                  prefix: item.icon,\n                  children: item.label,\n                }))}\n              />\n            </CollapsibleSection>\n          </nav>\n          <PanelBanner>\n            <Text variant=\"titles\">Inception is live</Text>\n            <Text color=\"subtle\">\n              An AI-powered design tool to explore ideas and instantly generate\n              HTML/CSS for Webstudio Builder or any other platform.\n            </Text>\n            <Link\n              className={buttonStyle({\n                color: \"gradient\",\n              })}\n              underline=\"none\"\n              href=\"https://wstd.us/inception\"\n              target=\"_blank\"\n              color=\"contrast\"\n            >\n              Get started with Inception\n            </Link>\n          </PanelBanner>\n        </Flex>\n        {view === \"projects\" && (\n          <Projects\n            projects={projects}\n            userPlanFeatures={userPlanFeatures}\n            publisherHost={publisherHost}\n            projectsTags={user.projectsTags}\n          />\n        )}\n        {view === \"templates\" && <Templates projects={templates} />}\n        {view === \"welcome\" && <Templates projects={templates} welcome />}\n        {view === \"search\" && <SearchResults {...data} />}\n      </Flex>\n      <CloneProject projectToClone={projectToClone} />\n      <Toaster />\n    </TooltipProvider>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/index.client.ts",
    "content": "export * from \"./dashboard\";\n"
  },
  {
    "path": "apps/builder/app/dashboard/profile-menu.tsx",
    "content": "import { forwardRef } from \"react\";\nimport { ChevronDownIcon, UpgradeIcon } from \"@webstudio-is/icons\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  Avatar,\n  theme,\n  Button,\n  ProBadge,\n  DropdownMenuSeparator,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport { useNavigate } from \"@remix-run/react\";\nimport { logoutPath, userPlanSubscriptionPath } from \"~/shared/router-utils\";\nimport type { User } from \"~/shared/db/user.server\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\n\nconst getAvatarLetter = (title?: string) => {\n  return (title || \"X\").charAt(0).toLocaleUpperCase();\n};\n\nconst defaultUserName = \"James Bond\";\n\nconst ProfileButton = forwardRef<\n  HTMLButtonElement,\n  {\n    name: string;\n    image?: string;\n    hasPurchases?: boolean;\n  }\n>(({ image, name, hasPurchases, ...rest }, forwardedRef) => {\n  return (\n    <Button\n      color=\"ghost\"\n      aria-label=\"Profile Menu\"\n      {...rest}\n      ref={forwardedRef}\n      prefix={\n        <Avatar src={image} fallback={getAvatarLetter(name)} alt={name} />\n      }\n      suffix={<ChevronDownIcon size={12} />}\n      css={{\n        // Exception for avatar. May need to introduce a 32px controls size later.\n        height: theme.spacing[13],\n      }}\n    >\n      {hasPurchases === false && <ProBadge>Free</ProBadge>}\n    </Button>\n  );\n});\n\nexport const ProfileMenu = ({\n  user,\n  userPlanFeatures,\n}: {\n  user: User;\n  userPlanFeatures: UserPlanFeatures;\n}) => {\n  const navigate = useNavigate();\n  const nameOrEmail = user.username ?? user.email ?? defaultUserName;\n  const purchases = userPlanFeatures.purchases;\n  const hasPaidPlan = purchases.length > 0;\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <ProfileButton\n          image={user.image || undefined}\n          name={nameOrEmail}\n          hasPurchases={hasPaidPlan}\n        />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"start\" width=\"regular\">\n        <DropdownMenuLabel>\n          {user.username ?? defaultUserName}\n          <Text>{user.email}</Text>\n        </DropdownMenuLabel>\n        {purchases.length > 0 && (\n          <>\n            <DropdownMenuSeparator />\n            <DropdownMenuLabel>Purchases</DropdownMenuLabel>\n          </>\n        )}\n        {purchases.map((purchase, index) =>\n          purchase.subscriptionId ? (\n            <DropdownMenuItem\n              key={purchase.subscriptionId}\n              onSelect={() =>\n                navigate(userPlanSubscriptionPath(purchase.subscriptionId))\n              }\n            >\n              {purchase.planName}\n            </DropdownMenuItem>\n          ) : (\n            <DropdownMenuLabel key={index}>\n              {purchase.planName}\n            </DropdownMenuLabel>\n          )\n        )}\n        {hasPaidPlan === false && (\n          <DropdownMenuItem\n            onSelect={() => {\n              window.open(\"https://webstudio.is/pricing\");\n            }}\n            css={{ gap: theme.spacing[3] }}\n          >\n            <UpgradeIcon />\n            <div>Upgrade</div>\n          </DropdownMenuItem>\n        )}\n        <DropdownMenuSeparator />\n        <DropdownMenuItem onSelect={() => navigate(logoutPath())}>\n          Sign Out\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/colors.ts",
    "content": "export const colors = Array.from({ length: 50 }, (_, i) => {\n  const l = 55 + (i % 3) * 3; // Reduced variation in lightness (55-61%) to lower contrast\n  const c = 0.14 + (i % 2) * 0.02; // Reduced variation in chroma (0.14-0.16) for balance\n  const h = (i * 137.5) % 360; // Golden angle for pleasing hue distribution\n  return `oklch(${l}% ${c.toFixed(2)} ${h.toFixed(1)})`;\n});\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/project-card.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport {\n  css,\n  Flex,\n  Text,\n  truncate,\n  theme,\n  Tooltip,\n  rawTheme,\n  Link,\n  Box,\n} from \"@webstudio-is/design-system\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport { builderUrl } from \"~/shared/router-utils\";\nimport { ProjectDialogs, type DialogType } from \"./project-dialogs\";\nimport {\n  ThumbnailLinkWithAbbr,\n  ThumbnailLinkWithImage,\n} from \"../shared/thumbnail\";\nimport { Spinner } from \"../shared/spinner\";\nimport { Card, CardContent, CardFooter } from \"../shared/card\";\nimport type { User } from \"~/shared/db/user.server\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\nimport { ProjectMenu } from \"./project-menu\";\nimport { formatDate } from \"./utils\";\n\nconst infoIconStyle = css({ flexShrink: 0 });\n\nconst PublishedLink = ({\n  domain,\n  tabIndex,\n}: {\n  domain: string;\n  tabIndex: number;\n}) => {\n  const publishedOrigin = `https://${domain}`;\n  return (\n    <Link\n      href={publishedOrigin}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      tabIndex={tabIndex}\n      color=\"subtle\"\n      underline=\"hover\"\n      css={truncate()}\n    >\n      {new URL(publishedOrigin).host}\n    </Link>\n  );\n};\n\ntype ProjectCardProps = {\n  project: DashboardProject;\n  userPlanFeatures: UserPlanFeatures;\n  publisherHost: string;\n  projectsTags: User[\"projectsTags\"];\n};\n\nexport const ProjectCard = ({\n  project: {\n    id,\n    title,\n    domain,\n    isPublished,\n    createdAt,\n    latestBuildVirtual,\n    previewImageAsset,\n    tags,\n    domainsVirtual,\n  },\n  userPlanFeatures,\n  publisherHost,\n  projectsTags,\n  ...props\n}: ProjectCardProps) => {\n  // Determine which domain to display: custom domain if available, otherwise wstd subdomain\n  const customDomain = domainsVirtual?.find(\n    (d: { domain: string; status: string; verified: boolean }) =>\n      d.status === \"ACTIVE\" && d.verified\n  )?.domain;\n  const displayDomain = customDomain ?? `${domain}.${publisherHost}`;\n  const [openDialog, setOpenDialog] = useState<DialogType | undefined>();\n  const [isHidden, setIsHidden] = useState(false);\n  const [isTransitioning, setIsTransitioning] = useState(false);\n\n  // Makes sure there are no project tags that reference deleted User tags.\n  // We are not deleting project tag from project.tags when deleting User tags.\n  const projectTagsIds = (tags || [])\n    .map((tagId) => {\n      const tag = projectsTags.find((tag) => tag.id === tagId);\n      return tag ? tag.id : undefined;\n    })\n    .filter(Boolean) as string[];\n\n  useEffect(() => {\n    const linkPath = builderUrl({ origin: window.origin, projectId: id });\n\n    const handleNavigate = (event: NavigateEvent) => {\n      if (event.destination.url === linkPath) {\n        setIsTransitioning(true);\n      }\n    };\n\n    if (window.navigation === undefined) {\n      return;\n    }\n\n    window.navigation.addEventListener(\"navigate\", handleNavigate);\n\n    return () => {\n      window.navigation.removeEventListener(\"navigate\", handleNavigate);\n    };\n  }, [id]);\n\n  const linkPath = builderUrl({ origin: window.origin, projectId: id });\n\n  return (\n    <Card hidden={isHidden} {...props}>\n      <CardContent\n        css={{\n          background: theme.colors.brandBackgroundProjectCardBack,\n          [`&:hover`]: {\n            \"--ws-project-card-prefetch-image-background\": `url(${linkPath}cgi/empty.gif)`,\n          },\n        }}\n      >\n        {/* This div with backgorundImage on card hover is used to prefetch DNS of the project domain on hover. */}\n        <Box\n          css={{\n            backgroundImage: `var(--ws-project-card-prefetch-image-background, none)`,\n            visibility: \"hidden\",\n            position: \"absolute\",\n            width: 1,\n            height: 1,\n            left: 0,\n            top: 0,\n            opacity: 0,\n          }}\n        />\n        <Flex\n          wrap=\"wrap\"\n          gap={1}\n          css={{\n            position: \"absolute\",\n            padding: theme.panel.padding,\n            bottom: 0,\n            zIndex: 1,\n          }}\n        >\n          {projectsTags.map((tag) => {\n            const isApplied = projectTagsIds.includes(tag.id);\n            if (isApplied) {\n              return (\n                <Text\n                  color=\"contrast\"\n                  key={tag.id}\n                  css={{\n                    background: \"oklch(0 0 0 / 0.3)\",\n                    borderRadius: theme.borderRadius[3],\n                    paddingInline: theme.spacing[3],\n                  }}\n                >{`#${tag.label}`}</Text>\n              );\n            }\n          })}\n        </Flex>\n        {previewImageAsset ? (\n          <ThumbnailLinkWithImage to={linkPath} name={previewImageAsset.name} />\n        ) : (\n          <ThumbnailLinkWithAbbr title={title} to={linkPath} />\n        )}\n        {isTransitioning && <Spinner delay={0} />}\n      </CardContent>\n      <CardFooter>\n        <Flex direction=\"column\" justify=\"around\" grow>\n          <Flex gap=\"1\">\n            <Text\n              variant=\"titles\"\n              userSelect=\"text\"\n              truncate\n              css={{ textTransform: \"none\" }}\n            >\n              {title}\n            </Text>\n            <Tooltip\n              variant=\"wrapped\"\n              content={\n                <Text variant=\"small\">\n                  Created: {formatDate(createdAt)}\n                  {latestBuildVirtual?.updatedAt && (\n                    <>\n                      <br />\n                      Last modified: {formatDate(latestBuildVirtual.updatedAt)}\n                    </>\n                  )}\n                  <br />\n                  {isPublished && latestBuildVirtual ? (\n                    <>Published: {formatDate(latestBuildVirtual.createdAt)}</>\n                  ) : (\n                    <>Not published</>\n                  )}\n                </Text>\n              }\n            >\n              <InfoCircleIcon\n                color={rawTheme.colors.foregroundSubtle}\n                tabIndex={-1}\n                className={infoIconStyle()}\n              />\n            </Tooltip>\n          </Flex>\n          {isPublished ? (\n            <PublishedLink domain={displayDomain} tabIndex={-1} />\n          ) : (\n            <Text color=\"subtle\">Not published</Text>\n          )}\n        </Flex>\n        <ProjectMenu projectId={id} onOpenChange={setOpenDialog} />\n      </CardFooter>\n      <ProjectDialogs\n        projectId={id}\n        title={title}\n        tags={tags}\n        openDialog={openDialog}\n        onOpenDialogChange={setOpenDialog}\n        onHiddenChange={setIsHidden}\n        userPlanFeatures={userPlanFeatures}\n        projectsTags={projectsTags}\n      />\n    </Card>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/project-dialogs.tsx",
    "content": "import { useRevalidator } from \"@remix-run/react\";\nimport { useEffect, useState, type JSX } from \"react\";\nimport {\n  Box,\n  Button,\n  Flex,\n  Label,\n  Text,\n  InputField,\n  DialogActions,\n  Dialog as BaseDialog,\n  DialogTrigger,\n  DialogContent as DialogContentBase,\n  DialogTitle,\n  DialogClose,\n  DialogDescription,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { PlusIcon } from \"@webstudio-is/icons\";\nimport { Title } from \"@webstudio-is/project\";\nimport { builderUrl } from \"~/shared/router-utils\";\nimport { ShareProjectContainer } from \"~/shared/share-project\";\nimport { trpcClient } from \"~/shared/trpc/trpc-client\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport {\n  ProjectSettingsDialog,\n  type SectionName,\n} from \"~/shared/project-settings\";\nimport type { User } from \"~/shared/db/user.server\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\nimport { TagsDialog } from \"./tags\";\nimport {\n  destroyClientSync,\n  initializeClientSync,\n} from \"~/shared/sync/sync-client\";\nimport { $userPlanFeatures } from \"~/shared/nano-states\";\n\nexport type DialogType = \"rename\" | \"delete\" | \"share\" | \"tags\" | \"settings\";\n\ntype DialogProps = {\n  title: string;\n  children: JSX.Element | Array<JSX.Element>;\n  trigger?: JSX.Element;\n  onOpenChange?: (open: boolean) => void;\n  isOpen?: boolean;\n};\n\nconst Dialog = ({\n  title,\n  children,\n  trigger,\n  onOpenChange,\n  isOpen,\n}: DialogProps) => {\n  return (\n    <BaseDialog open={isOpen} onOpenChange={onOpenChange}>\n      {trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}\n      <DialogContentBase>\n        {children}\n        <DialogTitle>{title}</DialogTitle>\n      </DialogContentBase>\n    </BaseDialog>\n  );\n};\n\nconst DialogContent = ({\n  onSubmit,\n  onChange,\n  placeholder,\n  errors,\n  primaryButton,\n  title,\n  description,\n  label,\n  width,\n}: {\n  onSubmit: (data: { title: string }) => void;\n  onChange?: (data: { title: string }) => void;\n  errors?: string;\n  placeholder?: string;\n  title?: string;\n  label: string | JSX.Element;\n  description?: string;\n  primaryButton: JSX.Element;\n  width?: string;\n}) => {\n  return (\n    <form\n      onSubmit={(event) => {\n        event.preventDefault();\n        const formData = new FormData(event.currentTarget as HTMLFormElement);\n        const title = String(formData.get(\"title\") ?? \"\").trim();\n        onSubmit({ title });\n      }}\n    >\n      <Flex\n        direction=\"column\"\n        css={{\n          px: theme.spacing[\"7\"],\n          paddingTop: theme.spacing[\"5\"],\n          width,\n        }}\n        gap=\"1\"\n      >\n        {description && (\n          <DialogDescription asChild>\n            <Text as=\"p\">{description}</Text>\n          </DialogDescription>\n        )}\n        {typeof label === \"string\" ? <Label>{label}</Label> : label}\n        <InputField\n          placeholder={placeholder}\n          name=\"title\"\n          defaultValue={title}\n          color={errors ? \"error\" : undefined}\n          onChange={(event) => {\n            onChange?.({ title: event.currentTarget.value });\n          }}\n        />\n        <Box css={{ minHeight: theme.spacing[\"10\"] }}>\n          {errors && <Text color=\"destructive\">{errors}</Text>}\n        </Box>\n      </Flex>\n      <DialogActions>\n        {primaryButton}\n        <DialogClose>\n          <Button color=\"ghost\">Cancel</Button>\n        </DialogClose>\n      </DialogActions>\n    </form>\n  );\n};\n\nconst useCreateProject = () => {\n  const { send, state } = trpcClient.dashboardProject.create.useMutation();\n  const [errors, setErrors] = useState<string>();\n\n  const handleSubmit = ({ title }: { title: string }) => {\n    const parsed = Title.safeParse(title);\n    const errors =\n      \"error\" in parsed\n        ? parsed.error?.issues.map((issue) => issue.message).join(\"\\n\")\n        : undefined;\n    setErrors(errors);\n    if (parsed.success) {\n      send({ title }, (data) => {\n        if (data?.id) {\n          window.location.href = builderUrl({\n            origin: window.origin,\n            projectId: data.id,\n          });\n        }\n      });\n    }\n  };\n\n  const handleOpenChange = () => {\n    setErrors(undefined);\n  };\n\n  return {\n    handleSubmit,\n    handleOpenChange,\n    state,\n    errors,\n  };\n};\n\nexport const CreateProject = ({\n  buttonText = \"New blank project\",\n}: {\n  buttonText?: string;\n}) => {\n  const { handleSubmit, handleOpenChange, state, errors } = useCreateProject();\n\n  return (\n    <Dialog\n      title=\"New Project\"\n      trigger={<Button prefix={<PlusIcon size={12} />}>{buttonText}</Button>}\n      onOpenChange={handleOpenChange}\n    >\n      <DialogContent\n        onSubmit={handleSubmit}\n        placeholder=\"My Project\"\n        label=\"Project Title\"\n        errors={errors}\n        primaryButton={\n          <Button\n            state={state === \"idle\" ? undefined : \"pending\"}\n            type=\"submit\"\n          >\n            Create Project\n          </Button>\n        }\n      />\n    </Dialog>\n  );\n};\n\nconst useRenameProject = ({\n  projectId,\n  onOpenChange,\n}: {\n  projectId: string;\n  onOpenChange: (isOpen: boolean) => void;\n}) => {\n  const { send, state } = trpcClient.dashboardProject.rename.useMutation();\n  const [errors, setErrors] = useState<string>();\n  const revalidator = useRevalidator();\n\n  const handleSubmit = ({ title }: { title: string }) => {\n    const parsed = Title.safeParse(title);\n    const errors =\n      \"error\" in parsed\n        ? parsed.error?.issues.map((issue) => issue.message).join(\"\\n\")\n        : undefined;\n    setErrors(errors);\n    if (parsed.success) {\n      send({ projectId, title }, () => {\n        revalidator.revalidate();\n      });\n      onOpenChange(false);\n    }\n  };\n\n  return {\n    handleSubmit,\n    errors,\n    state,\n  };\n};\n\nexport const RenameProjectDialog = ({\n  isOpen,\n  title,\n  projectId,\n  onOpenChange,\n}: {\n  isOpen: boolean;\n  title: string;\n  projectId: string;\n  onOpenChange: (isOpen: boolean) => void;\n}) => {\n  const { handleSubmit, errors, state } = useRenameProject({\n    projectId,\n    onOpenChange,\n  });\n  return (\n    <Dialog title=\"Rename\" isOpen={isOpen} onOpenChange={onOpenChange}>\n      <DialogContent\n        onSubmit={handleSubmit}\n        errors={errors}\n        title={title}\n        label=\"Project Title\"\n        primaryButton={\n          <Button\n            type=\"submit\"\n            state={state === \"idle\" ? undefined : \"pending\"}\n          >\n            Rename Project\n          </Button>\n        }\n      />\n    </Dialog>\n  );\n};\n\nconst useDeleteProject = ({\n  projectId,\n  title,\n  onOpenChange,\n  onHiddenChange,\n}: {\n  projectId: string;\n  title: string;\n  onOpenChange: (isOpen: boolean) => void;\n  onHiddenChange: (isHidden: boolean) => void;\n}) => {\n  const { send, data, state } =\n    trpcClient.dashboardProject.delete.useMutation();\n  const [isMatch, setIsMatch] = useState(false);\n  const errors = data && \"errors\" in data ? data.errors : undefined;\n  const revalidator = useRevalidator();\n\n  useEffect(() => {\n    if (errors) {\n      onOpenChange(true);\n      onHiddenChange(false);\n    }\n  }, [errors, onOpenChange, onHiddenChange]);\n\n  const handleSubmit = () => {\n    send({ projectId }, () => {\n      revalidator.revalidate();\n    });\n    onHiddenChange(true);\n    onOpenChange(false);\n  };\n\n  const handleChange = ({ title: currentTitle }: { title: string }) => {\n    setIsMatch(\n      currentTitle.trim().toLocaleLowerCase() ===\n        title.trim().toLocaleLowerCase()\n    );\n  };\n\n  return {\n    handleSubmit,\n    handleChange,\n    errors,\n    isMatch,\n    state,\n  };\n};\n\nexport const DeleteProjectDialog = ({\n  isOpen,\n  title,\n  projectId,\n  onOpenChange,\n  onHiddenChange,\n}: {\n  isOpen: boolean;\n  title: string;\n  projectId: string;\n  onOpenChange: (isOpen: boolean) => void;\n  onHiddenChange: (isHidden: boolean) => void;\n}) => {\n  const { handleSubmit, handleChange, errors, isMatch, state } =\n    useDeleteProject({\n      projectId,\n      title,\n      onOpenChange,\n      onHiddenChange,\n    });\n  return (\n    <Dialog\n      title=\"Delete Confirmation\"\n      isOpen={isOpen}\n      onOpenChange={onOpenChange}\n    >\n      <DialogContent\n        onSubmit={handleSubmit}\n        onChange={handleChange}\n        errors={errors}\n        label={\n          <Label css={{ userSelect: \"text\" }}>\n            Confirm by typing\n            <Text\n              as=\"span\"\n              color=\"destructive\"\n              variant=\"labels\"\n              css={{ userSelect: \"text\" }}\n            >\n              {` ${title} `}\n            </Text>\n            below.\n          </Label>\n        }\n        description=\"This project and its styles, pages and images will be deleted permanently.\"\n        primaryButton={\n          <Button\n            type=\"submit\"\n            color=\"destructive\"\n            disabled={isMatch === false}\n            state={state === \"idle\" ? undefined : \"pending\"}\n          >\n            Delete Forever\n          </Button>\n        }\n        width={theme.spacing[\"33\"]}\n      />\n    </Dialog>\n  );\n};\n\nexport const useDuplicateProject = (projectId: string) => {\n  const { send } = trpcClient.dashboardProject.clone.useMutation();\n  const revalidator = useRevalidator();\n\n  return () => {\n    send({ projectId }, () => {\n      revalidator.revalidate();\n    });\n  };\n};\n\nexport const ShareProjectDialog = ({\n  isOpen,\n  onOpenChange,\n  projectId,\n}: {\n  isOpen: boolean;\n  onOpenChange: (isOpen: boolean) => void;\n  projectId: string;\n}) => {\n  return (\n    <Dialog title=\"Share Project\" isOpen={isOpen} onOpenChange={onOpenChange}>\n      <ShareProjectContainer projectId={projectId} />\n    </Dialog>\n  );\n};\n\n/**\n * Container component that manages data loading for ProjectSettingsDialog.\n * Handles sync initialization when the dialog is opened from the dashboard.\n */\nconst ProjectSettingsDialogContainer = ({\n  projectId,\n  onOpenChange,\n  isOpen,\n  userPlanFeatures,\n}: {\n  projectId: string;\n  onOpenChange: (isOpen: boolean) => void;\n  isOpen: boolean;\n  userPlanFeatures: UserPlanFeatures;\n}) => {\n  const [currentSection, setCurrentSection] = useState<\n    SectionName | undefined\n  >();\n  const [loadingState, setLoadingState] = useState<\n    \"idle\" | \"loading\" | \"loaded\"\n  >(\"idle\");\n\n  // Set section and user plan features when dialog opens\n  useEffect(() => {\n    if (isOpen) {\n      setCurrentSection(\"general\");\n      $userPlanFeatures.set(userPlanFeatures);\n      setLoadingState(\"loading\");\n    } else {\n      setCurrentSection(undefined);\n      setLoadingState(\"idle\");\n      // Reset data stores and stop sync when dialog closes\n      destroyClientSync();\n    }\n  }, [isOpen, userPlanFeatures]);\n\n  // Initialize sync when settings dialog is opened\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n\n    // Initialize sync which will load data, start project sync, and start polling\n    const controller = new AbortController();\n\n    initializeClientSync({\n      projectId,\n      authPermit: \"own\", // Dashboard projects are always owned by the current user\n      signal: controller.signal,\n      onReady() {\n        setLoadingState(\"loaded\");\n      },\n    });\n\n    return () => {\n      controller.abort(\"settings-closed\");\n    };\n  }, [isOpen, projectId]);\n\n  return (\n    <ProjectSettingsDialog\n      projectId={projectId}\n      currentSection={currentSection}\n      onSectionChange={setCurrentSection}\n      onOpenChange={onOpenChange}\n      status={loadingState}\n    />\n  );\n};\n\ntype ProjectDialogsProps = {\n  projectId: string;\n  title: string;\n  tags: DashboardProject[\"tags\"];\n  openDialog: DialogType | undefined;\n  onOpenDialogChange: (dialog: DialogType | undefined) => void;\n  onHiddenChange: (isHidden: boolean) => void;\n  userPlanFeatures: UserPlanFeatures;\n  projectsTags: User[\"projectsTags\"];\n};\n\n/**\n * Shared component that handles all project dialogs.\n */\nexport const ProjectDialogs = ({\n  projectId,\n  title,\n  tags,\n  openDialog,\n  onOpenDialogChange,\n  onHiddenChange,\n  userPlanFeatures,\n  projectsTags,\n}: ProjectDialogsProps) => {\n  const projectTagsIds = (tags || [])\n    .map((tagId) => {\n      const tag = projectsTags.find((tag) => tag.id === tagId);\n      return tag ? tag.id : undefined;\n    })\n    .filter(Boolean) as string[];\n\n  return (\n    <>\n      <RenameProjectDialog\n        isOpen={openDialog === \"rename\"}\n        onOpenChange={(open) => onOpenDialogChange(open ? \"rename\" : undefined)}\n        title={title}\n        projectId={projectId}\n      />\n      <DeleteProjectDialog\n        isOpen={openDialog === \"delete\"}\n        onOpenChange={(open) => onOpenDialogChange(open ? \"delete\" : undefined)}\n        onHiddenChange={onHiddenChange}\n        title={title}\n        projectId={projectId}\n      />\n      <ShareProjectDialog\n        isOpen={openDialog === \"share\"}\n        onOpenChange={(open) => onOpenDialogChange(open ? \"share\" : undefined)}\n        projectId={projectId}\n      />\n      <TagsDialog\n        projectId={projectId}\n        projectsTags={projectsTags}\n        projectTagsIds={projectTagsIds}\n        isOpen={openDialog === \"tags\"}\n        onOpenChange={(open) => onOpenDialogChange(open ? \"tags\" : undefined)}\n      />\n      <ProjectSettingsDialogContainer\n        projectId={projectId}\n        onOpenChange={(open) =>\n          onOpenDialogChange(open ? \"settings\" : undefined)\n        }\n        isOpen={openDialog === \"settings\"}\n        userPlanFeatures={userPlanFeatures}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/project-menu.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  IconButton,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport { EllipsesIcon } from \"@webstudio-is/icons\";\nimport type { DialogType } from \"./project-dialogs\";\nimport { useDuplicateProject } from \"./project-dialogs\";\nimport { builderUrl } from \"~/shared/router-utils\";\n\ntype ProjectMenuProps = {\n  projectId: string;\n  onOpenChange: (dialog: DialogType) => void;\n};\n\nexport const ProjectMenu = ({ projectId, onOpenChange }: ProjectMenuProps) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const handleDuplicateProject = useDuplicateProject(projectId);\n\n  const handleOpenInSafeMode = () => {\n    window.location.href = builderUrl({\n      origin: window.origin,\n      projectId,\n      safemode: true,\n    });\n  };\n\n  return (\n    <DropdownMenu open={isOpen} onOpenChange={setIsOpen}>\n      <DropdownMenuTrigger asChild>\n        <IconButton\n          aria-label=\"Menu Button\"\n          tabIndex={-1}\n          css={{ alignSelf: \"center\", position: \"relative\", zIndex: 1 }}\n        >\n          <EllipsesIcon width={15} height={15} />\n        </IconButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\" css={{ width: theme.spacing[24] }}>\n        <DropdownMenuItem onSelect={handleDuplicateProject}>\n          Duplicate\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => onOpenChange(\"rename\")}>\n          Rename\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => onOpenChange(\"share\")}>\n          Share\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => onOpenChange(\"delete\")}>\n          Delete\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => onOpenChange(\"tags\")}>\n          Tags\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={() => onOpenChange(\"settings\")}>\n          Settings\n        </DropdownMenuItem>\n        <DropdownMenuItem onSelect={handleOpenInSafeMode}>\n          Open in safe mode\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/projects-list.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Flex,\n  Text,\n  theme,\n  Link,\n  css,\n  List,\n  ListItem,\n  IconButton,\n} from \"@webstudio-is/design-system\";\nimport { ChevronUpIcon, ChevronDownIcon } from \"@webstudio-is/icons\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport { builderUrl } from \"~/shared/router-utils\";\nimport { ProjectDialogs, type DialogType } from \"./project-dialogs\";\nimport type { User } from \"~/shared/db/user.server\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\nimport { ProjectMenu } from \"./project-menu\";\nimport { formatDate } from \"./utils\";\nimport type { SortField, SortOrder } from \"./sort\";\n\nconst tableStyles = css({\n  display: \"table\",\n  width: \"100%\",\n  tableLayout: \"fixed\",\n  borderCollapse: \"collapse\",\n  marginBottom: theme.spacing[13],\n  minWidth: 550,\n  flexGrow: 1,\n\n  '& [role=\"rowgroup\"]': {\n    display: \"table-row-group\",\n  },\n\n  '& [role=\"row\"]': {\n    display: \"table-row\",\n    position: \"relative\",\n    \"&:focus-visible\": {\n      outline: `1px solid ${theme.colors.borderFocus}`,\n    },\n    '&:has([role=\"cell\"]):hover': {\n      background: theme.colors.backgroundHover,\n    },\n  },\n\n  '& [role=\"columnheader\"]': {\n    display: \"table-cell\",\n    padding: theme.spacing[5],\n    paddingBottom: theme.spacing[3],\n    textAlign: \"left\",\n    borderBottom: `1px solid ${theme.colors.borderMain}`,\n    \"&:first-child\": {\n      width: \"35%\",\n    },\n    \"&:nth-child(2), &:nth-child(3), &:nth-child(4)\": {\n      width: \"20%\",\n    },\n    \"&:last-child\": {\n      width: \"5%\",\n    },\n  },\n\n  '& [role=\"cell\"]': {\n    display: \"table-cell\",\n    padding: theme.spacing[5],\n    verticalAlign: \"middle\",\n  },\n});\n\ntype ProjectsListItemProps = {\n  project: DashboardProject;\n  userPlanFeatures: UserPlanFeatures;\n  publisherHost: string;\n  projectsTags: User[\"projectsTags\"];\n};\n\nexport const ProjectsListItem = ({\n  project: {\n    id,\n    title,\n    domain,\n    isPublished,\n    createdAt,\n    latestBuildVirtual,\n    tags,\n    domainsVirtual,\n  },\n  userPlanFeatures,\n  publisherHost,\n  projectsTags,\n}: ProjectsListItemProps) => {\n  const customDomain = domainsVirtual?.find(\n    (d: { domain: string; status: string; verified: boolean }) =>\n      d.status === \"ACTIVE\" && d.verified\n  )?.domain;\n  const displayDomain = customDomain ?? `${domain}.${publisherHost}`;\n  const [openDialog, setOpenDialog] = useState<DialogType | undefined>();\n  const [isHidden, setIsHidden] = useState(false);\n\n  if (isHidden) {\n    return;\n  }\n\n  const linkPath = builderUrl({ origin: window.origin, projectId: id });\n\n  return (\n    <>\n      <ListItem index={0} asChild>\n        <div role=\"row\">\n          <div role=\"cell\">\n            <Flex direction=\"column\" gap=\"1\">\n              <Link\n                href={linkPath}\n                color=\"inherit\"\n                underline=\"none\"\n                tabIndex={-1}\n                stretched\n              >\n                {title}\n              </Link>\n              {isPublished && (\n                <Link\n                  href={`https://${displayDomain}`}\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                  color=\"subtle\"\n                  underline=\"hover\"\n                  tabIndex={-1}\n                  aria-label={`Visit ${title} website at ${displayDomain}`}\n                  css={{ zIndex: 1 }}\n                >\n                  {displayDomain}\n                </Link>\n              )}\n            </Flex>\n          </div>\n\n          <div role=\"cell\">\n            <Text color=\"subtle\">\n              {latestBuildVirtual?.updatedAt\n                ? formatDate(latestBuildVirtual.updatedAt)\n                : formatDate(createdAt)}\n            </Text>\n          </div>\n\n          <div role=\"cell\">\n            <Text color=\"subtle\">\n              {isPublished && latestBuildVirtual\n                ? formatDate(latestBuildVirtual.createdAt)\n                : \"Not published\"}\n            </Text>\n          </div>\n\n          <div role=\"cell\">\n            <Text color=\"subtle\">{formatDate(createdAt)}</Text>\n          </div>\n\n          <div role=\"cell\">\n            <ProjectMenu projectId={id} onOpenChange={setOpenDialog} />\n          </div>\n        </div>\n      </ListItem>\n\n      <ProjectDialogs\n        projectId={id}\n        title={title}\n        tags={tags}\n        openDialog={openDialog}\n        onOpenDialogChange={setOpenDialog}\n        onHiddenChange={setIsHidden}\n        userPlanFeatures={userPlanFeatures}\n        projectsTags={projectsTags}\n      />\n    </>\n  );\n};\n\ntype ProjectsListProps = {\n  projects: Array<DashboardProject>;\n  userPlanFeatures: UserPlanFeatures;\n  publisherHost: string;\n  projectsTags: User[\"projectsTags\"];\n  sortBy?: SortField;\n  sortOrder?: SortOrder;\n  onSortChange: (field: SortField) => void;\n};\n\nconst columns: Array<{ field: SortField; label: string } | null> = [\n  { field: \"title\", label: \"Name\" },\n  { field: \"updatedAt\", label: \"Last modified\" },\n  { field: \"publishedAt\", label: \"Last published\" },\n  { field: \"createdAt\", label: \"Date created\" },\n  null, // Actions column (no sorting)\n];\n\nexport const ProjectsList = ({\n  projects,\n  userPlanFeatures,\n  publisherHost,\n  projectsTags,\n  sortBy,\n  sortOrder,\n  onSortChange,\n}: ProjectsListProps) => {\n  return (\n    <div className={tableStyles()} role=\"table\" aria-label=\"Projects list\">\n      <List asChild>\n        <div role=\"rowgroup\">\n          <ListItem index={0} asChild>\n            <div role=\"row\">\n              {columns.map((column, index) => (\n                <div role=\"columnheader\" key={index}>\n                  {column ? (\n                    <Flex gap=\"1\" align=\"center\">\n                      <Text\n                        variant=\"regularBold\"\n                        id={`sort-${column.field}-label`}\n                      >\n                        {column.label}\n                      </Text>\n                      <IconButton\n                        onClick={() => onSortChange(column.field)}\n                        css={{ opacity: sortBy === column.field ? 1 : 0.5 }}\n                        aria-label={`Sort by ${column.label}${\n                          sortBy === column.field\n                            ? sortOrder === \"asc\"\n                              ? \", sorted ascending\"\n                              : \", sorted descending\"\n                            : \", not sorted\"\n                        }`}\n                        aria-describedby={`sort-${column.field}-label`}\n                        tabIndex={-1}\n                      >\n                        {sortBy === column.field && sortOrder === \"asc\" ? (\n                          <ChevronUpIcon />\n                        ) : (\n                          <ChevronDownIcon />\n                        )}\n                      </IconButton>\n                    </Flex>\n                  ) : undefined}\n                </div>\n              ))}\n            </div>\n          </ListItem>\n        </div>\n      </List>\n\n      <List asChild>\n        <div role=\"rowgroup\">\n          {projects.map((project) => (\n            <ProjectsListItem\n              key={project.id}\n              project={project}\n              userPlanFeatures={userPlanFeatures}\n              publisherHost={publisherHost}\n              projectsTags={projectsTags}\n            />\n          ))}\n        </div>\n      </List>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/projects.tsx",
    "content": "import {\n  Flex,\n  Grid,\n  List,\n  ListItem,\n  Text,\n  rawTheme,\n  theme,\n  ToggleGroup,\n  ToggleGroupButton,\n} from \"@webstudio-is/design-system\";\nimport { RepeatGridIcon, ListViewIcon } from \"@webstudio-is/icons\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport { ProjectCard } from \"./project-card\";\nimport { CreateProject } from \"./project-dialogs\";\nimport { Header, Main } from \"../shared/layout\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { setIsSubsetOf } from \"~/shared/shim\";\nimport type { User } from \"~/shared/db/user.server\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\nimport { Tag } from \"./tags\";\nimport {\n  SortSelect,\n  sortProjects,\n  type SortState,\n  type SortField,\n} from \"./sort\";\nimport { ProjectsList } from \"./projects-list\";\n\nexport const ProjectsGrid = ({\n  projects,\n  userPlanFeatures,\n  publisherHost,\n  projectsTags,\n}: ProjectsProps) => {\n  return (\n    <List asChild>\n      <Grid\n        gap=\"6\"\n        css={{\n          gridTemplateColumns: `repeat(auto-fill, minmax(${rawTheme.spacing[31]}, 1fr))`,\n          paddingBottom: theme.spacing[13],\n          width: \"100%\",\n        }}\n      >\n        {projects.map((project) => {\n          return (\n            <ListItem index={0} key={project.id} asChild>\n              <ProjectCard\n                project={project}\n                userPlanFeatures={userPlanFeatures}\n                publisherHost={publisherHost}\n                projectsTags={projectsTags}\n              />\n            </ListItem>\n          );\n        })}\n      </Grid>\n    </List>\n  );\n};\n\ntype ProjectsProps = {\n  projects: Array<DashboardProject>;\n  userPlanFeatures: UserPlanFeatures;\n  publisherHost: string;\n  projectsTags: User[\"projectsTags\"];\n};\n\nexport const Projects = (props: ProjectsProps) => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const selectedTags = searchParams.getAll(\"tag\");\n  const viewMode = (searchParams.get(\"view\") as \"grid\" | \"list\") ?? \"grid\";\n\n  const sortState: SortState = {\n    sortBy: searchParams.get(\"sortBy\") as SortState[\"sortBy\"],\n    order: searchParams.get(\"order\") as SortState[\"order\"],\n  };\n\n  const handleSortChange = (newSortState: Required<SortState>) => {\n    const newParams = new URLSearchParams(searchParams);\n    newParams.set(\"sortBy\", newSortState.sortBy);\n    newParams.set(\"order\", newSortState.order);\n    setSearchParams(newParams);\n  };\n\n  const handleTableSort = (field: SortField) => {\n    const currentSortBy = sortState.sortBy ?? \"updatedAt\";\n    const currentOrder = sortState.order ?? \"desc\";\n\n    // If clicking the same field, toggle the order\n    if (field === currentSortBy) {\n      const newOrder = currentOrder === \"asc\" ? \"desc\" : \"asc\";\n      handleSortChange({ sortBy: field, order: newOrder });\n    } else {\n      // When switching to a new field, use smart defaults\n      const newOrder = field === \"title\" ? \"asc\" : \"desc\";\n      handleSortChange({ sortBy: field, order: newOrder });\n    }\n  };\n\n  const handleViewChange = (value: string) => {\n    const newParams = new URLSearchParams(searchParams);\n    if (value === \"grid\") {\n      newParams.delete(\"view\");\n    } else {\n      newParams.set(\"view\", value);\n    }\n    setSearchParams(newParams);\n  };\n\n  // Filter by tags\n  let projects = props.projects;\n  if (selectedTags.length > 0) {\n    projects = projects.filter((project) =>\n      setIsSubsetOf(new Set(selectedTags), new Set(project.tags))\n    );\n  }\n  projects = sortProjects(projects, sortState);\n\n  return (\n    <Main>\n      <Header variant=\"main\">\n        <Text variant=\"brandSectionTitle\" as=\"h2\">\n          Projects\n        </Text>\n        <Flex gap=\"2\">\n          <ToggleGroup\n            type=\"single\"\n            value={viewMode}\n            onValueChange={handleViewChange}\n          >\n            <ToggleGroupButton value=\"grid\" aria-label=\"Grid view\">\n              <RepeatGridIcon />\n            </ToggleGroupButton>\n            <ToggleGroupButton value=\"list\" aria-label=\"List view\">\n              <ListViewIcon />\n            </ToggleGroupButton>\n          </ToggleGroup>\n          <SortSelect value={sortState} onValueChange={handleSortChange} />\n          <CreateProject />\n        </Flex>\n      </Header>\n      <Flex\n        gap=\"2\"\n        shrink={false}\n        justify=\"between\"\n        css={{\n          paddingInline: theme.spacing[13],\n          paddingBlockStart: theme.spacing[2],\n          paddingBlockEnd: theme.spacing[10],\n        }}\n      >\n        <Flex gap=\"2\" wrap=\"wrap\" align=\"center\">\n          {props.projectsTags.map((tag, index) => {\n            return (\n              <Tag\n                tag={tag}\n                key={tag.id}\n                index={index}\n                state={selectedTags.includes(tag.id) ? \"pressed\" : \"auto\"}\n              >\n                {tag.label}\n              </Tag>\n            );\n          })}\n        </Flex>\n      </Flex>\n      <Flex css={{ paddingInline: theme.spacing[13] }}>\n        {projects.length === 0 ? (\n          <Text\n            variant=\"brandRegular\"\n            css={{\n              paddingBlock: theme.spacing[20],\n              textAlign: \"center\",\n              flexGrow: 1,\n            }}\n          >\n            No projects found\n          </Text>\n        ) : viewMode === \"grid\" ? (\n          <ProjectsGrid {...props} projects={projects} />\n        ) : (\n          <ProjectsList\n            {...props}\n            projects={projects}\n            sortBy={sortState.sortBy}\n            sortOrder={sortState.order}\n            onSortChange={handleTableSort}\n          />\n        )}\n      </Flex>\n    </Main>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/sort.test.ts",
    "content": "/**\n * Tests for the sortProjects function and sort order semantics.\n *\n * This test suite covers:\n * 1. All sort fields (title, createdAt, updatedAt, publishedAt)\n * 2. Both sort orders (asc, desc)\n * 3. Special cases (unpublished projects, fallback to createdAt)\n * 4. Immutability guarantees\n * 5. Edge cases (empty arrays, single items, identical values)\n * 6. Order semantics:\n *    - Alphabetical: asc = A→Z, desc = Z→A\n *    - Dates: asc = Oldest first, desc = Newest first\n */\n\nimport { describe, test, expect } from \"vitest\";\nimport { sortProjects } from \"./sort\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\n\ntype LatestBuildVirtual = NonNullable<DashboardProject[\"latestBuildVirtual\"]>;\n\nconst createMockProject = (\n  overrides: Partial<DashboardProject>\n): DashboardProject => {\n  return {\n    id: \"project-1\",\n    title: \"Test Project\",\n    domain: \"test\",\n    isDeleted: false,\n    createdAt: \"2024-01-01T00:00:00.000Z\",\n    marketplaceApprovalStatus: \"UNLISTED\",\n    latestBuildVirtual: null,\n    previewImageAsset: null,\n    tags: [],\n    isPublished: false,\n    ...overrides,\n  } as DashboardProject;\n};\n\ndescribe(\"sortProjects\", () => {\n  describe(\"sort by title\", () => {\n    test(\"sorts alphabetically in ascending order\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"Zebra\" }),\n        createMockProject({ id: \"2\", title: \"Apple\" }),\n        createMockProject({ id: \"3\", title: \"Mango\" }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n\n      expect(sorted.map((p) => p.title)).toEqual([\"Apple\", \"Mango\", \"Zebra\"]);\n    });\n\n    test(\"sorts alphabetically in descending order\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"Apple\" }),\n        createMockProject({ id: \"2\", title: \"Zebra\" }),\n        createMockProject({ id: \"3\", title: \"Mango\" }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"desc\" });\n\n      expect(sorted.map((p) => p.title)).toEqual([\"Zebra\", \"Mango\", \"Apple\"]);\n    });\n\n    test(\"handles case-insensitive sorting\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"zebra\" }),\n        createMockProject({ id: \"2\", title: \"Apple\" }),\n        createMockProject({ id: \"3\", title: \"MANGO\" }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n\n      expect(sorted.map((p) => p.title)).toEqual([\"Apple\", \"MANGO\", \"zebra\"]);\n    });\n  });\n\n  describe(\"sort by createdAt\", () => {\n    test(\"sorts by creation date in ascending order (oldest first)\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", createdAt: \"2024-03-01T00:00:00.000Z\" }),\n        createMockProject({ id: \"2\", createdAt: \"2024-01-01T00:00:00.000Z\" }),\n        createMockProject({ id: \"3\", createdAt: \"2024-02-01T00:00:00.000Z\" }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"createdAt\",\n        order: \"asc\",\n      });\n\n      expect(sorted.map((p) => p.id)).toEqual([\"2\", \"3\", \"1\"]);\n    });\n\n    test(\"sorts by creation date in descending order (newest first)\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", createdAt: \"2024-01-01T00:00:00.000Z\" }),\n        createMockProject({ id: \"2\", createdAt: \"2024-03-01T00:00:00.000Z\" }),\n        createMockProject({ id: \"3\", createdAt: \"2024-02-01T00:00:00.000Z\" }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"createdAt\",\n        order: \"desc\",\n      });\n\n      expect(sorted.map((p) => p.id)).toEqual([\"2\", \"3\", \"1\"]);\n    });\n  });\n\n  describe(\"sort by updatedAt (last modified)\", () => {\n    test(\"sorts by latest build updatedAt when available\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            buildId: \"build1\",\n            projectId: \"1\",\n            domainsVirtualId: \"\",\n            domain: \"test\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            updatedAt: \"2024-03-01T00:00:00.000Z\",\n            publishStatus: \"PUBLISHED\",\n          },\n        }),\n        createMockProject({\n          id: \"2\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            buildId: \"build2\",\n            projectId: \"2\",\n            domainsVirtualId: \"\",\n            domain: \"test\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            updatedAt: \"2024-01-15T00:00:00.000Z\",\n            publishStatus: \"PUBLISHED\",\n          },\n        }),\n        createMockProject({\n          id: \"3\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            buildId: \"build3\",\n            projectId: \"3\",\n            domainsVirtualId: \"\",\n            domain: \"test\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            updatedAt: \"2024-02-01T00:00:00.000Z\",\n            publishStatus: \"PUBLISHED\",\n          },\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"updatedAt\",\n        order: \"desc\",\n      });\n\n      expect(sorted.map((p) => p.id)).toEqual([\"1\", \"3\", \"2\"]);\n    });\n\n    test(\"falls back to createdAt when latestBuildVirtual is null\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          createdAt: \"2024-03-01T00:00:00.000Z\",\n          latestBuildVirtual: null,\n        }),\n        createMockProject({\n          id: \"2\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            buildId: \"build2\",\n            projectId: \"2\",\n            domainsVirtualId: \"\",\n            domain: \"test\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            updatedAt: \"2024-02-01T00:00:00.000Z\",\n            publishStatus: \"PUBLISHED\",\n          },\n        }),\n        createMockProject({\n          id: \"3\",\n          createdAt: \"2024-02-01T00:00:00.000Z\",\n          latestBuildVirtual: null,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"updatedAt\",\n        order: \"desc\",\n      });\n\n      // Project 1: March 1 (createdAt fallback)\n      // Project 2: Feb 1 (latestBuildVirtual.updatedAt)\n      // Project 3: Feb 1 (createdAt fallback)\n      expect(sorted.map((p) => p.id)).toEqual([\"1\", \"2\", \"3\"]);\n    });\n\n    test(\"sorts in ascending order (oldest first)\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            buildId: \"build1\",\n            projectId: \"1\",\n            domainsVirtualId: \"\",\n            domain: \"test\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            updatedAt: \"2024-03-01T00:00:00.000Z\",\n            publishStatus: \"PUBLISHED\",\n          },\n        }),\n        createMockProject({\n          id: \"2\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            buildId: \"build2\",\n            projectId: \"2\",\n            domainsVirtualId: \"\",\n            domain: \"test\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            updatedAt: \"2024-01-15T00:00:00.000Z\",\n            publishStatus: \"PUBLISHED\",\n          },\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"updatedAt\",\n        order: \"asc\",\n      });\n\n      expect(sorted.map((p) => p.id)).toEqual([\"2\", \"1\"]);\n    });\n  });\n\n  describe(\"sort by publishedAt (date published)\", () => {\n    test(\"sorts published projects by publish date\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-03-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"2\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"3\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-02-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"publishedAt\",\n        order: \"desc\",\n      });\n\n      expect(sorted.map((p) => p.id)).toEqual([\"1\", \"3\", \"2\"]);\n    });\n\n    test(\"puts unpublished projects at the end in descending order\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-02-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"2\",\n          isPublished: false,\n          latestBuildVirtual: null,\n        }),\n        createMockProject({\n          id: \"3\",\n          isPublished: false,\n          latestBuildVirtual: {\n            publishStatus: \"PENDING\",\n            createdAt: \"2024-03-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"4\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"publishedAt\",\n        order: \"desc\",\n      });\n\n      // Unpublished projects (isPublished: false) come at the end\n      // Published projects sorted by date descending\n      expect(sorted.map((p) => p.id)).toEqual([\"1\", \"4\", \"2\", \"3\"]);\n    });\n\n    test(\"puts unpublished projects at the end in ascending order\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-02-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"2\",\n          isPublished: false,\n          latestBuildVirtual: null,\n        }),\n        createMockProject({\n          id: \"3\",\n          isPublished: true,\n          latestBuildVirtual: {\n            publishStatus: \"PUBLISHED\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n          } as LatestBuildVirtual,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"publishedAt\",\n        order: \"asc\",\n      });\n\n      // Published projects first (sorted by date ascending), then unpublished\n      expect(sorted.map((p) => p.id)).toEqual([\"3\", \"1\", \"2\"]);\n    });\n\n    test(\"maintains order for multiple unpublished projects\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          isPublished: false,\n          latestBuildVirtual: null,\n        }),\n        createMockProject({\n          id: \"2\",\n          isPublished: false,\n          latestBuildVirtual: {\n            publishStatus: \"PENDING\",\n          } as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"3\",\n          isPublished: false,\n          latestBuildVirtual: null,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {\n        sortBy: \"publishedAt\",\n        order: \"desc\",\n      });\n\n      expect(sorted.map((p) => p.id)).toEqual([\"1\", \"2\", \"3\"]);\n    });\n  });\n\n  describe(\"immutability\", () => {\n    test(\"does not mutate the original array\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"B\" }),\n        createMockProject({ id: \"2\", title: \"A\" }),\n      ];\n\n      const originalOrder = projects.map((p) => p.id);\n      sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n\n      expect(projects.map((p) => p.id)).toEqual(originalOrder);\n    });\n\n    test(\"returns a new array\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"A\" }),\n        createMockProject({ id: \"2\", title: \"B\" }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n\n      expect(sorted).not.toBe(projects);\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"handles empty array\", () => {\n      const sorted = sortProjects([], { sortBy: \"title\", order: \"asc\" });\n      expect(sorted).toEqual([]);\n    });\n\n    test(\"handles single project\", () => {\n      const projects = [createMockProject({ id: \"1\", title: \"Only\" })];\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n      expect(sorted).toHaveLength(1);\n      expect(sorted[0].id).toBe(\"1\");\n    });\n\n    test(\"handles projects with identical values\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          title: \"Same\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n        }),\n        createMockProject({\n          id: \"2\",\n          title: \"Same\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n        }),\n        createMockProject({\n          id: \"3\",\n          title: \"Same\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n        }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n      expect(sorted).toHaveLength(3);\n    });\n  });\n\n  describe(\"default sort options\", () => {\n    test(\"defaults to updatedAt desc when no sort state provided\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-01-15T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"2\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-03-01T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"3\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-02-01T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n      ];\n\n      const sorted = sortProjects(projects);\n\n      // Default: updatedAt desc (newest first)\n      expect(sorted.map((p) => p.id)).toEqual([\"2\", \"3\", \"1\"]);\n    });\n\n    test(\"defaults to updatedAt when only order is provided\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-01-15T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"2\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-03-01T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, { order: \"asc\" });\n\n      // updatedAt asc (oldest first)\n      expect(sorted.map((p) => p.id)).toEqual([\"1\", \"2\"]);\n    });\n\n    test(\"defaults to desc order when only sortBy is provided\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"Apple\" }),\n        createMockProject({ id: \"2\", title: \"Zebra\" }),\n        createMockProject({ id: \"3\", title: \"Mango\" }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\" });\n\n      // title desc (Z→A)\n      expect(sorted.map((p) => p.title)).toEqual([\"Zebra\", \"Mango\", \"Apple\"]);\n    });\n\n    test(\"uses provided sortBy and order when both are specified\", () => {\n      const projects = [\n        createMockProject({ id: \"1\", title: \"Zebra\" }),\n        createMockProject({ id: \"2\", title: \"Apple\" }),\n        createMockProject({ id: \"3\", title: \"Mango\" }),\n      ];\n\n      const sorted = sortProjects(projects, { sortBy: \"title\", order: \"asc\" });\n\n      // title asc (A→Z)\n      expect(sorted.map((p) => p.title)).toEqual([\"Apple\", \"Mango\", \"Zebra\"]);\n    });\n\n    test(\"handles empty sort state object\", () => {\n      const projects = [\n        createMockProject({\n          id: \"1\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-01-15T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n        createMockProject({\n          id: \"2\",\n          createdAt: \"2024-01-01T00:00:00.000Z\",\n          latestBuildVirtual: {\n            updatedAt: \"2024-03-01T00:00:00.000Z\",\n          } as unknown as LatestBuildVirtual,\n        }),\n      ];\n\n      const sorted = sortProjects(projects, {});\n\n      // Default: updatedAt desc\n      expect(sorted.map((p) => p.id)).toEqual([\"2\", \"1\"]);\n    });\n  });\n\n  describe(\"order semantics\", () => {\n    describe(\"alphabetical sorting\", () => {\n      test(\"asc order produces A→Z sorting\", () => {\n        const projects = [\n          createMockProject({ id: \"1\", title: \"Zebra\" }),\n          createMockProject({ id: \"2\", title: \"Apple\" }),\n          createMockProject({ id: \"3\", title: \"Mango\" }),\n        ];\n\n        const sorted = sortProjects(projects, {\n          sortBy: \"title\",\n          order: \"asc\",\n        });\n\n        // A→Z: Apple, Mango, Zebra\n        expect(sorted.map((p) => p.title)).toEqual([\"Apple\", \"Mango\", \"Zebra\"]);\n      });\n\n      test(\"desc order produces Z→A sorting\", () => {\n        const projects = [\n          createMockProject({ id: \"1\", title: \"Apple\" }),\n          createMockProject({ id: \"2\", title: \"Zebra\" }),\n          createMockProject({ id: \"3\", title: \"Mango\" }),\n        ];\n\n        const sorted = sortProjects(projects, {\n          sortBy: \"title\",\n          order: \"desc\",\n        });\n\n        // Z→A: Zebra, Mango, Apple\n        expect(sorted.map((p) => p.title)).toEqual([\"Zebra\", \"Mango\", \"Apple\"]);\n      });\n    });\n\n    describe(\"date sorting\", () => {\n      test(\"desc order produces newest first for createdAt\", () => {\n        const projects = [\n          createMockProject({ id: \"1\", createdAt: \"2024-01-01T00:00:00.000Z\" }),\n          createMockProject({ id: \"2\", createdAt: \"2024-03-01T00:00:00.000Z\" }),\n          createMockProject({ id: \"3\", createdAt: \"2024-02-01T00:00:00.000Z\" }),\n        ];\n\n        const sorted = sortProjects(projects, {\n          sortBy: \"createdAt\",\n          order: \"desc\",\n        });\n\n        // Newest first: March, February, January\n        expect(sorted.map((p) => p.id)).toEqual([\"2\", \"3\", \"1\"]);\n      });\n\n      test(\"asc order produces oldest first for createdAt\", () => {\n        const projects = [\n          createMockProject({ id: \"1\", createdAt: \"2024-03-01T00:00:00.000Z\" }),\n          createMockProject({ id: \"2\", createdAt: \"2024-01-01T00:00:00.000Z\" }),\n          createMockProject({ id: \"3\", createdAt: \"2024-02-01T00:00:00.000Z\" }),\n        ];\n\n        const sorted = sortProjects(projects, {\n          sortBy: \"createdAt\",\n          order: \"asc\",\n        });\n\n        // Oldest first: January, February, March\n        expect(sorted.map((p) => p.id)).toEqual([\"2\", \"3\", \"1\"]);\n      });\n\n      test(\"desc order produces newest first for updatedAt\", () => {\n        const projects = [\n          createMockProject({\n            id: \"1\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            latestBuildVirtual: {\n              updatedAt: \"2024-01-15T00:00:00.000Z\",\n            } as unknown as LatestBuildVirtual,\n          }),\n          createMockProject({\n            id: \"2\",\n            createdAt: \"2024-01-01T00:00:00.000Z\",\n            latestBuildVirtual: {\n              updatedAt: \"2024-03-01T00:00:00.000Z\",\n            } as unknown as LatestBuildVirtual,\n          }),\n        ];\n\n        const sorted = sortProjects(projects, {\n          sortBy: \"updatedAt\",\n          order: \"desc\",\n        });\n\n        // Newest first: March update, January update\n        expect(sorted.map((p) => p.id)).toEqual([\"2\", \"1\"]);\n      });\n\n      test(\"desc order produces newest first for publishedAt\", () => {\n        const projects = [\n          createMockProject({\n            id: \"1\",\n            isPublished: true,\n            latestBuildVirtual: {\n              publishStatus: \"PUBLISHED\",\n              createdAt: \"2024-01-01T00:00:00.000Z\",\n            } as LatestBuildVirtual,\n          }),\n          createMockProject({\n            id: \"2\",\n            isPublished: true,\n            latestBuildVirtual: {\n              publishStatus: \"PUBLISHED\",\n              createdAt: \"2024-03-01T00:00:00.000Z\",\n            } as LatestBuildVirtual,\n          }),\n        ];\n\n        const sorted = sortProjects(projects, {\n          sortBy: \"publishedAt\",\n          order: \"desc\",\n        });\n\n        // Newest first: March publish, January publish\n        expect(sorted.map((p) => p.id)).toEqual([\"2\", \"1\"]);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/sort.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuLabel,\n  Button,\n  MenuCheckedIcon,\n} from \"@webstudio-is/design-system\";\nimport {\n  ChevronDownIcon,\n  CalendarIcon,\n  UploadIcon,\n  ArrowDownAZIcon,\n  ArrowDownZAIcon,\n} from \"@webstudio-is/icons\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\n\nexport type SortField = \"createdAt\" | \"title\" | \"updatedAt\" | \"publishedAt\";\nexport type SortOrder = \"asc\" | \"desc\";\n\nexport type SortState = {\n  sortBy?: SortField;\n  order?: SortOrder;\n};\n\nexport const sortProjects = (\n  projects: Array<DashboardProject>,\n  sortState: SortState = {}\n) => {\n  const sortBy = sortState.sortBy ?? \"updatedAt\";\n  const order = sortState.order ?? \"desc\";\n\n  const sorted = [...projects];\n\n  sorted.sort((a, b) => {\n    let comparison = 0;\n\n    if (sortBy === \"title\") {\n      const aTitle = a.title ?? \"\";\n      const bTitle = b.title ?? \"\";\n      comparison = aTitle.localeCompare(bTitle);\n    } else if (sortBy === \"createdAt\") {\n      const aCreated = a.createdAt ?? \"\";\n      const bCreated = b.createdAt ?? \"\";\n      comparison = new Date(aCreated).getTime() - new Date(bCreated).getTime();\n    } else if (sortBy === \"updatedAt\") {\n      // Uses Build's updatedAt field from latestBuildVirtual view, falls back to project createdAt\n      const aUpdated = a.latestBuildVirtual?.updatedAt || a.createdAt || \"\";\n      const bUpdated = b.latestBuildVirtual?.updatedAt || b.createdAt || \"\";\n      comparison = new Date(aUpdated).getTime() - new Date(bUpdated).getTime();\n    } else if (sortBy === \"publishedAt\") {\n      // Sort by published date, putting unpublished projects at the end\n      const aPublished = a.isPublished\n        ? a.latestBuildVirtual?.createdAt\n        : undefined;\n      const bPublished = b.isPublished\n        ? b.latestBuildVirtual?.createdAt\n        : undefined;\n\n      // If both are published, compare dates normally (will be reversed by order)\n      if (aPublished && bPublished) {\n        comparison =\n          new Date(aPublished).getTime() - new Date(bPublished).getTime();\n      } else if (aPublished && !bPublished) {\n        // Published should always come before unpublished, regardless of order\n        // In asc: -1 means a before b (correct)\n        // In desc: we return -(-1) = 1, meaning a after b (wrong!)\n        // So we need to account for the order reversal\n        comparison = order === \"asc\" ? -1 : 1;\n      } else if (!aPublished && bPublished) {\n        // Unpublished should always come after published, regardless of order\n        comparison = order === \"asc\" ? 1 : -1;\n      } else {\n        comparison = 0; // both unpublished, maintain order\n      }\n    }\n\n    return order === \"asc\" ? comparison : -comparison;\n  });\n\n  return sorted;\n};\n\ntype SortSelectProps = {\n  value: SortState;\n  onValueChange: (value: Required<SortState>) => void;\n};\n\nexport const SortSelect = ({ value, onValueChange }: SortSelectProps) => {\n  const sortBy = value.sortBy ?? \"updatedAt\";\n  const order = value.order ?? \"desc\";\n\n  const sortLabel =\n    sortBy === \"createdAt\"\n      ? \"Date created\"\n      : sortBy === \"title\"\n        ? \"Alphabetical\"\n        : sortBy === \"publishedAt\"\n          ? \"Date published\"\n          : \"Last modified\";\n\n  const sortIcon =\n    sortBy === \"createdAt\" ? (\n      <CalendarIcon />\n    ) : sortBy === \"title\" ? (\n      order === \"asc\" ? (\n        <ArrowDownAZIcon />\n      ) : (\n        <ArrowDownZAIcon />\n      )\n    ) : sortBy === \"publishedAt\" ? (\n      <UploadIcon />\n    ) : (\n      <CalendarIcon />\n    );\n\n  const handleSortChange = (newSortBy: SortField, newOrder: SortOrder) => {\n    // When switching to alphabetical sorting, default to A→Z (asc)\n    // When switching to date sorting, default to newest first (desc)\n    if (newSortBy !== sortBy) {\n      onValueChange({\n        sortBy: newSortBy,\n        order: newSortBy === \"title\" ? \"asc\" : \"desc\",\n      });\n    } else {\n      onValueChange({ sortBy: newSortBy, order: newOrder });\n    }\n  };\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button color=\"ghost\" prefix={sortIcon} suffix={<ChevronDownIcon />}>\n          {sortLabel}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        <DropdownMenuLabel>Sort</DropdownMenuLabel>\n        <DropdownMenuRadioGroup\n          value={sortBy}\n          onValueChange={(value) => handleSortChange(value as SortField, order)}\n        >\n          <DropdownMenuRadioItem value=\"title\" icon={<MenuCheckedIcon />}>\n            Alphabetical\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"createdAt\" icon={<MenuCheckedIcon />}>\n            Date created\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"updatedAt\" icon={<MenuCheckedIcon />}>\n            Last modified\n          </DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"publishedAt\" icon={<MenuCheckedIcon />}>\n            Date published\n          </DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n        <DropdownMenuSeparator />\n        <DropdownMenuLabel>Order</DropdownMenuLabel>\n        <DropdownMenuRadioGroup\n          value={order}\n          onValueChange={(value) =>\n            handleSortChange(sortBy, value as SortOrder)\n          }\n        >\n          {sortBy === \"title\" ? (\n            <>\n              <DropdownMenuRadioItem value=\"asc\" icon={<MenuCheckedIcon />}>\n                A→Z\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"desc\" icon={<MenuCheckedIcon />}>\n                Z→A\n              </DropdownMenuRadioItem>\n            </>\n          ) : (\n            <>\n              <DropdownMenuRadioItem value=\"desc\" icon={<MenuCheckedIcon />}>\n                Newest first\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem value=\"asc\" icon={<MenuCheckedIcon />}>\n                Oldest first\n              </DropdownMenuRadioItem>\n            </>\n          )}\n        </DropdownMenuRadioGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/tags.tsx",
    "content": "import { useRevalidator, useSearchParams } from \"react-router-dom\";\nimport { useState, type ComponentProps } from \"react\";\nimport {\n  Text,\n  theme,\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  Button,\n  DialogActions,\n  DialogClose,\n  Checkbox,\n  CheckboxAndLabel,\n  Label,\n  InputField,\n  DialogTitleActions,\n  Grid,\n  List,\n  ListItem,\n  Flex,\n  DropdownMenu,\n  DropdownMenuTrigger,\n  SmallIconButton,\n  DropdownMenuContent,\n  DropdownMenuItem,\n} from \"@webstudio-is/design-system\";\nimport { nativeClient } from \"~/shared/trpc/trpc-client\";\nimport type { User } from \"~/shared/db/user.server\";\nimport { nanoid } from \"nanoid\";\nimport { EllipsesIcon, SpinnerIcon } from \"@webstudio-is/icons\";\nimport { colors } from \"./colors\";\n\ntype DeleteConfirmationDialogProps = {\n  onClose: () => void;\n  onConfirm: () => void;\n  question: string;\n};\n\nconst DeleteConfirmationDialog = ({\n  onClose,\n  onConfirm,\n  question,\n}: DeleteConfirmationDialogProps) => {\n  return (\n    <Dialog\n      open\n      onOpenChange={(isOpen) => {\n        if (isOpen === false) {\n          onClose();\n        }\n      }}\n    >\n      <DialogContent>\n        <Flex gap=\"3\" direction=\"column\" css={{ padding: theme.panel.padding }}>\n          <Text>{question}</Text>\n          <Flex direction=\"rowReverse\" gap=\"2\">\n            <DialogClose>\n              <Button\n                color=\"destructive\"\n                onClick={() => {\n                  onConfirm();\n                }}\n              >\n                Delete\n              </Button>\n            </DialogClose>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </Flex>\n        </Flex>\n        <DialogTitle>Delete confirmation</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst TagsList = ({\n  projectId,\n  projectsTags,\n  projectTagsIds,\n  onEdit,\n}: {\n  projectId: string;\n  projectsTags: User[\"projectsTags\"];\n  projectTagsIds: string[];\n  onEdit: (tagId: string) => void;\n}) => {\n  const revalidator = useRevalidator();\n  const [deleteConfirmationTagId, setDeleteConfirmationTagId] =\n    useState<string>();\n\n  return (\n    <form\n      onChange={async (event) => {\n        event.preventDefault();\n        const formData = new FormData(event.currentTarget);\n        const tagsIds = formData.getAll(\"tagId\") as string[];\n        await nativeClient.project.updateTags.mutate({\n          projectId,\n          tags: tagsIds,\n        });\n        revalidator.revalidate();\n      }}\n    >\n      <List asChild>\n        <Grid\n          gap={1}\n          css={{\n            paddingBlock: theme.panel.paddingBlock,\n            maxHeight: theme.spacing[30],\n            overflow: \"auto\",\n          }}\n        >\n          {projectsTags\n            .sort((a, b) => a.label.localeCompare(b.label))\n            .map((tag, index) => (\n              <ListItem asChild onSelect={() => {}} index={index} key={tag.id}>\n                <Flex\n                  justify=\"between\"\n                  align=\"center\"\n                  gap=\"2\"\n                  css={{\n                    paddingInline: theme.panel.paddingInline,\n                    outlineColor: theme.colors.borderFocus,\n                    outlineOffset: -2,\n                    paddingBlock: theme.spacing[2],\n                  }}\n                >\n                  <CheckboxAndLabel\n                    key={tag.id}\n                    css={{ overflow: \"hidden\", flexGrow: 1 }}\n                  >\n                    <Checkbox\n                      id={tag.id}\n                      name=\"tagId\"\n                      value={tag.id}\n                      tabIndex={-1}\n                      defaultChecked={projectTagsIds.includes(tag.id)}\n                    />\n                    <Label truncate htmlFor={tag.id}>\n                      {tag.label}\n                    </Label>\n                  </CheckboxAndLabel>\n                  <DropdownMenu modal>\n                    <DropdownMenuTrigger asChild>\n                      {/* a11y is completely broken here\n                          focus is not restored to button invoker\n                          @todo fix it eventually and consider restoring from closed value preview dialog\n                      */}\n                      <SmallIconButton\n                        tabIndex={-1}\n                        aria-label=\"Open variable menu\"\n                        icon={<EllipsesIcon />}\n                      />\n                    </DropdownMenuTrigger>\n                    <DropdownMenuContent\n                      css={{ width: theme.spacing[24] }}\n                      onCloseAutoFocus={(event) => event.preventDefault()}\n                      align=\"end\"\n                    >\n                      <DropdownMenuItem\n                        onSelect={() => {\n                          onEdit(tag.id);\n                        }}\n                      >\n                        Edit\n                      </DropdownMenuItem>\n                      <DropdownMenuItem\n                        onSelect={() => {\n                          setDeleteConfirmationTagId(tag.id);\n                        }}\n                      >\n                        Delete\n                      </DropdownMenuItem>\n                    </DropdownMenuContent>\n                  </DropdownMenu>\n                </Flex>\n              </ListItem>\n            ))}\n          {projectsTags.length === 0 && (\n            <Text align=\"center\">No tags found</Text>\n          )}\n          {deleteConfirmationTagId && (\n            <DeleteConfirmationDialog\n              question=\"Are you sure you want to delete this tag? It will be removed from all projects.\"\n              onClose={() => setDeleteConfirmationTagId(undefined)}\n              onConfirm={async () => {\n                setDeleteConfirmationTagId(undefined);\n                const updatedTags = projectsTags.filter(\n                  (tag) => tag.id !== deleteConfirmationTagId\n                );\n                await nativeClient.user.updateProjectsTags.mutate({\n                  tags: updatedTags,\n                });\n                revalidator.revalidate();\n              }}\n            />\n          )}\n        </Grid>\n      </List>\n    </form>\n  );\n};\n\nconst TagEdit = ({\n  projectsTags,\n  tag,\n  onComplete,\n}: {\n  projectsTags: User[\"projectsTags\"];\n  tag: User[\"projectsTags\"][number];\n  onComplete: () => void;\n}) => {\n  const revalidator = useRevalidator();\n  const isExisting = projectsTags.some(({ id }) => id === tag.id);\n\n  return (\n    <form\n      onSubmit={async (event) => {\n        event.preventDefault();\n        const formData = new FormData(event.currentTarget);\n        const label = ((formData.get(\"tag\") as string) || \"\").trim();\n        if (tag.label === label || !label) {\n          return;\n        }\n        let updatedTags = [];\n        if (isExisting) {\n          updatedTags = projectsTags.map((availableTag) => {\n            if (availableTag.id === tag.id) {\n              return { ...availableTag, label };\n            }\n            return availableTag;\n          });\n        } else {\n          updatedTags = [...projectsTags, { id: tag.id, label }];\n        }\n\n        await nativeClient.user.updateProjectsTags.mutate({\n          tags: updatedTags,\n        });\n        revalidator.revalidate();\n        onComplete();\n      }}\n    >\n      <Grid css={{ padding: theme.panel.padding }}>\n        <InputField\n          autoFocus\n          defaultValue={tag.label}\n          name=\"tag\"\n          placeholder=\"My tag\"\n          minLength={1}\n        />\n      </Grid>\n      <DialogActions>\n        <Button type=\"submit\">\n          {isExisting ? \"Update tag\" : \"Create tag\"}\n        </Button>\n        <Button\n          color=\"ghost\"\n          type=\"button\"\n          onClick={() => {\n            onComplete();\n          }}\n        >\n          Cancel\n        </Button>\n      </DialogActions>\n    </form>\n  );\n};\n\nexport const TagsDialog = ({\n  projectId,\n  projectsTags,\n  projectTagsIds,\n  isOpen,\n  onOpenChange,\n}: {\n  projectId: string;\n  projectsTags: User[\"projectsTags\"];\n  projectTagsIds: string[];\n  isOpen: boolean;\n  onOpenChange: (isOpen: boolean) => void;\n}) => {\n  const [editingTag, setEditingTag] = useState<\n    User[\"projectsTags\"][number] | undefined\n  >();\n  const revalidator = useRevalidator();\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange}>\n      <DialogContent aria-describedby={undefined}>\n        <DialogTitle\n          suffix={\n            <DialogTitleActions>\n              {revalidator.state === \"loading\" && <SpinnerIcon />}\n              <DialogClose />\n            </DialogTitleActions>\n          }\n        >\n          Project tags\n        </DialogTitle>\n        {!editingTag && (\n          <>\n            <TagsList\n              projectId={projectId}\n              projectsTags={projectsTags}\n              projectTagsIds={projectTagsIds}\n              onEdit={(tagId) => {\n                setEditingTag(projectsTags.find((tag) => tag.id === tagId));\n              }}\n            />\n            <DialogActions>\n              <Button\n                onClick={() => setEditingTag({ id: nanoid(5), label: \"\" })}\n              >\n                Create tag\n              </Button>\n            </DialogActions>\n          </>\n        )}\n        {editingTag && (\n          <TagEdit\n            projectsTags={projectsTags}\n            tag={editingTag}\n            onComplete={() => setEditingTag(undefined)}\n          />\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const Tag = ({\n  index,\n  tag,\n  ...props\n}: { index: number; tag: User[\"projectsTags\"][number] } & ComponentProps<\n  typeof Button\n>) => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const selectedTagsIds = searchParams.getAll(\"tag\");\n  const color = colors[index] ?? theme.colors.backgroundNeutralDark;\n  return (\n    <Button\n      color=\"neutral\"\n      css={{\n        \"&:hover[data-state='auto'], &[data-state='pressed']\": {\n          backgroundColor: color,\n          color: theme.colors.white,\n        },\n        \"&[data-state='pressed']:hover\": {\n          backgroundColor: `oklch(from ${color} l c h / 0.8)`,\n        },\n      }}\n      onClick={() => {\n        const newSearchParams = new URLSearchParams(searchParams);\n        newSearchParams.delete(\"tag\");\n        if (!selectedTagsIds.includes(tag.id)) {\n          newSearchParams.append(\"tag\", tag.id);\n        }\n        for (const item of selectedTagsIds) {\n          if (item !== tag.id) {\n            newSearchParams.append(\"tag\", item);\n          }\n        }\n        setSearchParams(newSearchParams);\n      }}\n      {...props}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/projects/utils.ts",
    "content": "export const formatDate = (date: string) => {\n  return new Date(date).toLocaleDateString(\"en-US\", {\n    year: \"numeric\",\n    month: \"short\",\n    day: \"numeric\",\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/search/nothing-found.tsx",
    "content": "import { Flex, Text } from \"@webstudio-is/design-system\";\n\nexport const NothingFound = () => (\n  <Flex align=\"center\" justify=\"center\" direction=\"column\" gap=\"6\">\n    <Text variant=\"brandSectionTitle\" as=\"h1\" align=\"center\">\n      Nothing found 🙁\n    </Text>\n  </Flex>\n);\n"
  },
  {
    "path": "apps/builder/app/dashboard/search/search-field.tsx",
    "content": "import { SearchField } from \"@webstudio-is/design-system\";\nimport { useLocation, useNavigate, useSearchParams } from \"react-router-dom\";\nimport { dashboardPath } from \"~/shared/router-utils\";\n\nexport const Search = () => {\n  const navigate = useNavigate();\n  const [searchParams, setSearchParams] = useSearchParams();\n  const location = useLocation();\n  const handleAbortSearch = () => {\n    // When user cancels the search, we try to return to the last path they were on.\n    if (location.state?.previousPathname) {\n      return navigate(location.state.previousPathname);\n    }\n    navigate(dashboardPath(\"projects\"));\n  };\n  const isSearchRoute = location.pathname === dashboardPath(\"search\");\n\n  return (\n    <SearchField\n      value={searchParams.get(\"q\") ?? undefined}\n      onChange={(event) => {\n        const value = event.currentTarget.value.trim();\n        if (value === \"\") {\n          handleAbortSearch();\n          return;\n        }\n        if (isSearchRoute === false) {\n          navigate(\n            {\n              pathname: dashboardPath(\"search\"),\n              search: `?q=${value}`,\n            },\n            // Remember the last path to return to on abort\n            {\n              state: { previousPathname: location.pathname },\n            }\n          );\n          return;\n        }\n        setSearchParams({ q: value }, { replace: true });\n      }}\n      onAbort={handleAbortSearch}\n      autoFocus\n      placeholder=\"Search for anything\"\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/search/search-results.tsx",
    "content": "import { useMemo } from \"react\";\nimport { matchSorter } from \"match-sorter\";\nimport { useSearchParams } from \"react-router-dom\";\nimport { Flex, Separator, Text, theme } from \"@webstudio-is/design-system\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport { ProjectsGrid } from \"../projects/projects\";\nimport { Header, Main } from \"../shared/layout\";\nimport type { DashboardData } from \"../shared/types\";\nimport { NothingFound } from \"./nothing-found\";\nimport { TemplatesGrid } from \"../templates/templates\";\n\ntype SearchResults = {\n  projects: Array<DashboardProject>;\n  templates: Array<DashboardProject>;\n};\n\nconst initialSearchResults: SearchResults = {\n  templates: [],\n  projects: [],\n} as const;\n\nexport const SearchResults = (props: DashboardData) => {\n  const [searchParams] = useSearchParams();\n  const { projects, templates, publisherHost, userPlanFeatures } = props;\n  const search = searchParams.get(\"q\");\n\n  const results = useMemo(() => {\n    if (!search || !projects || !templates) {\n      return initialSearchResults;\n    }\n    const keys = [\"title\", \"domain\"];\n    return {\n      projects: matchSorter(projects, search, { keys }),\n      templates: matchSorter(templates, search, { keys }),\n    };\n  }, [projects, templates, search]);\n\n  const nothingFound =\n    results.projects.length === 0 && results.templates.length === 0;\n\n  return (\n    <Main>\n      <Header variant=\"main\">\n        <Text variant=\"brandRegular\">\n          Search results for <b>\"{search}\"</b>\n        </Text>\n      </Header>\n      <Flex\n        direction=\"column\"\n        gap=\"3\"\n        css={{\n          paddingInline: theme.spacing[13],\n          paddingTop: nothingFound ? \"20vh\" : 0,\n        }}\n      >\n        {nothingFound && <NothingFound />}\n        {results.projects.length > 0 && (\n          <>\n            <Text variant=\"brandSectionTitle\" as=\"h2\">\n              Projects\n            </Text>\n            <ProjectsGrid\n              projects={results.projects}\n              userPlanFeatures={userPlanFeatures}\n              publisherHost={publisherHost}\n              projectsTags={props.user.projectsTags}\n            />\n          </>\n        )}\n        {results.templates.length > 0 && (\n          <>\n            <Separator />\n            <Text variant=\"brandSectionTitle\" as=\"h2\">\n              Templates\n            </Text>\n            <TemplatesGrid projects={results.templates} />\n          </>\n        )}\n      </Flex>\n    </Main>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/shared/card.tsx",
    "content": "import {\n  Flex,\n  theme,\n  Box,\n  Grid,\n  styled,\n  css,\n  Slot,\n  type SlotProps,\n} from \"@webstudio-is/design-system\";\nimport { forwardRef, type ComponentProps } from \"react\";\n\nconst borderColorVar = \"--ws-dashboard-card-border-color\";\n\nconst cardStyle = css({\n  position: \"relative\",\n  isolation: \"isolate\",\n  display: \"flex\",\n  padding: 0,\n  height: \"100%\",\n  flexDirection: \"column\",\n  alignItems: \"center\",\n  flexShrink: 0,\n  outline: \"none\",\n  \"&:focus-within, &[aria-selected=true]\": {\n    [borderColorVar]: theme.colors.borderFocus,\n  },\n});\n\ntype CardProps = ComponentProps<\"div\"> & {\n  asChild?: boolean;\n} & SlotProps;\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(\n  ({ asChild, ...props }, ref) => {\n    const Component = asChild ? Slot : Box;\n    return <Component {...props} className={cardStyle()} ref={ref} />;\n  }\n);\nCard.displayName = \"Card\";\n\nexport const CardContent = styled(Grid, {\n  position: \"relative\",\n  overflow: \"hidden\",\n  width: \"100%\",\n  aspectRatio: \"1.91/1\",\n  borderRadius: theme.borderRadius[5],\n  \"&::after\": {\n    content: '\"\"',\n    position: \"absolute\",\n    inset: 0,\n    border: `1px solid var(${borderColorVar}, transparent)`,\n    borderRadius: theme.borderRadius[5],\n    pointerEvents: \"none\",\n  },\n});\n\nexport const CardFooter = styled(Flex, {\n  justifyContent: \"space-between\",\n  flexShrink: 0,\n  alignSelf: \"stretch\",\n  flexGap: theme.spacing[3],\n  background: theme.colors.brandBackgroundProjectCardTextArea,\n  height: theme.spacing[17],\n  paddingBlock: theme.panel.paddingBlock,\n});\n"
  },
  {
    "path": "apps/builder/app/dashboard/shared/layout.tsx",
    "content": "import type { ComponentProps } from \"react\";\nimport { Flex, theme } from \"@webstudio-is/design-system\";\n\nexport const Header = ({\n  variant,\n  ...props\n}: { variant: \"aside\" | \"main\" } & ComponentProps<typeof Flex>) => {\n  return (\n    <Flex\n      as=\"header\"\n      align=\"center\"\n      justify=\"between\"\n      shrink={false}\n      css={{\n        paddingInline:\n          variant === \"aside\" ? theme.spacing[5] : theme.spacing[13],\n        height: theme.spacing[19],\n        position: \"sticky\",\n        top: 0,\n        background: theme.colors.backgroundPanel,\n        zIndex: 1,\n      }}\n      {...props}\n    />\n  );\n};\n\nexport const Main = (props: ComponentProps<typeof Flex>) => {\n  return (\n    <Flex\n      direction=\"column\"\n      as=\"main\"\n      grow\n      css={{\n        // Allows scrolling most parent container while header stays sticky\n        overflow: \"auto\",\n        // Keeps dialogs on top of the main content\n        isolation: \"isolate\",\n      }}\n      {...props}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/shared/spinner.tsx",
    "content": "import { css } from \"@webstudio-is/design-system\";\nimport { useDebounce } from \"use-debounce\";\nimport { SpinnerIcon } from \"@webstudio-is/icons\";\nimport { useEffect } from \"react\";\n\nconst containerStyle = css({\n  position: \"absolute\",\n  inset: 0,\n  background:\n    \"radial-gradient(34.37% 50% at 50% 50%, rgba(255, 255, 255, 0.5) 0%, rgba(248, 248, 248, 0.5) 100%);\",\n  backdropFilter: \"blur(8px)\",\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n});\n\nexport const Spinner = ({\n  delay = 600,\n  size = 64,\n}: {\n  delay?: number;\n  size?: number;\n}) => {\n  const [isVisible, setIsVisible] = useDebounce(false, delay);\n\n  useEffect(() => {\n    setIsVisible(true);\n  }, [setIsVisible]);\n\n  if (isVisible === false) {\n    return;\n  }\n\n  return (\n    <div className={containerStyle()}>\n      <SpinnerIcon size={size} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/shared/thumbnail.tsx",
    "content": "import { forwardRef } from \"react\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport { css, theme, textVariants } from \"@webstudio-is/design-system\";\n\nconst abbrStyle = css(textVariants.brandThumbnailLargeDefault, {\n  display: \"flex\",\n  alignItems: \"center\",\n  alignSelf: \"center\",\n  minHeight: 0,\n  background: theme.colors.brandBackgroundProjectCardFront,\n  WebkitBackgroundClip: \"text\",\n  backgroundClip: \"text\",\n  color: \"transparent\",\n  outline: \"none\",\n  transition: \"100ms\",\n  \"&:hover, &:focus\": textVariants.brandThumbnailLargeHover,\n});\n\n// My Next Project > MN\nconst getThumbnailAbbreviation = (title: string) =>\n  title\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((word) => word.charAt(0).toUpperCase())\n    .join(\"\");\n\nexport const ThumbnailLinkWithAbbr = forwardRef<\n  HTMLAnchorElement,\n  { title: string; to: string }\n>(({ title, to }, ref) => {\n  return (\n    <a ref={ref} href={to} className={abbrStyle()} tabIndex={-1}>\n      {getThumbnailAbbreviation(title)}\n    </a>\n  );\n});\nThumbnailLinkWithAbbr.displayName = \"ThumbnailLinkWithAbbr\";\n\nexport const ThumbnailWithAbbr = forwardRef<\n  HTMLDivElement,\n  { title: string; onClick: React.MouseEventHandler<HTMLDivElement> }\n>(({ title, onClick }, ref) => {\n  return (\n    <div ref={ref} onClick={onClick} className={abbrStyle()} tabIndex={-1}>\n      {getThumbnailAbbreviation(title)}\n    </div>\n  );\n});\n\nThumbnailWithAbbr.displayName = \"ThumbnailWithAbbr\";\n\nconst imageContainerStyle = css({\n  position: \"relative\",\n  background: theme.colors.brandBackgroundProjectCardFront,\n  outline: \"none\",\n  overflow: \"hidden\",\n  transition: \"scale 100ms\",\n  \"&:hover, &:focus\": {\n    scale: \"1.1\",\n  },\n});\n\nconst imageStyle = css({\n  position: \"absolute\",\n  top: 0,\n  width: \"100%\",\n  height: \"100%\",\n  objectFit: \"cover\",\n});\n\nexport const ThumbnailLinkWithImage = forwardRef<\n  HTMLAnchorElement,\n  { name: string; to: string }\n>(({ name, to }, ref) => {\n  return (\n    <a ref={ref} href={to} className={imageContainerStyle()} tabIndex={-1}>\n      <Image src={name} loader={wsImageLoader} className={imageStyle()} />\n    </a>\n  );\n});\nThumbnailLinkWithImage.displayName = \"ThumbnailLinkWithImage\";\n\nexport const ThumbnailWithImage = forwardRef<\n  HTMLDivElement,\n  {\n    name: string;\n    onClick: React.MouseEventHandler<HTMLDivElement>;\n  }\n>(({ name, onClick }, ref) => {\n  return (\n    <div\n      ref={ref}\n      onClick={onClick}\n      className={imageContainerStyle()}\n      tabIndex={-1}\n    >\n      <Image src={name} loader={wsImageLoader} className={imageStyle()} />\n    </div>\n  );\n});\n\nThumbnailWithImage.displayName = \"ThumbnailWithImage\";\n"
  },
  {
    "path": "apps/builder/app/dashboard/shared/types.ts",
    "content": "import type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport type { User } from \"~/shared/db/user.server\";\nimport type { UserPlanFeatures } from \"~/shared/db/user-plan-features.server\";\n\nexport type DashboardData = {\n  user: User;\n  projects: Array<DashboardProject>;\n  templates: Array<DashboardProject>;\n  userPlanFeatures: UserPlanFeatures;\n  publisherHost: string;\n  projectToClone?: {\n    authToken: string;\n    id: string;\n    title: string;\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/templates/template-card.tsx",
    "content": "import { useState } from \"react\";\nimport { Flex, Text, theme } from \"@webstudio-is/design-system\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport { builderUrl } from \"~/shared/router-utils\";\nimport { Card, CardContent, CardFooter } from \"../shared/card\";\nimport { ThumbnailWithAbbr, ThumbnailWithImage } from \"../shared/thumbnail\";\nimport { CloneProjectDialog } from \"~/shared/clone-project\";\n\ntype TemplateCardProps = {\n  project: DashboardProject;\n};\n\nexport const TemplateCard = ({ project, ...props }: TemplateCardProps) => {\n  const [isDuplicateDialogOpen, setIsDuplicateDialogOpen] = useState(false);\n  const { title, previewImageAsset } = project;\n  return (\n    <Card {...props}>\n      <CardContent\n        css={{\n          background: theme.colors.brandBackgroundProjectCardBack,\n          cursor: \"default\",\n        }}\n      >\n        {previewImageAsset ? (\n          <ThumbnailWithImage\n            name={previewImageAsset.name}\n            onClick={() => {\n              setIsDuplicateDialogOpen(true);\n            }}\n          />\n        ) : (\n          <ThumbnailWithAbbr\n            title={title}\n            onClick={() => {\n              setIsDuplicateDialogOpen(true);\n            }}\n          />\n        )}\n      </CardContent>\n      <CardFooter>\n        <Flex direction=\"column\" justify=\"around\">\n          <Text variant=\"titles\" truncate userSelect=\"text\">\n            {title}\n          </Text>\n        </Flex>\n      </CardFooter>\n      <CloneProjectDialog\n        isOpen={isDuplicateDialogOpen}\n        onOpenChange={setIsDuplicateDialogOpen}\n        project={project}\n        onCreate={(projectId) => {\n          window.location.href = builderUrl({\n            origin: window.origin,\n            projectId: projectId,\n          });\n        }}\n      />\n    </Card>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/dashboard/templates/templates.tsx",
    "content": "import {\n  Flex,\n  Grid,\n  List,\n  ListItem,\n  Text,\n  rawTheme,\n  theme,\n} from \"@webstudio-is/design-system\";\nimport type { DashboardProject } from \"@webstudio-is/dashboard\";\nimport { Header, Main } from \"../shared/layout\";\nimport { CreateProject } from \"../projects/project-dialogs\";\nimport { TemplateCard } from \"./template-card\";\n\nexport const TemplatesGrid = ({\n  projects,\n}: {\n  projects: Array<DashboardProject>;\n}) => {\n  return (\n    <List asChild>\n      <Grid\n        gap=\"6\"\n        css={{\n          gridTemplateColumns: `repeat(auto-fill, minmax(${rawTheme.spacing[31]}, 1fr))`,\n          paddingBottom: theme.spacing[13],\n        }}\n      >\n        {projects.map((project) => {\n          return (\n            <ListItem index={0} key={project.id} asChild>\n              <TemplateCard project={project} />\n            </ListItem>\n          );\n        })}\n      </Grid>\n    </List>\n  );\n};\n\ntype ProjectsProps = {\n  projects: Array<DashboardProject>;\n  welcome?: boolean;\n};\n\nexport const Templates = ({ projects, welcome = false }: ProjectsProps) => {\n  return (\n    <Main>\n      <Header variant=\"main\">\n        <Text variant=\"brandSectionTitle\" as=\"h2\">\n          {welcome ? \"What will you create?\" : \"Starter templates\"}\n        </Text>\n        <Flex gap=\"2\">\n          <CreateProject />\n        </Flex>\n      </Header>\n      <Flex\n        direction=\"column\"\n        gap=\"3\"\n        css={{ paddingInline: theme.spacing[13] }}\n      >\n        <TemplatesGrid projects={projects} />\n      </Flex>\n    </Main>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/env/env.server.ts",
    "content": "// @todo zod parse env, make explicit types i.e\nconst env = {\n  // Authentication\n  DEV_LOGIN: process.env.DEV_LOGIN,\n  GH_CLIENT_ID: process.env.GH_CLIENT_ID,\n  GH_CLIENT_SECRET: process.env.GH_CLIENT_SECRET,\n  GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,\n  GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,\n\n  // Secret session key, context encode\n  AUTH_SECRET: process.env.AUTH_SECRET,\n\n  // DEPLOYMENT_ENVIRONMENT development | preview | production\n  DEPLOYMENT_ENVIRONMENT: process.env.DEPLOYMENT_ENVIRONMENT,\n  DEPLOYMENT_URL: process.env.DEPLOYMENT_URL,\n\n  // Trpc on SaaS\n  TRPC_SERVER_URL: process.env.TRPC_SERVER_URL,\n  TRPC_SERVER_API_TOKEN: process.env.TRPC_SERVER_API_TOKEN,\n\n  PORT: process.env.PORT,\n\n  // Assets\n  MAX_UPLOAD_SIZE: process.env.MAX_UPLOAD_SIZE,\n  MAX_ASSETS_PER_PROJECT: process.env.MAX_ASSETS_PER_PROJECT,\n\n  // Remote assets\n  S3_ENDPOINT: process.env.S3_ENDPOINT,\n  S3_REGION: process.env.S3_REGION,\n  S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID,\n  S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,\n  S3_BUCKET: process.env.S3_BUCKET,\n  S3_ACL: process.env.S3_ACL,\n  /**\n   * Origin of service implementing /cdn-cgi/image/ cloudflare endpoint\n   * without ending slash\n   */\n  RESIZE_ORIGIN: process.env.RESIZE_ORIGIN,\n\n  /**\n   * Entri API credentials\n   */\n  ENTRI_APPLICATION_ID: process.env.ENTRI_APPLICATION_ID ?? \"webstudio\",\n  ENTRI_SECRET: process.env.ENTRI_SECRET,\n\n  /**\n   * Projects as templates in dashboard\n   */\n  PROJECT_TEMPLATES:\n    process.env.PROJECT_TEMPLATES?.split(\",\").map((projectId) =>\n      projectId.trim()\n    ) ?? [],\n\n  N8N_WEBHOOK_URL: process.env.N8N_WEBHOOK_URL,\n  N8N_WEBHOOK_TOKEN: process.env.N8N_WEBHOOK_TOKEN,\n\n  PUBLISHER_HOST: process.env.PUBLISHER_HOST || \"wstd.work\",\n\n  STAGING_USERNAME: process.env.STAGING_USERNAME ?? \"admin\",\n  STAGING_PASSWORD: process.env.STAGING_PASSWORD ?? \"webstudio\",\n\n  FEATURES: process.env.FEATURES ?? \"\",\n\n  // current user plan features (default)\n  USER_PLAN: process.env.USER_PLAN ?? \"\",\n\n  POSTGREST_URL: process.env.POSTGREST_URL ?? \"http://localhost:3000\",\n  POSTGREST_API_KEY: process.env.POSTGREST_API_KEY ?? \"\",\n\n  SECURE_COOKIE: true,\n\n  // Used for project oauth login flow @todo remove ??\n  AUTH_WS_CLIENT_ID: process.env.AUTH_WS_CLIENT_ID ?? \"12345\",\n  AUTH_WS_CLIENT_SECRET: process.env.AUTH_WS_CLIENT_SECRET ?? \"12345678\",\n};\n\nexport type ServerEnv = typeof env;\n\n// https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables\nif (process.env.VERCEL !== undefined) {\n  if (env.DEPLOYMENT_ENVIRONMENT === undefined) {\n    env.DEPLOYMENT_ENVIRONMENT = process.env.VERCEL_ENV;\n  }\n  if (env.DEPLOYMENT_URL === undefined) {\n    env.DEPLOYMENT_URL = process.env.VERCEL_URL;\n  }\n}\n\nexport default env;\n"
  },
  {
    "path": "apps/builder/app/env/env.static.server.ts",
    "content": "/**\n * Build-time Environment Variables\n *\n * These variables are injected into your bundle at build time based on the environment settings.\n * - Configuration: See envPrefix in [vite.config.ts](../../vite.config.ts)  (GITHUB_)\n * - Documentation: Refer to the [Vite documentation](https://vitejs.dev/guide/env-and-mode)\n * - Type Definitions: See [vite-env.d.ts](./vite-env.d.ts) in this directory\n */\nexport const staticEnv = {\n  GITHUB_REF_NAME: import.meta.env.GITHUB_REF_NAME,\n  GITHUB_SHA: import.meta.env.GITHUB_SHA,\n};\n"
  },
  {
    "path": "apps/builder/app/env/env.static.ts",
    "content": "/**\n * Public Build-time Environment Variables\n *\n * These variables are injected into your bundle at build time based on the environment settings.\n * - Configuration: See envPrefix in [vite.config.ts](../../vite.config.ts)  (GITHUB_)\n * - Documentation: Refer to the [Vite documentation](https://vitejs.dev/guide/env-and-mode)\n * - Type Definitions: See [vite-env.d.ts](./vite-env.d.ts) in this directory\n */\n\nexport const publicStaticEnv = {\n  VERSION: import.meta.env.GITHUB_SHA ?? \"local\",\n};\n"
  },
  {
    "path": "apps/builder/app/env/vite-env.d.ts",
    "content": "interface ImportMetaEnv {\n  readonly GITHUB_REF_NAME: string | undefined;\n  readonly GITHUB_SHA: string | undefined;\n}\n"
  },
  {
    "path": "apps/builder/app/root.tsx",
    "content": "// Our root outlet doesn't contain a layout because we have 2 types of documents: canvas and builder and we need to decide down the line which one to render, thre is no single root document.\nimport {\n  Outlet,\n  json,\n  useLoaderData,\n  type ShouldRevalidateFunction,\n} from \"@remix-run/react\";\nimport { setEnv } from \"@webstudio-is/feature-flags\";\nimport env from \"./env/env.server\";\nimport { useSetFeatures } from \"./shared/use-set-features\";\n\nexport const loader = () => {\n  return json({\n    features: env.FEATURES,\n  });\n};\n\nexport default function App() {\n  const { features } = useLoaderData<typeof loader>();\n  setEnv(features);\n  useSetFeatures();\n\n  return <Outlet />;\n}\n\nexport const shouldRevalidate: ShouldRevalidateFunction = () => {\n  return false;\n};\n"
  },
  {
    "path": "apps/builder/app/routes/_canvas.canvas.tsx",
    "content": "import { lazy } from \"react\";\nimport type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { Scripts, ScrollRestoration } from \"@remix-run/react\";\nimport { isCanvas } from \"~/shared/router-utils\";\nimport { ClientOnly } from \"~/shared/client-only\";\n\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  if (isCanvas(request) === false) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n  return {};\n};\n\nconst Canvas = lazy(async () => {\n  const { Canvas } = await import(\"~/canvas/index.client\");\n  return { default: Canvas };\n});\n\nconst CanvasRoute = () => {\n  return (\n    // this setup remix scripts on canvas and after rendering a website\n    // scripts will continue to work even though removed from dom\n    <ClientOnly\n      fallback={\n        <body>\n          <Scripts />\n          <ScrollRestoration />\n        </body>\n      }\n    >\n      <Canvas />\n    </ClientOnly>\n  );\n};\n\nexport default CanvasRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_canvas.tsx",
    "content": "import { Links, Meta, Outlet } from \"@remix-run/react\";\nimport { ErrorBoundary as ErrorBoundaryComponent } from \"~/shared/error/error-boundary\";\n\nconst Document = (props: { children: React.ReactNode }) => {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      {props.children}\n    </html>\n  );\n};\n\nexport const ErrorBoundary = () => {\n  return (\n    <Document>\n      <ErrorBoundaryComponent />\n    </Document>\n  );\n};\n\nexport default function CanvasLayout() {\n  return (\n    <Document>\n      <Outlet />\n    </Document>\n  );\n}\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.$.tsx",
    "content": "import {\n  type HeadersFunction,\n  type LoaderFunctionArgs,\n} from \"@remix-run/server-runtime\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  preventCrossOriginCookie(request);\n\n  // No data to protect with CSRF token\n\n  const url = new URL(request.url);\n\n  // Redirecting asset files (e.g., .js, .css) to the dashboard should be avoided.\n  // This is because immutable caching rules apply to redirects, causing these files\n  // to become permanently inaccessible. Ensure asset files are served correctly\n  // without redirects to maintain availability and proper caching behavior.\n  const publicPaths = [\"/cgi/\", \"/assets/\", \"/apple-touch-icon-\"];\n\n  // In case of 404 on static assets, this route will be executed\n  if (publicPaths.some((publicPath) => url.pathname.startsWith(publicPath))) {\n    throw new Response(\"Not found\", {\n      status: 404,\n      headers: {\n        \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n      },\n    });\n  }\n\n  const contentType = request.headers.get(\"Content-Type\");\n\n  if (contentType?.includes(\"application/json\")) {\n    // Return an error to not trigger the ErrorBoundary rendering (api request)\n    throw new Response(\"Not found\", {\n      status: 404,\n    });\n  }\n\n  // Throw an error to trigger the ErrorBoundary rendering\n  throw new Response(\"Not found\", {\n    status: 404,\n  });\n};\n\nexport default function NotFound() {\n  // Placeholder component to prevent Remix warning:\n  // \"Matched leaf route at location '/{SOME_LOCATION}' does not have an element or Component.\"\n  // Without this, an <Outlet /> with a null value would render an empty page.\n  return (\n    <div>\n      <h1>Not Found</h1>\n      <p>The page you requested could not be found.</p>\n    </div>\n  );\n}\n\nexport const headers: HeadersFunction = ({ errorHeaders, loaderHeaders }) => {\n  // !!!! VERY IMPORTANT !!!\n  // Vercel sets Cache-Control: public, max-age=31536000, immutable for all /assets/* paths,\n  // even when a 404 error occurs during deployment.\n  // This causes 404 errors on assets to be cached permanently, preventing updates.\n  // To prevent this, we set \"Cache-Control\": \"public, max-age=0, must-revalidate\" when an asset is not found.\n  const cacheControl = errorHeaders?.get(\"Cache-Control\");\n\n  if (cacheControl) {\n    return {\n      \"Cache-Control\": cacheControl,\n    };\n  }\n  return loaderHeaders;\n};\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.(builder).tsx",
    "content": "/**\n * The file is named _ui.(builder) instead of _ui._index due to an issue with Vercel.\n * The _ui._index route isn’t recognized on Vercel, even though it works perfectly in other environments.\n */\n\nimport { lazy } from \"react\";\nimport { useLoaderData } from \"@remix-run/react\";\nimport type { MetaFunction, ShouldRevalidateFunction } from \"@remix-run/react\";\nimport {\n  json,\n  type HeadersArgs,\n  type LoaderFunctionArgs,\n} from \"@remix-run/server-runtime\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport { db as authDb } from \"@webstudio-is/authorization-token/index.server\";\n\nimport {\n  AuthorizationError,\n  authorizeProject,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { createContext } from \"~/shared/context.server\";\nimport { dashboardPath, isBuilder, isDashboard } from \"~/shared/router-utils\";\n\nimport env from \"~/env/env.server\";\n\nimport builderStyles from \"~/builder/builder.css?url\";\nimport { ClientOnly } from \"~/shared/client-only\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect } from \"~/services/no-store-redirect\";\nimport { builderSessionStorage } from \"~/services/builder-session.server\";\nimport {\n  allowedDestinations,\n  isFetchDestination,\n} from \"~/services/destinations.server\";\nimport { loader as authWsLoader } from \"./auth.ws\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nexport const links = () => {\n  return [{ rel: \"stylesheet\", href: builderStyles }];\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n\n  if (data === undefined) {\n    return metas;\n  }\n\n  // Project title will be set dynamically after data loads\n  return metas;\n};\n\nexport const loader = async (loaderArgs: LoaderFunctionArgs) => {\n  const { request } = loaderArgs;\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n\n  if (isDashboard(request)) {\n    throw redirect(dashboardPath());\n  }\n\n  if (false === isBuilder(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  if (isFetchDestination(request)) {\n    // Remix does not provide a built-in way to add CSRF tokens to data fetches,\n    // such as client-side navigation or data refreshes.\n    // Therefore, ensure that all data fetched here is not sensitive and does not require CSRF protection.\n    // await checkCsrf(request);\n  }\n\n  const context = await createContext(request);\n\n  if (context.authorization.type === \"service\") {\n    throw new AuthorizationError(\"Service calls are not allowed\");\n  }\n\n  if (context.authorization.type === \"anonymous\") {\n    throw await authWsLoader(loaderArgs); // redirect(\"/auth/ws\");\n  }\n\n  if (\n    context.authorization.type === \"user\" &&\n    request.headers.get(\"sec-fetch-mode\") === \"navigate\"\n  ) {\n    // If logout fails, or the session cookie in the dashboard is deleted or expired,\n    // enforce reauthorization on builder reload or navigation (sec-fetch-mode === 'navigate') after a timeout.\n    const RELOAD_ON_NAVIGATE_TIMEOUT =\n      env.DEPLOYMENT_ENVIRONMENT === \"production\"\n        ? 1000 * 60 * 60 * 24 * 7 // 1 week\n        : 1000 * 60 * 60 * 1; // 1 hour\n\n    if (\n      Date.now() - context.authorization.sessionCreatedAt >\n      RELOAD_ON_NAVIGATE_TIMEOUT\n    ) {\n      throw await authWsLoader(loaderArgs); // start immediately instead of redirect(\"/auth/ws\");\n    }\n  }\n\n  try {\n    const url = new URL(request.url);\n\n    const { projectId } = parseBuilderUrl(request.url);\n\n    if (projectId === undefined) {\n      throw new Response(\"Project ID is not defined\", {\n        status: 404,\n      });\n    }\n\n    const start = Date.now();\n    const project = await projectApi.loadById(projectId, context);\n\n    if (project === null) {\n      throw new Response(`Project \"${projectId}\" not found`, {\n        status: 404,\n      });\n    }\n\n    const authPermit =\n      (await authorizeProject.getProjectPermit(\n        {\n          projectId: project.id,\n          // At this point we already knew that if project loaded we have at least \"view\" permit\n          // having that getProjectPermit is heavy operation we can skip check \"view\" permit\n          permits: [\"own\", \"admin\", \"build\", \"edit\"] as const,\n        },\n        context\n      )) ?? \"view\";\n\n    const end = Date.now();\n\n    const diff = end - start;\n\n    // we need to log timings to figure out how to speed up loading\n\n    console.info(`Project ${project.id} is loaded in ${diff}ms`);\n\n    const authToken = url.searchParams.get(\"authToken\") ?? undefined;\n\n    const authTokenPermissions =\n      authPermit !== \"own\" && authToken !== undefined\n        ? await authDb.getTokenPermissions(\n            {\n              projectId: project.id,\n              token: authToken,\n            },\n            context\n          )\n        : authDb.tokenDefaultPermissions;\n\n    const { userPlanFeatures } = context;\n    if (userPlanFeatures === undefined) {\n      throw new Response(\"User plan features are not defined\", {\n        status: 404,\n      });\n    }\n\n    if (project.userId === null) {\n      throw new AuthorizationError(\"Project must have project userId defined\");\n    }\n\n    const headers = new Headers();\n\n    if (context.authorization.type === \"token\") {\n      // To protect against cookie overwrites, we set a null session cookie if a user is using an authToken.\n      // This ensures that any existing HttpOnly, secure session cookies cannot be overwritten by client-side scripts\n\n      // See Storage model https://datatracker.ietf.org/doc/html/rfc6265#section-5.3\n      // If the cookie store contains a cookie with the same name,\n      // domain, and path as the newly created cookie:\n      // ...\n      // If the newly created cookie was received from a \"non-HTTP\"\n      //  API and the old-cookie's http-only-flag is set, abort these\n      //  steps and ignore the newly created cookie entirely.\n      const builderSession = await builderSessionStorage.getSession(null);\n      headers.set(\n        \"Set-Cookie\",\n        await builderSessionStorage.commitSession(builderSession)\n      );\n    }\n\n    headers.set(\n      // Disallowing iframes from loading any content except the canvas\n      // Still possible create iframes on canvas itself (but we use credentialless attribute)\n      // Still possible create iframe without src attribute\n      // Disable workers on builder\n      \"Content-Security-Policy\",\n      `frame-src ${url.origin}/canvas https://app.goentri.com/ https://help.webstudio.is/; worker-src 'none'`\n    );\n\n    return json(\n      {\n        projectId: project.id,\n        authToken,\n        authTokenPermissions,\n        authPermit,\n        userPlanFeatures,\n        stagingUsername: env.STAGING_USERNAME,\n        stagingPassword: env.STAGING_PASSWORD,\n      } as const,\n      {\n        headers,\n      }\n    );\n  } catch (error) {\n    if (error instanceof AuthorizationError) {\n      // try to re-login user if he has no access to the project\n      throw redirect(`/auth/ws`);\n    }\n\n    throw error;\n  }\n};\n\n/**\n * When doing changes in a project, then navigating to a dashboard then pressing the back button,\n * the builder page may display stale data because it’s being retrieved from the browser’s back/forward cache (bfcache).\n *\n * https://web.dev/articles/bfcache\n *\n */\nexport const headers = ({ loaderHeaders }: HeadersArgs) => {\n  return {\n    \"Cache-Control\": \"no-store\",\n    \"Content-Security-Policy\": loaderHeaders.get(\"Content-Security-Policy\"),\n  };\n};\n\nconst Builder = lazy(async () => {\n  const { Builder } = await import(\"~/builder/index.client\");\n  return { default: Builder };\n});\n\nconst BuilderRoute = () => {\n  const data = useLoaderData<typeof loader>();\n\n  return (\n    <ClientOnly>\n      {/* Using a key here ensures that certain effects are re-executed inside the builder,\n      especially in cases like cloning a project */}\n      <Builder key={data.projectId} {...data} />\n    </ClientOnly>\n  );\n};\n\n/**\n * We do not want trpc and other mutations that use the Remix useFetcher hook\n * to cause a reload of all builder data.\n */\nexport const shouldRevalidate: ShouldRevalidateFunction = ({\n  currentUrl,\n  nextUrl,\n  defaultShouldRevalidate,\n}) => {\n  const currentUrlCopy = new URL(currentUrl);\n  const nextUrlCopy = new URL(nextUrl);\n  // prevent revalidating data when pageId changes\n  // to not regenerate auth token and preserve canvas url\n  currentUrlCopy.searchParams.delete(\"pageId\");\n  nextUrlCopy.searchParams.delete(\"pageId\");\n\n  currentUrlCopy.searchParams.delete(\"mode\");\n  nextUrlCopy.searchParams.delete(\"mode\");\n\n  currentUrlCopy.searchParams.delete(\"pageHash\");\n  nextUrlCopy.searchParams.delete(\"pageHash\");\n\n  return currentUrlCopy.href === nextUrlCopy.href\n    ? false\n    : defaultShouldRevalidate;\n};\n\nexport default BuilderRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.dashboard._index.tsx",
    "content": "import { lazy } from \"react\";\nimport { ClientOnly } from \"~/shared/client-only\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nconst Dashboard = lazy(async () => {\n  const { Dashboard } = await import(\"~/dashboard/index.client\");\n  return { default: Dashboard };\n});\n\nconst DashboardRoute = () => {\n  return (\n    <ClientOnly>\n      <Dashboard />\n    </ClientOnly>\n  );\n};\n\nexport default DashboardRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.dashboard.search.tsx",
    "content": "import { lazy } from \"react\";\nimport { type MetaFunction } from \"@remix-run/react\";\nimport { ClientOnly } from \"~/shared/client-only\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nexport const meta = () => {\n  const metas: ReturnType<MetaFunction> = [];\n\n  metas.push({ title: \"Webstudio Dashboard | Search\" });\n\n  return metas;\n};\n\nconst Dashboard = lazy(async () => {\n  const { Dashboard } = await import(\"~/dashboard/index.client\");\n  return { default: Dashboard };\n});\n\nconst DashboardRoute = () => {\n  return (\n    <ClientOnly>\n      <Dashboard />\n    </ClientOnly>\n  );\n};\n\nexport default DashboardRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.dashboard.templates.tsx",
    "content": "import { lazy } from \"react\";\nimport { type MetaFunction } from \"@remix-run/react\";\nimport { ClientOnly } from \"~/shared/client-only\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nexport const meta = () => {\n  const metas: ReturnType<MetaFunction> = [];\n\n  metas.push({ title: \"Webstudio Dashboard | Templates\" });\n\n  return metas;\n};\n\nconst Dashboard = lazy(async () => {\n  const { Dashboard } = await import(\"~/dashboard/index.client\");\n  return { default: Dashboard };\n});\n\nconst DashboardRoute = () => {\n  return (\n    <ClientOnly>\n      <Dashboard />\n    </ClientOnly>\n  );\n};\n\nexport default DashboardRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.dashboard.tsx",
    "content": "import { lazy } from \"react\";\nimport { preconnect, prefetchDNS } from \"react-dom\";\nimport {\n  Outlet,\n  redirect,\n  type ShouldRevalidateFunction,\n} from \"react-router-dom\";\nimport { useLoaderData, type MetaFunction } from \"@remix-run/react\";\nimport { type LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport {\n  createCallerFactory,\n  AuthorizationError,\n  type AppContext,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { db as authDb } from \"@webstudio-is/authorization-token/index.server\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport { dashboardProjectRouter } from \"@webstudio-is/dashboard/index.server\";\nimport { builderUrl, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport env from \"~/env/env.server\";\nimport { ClientOnly } from \"~/shared/client-only\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\nimport { findAuthenticatedUser } from \"~/services/auth.server\";\nimport { createContext } from \"~/shared/context.server\";\n\nexport const meta = () => {\n  const metas: ReturnType<MetaFunction> = [];\n\n  metas.push({ title: \"Webstudio Dashboard | Projects\" });\n\n  return metas;\n};\n\n/**\n * When deleting/adding a project, then navigating to a new project and pressing the back button,\n * the dashboard page may display stale data because it’s being retrieved from the browser’s back/forward cache (bfcache).\n *\n * https://web.dev/articles/bfcache\n *\n */\nexport const headers = () => {\n  return {\n    \"Cache-Control\": \"no-store\",\n  };\n};\n\nconst dashboardProjectCaller = createCallerFactory(dashboardProjectRouter);\n\nconst loadDashboardData = async (request: Request) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  const user = await findAuthenticatedUser(request);\n\n  const url = new URL(request.url);\n\n  if (user === null) {\n    throw redirect(\n      loginPath({\n        returnTo: `${url.pathname}${url.search}`,\n      })\n    );\n  }\n\n  const context = await createContext(request);\n\n  if (context.authorization.type !== \"user\") {\n    throw new AuthorizationError(\"You must be logged in to access this page\");\n  }\n\n  const { userPlanFeatures } = context;\n\n  if (userPlanFeatures === undefined) {\n    throw new Response(\"User plan features are not defined\", {\n      status: 404,\n    });\n  }\n\n  const { sourceOrigin } = parseBuilderUrl(request.url);\n\n  const projects = await dashboardProjectCaller(context).findMany({\n    userId: user.id,\n  });\n\n  const templates = await dashboardProjectCaller(context).findManyByIds({\n    projectIds: env.PROJECT_TEMPLATES,\n  });\n\n  return {\n    context,\n    user,\n    origin: sourceOrigin,\n    userPlanFeatures,\n    projects,\n    templates,\n  };\n};\n\nconst getProjectToClone = async (request: Request, context: AppContext) => {\n  const url = new URL(request.url);\n  const projectToCloneAuthToken = url.searchParams.get(\n    \"projectToCloneAuthToken\"\n  );\n\n  if (\n    // Only on navigation requests\n    request.headers.get(\"sec-fetch-mode\") !== \"navigate\" ||\n    projectToCloneAuthToken === null\n  ) {\n    return;\n  }\n\n  // Clone project\n  const token = await authDb.getTokenInfo(projectToCloneAuthToken, context);\n  if (token.canClone === false) {\n    throw new AuthorizationError(\"You don't have access to clone this project\");\n  }\n\n  const project = await projectApi.loadById(\n    token.projectId,\n    await context.createTokenContext(projectToCloneAuthToken)\n  );\n\n  return {\n    id: token.projectId,\n    authToken: projectToCloneAuthToken,\n    title: project.title,\n  };\n};\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n\n  const { context, user, userPlanFeatures, origin, projects, templates } =\n    await loadDashboardData(request);\n\n  const projectToClone = await getProjectToClone(request, context);\n\n  return {\n    user,\n    projects,\n    templates,\n    userPlanFeatures,\n    publisherHost: env.PUBLISHER_HOST,\n    origin,\n    projectToClone,\n  };\n};\n\nexport const shouldRevalidate: ShouldRevalidateFunction = ({\n  defaultShouldRevalidate,\n  currentUrl,\n  nextUrl,\n}) => {\n  // We have the entire data on the client, so we don't need to revalidate when\n  // URL is changing.\n  if (currentUrl.href !== nextUrl.href) {\n    return false;\n  }\n  // When .revalidate() was called explicitely without chaning the URL,\n  // `defaultShouldRevalidate` will be true\n  return defaultShouldRevalidate;\n};\n\nconst DashboardSetup = lazy(async () => {\n  const { DashboardSetup } = await import(\"~/dashboard/index.client\");\n  return { default: DashboardSetup };\n});\n\nconst DashboardRoute = () => {\n  const data = useLoaderData<typeof loader>();\n\n  data.projects.slice(0, 5).forEach((project) => {\n    prefetchDNS(builderUrl({ projectId: project.id, origin: data.origin }));\n  });\n  data.projects.slice(0, 5).forEach((project) => {\n    preconnect(builderUrl({ projectId: project.id, origin: data.origin }));\n  });\n\n  return (\n    <ClientOnly>\n      <DashboardSetup data={data} />\n      <Outlet />\n    </ClientOnly>\n  );\n};\n\nexport default DashboardRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.error.tsx",
    "content": "import { type LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { type MetaFunction } from \"@remix-run/react\";\nimport { builderSessionStorage } from \"~/services/builder-session.server\";\nimport { sessionStorage } from \"~/services/session.server\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { builderAuthenticator } from \"~/services/builder-auth.server\";\nimport { z } from \"zod\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { isBuilder } from \"~/shared/router-utils\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nconst SessionError = z.object({\n  message: z.string(),\n  description: z.string().optional(),\n});\n\nexport const meta: MetaFunction<typeof loader> = () => {\n  const metas: ReturnType<MetaFunction> = [];\n\n  metas.push({ title: \"Webstudio Error\" });\n\n  return metas;\n};\n\nconst developmentErrors = [\n  {\n    message: \"Unknown error\",\n    description: undefined,\n  },\n\n  {\n    message:\n      \"Unknown error Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\",\n    description: undefined,\n  },\n\n  {\n    message: \"Unknown error\",\n    description:\n      \"Unknown error Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\",\n  },\n  {\n    message:\n      \"Unknown error Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\",\n    description:\n      \"Unknown error Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.\",\n  },\n  {\n    message:\n      \"UnknownerrorLoremIpsumissimplydummytextoftheprintingandtypesettingindustry.LoremIpsumhasbeentheindustry'sstandarddummytexteversincethe1500s,whenanunknownprintertookagalleyoftypeandscrambledittomakeatypespecimenbook.Ithassurvivednotonlyfivecenturies,butalsotheleapintoelectronictypesetting,remainingessentiallyunchanged.Itwaspopularisedinthe1960swiththereleaseofLetrasetsheetscontainingLoremIpsumpassages,andmorerecentlywithdesktoppublishingsoftwarelikeAldusPageMakerincludingversionsofLoremIpsum.\",\n    description:\n      \"UnknownerrorLoremIpsumissimplydummytextoftheprintingandtypesettingindustry.LoremIpsumhasbeentheindustry'sstandarddummytexteversincethe1500s,whenanunknownprintertookagalleyoftypeandscrambledittomakeatypespecimenbook.Ithassurvivednotonlyfivecenturies,butalsotheleapintoelectronictypesetting,remainingessentiallyunchanged.Itwaspopularisedinthe1960swiththereleaseofLetrasetsheetscontainingLoremIpsumpassages,andmorerecentlywithdesktoppublishingsoftwarelikeAldusPageMakerincludingversionsofLoremIpsum.\",\n  },\n];\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  preventCrossOriginCookie(request);\n\n  // We leave it here because production errors in remix has a different behavior\n  // https://wstd.dev:5173/error?development-error=0\n  // https://wstd.dev:5173/error?development-error=1\n  // ...\n  const url = new URL(request.url);\n  if (url.searchParams.has(\"development-error\")) {\n    const index = Number(url.searchParams.get(\"development-error\"));\n    if (index >= 0 && index < developmentErrors.length) {\n      throw new Response(JSON.stringify(developmentErrors[index]), {\n        status: 400,\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n      });\n    }\n  }\n\n  const storage = isBuilder(request) ? builderSessionStorage : sessionStorage;\n  const sessionErrorKey = isBuilder(request)\n    ? builderAuthenticator.sessionErrorKey\n    : authenticator.sessionErrorKey;\n\n  const session = await storage.getSession(request.headers.get(\"Cookie\"));\n\n  const rawError = session.get(sessionErrorKey);\n\n  const parsedError = SessionError.safeParse(rawError);\n\n  const error = parsedError.success\n    ? parsedError.data\n    : {\n        message: \"Unknown error\",\n        description: \"\",\n      };\n\n  throw new Response(JSON.stringify(error), {\n    status: 400,\n    headers: {\n      \"Content-Type\": \"application/json\",\n      // Clear the error from the session\n      \"Set-Cookie\": await storage.commitSession(session),\n    },\n  });\n};\n\nexport default function Error() {\n  // Placeholder component to prevent Remix warning:\n  // \"Matched leaf route at location '/{SOME_LOCATION}' does not have an element or Component.\"\n  // Without this, an <Outlet /> with a null value would render an empty page.\n  return (\n    <div>\n      <h1>Error</h1>\n      <p>Unknown error.</p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.login._index.tsx",
    "content": "import {\n  type LinksFunction,\n  type LoaderFunctionArgs,\n  type TypedResponse,\n  json,\n} from \"@remix-run/server-runtime\";\nimport { useLoaderData, type MetaFunction } from \"@remix-run/react\";\nimport { findAuthenticatedUser } from \"~/services/auth.server\";\nimport env from \"~/env/env.server\";\nimport type { LoginProps } from \"~/auth/index.client\";\nimport { useLoginErrorMessage } from \"~/shared/session\";\nimport {\n  comparePathnames,\n  dashboardPath,\n  isDashboard,\n} from \"~/shared/router-utils\";\nimport { returnToCookie } from \"~/services/cookie.server\";\nimport { ClientOnly } from \"~/shared/client-only\";\nimport { lazy } from \"react\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nexport const links: LinksFunction = () => {\n  return [\n    {\n      rel: \"canonical\",\n      href: \"https://apps.webstudio.is/login\",\n    },\n  ];\n};\n\nexport const meta: MetaFunction<typeof loader> = () => {\n  const metas: ReturnType<MetaFunction> = [\n    {\n      name: \"title\",\n      content: \"Webstudio Login\",\n    },\n    {\n      name: \"description\",\n      content: \"Log in to Webstudio to start creating websites.\",\n    },\n    { name: \"robots\", content: \"index, follow\" },\n  ];\n\n  metas.push({ title: \"Webstudio Login\" });\n\n  return metas;\n};\n\nexport const loader = async ({\n  request,\n}: LoaderFunctionArgs): Promise<TypedResponse<LoginProps>> => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  const user = await findAuthenticatedUser(request);\n\n  const url = new URL(request.url);\n  let returnTo = url.searchParams.get(\"returnTo\");\n\n  if (user) {\n    returnTo = returnTo ?? dashboardPath();\n    // Avoid loops\n    if (comparePathnames(returnTo, request.url)) {\n      returnTo = dashboardPath();\n    }\n\n    throw redirect(returnTo);\n  }\n\n  const headers = new Headers();\n\n  headers.append(\"Set-Cookie\", await returnToCookie.serialize(returnTo));\n\n  return json(\n    {\n      isSecretLoginEnabled: env.DEV_LOGIN === \"true\",\n      isGithubEnabled: Boolean(env.GH_CLIENT_ID && env.GH_CLIENT_SECRET),\n      isGoogleEnabled: Boolean(\n        env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET\n      ),\n    },\n    { headers }\n  );\n};\n\nconst Login = lazy(async () => {\n  const { Login } = await import(\"~/auth/index.client\");\n  return { default: Login };\n});\n\nconst LoginRoute = () => {\n  const errorMessage = useLoginErrorMessage();\n  const data = useLoaderData<typeof loader>();\n  return (\n    <ClientOnly>\n      <Login {...data} errorMessage={errorMessage} />\n    </ClientOnly>\n  );\n};\n\nexport default LoginRoute;\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.logout.tsx",
    "content": "import { json, type LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { createDebug } from \"~/shared/debug\";\nimport { builderUrl, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { createCallerFactory } from \"@webstudio-is/trpc-interface/index.server\";\nimport { logoutRouter } from \"~/services/logout-router.server\";\nimport { createContext } from \"~/shared/context.server\";\nimport { redirect } from \"react-router-dom\";\nimport { ClientOnly } from \"~/shared/client-only\";\nimport { useLoaderData, type MetaFunction } from \"@remix-run/react\";\nimport { lazy } from \"react\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\nexport { ErrorBoundary } from \"~/shared/error/error-boundary\";\n\nconst logoutCaller = createCallerFactory(logoutRouter);\n\nconst debug = createDebug(import.meta.url);\n\nexport const meta: MetaFunction<typeof loader> = () => {\n  const metas: ReturnType<MetaFunction> = [];\n\n  metas.push({ title: \"Webstudio Logout\" });\n\n  return metas;\n};\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  const context = await createContext(request);\n\n  const redirectTo = loginPath({});\n\n  if (context.authorization.type !== \"user\") {\n    debug(\"User is not logged in, redirecting to\", redirectTo);\n    throw redirect(redirectTo);\n  }\n\n  try {\n    const buildProjectIdsToLogout =\n      await logoutCaller(context).getLoggedInProjectIds();\n\n    debug(\"buildProjectIdsToLogout\", buildProjectIdsToLogout);\n\n    const url = new URL(request.url);\n    const logoutUrls = buildProjectIdsToLogout.map(\n      (projectId) =>\n        `${builderUrl({ projectId, origin: url.origin })}builder-logout`\n    );\n\n    return json({\n      redirectTo,\n      logoutUrls,\n    });\n  } catch (error) {\n    if (error instanceof Response) {\n      throw error;\n    }\n\n    console.error(error);\n    throw error;\n  }\n};\n\nconst LogoutPage = lazy(async () => {\n  const { LogoutPage } = await import(\"~/shared/logout.client\");\n  return { default: LogoutPage };\n});\n\nexport default function Logout() {\n  const data = useLoaderData<typeof loader>();\n\n  return (\n    <ClientOnly>\n      <LogoutPage logoutUrls={data.logoutUrls} />\n    </ClientOnly>\n  );\n}\n"
  },
  {
    "path": "apps/builder/app/routes/_ui.tsx",
    "content": "import {\n  Links,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n  type ClientLoaderFunctionArgs,\n  type ShouldRevalidateFunction,\n} from \"@remix-run/react\";\nimport interFont from \"@fontsource-variable/inter/index.css?url\";\nimport manropeVariableFont from \"@fontsource-variable/manrope/index.css?url\";\nimport robotoMonoFont from \"@fontsource/roboto-mono/index.css?url\";\nimport appCss from \"../shared/app.css?url\";\nimport {\n  json,\n  type LinksFunction,\n  type LoaderFunctionArgs,\n} from \"@remix-run/server-runtime\";\nimport { ErrorBoundary as ErrorBoundaryComponent } from \"~/shared/error/error-boundary\";\nimport { getCsrfTokenAndCookie } from \"~/services/csrf-session.server\";\nimport invariant from \"tiny-invariant\";\nimport {\n  csrfToken as clientCsrfToken,\n  updateCsrfToken,\n} from \"~/shared/csrf.client\";\n\nexport const links: LinksFunction = () => {\n  // `links` returns an array of objects whose\n  // properties map to the `<link />` component props\n  return [\n    { rel: \"stylesheet\", href: interFont },\n    { rel: \"stylesheet\", href: manropeVariableFont },\n    { rel: \"stylesheet\", href: robotoMonoFont },\n    { rel: \"stylesheet\", href: appCss },\n  ];\n};\n\nconst Document = (props: { children: React.ReactNode }) => {\n  return (\n    <html lang=\"en\">\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n        <Meta />\n        <Links />\n      </head>\n      <body>\n        {props.children}\n        <ScrollRestoration />\n        <Scripts />\n      </body>\n    </html>\n  );\n};\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  const [csrfToken, setCookieValue] = await getCsrfTokenAndCookie(request);\n\n  if (request.headers.get(\"sec-fetch-mode\") !== \"navigate\") {\n    return json({ csrfToken: \"\" });\n  }\n\n  const headers = new Headers();\n\n  if (setCookieValue !== undefined) {\n    headers.set(\"Set-Cookie\", setCookieValue);\n  }\n\n  return json(\n    { csrfToken },\n    {\n      headers,\n    }\n  );\n};\n\nexport const clientLoader = async ({\n  serverLoader,\n}: ClientLoaderFunctionArgs) => {\n  const serverData = await serverLoader<typeof loader>();\n\n  if (clientCsrfToken === undefined) {\n    const { csrfToken } = serverData;\n    invariant(csrfToken !== \"\", \"CSRF token is empty\");\n    updateCsrfToken(csrfToken);\n  }\n\n  // Hide real CSRF token from window.__remixContext\n  serverData.csrfToken = \"\";\n  return serverData;\n};\n\nclientLoader.hydrate = true;\n\nexport const ErrorBoundary = () => {\n  return (\n    <Document>\n      <ErrorBoundaryComponent />\n    </Document>\n  );\n};\n\nexport default function Layout() {\n  return (\n    <Document>\n      <Outlet />\n    </Document>\n  );\n}\n\nexport const shouldRevalidate: ShouldRevalidateFunction = () => {\n  return false;\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.dev.tsx",
    "content": "import { type ActionFunctionArgs } from \"@remix-run/server-runtime\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { dashboardPath, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { clearReturnToCookie, returnToPath } from \"~/services/cookie.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect, setNoStoreToRedirect } from \"~/services/no-store-redirect\";\n\nexport default function Dev() {\n  return null;\n}\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  preventCrossOriginCookie(request);\n\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  const returnTo = (await returnToPath(request)) ?? dashboardPath();\n\n  try {\n    await authenticator.authenticate(\"dev\", request, {\n      successRedirect: returnTo,\n      throwOnError: true,\n    });\n  } catch (error: unknown) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      return setNoStoreToRedirect(await clearReturnToCookie(request, error));\n    }\n\n    if (error instanceof Error) {\n      console.error(\"Error authenticating with dev\", error);\n      return redirect(\n        loginPath({\n          error: AUTH_PROVIDERS.LOGIN_DEV,\n          message: error?.message,\n          returnTo,\n        })\n      );\n    }\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.github.tsx",
    "content": "import { type ActionFunctionArgs } from \"@remix-run/server-runtime\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { dashboardPath, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { clearReturnToCookie, returnToPath } from \"~/services/cookie.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect, setNoStoreToRedirect } from \"~/services/no-store-redirect\";\n\nexport default function GH() {\n  return null;\n}\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All POST requests from the builder or canvas app are safeguarded by preventCrossOriginCookie\n\n  const returnTo = (await returnToPath(request)) ?? dashboardPath();\n\n  try {\n    return await authenticator.authenticate(\"github\", request, {\n      successRedirect: returnTo,\n      throwOnError: true,\n    });\n  } catch (error) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      return setNoStoreToRedirect(await clearReturnToCookie(request, error));\n    }\n\n    const message = error instanceof Error ? error?.message : \"unknown error\";\n\n    console.error({\n      error,\n      extras: {\n        loginMethod: AUTH_PROVIDERS.LOGIN_GITHUB,\n      },\n    });\n\n    return redirect(\n      loginPath({\n        error: AUTH_PROVIDERS.LOGIN_GITHUB,\n        message: message,\n        returnTo,\n      })\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.github_.callback.tsx",
    "content": "import { type LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { dashboardPath, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { clearReturnToCookie, returnToPath } from \"~/services/cookie.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect, setNoStoreToRedirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n  preventCrossOriginCookie(request);\n\n  allowedDestinations(request, [\"document\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  const returnTo = (await returnToPath(request)) ?? dashboardPath();\n\n  try {\n    await authenticator.authenticate(\"github\", request, {\n      successRedirect: returnTo,\n      throwOnError: true,\n    });\n  } catch (error) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      return setNoStoreToRedirect(await clearReturnToCookie(request, error));\n    }\n\n    const message = error instanceof Error ? error?.message : \"unknown error\";\n\n    console.error({\n      error,\n      extras: {\n        loginMethod: AUTH_PROVIDERS.LOGIN_GITHUB,\n      },\n    });\n\n    return redirect(\n      loginPath({\n        error: AUTH_PROVIDERS.LOGIN_GITHUB,\n        message,\n        returnTo,\n      })\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.google.tsx",
    "content": "import {\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n} from \"@remix-run/server-runtime\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { dashboardPath, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { clearReturnToCookie, returnToPath } from \"~/services/cookie.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect, setNoStoreToRedirect } from \"~/services/no-store-redirect\";\n\nexport default function Google() {\n  return null;\n}\n\nexport const loader = (_args: LoaderFunctionArgs) => redirect(\"/login\");\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All POST requests from the builder or canvas app are safeguarded by preventCrossOriginCookie\n\n  const returnTo = (await returnToPath(request)) ?? dashboardPath();\n\n  try {\n    return await authenticator.authenticate(\"google\", request, {\n      successRedirect: returnTo,\n      throwOnError: true,\n    });\n  } catch (error) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      return setNoStoreToRedirect(await clearReturnToCookie(request, error));\n    }\n\n    const message = error instanceof Error ? error?.message : \"unknown error\";\n\n    console.error({\n      error,\n      extras: {\n        loginMethod: AUTH_PROVIDERS.LOGIN_GOOGLE,\n      },\n    });\n    return redirect(\n      loginPath({\n        error: AUTH_PROVIDERS.LOGIN_GOOGLE,\n        message: message,\n        returnTo,\n      })\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.google_.callback.tsx",
    "content": "import { type LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { dashboardPath, isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { clearReturnToCookie, returnToPath } from \"~/services/cookie.server\";\nimport { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect, setNoStoreToRedirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  const returnTo = (await returnToPath(request)) ?? dashboardPath();\n\n  try {\n    await authenticator.authenticate(\"google\", request, {\n      successRedirect: returnTo,\n      throwOnError: true,\n    });\n  } catch (error) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      return setNoStoreToRedirect(await clearReturnToCookie(request, error));\n    }\n\n    const message = error instanceof Error ? error?.message : \"unknown error\";\n\n    console.error({\n      error,\n      extras: {\n        loginMethod: AUTH_PROVIDERS.LOGIN_GOOGLE,\n      },\n    });\n    return redirect(\n      loginPath({\n        error: AUTH_PROVIDERS.LOGIN_GOOGLE,\n        message,\n        returnTo,\n      })\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.ws.ts",
    "content": "import type { LoaderFunction } from \"@remix-run/node\";\nimport { createDebug } from \"~/shared/debug\";\nimport { builderAuthenticator } from \"~/services/builder-auth.server\";\nimport { comparePathnames, isBuilder } from \"~/shared/router-utils\";\nimport { isRedirectResponse, returnToCookie } from \"~/services/cookie.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { setNoStoreToRedirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\n\nconst debug = createDebug(import.meta.url);\n\n// Endpoint to force user relogin\nexport const loader: LoaderFunction = async ({ request }) => {\n  if (false === isBuilder(request)) {\n    debug(`Request url is not the builder URL ${request.url}`);\n\n    return new Response(\"Only builder URL is allowed\", {\n      status: 404,\n    });\n  }\n\n  try {\n    preventCrossOriginCookie(request);\n    allowedDestinations(request, [\"document\"]);\n    // CSRF is not needed for document only routes\n\n    debug(\n      \"Authenticate request received, starting authentication and authorization process\"\n    );\n\n    return await builderAuthenticator.authenticate(\"ws\", request, {\n      throwOnError: true,\n    });\n  } catch (error) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      if (isRedirectResponse(error)) {\n        const noStoreResponse = setNoStoreToRedirect(error);\n        const response = new Response(noStoreResponse.body, noStoreResponse);\n\n        const url = new URL(request.url);\n        let returnTo = url.searchParams.get(\"returnTo\");\n\n        if (returnTo !== null) {\n          if (comparePathnames(returnTo, request.url)) {\n            // avoid loops\n            returnTo = \"/\";\n          }\n\n          // Do not allow absolute URLs\n          if (URL.canParse(returnTo)) {\n            returnTo = \"/\";\n          }\n        }\n\n        const options = returnTo === null ? { maxAge: -1 } : {};\n\n        response.headers.append(\n          \"Set-Cookie\",\n          await returnToCookie.serialize(returnTo, options)\n        );\n\n        return response;\n      }\n\n      return error;\n    }\n\n    debug(\"error\", error);\n\n    console.error(\"error\", error);\n    throw error;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/auth.ws_.callback.ts",
    "content": "import { type LoaderFunction } from \"@remix-run/server-runtime\";\nimport { AuthorizationError } from \"remix-auth\";\nimport { createDebug } from \"~/shared/debug\";\nimport { clearReturnToCookie, returnToPath } from \"~/services/cookie.server\";\nimport { builderAuthenticator } from \"~/services/builder-auth.server\";\nimport { OAuth2Error } from \"remix-auth-oauth2\";\nimport { builderSessionStorage } from \"~/services/builder-session.server\";\nimport { builderPath, isBuilder } from \"~/shared/router-utils\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect, setNoStoreToRedirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\n\nconst debug = createDebug(import.meta.url);\n\nexport const loader: LoaderFunction = async ({ request }) => {\n  if (false === isBuilder(request)) {\n    debug(`Request url is not the builder URL ${request.url}`);\n\n    return new Response(\"Only builder URL is allowed\", {\n      status: 404,\n    });\n  }\n\n  try {\n    preventCrossOriginCookie(request);\n    allowedDestinations(request, [\"document\"]);\n    // CSRF is not needed for document only routes\n\n    const returnTo = (await returnToPath(request)) ?? builderPath();\n\n    debug(\"Start exchanging the code for the access token\");\n\n    await builderAuthenticator.authenticate(\"ws\", request, {\n      throwOnError: true,\n      successRedirect: returnTo,\n    });\n  } catch (error) {\n    // all redirects are basically errors and in that case we don't want to catch it\n    if (error instanceof Response) {\n      debug(\"Response with Cookie\");\n\n      const session = await builderSessionStorage.getSession(\n        error.headers.get(\"Set-Cookie\")\n      );\n\n      // Cleanup due to a bug in remix-auth-oauth2: https://github.com/sergiodxa/remix-auth-oauth2/issues/122\n      const sessionStateKey = \"oauth2:state\";\n      const sessionCodeVerifierKey = \"oauth2:codeVerifier\";\n      session.unset(sessionStateKey);\n      session.unset(sessionCodeVerifierKey);\n\n      const response = new Response(error.body, error);\n      // Use set instead of append to overwrite the cookie\n      response.headers.set(\n        \"Set-Cookie\",\n        await builderSessionStorage.commitSession(session)\n      );\n\n      return setNoStoreToRedirect(await clearReturnToCookie(request, response));\n    }\n\n    if (error instanceof AuthorizationError) {\n      debug(\"Authorization error\", error.message, error.cause?.message);\n\n      const sessionError = {\n        message: error.message,\n        description: \"\",\n      };\n\n      if (error.cause instanceof OAuth2Error) {\n        debug(\n          \"OAuth2Error error\",\n          error.cause?.message,\n          error.cause?.description\n        );\n        sessionError.description = error.cause.description ?? \"\";\n      }\n\n      const session = await builderSessionStorage.getSession(\n        request.headers.get(\"Cookie\")\n      );\n      session.flash(builderAuthenticator.sessionErrorKey, sessionError);\n\n      throw redirect(\"/error\", {\n        headers: {\n          \"Set-Cookie\": await builderSessionStorage.commitSession(session),\n        },\n      });\n    }\n\n    debug(\"error\", error);\n    console.error(\"error\", error);\n    throw error;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/builder-logout.ts",
    "content": "import { json, type ActionFunctionArgs } from \"@remix-run/server-runtime\";\nimport { createDebug } from \"~/shared/debug\";\nimport { builderAuthenticator } from \"~/services/builder-auth.server\";\nimport { getAuthorizationServerOrigin } from \"~/shared/router-utils/origins\";\nimport { isBuilder, loginPath } from \"~/shared/router-utils\";\nimport { isRedirectResponse } from \"~/services/cookie.server\";\n\nconst debug = createDebug(import.meta.url);\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  // IMPORTANT: This route allows cross-origin cookies to enable dashboard logout from builders.\n  // Enforcing preflight by checking Content-Type to be application/json.\n  // At the SaaS proxy side, only the allowed \"access-control-allow-origin\" header is set for OPTIONS requests.\n\n  if (false === isBuilder(request)) {\n    throw new Response(\"Not found\", {\n      status: 404,\n    });\n  }\n\n  if (request.method !== \"POST\") {\n    return json(\n      { message: \"Method not allowed\" },\n      {\n        status: 405,\n      }\n    );\n  }\n\n  if (request.headers.get(\"sec-fetch-site\") === \"same-origin\") {\n    // To prevent logout initiated from the builder iframe\n\n    throw new Response(\"Only cross-origin requests are allowed\", {\n      status: 403,\n    });\n  }\n\n  if (\n    false === request.headers.get(\"Content-Type\")?.includes(\"application/json\")\n  ) {\n    // Enforce preflight request, preflight is checked on allowed origin\n\n    throw new Response(\n      \"Invalid content type, only application/json is allowed\",\n      {\n        status: 415,\n      }\n    );\n  }\n\n  const redirectTo = `${getAuthorizationServerOrigin(request.url)}${loginPath({})}`;\n\n  try {\n    debug(\"Logging out\");\n\n    await builderAuthenticator.logout(request, {\n      redirectTo,\n    });\n  } catch (error) {\n    if (error instanceof Response) {\n      if (isRedirectResponse(error)) {\n        return new Response(null, {\n          status: 204,\n          headers: error.headers,\n        });\n      }\n\n      return error;\n    }\n\n    console.error(error);\n    throw error;\n  }\n\n  throw new Response(\"Should not reach this point\", {\n    status: 500,\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/routes/cgi.asset.$.ts",
    "content": "import { createReadStream, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { createReadableStreamFromReadable } from \"@remix-run/node\";\nimport type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport {\n  getMimeTypeByFilename,\n  isAllowedExtension,\n  decodePathFragment,\n} from \"@webstudio-is/sdk\";\nimport { fileUploadPath } from \"~/shared/asset-client\";\n\n// This route serves generic assets without processing\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  const basePath = `/cgi/asset/`;\n\n  const url = new URL(request.url);\n  const name = decodePathFragment(url.pathname.slice(basePath.length));\n\n  // Allow direct asset access, and from the same origin\n  const refererRawUrl = request.headers.get(\"referer\");\n  const refererUrl = refererRawUrl === null ? url : new URL(refererRawUrl);\n  if (refererUrl.host !== url.host) {\n    throw new Response(\"Forbidden\", {\n      status: 403,\n    });\n  }\n\n  // Support absolute urls locally\n  if (URL.canParse(name)) {\n    return fetch(name);\n  }\n\n  const filePath = join(process.cwd(), fileUploadPath, name);\n\n  if (existsSync(filePath) === false) {\n    throw new Response(\"Not found\", {\n      status: 404,\n    });\n  }\n\n  // Validate file extension against allowed types\n  const extension = name.split(\".\").pop()?.toLowerCase();\n  const contentType = getMimeTypeByFilename(name);\n\n  // Reject files with disallowed extensions or MIME types\n  if (\n    contentType === \"application/octet-stream\" ||\n    !extension ||\n    !isAllowedExtension(extension)\n  ) {\n    throw new Response(\"File type not allowed\", {\n      status: 403,\n    });\n  }\n\n  return new Response(\n    createReadableStreamFromReadable(createReadStream(filePath)),\n    {\n      headers: {\n        \"content-type\": contentType,\n        \"Access-Control-Allow-Origin\": url.origin,\n      },\n    }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/routes/cgi.empty[.]gif.ts",
    "content": "// For development purposes only\n// Exists on saas worker\nexport const loader = async () => {\n  const emptyGif = \"R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=\";\n\n  return new Response(\n    Uint8Array.from(atob(emptyGif), (c) => c.charCodeAt(0)).buffer,\n    {\n      headers: {\n        \"Content-Type\": \"image/gif\",\n        \"Cache-Control\": \"public, max-age=10\",\n      },\n    }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/routes/cgi.image.$.ts",
    "content": "import { createReadStream, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { z } from \"zod\";\nimport { createReadableStreamFromReadable } from \"@remix-run/node\";\nimport type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { wsImageLoader } from \"@webstudio-is/image\";\nimport { decodePathFragment } from \"@webstudio-is/sdk\";\nimport env from \"~/env/env.server\";\nimport { getImageNameAndType } from \"~/builder/shared/assets/asset-utils\";\nimport { fileUploadPath } from \"~/shared/asset-client\";\n\nconst ImageParams = z.object({\n  width: z.string().transform((value) => Math.round(parseFloat(value))),\n  height: z.optional(\n    z.string().transform((value) => Math.round(parseFloat(value)))\n  ),\n  fit: z.optional(z.literal(\"pad\")),\n  background: z.optional(z.string()),\n\n  quality: z.string().transform((value) => Math.round(parseFloat(value))),\n\n  format: z.union([\n    z.literal(\"auto\"),\n    z.literal(\"avif\"),\n    z.literal(\"webp\"),\n    z.literal(\"json\"),\n    z.literal(\"jpeg\"),\n    z.literal(\"png\"),\n    z.literal(\"raw\"),\n  ]),\n});\n\n// this route used as proxy for images to cloudflare endpoint\n// https://developers.cloudflare.com/fundamentals/get-started/reference/cdn-cgi-endpoint/\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  const basePath = `/cgi/image/`;\n\n  const url = new URL(request.url);\n  const name = decodePathFragment(url.pathname.slice(basePath.length));\n\n  // The code should be +- same as here https://github.com/webstudio-is/webstudio-saas/blob/6c9a3bfb67cf5a221c20666de34bd20dc14bd558/packages/assets-proxy/src/image-transform.ts#L68\n  const rawParameters: Record<string, string> = {\n    format: url.searchParams.get(\"format\") ?? \"auto\",\n    width: url.searchParams.get(\"width\") ?? \"0\",\n    quality: url.searchParams.get(\"quality\") ?? \"80\",\n  };\n\n  const height = url.searchParams.get(\"height\");\n\n  if (height != null) {\n    rawParameters.height = height;\n  }\n\n  const fit = url.searchParams.get(\"fit\");\n\n  if (fit != null) {\n    rawParameters.fit = fit;\n  }\n\n  const imageParameters = ImageParams.parse(rawParameters);\n\n  // Allow direct image access, and from the same origin\n  const refererRawUrl = request.headers.get(\"referer\");\n  const refererUrl = refererRawUrl === null ? url : new URL(refererRawUrl);\n  if (refererUrl.host !== url.host) {\n    throw new Response(\"Forbidden\", {\n      status: 403,\n    });\n  }\n\n  if (env.RESIZE_ORIGIN !== undefined) {\n    const imgHref = wsImageLoader({\n      src: name,\n      ...imageParameters,\n      format: \"auto\",\n    });\n\n    const imgUrl = new URL(env.RESIZE_ORIGIN + imgHref);\n    imgUrl.search = url.search;\n\n    const response = await fetch(imgUrl.href, {\n      headers: {\n        accept: request.headers.get(\"accept\") ?? \"\",\n        \"accept-encoding\": request.headers.get(\"accept-encoding\") ?? \"\",\n      },\n    });\n\n    const responseWHeaders = new Response(response.body, response);\n\n    if (false === responseWHeaders.ok) {\n      console.error(\n        `Request to Image url ${imgUrl.href} responded with status = ${responseWHeaders.status}`\n      );\n    }\n\n    responseWHeaders.headers.set(\"Access-Control-Allow-Origin\", url.origin);\n\n    return responseWHeaders;\n  }\n\n  // support absolute urls locally\n  if (URL.canParse(name)) {\n    return fetch(name);\n  }\n  const filePath = join(process.cwd(), fileUploadPath, name);\n\n  if (existsSync(filePath) === false) {\n    throw new Response(\"Not found\", {\n      status: 404,\n    });\n  }\n\n  const [contentType] = getImageNameAndType(name) ?? [\"image/png\"];\n\n  return new Response(\n    createReadableStreamFromReadable(createReadStream(filePath)),\n    {\n      headers: {\n        \"content-type\": contentType,\n      },\n    }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/routes/cgi.video.$.ts",
    "content": "import { createReadStream, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { createReadableStreamFromReadable } from \"@remix-run/node\";\nimport type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport env from \"~/env/env.server\";\nimport { getMimeTypeByFilename, decodePathFragment } from \"@webstudio-is/sdk\";\nimport { fileUploadPath } from \"~/shared/asset-client\";\n\n// this route used as proxy for videos to cloudflare endpoint or serve local files\n// https://developers.cloudflare.com/fundamentals/get-started/reference/cdn-cgi-endpoint/\n\nexport const loader = async ({ request }: LoaderFunctionArgs) => {\n  const basePath = `/cgi/video/`;\n  const url = new URL(request.url);\n  const name = decodePathFragment(url.pathname.slice(basePath.length));\n\n  if (env.RESIZE_ORIGIN !== undefined) {\n    const videoUrl = new URL(env.RESIZE_ORIGIN + url.pathname);\n    videoUrl.search = url.search;\n\n    const response = await fetch(videoUrl.href, {\n      headers: {\n        accept: request.headers.get(\"accept\") ?? \"\",\n        \"accept-encoding\": request.headers.get(\"accept-encoding\") ?? \"\",\n        range: request.headers.get(\"range\") ?? \"\",\n      },\n    });\n\n    const responseWHeaders = new Response(response.body, response);\n\n    if (false === responseWHeaders.ok) {\n      console.error(\n        `Request to Video url ${videoUrl.href} responded with status = ${responseWHeaders.status}`\n      );\n    }\n\n    responseWHeaders.headers.set(\"Access-Control-Allow-Origin\", url.origin);\n\n    return responseWHeaders;\n  }\n\n  // support absolute urls locally\n  if (URL.canParse(name)) {\n    return fetch(name);\n  }\n\n  const filePath = join(process.cwd(), fileUploadPath, name);\n\n  if (existsSync(filePath) === false) {\n    throw new Response(\"Not found\", {\n      status: 404,\n    });\n  }\n\n  const contentType = getMimeTypeByFilename(name);\n\n  return new Response(\n    createReadableStreamFromReadable(createReadStream(filePath)),\n    {\n      headers: {\n        \"content-type\": contentType,\n        \"accept-ranges\": \"bytes\",\n      },\n    }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/routes/dashboard-logout.ts",
    "content": "import { json } from \"@remix-run/server-runtime\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { isDashboard, loginPath } from \"~/shared/router-utils\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { type ActionFunctionArgs } from \"react-router-dom\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\nimport { isRedirectResponse } from \"@remix-run/server-runtime/dist/responses\";\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  try {\n    if (false === isDashboard(request)) {\n      throw new Response(\"Not found\", {\n        status: 404,\n      });\n    }\n\n    preventCrossOriginCookie(request);\n    await checkCsrf(request);\n\n    const redirectTo = loginPath({});\n\n    await authenticator.logout(request, {\n      redirectTo,\n    });\n  } catch (error) {\n    if (error instanceof Response && isRedirectResponse(error)) {\n      const headers = new Headers();\n\n      if (error.headers.get(\"Set-Cookie\")) {\n        headers.set(\"Set-Cookie\", error.headers.get(\"Set-Cookie\")!);\n      }\n\n      headers.set(\"Content-Type\", \"application/json\");\n\n      return json(\n        {\n          redirectTo: error.headers.get(\"Location\"),\n        },\n        {\n          headers,\n        }\n      );\n    }\n\n    return error;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/n8n.$.tsx",
    "content": "import { type LoaderFunctionArgs, json } from \"@remix-run/server-runtime\";\nimport { isRouteErrorResponse, useRouteError } from \"@remix-run/react\";\nimport { z } from \"zod\";\nimport { findAuthenticatedUser } from \"~/services/auth.server\";\nimport { isDashboard, loginPath } from \"~/shared/router-utils\";\nimport env from \"~/env/env.server\";\nimport cookie from \"cookie\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { redirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\n\nconst zN8NResponse = z.union([\n  z.object({\n    type: z.literal(\"error\"),\n    error: z.string(),\n  }),\n\n  z.object({\n    type: z.literal(\"redirect\"),\n    to: z.string(),\n  }),\n]);\nconst zWebhookEnv = z.object({\n  N8N_WEBHOOK_URL: z.string(),\n  N8N_WEBHOOK_TOKEN: z.string(),\n});\n\nexport const loader = async ({ request, params }: LoaderFunctionArgs) => {\n  if (isDashboard(request) === false) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  const user = await findAuthenticatedUser(request);\n\n  if (user === null) {\n    const url = new URL(request.url);\n    throw redirect(\n      loginPath({\n        returnTo: `${url.pathname}?${url.searchParams.toString()}`,\n      })\n    );\n  }\n\n  const webhookEnvParsed = zWebhookEnv.safeParse(env);\n  if (webhookEnvParsed.success === false) {\n    throw new Response(webhookEnvParsed.error.message, {\n      status: 400,\n    });\n  }\n\n  const webhookEnv = webhookEnvParsed.data;\n\n  const n8nWebhookUrl = new URL(webhookEnv.N8N_WEBHOOK_URL);\n  n8nWebhookUrl.pathname = `${n8nWebhookUrl.pathname}/${\n    env.DEPLOYMENT_ENVIRONMENT ?? \"local\"\n  }/${params[\"*\"]}`\n    .split(\"/\")\n    .filter(Boolean)\n    .join(\"/\");\n  n8nWebhookUrl.search = new URL(request.url).search;\n\n  const requestUrl = new URL(request.url);\n\n  const response = await fetch(n8nWebhookUrl.href, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: `Bearer ${webhookEnv.N8N_WEBHOOK_TOKEN}`,\n    },\n    body: JSON.stringify({\n      userId: user.id,\n      // For anonymous tracking like posthog\n      cookies: cookie.parse(request.headers.get(\"cookie\") ?? \"\"),\n      requestUrl: requestUrl.href,\n    }),\n  });\n\n  if (response.ok === false) {\n    const text = await response.text();\n\n    throw new Response(\n      `Fetch error status=\"${response.status}\"\\nMessage:\\n${text.slice(\n        0,\n        1000\n      )}\"`,\n      {\n        status: response.status,\n      }\n    );\n  }\n  const responseJson = await response.json();\n  const n8nResponseParsed = zN8NResponse.safeParse(responseJson);\n\n  if (n8nResponseParsed.success === false) {\n    throw new Response(n8nResponseParsed.error.message, {\n      status: 400,\n    });\n  }\n\n  const n8nResponse = n8nResponseParsed.data;\n\n  if (n8nResponse.type === \"error\") {\n    throw new Response(n8nResponse.error, {\n      status: 400,\n    });\n  }\n\n  if (n8nResponse.type === \"redirect\") {\n    throw redirect(n8nResponse.to);\n  }\n\n  n8nResponse satisfies never;\n\n  return json({});\n};\n\nexport const ErrorBoundary = () => {\n  const error = useRouteError();\n\n  if (isRouteErrorResponse(error)) {\n    return <div style={{ whiteSpace: \"pre-wrap\" }}>{error.data}</div>;\n  }\n\n  if (error instanceof Error) {\n    return <div style={{ whiteSpace: \"pre-wrap\" }}>{error.message}</div>;\n  }\n\n  return <div style={{ whiteSpace: \"pre-wrap\" }}>{String(error)}</div>;\n};\n"
  },
  {
    "path": "apps/builder/app/routes/oauth.ws.authorize.tsx",
    "content": "import { json, type LoaderFunction } from \"@remix-run/server-runtime\";\nimport { z } from \"zod\";\nimport { createDebug } from \"~/shared/debug\";\nimport { fromError } from \"zod-validation-error\";\nimport {\n  getAuthorizationServerOrigin,\n  isBuilderUrl,\n} from \"~/shared/router-utils/origins\";\nimport env from \"~/env/env.server\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { createCodeToken } from \"~/services/token.server\";\nimport { isUserAuthorizedForProject } from \"~/services/builder-access.server\";\nimport {\n  builderUrl,\n  compareUrls,\n  isDashboard,\n  loginPath,\n} from \"~/shared/router-utils\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport * as session from \"~/services/session.server\";\nimport { redirect } from \"~/services/no-store-redirect\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\n\nconst debug = createDebug(import.meta.url);\n\n// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1\nconst createOauthError =\n  (redirectUri: string, state: string) =>\n  (\n    error: \"invalid_request\" | \"invalid_scope\" | \"unauthorized_client\",\n    errorDescription: string\n  ) => {\n    const url = new URL(redirectUri);\n    url.search = \"\";\n\n    url.searchParams.set(\"error\", error);\n    url.searchParams.set(\"error_description\", errorDescription);\n    if (state) {\n      url.searchParams.set(\"state\", state);\n    }\n\n    return redirect(url.href, { status: 302 });\n  };\n\n/**\n * OAuth 2.0 Authorization Request\n *\n * https://datatracker.ietf.org/doc/html/rfc7636#section-4.3\n *\n * https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1\n */\nconst OAuthParams = z.object({\n  // Ensure that the response_type is valid and supported by the authorization server (e.g., code for the authorization code grant type).\n  response_type: z.literal(\"code\"),\n  redirect_uri: z.string().url(),\n\n  client_id: z.string(),\n  state: z.string(),\n  scope: z\n    .string()\n    .refine((str) => str.startsWith(\"project:\"), {\n      message: \"Only 'project:' scopes are allowed\",\n    })\n    .transform((scope) => ({\n      projectId: scope.split(\":\")[1],\n    })),\n  code_challenge: z.string(),\n  code_challenge_method: z.literal(\"S256\"),\n});\n\nconst OAuthRedirectUri = z.object({\n  redirect_uri: z.string().url(),\n});\n\n/**\n * Based RFC 6749 and RFC 7636\n *\n * https://datatracker.ietf.org/doc/html/rfc6749\n *\n * https://datatracker.ietf.org/doc/html/rfc7636\n */\nexport const loader: LoaderFunction = async ({ request }) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  try {\n    debug(\"Authorize request received\", request.url);\n\n    const url = new URL(request.url);\n    const searchParams = Object.fromEntries(url.searchParams);\n\n    const parsedRedirect = OAuthRedirectUri.safeParse(searchParams);\n\n    if (false === parsedRedirect.success) {\n      debug(\"redirect_uri not provided in query params\");\n\n      return json(\n        {\n          error: \"invalid_request\",\n          error_description: \"No redirect_uri provided\",\n          error_uri: \"https://tools.ietf.org/html/rfc6749#section-3.1.2\",\n        },\n        { status: 400 }\n      );\n    }\n\n    // eslint-disable-next-line camelcase\n    const { redirect_uri } = parsedRedirect.data;\n\n    // Validate the redirect_uri\n    // It is not pre-registered but it must match the AuthorizationServerOrigin\n    if (\n      getAuthorizationServerOrigin(request.url) !==\n        getAuthorizationServerOrigin(redirect_uri) ||\n      new URL(redirect_uri).pathname !== \"/auth/ws/callback\" ||\n      false === isBuilderUrl(redirect_uri)\n    ) {\n      debug(\"redirect_uri does not match the registered redirect URIs\");\n\n      return json(\n        {\n          error: \"invalid_request\",\n          error_description:\n            \"The redirect_uri provided does not match the registered redirect URIs.\",\n          error_uri: \"https://tools.ietf.org/html/rfc6749#section-3.1.2\",\n        },\n        { status: 400 }\n      );\n    }\n\n    let oauthError = createOauthError(redirect_uri, searchParams.state);\n\n    const parsedOAuthParams = OAuthParams.safeParse(searchParams);\n\n    if (false === parsedOAuthParams.success) {\n      debug(fromError(parsedOAuthParams.error).toString());\n\n      return oauthError(\n        \"invalid_request\",\n        fromError(parsedOAuthParams.error).toString()\n      );\n    }\n\n    // Reinit with parsed state\n    oauthError = createOauthError(redirect_uri, parsedOAuthParams.data.state);\n\n    // client_id: Verify that the client_id is valid and corresponds to a registered client.\n    if (parsedOAuthParams.data.client_id !== env.AUTH_WS_CLIENT_ID) {\n      debug(\"Client is not registered\");\n\n      return oauthError(\"unauthorized_client\", \"Client is not registered\");\n    }\n\n    const oAuthParams = parsedOAuthParams.data;\n\n    const sessionData = await authenticator.isAuthenticated(request);\n\n    if (sessionData) {\n      debug(`User id=${sessionData.userId} is authenticated`);\n\n      const isAuthorized = await isUserAuthorizedForProject(\n        sessionData.userId,\n        oAuthParams.scope.projectId\n      );\n\n      // scope: Ensure the requested scope is valid, authorized, and within the permissions granted to the client.\n      if (false === isAuthorized) {\n        debug(\n          `User ${sessionData.userId} is not the owner of ${oAuthParams.scope.projectId}, denying access`\n        );\n        return oauthError(\n          \"unauthorized_client\",\n          \"User does not have access to the project\"\n        );\n      }\n\n      // redirect_uri: Ensure the redirect_uri parameter value is valid and authorized\n      if (\n        false ===\n        compareUrls(\n          new URL(redirect_uri).origin,\n          builderUrl({\n            projectId: oAuthParams.scope.projectId,\n            origin: getAuthorizationServerOrigin(request.url),\n          })\n        )\n      ) {\n        debug(\"redirect_uri does not match the registered redirect URIs\");\n\n        return json(\n          {\n            error: \"invalid_request\",\n            error_description:\n              \"The redirect_uri provided does not match the registered redirect URIs.\",\n            error_uri: \"https://tools.ietf.org/html/rfc6749#section-3.1.2\",\n          },\n          { status: 400 }\n        );\n      }\n\n      debug(\n        `User ${sessionData.userId} is the owner of ${oAuthParams.scope.projectId}, creating token`\n      );\n\n      // We do not use database now.\n      // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4\n      const code = await createCodeToken(\n        {\n          userId: sessionData.userId,\n          projectId: oAuthParams.scope.projectId,\n          codeChallenge: oAuthParams.code_challenge,\n        },\n        env.AUTH_WS_CLIENT_SECRET,\n        { maxAge: 1000 * 60 * 5 }\n      );\n\n      const redirectUri = new URL(oAuthParams.redirect_uri);\n      redirectUri.search = \"\";\n\n      redirectUri.searchParams.set(\"code\", code);\n      // state: If present, store the state parameter to return it unchanged in the response\n      redirectUri.searchParams.set(\"state\", oAuthParams.state);\n\n      debug(\n        `Code ${code} created, redirecting to redirect_uri: ${redirectUri.href}`\n      );\n\n      const bloomFilter = await session.readLoginSessionBloomFilter(request);\n\n      bloomFilter.add(oAuthParams.scope.projectId);\n\n      return session.writeLoginSessionBloomFilter(\n        request,\n        redirect(redirectUri.href),\n        bloomFilter\n      );\n    }\n\n    sessionData satisfies null;\n\n    debug(\n      \"User is not authenticated, saving current url to returnTo cookie and redirecting to login\"\n    );\n\n    return redirect(loginPath({ returnTo: request.url }));\n  } catch (error) {\n    if (error instanceof Response) {\n      return error;\n    }\n\n    console.error(\"error\", error);\n    debug(\"error\", error);\n\n    throw json(\n      {\n        error: \"server_error\",\n        error_description:\n          error instanceof Error ? error.message : \"Unknown error\",\n        error_uri: \"\",\n      },\n      { status: 500 }\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/oauth.ws.token.ts",
    "content": "import { type ActionFunctionArgs, json } from \"@remix-run/server-runtime\";\nimport { z } from \"zod\";\n\nimport { createDebug } from \"~/shared/debug\";\nimport { fromError } from \"zod-validation-error\";\nimport env from \"~/env/env.server\";\nimport {\n  createAccessToken,\n  readAccessToken,\n  readCodeToken,\n  verifyChallenge,\n} from \"~/services/token.server\";\nimport { isUserAuthorizedForProject } from \"~/services/builder-access.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { isDashboard } from \"~/shared/router-utils\";\n\n/**\n * OAuth 2.0 Token Request\n *\n * https://datatracker.ietf.org/doc/html/rfc7636#section-4.5\n *\n * https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3\n */\nconst TokenRequest = z.object({\n  // Check that the grant_type parameter is present and is one of the supported values\n  grant_type: z.literal(\"authorization_code\"),\n  code: z.string(),\n  redirect_uri: z.string().url(),\n  code_verifier: z.string(),\n});\n\nconst debug = createDebug(import.meta.url);\n\n/**\n * OAuth 2.0 Token Request\n *\n * https://datatracker.ietf.org/doc/html/rfc7636\n *\n * https://datatracker.ietf.org/doc/html/rfc6749\n */\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All POST requests from the builder or canvas app are safeguarded by preventCrossOriginCookie\n\n  debug(\"Token request received\");\n\n  const authorizationHeader = request.headers.get(\"Authorization\");\n\n  if (authorizationHeader === null) {\n    return json(\n      {\n        error: \"invalid_request\",\n        error_description: \"missing client credentials\",\n        error_uri: \"https://tools.ietf.org/html/rfc6749#section-5.2\",\n      },\n      { status: 401 }\n    );\n  }\n\n  const basicAuth = authorizationHeader.split(\" \")[1] ?? \"\";\n\n  const [clientId, clientSecret]: (string | undefined)[] = Buffer.from(\n    basicAuth,\n    \"base64\"\n  )\n    .toString(\"utf-8\")\n    .split(\":\");\n\n  // Validate the client’s credentials (e.g., client_id and client_secret) using HTTP Basic Authentication or form-encoded parameters.\n  if (\n    clientId !== env.AUTH_WS_CLIENT_ID ||\n    clientSecret !== env.AUTH_WS_CLIENT_SECRET\n  ) {\n    debug(\"client_id and client_secret do not match\", clientId, clientSecret);\n    return json(\n      {\n        error: \"invalid_client\",\n        error_description: \"invalid client credentials\",\n        error_uri: \"https://tools.ietf.org/html/rfc6749#section-5.2\",\n      },\n      { status: 401 }\n    );\n  }\n\n  const jsonBody = Object.fromEntries((await request.formData()).entries());\n  debug(\"Token request received\", jsonBody);\n\n  const parsedBody = TokenRequest.safeParse(jsonBody);\n\n  if (false === parsedBody.success) {\n    debug(fromError(parsedBody.error).toString());\n\n    return json(\n      {\n        error: \"invalid_request\",\n        error_description: fromError(parsedBody.error).toString(),\n        error_uri: \"https://tools.ietf.org/html/rfc6749#section-5.2\",\n      },\n      { status: 400 }\n    );\n  }\n\n  const body = parsedBody.data;\n\n  // Ensure the code parameter is present and valid.\n  const codeToken = await readCodeToken(body.code, env.AUTH_WS_CLIENT_SECRET);\n\n  if (codeToken === undefined) {\n    debug(\"Code can not be read\", body.code);\n    return json(\n      {\n        error: \"invalid_grant\",\n        error_description: \"invalid code\",\n        error_uri: \"https://tools.ietf.org/html/rfc6749#section-5.2\",\n      },\n      { status: 400 }\n    );\n  }\n\n  // verify the code_verifier against the stored code_challenge\n  const isChallengeVerified = await verifyChallenge(\n    body.code_verifier,\n    codeToken.codeChallenge\n  );\n  if (false === isChallengeVerified) {\n    debug(\n      \"Code verifier does not match\",\n      body.code_verifier,\n      codeToken.codeChallenge\n    );\n    return json(\n      {\n        error: \"invalid_grant\",\n        error_description: \"invalid code_verifier\",\n        error_uri: \"https://tools.ietf.org/html/rfc6749#section-5.2\",\n      },\n      { status: 400 }\n    );\n  }\n\n  const { projectId, userId } = codeToken;\n\n  const isAuthorized = await isUserAuthorizedForProject(userId, projectId);\n\n  if (false === isAuthorized) {\n    debug(\"User does not have access to the project\", userId, projectId);\n    return json(\n      {\n        error: \"invalid_grant\",\n        error_description: \"user does not have access to the project\",\n        error_uri: \"https://tools.ietf.org/html/rfc6749#section-5.2\",\n      },\n      { status: 400 }\n    );\n  }\n\n  const maxAge = 1000 * 60;\n\n  // Generate a short-lived token, as its sole purpose is to log the user in.\n  const accessToken = await createAccessToken(\n    { userId, projectId },\n    env.AUTH_WS_CLIENT_SECRET,\n    {\n      maxAge,\n    }\n  );\n\n  debug(\"Token created\", accessToken);\n\n  debug(\n    \"readAccessToken\",\n    await readAccessToken(accessToken, env.AUTH_WS_CLIENT_SECRET)\n  );\n\n  return json(\n    {\n      access_token: accessToken,\n      token_type: \"Bearer\",\n      expires_in: Date.now() + maxAge,\n    },\n    { status: 200 }\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.assets.tsx",
    "content": "import type {\n  ActionFunctionArgs,\n  LoaderFunctionArgs,\n} from \"@remix-run/server-runtime\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport { MaxAssets } from \"@webstudio-is/asset-uploader\";\nimport {\n  loadAssetsByProject,\n  createUploadName,\n} from \"@webstudio-is/asset-uploader/index.server\";\nimport { createContext } from \"~/shared/context.server\";\nimport env from \"~/env/env.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\nimport { parseError } from \"~/shared/error/error-parse\";\n\nexport const loader = async ({\n  params,\n  request,\n}: LoaderFunctionArgs): Promise<Array<Asset>> => {\n  if (params.projectId === undefined) {\n    throw new Error(\"Project id undefined\");\n  }\n  const context = await createContext(request);\n  return await loadAssetsByProject(params.projectId, context);\n};\n\nexport const action = async (props: ActionFunctionArgs) => {\n  try {\n    preventCrossOriginCookie(props.request);\n    await checkCsrf(props.request);\n\n    const { request } = props;\n\n    const context = await createContext(request);\n\n    if (request.method === \"POST\") {\n      const formData = await request.formData();\n      const projectId = formData.get(\"projectId\") as string;\n      const type = formData.get(\"type\") as string;\n      const filename = formData.get(\"filename\") as string;\n      if (projectId === null || type === null || filename === null) {\n        throw Error(\"Project id, asset id or filename are missing\");\n      }\n      const name = await createUploadName(\n        {\n          projectId,\n          type,\n          filename,\n          maxAssetsPerProject: MaxAssets.parse(env.MAX_ASSETS_PER_PROJECT),\n        },\n        context\n      );\n      return {\n        name,\n      };\n    }\n  } catch (error) {\n    console.error(error);\n\n    return {\n      errors: parseError(error).message,\n    };\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.assets_.$name.tsx",
    "content": "import { z } from \"zod\";\nimport type { ActionFunctionArgs } from \"@remix-run/server-runtime\";\nimport { uploadFile } from \"@webstudio-is/asset-uploader/index.server\";\nimport {\n  isAllowedMimeCategory,\n  RESIZABLE_IMAGE_MIME_TYPES,\n  ALLOWED_FILE_TYPES,\n} from \"@webstudio-is/sdk\";\nimport type { AssetActionResponse } from \"~/builder/shared/assets\";\nimport { createAssetClient } from \"~/shared/asset-client\";\nimport { createContext } from \"~/shared/context.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\nimport { parseError } from \"~/shared/error/error-parse\";\n\nconst UrlBody = z.object({\n  url: z.string(),\n});\n\nexport const action = async (\n  props: ActionFunctionArgs\n): Promise<AssetActionResponse> => {\n  preventCrossOriginCookie(props.request);\n  await checkCsrf(props.request);\n\n  const { request, params } = props;\n\n  // await new Promise((resolve) => setTimeout(resolve, 20000));\n\n  if (params.name === undefined) {\n    throw new Error(\"Name is undefined\");\n  }\n\n  try {\n    if (request.method === \"POST\" && request.body !== null) {\n      let body = request.body;\n\n      const contentType = request.headers.get(\"Content-Type\");\n\n      // Check if this is a request to download from URL (has url field in JSON body)\n      // vs uploading a JSON file directly (JSON file content in body)\n      if (contentType?.includes(\"application/json\")) {\n        const jsonBody = await request.json();\n        const urlParse = UrlBody.safeParse(jsonBody);\n\n        // Only fetch from URL if the body has a valid url field\n        if (urlParse.success) {\n          const { url } = urlParse.data;\n\n          const imageRequest = await fetch(url, {\n            method: \"GET\",\n            headers: {\n              Accept: RESIZABLE_IMAGE_MIME_TYPES.join(\",\"),\n            },\n          });\n\n          if (false === imageRequest.ok) {\n            const error = await imageRequest.text();\n            const errors = `An error occurred while fetching the image at ${url}: ${error.slice(0, 500)}`;\n            throw new Error(errors);\n          }\n\n          if (imageRequest.body === null) {\n            throw new Error(\n              `An error occurred while fetching the image at ${url}: Image body is null`\n            );\n          }\n\n          body = imageRequest.body;\n        } else {\n          // This is a JSON file being uploaded, use the JSON content as body\n          body = new Blob([JSON.stringify(jsonBody)], {\n            type: \"application/json\",\n          }).stream();\n        }\n      }\n\n      const url = new URL(request.url);\n\n      // Get file extension from filename\n      const fileExtension = params.name.split(\".\").pop()?.toLowerCase();\n\n      // Use the file extension to determine the correct MIME type\n      // This handles cases where browsers send legacy MIME types (e.g., application/font-woff instead of font/woff)\n      const correctMimeType = fileExtension\n        ? ALLOWED_FILE_TYPES[fileExtension as keyof typeof ALLOWED_FILE_TYPES]\n        : contentType?.split(\";\")[0];\n\n      const contentTypeArr = correctMimeType?.split(\"/\") ?? [];\n\n      // Validate MIME type against allowed categories\n      const mimeCategory = contentTypeArr[0];\n      if (\n        mimeCategory &&\n        correctMimeType &&\n        !isAllowedMimeCategory(mimeCategory)\n      ) {\n        throw new Error(`MIME type \"${mimeCategory}/*\" is not allowed`);\n      }\n\n      const format =\n        contentTypeArr[0] === \"video\" ? contentTypeArr[1] : undefined;\n\n      const width = url.searchParams.has(\"width\")\n        ? Number.parseInt(url.searchParams.get(\"width\")!, 10)\n        : undefined;\n      const height = url.searchParams.has(\"height\")\n        ? Number.parseInt(url.searchParams.get(\"height\")!, 10)\n        : undefined;\n\n      const assetInfoFallback =\n        height !== undefined && width !== undefined && format !== undefined\n          ? { width, height, format }\n          : undefined;\n\n      const context = await createContext(request);\n      const asset = await uploadFile(\n        params.name,\n        body,\n        createAssetClient(),\n        context,\n        assetInfoFallback\n      );\n      return {\n        uploadedAssets: [asset],\n      };\n    }\n  } catch (error) {\n    console.error(error);\n\n    return {\n      errors: parseError(error).message,\n    };\n  }\n\n  return {\n    errors: \"Method not allowed\",\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.build.$buildId.tsx",
    "content": "import {\n  json,\n  type LoaderFunctionArgs,\n  type TypedResponse,\n} from \"@remix-run/server-runtime\";\nimport type { Data } from \"@webstudio-is/http-client\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport { loadProductionCanvasData } from \"~/shared/db\";\nimport { createContext } from \"~/shared/context.server\";\nimport { getUserById, type User } from \"~/shared/db/user.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\nimport { isDashboard } from \"~/shared/router-utils\";\nimport { parseError } from \"~/shared/error/error-parse\";\n\nexport const loader = async ({\n  params,\n  request,\n}: LoaderFunctionArgs): Promise<\n  | (Data & { user: { email: User[\"email\"] } | undefined } & {\n      projectDomain: string;\n      projectTitle: string;\n    })\n  | TypedResponse<{ error: string; message: string }>\n> => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  try {\n    const buildId = params.buildId;\n\n    if (buildId === undefined) {\n      throw json(\"Required build id\", { status: 400 });\n    }\n\n    const context = await createContext(request);\n\n    const pagesCanvasData = await loadProductionCanvasData(buildId, context);\n\n    const project = await projectApi.loadById(\n      pagesCanvasData.build.projectId,\n      context\n    );\n\n    const user =\n      project === null || project.userId === null\n        ? undefined\n        : await getUserById(context, project.userId);\n\n    return {\n      ...pagesCanvasData,\n      user: user ? { email: user.email } : undefined,\n      projectDomain: project.domain,\n      projectTitle: project.title,\n    };\n  } catch (error) {\n    // If a Response is thrown, we're rethrowing it for Remix to handle.\n    // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders\n    if (error instanceof Response) {\n      throw error;\n    }\n\n    console.error(error);\n\n    // We have no idea what happened, so we'll return a 500 error.\n    throw json(parseError(error), {\n      status: 500,\n    });\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.buildId.$projectId.tsx",
    "content": "import {\n  json,\n  type LoaderFunctionArgs,\n  type TypedResponse,\n} from \"@remix-run/server-runtime\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { createContext } from \"~/shared/context.server\";\nimport { parseError } from \"~/shared/error/error-parse\";\nimport { isDashboard } from \"~/shared/router-utils\";\n\n// This loader is only accessible from the dashboard origin\n// and is used exclusively for the CLI.\nexport const loader = async ({\n  params,\n  request,\n}: LoaderFunctionArgs): Promise<\n  { buildId: string | null } | TypedResponse<{ error: string; message: string }>\n> => {\n  if (false === isDashboard(request)) {\n    throw new Response(\"Not Found\", {\n      status: 404,\n    });\n  }\n\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n  // CSRF token checks are not necessary for dashboard-only pages.\n  // All requests from the builder or canvas app are safeguarded either by preventCrossOriginCookie for fetch requests\n  // or by allowedDestinations for iframe requests.\n\n  try {\n    const projectId = params.projectId;\n\n    if (projectId === undefined) {\n      throw json(\"Required project id\", { status: 400 });\n    }\n\n    // @todo Create a context without user authentication information.\n    const context = await createContext(request);\n\n    const project = await projectApi.loadById(projectId, context);\n    const buildId = project.latestBuildVirtual?.buildId ?? null;\n\n    return {\n      buildId,\n    };\n  } catch (error) {\n    // If a Response is thrown, we're rethrowing it for Remix to handle.\n    // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders\n    if (error instanceof Response) {\n      throw error;\n    }\n\n    console.error(error);\n\n    throw json(parseError(error), {\n      status: 500,\n    });\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.data.$projectId.ts",
    "content": "import type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport { loadDevBuildByProjectId } from \"@webstudio-is/project-build/index.server\";\nimport { loadAssetsByProject } from \"@webstudio-is/asset-uploader/index.server\";\nimport { createContext } from \"~/shared/context.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\nimport { allowedDestinations } from \"~/services/destinations.server\";\nimport env from \"~/env/env.server\";\n\nexport const loader = async ({ params, request }: LoaderFunctionArgs) => {\n  preventCrossOriginCookie(request);\n  allowedDestinations(request, [\"document\", \"empty\"]);\n  await checkCsrf(request);\n\n  if (params.projectId === undefined) {\n    throw new Error(\"Project id undefined\");\n  }\n  const context = await createContext(request);\n  const project = await projectApi.loadById(params.projectId, context);\n  if (project === null) {\n    throw new Error(`Project \"${params.projectId}\" not found`);\n  }\n  if (project.userId === null) {\n    throw new Error(\"Project must have project userId defined\");\n  }\n  const build = await loadDevBuildByProjectId(context, project.id);\n  const assets = await loadAssetsByProject(project.id, context);\n  return {\n    ...build,\n    assets,\n    project,\n    publisherHost: env.PUBLISHER_HOST,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.patch.ts",
    "content": "import { applyPatches, enableMapSet, enablePatches, type Patch } from \"immer\";\nimport type { ActionFunctionArgs } from \"@remix-run/server-runtime\";\nimport {\n  Breakpoints,\n  Breakpoint,\n  Instances,\n  Instance,\n  Pages,\n  Props,\n  Prop,\n  DataSources,\n  DataSource,\n  StyleSourceSelections,\n  StyleSources,\n  StyleSource,\n  Styles,\n  Resources,\n  Resource,\n} from \"@webstudio-is/sdk\";\nimport {\n  type Build,\n  findCycles,\n  MarketplaceProduct,\n} from \"@webstudio-is/project-build\";\nimport {\n  parsePages,\n  parseStyleSourceSelections,\n  parseStyles,\n  serializePages,\n  serializeStyleSourceSelections,\n  serializeStyles,\n  parseData,\n  serializeData,\n  parseConfig,\n  serializeConfig,\n  loadRawBuildById,\n  parseInstanceData,\n} from \"@webstudio-is/project-build/index.server\";\nimport { patchAssets } from \"@webstudio-is/asset-uploader/index.server\";\nimport type { Project } from \"@webstudio-is/project\";\nimport {\n  AuthorizationError,\n  authorizeProject,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { createContext } from \"~/shared/context.server\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport type { Database } from \"@webstudio-is/postgrest/index.server\";\nimport { publicStaticEnv } from \"~/env/env.static\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\nimport type { Transaction } from \"~/shared/sync-client\";\n\ntype Change = {\n  namespace: string;\n  patches: Array<Patch>;\n};\n\ntype PatchData = {\n  transactions: Transaction<Change[]>[];\n  buildId: Build[\"id\"];\n  projectId: Project[\"id\"];\n  version: number;\n};\n\nexport const action = async ({\n  request,\n}: ActionFunctionArgs): Promise<\n  | { status: \"ok\" }\n  | { status: \"version_mismatched\"; errors: string }\n  | { status: \"authorization_error\"; errors: string }\n  | { status: \"error\"; errors: string }\n> => {\n  try {\n    preventCrossOriginCookie(request);\n\n    enableMapSet();\n    enablePatches();\n\n    await checkCsrf(request);\n\n    const {\n      buildId,\n      projectId,\n      transactions,\n      version: clientVersion,\n    }: PatchData = await request.json();\n\n    const version = new URL(request.url).searchParams.get(\"client-version\");\n\n    if (publicStaticEnv.VERSION !== version) {\n      return {\n        status: \"version_mismatched\",\n        errors: `The client and server versions do not match. Please reload to continue.`,\n      };\n    }\n\n    if (buildId === undefined) {\n      return { status: \"error\", errors: \"Build id required\" };\n    }\n    if (projectId === undefined) {\n      return { status: \"error\", errors: \"Project id required\" };\n    }\n\n    const lastTransactionId = transactions.at(-1)?.id;\n\n    if (lastTransactionId === undefined) {\n      return {\n        status: \"error\",\n        errors: \"Transaction array must not be empty.\",\n      };\n    }\n\n    const context = await createContext(request);\n\n    if (context.authorization.type === \"service\") {\n      throw new AuthorizationError(\"Service calls are not allowed\");\n    }\n\n    if (context.authorization.type === \"anonymous\") {\n      return {\n        status: \"authorization_error\",\n        errors:\n          \"Due to a recent update or a possible logout, you may need to log in again. Please reload the page and sign in to continue.\",\n      };\n    }\n\n    // @todo: Commented until better Content Edit mode checks are implemented\n    const [canEditContent /* canEdit */] = await Promise.all([\n      authorizeProject.hasProjectPermit({ projectId, permit: \"edit\" }, context),\n      /*\n      authorizeProject.hasProjectPermit(\n        { projectId, permit: \"build\" },\n        context\n      ),\n      */\n    ]);\n\n    if (canEditContent === false) {\n      return {\n        status: \"authorization_error\",\n        errors: \"You don't have permission to edit this project.\",\n      };\n    }\n\n    // const isContentEditMode = canEditContent && !canEdit;\n\n    const build = await loadRawBuildById(context, buildId);\n\n    const serverVersion = build.version;\n    if (clientVersion !== serverVersion) {\n      // Check if a retry attempt is made with a previously successful transaction.\n      // This can occur if the connection was lost or an error occurred post-transaction completion,\n      // leaving the client in an erroneous state and prompting a retry.\n      if (lastTransactionId === build.lastTransactionId) {\n        return { status: \"ok\" };\n      }\n\n      return {\n        status: \"version_mismatched\",\n        errors: `You are currently in single-player mode. The project has been edited in a different tab, browser, or by another user. Please reload the page to get the latest version.`,\n      };\n    }\n\n    const buildData: {\n      pages?: Pages;\n      breakpoints?: Breakpoints;\n      instances?: Instances;\n      props?: Props;\n      dataSources?: DataSources;\n      resources?: Resources;\n      styleSources?: StyleSources;\n      styleSourceSelections?: StyleSourceSelections;\n      styles?: Styles;\n      marketplaceProduct?: MarketplaceProduct;\n    } = {};\n\n    let previewImageAssetId: string | null | undefined = undefined;\n\n    // Used to optimize by validating only changed styles, as they accounted for 99% of validation time\n    const patchedStyleDeclKeysSet = new Set<string>();\n\n    for await (const transaction of transactions) {\n      for await (const change of transaction.payload) {\n        const { namespace, patches } = change;\n        if (patches.length === 0) {\n          continue;\n        }\n\n        if (namespace === \"pages\") {\n          // lazily parse build data before patching\n          const pages = buildData.pages ?? parsePages(build.pages);\n          const currentSocialImageAssetId =\n            pages.homePage.meta.socialImageAssetId;\n          buildData.pages = applyPatches(pages, patches);\n          const newSocialImageAssetId =\n            buildData.pages.homePage.meta.socialImageAssetId;\n          if (currentSocialImageAssetId !== newSocialImageAssetId) {\n            previewImageAssetId = newSocialImageAssetId || null;\n          }\n          continue;\n        }\n\n        if (namespace === \"instances\") {\n          const instances =\n            buildData.instances ?? parseInstanceData(build.instances);\n\n          buildData.instances = applyPatches(instances, patches);\n\n          // Detect cycles\n          const cycles = findCycles(buildData.instances.values());\n          if (cycles.length > 0) {\n            console.error(\n              \"Cycles detected in the instance tree after patching\",\n              cycles\n            );\n\n            return {\n              status: \"error\",\n              errors: \"Cycles detected in the instance tree\",\n            };\n          }\n\n          continue;\n        }\n\n        if (namespace === \"props\") {\n          const props = buildData.props ?? parseData<Prop>(build.props);\n          buildData.props = applyPatches(props, patches);\n          continue;\n        }\n\n        if (namespace === \"assets\") {\n          // assets implements own patching\n          // @todo parallelize the updates\n          // currently not possible because we fetch the entire tree\n          // and parallelized updates will cause unpredictable side effects\n          await patchAssets({ projectId }, patches, context);\n          continue;\n        }\n\n        // This is super simple and naive implementation of permissions checks for Content Edit mode\n        // @todo: Implement proper permissions checks, one of idea is to allow any changes to the new instances\n        /*\n        if (isContentEditMode) {\n          return {\n            status: \"error\",\n            errors: `You don't have permission to patch namespace ${namespace}`,\n          };\n        }*/\n\n        if (namespace === \"styleSourceSelections\") {\n          const styleSourceSelections =\n            buildData.styleSourceSelections ??\n            parseStyleSourceSelections(build.styleSourceSelections);\n          buildData.styleSourceSelections = applyPatches(\n            styleSourceSelections,\n            patches\n          );\n          continue;\n        }\n\n        if (namespace === \"styleSources\") {\n          const styleSources =\n            buildData.styleSources ??\n            parseData<StyleSource>(build.styleSources);\n          buildData.styleSources = applyPatches(styleSources, patches);\n          continue;\n        }\n\n        if (namespace === \"styles\") {\n          // It's somehow implementation detail leak, as we use the fact that styles patches has ids in path\n          for (const patch of patches) {\n            patchedStyleDeclKeysSet.add(`${patch.path[0]}`);\n          }\n\n          const styles = buildData.styles ?? parseStyles(build.styles);\n          buildData.styles = applyPatches(styles, patches);\n          continue;\n        }\n\n        if (namespace === \"dataSources\") {\n          const dataSources =\n            buildData.dataSources ?? parseData<DataSource>(build.dataSources);\n          buildData.dataSources = applyPatches(dataSources, patches);\n          continue;\n        }\n\n        if (namespace === \"resources\") {\n          const resources =\n            buildData.resources ?? parseData<Resource>(build.resources);\n          buildData.resources = applyPatches(resources, patches);\n          continue;\n        }\n\n        if (namespace === \"breakpoints\") {\n          const breakpoints =\n            buildData.breakpoints ?? parseData<Breakpoint>(build.breakpoints);\n          buildData.breakpoints = applyPatches(breakpoints, patches);\n          continue;\n        }\n\n        if (namespace === \"marketplaceProduct\") {\n          const marketplaceProduct =\n            buildData.marketplaceProduct ??\n            parseConfig<MarketplaceProduct>(build.marketplaceProduct);\n\n          buildData.marketplaceProduct = applyPatches(\n            marketplaceProduct,\n            patches\n          );\n\n          continue;\n        }\n\n        return { status: \"error\", errors: `Unknown namespace \"${namespace}\"` };\n      }\n    }\n\n    // save build data when all patches applied\n    const dbBuildData: Database[\"public\"][\"Tables\"][\"Build\"][\"Update\"] = {\n      version: clientVersion + 1,\n      lastTransactionId,\n      updatedAt: new Date().toISOString(),\n    };\n\n    if (buildData.pages) {\n      // parse with zod before serialization to avoid saving invalid data\n      dbBuildData.pages = serializePages(Pages.parse(buildData.pages));\n    }\n\n    if (buildData.breakpoints) {\n      dbBuildData.breakpoints = serializeData<Breakpoint>(\n        Breakpoints.parse(buildData.breakpoints)\n      );\n    }\n\n    if (buildData.instances) {\n      dbBuildData.instances = serializeData<Instance>(\n        Instances.parse(buildData.instances)\n      );\n    }\n\n    if (buildData.props) {\n      dbBuildData.props = serializeData<Prop>(Props.parse(buildData.props));\n    }\n\n    if (buildData.dataSources) {\n      dbBuildData.dataSources = serializeData<DataSource>(\n        DataSources.parse(buildData.dataSources)\n      );\n    }\n\n    if (buildData.resources) {\n      dbBuildData.resources = serializeData<Resource>(\n        Resources.parse(buildData.resources)\n      );\n    }\n\n    if (buildData.styleSources) {\n      dbBuildData.styleSources = serializeData<StyleSource>(\n        StyleSources.parse(buildData.styleSources)\n      );\n    }\n    if (buildData.styleSourceSelections) {\n      dbBuildData.styleSourceSelections = serializeStyleSourceSelections(\n        StyleSourceSelections.parse(buildData.styleSourceSelections)\n      );\n    }\n\n    if (buildData.styles) {\n      // Optimize by validating only changed styles, as they accounted for 99% of validation time\n      const stylesToValidate: Styles = new Map();\n      for (const styleId of patchedStyleDeclKeysSet) {\n        const style = buildData.styles.get(styleId);\n        // In case of deletion style could be undefined\n        if (style === undefined) {\n          continue;\n        }\n\n        stylesToValidate.set(styleId, style);\n      }\n\n      Styles.parse(stylesToValidate);\n\n      dbBuildData.styles = serializeStyles(buildData.styles);\n    }\n\n    if (buildData.marketplaceProduct) {\n      dbBuildData.marketplaceProduct = serializeConfig<MarketplaceProduct>(\n        MarketplaceProduct.parse(buildData.marketplaceProduct)\n      );\n    }\n\n    const update = await context.postgrest.client\n      .from(\"Build\")\n      .update(dbBuildData, { count: \"exact\" })\n      .match({\n        id: buildId,\n        projectId,\n        version: clientVersion,\n      });\n\n    if (update.error) {\n      console.error(update.error);\n      throw update.error;\n    }\n    if (update.count == null) {\n      console.error(\"Update count is null\");\n      throw new Error(\"Update count is null\");\n    }\n\n    // ensure only build with client version is updated\n    // to avoid race conditions\n    if (update.count === 0) {\n      // We don't validate if lastTransactionId matches the user's transaction ID here, as we've already done so earlier.\n      // Given the sequential nature of messages from a single client, this situation is deemed improbable.\n      return {\n        status: \"version_mismatched\",\n        errors: `You are currently in single-player mode. The project has been edited in a different tab, browser, or by another user. Please reload the page to get the latest version.`,\n      };\n    }\n\n    if (previewImageAssetId !== undefined) {\n      await projectApi.updatePreviewImage(\n        { assetId: previewImageAssetId, projectId },\n        context\n      );\n    }\n\n    return { status: \"ok\" };\n  } catch (error) {\n    if (error instanceof AuthorizationError) {\n      return {\n        status: \"authorization_error\",\n        errors: error.message,\n      };\n    }\n\n    if (error instanceof Response && error.ok === false) {\n      return {\n        status: \"authorization_error\",\n        errors: error.statusText,\n      };\n    }\n\n    if (error instanceof Response) {\n      return {\n        status: \"error\",\n        errors: await error.text(),\n      };\n    }\n\n    console.error(error);\n    return {\n      status: \"error\",\n      errors: error instanceof Error ? error.message : JSON.stringify(error),\n    };\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/routes/rest.resources-loader.ts",
    "content": "import { z } from \"zod\";\nimport { type ActionFunctionArgs, data } from \"@remix-run/server-runtime\";\nimport { ResourceRequest } from \"@webstudio-is/sdk\";\nimport { isLocalResource, loadResource } from \"@webstudio-is/sdk/runtime\";\nimport { loader as siteMapLoader } from \"../shared/$resources/sitemap.xml.server\";\nimport { loader as currentDateLoader } from \"../shared/$resources/current-date.server\";\nimport { loader as assetsLoader } from \"../shared/$resources/assets.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\nimport { getResourceKey } from \"~/shared/resources\";\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  preventCrossOriginCookie(request);\n  await checkCsrf(request);\n\n  // Hope Remix will have customFetch by default, see https://kit.svelte.dev/docs/load#making-fetch-requests\n  const customFetch: typeof fetch = (input, init) => {\n    if (typeof input !== \"string\") {\n      return fetch(input, init);\n    }\n\n    if (isLocalResource(input, \"sitemap.xml\")) {\n      return siteMapLoader({ request });\n    }\n\n    if (isLocalResource(input, \"current-date\")) {\n      return currentDateLoader({ request });\n    }\n\n    if (isLocalResource(input, \"assets\")) {\n      return assetsLoader({ request });\n    }\n\n    return fetch(input, init);\n  };\n\n  const requestJson = await request.json();\n  const requestList = z.array(z.unknown()).safeParse(requestJson);\n\n  if (requestList.success === false) {\n    console.error(\"data:\", requestJson);\n    throw data(requestList.error, {\n      status: 400,\n    });\n  }\n\n  const output = await Promise.all(\n    requestList.data.map(async (item) => {\n      const resource = ResourceRequest.safeParse(item);\n      if (resource.success === false) {\n        return [\n          getResourceKey(item as ResourceRequest),\n          {\n            ok: false,\n            data: resource.error.format(),\n            status: 403,\n            statusText: \"Resource validation error\",\n          },\n        ];\n      }\n      return [\n        getResourceKey(resource.data),\n        await loadResource(customFetch, resource.data),\n      ];\n    })\n  );\n\n  return output;\n};\n"
  },
  {
    "path": "apps/builder/app/routes/trpc.$.ts",
    "content": "import { fetchRequestHandler } from \"@trpc/server/adapters/fetch\";\nimport type { ActionFunctionArgs } from \"@remix-run/server-runtime\";\nimport { createContext } from \"~/shared/context.server\";\nimport { appRouter } from \"~/services/trcp-router.server\";\nimport { preventCrossOriginCookie } from \"~/services/no-cross-origin-cookie\";\nimport { checkCsrf } from \"~/services/csrf-session.server\";\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n  preventCrossOriginCookie(request);\n  await checkCsrf(request);\n\n  // https://trpc.io/docs/server/adapters/fetch\n  const response = await fetchRequestHandler({\n    req: request,\n    router: appRouter,\n    endpoint: \"/trpc\",\n    batching: { enabled: true },\n    responseMeta(opts) {\n      // Disable trpc cache\n      if (process.env.NODE_ENV !== \"production\") {\n        return {};\n      }\n\n      // tRPC batches multiple requests into a single network call.\n      // The `paths` array lists all request paths included in the batch.\n      const { paths, errors, type, ctx } = opts;\n\n      if (paths === undefined) {\n        return {};\n      }\n\n      if (type !== \"query\") {\n        // Only queries can be cached\n        return {};\n      }\n\n      if (errors.length > 0) {\n        // Errors should not be cached\n        return {};\n      }\n\n      // To enable efficient batching of tRPC requests,\n      // adopt the least max age among all paths for caching, or disable caching entirely if no max-age is set.\n      let minMaxAge = Number.MAX_SAFE_INTEGER;\n      for (const path of paths) {\n        const maxAge = ctx?.trpcCache.getMaxAge(path);\n\n        if (maxAge === undefined) {\n          return {};\n        }\n\n        minMaxAge = Math.min(minMaxAge, maxAge);\n      }\n\n      // Cap the max age at 1 hour\n      minMaxAge = Math.min(minMaxAge, 60 * 60);\n\n      return {\n        headers: {\n          \"Cache-Control\": `public, max-age=${minMaxAge}, s-maxage=${minMaxAge}`,\n        },\n      };\n    },\n    async createContext(opts) {\n      return await createContext(opts.req);\n    },\n  });\n\n  return response;\n};\n\nexport const loader = action;\n"
  },
  {
    "path": "apps/builder/app/services/auth-strategy/ws.server.ts",
    "content": "import type { StrategyVerifyCallback } from \"remix-auth\";\nimport {\n  OAuth2Strategy,\n  type OAuth2Profile,\n  type OAuth2StrategyOptions,\n  type OAuth2StrategyVerifyParams,\n} from \"remix-auth-oauth2\";\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\ntype DynamicProps = \"authorizationEndpoint\" | \"tokenEndpoint\" | \"redirectURI\";\n\nexport type OAuth2StrategyOptionsOverrides = Partial<OAuth2StrategyOptions> &\n  Pick<OAuth2StrategyOptions, DynamicProps>;\n\nconst asyncLocalStorage = new AsyncLocalStorage<\n  Partial<OAuth2StrategyOptions>\n>();\n\n/**\n * The main issue with OAuth2Strategy is that it forces us to define authorizationEndpoint, tokenEndpoint, and redirectURI\n * at the time of constructing the instance.\n * However, in our case, these values are dynamic because we use multiple domains, such as development, staging, and production.\n * The solution is to wrap the options object with a proxy, retrieving the necessary options from localAsyncStorage and proxying\n * all requests through asyncLocalStorage.run\n */\nexport class WsStrategy<\n  User,\n  Profile extends OAuth2Profile = { provider: string },\n  ExtraParams extends Record<string, unknown> = Record<string, never>,\n> extends OAuth2Strategy<User, Profile, ExtraParams> {\n  constructor(\n    options: OAuth2StrategyOptions,\n    verify: StrategyVerifyCallback<\n      User,\n      OAuth2StrategyVerifyParams<Profile, ExtraParams>\n    >,\n    overrideOptions: (request: Request) => OAuth2StrategyOptionsOverrides\n  ) {\n    super(options, verify);\n\n    this.options = new Proxy<OAuth2StrategyOptions>(this.options, {\n      get: (target, property, receiver) => {\n        const store = asyncLocalStorage.getStore();\n        return (\n          store?.[property as keyof OAuth2StrategyOptions] ??\n          Reflect.get(target, property, receiver)\n        );\n      },\n    });\n\n    return new Proxy(this, {\n      get: function (target, property, receiver) {\n        const targetProp = target[property as keyof typeof target];\n        if (\n          typeof targetProp === \"function\" &&\n          // do not wrap if already wrapped\n          asyncLocalStorage.getStore() === undefined\n        ) {\n          return (...args: unknown[]) => {\n            const request = args.find((arg) => arg instanceof Request);\n\n            return asyncLocalStorage.run(\n              request ? overrideOptions(request) : {},\n              () => {\n                return Reflect.apply(targetProp, receiver, args);\n              }\n            );\n          };\n        }\n\n        return Reflect.get(target, property, receiver);\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "apps/builder/app/services/auth.server.ts",
    "content": "import { Authenticator } from \"remix-auth\";\nimport { FormStrategy } from \"remix-auth-form\";\nimport { GitHubStrategy, type GitHubProfile } from \"remix-auth-github\";\nimport { GoogleStrategy, type GoogleProfile } from \"remix-auth-google\";\nimport * as db from \"~/shared/db\";\nimport { sessionStorage } from \"~/services/session.server\";\nimport { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { authCallbackPath, isBuilder } from \"~/shared/router-utils\";\nimport { getUserById } from \"~/shared/db/user.server\";\nimport env from \"~/env/env.server\";\nimport { builderAuthenticator } from \"./builder-auth.server\";\nimport { staticEnv } from \"~/env/env.static.server\";\nimport type { SessionData } from \"./auth.server.utils\";\nimport { createContext } from \"~/shared/context.server\";\n\nconst transformRefToAlias = (input: string) => {\n  const rawAlias = input.endsWith(\".staging\") ? input.slice(0, -8) : input;\n\n  return rawAlias\n    .replace(/[^a-zA-Z0-9_-]/g, \"\") // Remove all characters except a-z, A-Z, 0-9, _ and -\n    .toLowerCase() // Convert to lowercase\n    .replace(/_/g, \"-\") // Replace underscores with hyphens\n    .replace(/-+/g, \"-\"); // Replace multiple hyphens with a single hyphen\n};\n\nexport const callbackOrigin =\n  env.DEPLOYMENT_ENVIRONMENT === \"production\"\n    ? env.DEPLOYMENT_URL\n    : env.DEPLOYMENT_ENVIRONMENT === \"staging\" ||\n        env.DEPLOYMENT_ENVIRONMENT === \"development\"\n      ? `https://${transformRefToAlias(staticEnv.GITHUB_REF_NAME ?? \"main\")}.${env.DEPLOYMENT_ENVIRONMENT}.webstudio.is`\n      : `https://wstd.dev:${env.PORT || 5173}`;\n\nconst strategyCallback = async ({\n  profile,\n  request,\n}: {\n  profile: GitHubProfile | GoogleProfile;\n  request: Request;\n}) => {\n  const context = await createContext(request);\n\n  try {\n    const user = await db.user.createOrLoginWithOAuth(context, profile);\n    return { userId: user.id, createdAt: Date.now() };\n  } catch (error) {\n    if (error instanceof Error) {\n      console.error({\n        error,\n        extras: {\n          loginMethod: AUTH_PROVIDERS.LOGIN_DEV,\n        },\n      });\n    }\n    throw error;\n  }\n};\n\n// Create an instance of the authenticator, pass a generic with what\n// strategies will return and will store in the session\nexport const authenticator = new Authenticator<SessionData>(sessionStorage, {\n  throwOnError: true,\n});\n\nif (env.GH_CLIENT_ID && env.GH_CLIENT_SECRET) {\n  const github = new GitHubStrategy(\n    {\n      clientID: env.GH_CLIENT_ID,\n      clientSecret: env.GH_CLIENT_SECRET,\n      callbackURL: `${callbackOrigin}${authCallbackPath({ provider: \"github\" })}`,\n    },\n    strategyCallback\n  );\n  authenticator.use(github, \"github\");\n}\n\nif (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {\n  const google = new GoogleStrategy(\n    {\n      clientID: env.GOOGLE_CLIENT_ID,\n      clientSecret: env.GOOGLE_CLIENT_SECRET,\n      callbackURL: `${callbackOrigin}${authCallbackPath({ provider: \"google\" })}`,\n    },\n    strategyCallback\n  );\n  authenticator.use(google, \"google\");\n}\n\nif (env.DEV_LOGIN === \"true\") {\n  authenticator.use(\n    new FormStrategy(async ({ form, request }) => {\n      const secretValue = form.get(\"secret\");\n\n      if (secretValue == null) {\n        throw new Error(\"Secret is required\");\n      }\n\n      const [secret, email = \"hello@webstudio.is\"] = secretValue\n        .toString()\n        .split(\":\");\n\n      if (secret === env.AUTH_SECRET) {\n        try {\n          const context = await createContext(request);\n\n          const user = await db.user.createOrLoginWithDev(context, email);\n          return {\n            userId: user.id,\n            createdAt: Date.now(),\n          };\n        } catch (error) {\n          if (error instanceof Error) {\n            console.error({\n              error,\n              extras: {\n                loginMethod: AUTH_PROVIDERS.LOGIN_DEV,\n              },\n            });\n          }\n          throw error;\n        }\n      }\n\n      throw new Error(\"Secret is incorrect\");\n    }),\n    \"dev\"\n  );\n}\n\nexport const findAuthenticatedUser = async (request: Request) => {\n  const user = isBuilder(request)\n    ? await builderAuthenticator.isAuthenticated(request)\n    : await authenticator.isAuthenticated(request);\n\n  if (user == null) {\n    return null;\n  }\n  const context = await createContext(request);\n\n  try {\n    return await getUserById(context, user.userId);\n  } catch (error) {\n    return null;\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/services/auth.server.utils.ts",
    "content": "export type SessionData = {\n  userId: string;\n  createdAt: number;\n};\n\nexport const getSessionCookieNameVersion = () => {\n  // IMPORTANT: If you see an error here, you need to increase the version number.\n  // Explanation:\n  // Changing the SessionData type will cause all existing user sessions to not work as expected.\n  // There is no logic to validate or clean up sessions, so we avoid session migration issues by changing the session cookie name.\n  // This ensures that old sessions are invalidated and new sessions are created with the updated structure.\n  const obj: SessionData = { userId: \"\", createdAt: 0 };\n  obj.userId = \"\";\n\n  // IMPORTANT: Change version in the SaaS platform as well!\n  // IMPORTANT: Changing the version will cause all users to be logged out.\n  return \"3\";\n};\n"
  },
  {
    "path": "apps/builder/app/services/bloom-filter.server.test.ts",
    "content": "import { test, expect, describe, beforeEach } from \"vitest\";\nimport { BloomFilter } from \"./bloom-filter.server\";\n\ndescribe(\"BloomFilter add/has\", () => {\n  let bloomFilter: BloomFilter;\n\n  beforeEach(() => {\n    bloomFilter = new BloomFilter(100, 0.01);\n  });\n\n  test(\"should initialize correctly\", () => {\n    expect(bloomFilter.numHashes).toBeGreaterThan(0);\n    expect(bloomFilter.bitArray.length).toBeGreaterThan(0);\n  });\n\n  test(\"should add an item and check its existence\", () => {\n    const item = \"test-item\";\n    bloomFilter.add(item);\n    const hasItem = bloomFilter.has(item);\n    expect(hasItem).toBe(true);\n  });\n\n  test(\"should return false for non-existent items\", () => {\n    const item = \"non-existent-item\";\n    const hasItem = bloomFilter.has(item);\n    expect(hasItem).toBe(false);\n  });\n\n  test(\"should handle adding the same item multiple times\", () => {\n    const item = \"duplicate-item\";\n    bloomFilter.add(item);\n    bloomFilter.add(item);\n    const hasItem = bloomFilter.has(item);\n    expect(hasItem).toBe(true);\n  });\n\n  test(\"should handle multiple items\", () => {\n    const items = [\"item1\", \"item2\", \"item3\"];\n    for (const item of items) {\n      bloomFilter.add(item);\n    }\n\n    for (const item of items) {\n      const hasItem = bloomFilter.has(item);\n      expect(hasItem).toBe(true);\n    }\n\n    const notExistedItems = [\"na-item1\", \"na-item2\", \"na-item3\"];\n    for (const item of notExistedItems) {\n      const hasItem = bloomFilter.has(item);\n      expect(hasItem).toBe(false);\n    }\n  });\n});\n\ndescribe(\"BloomFilter serialize/deserialize\", () => {\n  let bloomFilter: BloomFilter;\n\n  beforeEach(() => {\n    bloomFilter = new BloomFilter(100, 0.01);\n  });\n\n  test(\"should serialize and deserialize correctly\", () => {\n    const items = [\"item1\", \"item2\", \"item3\", \"item4\", \"item5\"];\n\n    for (const item of items) {\n      bloomFilter.add(item);\n    }\n\n    const serialized = bloomFilter.serialize();\n    const deserialized = BloomFilter.deserialize(serialized);\n\n    expect(deserialized.numHashes).toBe(bloomFilter.numHashes);\n    expect(deserialized.bitArray).toEqual(bloomFilter.bitArray);\n  });\n});\n\ndescribe(\"BloomFilter size\", () => {\n  test(\"Visual test bloom filter size\", () => {\n    const bloomFilter = new BloomFilter(100, 0.02);\n    for (let i = 0; i < 20; i++) {\n      bloomFilter.add(i.toString());\n    }\n\n    const serialized = bloomFilter.serialize();\n    expect(serialized).toMatchInlineSnapshot(\n      `\"AAABKACCAIQICCAAFFIAEUAEKANBBoAJEAGAAAGAgIgAAIGCAIwCAABBIAgEgBBAAIAAUEAAQRAAiCAAEgAAEggAEkMgEAECAAjAAABighAAgEoAEEDEJGAgQAABATgIAEAQCAAQBg\"`\n    );\n  });\n\n  test(\"Visual test bloom filter size\", () => {\n    const bloomFilter = new BloomFilter(50, 0.02);\n    for (let i = 0; i < 20; i++) {\n      bloomFilter.add(i.toString());\n    }\n\n    const serialized = bloomFilter.serialize();\n    expect(serialized).toMatchInlineSnapshot(\n      `\"UEABaRCCiKQIGiAAFloAE0MkOANDBojJEAHighGAgMoAEMHGJOwiQABBITgMgFBQCIAQBg\"`\n    );\n  });\n});\n\ndescribe(\"real life test\", () => {\n  test.each([50, 100])(\"should work with real life data\", (maxItems) => {\n    const falsePositiveProbability = 0.02;\n\n    const bloomFilter = new BloomFilter(maxItems, falsePositiveProbability);\n\n    // Fill with maxItems with 2/3\n    const uuids = Array.from(Array(Math.ceil((maxItems * 2) / 3)), () =>\n      crypto.randomUUID()\n    );\n    for (const uuid of uuids) {\n      bloomFilter.add(uuid);\n    }\n\n    for (const uuid of uuids) {\n      expect(bloomFilter.has(uuid)).toBe(true);\n    }\n\n    let falsePositives = 0;\n\n    const notInSetCount = 1000;\n\n    const notInSetUuid = Array.from(Array(notInSetCount), () =>\n      crypto.randomUUID()\n    );\n\n    for (const uuid of notInSetUuid) {\n      const isInSet = bloomFilter.has(uuid);\n      falsePositives += isInSet ? 1 : 0;\n    }\n\n    /*\n    // Uncomment to play with parameters\n    console.info(\n      \"False positives\",\n      falsePositives,\n      \"of\",\n      notInSetUuid.length,\n      bloomFilter.serialize().length\n    );\n    */\n\n    expect(falsePositives).toBeLessThan(\n      notInSetCount * falsePositiveProbability\n    );\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/services/bloom-filter.server.ts",
    "content": "const fnv1a = (input: string): number => {\n  const prime = 0x01000193;\n  let hash = 0x811c9dc5;\n\n  for (let i = 0; i < input.length; i++) {\n    hash ^= input.charCodeAt(i);\n    hash = Math.imul(hash, prime);\n  }\n\n  return hash >>> 0; // Convert to 32-bit unsigned integer\n};\n\nconst getBit = (arr: Uint8Array, i: number) => (arr[i >> 3] >> (i & 7)) & 1;\n\nconst setBit = (arr: Uint8Array, i: number) => {\n  arr[i >> 3] = arr[i >> 3] | (1 << (i & 7));\n};\n\nexport class BloomFilter {\n  numHashes: number;\n  bitArray: Uint8Array;\n\n  constructor(maxItems: number, falsePositiveProbability: number) {\n    if (maxItems === 0) {\n      this.numHashes = 0;\n      this.bitArray = new Uint8Array(0);\n      return;\n    }\n\n    const bitSize = Math.ceil(\n      (maxItems * Math.log(falsePositiveProbability)) /\n        Math.log(1 / Math.pow(2, Math.log(2)))\n    );\n\n    this.numHashes = Math.round((bitSize / maxItems) * Math.log(2));\n\n    const size = Math.ceil(bitSize / 8);\n\n    this.bitArray = new Uint8Array(size);\n  }\n\n  add(item: string) {\n    const hashes = this.#getHashes(item);\n    const size = this.bitArray.length * 8;\n\n    for (const hash of hashes) {\n      setBit(this.bitArray, hash % size);\n    }\n  }\n\n  has(item: string) {\n    const hashes = this.#getHashes(item);\n    const size = this.bitArray.length * 8;\n\n    return hashes.every((hash) => getBit(this.bitArray, hash % size) === 1);\n  }\n\n  #getHashes(item: string): number[] {\n    const hashes: number[] = [];\n    let value = item;\n    for (let i = 0; i < this.numHashes; i++) {\n      const hashValue = fnv1a(value);\n      value = hashValue.toString();\n\n      hashes.push(hashValue);\n    }\n\n    return hashes;\n  }\n\n  serialize(): string {\n    return Buffer.concat([\n      Buffer.from(this.bitArray),\n      Buffer.from([this.numHashes]),\n    ]).toString(\"base64url\");\n  }\n\n  static deserialize(serialized: string): BloomFilter {\n    const buffer = Buffer.from(serialized, \"base64url\");\n    const numHashes = buffer[buffer.length - 1];\n    const bitArray = buffer.subarray(0, buffer.length - 1);\n\n    const filter = new BloomFilter(0, 0);\n    filter.numHashes = numHashes;\n    filter.bitArray = new Uint8Array(bitArray);\n\n    return filter;\n  }\n}\n"
  },
  {
    "path": "apps/builder/app/services/builder-access.server.ts",
    "content": "import { authorizeProject } from \"@webstudio-is/trpc-interface/index.server\";\nimport { createPostgrestContext } from \"~/shared/context.server\";\n\n/**\n * Check if a user is authorized to access a project during the Builder authentication process.\n */\nexport const isUserAuthorizedForProject = async (\n  userId: string,\n  projectId: string\n) => {\n  const postgrestContext = createPostgrestContext();\n\n  // Only the project owner can access the Builder URL with authentication credentials (session).\n  const isProjectOwner = await authorizeProject.checkProjectPermit(\n    projectId,\n    \"own\",\n    { type: \"user\", userId },\n    postgrestContext.client\n  );\n\n  return isProjectOwner;\n};\n"
  },
  {
    "path": "apps/builder/app/services/builder-auth.server.ts",
    "content": "import { Authenticator, AuthorizationError } from \"remix-auth\";\nimport { builderSessionStorage } from \"./builder-session.server\";\nimport env from \"~/env/env.server\";\n\nimport {\n  type OAuth2StrategyOptionsOverrides,\n  WsStrategy,\n} from \"./auth-strategy/ws.server\";\nimport {\n  getAuthorizationServerOrigin,\n  getRequestOrigin,\n} from \"~/shared/router-utils/origins\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport { createDebug } from \"~/shared/debug\";\nimport { readAccessToken } from \"./token.server\";\nimport { isUserAuthorizedForProject } from \"./builder-access.server\";\nimport type { SessionData } from \"./auth.server.utils\";\n\nconst debug = createDebug(import.meta.url);\n\nexport const builderAuthenticator = new Authenticator<SessionData>(\n  builderSessionStorage,\n  {\n    throwOnError: true,\n  }\n);\n\nbuilderAuthenticator.use(\n  new WsStrategy<SessionData>(\n    {\n      clientId: env.AUTH_WS_CLIENT_ID,\n      clientSecret: env.AUTH_WS_CLIENT_SECRET,\n      authorizationEndpoint: \"https://OVERRIDDEN_ENDPOINT/oauth2/authorize\",\n      tokenEndpoint: \"https://OVERRIDDEN_ENDPOINT/oauth2/token\",\n      redirectURI: \"https://OVERRIDDEN_ENDPOINT/auth/callback\",\n      codeChallengeMethod: \"S256\",\n      // use http_basic_auth to bypass no-cross-origin-cookie.ts/preventCrossOriginCookie\n      authenticateWith: \"http_basic_auth\",\n    },\n    async ({ tokens, request }) => {\n      const accessToken = await readAccessToken(\n        tokens.access_token,\n        env.AUTH_WS_CLIENT_SECRET\n      );\n\n      if (accessToken === undefined) {\n        debug(\"Invalid or expired access token\", tokens.access_token);\n\n        throw new AuthorizationError(\"Invalid or expired access token\");\n      }\n\n      const { projectId } = parseBuilderUrl(request.url);\n\n      if (accessToken.projectId !== projectId) {\n        throw new AuthorizationError(\n          \"Token projectId and request projectId do not match\"\n        );\n      }\n\n      const isAuthorized = await isUserAuthorizedForProject(\n        accessToken.userId,\n        projectId\n      );\n\n      // We don't need this check because the token is already verified, anyway let's keep it for now\n      if (false === isAuthorized) {\n        throw new AuthorizationError(\n          \"User does not have access to this project\"\n        );\n      }\n\n      debug(\"User authenticated\", accessToken.userId);\n\n      return await { userId: accessToken.userId, createdAt: Date.now() };\n    },\n    (request: Request): OAuth2StrategyOptionsOverrides => {\n      const origin = getRequestOrigin(request.url);\n      const authOrigin = getAuthorizationServerOrigin(request.url);\n      const { projectId } = parseBuilderUrl(request.url);\n\n      if (origin === authOrigin) {\n        throw new Error(\"Origin and authOrigin cannot be same\");\n      }\n\n      return {\n        authorizationEndpoint: `${authOrigin}/oauth/ws/authorize`,\n        tokenEndpoint: `${authOrigin}/oauth/ws/token`,\n        redirectURI: `${origin}/auth/ws/callback`,\n        scopes: [`project:${projectId}`],\n      };\n    }\n  ),\n  \"ws\"\n);\n"
  },
  {
    "path": "apps/builder/app/services/builder-session.server.ts",
    "content": "import { createCookieSessionStorage } from \"@remix-run/node\";\nimport env from \"~/env/env.server\";\nimport { getSessionCookieNameVersion } from \"./auth.server.utils\";\n\nexport const builderSessionStorage = createCookieSessionStorage({\n  cookie: {\n    // Using the __Host- prefix to prevent a malicious user from setting another person's session cookie\n    // on all subdomains of apps.webstudio.is, e.g., setting Domain=.apps.webstudio.is.\n    // For more information, see: https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Cookies#name\n    name: `__Host-_session_builder_session_${getSessionCookieNameVersion()}`,\n    sameSite: \"lax\",\n    path: \"/\",\n    httpOnly: true,\n    secrets: env.AUTH_SECRET ? [env.AUTH_SECRET] : undefined,\n    secure: true,\n  },\n});\n"
  },
  {
    "path": "apps/builder/app/services/cookie.server.ts",
    "content": "import { createCookie } from \"@remix-run/node\";\nimport { compareUrls } from \"~/shared/router-utils\";\n\n// https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Cookies#name\nexport const returnToCookie = createCookie(\"__Host-_returnTo\", {\n  path: \"/\",\n  httpOnly: true,\n  sameSite: \"lax\",\n  // Set the expiration to 5 minutes as it is unnecessary to retain the cookie for a longer duration.\n  maxAge: 60 * 5,\n  secure: true,\n});\n\nexport const returnToPath = async (\n  request: Request\n): Promise<string | null> => {\n  const returnTo = await returnToCookie.parse(request.headers.get(\"Cookie\"));\n\n  if (returnTo === null || typeof returnTo === \"string\") {\n    return returnTo;\n  }\n  return null;\n};\n\nexport const isRedirectResponse = (response: Response) => {\n  return (\n    response.status >= 300 &&\n    response.status < 400 &&\n    response.headers.get(\"Location\") !== null\n  );\n};\n\nexport const clearReturnToCookie = async (\n  request: Request,\n  response: Response\n) => {\n  const returnTo = await returnToPath(request);\n\n  if (returnTo === null) {\n    return response;\n  }\n\n  if (false === isRedirectResponse(response)) {\n    return response;\n  }\n\n  if (false === compareUrls(returnTo, response.headers.get(\"Location\")!)) {\n    return response;\n  }\n\n  const resultResponse = new Response(response.body, response);\n  resultResponse.headers.append(\n    \"Set-Cookie\",\n    await returnToCookie.serialize(null, { maxAge: -1 })\n  );\n  return resultResponse;\n};\n"
  },
  {
    "path": "apps/builder/app/services/csrf-session.server.ts",
    "content": "import { createCookieSessionStorage } from \"@remix-run/node\";\nimport env from \"~/env/env.server\";\nimport { extractAuthFromRequest } from \"~/shared/context.server\";\nimport { allowedDestinations } from \"./destinations.server\";\n\ntype CsrfSessionData = {\n  hash: string;\n  token: string;\n};\n\nconst getCsrfSessionCookieNameVersion = () => {\n  // IMPORTANT: If you see an error here, you need to increase the version number.\n  // Explanation:\n  // Changing the CsrfSessionData type will cause all existing user sessions to not work as expected.\n  // There is no logic to validate or clean up sessions, so we avoid session migration issues by changing the session cookie name.\n  // This ensures that old sessions are invalidated and new sessions are created with the updated structure.\n  const obj: CsrfSessionData = { token: \"\", hash: \"\" };\n  obj.hash = \"\";\n\n  // IMPORTANT: Change version in the SaaS platform as well!\n  // IMPORTANT: Changing the version will cause all users to be logged out.\n  return \"1\";\n};\n\nconst csrfSessionStorage = createCookieSessionStorage({\n  cookie: {\n    // Using the __Host- prefix to prevent a malicious user from setting another person's session cookie\n    // on all subdomains of apps.webstudio.is, e.g., setting Domain=.apps.webstudio.is.\n    // For more information, see: https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Cookies#name\n    name: `__Host-_csrf_${getCsrfSessionCookieNameVersion()}`,\n    sameSite: \"lax\",\n    path: \"/\",\n    httpOnly: true,\n    secrets: env.AUTH_WS_CLIENT_SECRET\n      ? [env.AUTH_WS_CLIENT_SECRET]\n      : undefined,\n    secure: true,\n  },\n});\n\nconst toBase64Url = (buffer: ArrayBuffer | ArrayBufferView) => {\n  return Buffer.from(\n    buffer instanceof ArrayBuffer ? buffer : buffer.buffer\n  ).toString(\"base64url\");\n};\n\nexport const getRequestAuthHash = async (request: Request) => {\n  const data = await extractAuthFromRequest(request);\n\n  if (data.isServiceCall) {\n    throw new Error(\"Service calls are not allowed to use CSRF tokens.\");\n  }\n\n  // Because of cookie session we don't have session id in the request, so we derive it from the auth credentials data.\n  const sessionId = data.authToken ?? data.sessionData?.userId ?? \"anonymous\";\n\n  const encoder = new TextEncoder();\n  const dataBuffer = encoder.encode(sessionId);\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", dataBuffer);\n  return toBase64Url(hashBuffer.slice(0, 16));\n};\n\n/**\n * We use a hash to ensure that any change in authentication credentials also changes the CSRF token.\n *\n * If setCookieValue is defined, it means that we need to set the cookie.\n */\nexport const getCsrfTokenAndCookie = async (\n  request: Request\n): Promise<[csrfToken: string, setCookieValue: string | undefined]> => {\n  const hash = await getRequestAuthHash(request);\n\n  const csrfSession = await csrfSessionStorage.getSession(\n    request.headers.get(\"Cookie\")\n  );\n\n  let sessionCreateCookieValue: string | undefined = undefined;\n\n  if (csrfSession.get(\"hash\") !== hash) {\n    const csrfTokenLength = 16;\n    const array = new Uint8Array(csrfTokenLength);\n    crypto.getRandomValues(array);\n    const token = toBase64Url(array);\n\n    csrfSession.set(\"hash\", hash);\n    csrfSession.set(\"token\", token);\n    sessionCreateCookieValue =\n      await csrfSessionStorage.commitSession(csrfSession);\n  }\n\n  return [csrfSession.get(\"token\"), sessionCreateCookieValue];\n};\n\nexport const checkCsrf = async (request: Request) => {\n  if (\n    request.headers.get(\"sec-fetch-mode\") === \"navigate\" &&\n    request.method === \"GET\"\n  ) {\n    // Do not check CSRF for GET navigation requests to allow logged-in users to view the data.\n    // However, prevent loading the data from an iframe.\n    allowedDestinations(request, [\"document\"]);\n    return;\n  }\n\n  const [token, setCookieValue] = await getCsrfTokenAndCookie(request);\n\n  if (setCookieValue) {\n    throw new Response(\n      \"Authentication credentials have changed. Please reload and try again.\",\n      {\n        status: 403,\n        statusText:\n          \"Authentication credentials have changed. Please reload and try again.\",\n      }\n    );\n  }\n\n  const csrfToken = request.headers.get(\"X-CSRF-Token\");\n\n  if (token !== csrfToken) {\n    throw new Response(\n      `Forbidden: \"The CSRF token is invalid. Please reload and try again.`,\n      {\n        status: 403,\n        statusText: `Forbidden: \"The CSRF token is invalid. Please reload and try again.`,\n      }\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/services/destinations.server.ts",
    "content": "type Destinations = RequestDestination | \"empty\";\n\n/**\n * Prevent loaders of iframe injections like `<iframe src=\"/rest/data/98b712e5-247c-448d-b68c-8c1681125998\">`\n * or even cross domain injections like `<iframe src=\"https://example.com\">`\n *\n * - document: The request is intended to obtain a document or an embedded resource.\n * - empty: The request is intended to obtain a resource that is not associated with a document i.e. fetch etc\n * - iframe: The request is intended to obtain a resource that is to be embedded in an iframe.\n *\n * IMPORTANT!!!: In most cases must be used in loaders only.\n **/\nexport const allowedDestinations = (\n  request: Request,\n  destinations: Destinations[]\n) => {\n  const destination = request.headers.get(\"sec-fetch-dest\");\n\n  if (destination === null) {\n    return;\n  }\n\n  if (destinations.includes(destination as Destinations)) {\n    return;\n  }\n\n  throw new Response(null, {\n    status: 403,\n    statusText: `Forbidden: Destination ${destination} not allowed`,\n  });\n};\n\nexport const isFetchDestination = (request: Request) => {\n  return request.headers.get(\"sec-fetch-dest\") === \"empty\";\n};\n"
  },
  {
    "path": "apps/builder/app/services/logout-router.server.ts",
    "content": "import { dashboardProjectRouter } from \"@webstudio-is/dashboard/index.server\";\nimport {\n  router,\n  procedure,\n  createCallerFactory,\n} from \"@webstudio-is/trpc-interface/index.server\";\n\nconst dashboardProjectCaller = createCallerFactory(dashboardProjectRouter);\n\nexport const logoutRouter = router({\n  /**\n   * The builderUrl can be derived/constructed using the projectId.\n   * Therefore, instead of returning builder app endpoints, we are using projectIds.\n   */\n  getLoggedInProjectIds: procedure.query(async ({ ctx }) => {\n    if (ctx.authorization.type !== \"user\") {\n      return [];\n    }\n\n    const { isLoggedInToBuilder } = ctx.authorization;\n\n    const projectIds =\n      await dashboardProjectCaller(ctx).findCurrentUserProjectIds();\n\n    const loggedInIds = (\n      await Promise.all(\n        projectIds.map(async (projectId) => {\n          const isLoggedIn = await isLoggedInToBuilder(projectId);\n          return isLoggedIn ? projectId : undefined;\n        })\n      )\n    ).filter((id) => id !== undefined);\n\n    return loggedInIds;\n  }),\n});\n"
  },
  {
    "path": "apps/builder/app/services/no-cross-origin-cookie.ts",
    "content": "import { json } from \"@remix-run/server-runtime\";\n\n/**\n * https://kevincox.ca/2024/08/24/cors/\n *\n * The function is specifically needed to handle “simple” CORS requests,\n * which are more prone to bypassing the stricter CORS preflight checks.\n * By clearing cookies from these cross-origin requests,\n * it reduces the risk of CSRF attacks and other vulnerabilities associated with simple CORS requests.\n *\n * Warning: There is no combination of Access-Control-Allow-* headers that you can set that solves simple requests,\n * they are made before any policy is checked. You need to handle them in another way.\n * Do not try to fix this by setting a CORS policy\n **/\nexport const preventCrossOriginCookie = (\n  request: Request,\n  throwError: boolean = true\n) => {\n  if (request.headers.get(\"sec-fetch-site\") === \"same-origin\") {\n    // Same origin, OK\n    return;\n  }\n\n  if (\n    request.headers.get(\"sec-fetch-mode\") === \"navigate\" &&\n    request.method === \"GET\"\n  ) {\n    //  GET requests shouldn't mutate state so this is safe.\n    return;\n  }\n\n  request.headers.delete(\"cookie\");\n\n  if (\n    request.headers.has(\"Authorization\") ||\n    request.headers.has(\"x-auth-token\")\n  ) {\n    // Do not throw an error if the request has an Authorization or x-auth-token header.\n    // In that case, it is not a simple CORS request and will be prevented by a preflight check.\n    return;\n  }\n\n  if (throwError) {\n    console.error(`Cross-origin request to ${request.url} blocked`, [\n      ...request.headers.entries(),\n    ]);\n\n    // allow service calls\n    throw json(\n      {\n        message: `Cross-origin request to ${request.url}`,\n      },\n      {\n        status: 403,\n        statusText: \"Forbidden\",\n      }\n    );\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/services/no-store-redirect.ts",
    "content": "import { redirect as remixRedirect } from \"@remix-run/server-runtime\";\nimport { isRedirectResponse } from \"./cookie.server\";\n\n/**\n * Chrome aggressively uses cache when restoring tabs (e.g., using Shift+Command+T or automatic session restore).\n * This behavior can cause issues with loading a builder application due to the following cycle:\n *\n * 1. The builder checks if the session needs to be renewed (a navigation is detected) and redirects to `/auth/ws`.\n * 2. Chrome retrieves the `/auth/ws` response from the cache, followed by all OAuth redirect responses from the cache.\n * 3. The last cached response redirects back to the builder's root (`/`). Chrome detects this as a redirect cycle\n *    and reloads the most recent OAuth step, resulting in an \"expected state\" error.\n * 4. To prevent this issue, the only solution is to add a `no-store` directive to all redirect responses.\n */\nexport const redirect: typeof remixRedirect = (url, init) => {\n  const headers =\n    typeof init === \"object\" ? new Headers(init.headers) : new Headers();\n  headers.set(\"Cache-Control\", \"no-store\");\n\n  const responseInit: ResponseInit =\n    typeof init === \"number\" ? { status: init, headers } : { ...init, headers };\n\n  return remixRedirect(url, responseInit);\n};\n\n/**\n * Chrome aggressively uses cache when restoring tabs (e.g., using Shift+Command+T or automatic session restore).\n * This behavior can cause issues with loading a builder application due to the following cycle:\n *\n * 1. The builder checks if the session needs to be renewed (a navigation is detected) and redirects to `/auth/ws`.\n * 2. Chrome retrieves the `/auth/ws` response from the cache, followed by all OAuth redirect responses from the cache.\n * 3. The last cached response redirects back to the builder's root (`/`). Chrome detects this as a redirect cycle\n *    and reloads the most recent OAuth step, resulting in an \"expected state\" error.\n * 4. To prevent this issue, the only solution is to add a `no-store` directive to all redirect responses.\n */\nexport const setNoStoreToRedirect = (response: Response) => {\n  if (isRedirectResponse(response)) {\n    const newResponse = new Response(response.body, response);\n    newResponse.headers.set(\"Cache-Control\", \"no-store\");\n    return newResponse;\n  }\n\n  return response;\n};\n"
  },
  {
    "path": "apps/builder/app/services/session.server.ts",
    "content": "import { createCookieSessionStorage } from \"@remix-run/node\";\nimport env from \"~/env/env.server\";\nimport { BloomFilter } from \"./bloom-filter.server\";\nimport { getSessionCookieNameVersion } from \"./auth.server.utils\";\n\n// export the whole sessionStorage object\nexport const sessionStorage = createCookieSessionStorage({\n  cookie: {\n    // Using the __Host- prefix to prevent a malicious user from setting another person's session cookie\n    // on all subdomains of apps.webstudio.is, e.g., setting Domain=.apps.webstudio.is.\n    // For more information, see: https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Cookies#name\n    name: `__Host-_session_${getSessionCookieNameVersion()}`,\n    maxAge: 60 * 60 * 24 * 30,\n    sameSite: \"lax\",\n    path: \"/\",\n    httpOnly: true,\n    secrets: env.AUTH_SECRET ? [env.AUTH_SECRET] : undefined,\n    secure: true,\n  },\n});\n\n// Bloom filter: probabilistic data structure to efficiently check if session IDs are logged in.\n// Allowing tracking of user login status for specific projects\nconst bloomFilterKey = \"login_session_bloom_filter\";\n\nexport const readLoginSessionBloomFilter = async (request: Request) => {\n  try {\n    const session = await sessionStorage.getSession(\n      request.headers.get(\"Cookie\")\n    );\n\n    const bloomFilter = session.get(bloomFilterKey);\n\n    if (typeof bloomFilter === \"string\") {\n      return BloomFilter.deserialize(bloomFilter);\n    }\n\n    return new BloomFilter(50, 0.02);\n  } catch (error) {\n    console.error(\"Error reading bloom filter\", error);\n    return new BloomFilter(50, 0.02);\n  }\n};\n\nexport const writeLoginSessionBloomFilter = async (\n  request: Request,\n  response: Response,\n  bloomFilter: BloomFilter\n) => {\n  const session = await sessionStorage.getSession(\n    request.headers.get(\"Cookie\")\n  );\n\n  session.set(bloomFilterKey, bloomFilter.serialize());\n\n  const result = new Response(response.body, response);\n\n  result.headers.append(\n    \"Set-Cookie\",\n    await sessionStorage.commitSession(session)\n  );\n\n  return result;\n};\n"
  },
  {
    "path": "apps/builder/app/services/token.server.test.ts",
    "content": "import {\n  encrypt,\n  decrypt,\n  createAccessToken,\n  readAccessToken,\n} from \"./token.server\";\nimport { test, expect } from \"vitest\";\nimport { nanoid } from \"nanoid\";\n\ntest.each([\n  \"\",\n  nanoid(1),\n  nanoid(2),\n  nanoid(5),\n  nanoid(10),\n  nanoid(20),\n  nanoid(30),\n  nanoid(40),\n])(\"Encrypt and decrypt\", async (cithertext) => {\n  const secret =\n    \"JDKHSJFHKJHFSKJDHFJSDHFKJHDKJFHSKJDHFKJHFKJSFHKKADHJKSHDJKSHAJKHDASKDJH\";\n\n  expect(await decrypt(await encrypt(cithertext, secret), secret)).toBe(\n    cithertext\n  );\n});\n\ntest(\"Access token\", async () => {\n  const secret = \"1212121212\";\n\n  const token = await createAccessToken(\n    { userId: \"1\", projectId: \"2\" },\n    secret,\n    {\n      maxAge: 1000 * 60,\n    }\n  );\n\n  const tokenPayload = await readAccessToken(token, secret);\n\n  expect(tokenPayload).toEqual({ userId: \"1\", projectId: \"2\" });\n});\n"
  },
  {
    "path": "apps/builder/app/services/token.server.ts",
    "content": "import jwt from \"@tsndr/cloudflare-worker-jwt\";\nimport { z } from \"zod\";\n\n// Ensure your key is of the correct length\nconst normalizeKey = (key: string, length = 32) => {\n  const encoder = new TextEncoder();\n  const keyBuffer = encoder.encode(key);\n\n  if (keyBuffer.length === length) {\n    return keyBuffer;\n  }\n\n  if (keyBuffer.length > length) {\n    // Truncate the key if it's too long\n    return keyBuffer.slice(0, length);\n  }\n\n  // Pad the key if it's too short\n  const paddedKey = new Uint8Array(length);\n  paddedKey.set(keyBuffer);\n  return paddedKey;\n};\n\nexport const encrypt = async (text: string, key: string) => {\n  const enc = new TextEncoder();\n  const keyBuffer = await crypto.subtle.importKey(\n    \"raw\",\n    normalizeKey(key),\n    { name: \"AES-GCM\" },\n    false,\n    [\"encrypt\"]\n  );\n  const iv = crypto.getRandomValues(new Uint8Array(12));\n  const ciphertext = await crypto.subtle.encrypt(\n    { name: \"AES-GCM\", iv },\n    keyBuffer,\n    enc.encode(text)\n  );\n\n  return Buffer.from([...iv, ...new Uint8Array(ciphertext)]).toString(\n    \"base64url\"\n  );\n};\n\nexport const decrypt = async (ciphertext: string, key: string) => {\n  const keyBuffer = await crypto.subtle.importKey(\n    \"raw\",\n    normalizeKey(key),\n    { name: \"AES-GCM\" },\n    false,\n    [\"decrypt\"]\n  );\n  const data = Uint8Array.from(Buffer.from(ciphertext, \"base64url\"));\n\n  const decrypted = await crypto.subtle.decrypt(\n    { name: \"AES-GCM\", iv: data.slice(0, 12) },\n    keyBuffer,\n    data.slice(12)\n  );\n  return new TextDecoder().decode(decrypted);\n};\n\nexport const verifyChallenge = async (\n  codeVerifier: string,\n  codeChallenge: string\n): Promise<boolean> => {\n  const encoder = new TextEncoder();\n  const data = encoder.encode(codeVerifier);\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n  const base64Hash = Buffer.from(hashBuffer).toString(\"base64url\");\n\n  return base64Hash === codeChallenge;\n};\n\nconst CodeTokenPayload = z.object({\n  userId: z.string(),\n  projectId: z.string(),\n  codeChallenge: z.string(),\n});\n\ntype CodeTokenPayload = z.infer<typeof CodeTokenPayload>;\n\n/**\n * JWT is used because it includes a signature check and expiration,\n * allowing us to verify in a single call that the token was issued by us and has not expired\n *\n * The token is encrypted in accordance with the RFC.\n *\n * https://datatracker.ietf.org/doc/html/rfc7636#section-4.4\n *\n * Typically, the \"code_challenge\" and \"code_challenge_method\" values\n * are stored in encrypted form in the \"code\" itself but could\n * alternatively be stored on the server associated with the code.  The\n * server MUST NOT include the \"code_challenge\" value in client requests\n * in a form that other entities can extract.\n */\nexport const createCodeToken = async (\n  payload: CodeTokenPayload,\n  secret: string,\n  options: { maxAge: number }\n) => {\n  const expiresAt = Math.round((Date.now() + options.maxAge) / 1000);\n\n  const jwtKey = await jwt.sign(\n    {\n      iss: \"webstudio\",\n      sub: `${payload.userId}:${payload.projectId}`,\n      exp: expiresAt,\n      iat: Math.round(Date.now() / 1000),\n      ...payload,\n    },\n    secret\n  );\n\n  return await encrypt(jwtKey, secret);\n};\n\nexport const readCodeToken = async (\n  accessToken: string,\n  secret: string\n): Promise<CodeTokenPayload | undefined> => {\n  const jwtToken = await decrypt(accessToken, secret);\n\n  if (false === (await jwt.verify(jwtToken, secret))) {\n    return;\n  }\n\n  const token = await jwt.decode(jwtToken);\n\n  const parsedToken = CodeTokenPayload.parse(token.payload);\n\n  return parsedToken;\n};\n\nconst AccessTokenPayload = z.object({\n  userId: z.string(),\n  projectId: z.string(),\n});\n\ntype AccessTokenPayload = z.infer<typeof AccessTokenPayload>;\n\n/**\n * JWT is used because it includes a signature check and expiration,\n * allowing us to verify in a single call that the token was issued by us and has not expired\n */\nexport const createAccessToken = async (\n  payload: AccessTokenPayload,\n  secret: string,\n  options: { maxAge: number }\n) => {\n  const expiresAt = Math.round((Date.now() + options.maxAge) / 1000);\n\n  return await jwt.sign(\n    {\n      iss: \"webstudio\",\n      sub: `${payload.userId}:${payload.projectId}`,\n      exp: expiresAt,\n      iat: Math.round(Date.now() / 1000),\n      ...payload,\n    },\n    secret\n  );\n};\n\nexport const readAccessToken = async (\n  accessToken: string,\n  secret: string\n): Promise<AccessTokenPayload | undefined> => {\n  if (false === (await jwt.verify(accessToken, secret))) {\n    return;\n  }\n\n  const token = jwt.decode(accessToken);\n\n  const parsedToken = AccessTokenPayload.parse(token.payload);\n\n  return parsedToken;\n};\n"
  },
  {
    "path": "apps/builder/app/services/trcp-router.server.ts",
    "content": "import { router } from \"@webstudio-is/trpc-interface/index.server\";\nimport { domainRouter } from \"@webstudio-is/domain/index.server\";\nimport { projectRouter } from \"@webstudio-is/project/index.server\";\nimport { authorizationTokenRouter } from \"@webstudio-is/authorization-token/index.server\";\nimport { dashboardProjectRouter } from \"@webstudio-is/dashboard/index.server\";\nimport { marketplaceRouter } from \"~/shared/marketplace/router.server\";\nimport { userRouter } from \"./user-router.server\";\nimport { logoutRouter } from \"./logout-router.server\";\n\nexport const appRouter = router({\n  user: userRouter,\n  marketplace: marketplaceRouter,\n  domain: domainRouter,\n  project: projectRouter,\n  authorizationToken: authorizationTokenRouter,\n  dashboardProject: dashboardProjectRouter,\n  logout: logoutRouter,\n});\n\nexport type AppRouter = typeof appRouter;\n"
  },
  {
    "path": "apps/builder/app/services/trpc.server.ts",
    "content": "import { createTrpcProxyServiceClient } from \"@webstudio-is/trpc-interface/index.server\";\nimport env from \"~/env/env.server\";\nimport { staticEnv } from \"~/env/env.static.server\";\n\nconst TRPC_SERVER_URL = env.TRPC_SERVER_URL ?? \"\";\nconst TRPC_SERVER_API_TOKEN = env.TRPC_SERVER_API_TOKEN ?? \"\";\nconst GITHUB_REF_NAME = staticEnv.GITHUB_REF_NAME;\n\nexport const trpcSharedClient = createTrpcProxyServiceClient(\n  TRPC_SERVER_URL !== \"\" && TRPC_SERVER_API_TOKEN !== \"\"\n    ? {\n        url: TRPC_SERVER_URL,\n        token: TRPC_SERVER_API_TOKEN,\n        branchName: GITHUB_REF_NAME,\n      }\n    : undefined\n);\n"
  },
  {
    "path": "apps/builder/app/services/user-router.server.ts",
    "content": "import { z } from \"zod\";\nimport { procedure, router } from \"@webstudio-is/trpc-interface/index.server\";\nimport {\n  updateUserProjectsTags,\n  userProjectTagSchema,\n} from \"../shared/db/user.server\";\n\nexport const userRouter = router({\n  updateProjectsTags: procedure\n    .input(z.object({ tags: z.array(userProjectTagSchema) }))\n    .mutation(async ({ input, ctx }) => {\n      return await updateUserProjectsTags(input, ctx);\n    }),\n});\n"
  },
  {
    "path": "apps/builder/app/shared/$resources/assets.server.ts",
    "content": "import { json } from \"@remix-run/server-runtime\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport { loadAssetsByProject } from \"@webstudio-is/asset-uploader/index.server\";\nimport { toRuntimeAsset } from \"@webstudio-is/sdk\";\nimport { isBuilder } from \"../router-utils\";\nimport { createContext } from \"../context.server\";\n\n/**\n * System Resource that provides the list of assets for the current project.\n * This allows assets to be dynamically referenced in the builder using the expression editor.\n */\nexport const loader = async ({ request }: { request: Request }) => {\n  if (isBuilder(request) === false) {\n    throw new Error(\n      \"Asset resource loader can only be accessed from the builder interface\"\n    );\n  }\n\n  const { projectId } = parseBuilderUrl(request.url);\n\n  if (projectId === undefined) {\n    throw new Error(\n      \"Project ID is required to load assets. Ensure the request includes a valid project context.\"\n    );\n  }\n\n  const context = await createContext(request);\n\n  const assets = await loadAssetsByProject(projectId, context);\n\n  const requestUrl = new URL(request.url);\n  const origin = `${requestUrl.protocol}//${requestUrl.host}`;\n\n  // Convert array to object with asset IDs as keys\n  // Use /cgi/ endpoint URLs (relative paths)\n  const assetsById = Object.fromEntries(\n    assets.map((asset) => [asset.id, toRuntimeAsset(asset, origin)])\n  );\n\n  return json(assetsById);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/$resources/current-date.server.ts",
    "content": "import { json } from \"@remix-run/server-runtime\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport { isBuilder } from \"../router-utils\";\n\n/**\n * System Resource that provides current date information.\n * This prevents React hydration errors when displaying dynamic dates\n * (e.g., copyright years in footers) by ensuring server and client\n * render the same date.\n *\n * All values are normalized to midnight UTC (00:00:00.000Z) to ensure\n * consistency throughout the entire day, preventing hydration mismatches.\n */\nexport const loader = async ({ request }: { request: Request }) => {\n  if (isBuilder(request) === false) {\n    throw new Error(\"Only builder requests are allowed\");\n  }\n\n  const { projectId } = parseBuilderUrl(request.url);\n\n  if (projectId === undefined) {\n    throw new Error(\"projectId is required\");\n  }\n\n  const now = new Date();\n\n  // Normalize to midnight UTC to prevent hydration mismatches\n  const startOfDay = new Date(\n    Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n  );\n\n  return json({\n    iso: startOfDay.toISOString(),\n    year: startOfDay.getUTCFullYear(),\n    month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n    day: startOfDay.getUTCDate(),\n    timestamp: startOfDay.getTime(),\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/$resources/sitemap.xml.server.ts",
    "content": "import { json } from \"@remix-run/server-runtime\";\nimport { parsePages } from \"@webstudio-is/project-build/index.server\";\nimport { getStaticSiteMapXml } from \"@webstudio-is/sdk\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport { isBuilder } from \"../router-utils\";\nimport { createContext } from \"../context.server\";\n\n/**\n * This should be a route in SvelteKit, as it can be fetched server-side without an actual HTTP request.\n * Consider moving it to routes if Remix supports similar functionality in the future.\n * Note: Fetching own routes using request.origin is prohibited on Cloudflare Workers.\n * Note: We are not moving this to routes to avoid generating an additional 30MB function on deploy.\n */\nexport const loader = async ({ request }: { request: Request }) => {\n  if (isBuilder(request) === false) {\n    throw new Error(\"Only builder requests are allowed\");\n  }\n\n  const { projectId } = parseBuilderUrl(request.url);\n\n  if (projectId === undefined) {\n    throw new Error(\"projectId is required\");\n  }\n\n  const context = await createContext(request);\n\n  const buildResult = await context.postgrest.client\n    .from(\"Build\")\n    .select(\"pages, updatedAt\")\n    .eq(\"projectId\", projectId)\n    .is(\"deployment\", null)\n    .single();\n\n  if (buildResult.error) {\n    throw json({ message: buildResult.error.message }, { status: 404 });\n  }\n\n  const build = buildResult.data;\n\n  const pages = parsePages(build.pages);\n\n  const siteMap = getStaticSiteMapXml(pages, build.updatedAt);\n\n  return json(siteMap);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/app.css",
    "content": "body {\n  margin: 0;\n  font-family: var(--fonts-sans);\n  font-size: var(--fontSizes-1);\n}\n"
  },
  {
    "path": "apps/builder/app/shared/array-utils.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { removeByMutable, repeatUntil } from \"./array-utils\";\n\ntest(\"removeByMutable\", () => {\n  const array = [\n    { param: 1 },\n    { param: 2 },\n    { param: 3 },\n    { param: 4 },\n    { param: 5 },\n    { param: 6 },\n  ];\n  removeByMutable(array, (item) => item.param % 2 === 1);\n  expect(array).toMatchInlineSnapshot(`\n    [\n      {\n        \"param\": 2,\n      },\n      {\n        \"param\": 4,\n      },\n      {\n        \"param\": 6,\n      },\n    ]\n  `);\n  removeByMutable(array, (item) => item.param !== 4);\n  expect(array).toMatchInlineSnapshot(`\n    [\n      {\n        \"param\": 4,\n      },\n    ]\n  `);\n});\n\ntest(\"repeatUntil\", () => {\n  expect(repeatUntil([1, 2, 3], 5)).toEqual([1, 2, 3, 1, 2]);\n  expect(repeatUntil([1, 2, 3], 1)).toEqual([1, 2, 3]);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/array-utils.ts",
    "content": "type Predicate<Item> = (value: Item) => boolean;\n\nexport const removeByMutable = <Item>(\n  array: Item[],\n  predicate: Predicate<Item>\n) => {\n  // reversed order to splice without breaking index\n  for (let index = array.length - 1; index >= 0; index -= 1) {\n    if (predicate(array[index])) {\n      array.splice(index, 1);\n    }\n  }\n};\n\nexport const repeatUntil = <Item>(array: Item[], count: number) => {\n  const repeatedArray: Item[] = [];\n  for (let index = 0; index < Math.max(count, array.length); index += 1) {\n    repeatedArray.push(array[index % array.length]);\n  }\n  return repeatedArray;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/asset-client.ts",
    "content": "import * as path from \"node:path\";\nimport { MaxSize } from \"@webstudio-is/asset-uploader\";\nimport {\n  createFsClient,\n  createS3Client,\n} from \"@webstudio-is/asset-uploader/index.server\";\nimport env from \"~/env/env.server\";\n\nexport const fileUploadPath = \"public/cgi/asset\";\n\nexport const createAssetClient = () => {\n  const maxUploadSize = MaxSize.parse(env.MAX_UPLOAD_SIZE);\n  if (\n    env.S3_ENDPOINT !== undefined &&\n    env.S3_REGION !== undefined &&\n    env.S3_ACCESS_KEY_ID !== undefined &&\n    env.S3_SECRET_ACCESS_KEY !== undefined &&\n    env.S3_BUCKET !== undefined\n  ) {\n    return createS3Client({\n      endpoint: env.S3_ENDPOINT,\n      region: env.S3_REGION,\n      accessKeyId: env.S3_ACCESS_KEY_ID,\n      secretAccessKey: env.S3_SECRET_ACCESS_KEY,\n      bucket: env.S3_BUCKET,\n      acl: env.S3_ACL,\n      maxUploadSize,\n    });\n  } else {\n    return createFsClient({\n      maxUploadSize,\n      fileDirectory: path.join(process.cwd(), fileUploadPath),\n    });\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/awareness.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport { findAwarenessByInstanceId } from \"./awareness\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $, renderData } from \"@webstudio-is/template\";\n\ntest(\"find awareness by instance\", () => {\n  const pages = createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"bodyId\",\n  });\n  const { instances } = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <$.Box ws:id=\"boxId\">\n        <$.Text ws:id=\"textId\"></$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  expect(findAwarenessByInstanceId(pages, instances, \"textId\")).toEqual({\n    pageId: \"homePageId\",\n    instanceSelector: [\"textId\", \"boxId\", \"bodyId\"],\n  });\n});\n\ntest(\"find awareness by instance inside of slot\", () => {\n  const pages = createDefaultPages({\n    homePageId: \"homePageId\",\n    rootInstanceId: \"bodyId\",\n  });\n  const { instances } = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <$.Slot ws:id=\"slotOneId\">\n        <$.Fragment ws:id=\"fragmentId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </$.Fragment>\n      </$.Slot>\n      <$.Slot ws:id=\"slotTwoId\">\n        <$.Fragment ws:id=\"fragmentId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </$.Fragment>\n      </$.Slot>\n    </$.Body>\n  );\n  expect(findAwarenessByInstanceId(pages, instances, \"boxId\")).toEqual({\n    pageId: \"homePageId\",\n    instanceSelector: [\"boxId\", \"fragmentId\", \"slotTwoId\", \"bodyId\"],\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/awareness.ts",
    "content": "import { atom, computed, onSet } from \"nanostores\";\nimport {\n  findPageByIdOrPath,\n  Instance,\n  Instances,\n  ROOT_INSTANCE_ID,\n  type Page,\n  rootComponent,\n  Pages,\n  findParentFolderByChildId,\n  getPagePath,\n  ROOT_FOLDER_ID,\n} from \"@webstudio-is/sdk\";\nimport { $pages } from \"./nano-states/pages\";\nimport { $instances, $selectedInstanceSelector } from \"./nano-states/instances\";\nimport type { InstanceSelector } from \"./tree-utils\";\n\nexport type Awareness = {\n  pageId: Page[\"id\"];\n  instanceSelector?: Instance[\"id\"][];\n};\n\nexport const $awareness = atom<undefined | Awareness>();\n\nonSet($awareness, ({ newValue }) => {\n  $selectedInstanceSelector.set(newValue?.instanceSelector);\n});\n\nexport const $selectedPage = computed(\n  [$pages, $awareness],\n  (pages, awareness) => {\n    if (pages === undefined || awareness === undefined) {\n      return;\n    }\n    return findPageByIdOrPath(awareness.pageId, pages);\n  }\n);\n\nexport const $selectedPagePath = computed(\n  [$selectedPage, $pages],\n  (page, pages) => {\n    if (pages === undefined || page === undefined) {\n      return \"/\";\n    }\n    const parentFolder = findParentFolderByChildId(page.id, pages.folders);\n    const parentFolderId = parentFolder?.id ?? ROOT_FOLDER_ID;\n    const foldersPath = getPagePath(parentFolderId, pages);\n    return [foldersPath, page?.path ?? \"\"]\n      .filter(Boolean)\n      .join(\"/\")\n      .replace(/\\/+/g, \"/\");\n  }\n);\n\nexport const $temporaryInstances = atom<Instances>(new Map());\nexport const addTemporaryInstance = (instance: Instance) => {\n  $temporaryInstances.get().set(instance.id, instance);\n  $temporaryInstances.set($temporaryInstances.get());\n};\n\nexport const $virtualInstances = computed($selectedPage, (selectedPage) => {\n  const virtualInstances: Instances = new Map();\n  if (selectedPage) {\n    virtualInstances.set(ROOT_INSTANCE_ID, {\n      type: \"instance\",\n      id: ROOT_INSTANCE_ID,\n      component: rootComponent,\n      children: [{ type: \"id\", value: selectedPage.rootInstanceId }],\n    });\n  }\n  return virtualInstances;\n});\n\nexport const $selectedInstance = computed(\n  [$instances, $virtualInstances, $temporaryInstances, $awareness],\n  (instances, virtualInstances, tempInstances, awareness) => {\n    if (awareness?.instanceSelector === undefined) {\n      return;\n    }\n    const [selectedInstanceId] = awareness.instanceSelector;\n    return (\n      instances.get(selectedInstanceId) ??\n      virtualInstances.get(selectedInstanceId) ??\n      tempInstances.get(selectedInstanceId)\n    );\n  }\n);\n\nexport const getInstanceKey = <\n  InstanceSelector extends undefined | Instance[\"id\"][],\n>(\n  instanceSelector: InstanceSelector\n): (InstanceSelector extends undefined ? undefined : never) | string =>\n  JSON.stringify(instanceSelector);\n\nexport const $selectedInstanceKeyWithRoot = computed(\n  $awareness,\n  (awareness) => {\n    const instanceSelector = awareness?.instanceSelector;\n    if (instanceSelector) {\n      if (instanceSelector[0] === ROOT_INSTANCE_ID) {\n        return getInstanceKey(instanceSelector);\n      }\n      return getInstanceKey([...instanceSelector, ROOT_INSTANCE_ID]);\n    }\n  }\n);\n\nexport const $selectedInstanceKey = computed($awareness, (awareness) =>\n  getInstanceKey(awareness?.instanceSelector)\n);\n\nexport type InstancePath = Array<{\n  instance: Instance;\n  instanceSelector: string[];\n}>;\n\nexport const getInstancePath = (\n  instanceSelector: string[],\n  instances: Instances,\n  virtualInstances?: Instances,\n  temporaryInstances?: Instances\n): undefined | InstancePath => {\n  const instancePath: InstancePath = [];\n  for (let index = 0; index < instanceSelector.length; index += 1) {\n    const instanceId = instanceSelector[index];\n    const instance =\n      instances.get(instanceId) ??\n      virtualInstances?.get(instanceId) ??\n      temporaryInstances?.get(instanceId);\n    // collection item can be undefined\n    if (instance === undefined) {\n      continue;\n    }\n    instancePath.push({\n      instance,\n      instanceSelector: instanceSelector.slice(index),\n    });\n  }\n  // all consuming code expect at least one instance to be selected\n  // though it is possible to get empty array when undo created page\n  if (instancePath.length === 0) {\n    return;\n  }\n  return instancePath;\n};\n\nexport const $selectedInstancePath = computed(\n  [$instances, $virtualInstances, $temporaryInstances, $awareness],\n  (instances, virtualInstances, temporaryInstances, awareness) => {\n    const instanceSelector = awareness?.instanceSelector;\n    if (instanceSelector === undefined) {\n      return;\n    }\n    return getInstancePath(\n      instanceSelector,\n      instances,\n      virtualInstances,\n      temporaryInstances\n    );\n  }\n);\n\nexport const $selectedInstancePathWithRoot = computed(\n  [$instances, $virtualInstances, $temporaryInstances, $awareness],\n  (instances, virtualInstances, temporaryInstances, awareness) => {\n    let instanceSelector = awareness?.instanceSelector;\n    if (instanceSelector === undefined) {\n      return;\n    }\n    // add root as ancestor when root is not selected\n    if (instanceSelector[0] !== ROOT_INSTANCE_ID) {\n      instanceSelector = [...instanceSelector, ROOT_INSTANCE_ID];\n    }\n    return getInstancePath(\n      instanceSelector,\n      instances,\n      virtualInstances,\n      temporaryInstances\n    );\n  }\n);\n\nexport const selectPage = (pageId: Page[\"id\"]) => {\n  const pages = $pages.get();\n  if (pages === undefined) {\n    return;\n  }\n  const page = findPageByIdOrPath(pageId, pages);\n  if (page === undefined) {\n    return;\n  }\n  $awareness.set({ pageId: page.id, instanceSelector: [page.rootInstanceId] });\n};\n\nexport const selectInstance = (\n  instanceSelector: undefined | Instance[\"id\"][]\n) => {\n  const awareness = $awareness.get();\n  if (\n    awareness &&\n    // prevent triggering select across the builder when selector is the same\n    // useful when click and focus events have to select instance\n    awareness.instanceSelector?.join() !== instanceSelector?.join()\n  ) {\n    $awareness.set({\n      pageId: awareness.pageId,\n      instanceSelector,\n    });\n  }\n};\n\nconst findPageId = (pages: Pages, instanceSelector: InstanceSelector) => {\n  const rootInstanceId = instanceSelector.at(-1);\n  for (const page of [pages.homePage, ...pages.pages]) {\n    if (page.rootInstanceId === rootInstanceId) {\n      return page.id;\n    }\n  }\n  return pages.homePage.id;\n};\n\nconst parentInstanceByIdCache = new WeakMap<\n  Instances,\n  Map<Instance[\"id\"], Instance[\"id\"]>\n>();\n\n/**\n * traverse the tree up until body to build awareness\n * when instance id is inside of slot last matching parent is used to further\n */\nexport const findAwarenessByInstanceId = (\n  pages: Pages,\n  instances: Instances,\n  startingInstanceId: Instance[\"id\"]\n): Awareness => {\n  // recompute parent instances only when instances are changed\n  let parentInstanceById = parentInstanceByIdCache.get(instances);\n  if (parentInstanceById === undefined) {\n    parentInstanceById = new Map<Instance[\"id\"], Instance[\"id\"]>();\n    for (const instance of instances.values()) {\n      for (const child of instance.children) {\n        if (child.type === \"id\") {\n          parentInstanceById.set(child.value, instance.id);\n        }\n      }\n    }\n    parentInstanceByIdCache.set(instances, parentInstanceById);\n  }\n  const instanceSelector = [];\n  let currentInstanceId: undefined | Instance[\"id\"] = startingInstanceId;\n  while (currentInstanceId) {\n    instanceSelector.push(currentInstanceId);\n    currentInstanceId = parentInstanceById.get(currentInstanceId);\n  }\n  const pageId = findPageId(pages, instanceSelector);\n  return { pageId, instanceSelector };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/breakpoints/index.ts",
    "content": "export {\n  isBaseBreakpoint,\n  groupBreakpoints,\n  buildMergedBreakpointIds,\n  minCanvasWidth,\n} from \"../breakpoints-utils\";\nexport * from \"./select-breakpoint-by-order\";\nexport * from \"./stores\";\n"
  },
  {
    "path": "apps/builder/app/shared/breakpoints/select-breakpoint-by-order.ts",
    "content": "import { groupBreakpoints } from \"../breakpoints-utils\";\nimport { $selectedBreakpointId, $breakpoints } from \"../nano-states\";\nimport { setCanvasWidth } from \"~/builder/shared/calc-canvas-width\";\n\n/**\n * Order number starts with 1 and covers all existing breakpoints\n */\nexport const selectBreakpointByOrder = (orderNumber: number) => {\n  const breakpoints = $breakpoints.get();\n  const index = orderNumber - 1;\n  const grouped = groupBreakpoints(Array.from(breakpoints.values()));\n  const allBreakpoints = [...grouped.widthBased, ...grouped.custom];\n  const breakpoint = allBreakpoints.at(index);\n  if (breakpoint) {\n    $selectedBreakpointId.set(breakpoint.id);\n    setCanvasWidth(breakpoint.id);\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/breakpoints/stores.ts",
    "content": "import { atom } from \"nanostores\";\n\nexport const $breakpointsMenuView = atom<\n  \"initial\" | \"editor\" | \"confirmation\" | undefined\n>();\n"
  },
  {
    "path": "apps/builder/app/shared/breakpoints-utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  isBaseBreakpoint,\n  groupBreakpoints,\n  buildMergedBreakpointIds,\n} from \"./breakpoints-utils\";\nimport type { Breakpoint, Breakpoints } from \"@webstudio-is/sdk\";\n\ndescribe(\"isBaseBreakpoint\", () => {\n  test(\"returns true for breakpoint without min or max width\", () => {\n    expect(isBaseBreakpoint({})).toBe(true);\n    expect(isBaseBreakpoint({ minWidth: undefined, maxWidth: undefined })).toBe(\n      true\n    );\n  });\n\n  test(\"returns false for breakpoint with min width\", () => {\n    expect(isBaseBreakpoint({ minWidth: 768 })).toBe(false);\n  });\n\n  test(\"returns false for breakpoint with max width\", () => {\n    expect(isBaseBreakpoint({ maxWidth: 991 })).toBe(false);\n  });\n\n  test(\"returns false for breakpoint with both min and max width\", () => {\n    expect(isBaseBreakpoint({ minWidth: 768, maxWidth: 991 })).toBe(false);\n  });\n});\n\ndescribe(\"groupBreakpoints\", () => {\n  test(\"groups breakpoints in UI order: min-width (desc), base, max-width (desc)\", () => {\n    const initial = [\n      { minWidth: 1920 },\n      { minWidth: 1440 },\n      { minWidth: 1280 },\n      {},\n      { maxWidth: 991 },\n      { maxWidth: 767 },\n      { maxWidth: 479 },\n    ];\n    const result = groupBreakpoints(initial);\n    expect(result.widthBased).toStrictEqual([\n      { minWidth: 1920 },\n      { minWidth: 1440 },\n      { minWidth: 1280 },\n      {},\n      { maxWidth: 991 },\n      { maxWidth: 767 },\n      { maxWidth: 479 },\n    ]);\n    expect(result.custom).toStrictEqual([]);\n  });\n\n  test(\"handles unsorted input\", () => {\n    const initial = [\n      { maxWidth: 479 },\n      { minWidth: 1920 },\n      {},\n      { maxWidth: 991 },\n      { minWidth: 1280 },\n    ];\n    const result = groupBreakpoints(initial);\n    expect(result.widthBased).toStrictEqual([\n      { minWidth: 1920 },\n      { minWidth: 1280 },\n      {},\n      { maxWidth: 991 },\n      { maxWidth: 479 },\n    ]);\n    expect(result.custom).toStrictEqual([]);\n  });\n\n  test(\"handles only min-width breakpoints\", () => {\n    const initial = [{ minWidth: 768 }, { minWidth: 1024 }];\n    const result = groupBreakpoints(initial);\n    expect(result.widthBased).toStrictEqual([\n      { minWidth: 1024 },\n      { minWidth: 768 },\n    ]);\n    expect(result.custom).toStrictEqual([]);\n  });\n\n  test(\"handles only max-width breakpoints\", () => {\n    const initial = [{ maxWidth: 991 }, { maxWidth: 479 }];\n    const result = groupBreakpoints(initial);\n    expect(result.widthBased).toStrictEqual([\n      { maxWidth: 991 },\n      { maxWidth: 479 },\n    ]);\n    expect(result.custom).toStrictEqual([]);\n  });\n\n  test(\"handles only base breakpoint\", () => {\n    const initial = [{}];\n    const result = groupBreakpoints(initial);\n    expect(result.widthBased).toStrictEqual([{}]);\n    expect(result.custom).toStrictEqual([]);\n  });\n\n  test(\"handles empty array\", () => {\n    const result = groupBreakpoints([]);\n    expect(result.widthBased).toStrictEqual([]);\n    expect(result.custom).toStrictEqual([]);\n  });\n\n  test(\"separates custom condition breakpoints from width-based\", () => {\n    const initial = [\n      { minWidth: 1920 },\n      { condition: \"orientation: portrait\" },\n      { minWidth: 1280 },\n      {},\n      { condition: \"hover: hover\" },\n      { maxWidth: 991 },\n    ];\n    const result = groupBreakpoints(initial);\n    expect(result.widthBased).toStrictEqual([\n      { minWidth: 1920 },\n      { minWidth: 1280 },\n      {},\n      { maxWidth: 991 },\n    ]);\n    expect(result.custom).toStrictEqual([\n      { condition: \"orientation: portrait\" },\n      { condition: \"hover: hover\" },\n    ]);\n  });\n});\n\ndescribe(\"buildMergedBreakpointIds\", () => {\n  test(\"merges breakpoints with matching minWidth, maxWidth, and label\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [\n      { id: \"frag-1\", minWidth: 768, label: \"Tablet\" },\n      { id: \"frag-2\", maxWidth: 479, label: \"Mobile\" },\n      { id: \"frag-3\", minWidth: 1024, label: \"Desktop\" },\n    ];\n\n    const existingBreakpoints: Breakpoints = new Map([\n      [\"exist-1\", { id: \"exist-1\", minWidth: 768, label: \"Tablet\" }],\n      [\"exist-2\", { id: \"exist-2\", maxWidth: 991, label: \"Small Desktop\" }],\n      [\"exist-3\", { id: \"exist-3\", maxWidth: 479, label: \"Mobile\" }],\n    ]);\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    expect(result.size).toBe(2);\n    expect(result.get(\"frag-1\")).toBe(\"exist-1\"); // Tablet matches\n    expect(result.get(\"frag-2\")).toBe(\"exist-3\"); // Mobile matches\n    expect(result.has(\"frag-3\")).toBe(false); // Desktop doesn't match\n  });\n\n  test(\"returns empty map when no matches found\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [\n      { id: \"frag-1\", minWidth: 1440, label: \"Large Desktop\" },\n    ];\n\n    const existingBreakpoints: Breakpoints = new Map([\n      [\"exist-1\", { id: \"exist-1\", minWidth: 768, label: \"Tablet\" }],\n    ]);\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    expect(result.size).toBe(0);\n  });\n\n  test(\"matches base breakpoints (no min/max)\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [\n      { id: \"frag-base\", label: \"Base\" },\n    ];\n\n    const existingBreakpoints: Breakpoints = new Map([\n      [\"exist-base\", { id: \"exist-base\", label: \"Base\" }],\n    ]);\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    expect(result.size).toBe(1);\n    expect(result.get(\"frag-base\")).toBe(\"exist-base\");\n  });\n\n  test(\"handles empty fragment breakpoints\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [];\n    const existingBreakpoints: Breakpoints = new Map([\n      [\"exist-1\", { id: \"exist-1\", minWidth: 768, label: \"Tablet\" }],\n    ]);\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    expect(result.size).toBe(0);\n  });\n\n  test(\"handles empty existing breakpoints\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [\n      { id: \"frag-1\", minWidth: 768, label: \"Tablet\" },\n    ];\n    const existingBreakpoints: Breakpoints = new Map();\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    expect(result.size).toBe(0);\n  });\n\n  test(\"merges only first matching breakpoint\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [\n      { id: \"frag-1\", minWidth: 768, label: \"Tablet\" },\n    ];\n\n    const existingBreakpoints: Breakpoints = new Map([\n      [\"exist-1\", { id: \"exist-1\", minWidth: 768, label: \"Tablet\" }],\n      [\"exist-2\", { id: \"exist-2\", minWidth: 768, label: \"Tablet\" }],\n    ]);\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    expect(result.size).toBe(1);\n    expect(result.get(\"frag-1\")).toBe(\"exist-1\"); // Should match first one\n  });\n\n  test(\"does not merge breakpoints with different labels\", () => {\n    const fragmentBreakpoints: Breakpoint[] = [\n      { id: \"frag-1\", minWidth: 768, label: \"Tablet\" },\n    ];\n\n    const existingBreakpoints: Breakpoints = new Map([\n      [\"exist-1\", { id: \"exist-1\", minWidth: 768, label: \"iPad\" }],\n    ]);\n\n    const result = buildMergedBreakpointIds(\n      fragmentBreakpoints,\n      existingBreakpoints\n    );\n\n    // equalMedia only checks minWidth/maxWidth, not labels\n    // So breakpoints with same dimensions but different labels will still merge\n    expect(result.size).toBe(1);\n    expect(result.get(\"frag-1\")).toBe(\"exist-1\");\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/breakpoints-utils.ts",
    "content": "import { compareMedia, equalMedia } from \"@webstudio-is/css-engine\";\nimport type { Breakpoint, Breakpoints } from \"@webstudio-is/sdk\";\n\n/**\n * Check if a breakpoint is the base breakpoint (no min or max width).\n * Note: Does not check for custom conditions.\n */\nexport const isBaseBreakpoint = (breakpoint: {\n  minWidth?: number;\n  maxWidth?: number;\n}) => breakpoint.minWidth === undefined && breakpoint.maxWidth === undefined;\n\n/**\n * Group breakpoints into width-based and custom condition categories.\n * Width-based breakpoints are ordered: min-width (largest to smallest), base, max-width (largest to smallest).\n * Custom condition breakpoints are kept separate and not sorted.\n *\n * Note: minWidth/maxWidth and condition are mutually exclusive - a breakpoint has either\n * width-based properties OR a custom condition, never both.\n */\nexport const groupBreakpoints = <\n  T extends { minWidth?: number; maxWidth?: number; condition?: string },\n>(\n  breakpoints: Array<T>\n): { widthBased: Array<T>; custom: Array<T> } => {\n  const custom = breakpoints.filter(\n    (breakpoint) => breakpoint.condition !== undefined\n  );\n  const widthBased = breakpoints.filter(\n    (breakpoint) => breakpoint.condition === undefined\n  );\n\n  const sorted = [...widthBased].sort(compareMedia);\n  const maxs = sorted.filter((breakpoint) => breakpoint.maxWidth !== undefined);\n  const mins = sorted\n    .filter((breakpoint) => breakpoint.minWidth !== undefined)\n    .reverse();\n  const base = sorted.filter(isBaseBreakpoint);\n\n  return {\n    widthBased: [...mins, ...base, ...maxs],\n    custom,\n  };\n};\n\n/**\n * Build a map of merged breakpoint IDs from fragment breakpoints to existing breakpoints.\n * Breakpoints are merged when they have matching minWidth, maxWidth, condition, and label.\n * Note: minWidth/maxWidth and condition are mutually exclusive.\n *\n * @param fragmentBreakpoints - Breakpoints from the fragment being inserted\n * @param existingBreakpoints - Existing breakpoints in the project\n * @returns Map of fragment breakpoint IDs to existing breakpoint IDs\n */\nexport const buildMergedBreakpointIds = (\n  fragmentBreakpoints: Breakpoint[],\n  existingBreakpoints: Breakpoints\n): Map<Breakpoint[\"id\"], Breakpoint[\"id\"]> => {\n  const mergedBreakpointIds = new Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>();\n  for (const newBreakpoint of fragmentBreakpoints) {\n    for (const breakpoint of existingBreakpoints.values()) {\n      if (equalMedia(breakpoint, newBreakpoint)) {\n        mergedBreakpointIds.set(newBreakpoint.id, breakpoint.id);\n        break;\n      }\n    }\n  }\n  return mergedBreakpointIds;\n};\n\n// Doesn't make sense to allow resizing the canvas lower than this.\nexport const minCanvasWidth = 240;\n"
  },
  {
    "path": "apps/builder/app/shared/builder-api.ts",
    "content": "import { createRecursiveProxy } from \"@trpc/server/shared\";\nimport invariant from \"tiny-invariant\";\nimport { toast } from \"@webstudio-is/design-system\";\nimport { uploadAssets } from \"~/builder/shared/assets/upload-assets\";\nimport { showTokenConflictDialog } from \"./token-conflict-dialog\";\n\nconst apiWindowNamespace = \"__webstudio__$__builderApi\";\n\ntype ToastHandler = (message: string) => void;\n\nconst isSafeMode = (() => {\n  if (typeof window === \"undefined\") {\n    return false;\n  }\n  return new URLSearchParams(window.location.search).get(\"safemode\") === \"true\";\n})();\n\nconst _builderApi = {\n  isInitialized: () => true,\n  isSafeMode: () => isSafeMode,\n  toast: {\n    info: toast.info as ToastHandler,\n    warn: toast.warn as ToastHandler,\n    error: toast.error as ToastHandler,\n    success: toast.success as ToastHandler,\n  },\n  uploadImages: async (srcs: string[]) => {\n    const urlToIds = await uploadAssets(\n      \"image\",\n      srcs.map((src) => new URL(src))\n    );\n\n    return new Map([...urlToIds.entries()].map(([url, id]) => [url.href, id]));\n  },\n  showTokenConflictDialog,\n};\n\ndeclare global {\n  interface Window {\n    [apiWindowNamespace]: typeof _builderApi;\n  }\n}\n\nconst isInTop = () => {\n  try {\n    return window.self === window.top;\n  } catch {\n    return true;\n  }\n};\n\nconst getTopApi = () => {\n  if (isInTop()) {\n    // Inside the iframe, use the local window.api\n    return _builderApi;\n  } else {\n    // Find first iframe with the API\n    invariant(window.top);\n    return window.top[apiWindowNamespace];\n  }\n};\n\nconst isKeyOf = <T>(key: unknown, obj: T): key is keyof T => {\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore\n  return key in obj;\n};\n\n/**\n * Forwards the call from the builder to the iframe, invoking the original API in the iframe.\n */\nexport const builderApi = createRecursiveProxy((options) => {\n  const api = getTopApi();\n\n  if (api == null) {\n    if (\n      options.path.join(\".\") ===\n      (\"isInitialized\" satisfies keyof typeof _builderApi)\n    ) {\n      return false;\n    }\n\n    console.warn(\n      `API not found in the iframe, skipping ${options.path.join(\".\")} call, iframe probably not loaded yet`\n    );\n    return null;\n  }\n\n  let currentMethod = api as unknown;\n\n  for (const key of options.path) {\n    invariant(\n      isKeyOf(key, currentMethod),\n      `API method ${options.path.join(\".\")} not found`\n    );\n    invariant(typeof currentMethod === \"object\");\n    invariant(currentMethod != null);\n\n    currentMethod = currentMethod[key];\n  }\n\n  invariant(\n    typeof currentMethod === \"function\",\n    `API method ${options.path.join(\".\")} is not a function`\n  );\n\n  return currentMethod.call(null, ...options.args);\n}) as typeof _builderApi;\n\n/**\n * Initializes the builder API in the window. Must be called in the builder context.\n */\nexport const initBuilderApi = () => {\n  if (isInTop()) {\n    window[apiWindowNamespace] = _builderApi;\n  }\n  return () => {};\n};\n"
  },
  {
    "path": "apps/builder/app/shared/builder-data.ts",
    "content": "import { getStyleDeclKey, type WebstudioData } from \"@webstudio-is/sdk\";\nimport type { MarketplaceProduct } from \"@webstudio-is/project-build\";\nimport type { Project } from \"@webstudio-is/project\";\nimport type { loader } from \"~/routes/rest.data.$projectId\";\nimport {\n  $project,\n  $assets,\n  $breakpoints,\n  $dataSources,\n  $instances,\n  $marketplaceProduct,\n  $pages,\n  $props,\n  $resources,\n  $styleSourceSelections,\n  $styleSources,\n  $styles,\n} from \"./nano-states\";\nimport { fetch } from \"~/shared/fetch.client\";\n\nexport type BuilderData = WebstudioData & {\n  marketplaceProduct: undefined | MarketplaceProduct;\n  project: Project;\n};\n\nexport type LoadedBuilderData = BuilderData &\n  Pick<\n    Awaited<ReturnType<typeof loader>>,\n    \"id\" | \"version\" | \"publisherHost\" | \"projectId\"\n  >;\n\nexport const getBuilderData = (): BuilderData => {\n  const pages = $pages.get();\n  if (pages === undefined) {\n    throw Error(`Cannot get webstudio data with empty pages`);\n  }\n  const project = $project.get();\n  if (project === undefined) {\n    throw Error(`Cannot get webstudio data with empty project`);\n  }\n  return {\n    pages,\n    project,\n    instances: $instances.get(),\n    props: $props.get(),\n    dataSources: $dataSources.get(),\n    resources: $resources.get(),\n    breakpoints: $breakpoints.get(),\n    styleSourceSelections: $styleSourceSelections.get(),\n    styleSources: $styleSources.get(),\n    styles: $styles.get(),\n    assets: $assets.get(),\n    marketplaceProduct: $marketplaceProduct.get(),\n  };\n};\n\nconst getPair = <Item extends { id: string }>(item: Item) =>\n  [item.id, item] as const;\n\nexport const loadBuilderData = async ({\n  projectId,\n  signal,\n}: {\n  projectId: string;\n  signal: AbortSignal;\n}): Promise<LoadedBuilderData> => {\n  const currentUrl = new URL(location.href);\n  const url = new URL(`/rest/data/${projectId}`, currentUrl.origin);\n  const headers = new Headers();\n\n  const response = await fetch(url, { headers, signal });\n\n  if (response.ok) {\n    const data = (await response.json()) as Awaited<ReturnType<typeof loader>>;\n    return {\n      id: data.id,\n      version: data.version,\n      projectId: data.projectId,\n      project: data.project,\n      publisherHost: data.publisherHost,\n      assets: new Map(data.assets.map(getPair)),\n      instances: new Map(data.instances.map(getPair)),\n      dataSources: new Map(data.dataSources.map(getPair)),\n      resources: new Map(data.resources.map(getPair)),\n      props: new Map(data.props.map(getPair)),\n      pages: data.pages,\n      breakpoints: new Map(data.breakpoints.map(getPair)),\n      styleSources: new Map(data.styleSources.map(getPair)),\n      styleSourceSelections: new Map(\n        data.styleSourceSelections.map((item) => [item.instanceId, item])\n      ),\n      styles: new Map(data.styles.map((item) => [getStyleDeclKey(item), item])),\n      marketplaceProduct: data.marketplaceProduct,\n    };\n  }\n\n  const text = await response.text();\n\n  // No toasts available in this context\n  alert(\n    `Unable to load builder data. Response status: ${response.status}. Response text: ${text}`\n  );\n\n  throw Error(\n    `Unable to load builder data. Response status: ${response.status}. Response text: ${text}`\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/canvas-api.ts",
    "content": "import { preventUnhandled } from \"@atlaskit/pragmatic-drag-and-drop/prevent-unhandled\";\nimport { setInert, resetInert } from \"../canvas/shared/inert\";\nimport { monitorForExternal } from \"@atlaskit/pragmatic-drag-and-drop/external/adapter\";\nimport { createRecursiveProxy } from \"@trpc/server/shared\";\nimport invariant from \"tiny-invariant\";\nimport { $canvasIframeState } from \"./nano-states\";\nimport { detectSupportedFontWeights } from \"~/canvas/shared/font-weight-support\";\n\nconst apiWindowNamespace = \"__webstudio__$__canvasApi\";\n\nconst _canvasApi = {\n  isInitialized: () => true,\n  setInert,\n  resetInert,\n  preventUnhandled,\n  monitorForExternal,\n  detectSupportedFontWeights,\n};\n\ndeclare global {\n  interface Window {\n    [apiWindowNamespace]: typeof _canvasApi;\n  }\n}\n\nconst isInIframe = () => {\n  try {\n    return window.self !== window.top;\n  } catch (error) {\n    return true;\n  }\n};\n\nconst getIframeApi = () => {\n  if (isInIframe()) {\n    // Inside the iframe, use the local window.api\n    return _canvasApi;\n  } else {\n    // Find first iframe with the API\n    for (let i = 0; i < window.frames.length; ++i) {\n      try {\n        const frame = window.frames[i];\n        if (frame && frame[apiWindowNamespace]) {\n          return frame[apiWindowNamespace];\n        }\n      } catch {\n        // Certain extensions, such as Zotero, inject iframes into the page\n        // These iframes can be inaccessible and may cause access errors\n        // Therefore, we should skip processing them\n      }\n    }\n\n    return;\n  }\n};\n\nconst isKeyOf = <T>(key: unknown, obj: T): key is keyof T => {\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore\n  return key in obj;\n};\n\n/**\n * Forwards the call from the builder to the iframe, invoking the original API in the iframe.\n */\nexport const canvasApi = createRecursiveProxy((options) => {\n  const api = getIframeApi();\n\n  if (api == null) {\n    if (\n      options.path.join(\".\") ===\n      (\"isInitialized\" satisfies keyof typeof _canvasApi)\n    ) {\n      return false;\n    }\n\n    console.warn(\n      `API not found in the iframe, skipping ${options.path.join(\".\")} call, iframe probably not loaded yet`\n    );\n    return null;\n  }\n\n  let currentMethod = api as unknown;\n\n  for (const key of options.path) {\n    invariant(\n      isKeyOf(key, currentMethod),\n      `API method ${options.path.join(\".\")} not found`\n    );\n    invariant(typeof currentMethod === \"object\");\n    invariant(currentMethod != null);\n\n    currentMethod = currentMethod[key];\n  }\n\n  invariant(\n    typeof currentMethod === \"function\",\n    `API method ${options.path.join(\".\")} is not a function`\n  );\n\n  return currentMethod.call(null, ...options.args);\n}) as typeof _canvasApi;\n\n/**\n * Initializes the canvas API in the iframe. Must be called in the iframe context.\n */\nexport const initCanvasApi = () => {\n  if (isInIframe()) {\n    $canvasIframeState.set(\"ready\");\n    window[apiWindowNamespace] = _canvasApi;\n  }\n  return () => {\n    // Does not work as expected, because the iframe is detached from the builder\n    $canvasIframeState.set(\"idle\");\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/client-only.ts",
    "content": "import { type ReactNode, useSyncExternalStore } from \"react\";\n\nexport const ClientOnly = ({\n  fallback,\n  children,\n}: {\n  fallback?: ReactNode;\n  children: ReactNode;\n}) => {\n  // https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store\n  const isServer = useSyncExternalStore(\n    () => () => {},\n    () => false,\n    () => true\n  );\n  if (isServer) {\n    return fallback;\n  }\n  return children;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/client-supports.ts",
    "content": "import { useSyncExternalStore } from \"react\";\n\nconst subscribe = () => () => {};\n\n// https://react.dev/reference/react/useSyncExternalStore#subscribing-to-a-browser-api\n// Get a value from the browser API on the first (non hydration) render (without using useEffect or similar methods)\n// Ensures the value is consistent between server and client (hydration)\nexport const useClientSupports = (isSupported: () => boolean) => {\n  const result = useSyncExternalStore(subscribe, isSupported, () => false);\n\n  return result;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/clone-project.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Box,\n  Button,\n  Flex,\n  Label,\n  Text,\n  theme,\n  InputField,\n  DialogContent,\n  DialogTitle,\n  DialogClose,\n  Dialog,\n  DialogActions,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport { Title, type Project } from \"@webstudio-is/project\";\nimport { nativeClient } from \"./trpc/trpc-client\";\nimport { useEffectEvent } from \"./hook-utils/effect-event\";\n\nconst useCloneProjectWithDialog = ({\n  projectId,\n  onCreate,\n  authToken,\n}: {\n  projectId: Project[\"id\"];\n  authToken?: string;\n  onCreate: (projectId: Project[\"id\"]) => void;\n}) => {\n  const [state, setState] = useState<\"idle\" | \"loading\" | \"submitting\">(\"idle\");\n  const [errors, setErrors] = useState<string>();\n\n  const handleSubmit = async ({ title }: { title: string }) => {\n    const parsed = Title.safeParse(title);\n    const errors =\n      \"error\" in parsed\n        ? parsed.error?.issues.map((issue) => issue.message).join(\"\\n\")\n        : undefined;\n\n    setErrors(errors);\n\n    if (parsed.success) {\n      try {\n        setState(\"submitting\");\n\n        const data = await nativeClient.project.clone.mutate({\n          projectId,\n          title,\n          authToken,\n        });\n\n        setState(\"idle\");\n\n        onCreate(data.id);\n      } catch (error) {\n        toast.error(error instanceof Error ? error.message : \"Unknown error\");\n        setState(\"idle\");\n      }\n    }\n  };\n\n  return {\n    handleSubmit,\n    errors,\n    state,\n  };\n};\n\nconst CloneProjectView = ({\n  isOpen,\n  title,\n  errors,\n  state,\n  onOpenChange,\n  onSubmit,\n}: {\n  isOpen: boolean;\n  title: string;\n  errors?: string;\n  state: \"idle\" | \"loading\" | \"submitting\";\n  onOpenChange: (isOpen: boolean) => void;\n  onSubmit: ({ title }: { title: string }) => void;\n}) => {\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange}>\n      <DialogContent>\n        <form\n          onSubmit={(event) => {\n            event.preventDefault();\n            const formData = new FormData(\n              event.currentTarget as HTMLFormElement\n            );\n            const title = String(formData.get(\"title\") ?? \"\");\n            onSubmit({ title });\n          }}\n        >\n          <Flex\n            direction=\"column\"\n            css={{\n              px: theme.spacing[\"7\"],\n              paddingTop: theme.spacing[\"5\"],\n            }}\n            gap=\"1\"\n          >\n            <Label>Project title</Label>\n            <InputField\n              name=\"title\"\n              defaultValue={title}\n              color={errors ? \"error\" : undefined}\n            />\n            <Box css={{ minHeight: theme.spacing[\"10\"] }}>\n              {errors && <Text color=\"destructive\">{errors}</Text>}\n            </Box>\n          </Flex>\n          <DialogActions>\n            <Button\n              type=\"submit\"\n              state={state === \"idle\" ? undefined : \"pending\"}\n            >\n              Clone\n            </Button>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n          </DialogActions>\n        </form>\n        <DialogTitle>Clone project</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const CloneProjectDialog = ({\n  isOpen,\n  project: { id, title },\n  onOpenChange,\n  authToken,\n  onCreate,\n}: {\n  isOpen: boolean;\n  project: Pick<Project, \"id\" | \"title\">;\n  authToken?: string;\n  onOpenChange: (isOpen: boolean) => void;\n  onCreate: (projectId: Project[\"id\"]) => void;\n}) => {\n  const handleOnCreate = useEffectEvent((projectId: Project[\"id\"]) => {\n    onCreate(projectId);\n    onOpenChange(false);\n  });\n\n  const { handleSubmit, errors, state } = useCloneProjectWithDialog({\n    projectId: id,\n    authToken,\n    onCreate: handleOnCreate,\n  });\n\n  return (\n    <CloneProjectView\n      title={title}\n      onSubmit={handleSubmit}\n      errors={errors}\n      state={state}\n      isOpen={isOpen}\n      onOpenChange={onOpenChange}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/code-editor-base.tsx",
    "content": "import {\n  useEffect,\n  useRef,\n  type ReactNode,\n  forwardRef,\n  type ComponentProps,\n  type RefObject,\n  useImperativeHandle,\n} from \"react\";\nimport {\n  Annotation,\n  EditorState,\n  StateEffect,\n  type Extension,\n} from \"@codemirror/state\";\nimport {\n  EditorView,\n  drawSelection,\n  dropCursor,\n  keymap,\n} from \"@codemirror/view\";\nimport {\n  defaultKeymap,\n  history,\n  historyKeymap,\n  indentWithTab,\n} from \"@codemirror/commands\";\nimport { foldGutter, syntaxHighlighting } from \"@codemirror/language\";\nimport {\n  theme,\n  textVariants,\n  css,\n  SmallIconButton,\n  Grid,\n  Flex,\n  rawTheme,\n  globalCss,\n  Kbd,\n  Text,\n  FloatingPanel,\n} from \"@webstudio-is/design-system\";\nimport { MaximizeIcon } from \"@webstudio-is/icons\";\nimport { ChevronDownIcon, ChevronRightIcon } from \"@webstudio-is/icons/svg\";\nimport { solarizedLight } from \"./code-highlight\";\n\n// This undocumented flag is required to keep contenteditable fields editable after the first activation of EditorView.\n// To reproduce the issue, open any Binding dialog and then try to edit a Navigation Item in the Navigation menu.\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-expect-error\nEditorView.EDIT_CONTEXT = false;\n\nconst ExternalChange = Annotation.define<boolean>();\n\nconst minHeightVar = \"--ws-code-editor-min-height\";\nconst maxHeightVar = \"--ws-code-editor-max-height\";\nconst maximizeIconVisibilityVar = \"--ws-code-editor-maximize-icon-visibility\";\n\nexport const getCodeEditorCssVars = ({\n  minHeight,\n  maxHeight,\n}: {\n  minHeight: string;\n  maxHeight: string;\n}) => ({\n  [minHeightVar]: minHeight,\n  [maxHeightVar]: maxHeight,\n});\n\nconst globalStyles = globalCss({\n  \"fieldset[disabled] .cm-editor\": {\n    opacity: 0.3,\n  },\n});\n\nconst editorContentStyle = css({\n  ...textVariants.mono,\n  // fit editor into parent if stretched\n  display: \"flex\",\n  position: \"relative\",\n  minHeight: 0,\n  boxSizing: \"border-box\",\n  color: theme.colors.foregroundMain,\n  borderRadius: theme.borderRadius[4],\n  border: `1px solid transparent`,\n  background: theme.colors.backgroundControls,\n  paddingTop: 4,\n  paddingBottom: 2,\n  paddingRight: theme.spacing[2],\n  paddingLeft: theme.spacing[3],\n  // required to support copying selected text\n  userSelect: \"text\",\n  \"&:hover\": {\n    borderColor: theme.colors.borderMain,\n  },\n  \"&:focus-within\": {\n    borderColor: theme.colors.borderFocus,\n  },\n  '&[data-invalid=\"true\"]': {\n    borderColor: theme.colors.borderDestructiveMain,\n    outlineColor: theme.colors.borderDestructiveMain,\n  },\n  \"& .cm-focused\": {\n    outline: \"none\",\n  },\n  // fix scrolls appear on mount\n  \"& .cm-scroller\": {\n    overflowX: \"hidden\",\n  },\n  \"& .cm-content\": {\n    padding: 0,\n    // makes sure you can click to focus when editor content is smaller than the container\n    minHeight: \"100%\",\n  },\n  \"& .cm-line\": {\n    padding: 0,\n  },\n  \"& .cm-editor\": {\n    width: \"100%\",\n    // avoid modifying height in .cm-content\n    // because it breaks scroll events and makes scrolling laggy\n    minHeight: `var(${minHeightVar}, auto)`,\n    maxHeight: `var(${maxHeightVar}, none)`,\n  },\n  \".cm-lintRange-error\": {\n    textDecoration: \"underline wavy red\",\n    backgroundColor: \"rgba(255, 0, 0, 0.1)\",\n  },\n  \".cm-lintRange-warning\": {\n    textDecoration: \"underline wavy orange\",\n    backgroundColor: \"rgba(255, 0, 0, 0.1)\",\n  },\n  \".cm-gutters\": {\n    backgroundColor: \"transparent\",\n    border: 0,\n  },\n});\n\nconst shortcutStyle = css({\n  position: \"absolute\",\n  left: 0,\n  bottom: 0,\n  width: \"100%\",\n  paddingInline: theme.spacing[3],\n  background: \"oklch(100% 0 0 / 50%)\",\n  zIndex: 1,\n  pointerEvents: \"none\",\n});\n\nconst autocompletionTooltipTheme = EditorView.theme({\n  \".cm-tooltip.cm-tooltip-autocomplete\": {\n    ...textVariants.mono,\n    border: \"none\",\n    backgroundColor: \"transparent\",\n    // override none set on body by radix popover\n    pointerEvents: \"auto\",\n  },\n  \".cm-tooltip.cm-tooltip-autocomplete ul\": {\n    minWidth: \"160px\",\n    maxWidth: \"260px\",\n    width: \"max-content\",\n    boxSizing: \"border-box\",\n    borderRadius: rawTheme.borderRadius[6],\n    backgroundColor: rawTheme.colors.backgroundMenu,\n    border: `1px solid ${rawTheme.colors.borderMain}`,\n    boxShadow: `${rawTheme.shadows.menuDropShadow}, inset 0 0 0 1px ${rawTheme.colors.borderMenuInner}`,\n    padding: rawTheme.spacing[3],\n  },\n  \".cm-tooltip.cm-tooltip-autocomplete ul li\": {\n    ...textVariants.labels,\n    textTransform: \"none\",\n    position: \"relative\",\n    display: \"flex\",\n    alignItems: \"center\",\n    color: rawTheme.colors.foregroundMain,\n    padding: rawTheme.spacing[3],\n    borderRadius: rawTheme.borderRadius[3],\n  },\n  \".cm-tooltip.cm-tooltip-autocomplete li[aria-selected], .cm-tooltip.cm-tooltip-autocomplete li:hover\":\n    {\n      color: rawTheme.colors.foregroundMain,\n      backgroundColor: rawTheme.colors.backgroundItemMenuItemHover,\n    },\n  \".cm-tooltip.cm-tooltip-autocomplete .cm-completionLabel\": {\n    flexGrow: 1,\n  },\n  \".cm-tooltip.cm-tooltip-autocomplete .cm-completionDetail\": {\n    overflow: \"hidden\",\n    textOverflow: \"ellipsis\",\n    fontStyle: \"normal\",\n    color: rawTheme.colors.foregroundSubtle,\n  },\n});\n\nconst keyBindings = [\n  ...defaultKeymap.filter((binding) => {\n    // We are redefining it later and CodeMirror won't take an override\n    return binding.key !== \"Mod-Enter\";\n  }),\n  ...historyKeymap,\n  indentWithTab,\n];\n\nexport const foldGutterExtension = foldGutter({\n  markerDOM: (isOpen) => {\n    const div = document.createElement(\"div\");\n    div.style.width = \"16px\";\n    div.style.height = \"16px\";\n    div.style.cursor = \"pointer\";\n    div.innerHTML = isOpen ? ChevronDownIcon : ChevronRightIcon;\n    return div;\n  },\n});\n\nexport type EditorApi = {\n  replaceSelection: (string: string) => void;\n  focus: () => void;\n};\n\ntype EditorContentProps = {\n  editorApiRef?: RefObject<undefined | EditorApi>;\n  extensions?: Extension[];\n  readOnly?: boolean;\n  autoFocus?: boolean;\n  invalid?: boolean;\n  showShortcuts?: boolean;\n  value: string;\n  onChange: (value: string) => void;\n  onChangeComplete: (value: string) => void;\n};\n\nexport const EditorContent = ({\n  editorApiRef,\n  extensions = [],\n  readOnly = false,\n  autoFocus = false,\n  invalid = false,\n  showShortcuts = false,\n  value,\n  onChange,\n  onChangeComplete,\n}: EditorContentProps) => {\n  globalStyles();\n\n  const editorRef = useRef<HTMLDivElement>(null);\n  const viewRef = useRef<undefined | EditorView>(undefined);\n\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n  const onChangeCompleteRef = useRef(onChangeComplete);\n  onChangeCompleteRef.current = onChangeComplete;\n\n  useEffect(() => {\n    const abortController = new AbortController();\n\n    document.addEventListener(\n      // https://github.com/radix-ui/primitives/blob/dac4fd8ab0c1974020e316c865db258ab10d2279/packages/react/dismissable-layer/src/DismissableLayer.tsx#L14\n      \"dismissableLayer.pointerDownOutside\",\n      (event) => {\n        if (\n          event.target instanceof Element &&\n          // Prevent radix dialogs and popups from closing when clicking on the editor's autocomplete items\n          event.target.closest(\".cm-tooltip.cm-tooltip-autocomplete\")\n        ) {\n          event.preventDefault();\n        }\n      },\n      {\n        capture: true,\n        signal: abortController.signal,\n      }\n    );\n    return () => {\n      abortController.abort();\n    };\n  }, []);\n\n  // setup editor\n  useEffect(() => {\n    if (editorRef.current === null) {\n      return;\n    }\n    const view = new EditorView({\n      doc: \"\",\n      parent: editorRef.current,\n    });\n\n    viewRef.current = view;\n    return () => {\n      view.destroy();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (autoFocus) {\n      viewRef.current?.focus();\n    }\n  }, [autoFocus]);\n\n  // update extensions whenever variables data is changed\n\n  useEffect(() => {\n    const view = viewRef.current;\n    if (view === undefined) {\n      return;\n    }\n    const hasDisabledFieldset =\n      editorRef.current?.closest(\"fieldset[disabled]\");\n\n    view.dispatch({\n      effects: StateEffect.reconfigure.of([\n        ...extensions,\n        ...(hasDisabledFieldset ? [EditorView.editable.of(false)] : []),\n        autocompletionTooltipTheme,\n        history(),\n        drawSelection(),\n        dropCursor(),\n        syntaxHighlighting(solarizedLight, { fallback: true }),\n        keymap.of([\n          ...keyBindings,\n          {\n            key: \"Mod-Enter\",\n            run(view) {\n              onChangeCompleteRef.current(view.state.doc.toString());\n              return true;\n            },\n          },\n          {\n            key: \"Mod-s\",\n            run(view) {\n              onChangeCompleteRef.current(view.state.doc.toString());\n              return true;\n            },\n          },\n        ]),\n        EditorView.lineWrapping,\n        EditorView.editable.of(readOnly === false),\n        EditorState.readOnly.of(readOnly === true),\n        // https://github.com/uiwjs/react-codemirror/blob/5d7a37245ce70e61f215b77dc42a7eaf295c46e7/core/src/useCodeMirror.ts#L57-L70\n        EditorView.updateListener.of((update) => {\n          if (\n            // prevent invoking callback when focus or selection is changed\n            update.docChanged &&\n            // prevent invoking callback when the change came from react value\n            update.transactions.some((trx) =>\n              trx.annotation(ExternalChange)\n            ) === false\n          ) {\n            onChangeRef.current(update.state.doc.toString());\n          }\n        }),\n        EditorView.domEventHandlers({\n          blur() {\n            onChangeCompleteRef.current(view.state.doc.toString());\n          },\n          cut(event) {\n            // prevent catching cut by global copy paste\n            // with target outside of contenteditable\n            event.stopPropagation();\n          },\n        }),\n      ]),\n    });\n  }, [readOnly, extensions]);\n\n  // update editor with react value\n  // https://github.com/uiwjs/react-codemirror/blob/5d7a37245ce70e61f215b77dc42a7eaf295c46e7/core/src/useCodeMirror.ts#L158-L169\n  useEffect(() => {\n    const view = viewRef.current;\n    if (view === undefined) {\n      return;\n    }\n    // prevent updating when editor has the same state\n    // and can be the source of new value\n    if (value === view.state.doc.toString()) {\n      return;\n    }\n\n    view.dispatch({\n      changes: { from: 0, to: view.state.doc.length, insert: value },\n      annotations: [ExternalChange.of(true)],\n    });\n  }, [value]);\n\n  useImperativeHandle(editorApiRef, () => ({\n    replaceSelection: (string) => {\n      const view = viewRef.current;\n      if (view === undefined) {\n        return;\n      }\n\n      view.dispatch(view.state.replaceSelection(string));\n      view.focus();\n    },\n    focus() {\n      viewRef.current?.focus();\n    },\n  }));\n\n  return (\n    <div\n      className={editorContentStyle()}\n      data-invalid={invalid}\n      ref={editorRef}\n    >\n      {showShortcuts && (\n        <Flex align=\"center\" justify=\"end\" gap=\"1\" className={shortcutStyle()}>\n          <Text variant=\"small\">Submit</Text>\n          <Kbd value={[\"meta\", \"enter\"]} />\n        </Flex>\n      )}\n    </div>\n  );\n};\n\nconst editorDialogControlStyle = css({\n  position: \"relative\",\n  \"&:hover\": {\n    [maximizeIconVisibilityVar]: \"visible\",\n  },\n});\n\nexport const EditorDialogControl = ({ children }: { children: ReactNode }) => {\n  return <div className={editorDialogControlStyle()}>{children}</div>;\n};\n\nexport const EditorDialogButton = forwardRef<\n  HTMLButtonElement,\n  Partial<ComponentProps<typeof SmallIconButton>>\n>((props, ref) => {\n  return (\n    <SmallIconButton\n      {...props}\n      ref={ref}\n      icon={<MaximizeIcon />}\n      css={{\n        position: \"absolute\",\n        top: 4,\n        right: 4,\n        visibility: `var(${maximizeIconVisibilityVar}, hidden)`,\n        background: \"oklch(100% 0 0 / 50%)\",\n      }}\n    />\n  );\n});\nEditorDialogButton.displayName = \"EditorDialogButton\";\n\nexport const EditorDialog = ({\n  content,\n  children,\n  placement = \"center\",\n  width = 640,\n  height = 480,\n  ...panelProps\n}: {\n  title: ReactNode;\n  content: ReactNode;\n  children: ReactNode;\n  width?: number;\n  height?: number;\n  placement?: ComponentProps<typeof FloatingPanel>[\"placement\"];\n  resize?: ComponentProps<typeof FloatingPanel>[\"resize\"];\n  open?: boolean;\n  onOpenChange?: (newOpen: boolean) => void;\n}) => {\n  return (\n    <FloatingPanel\n      {...panelProps}\n      width={width}\n      height={height}\n      placement={placement}\n      maximizable\n      resize=\"both\"\n      content={\n        <Grid\n          align=\"stretch\"\n          css={{\n            padding: theme.panel.padding,\n            height: \"100%\",\n            overflow: \"hidden\",\n            boxSizing: \"content-box\",\n          }}\n        >\n          {content}\n        </Grid>\n      }\n    >\n      {children}\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/code-editor.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { CodeEditor as CodeEditorComponent } from \"./code-editor\";\n\nexport default {\n  title: \"Code editor\",\n  component: CodeEditorComponent,\n};\n\nconst initialHtml = `\n<div>\n  <a href=\"#\">Click me</a>\n</div>\n<script>\n  runMyJavascript()\n</script>\n`.trim();\n\nconst BaseEditorDemo = () => {\n  const [value, setValue] = useState(initialHtml);\n  return (\n    <CodeEditorComponent\n      value={value}\n      onChange={setValue}\n      onChangeComplete={setValue}\n    />\n  );\n};\n\nconst HtmlEditorDemo = () => {\n  const [value, setValue] = useState(initialHtml);\n  return (\n    <CodeEditorComponent\n      value={value}\n      onChange={setValue}\n      onChangeComplete={setValue}\n      lang=\"html\"\n    />\n  );\n};\n\nexport const CodeEditor = () => (\n  <>\n    <StorySection title=\"Base editor\">\n      <BaseEditorDemo />\n    </StorySection>\n    <StorySection title=\"HTML editor\">\n      <HtmlEditorDemo />\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "apps/builder/app/shared/code-editor.tsx",
    "content": "import {\n  forwardRef,\n  useMemo,\n  type ComponentProps,\n  useEffect,\n  type ReactNode,\n} from \"react\";\nimport { styleTags, tags } from \"@lezer/highlight\";\nimport {\n  keymap,\n  tooltips,\n  highlightSpecialChars,\n  highlightActiveLine,\n} from \"@codemirror/view\";\nimport {\n  bracketMatching,\n  indentOnInput,\n  LanguageSupport,\n  LRLanguage,\n} from \"@codemirror/language\";\nimport {\n  autocompletion,\n  closeBrackets,\n  closeBracketsKeymap,\n  completionKeymap,\n} from \"@codemirror/autocomplete\";\nimport { html } from \"@codemirror/lang-html\";\nimport { markdown } from \"@codemirror/lang-markdown\";\nimport { css } from \"@webstudio-is/design-system\";\nimport {\n  EditorContent,\n  EditorDialog,\n  EditorDialogButton,\n  EditorDialogControl,\n  foldGutterExtension,\n  getCodeEditorCssVars,\n} from \"~/shared/code-editor-base\";\nimport { cssCompletionSource, cssLanguage } from \"@codemirror/lang-css\";\n\nconst wrapperStyle = css({\n  position: \"relative\",\n\n  variants: {\n    size: {\n      default: getCodeEditorCssVars({ minHeight: \"160px\", maxHeight: \"320px\" }),\n      small: getCodeEditorCssVars({ minHeight: \"16px\", maxHeight: \"120px\" }),\n    },\n  },\n  defaultVariants: {\n    size: \"default\",\n  },\n});\n\nconst getHtmlExtensions = () => [\n  highlightActiveLine(),\n  highlightSpecialChars(),\n  indentOnInput(),\n  html({}),\n  bracketMatching(),\n  closeBrackets(),\n  // render autocomplete in body\n  // to prevent popover scroll overflow\n  tooltips({ parent: document.body }),\n  autocompletion({ icons: false }),\n  keymap.of([...closeBracketsKeymap, ...completionKeymap]),\n];\n\nconst getMarkdownExtensions = () => [\n  highlightActiveLine(),\n  highlightSpecialChars(),\n  indentOnInput(),\n  markdown({\n    extensions: [\n      {\n        props: [\n          styleTags({\n            HorizontalRule: tags.separator,\n            HeaderMark: tags.annotation,\n            QuoteMark: tags.annotation,\n            ListMark: tags.annotation,\n            LinkMark: tags.annotation,\n            EmphasisMark: tags.annotation,\n            CodeMark: tags.annotation,\n            InlineCode: tags.string,\n            URL: tags.url,\n          }),\n        ],\n      },\n    ],\n  }),\n  bracketMatching(),\n  closeBrackets(),\n  keymap.of(closeBracketsKeymap),\n];\n\nconst cssPropertiesLanguage = LRLanguage.define({\n  name: \"css\",\n  parser: cssLanguage.configure({ top: \"Styles\" }).parser,\n});\nconst cssProperties = new LanguageSupport(\n  cssPropertiesLanguage,\n  cssPropertiesLanguage.data.of({\n    autocomplete: cssCompletionSource,\n  })\n);\n\nconst getCssPropertiesExtensions = () => [\n  highlightActiveLine(),\n  highlightSpecialChars(),\n  indentOnInput(),\n  cssProperties,\n  // render autocomplete in body\n  // to prevent popover scroll overflow\n  tooltips({ parent: document.body }),\n  autocompletion({ icons: false }),\n];\n\nexport const CodeEditor = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentProps<typeof EditorContent>, \"extensions\"> & {\n    lang?: \"html\" | \"markdown\" | \"css-properties\";\n    title?: ReactNode;\n    size?: \"default\" | \"small\";\n  }\n>(({ lang, title, size, ...editorContentProps }, ref) => {\n  const extensions = useMemo(() => {\n    if (lang === \"html\") {\n      return getHtmlExtensions();\n    }\n\n    if (lang === \"markdown\") {\n      return getMarkdownExtensions();\n    }\n\n    if (lang === \"css-properties\") {\n      return getCssPropertiesExtensions();\n    }\n\n    if (lang === undefined) {\n      return [];\n    }\n\n    lang satisfies never;\n\n    return [];\n  }, [lang]);\n\n  const dialogExtensions = useMemo(\n    () => [...extensions, foldGutterExtension],\n    [extensions]\n  );\n\n  // prevent clicking on autocomplete options propagating to body\n  // and closing dialogs and popovers\n  useEffect(() => {\n    const handlePointerDown = (event: PointerEvent) => {\n      if (\n        event.target instanceof HTMLElement &&\n        event.target.closest(\".cm-tooltip-autocomplete\")\n      ) {\n        event.stopPropagation();\n      }\n    };\n    const options = { capture: true };\n    document.addEventListener(\"pointerdown\", handlePointerDown, options);\n    return () => {\n      document.removeEventListener(\"pointerdown\", handlePointerDown, options);\n    };\n  }, []);\n  return (\n    <div className={wrapperStyle({ size })} ref={ref}>\n      <EditorDialogControl>\n        <EditorContent {...editorContentProps} extensions={extensions} />\n        <EditorDialog\n          title={title}\n          content={\n            <EditorContent\n              {...editorContentProps}\n              extensions={dialogExtensions}\n            />\n          }\n        >\n          <EditorDialogButton />\n        </EditorDialog>\n      </EditorDialogControl>\n    </div>\n  );\n});\n\nCodeEditor.displayName = \"CodeEditor\";\n"
  },
  {
    "path": "apps/builder/app/shared/code-highlight.ts",
    "content": "import { HighlightStyle } from \"@codemirror/language\";\nimport { highlightCode, tags } from \"@lezer/highlight\";\nimport { parser } from \"@lezer/css\";\n\n// inspired by https://thememirror.net/solarized-light\nexport const solarizedLight = HighlightStyle.define([\n  {\n    tag: tags.comment,\n    color: \"#93A1A1\",\n  },\n  {\n    tag: tags.string,\n    color: \"#2AA198\",\n  },\n  {\n    tag: tags.regexp,\n    color: \"#D30102\",\n  },\n  {\n    tag: tags.number,\n    color: \"#D33682\",\n  },\n  {\n    tag: tags.variableName,\n    color: \"#268BD2\",\n  },\n  {\n    tag: [tags.keyword, tags.operator, tags.punctuation],\n    color: \"#859900\",\n  },\n  {\n    tag: [tags.definitionKeyword, tags.modifier],\n    color: \"#073642\",\n  },\n  {\n    tag: [tags.self, tags.definition(tags.propertyName)],\n    color: \"#268BD2\",\n  },\n  {\n    tag: tags.function(tags.variableName),\n    color: \"#268BD2\",\n  },\n  {\n    tag: [tags.bool, tags.null],\n    color: \"#B58900\",\n  },\n  {\n    tag: tags.tagName,\n    color: \"#268BD2\",\n  },\n  {\n    tag: tags.angleBracket,\n    color: \"#93A1A1\",\n  },\n  {\n    tag: tags.attributeName,\n    color: \"#93A1A1\",\n  },\n  {\n    tag: tags.typeName,\n    color: \"#859900\",\n  },\n]);\n\nexport const highlightCss = (code: string) => {\n  const styles = solarizedLight.module?.getRules();\n  // generated classes are scoped to parent\n  let highlightedCode = `<style>@scope {${styles}}</style>`;\n  highlightCode(\n    code,\n    parser.parse(code),\n    solarizedLight,\n    (text, classes) => {\n      if (classes) {\n        highlightedCode += `<span class=\"${classes}\">${text}</span>`;\n      } else {\n        highlightedCode += text;\n      }\n    },\n    () => {\n      highlightedCode += \"\\n\";\n    }\n  );\n  return highlightedCode;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/commands-emitter.ts",
    "content": "import { atom } from \"nanostores\";\nimport { $publisher, subscribe } from \"~/shared/pubsub\";\nimport { clientSyncStore } from \"~/shared/sync/sync-stores\";\n\nexport type CommandMeta<CommandName extends string> = {\n  name: CommandName;\n  label?: string;\n  /** brief description shown in keyboard shortcuts dialog */\n  description?: string;\n  /** category for grouping commands in UI for shortcuts dialog */\n  category?: \"General\" | \"Panels\" | \"Navigator\" | \"Top bar\" | \"Style panel\";\n  /** default because hotkeys can be customized from ui */\n  defaultHotkeys?: string[];\n  /** set to false when default browser or radix behavior is desired */\n  preventDefault?: boolean;\n  /** listen hotkeys only locally without sharing with other apps */\n  disableHotkeyOutsideApp?: boolean;\n  /**\n   * input, select and textarea, content editable and role=option used in Radix will not invoke command when hotkey is hit\n   * with the exception when default event behavior is prevented\n   **/\n  disableOnInputLikeControls?: boolean;\n  /**\n   * hide the command in meta+k panel\n   */\n  hidden?: boolean;\n  /**\n   * keep command panel open after executing the command\n   */\n  keepCommandPanelOpen?: boolean;\n};\n\ntype CommandHandler = () => void;\n\n/**\n * Command can be registered by builder, canvas or plugin\n */\nexport type Command<CommandName extends string> = CommandMeta<CommandName> & {\n  /**\n   * Command handler accepting source where was triggered\n   * which is builder, canvas or plugin name\n   */\n  handler: CommandHandler;\n};\n\n/*\n * expose command metas to synchronize between builder, canvas and plugins\n */\nexport const $commandMetas = atom(new Map<string, CommandMeta<string>>());\nclientSyncStore.register(\"commandMetas\", $commandMetas);\n\n// Copied from https://github.com/ai/keyux/blob/main/hotkey.js#L1C1-L2C1\n// eslint-disable-next-line no-control-regex\nconst nonEnglishLayout = /^[^\\x00-\\x7F]$/;\n\nconst getKey = (event: KeyboardEvent) => {\n  if (nonEnglishLayout.test(event.key) && /^Key.$/.test(event.code)) {\n    return event.code.replace(/^Key/, \"\").toLowerCase();\n  }\n  return event.key;\n};\n\nconst findCommandsMatchingHotkeys = (event: KeyboardEvent) => {\n  const { ctrlKey, metaKey, shiftKey, altKey } = event;\n  const key = getKey(event);\n  const pressedKeys = new Set<string>();\n  pressedKeys.add(key.toLocaleLowerCase());\n  if (ctrlKey) {\n    pressedKeys.add(\"ctrl\");\n  }\n  if (metaKey) {\n    pressedKeys.add(\"meta\");\n  }\n  if (shiftKey) {\n    pressedKeys.add(\"shift\");\n  }\n  if (altKey) {\n    pressedKeys.add(\"alt\");\n  }\n\n  const commandMetas = $commandMetas.get();\n  const matchingCommands = new Set<CommandMeta<string>>();\n  for (const commandMeta of commandMetas.values()) {\n    if (commandMeta.defaultHotkeys === undefined) {\n      continue;\n    }\n    for (const hotkey of commandMeta.defaultHotkeys) {\n      const keys = hotkey.toLocaleLowerCase().split(\"+\");\n      if (\n        keys.length === pressedKeys.size &&\n        keys.every((key) => pressedKeys.has(key))\n      ) {\n        matchingCommands.add(commandMeta);\n      }\n    }\n  }\n  return matchingCommands;\n};\n\nexport const createCommandsEmitter = <CommandName extends string>({\n  source,\n  commands,\n}: {\n  source: string;\n  // type only input to describe available commands from builder or other plugins\n  externalCommands?: CommandName[];\n  commands: Command<CommandName>[];\n}) => {\n  const commandHandlers = new Map<string, CommandHandler>();\n  for (const { handler, ...meta } of commands) {\n    commandHandlers.set(meta.name, handler);\n  }\n\n  const emitCommand = (name: CommandName) => {\n    const { publish } = $publisher.get();\n    // continue to work without emitter\n    // for example in tests\n    if (publish) {\n      publish({\n        type: \"command\",\n        payload: {\n          source,\n          name,\n        },\n      });\n    } else {\n      commandHandlers.get(name)?.();\n    }\n  };\n\n  /**\n   * subscribe to keydown in every app and emit command globally\n   * actual handlers are executed in app where defined\n   */\n  const subscribeCommands = () => {\n    // synchronize commands with other apps\n    // whenever current app is initialized\n    if (commands.length > 0) {\n      clientSyncStore.createTransaction([$commandMetas], (commandMetas) => {\n        for (const { handler, ...meta } of commands) {\n          commandMetas.set(meta.name, meta);\n        }\n      });\n    }\n\n    const unsubscribePubsub = subscribe(\"command\", ({ name }) => {\n      commandHandlers.get(name)?.();\n    });\n    const handleKeyDown = (event: KeyboardEvent) => {\n      let emitted = false;\n      let preventDefault = true;\n      for (const commandMeta of findCommandsMatchingHotkeys(event)) {\n        if (\n          commandMeta.disableHotkeyOutsideApp &&\n          commandHandlers.has(commandMeta.name) === false\n        ) {\n          continue;\n        }\n\n        const { disableOnInputLikeControls } = commandMeta;\n\n        if (disableOnInputLikeControls) {\n          const element = event.target as HTMLElement;\n          const isOnInputLikeControl =\n            [\"input\", \"select\", \"textarea\"].includes(\n              element.tagName.toLowerCase()\n            ) ||\n            element.isContentEditable ||\n            // Detect Radix select, dropdown and co.\n            element.getAttribute(\"role\") === \"option\";\n\n          if (isOnInputLikeControl) {\n            continue;\n          }\n        }\n\n        emitted = true;\n        if (commandMeta.preventDefault === false) {\n          preventDefault = false;\n        }\n        emitCommand(commandMeta.name as CommandName);\n      }\n      // command can redefine browser hotkeys\n      // prevent to avoid unexpected behavior\n      // unless at least one matching command disabled prevent default\n      if (emitted && preventDefault) {\n        event.preventDefault();\n      }\n    };\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => {\n      unsubscribePubsub();\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  };\n\n  return {\n    emitCommand,\n    subscribeCommands,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/content-model.test.tsx",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { coreMetas } from \"@webstudio-is/sdk\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport { $, expression, renderData, ws } from \"@webstudio-is/template\";\nimport {\n  findClosestContainer,\n  findClosestNonTextualContainer,\n  findClosestRichText,\n  isRichTextTree,\n  isTreeSatisfyingContentModel,\n} from \"./content-model\";\n\nconst defaultMetas = new Map(\n  Object.entries({ ...coreMetas, ...baseComponentMetas })\n);\n\ntest(\"support element with ws:tag\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"span\">\n            <ws.element ws:tag=\"article\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"support Box with ws:tag\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Box ws:tag=\"span\">\n            <$.Box ws:tag=\"article\"></$.Box>\n          </$.Box>\n        </$.Body>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"support legacy tag property\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Box tag=\"span\">\n            <$.Box tag=\"article\"></$.Box>\n          </$.Box>\n        </$.Body>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"flow accepts flow\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"article\"></ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"none category accepted by parent by tag\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"ul\">\n            <ws.element ws:tag=\"li\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"none category prevents unacceptable parent\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"li\"></ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"ul\">\n            <ws.element ws:tag=\"div\">\n              <ws.element ws:tag=\"li\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"slot without tag accepts transparent category\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"ul\">\n            <$.Slot>\n              <$.Fragment>\n                <ws.element ws:tag=\"li\"></ws.element>\n              </$.Fragment>\n            </$.Slot>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"collection without tag accepts transparent category\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"ul\">\n            <ws.collection>\n              <ws.element ws:tag=\"li\"></ws.element>\n            </ws.collection>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"transparent category accepts flow\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"a\">\n            <ws.element ws:tag=\"article\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"phrasing category accepts element with transparent children\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"span\">\n            <ws.element ws:tag=\"a\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"transparent category should pass through phrasing category and forbid flow inside\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"span\">\n            <ws.element ws:tag=\"a\">\n              <ws.element ws:tag=\"span\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"span\">\n            <ws.element ws:tag=\"a\">\n              <ws.element ws:tag=\"article\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"transparent category should not pass through invalid parent\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"ul\">\n            <ws.element ws:tag=\"a\">\n              <ws.element ws:tag=\"li\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"transparent category should pass through category when check deep in the tree\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n            <ws.element ws:tag=\"a\" ws:id=\"linkId\">\n              <ws.element ws:tag=\"strong\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"linkId\", \"spanId\", \"bodyId\"],\n    })\n  ).toBeTruthy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n            <ws.element ws:tag=\"a\" ws:id=\"linkId\">\n              <ws.element ws:tag=\"article\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"linkId\", \"spanId\", \"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"restrict empty category\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"hr\"></ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"hr\">\n            <ws.element ws:tag=\"span\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"prevent nesting interactive instances\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"button\">\n            <ws.element ws:tag=\"button\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"button\">\n            <ws.element ws:tag=\"span\">\n              <ws.element ws:tag=\"a\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"prevent nesting interactive instances with slots in between\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"button\">\n            <$.Slot>\n              <$.Fragment>\n                <ws.element ws:tag=\"textarea\"></ws.element>\n              </$.Fragment>\n            </$.Slot>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"prevent nesting interactive instances when check deep in the tree\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"button\" ws:id=\"buttonId\">\n            <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n              <ws.element ws:tag=\"a\" ws:id=\"linkId\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"linkId\", \"spanId\", \"buttonId\", \"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"prevent nesting forms\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"form\">\n            <ws.element ws:tag=\"button\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"form\" ws:id=\"formId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\">\n              <ws.element ws:tag=\"form\" ws:id=\"anotherFormId\">\n                <ws.element ws:tag=\"button\"></ws.element>\n              </ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"anotherFormId\", \"divId\", \"formId\", \"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"allow wrapping labelable controls with label\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"label\">\n            <ws.element ws:tag=\"span\">\n              <ws.element ws:tag=\"input\"></ws.element>\n            </ws.element>\n          </ws.element>\n          <ws.element ws:tag=\"label\">\n            <ws.element ws:tag=\"span\">\n              <ws.element ws:tag=\"button\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"label\">\n            <ws.element ws:tag=\"span\">\n              <ws.element ws:tag=\"a\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"label\">\n            <ws.element ws:tag=\"button\">\n              <ws.element ws:tag=\"input\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeFalsy();\n});\n\ntest(\"edge case: allow inserting div where phrasing is required\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"button\">\n            <ws.element ws:tag=\"div\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"edge case: support a > img\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"a\">\n            <ws.element ws:tag=\"img\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"support video > source\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"video\">\n            <ws.element ws:tag=\"source\" />\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"support xml node with tags\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.XmlNode tag=\"url\">\n            <$.XmlNode tag=\"loc\"></$.XmlNode>\n          </$.XmlNode>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"support headings inside of summary\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"details\">\n            <ws.element ws:tag=\"summary\">\n              <ws.element ws:tag=\"h3\"></ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ntest(\"support links inside of details\", () => {\n  expect(\n    isTreeSatisfyingContentModel({\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"details\">\n            <ws.element ws:tag=\"a\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ),\n      metas: defaultMetas,\n      instanceSelector: [\"bodyId\"],\n    })\n  ).toBeTruthy();\n});\n\ndescribe(\"component content model\", () => {\n  test(\"restrict children with specific component\", () => {\n    expect(\n      isTreeSatisfyingContentModel({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <$.HtmlEmbed>\n              <ws.descendant />\n            </$.HtmlEmbed>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"bodyId\"],\n      })\n    ).toBeTruthy();\n    expect(\n      isTreeSatisfyingContentModel({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <$.HtmlEmbed>\n              <ws.element ws:tag=\"div\" />\n            </$.HtmlEmbed>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"bodyId\"],\n      })\n    ).toBeFalsy();\n  });\n\n  test(\"restrict components within specific ancestor\", () => {\n    expect(\n      isTreeSatisfyingContentModel({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <$.Vimeo>\n              <$.VimeoSpinner></$.VimeoSpinner>\n            </$.Vimeo>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"bodyId\"],\n      })\n    ).toBeTruthy();\n    expect(\n      isTreeSatisfyingContentModel({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <$.Vimeo>\n              <ws.element ws:tag=\"div\">\n                <$.VimeoSpinner></$.VimeoSpinner>\n              </ws.element>\n            </$.Vimeo>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"bodyId\"],\n      })\n    ).toBeTruthy();\n    expect(\n      isTreeSatisfyingContentModel({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <$.VimeoSpinner></$.VimeoSpinner>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"bodyId\"],\n      })\n    ).toBeFalsy();\n  });\n\n  test(\"pass constraints when check deep in the tree\", () => {\n    expect(\n      isTreeSatisfyingContentModel({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <$.Vimeo ws:id=\"vimeoId\">\n              <ws.element ws:tag=\"div\" ws:id=\"divId\">\n                <$.VimeoSpinner></$.VimeoSpinner>\n              </ws.element>\n            </$.Vimeo>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"divId\", \"vimeoId\", \"bodyId\"],\n      })\n    ).toBeTruthy();\n  });\n});\n\ndescribe(\"rich text tree\", () => {\n  test(\"check empty instance is rich text\", () => {\n    expect(\n      isRichTextTree({\n        ...renderData(<$.Bold ws:id=\"instanceId\"></$.Bold>),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeTruthy();\n    expect(\n      isRichTextTree({\n        ...renderData(<$.HeadSlot ws:id=\"instanceId\"></$.HeadSlot>),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeFalsy();\n  });\n\n  test(\"any instance with text can be edited\", () => {\n    expect(\n      isRichTextTree({\n        ...renderData(<$.HeadSlot ws:id=\"instanceId\">my text</$.HeadSlot>),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeTruthy();\n    expect(\n      isRichTextTree({\n        ...renderData(\n          <$.HeadSlot ws:id=\"instanceId\">{expression``}</$.HeadSlot>\n        ),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeTruthy();\n  });\n\n  test(\"rich text content tags can be edited\", () => {\n    expect(\n      isRichTextTree({\n        ...renderData(\n          <$.Bold ws:id=\"instanceId\">\n            <$.Italic></$.Italic>\n          </$.Bold>\n        ),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeTruthy();\n  });\n\n  test(\"rich text instances with rich text content can be edited\", () => {\n    expect(\n      isRichTextTree({\n        ...renderData(\n          <$.Paragraph ws:id=\"instanceId\">\n            <$.Bold>bold</$.Bold>\n          </$.Paragraph>\n        ),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeTruthy();\n    expect(\n      isRichTextTree({\n        ...renderData(\n          <$.HeadSlot ws:id=\"instanceId\">\n            <$.Bold>bold</$.Bold>\n          </$.HeadSlot>\n        ),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeFalsy();\n  });\n\n  test(\"div with paragraph cannot be rich text\", () => {\n    expect(\n      isRichTextTree({\n        ...renderData(\n          <$.Box ws:id=\"instanceId\">\n            <$.Paragraph></$.Paragraph>\n          </$.Box>\n        ),\n        metas: defaultMetas,\n        instanceId: \"instanceId\",\n      })\n    ).toBeFalsy();\n  });\n\n  test(\"finds closest rich text with rich text content\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"p\" ws:id=\"paragraphId\">\n              <ws.element ws:tag=\"b\" ws:id=\"boldId\"></ws.element>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"boldId\", \"paragraphId\", \"bodyId\"],\n      })\n    ).toEqual([\"paragraphId\", \"bodyId\"]);\n  });\n\n  test(\"finds closest rich text with rich text content\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"p\" ws:id=\"paragraphId\">\n              text\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"paragraphId\", \"bodyId\"],\n      })\n    ).toEqual([\"paragraphId\", \"bodyId\"]);\n  });\n\n  test(\"treat Link component as container when look for closest rich text\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n              <$.Link ws:id=\"linkId\">\n                <$.Bold ws:id=\"boldId\">link</$.Bold>\n              </$.Link>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"linkId\", \"spanId\", \"bodyId\"],\n      })\n    ).toEqual([\"linkId\", \"spanId\", \"bodyId\"]);\n  });\n\n  test(\"treat body as rich text when has text inside\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            text\n            <ws.element ws:tag=\"p\" ws:id=\"paragraphId\">\n              <ws.element ws:tag=\"b\" ws:id=\"boldId\"></ws.element>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"boldId\", \"paragraphId\", \"bodyId\"],\n      })\n    ).toEqual([\"bodyId\"]);\n  });\n\n  test(\"ignore <a> with <div> inside\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"a\" ws:id=\"linkId\">\n          <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n        </ws.element>\n      </ws.element>\n    );\n    expect(\n      findClosestRichText({\n        ...data,\n        metas: defaultMetas,\n        instanceSelector: [\"divId\", \"linkId\", \"bodyId\"],\n      })\n    ).toEqual([\"divId\", \"linkId\", \"bodyId\"]);\n    expect(\n      findClosestRichText({\n        ...data,\n        metas: defaultMetas,\n        instanceSelector: [\"linkId\", \"bodyId\"],\n      })\n    ).toEqual(undefined);\n  });\n\n  test(\"finds link rich text\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\">\n              <ws.element ws:tag=\"a\" ws:id=\"linkId\">\n                my link\n              </ws.element>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"linkId\", \"divId\", \"bodyId\"],\n      })\n    ).toEqual([\"linkId\", \"divId\", \"bodyId\"]);\n  });\n\n  test(\"finds span rich text\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\">\n              <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n                my span\n              </ws.element>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"spanId\", \"divId\", \"bodyId\"],\n      })\n    ).toEqual([\"spanId\", \"divId\", \"bodyId\"]);\n  });\n\n  test(\"finds rich text with mixed children\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\">\n              <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n                my link\n              </ws.element>\n              <ws.element ws:tag=\"b\" ws:id=\"boldId\">\n                my link\n              </ws.element>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"spanId\", \"divId\", \"bodyId\"],\n      })\n    ).toEqual([\"divId\", \"bodyId\"]);\n  });\n\n  test(\"does not treat image component as rich text\", () => {\n    expect(\n      findClosestRichText({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\">\n              <$.Image ws:id=\"imgId\" />\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"imgId\", \"divId\", \"bodyId\"],\n      })\n    ).toEqual(undefined);\n  });\n});\n\ndescribe(\"closest container\", () => {\n  test(\"skips non-container instances\", () => {\n    expect(\n      findClosestContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Image ws:id=\"imageId\" />\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"imageId\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"boxId\", \"bodyId\"]);\n  });\n\n  test(\"allow containers with text\", () => {\n    expect(\n      findClosestContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Box ws:id=\"box-with-text\">text</$.Box>\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"box-with-text\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"box-with-text\", \"boxId\", \"bodyId\"]);\n  });\n\n  test(\"allow containers with expression\", () => {\n    expect(\n      findClosestContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Box ws:id=\"box-with-expr\">{expression`1 + 1`}</$.Box>\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"box-with-expr\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"box-with-expr\", \"boxId\", \"bodyId\"]);\n  });\n\n  test(\"allow root with text\", () => {\n    expect(\n      findClosestContainer({\n        ...renderData(<$.Body ws:id=\"bodyId\">text</$.Body>),\n        metas: defaultMetas,\n        instanceSelector: [\"bodyId\"],\n      })\n    ).toEqual([\"bodyId\"]);\n  });\n});\n\ndescribe(\"closest non textual container\", () => {\n  test(\"skips image tag\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Image ws:id=\"imageId\" />\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"imageId\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"boxId\", \"bodyId\"]);\n  });\n\n  test(\"skips CodeText component\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.CodeText ws:id=\"codeId\" />\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"codeId\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"boxId\", \"bodyId\"]);\n  });\n\n  test(\"skips containers with text\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Box ws:id=\"box-with-text\">text</$.Box>\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"box-with-text\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"boxId\", \"bodyId\"]);\n  });\n\n  test(\"skips containers with expression\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Box ws:id=\"box-with-expr\">{expression`1 + 1`}</$.Box>\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"box-with-expr\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"boxId\", \"bodyId\"]);\n  });\n\n  test(\"skips containers with rich text children\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">\n              <$.Box ws:id=\"box-with-bold\">\n                <$.Bold ws:id=\"boldId\"></$.Bold>\n              </$.Box>\n            </$.Box>\n          </$.Body>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"box-with-bold\", \"boxId\", \"bodyId\"],\n      })\n    ).toEqual([\"boxId\", \"bodyId\"]);\n  });\n\n  test(\"allow root with text\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(<$.Body ws:id=\"body\">text</$.Body>),\n        metas: defaultMetas,\n        instanceSelector: [\"body\"],\n      })\n    ).toEqual([\"body\"]);\n  });\n\n  test(\"matches empty div\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"divId\", \"bodyId\"],\n      })\n    ).toEqual([\"divId\", \"bodyId\"]);\n  });\n\n  test(\"treat Link component as rich text container\", () => {\n    expect(\n      findClosestNonTextualContainer({\n        ...renderData(\n          <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n            <ws.element ws:tag=\"span\" ws:id=\"spanId\">\n              <$.Link ws:id=\"linkId\">\n                <$.Bold ws:id=\"boldId\">link</$.Bold>\n              </$.Link>\n            </ws.element>\n          </ws.element>\n        ),\n        metas: defaultMetas,\n        instanceSelector: [\"boldId\", \"linkId\", \"spanId\", \"bodyId\"],\n      })\n    ).toEqual([\"spanId\", \"bodyId\"]);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/content-model.ts",
    "content": "import { elementsByTag } from \"@webstudio-is/html-data\";\nimport {\n  elementComponent,\n  parseComponentName,\n  type ContentModel,\n  type Instance,\n  type Instances,\n  type Props,\n  type WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport type { InstanceSelector } from \"./tree-utils\";\nimport { setIsSubsetOf } from \"./shim\";\n\ntype Metas = Map<Instance[\"component\"], WsComponentMeta>;\n\nconst tagByInstanceIdCache = new WeakMap<Props, Map<Instance[\"id\"], string>>();\n\nconst getTagByInstanceId = (props: Props) => {\n  let tagByInstanceId = tagByInstanceIdCache.get(props);\n  if (tagByInstanceId === undefined) {\n    tagByInstanceId = new Map<Instance[\"id\"], string>();\n    for (const prop of props.values()) {\n      if (prop.type === \"string\" && prop.name === \"tag\") {\n        tagByInstanceId.set(prop.instanceId, prop.value);\n      }\n    }\n    tagByInstanceIdCache.set(props, tagByInstanceId);\n  }\n  return tagByInstanceId;\n};\n\nconst getTag = ({\n  instance,\n  metas,\n  props,\n}: {\n  instance: Instance;\n  metas: Metas;\n  props: Props;\n}) => {\n  // ignore tag property on xml nodes\n  if (instance.component === \"XmlNode\") {\n    return;\n  }\n  const meta = metas.get(instance.component);\n  const metaTag = Object.keys(meta?.presetStyle ?? {}).at(0);\n  return instance.tag ?? getTagByInstanceId(props).get(instance.id) ?? metaTag;\n};\n\nconst isIntersected = (arrayA: string[], arrayB: string[]) => {\n  return arrayA.some((item) => arrayB.includes(item));\n};\n\n/**\n * checks if tag has interactive category\n * though img is an exception and historically its interactivity ignored\n * so img can be put into links and buttons\n */\nconst isTagInteractive = (tag: string) => {\n  return tag !== \"img\" && elementsByTag[tag].categories.includes(\"interactive\");\n};\n\nconst isTagSatisfyingContentModel = ({\n  tag,\n  allowedCategories,\n}: {\n  tag: undefined | string;\n  allowedCategories: undefined | string[];\n}) => {\n  // slot or collection does not have tag and should pass through allowed categories\n  if (tag === undefined) {\n    return true;\n  }\n  // body does not have parent\n  if (allowedCategories === undefined) {\n    return true;\n  }\n  // for example ul has \"li\" as children category\n  if (allowedCategories.includes(tag)) {\n    return true;\n  }\n  // very big hack to support putting div into buttons or headings\n  // users put \"Embed HTML\" all over the place to embed icons\n  // radix templates do it as well\n  if (allowedCategories.includes(\"phrasing\") && tag === \"div\") {\n    return true;\n  }\n  // interactive exception, label > input or label > button are considered\n  // valid way to nest interactive elements\n  if (\n    allowedCategories.includes(\"labelable\") &&\n    elementsByTag[tag].categories.includes(\"labelable\")\n  ) {\n    return true;\n  }\n  // prevent nesting interactive elements\n  // like button > button or a > input\n  if (allowedCategories.includes(\"non-interactive\") && isTagInteractive(tag)) {\n    return false;\n  }\n  // prevent nesting form elements\n  // like form > div > form\n  if (allowedCategories.includes(\"non-form\") && tag === \"form\") {\n    return false;\n  }\n  // instance matches parent constraints\n  return isIntersected(allowedCategories, elementsByTag[tag].categories);\n};\n\n/**\n * compute possible categories for tag children\n */\nconst getElementChildren = (\n  tag: undefined | string,\n  allowedCategories: undefined | string[]\n) => {\n  // components without tag behave like transparent category\n  // and pass through parent constraints\n  let elementChildren: string[] =\n    tag === undefined ? [\"transparent\"] : elementsByTag[tag].children;\n  if (elementChildren.includes(\"transparent\") && allowedCategories) {\n    // merge categories from parent and current element when transparent occured\n    elementChildren = elementChildren.flatMap((category) =>\n      category === \"transparent\" ? allowedCategories : category\n    );\n  }\n  // introduce custom non-interactive category to restrict nesting interactive elements\n  // like button > button or a > input\n  if (\n    tag &&\n    (isTagInteractive(tag) || allowedCategories?.includes(\"non-interactive\"))\n  ) {\n    elementChildren = [...elementChildren, \"non-interactive\"];\n  }\n  // interactive exception, label > input or label > button are considered\n  // valid way to nest interactive elements\n  // pass through labelable to match controls with labelable category\n  if (tag === \"label\" || allowedCategories?.includes(\"labelable\")) {\n    // stop passing through labelable to control children\n    // to prevent label > button > input\n    if (tag && elementsByTag[tag].categories.includes(\"labelable\") === false) {\n      elementChildren = [...elementChildren, \"labelable\"];\n    }\n  }\n  // introduce custom non-form category to restrict nesting form elements\n  // like form > div > form\n  if (tag === \"form\" || allowedCategories?.includes(\"non-form\")) {\n    elementChildren = [...elementChildren, \"non-form\"];\n  }\n  return elementChildren;\n};\n\n/**\n * compute allowed categories from all ancestors\n * considering inherited (transparent) categories\n * and other ancestor specific behaviors\n */\nconst computeAllowedCategories = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  let instance: undefined | Instance;\n  let allowedCategories: undefined | string[];\n  // skip selected instance for which these constraints are computed\n  for (const instanceId of instanceSelector.slice(1).reverse()) {\n    instance = instances.get(instanceId);\n    // collection item can be undefined\n    if (instance === undefined) {\n      continue;\n    }\n    const tag = getTag({ instance, metas, props });\n    allowedCategories = getElementChildren(tag, allowedCategories);\n  }\n  return allowedCategories;\n};\n\nconst defaultComponentContentModel: ContentModel = {\n  category: \"instance\",\n  children: [\"rich-text\", \"instance\"],\n};\n\nconst getComponentContentModel = (meta: undefined | WsComponentMeta) =>\n  meta?.contentModel ?? defaultComponentContentModel;\n\nconst isComponentSatisfyingContentModel = ({\n  metas,\n  component,\n  allowedParentCategories,\n  allowedAncestorCategories,\n}: {\n  metas: Metas;\n  component: string;\n  allowedParentCategories: undefined | string[];\n  allowedAncestorCategories: undefined | string[];\n}) => {\n  const contentModel = getComponentContentModel(metas.get(component));\n  return (\n    // body does not have parent\n    allowedParentCategories === undefined ||\n    // parents may restrict specific components with none category\n    // any instances\n    // or nothing\n    allowedParentCategories.includes(component) ||\n    allowedParentCategories.includes(contentModel.category) ||\n    allowedAncestorCategories?.includes(component) === true ||\n    allowedAncestorCategories?.includes(contentModel.category) === true\n  );\n};\n\nconst computeAllowedAncestorCategories = ({\n  instances,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  let allowedCategories: undefined | string[];\n  // skip selected instance for which these constraints are computed\n  for (const instanceId of instanceSelector.slice(1).reverse()) {\n    const instance = instances.get(instanceId);\n    // collection item can be undefined\n    if (instance === undefined) {\n      continue;\n    }\n    const contentModel = getComponentContentModel(\n      metas.get(instance.component)\n    );\n    if (contentModel.descendants) {\n      allowedCategories ??= [];\n      allowedCategories = [...allowedCategories, ...contentModel.descendants];\n    }\n  }\n  return allowedCategories;\n};\n\nconst getAllowedParentCategories = ({\n  instances,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  const instanceId = instanceSelector[1];\n  const instance = instances.get(instanceId);\n  if (instance === undefined) {\n    return;\n  }\n  const contentModel = getComponentContentModel(metas.get(instance.component));\n  return contentModel.children;\n};\n\n/**\n * Check all tags starting with specified instance select\n * for example\n *\n * Most rules are described by categoriesByTag and childrenCategoriesByTag\n * from html-data package. Basically all elements enforce children categories\n * and all elements has own categories. We check intersections to match them.\n *\n * See https://html.spec.whatwg.org/multipage/indices.html#elements-3\n *\n * div > span = true\n * where div is flow category\n * and requires flow or phrasing category in children\n * span is flow and phrasing category\n * and requires phrasing in children\n *\n * span > div = false\n * because span requires phrasing category in children\n *\n * p > div = false\n * because paragraph also requires phrasing category in children\n *\n * Interactive categories and form elements are exception\n * because button requires phrasing children\n * and does not prevent nesting interactive elements by content model\n * They pass through negative categories\n *\n * [categories]  [children]\n * interactive   non-interactive\n *\n * exampeles\n * button > input = false\n * form > div > form = false\n *\n */\nexport const isTreeSatisfyingContentModel = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n  onError,\n  _allowedCategories: allowedCategories,\n  _allowedAncestorCategories: allowedAncestorCategories,\n  _allowedParentCategories: allowedParentCategories,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n  onError?: (message: string) => void;\n  _allowedCategories?: string[];\n  _allowedAncestorCategories?: string[];\n  _allowedParentCategories?: string[];\n}): boolean => {\n  // compute constraints only when not passed from parent\n  allowedCategories ??= computeAllowedCategories({\n    instanceSelector,\n    instances,\n    props,\n    metas,\n  });\n  allowedParentCategories ??= getAllowedParentCategories({\n    instanceSelector,\n    instances,\n    metas,\n  });\n  allowedAncestorCategories ??= computeAllowedAncestorCategories({\n    instanceSelector,\n    instances,\n    metas,\n  });\n  const [instanceId, parentInstanceId] = instanceSelector;\n  const instance = instances.get(instanceId);\n  // collection item can be undefined\n  if (instance === undefined) {\n    return true;\n  }\n  const tag = getTag({ instance, metas, props });\n  const isTagSatisfying = isTagSatisfyingContentModel({\n    tag,\n    allowedCategories,\n  });\n  if (isTagSatisfying === false) {\n    const parentInstance = instances.get(parentInstanceId);\n    let parentTag: undefined | string;\n    if (parentInstance) {\n      parentTag = getTag({ instance: parentInstance, metas, props });\n    }\n    if (parentTag) {\n      onError?.(\n        `Placing <${tag}> element inside a <${parentTag}> violates HTML spec.`\n      );\n    } else {\n      onError?.(`Placing <${tag}> element here violates HTML spec.`);\n    }\n  }\n  const isComponentSatisfying = isComponentSatisfyingContentModel({\n    metas,\n    component: instance.component,\n    allowedParentCategories,\n    allowedAncestorCategories,\n  });\n  if (isComponentSatisfying === false) {\n    const [_namespace, name] = parseComponentName(instance.component);\n    const parentInstance = instances.get(parentInstanceId);\n    let parentName: undefined | string;\n    if (parentInstance) {\n      const [_namespace, name] = parseComponentName(parentInstance.component);\n      parentName = name;\n    }\n    if (parentName) {\n      onError?.(\n        `Placing \"${name}\" element inside a \"${parentName}\" violates content model.`\n      );\n    } else {\n      onError?.(`Placing \"${name}\" element here violates content model.`);\n    }\n  }\n  let isSatisfying = isTagSatisfying && isComponentSatisfying;\n  const contentModel = getComponentContentModel(metas.get(instance.component));\n  allowedCategories = getElementChildren(tag, allowedCategories);\n  allowedParentCategories = contentModel.children;\n  if (contentModel.descendants) {\n    allowedAncestorCategories ??= [];\n    allowedAncestorCategories = [\n      ...allowedAncestorCategories,\n      ...contentModel.descendants,\n    ];\n  }\n  for (const child of instance.children) {\n    if (child.type === \"id\") {\n      isSatisfying &&= isTreeSatisfyingContentModel({\n        instances,\n        props,\n        metas,\n        instanceSelector: [child.value, ...instanceSelector],\n        onError,\n        _allowedCategories: allowedCategories,\n        _allowedParentCategories: allowedParentCategories,\n        _allowedAncestorCategories: allowedAncestorCategories,\n      });\n    }\n  }\n  return isSatisfying;\n};\n\nexport const richTextContentTags = new Set<undefined | string>([\n  \"sup\",\n  \"sub\",\n  \"b\",\n  \"strong\",\n  \"i\",\n  \"em\",\n  \"a\",\n  \"span\",\n]);\n\nexport const richTextContentComponents = new Set<undefined | string>([\n  elementComponent,\n  \"Subscript\",\n  \"Bold\",\n  \"Italic\",\n  \"RichTextLink\",\n  \"Span\",\n]);\n\n/**\n * textual placeholder is used when no content specified while in builder\n * also signals to not insert components inside unless dropped explicitly\n */\nexport const richTextPlaceholders: Map<undefined | string, string> = new Map([\n  [\"h1\", \"Heading 1\"],\n  [\"h2\", \"Heading 2\"],\n  [\"h3\", \"Heading 3\"],\n  [\"h4\", \"Heading 4\"],\n  [\"h5\", \"Heading 5\"],\n  [\"h6\", \"Heading 6\"],\n  [\"p\", \"Paragraph\"],\n  [\"blockquote\", \"Blockquote\"],\n  [\"code\", \"Code Text\"],\n  [\"li\", \"List item\"],\n  [\"a\", \"Link\"],\n  [\"span\", \"\"],\n]);\n\nconst findContentTags = ({\n  instances,\n  props,\n  metas,\n  instance,\n  _tags: tags = new Set(),\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instance: Instance;\n  _tags?: Set<undefined | string>;\n}) => {\n  for (const child of instance.children) {\n    if (child.type === \"id\") {\n      const childInstance = instances.get(child.value);\n      // consider collection item as well\n      if (childInstance === undefined) {\n        tags.add(undefined);\n        continue;\n      }\n      const tag = getTag({ instance: childInstance, metas, props });\n      tags.add(tag);\n      findContentTags({\n        instances,\n        props,\n        metas,\n        instance: childInstance,\n        _tags: tags,\n      });\n    }\n  }\n  return tags;\n};\n\nconst findContentComponents = ({\n  instances,\n  instance,\n  _components: components = new Set(),\n}: {\n  instances: Instances;\n  instance: Instance;\n  _components?: Set<undefined | string>;\n}) => {\n  for (const child of instance.children) {\n    if (child.type === \"id\") {\n      const childInstance = instances.get(child.value);\n      // consider collection item as well\n      if (childInstance === undefined) {\n        components.add(undefined);\n        continue;\n      }\n      components.add(childInstance.component);\n      findContentComponents({\n        instances,\n        instance: childInstance,\n        _components: components,\n      });\n    }\n  }\n  return components;\n};\n\nexport const isRichTextTree = ({\n  instanceId,\n  instances,\n  props,\n  metas,\n}: {\n  instanceId: Instance[\"id\"];\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n}): boolean => {\n  const instance = instances.get(instanceId);\n  // collection item is not rich text\n  if (instance === undefined) {\n    return false;\n  }\n  const tag = getTag({ instance, metas, props });\n  const elementContentModel = tag ? elementsByTag[tag] : undefined;\n  const componentContentModel = getComponentContentModel(\n    metas.get(instance.component)\n  );\n  const isRichText =\n    (elementContentModel === undefined ||\n      elementContentModel.children.length > 0) &&\n    componentContentModel.children.includes(\"rich-text\");\n  // only empty instance with rich text content can be edited\n  if (instance.children.length === 0) {\n    return isRichText;\n  }\n  for (const child of instance.children) {\n    if (child.type === \"text\" || child.type === \"expression\") {\n      return true;\n    }\n  }\n  const contentTags = findContentTags({\n    instances,\n    props,\n    metas,\n    instance,\n  });\n  const contentComponents = findContentComponents({\n    instances,\n    instance,\n  });\n  return (\n    isRichText &&\n    // rich text must contain only supported elements in editor\n    setIsSubsetOf(contentTags, richTextContentTags) &&\n    setIsSubsetOf(contentComponents, richTextContentComponents) &&\n    // rich text cannot contain only span and only link\n    // those links and spans are containers in such cases\n    !setIsSubsetOf(contentTags, new Set(richTextPlaceholders.keys()))\n  );\n};\n\nexport const findClosestRichText = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}): undefined | InstanceSelector => {\n  let foundRichText: undefined | InstanceSelector = undefined;\n  for (let index = 0; index < instanceSelector.length; index += 1) {\n    const instanceId = instanceSelector[index];\n    if (!isRichTextTree({ instanceId, instances, props, metas })) {\n      break;\n    }\n    foundRichText = instanceSelector.slice(index);\n  }\n  return foundRichText;\n};\n\nexport const isRichTextContent = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  const richTextSelector = findClosestRichText({\n    instanceSelector,\n    instances,\n    props,\n    metas,\n  });\n  return (\n    richTextSelector && richTextSelector.join() !== instanceSelector.join()\n  );\n};\n\nexport const isRichText = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  const richTextSelector = findClosestRichText({\n    instanceSelector,\n    instances,\n    props,\n    metas,\n  });\n  return (\n    richTextSelector && richTextSelector.join() === instanceSelector.join()\n  );\n};\n\nexport const findClosestContainer = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  // page root with text can be used as container\n  if (instanceSelector.length === 1) {\n    return instanceSelector;\n  }\n  for (let index = 0; index < instanceSelector.length; index += 1) {\n    const instanceId = instanceSelector[index];\n    const instance = instances.get(instanceId);\n    // collection item can be undefined\n    if (instance === undefined) {\n      continue;\n    }\n    const tag = getTag({ instance, props, metas });\n    const meta = metas.get(instance.component);\n    const elementChildren = tag ? elementsByTag[tag].children : undefined;\n    const componentChildren = getComponentContentModel(meta).children;\n    if (\n      componentChildren.length === 0 ||\n      (elementChildren && elementChildren.length === 0)\n    ) {\n      continue;\n    }\n    return instanceSelector.slice(index);\n  }\n  return instanceSelector;\n};\n\nexport const findClosestNonTextualContainer = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Metas;\n  instanceSelector: InstanceSelector;\n}) => {\n  // page root with text can be used as container\n  if (instanceSelector.length === 1) {\n    return instanceSelector;\n  }\n  for (let index = 0; index < instanceSelector.length; index += 1) {\n    const instanceId = instanceSelector[index];\n    const instance = instances.get(instanceId);\n    // collection item can be undefined\n    if (instance === undefined) {\n      continue;\n    }\n    const tag = getTag({ instance, props, metas });\n    const meta = metas.get(instance.component);\n    const elementChildren = tag ? elementsByTag[tag].children : undefined;\n    const componentChildren = getComponentContentModel(meta).children;\n    if (\n      componentChildren.length === 0 ||\n      (elementChildren && elementChildren.length === 0)\n    ) {\n      continue;\n    }\n    if (\n      instance.children.length === 0 &&\n      !richTextPlaceholders.has(tag) &&\n      !richTextContentTags.has(tag)\n    ) {\n      return instanceSelector.slice(index);\n    }\n    // placeholder exists only inside of empty instances\n    let hasText = false;\n    for (const child of instance.children) {\n      if (child.type === \"text\" || child.type === \"expression\") {\n        hasText = true;\n      }\n    }\n    const contentTags = findContentTags({\n      instances,\n      props,\n      metas,\n      instance,\n    });\n    const contentComponents = findContentComponents({\n      instances,\n      instance,\n    });\n    if (\n      setIsSubsetOf(contentTags, richTextContentTags) &&\n      setIsSubsetOf(contentComponents, richTextContentComponents)\n    ) {\n      hasText = true;\n    }\n    if (!hasText) {\n      return instanceSelector.slice(index);\n    }\n  }\n  return instanceSelector;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/context.server.ts",
    "content": "import { type AppContext } from \"@webstudio-is/trpc-interface/index.server\";\nimport env from \"~/env/env.server\";\nimport { authenticator } from \"~/services/auth.server\";\nimport { trpcSharedClient } from \"~/services/trpc.server\";\nimport { entryApi } from \"./entri/entri-api.server\";\n\nimport { getUserPlanFeatures } from \"./db/user-plan-features.server\";\nimport { staticEnv } from \"~/env/env.static.server\";\nimport { createClient } from \"@webstudio-is/postgrest/index.server\";\nimport { builderAuthenticator } from \"~/services/builder-auth.server\";\nimport { readLoginSessionBloomFilter } from \"~/services/session.server\";\nimport type { BloomFilter } from \"~/services/bloom-filter.server\";\nimport { isBuilder, isCanvas } from \"./router-utils\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\n\nexport const extractAuthFromRequest = async (request: Request) => {\n  if (isCanvas(request)) {\n    throw new Error(\"Canvas requests can't have authorization context\");\n  }\n  const url = new URL(request.url);\n\n  const authToken =\n    url.searchParams.get(\"authToken\") ??\n    request.headers.get(\"x-auth-token\") ??\n    undefined;\n\n  const sessionData = isBuilder(request)\n    ? await builderAuthenticator.isAuthenticated(request)\n    : await authenticator.isAuthenticated(request);\n\n  const isServiceCall =\n    request.headers.has(\"Authorization\") &&\n    request.headers.get(\"Authorization\") === env.TRPC_SERVER_API_TOKEN;\n\n  return {\n    authToken,\n    sessionData,\n    isServiceCall,\n  };\n};\n\nconst createTokenAuthorizationContext = async (\n  authToken: string,\n  postgrest: AppContext[\"postgrest\"]\n) => {\n  const projectOwnerIdByToken = await postgrest.client\n    .from(\"AuthorizationToken\")\n    .select(\"project:Project(id, userId)\")\n    .eq(\"token\", authToken)\n    .single();\n\n  if (projectOwnerIdByToken.error) {\n    throw new Error(`Project owner can't be found for token ${authToken}`);\n  }\n\n  const ownerId = projectOwnerIdByToken.data.project?.userId ?? null;\n  if (ownerId === null) {\n    throw new Error(\n      `Project ${projectOwnerIdByToken.data.project?.id} has null userId`\n    );\n  }\n\n  return {\n    type: \"token\" as const,\n    authToken,\n    ownerId,\n  };\n};\n\nconst createAuthorizationContext = async (\n  request: Request,\n  postgrest: AppContext[\"postgrest\"]\n): Promise<AppContext[\"authorization\"]> => {\n  const { authToken, isServiceCall, sessionData } =\n    await extractAuthFromRequest(request);\n\n  if (isServiceCall) {\n    return {\n      type: \"service\",\n      isServiceCall: true,\n    };\n  }\n\n  if (authToken != null) {\n    return await createTokenAuthorizationContext(authToken, postgrest);\n  }\n\n  if (sessionData?.userId != null) {\n    const userId = sessionData.userId;\n\n    let loginBloomFilter: BloomFilter | undefined = undefined;\n\n    let isLoggedInToBuilder = async (projectId: string) => {\n      if (loginBloomFilter === undefined) {\n        loginBloomFilter = await readLoginSessionBloomFilter(request);\n      }\n\n      return loginBloomFilter.has(projectId);\n    };\n\n    if (isBuilder(request)) {\n      isLoggedInToBuilder = async (projectId: string) => {\n        const parsedUrl = parseBuilderUrl(request.url);\n        return parsedUrl.projectId === projectId;\n      };\n    }\n\n    return {\n      type: \"user\",\n      userId,\n      sessionCreatedAt: sessionData.createdAt,\n      isLoggedInToBuilder,\n    };\n  }\n\n  return { type: \"anonymous\" };\n};\n\nconst createDomainContext = () => {\n  const context: AppContext[\"domain\"] = {\n    domainTrpc: trpcSharedClient.domain,\n  };\n\n  return context;\n};\n\nconst getRequestOrigin = (urlStr: string) => {\n  const url = new URL(urlStr);\n\n  return url.origin;\n};\n\nconst createDeploymentContext = (builderOrigin: string) => {\n  const context: AppContext[\"deployment\"] = {\n    deploymentTrpc: trpcSharedClient.deployment,\n    env: {\n      BUILDER_ORIGIN: getRequestOrigin(builderOrigin),\n      GITHUB_REF_NAME: staticEnv.GITHUB_REF_NAME ?? \"undefined\",\n      GITHUB_SHA: staticEnv.GITHUB_SHA ?? undefined,\n      PUBLISHER_HOST: env.PUBLISHER_HOST,\n    },\n  };\n\n  return context;\n};\n\nconst createEntriContext = () => {\n  return {\n    entryApi,\n  };\n};\n\nconst createUserPlanContext = async (\n  authorization: AppContext[\"authorization\"],\n  postgrest: AppContext[\"postgrest\"]\n) => {\n  const ownerId =\n    authorization.type === \"token\"\n      ? authorization.ownerId\n      : authorization.type === \"user\"\n        ? authorization.userId\n        : undefined;\n\n  const planFeatures = ownerId\n    ? await getUserPlanFeatures(ownerId, postgrest)\n    : undefined;\n  return planFeatures;\n};\n\nconst createTrpcCache = () => {\n  const proceduresMaxAge = new Map<string, number>();\n  const setMaxAge = (path: string, value: number) => {\n    proceduresMaxAge.set(\n      path,\n      Math.min(proceduresMaxAge.get(path) ?? Number.MAX_SAFE_INTEGER, value)\n    );\n  };\n\n  const getMaxAge = (path: string) => proceduresMaxAge.get(path);\n\n  return {\n    setMaxAge,\n    getMaxAge,\n  };\n};\n\nexport const createPostgrestContext = () => {\n  return { client: createClient(env.POSTGREST_URL, env.POSTGREST_API_KEY) };\n};\n\n/**\n * argument buildEnv===\"prod\" only if we are loading project with production build\n */\nexport const createContext = async (request: Request): Promise<AppContext> => {\n  const postgrest = createPostgrestContext();\n  const authorization = await createAuthorizationContext(request, postgrest);\n\n  const domain = createDomainContext();\n  const deployment = createDeploymentContext(getRequestOrigin(request.url));\n  const entri = createEntriContext();\n  const userPlanFeatures = await createUserPlanContext(\n    authorization,\n    postgrest\n  );\n  const trpcCache = createTrpcCache();\n\n  const createTokenContext = async (authToken: string) => {\n    const authorization = await createTokenAuthorizationContext(\n      authToken,\n      postgrest\n    );\n    const userPlanFeatures = await createUserPlanContext(\n      authorization,\n      postgrest\n    );\n\n    return {\n      authorization,\n      domain,\n      deployment,\n      entri,\n      userPlanFeatures,\n      trpcCache,\n      postgrest,\n      createTokenContext,\n    };\n  };\n\n  return {\n    authorization,\n    domain,\n    deployment,\n    entri,\n    userPlanFeatures,\n    trpcCache,\n    postgrest,\n    createTokenContext,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/asset-upload.test.tsx",
    "content": "import { expect, test, vi } from \"vitest\";\nimport { $, AssetValue, renderTemplate } from \"@webstudio-is/template\";\nimport type { StyleDecl, WebstudioFragment } from \"@webstudio-is/sdk\";\nimport { denormalizeSrcProps } from \"./asset-upload\";\n\ntest(\"extractSrcProps works well\", async () => {\n  const data = renderTemplate(\n    <$.Body ws:id=\"boxA\">\n      <$.Image ws:id=\"imageA\" src=\"https://src-a/\"></$.Image>\n      <$.Box ws:id=\"boxB\">\n        <$.Image ws:id=\"imageB\" src=\"https://src-b/\"></$.Image>\n      </$.Box>\n    </$.Body>\n  );\n\n  const src2AssetId = (src: string) => `${src}asset-id`;\n\n  const uploadImages = async (srcs: string[]) => {\n    return new Map(srcs.map((src) => [src, `${src2AssetId(src)}`]));\n  };\n\n  const denormalizedData = await denormalizeSrcProps(\n    data,\n    uploadImages,\n    (instanceId, propName) => `${instanceId}:${propName}`\n  );\n\n  const assetA = new AssetValue(src2AssetId(\"https://src-a/\"));\n  const assetB = new AssetValue(src2AssetId(\"https://src-b/\"));\n\n  const desiredOutcome = renderTemplate(\n    <$.Body ws:id=\"boxA\">\n      <$.Image\n        ws:id=\"imageA\"\n        src={assetA}\n        width={assetA}\n        height={assetA}\n      ></$.Image>\n      <$.Box ws:id=\"boxB\">\n        <$.Image\n          ws:id=\"imageB\"\n          src={assetB}\n          width={assetB}\n          height={assetB}\n        ></$.Image>\n      </$.Box>\n    </$.Body>\n  );\n\n  expect(denormalizedData.instances).toEqual(desiredOutcome.instances);\n\n  expect(denormalizedData.props).toEqual(desiredOutcome.props);\n});\n\ntest(\"it works well with no background-images\", async () => {\n  const style: StyleDecl = {\n    styleSourceId: \"uulh2yW_VLZUgzjQ1bUt4\",\n    breakpointId: \"oQzjv7ZBzTiajBs6H03St\",\n    property: \"backgroundImage\",\n    value: {\n      type: \"layers\",\n      value: [\n        {\n          type: \"unparsed\",\n          value: \"linear-gradient(180deg,hsla(0,0.00%,0.00%,0.11),white)\",\n        },\n        {\n          type: \"image\",\n          value: {\n            type: \"url\",\n            url: \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png\",\n          },\n        },\n        {\n          type: \"image\",\n          value: {\n            type: \"url\",\n            url: \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp\",\n          },\n        },\n      ],\n    },\n  };\n\n  const data: WebstudioFragment = {\n    assets: [],\n    breakpoints: [],\n    children: [],\n    dataSources: [],\n    instances: [],\n    props: [],\n    resources: [],\n    styles: [style],\n    styleSources: [],\n    styleSourceSelections: [],\n  };\n\n  const src2AssetId = (src: string) => `asset-id::::${src}::::asset-id`;\n\n  const uploadImages = async (srcs: string[]) => {\n    return new Map(srcs.map((src) => [src, `${src2AssetId(src)}`]));\n  };\n\n  const denormalizedData = await denormalizeSrcProps(\n    data,\n    uploadImages,\n    (instanceId, propName) => `${instanceId}:${propName}`\n  );\n  const inputUrls = data.styles\n    .filter((style) => style.property === \"backgroundImage\")\n    .map((style) => style.value)\n    .filter((value) => value.type === \"layers\")\n    .flatMap((layer) => layer.value)\n    .filter((value) => value.type === \"image\")\n    .map((value) => value.value)\n    .filter((value) => value.type === \"url\")\n    .map((value) => value.url);\n\n  const denormalizedAssetIds = denormalizedData.styles\n    .filter((style) => style.property === \"backgroundImage\")\n    .map((style) => style.value)\n    .filter((value) => value.type === \"layers\")\n    .flatMap((layer) => layer.value)\n    .filter((value) => value.type === \"image\")\n    .map((value) => value.value)\n    .filter((value) => value.type === \"asset\")\n    .map((value) => value.value);\n\n  expect(denormalizedAssetIds).toEqual(inputUrls.map(src2AssetId));\n});\n\ntest(\"upload raw inception images\", async () => {\n  const data = renderTemplate(\n    <$.Body ws:id=\"boxA\">\n      <$.Image\n        ws:id=\"imageA\"\n        src=\"https://preview.webstudio.ai/cgi/image/dev/5036ed5c3dfce99eaac566a06bc3729620354a364357907a523f1feb2d6fb819.png?width=1024&height=1024&format=auto\"\n      ></$.Image>\n    </$.Body>\n  );\n  const uploadImages = vi.fn(async (srcs: string[]) => {\n    return new Map(srcs.map((src) => [src, src]));\n  });\n  await denormalizeSrcProps(\n    data,\n    uploadImages,\n    (instanceId, propName) => `${instanceId}:${propName}`\n  );\n  expect(uploadImages).toBeCalledWith([\n    \"https://preview.webstudio.ai/cgi/image/dev/5036ed5c3dfce99eaac566a06bc3729620354a364357907a523f1feb2d6fb819.png?format=raw\",\n  ]);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/asset-upload.ts",
    "content": "import type { WebstudioFragment } from \"@webstudio-is/sdk\";\nimport { builderApi } from \"../builder-api\";\nimport { nanoid } from \"nanoid\";\nimport { produce, enablePatches, type Patch, applyPatches } from \"immer\";\n\nenablePatches();\nconst extractSrcProps = (\n  data: WebstudioFragment\n): [propId: string, href: string][] => {\n  const imageComponentsSet = new Set(\n    data.instances\n      .filter(\n        (instance) =>\n          instance.type === \"instance\" && instance.component === \"Image\"\n      )\n      .map((instance) => instance.id)\n  );\n\n  const srcProps: [propId: string, href: string][] = [];\n  for (const prop of data.props) {\n    if (\n      prop.type === \"string\" &&\n      prop.name === \"src\" &&\n      imageComponentsSet.has(prop.instanceId)\n    ) {\n      try {\n        const url = new URL(prop.value);\n        // upload raw images from inception\n        if (url.hostname === \"preview.webstudio.ai\") {\n          url.search = \"\";\n          url.searchParams.set(\"format\", \"raw\");\n        }\n        srcProps.push([prop.id, url.href]);\n      } catch {\n        // ignore when invalid url\n      }\n    }\n  }\n\n  return srcProps;\n};\n\nconst extractUrlProps = (data: WebstudioFragment) => {\n  const changes: Patch[] = [];\n  const urls: string[] = [];\n\n  produce(\n    data,\n    (draft) => {\n      const images = draft.styles\n        .filter((style) => style.property === \"backgroundImage\")\n        .map((style) => style.value)\n        .filter((value) => value.type === \"layers\")\n        .flatMap((layer) => layer.value)\n        .filter((value) => value.type === \"image\");\n\n      for (const image of images) {\n        if (image.value.type === \"url\") {\n          const url = image.value.url;\n\n          urls.push(url);\n\n          image.value = {\n            type: \"asset\",\n            value: url,\n          };\n        }\n      }\n    },\n    (patches) => {\n      changes.push(...patches);\n    }\n  );\n\n  const applyUrlToIdChanges = (\n    sourceData: WebstudioFragment,\n    urlToId: Map<string, string>\n  ): WebstudioFragment => {\n    // Convert urls to ids in patches and apply them\n    const transformedPatched = JSON.parse(\n      JSON.stringify(changes),\n      (_key, value) => {\n        if (typeof value === \"string\") {\n          if (urlToId.has(value)) {\n            return urlToId.get(value);\n          }\n        }\n        return value;\n      }\n    );\n\n    return applyPatches(sourceData, transformedPatched);\n  };\n\n  return { urls, applyUrlToIdChanges };\n};\n\n/**\n *\n * Similar to normalizeProps, where asset properties are replaced with values,\n * here we replace the image src string property with the asset.\n */\nexport const denormalizeSrcProps = async (\n  data: WebstudioFragment,\n  uploadImages = builderApi.uploadImages,\n  generateId: (instanceId: string, propName: string) => string = () => nanoid()\n): Promise<WebstudioFragment> => {\n  const srcProps = extractSrcProps(data);\n  const { urls, applyUrlToIdChanges } = extractUrlProps(data);\n\n  const assetUrlToIds = await uploadImages([\n    ...srcProps.map(([, value]) => value),\n    ...urls,\n  ]);\n\n  const srcPropIdToAssetId = new Map(\n    srcProps.map(([id, url]) => [id, assetUrlToIds.get(url)] as const)\n  );\n\n  let result = applyUrlToIdChanges(data, assetUrlToIds);\n\n  result = {\n    ...result,\n\n    props: result.props\n      .map((prop) => {\n        if (prop.type === \"string\" && prop.name === \"src\") {\n          const assetId = srcPropIdToAssetId.get(prop.id);\n\n          if (assetId === undefined) {\n            return prop;\n          }\n\n          // @todo: add width and height props\n          const assetWithSizeProps: WebstudioFragment[\"props\"] = [\n            {\n              ...prop,\n              type: \"asset\",\n              value: assetId,\n            },\n            {\n              id: generateId(prop.instanceId, \"width\"),\n              name: \"width\",\n              instanceId: prop.instanceId,\n              type: \"asset\",\n              value: assetId,\n            },\n            {\n              id: generateId(prop.instanceId, \"height\"),\n              name: \"height\",\n              instanceId: prop.instanceId,\n              type: \"asset\",\n              value: assetId,\n            },\n          ];\n          return assetWithSizeProps;\n        }\n\n        return prop;\n      })\n      .flat(),\n  };\n\n  return result;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/init-copy-paste.ts",
    "content": "import { toastError } from \"../error/toast-error\";\nimport {\n  $authTokenPermissions,\n  $textEditingInstanceSelector,\n} from \"../nano-states\";\nimport { instanceText, instanceJson } from \"./plugin-instance\";\nimport { html } from \"./plugin-html\";\nimport { markdown } from \"./plugin-markdown\";\nimport { webflow } from \"./plugin-webflow/plugin-webflow\";\nimport { builderApi } from \"../builder-api\";\n\nconst isTextEditing = (event: ClipboardEvent) => {\n  // Text is edited on the Canvas using the default Canvas text editor settings.\n  if ($textEditingInstanceSelector.get() != null) {\n    return true;\n  }\n\n  // Text is edited inside an input, textarea, or contenteditable (i.e. codemirror editor) field.\n  if (\n    event.target instanceof HTMLInputElement ||\n    event.target instanceof HTMLTextAreaElement ||\n    (event.target instanceof HTMLElement &&\n      event.target.closest(\"[contenteditable]\"))\n  ) {\n    return true;\n  }\n\n  return false;\n};\n\n/**\n *\n * # Note:\n * validateClipboardEvent determines when to use default copy/paste behavior or if a WebStudio instance will be copied, pasted, or cut.\n * # How to test:\n * 1. Focus on an input in the style panel (width, style source) or name input in settings. Select the text, then copy and paste any instance in the Navigator tree or on the Canvas.\n * 2. Edit text in a paragraph on the Canvas. Select text (single or multiple lines), then press Cmd+X. The paragraph should not be deleted.\n * 3. Select CSS preview text, then select an instance in the Navigator. Press Cmd+C, then Cmd+V.\n **/\nconst validateClipboardEvent = (event: ClipboardEvent) => {\n  if (event.clipboardData === null) {\n    return false;\n  }\n\n  if (isTextEditing(event)) {\n    return false;\n  }\n\n  // Allows native selection of text in the Builder panels, such as CSS preview.\n  if (event.type === \"copy\") {\n    const isInBuilderContext = window.self === window.top;\n    const selection = window.getSelection();\n\n    if (isInBuilderContext && selection && selection.isCollapsed === false) {\n      return false;\n    }\n  }\n\n  if ($authTokenPermissions.get().canCopy === false) {\n    toastError(\"Copying has been disabled by the project owner\");\n    return false;\n  }\n  return true;\n};\n\nexport type Plugin = {\n  name: string;\n  mimeType: string;\n  onCopy?: () => undefined | string;\n  onCut?: () => undefined | string;\n  onPaste?: (data: string) => boolean | Promise<boolean>;\n};\n\nconst initPlugins = ({\n  plugins,\n  signal,\n}: {\n  plugins: Array<Plugin>;\n  signal: AbortSignal;\n}) => {\n  const handleCopy = (event: ClipboardEvent) => {\n    if (validateClipboardEvent(event) === false) {\n      return;\n    }\n\n    for (const { mimeType, onCopy } of plugins) {\n      const data = onCopy?.();\n\n      if (data) {\n        // must prevent default, otherwise setData() will not work\n        event.preventDefault();\n        event.clipboardData?.setData(mimeType, data);\n        break;\n      }\n    }\n  };\n\n  const handleCut = (event: ClipboardEvent) => {\n    if (validateClipboardEvent(event) === false) {\n      return;\n    }\n    for (const { mimeType, onCut } of plugins) {\n      const data = onCut?.();\n      if (data) {\n        // must prevent default, otherwise setData() will not work\n        event.preventDefault();\n        event.clipboardData?.setData(mimeType, data);\n        break;\n      }\n    }\n  };\n\n  const handlePaste = async (event: ClipboardEvent) => {\n    if (validateClipboardEvent(event) === false) {\n      return;\n    }\n\n    for (const { mimeType, onPaste } of plugins) {\n      // this shouldn't matter, but just in case\n      event.preventDefault();\n      const data = event.clipboardData?.getData(mimeType).trim();\n      if (data && (await onPaste?.(data))) {\n        break;\n      }\n    }\n  };\n\n  document.addEventListener(\"copy\", handleCopy, { signal });\n  document.addEventListener(\"cut\", handleCut, { signal });\n  // Capture is required so we get the element before content-editable removes it\n  // This way we can detect when we are inside content-editable and ignore the event\n  document.addEventListener(\"paste\", handlePaste, { capture: true, signal });\n};\n\nexport const initCopyPaste = ({ signal }: { signal: AbortSignal }) => {\n  initPlugins({\n    plugins: [instanceJson, instanceText, html, markdown, webflow],\n    signal,\n  });\n};\n\nexport const initCopyPasteForContentEditMode = ({\n  signal,\n}: {\n  signal: AbortSignal;\n}) => {\n  const handleClipboard = (event: ClipboardEvent) => {\n    if (validateClipboardEvent(event) === false) {\n      return;\n    }\n\n    builderApi.toast.info(\n      \"Copying and pasting is allowed in design mode only.\"\n    );\n  };\n\n  document.addEventListener(\"copy\", handleClipboard, { signal });\n  document.addEventListener(\"cut\", handleClipboard, { signal });\n  // Capture is required so we get the element before content-editable removes it\n  // This way we can detect when we are inside content-editable and ignore the event\n  document.addEventListener(\"paste\", handleClipboard, {\n    capture: true,\n    signal,\n  });\n};\n\n// Public API for programmatic copy/paste/cut operations\nexport const copyInstance = () => {\n  const data = instanceText.onCopy?.();\n  if (data) {\n    navigator.clipboard.writeText(data);\n  }\n};\n\nexport const emitPaste = async () => {\n  const text = await navigator.clipboard.readText();\n\n  // Create and dispatch a paste event to go through the normal handlePaste flow\n  const dataTransfer = new DataTransfer();\n  dataTransfer.setData(\"text/plain\", text);\n\n  const pasteEvent = new ClipboardEvent(\"paste\", {\n    clipboardData: dataTransfer,\n    bubbles: true,\n    cancelable: true,\n  });\n\n  document.dispatchEvent(pasteEvent);\n};\n\nexport const cutInstance = () => {\n  const data = instanceText.onCut?.();\n  if (data) {\n    navigator.clipboard.writeText(data);\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-html.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport { setEnv } from \"@webstudio-is/feature-flags\";\nimport { renderData, ws } from \"@webstudio-is/template\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { registerContainers } from \"../sync/sync-stores\";\nimport { $instances, $pages, $project } from \"../nano-states\";\nimport { $awareness } from \"../awareness\";\nimport { html } from \"./plugin-html\";\n\nsetEnv(\"*\");\nregisterContainers();\n\ntest(\"paste html fragment\", async () => {\n  const data = renderData(\n    <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    </ws.element>\n  );\n  $project.set({ id: \"\" } as Project);\n  $instances.set(data.instances);\n  $pages.set(\n    createDefaultPages({ rootInstanceId: \"bodyId\", homePageId: \"pageId\" })\n  );\n  $awareness.set({ pageId: \"pageId\", instanceSelector: [\"divId\", \"bodyId\"] });\n  expect(\n    await html.onPaste?.(`\n      <section>\n        <h1>It works</h1>\n      </section>\n    `)\n  ).toEqual(true);\n  const [_bodyId, _divId, sectionId, headingId] = $instances.get().keys();\n  expect(sectionId).toBeTruthy();\n  expect(headingId).toBeTruthy();\n  expect($instances.get()).toEqual(\n    renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"divId\">\n          <ws.element ws:tag=\"section\" ws:id={sectionId}>\n            <ws.element ws:tag=\"h1\" ws:id={headingId}>\n              It works\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      </ws.element>\n    ).instances\n  );\n});\n\ntest(\"ignore html without any tags\", async () => {\n  const data = renderData(\n    <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    </ws.element>\n  );\n  $project.set({ id: \"\" } as Project);\n  $instances.set(data.instances);\n  $pages.set(\n    createDefaultPages({ rootInstanceId: \"bodyId\", homePageId: \"pageId\" })\n  );\n  $awareness.set({ pageId: \"pageId\", instanceSelector: [\"divId\", \"bodyId\"] });\n  expect(await html.onPaste?.(`It works`)).toEqual(false);\n  expect($instances.get()).toEqual(data.instances);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-html.ts",
    "content": "import type { WebstudioFragment } from \"@webstudio-is/sdk\";\nimport { generateFragmentFromHtml } from \"../html\";\nimport { insertWebstudioFragmentAt } from \"../instance-utils\";\nimport { generateFragmentFromTailwind } from \"../tailwind/tailwind\";\nimport { denormalizeSrcProps } from \"./asset-upload\";\nimport type { Plugin } from \"./init-copy-paste\";\nimport { builderApi } from \"../builder-api\";\n\nconst inceptionMark = `<!-- @webstudio/inception/1 -->`;\n\nexport const html: Plugin = {\n  name: \"html\",\n  mimeType: \"text/plain\",\n  onPaste: async (html: string) => {\n    const parseResult = generateFragmentFromHtml(html);\n    const { skippedSelectors } = parseResult;\n    let fragment: WebstudioFragment = parseResult;\n    fragment = await denormalizeSrcProps(fragment);\n    if (html.includes(inceptionMark)) {\n      fragment = await generateFragmentFromTailwind(fragment);\n    }\n    const result = insertWebstudioFragmentAt(fragment);\n    if (skippedSelectors.length > 0) {\n      builderApi.toast.info(\n        `Skipped nested selectors (no matching elements): ${skippedSelectors.join(\", \")}`\n      );\n    }\n    return result;\n  },\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-instance.test.ts",
    "content": "import { describe, test, expect, vi, beforeAll } from \"vitest\";\nimport { enableMapSet } from \"immer\";\nimport type {\n  Instance,\n  Instances,\n  Prop,\n  Props,\n  DataSource,\n  DataSources,\n} from \"@webstudio-is/sdk\";\nimport {\n  encodeDataSourceVariable,\n  collectionComponent,\n  coreMetas,\n  portalComponent,\n} from \"@webstudio-is/sdk\";\nimport type { Project } from \"@webstudio-is/project\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport { registerContainers } from \"../sync/sync-stores\";\nimport {\n  $instances,\n  $dataSources,\n  $pages,\n  $project,\n  $props,\n  $registeredComponentMetas,\n} from \"../nano-states\";\nimport { instanceText } from \"./plugin-instance\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { $awareness, selectInstance } from \"../awareness\";\nimport * as instanceUtils from \"../instance-utils\";\n\nconst expectString = expect.any(String) as unknown as string;\n\nenableMapSet();\nregisterContainers();\n\n// Mock detectFragmentTokenConflicts to always return no conflicts\nbeforeAll(() => {\n  vi.spyOn(instanceUtils, \"detectFragmentTokenConflicts\").mockReturnValue([]);\n});\n\n$registeredComponentMetas.set(\n  new Map(Object.entries({ ...baseComponentMetas, ...coreMetas }))\n);\n$project.set({ id: \"my-project\" } as Project);\n$pages.set(\n  createDefaultPages({\n    homePageId: \"home-page\",\n    rootInstanceId: \"body0\",\n  })\n);\n$awareness.set({ pageId: \"home-page\" });\n\nconst createInstance = (\n  id: Instance[\"id\"],\n  component: string,\n  children: Instance[\"children\"]\n): Instance => {\n  return { type: \"instance\", id, component, children };\n};\n\nconst getIdValuePair = <T extends { id: string }>(item: T) =>\n  [item.id, item] as const;\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map(getIdValuePair));\n\nconst getMapDifference = <Type extends Map<unknown, unknown>>(\n  left: Type,\n  right: Type\n): Type => {\n  const leftSet = new Set(left.keys());\n  const rightSet = new Set(right.keys());\n  const difference = new Map() as Type;\n  for (const [key, value] of left) {\n    if (rightSet.has(key) === false) {\n      difference.set(key, value);\n    }\n  }\n  for (const [key, value] of right) {\n    if (leftSet.has(key) === false) {\n      difference.set(key, value);\n    }\n  }\n  return difference;\n};\n\ndescribe(\"paste target\", () => {\n  // body0\n  //   box1\n  //   box2\n  const instances: Instances = toMap([\n    createInstance(\"body0\", \"Body\", [\n      { type: \"id\", value: \"box1\" },\n      { type: \"id\", value: \"box2\" },\n    ]),\n    createInstance(\"box1\", \"Box\", []),\n    createInstance(\"box2\", \"Box\", []),\n  ] satisfies Instance[]);\n\n  test(\"is inside selected instance\", async () => {\n    $instances.set(instances);\n    selectInstance([\"box1\", \"body0\"]);\n    const clipboardData = instanceText.onCopy?.() ?? \"\";\n    selectInstance([\"box2\", \"body0\"]);\n    await instanceText.onPaste?.(clipboardData);\n\n    const instancesDifference = getMapDifference(instances, $instances.get());\n    const [newBox1] = instancesDifference.keys();\n    expect($instances.get().get(\"box2\")).toEqual(\n      createInstance(\"box2\", \"Box\", [{ type: \"id\", value: newBox1 }])\n    );\n    expect(instancesDifference).toEqual(\n      toMap([createInstance(newBox1, \"Box\", [])])\n    );\n  });\n\n  test(\"is after selected instance when same as copied\", async () => {\n    $instances.set(instances);\n    selectInstance([\"box1\", \"body0\"]);\n    await instanceText.onPaste?.(instanceText.onCopy?.() ?? \"\");\n\n    const instancesDifference = getMapDifference(instances, $instances.get());\n    const [newBox1] = instancesDifference.keys();\n    expect($instances.get().get(\"body0\")).toEqual(\n      createInstance(\"body0\", \"Body\", [\n        { type: \"id\", value: \"box1\" },\n        { type: \"id\", value: newBox1 },\n        { type: \"id\", value: \"box2\" },\n      ])\n    );\n    expect(instancesDifference).toEqual(\n      toMap([createInstance(newBox1, \"Box\", [])])\n    );\n  });\n});\n\ndescribe(\"data sources\", () => {\n  // body0\n  //   box1\n  //     box2\n  const instances: Instances = toMap([\n    createInstance(\"body0\", \"Body\", [{ type: \"id\", value: \"box1\" }]),\n    createInstance(\"box1\", \"Box\", [{ type: \"id\", value: \"box2\" }]),\n    createInstance(\"box2\", \"Box\", []),\n  ] satisfies Instance[]);\n  const dataSources: DataSources = toMap([\n    {\n      id: \"box1$state\",\n      scopeInstanceId: \"box1\",\n      type: \"variable\",\n      name: \"state\",\n      value: { type: \"string\", value: \"initial\" },\n    },\n  ] satisfies DataSource[]);\n  const props: Props = toMap([\n    {\n      id: \"box1$stateProp\",\n      instanceId: \"box1\",\n      name: \"state\",\n      type: \"expression\",\n      value: \"$ws$dataSource$box1$state\",\n    },\n    {\n      id: \"box2$stateProp\",\n      instanceId: \"box2\",\n      name: \"state\",\n      type: \"expression\",\n      value: \"$ws$dataSource$box1$state\",\n    },\n    {\n      id: \"box2$showProp\",\n      instanceId: \"box2\",\n      name: \"show\",\n      type: \"expression\",\n      value: `$ws$dataSource$box1$state === 'initial'`,\n    },\n    {\n      id: \"box2$onChangeProp\",\n      instanceId: \"box2\",\n      type: \"action\",\n      name: \"onChange\",\n      value: [\n        {\n          type: \"execute\",\n          args: [\"value\"],\n          code: `$ws$dataSource$box1$state = value`,\n        },\n      ],\n    },\n  ] satisfies Prop[]);\n\n  test(\"are copy pasted when scoped to copied instances\", async () => {\n    $instances.set(instances);\n    $dataSources.set(dataSources);\n    $props.set(props);\n    selectInstance([\"box1\", \"body0\"]);\n    const clipboardData = instanceText.onCopy?.() ?? \"\";\n    selectInstance([\"body0\"]);\n    await instanceText.onPaste?.(clipboardData);\n\n    const instancesDifference = getMapDifference(instances, $instances.get());\n    const [newBox1, newBox2] = instancesDifference.keys();\n\n    const dataSourcesDifference = getMapDifference(\n      dataSources,\n      $dataSources.get()\n    );\n    const [newDataSource1] = dataSourcesDifference.keys();\n    expect(dataSourcesDifference).toEqual(\n      toMap([\n        {\n          ...dataSources.get(\"box1$state\"),\n          id: newDataSource1,\n          scopeInstanceId: newBox1,\n        },\n      ])\n    );\n\n    const propsDifference = getMapDifference(props, $props.get());\n    const [newProp1, newProp2, newProp3, newProp4] = propsDifference.keys();\n    expect(propsDifference).toEqual(\n      toMap([\n        {\n          id: newProp1,\n          instanceId: newBox1,\n          name: \"state\",\n          type: \"expression\",\n          value: encodeDataSourceVariable(newDataSource1),\n        },\n        {\n          id: newProp2,\n          instanceId: newBox2,\n          name: \"state\",\n          type: \"expression\",\n          value: encodeDataSourceVariable(newDataSource1),\n        },\n        {\n          id: newProp3,\n          instanceId: newBox2,\n          name: \"show\",\n          type: \"expression\",\n          value: `${encodeDataSourceVariable(newDataSource1)} === 'initial'`,\n        },\n        {\n          id: newProp4,\n          instanceId: newBox2,\n          name: \"onChange\",\n          type: \"action\",\n          value: [\n            {\n              type: \"execute\",\n              args: [\"value\"],\n              code: `${encodeDataSourceVariable(newDataSource1)} = value`,\n            },\n          ],\n        },\n      ])\n    );\n  });\n\n  test(\"preserve data sources outside of scope when pasted within their scope\", async () => {\n    $instances.set(instances);\n    $props.set(props);\n    $dataSources.set(dataSources);\n    selectInstance([\"box2\", \"box1\", \"body0\"]);\n    const clipboardData = instanceText.onCopy?.() ?? \"\";\n    selectInstance([\"box1\", \"body0\"]);\n    await instanceText.onPaste?.(clipboardData);\n\n    const instancesDifference = getMapDifference(instances, $instances.get());\n    const [newBox2] = instancesDifference.keys();\n\n    const dataSourcesDifference = getMapDifference(\n      dataSources,\n      $dataSources.get()\n    );\n    expect(dataSourcesDifference).toEqual(new Map());\n\n    const propsDifference = getMapDifference(props, $props.get());\n    const [newProp1, newProp2, newProp3] = propsDifference.keys();\n    expect(propsDifference).toEqual(\n      toMap([\n        {\n          id: newProp1,\n          instanceId: newBox2,\n          name: \"state\",\n          type: \"expression\",\n          value: `$ws$dataSource$box1$state`,\n        },\n        {\n          id: newProp2,\n          instanceId: newBox2,\n          name: \"show\",\n          type: \"expression\",\n          value: `$ws$dataSource$box1$state === 'initial'`,\n        },\n        {\n          id: newProp3,\n          instanceId: newBox2,\n          type: \"action\",\n          name: \"onChange\",\n          value: [\n            {\n              args: [\"value\"],\n              code: \"$ws$dataSource$box1$state = value\",\n              type: \"execute\",\n            },\n          ],\n        },\n      ])\n    );\n  });\n\n  test(\"copy parameter prop with new data source\", async () => {\n    const instances: Instances = toMap([\n      createInstance(\"body\", \"Body\", [{ type: \"id\", value: \"list\" }]),\n      createInstance(\"list\", collectionComponent, []),\n    ] satisfies Instance[]);\n    const dataSources: DataSources = toMap([\n      {\n        id: \"itemDataSource\",\n        scopeInstanceId: \"list\",\n        type: \"parameter\",\n        name: \"item\",\n      },\n    ] satisfies DataSource[]);\n    const props: Props = toMap([\n      {\n        id: \"dataProp\",\n        instanceId: \"list\",\n        name: \"data\",\n        type: \"json\",\n        value: [],\n      },\n      {\n        id: \"itemProp\",\n        instanceId: \"list\",\n        name: \"item\",\n        type: \"parameter\",\n        value: \"itemDataSource\",\n      },\n    ] satisfies Prop[]);\n    $instances.set(instances);\n    $props.set(props);\n    $dataSources.set(dataSources);\n    selectInstance([\"list\", \"body\"]);\n    const clipboardData = instanceText.onCopy?.() ?? \"\";\n    selectInstance([\"body\"]);\n    await instanceText.onPaste?.(clipboardData);\n\n    const instancesDifference = getMapDifference(instances, $instances.get());\n    const [collectionId] = instancesDifference.keys();\n\n    const dataSourcesDifference = getMapDifference(\n      dataSources,\n      $dataSources.get()\n    );\n    const [itemDataSourceId] = dataSourcesDifference.keys();\n    expect(dataSourcesDifference).toEqual(\n      new Map([\n        [\n          itemDataSourceId,\n          {\n            id: itemDataSourceId,\n            scopeInstanceId: collectionId,\n            type: \"parameter\",\n            name: \"item\",\n          },\n        ],\n      ])\n    );\n\n    const propsDifference = getMapDifference(props, $props.get());\n    const [dataPropId, itemPropId] = propsDifference.keys();\n    expect(propsDifference).toEqual(\n      toMap([\n        {\n          id: dataPropId,\n          instanceId: collectionId,\n          name: \"data\",\n          type: \"json\",\n          value: [],\n        },\n        {\n          id: itemPropId,\n          instanceId: collectionId,\n          name: \"item\",\n          type: \"parameter\",\n          value: itemDataSourceId,\n        },\n      ])\n    );\n  });\n});\n\ntest(\"when paste into copied instance insert after it\", async () => {\n  $instances.set(\n    toMap([\n      createInstance(\"body\", \"Body\", [{ type: \"id\", value: \"box\" }]),\n      createInstance(\"box\", \"Box\", []),\n    ])\n  );\n  selectInstance([\"box\", \"body\"]);\n  const clipboardData = instanceText.onCopy?.() ?? \"\";\n  await instanceText.onPaste?.(clipboardData);\n\n  expect($instances.get()).toEqual(\n    toMap([\n      createInstance(\"body\", \"Body\", [\n        { type: \"id\", value: \"box\" },\n        { type: \"id\", value: expectString },\n      ]),\n      createInstance(\"box\", \"Box\", []),\n      createInstance(expectString, \"Box\", []),\n    ])\n  );\n});\n\ntest(\"prevent pasting portal into own descendants\", async () => {\n  const instances = toMap([\n    createInstance(\"body\", \"Body\", [{ type: \"id\", value: \"portal\" }]),\n    createInstance(\"portal\", portalComponent, [\n      { type: \"id\", value: \"fragment\" },\n    ]),\n    createInstance(\"fragment\", \"Fragment\", [{ type: \"id\", value: \"box\" }]),\n    createInstance(\"box\", \"Box\", []),\n  ]);\n  $instances.set(instances);\n  selectInstance([\"portal\", \"body\"]);\n  const clipboardData = instanceText.onCopy?.() ?? \"\";\n  selectInstance([\"fragment\", \"portal\", \"body\"]);\n  await instanceText.onPaste?.(clipboardData);\n  expect($instances.get()).toEqual(instances);\n});\n\ntest(\"prevent pasting portal into copy of it\", async () => {\n  const instances = toMap([\n    createInstance(\"body\", \"Body\", [\n      { type: \"id\", value: \"portal1\" },\n      { type: \"id\", value: \"portal2\" },\n    ]),\n    createInstance(\"portal1\", portalComponent, [\n      { type: \"id\", value: \"fragment\" },\n    ]),\n    createInstance(\"portal2\", portalComponent, [\n      { type: \"id\", value: \"fragment\" },\n    ]),\n    createInstance(\"fragment\", \"Fragment\", []),\n  ]);\n  $instances.set(instances);\n  selectInstance([\"box\", \"body\"]);\n  const clipboardData = instanceText.onCopy?.() ?? \"\";\n  selectInstance([\"portal\", \"body\"]);\n  await instanceText.onPaste?.(clipboardData);\n  expect($instances.get()).toEqual(instances);\n});\n\ntest(\"insert portal into its sibling\", async () => {\n  $instances.set(\n    toMap([\n      createInstance(\"body\", \"Body\", [\n        { type: \"id\", value: \"portal\" },\n        { type: \"id\", value: \"sibling\" },\n      ]),\n      createInstance(\"portal\", portalComponent, [\n        { type: \"id\", value: \"fragment\" },\n      ]),\n      createInstance(\"fragment\", \"Fragment\", []),\n      createInstance(\"sibling\", \"Box\", []),\n    ])\n  );\n  selectInstance([\"portal\", \"body\"]);\n  const clipboardData = instanceText.onCopy?.() ?? \"\";\n  selectInstance([\"sibling\", \"body\"]);\n  await instanceText.onPaste?.(clipboardData);\n\n  expect($instances.get()).toEqual(\n    toMap([\n      createInstance(\"body\", \"Body\", [\n        { type: \"id\", value: \"portal\" },\n        { type: \"id\", value: \"sibling\" },\n      ]),\n      createInstance(\"portal\", portalComponent, [\n        { type: \"id\", value: \"fragment\" },\n      ]),\n      createInstance(\"fragment\", \"Fragment\", []),\n      createInstance(\"sibling\", \"Box\", [{ type: \"id\", value: expectString }]),\n      createInstance(expectString, portalComponent, [\n        { type: \"id\", value: \"fragment\" },\n      ]),\n    ])\n  );\n});\n\ntest(\"insert into portal fragment when portal is a target\", async () => {\n  $instances.set(\n    toMap([\n      createInstance(\"body\", \"Body\", [\n        { type: \"id\", value: \"portal\" },\n        { type: \"id\", value: \"box\" },\n      ]),\n      createInstance(\"portal\", portalComponent, []),\n      createInstance(\"box\", \"Box\", []),\n    ])\n  );\n  selectInstance([\"box\", \"body\"]);\n  const clipboardData = instanceText.onCopy?.() ?? \"\";\n  selectInstance([\"portal\", \"body\"]);\n\n  // fragment not exists\n  const prevInstances = $instances.get();\n  await instanceText.onPaste?.(clipboardData);\n  const [boxId, fragmentId] = getMapDifference(\n    prevInstances,\n    $instances.get()\n  ).keys();\n  expect($instances.get()).toEqual(\n    toMap([\n      createInstance(\"body\", \"Body\", [\n        { type: \"id\", value: \"portal\" },\n        { type: \"id\", value: \"box\" },\n      ]),\n      createInstance(\"portal\", portalComponent, [\n        { type: \"id\", value: fragmentId },\n      ]),\n      createInstance(fragmentId, \"Fragment\", [{ type: \"id\", value: boxId }]),\n      createInstance(\"box\", \"Box\", []),\n      createInstance(boxId, \"Box\", []),\n    ])\n  );\n\n  // fragment already exists\n  await instanceText.onPaste?.(clipboardData);\n  expect($instances.get()).toEqual(\n    toMap([\n      createInstance(\"body\", \"Body\", [\n        { type: \"id\", value: \"portal\" },\n        { type: \"id\", value: \"box\" },\n      ]),\n      createInstance(\"portal\", portalComponent, [\n        { type: \"id\", value: fragmentId },\n      ]),\n      createInstance(fragmentId, \"Fragment\", [\n        { type: \"id\", value: boxId },\n        { type: \"id\", value: expectString },\n      ]),\n      createInstance(\"box\", \"Box\", []),\n      createInstance(boxId, \"Box\", []),\n      createInstance(expectString, \"Box\", []),\n    ])\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-instance.ts",
    "content": "import { shallowEqual } from \"shallow-equal\";\nimport { z } from \"zod\";\nimport { toast } from \"@webstudio-is/design-system\";\nimport {\n  Instance,\n  Instances,\n  WebstudioFragment,\n  findTreeInstanceIdsExcludingSlotDescendants,\n  isComponentDetachable,\n  portalComponent,\n} from \"@webstudio-is/sdk\";\nimport {\n  $selectedInstanceSelector,\n  $instances,\n  $project,\n} from \"../nano-states\";\nimport type { InstanceSelector } from \"../tree-utils\";\nimport {\n  deleteInstanceMutable,\n  extractWebstudioFragment,\n  insertWebstudioFragmentCopy,\n  updateWebstudioData,\n  getWebstudioData,\n  insertInstanceChildrenMutable,\n  findClosestInsertable,\n  detectFragmentTokenConflicts,\n  type Insertable,\n} from \"../instance-utils\";\nimport { $selectedInstancePath } from \"../awareness\";\nimport { findAvailableVariables } from \"../data-variables\";\nimport { builderApi } from \"../builder-api\";\nimport type { Plugin } from \"./init-copy-paste\";\n\nconst version = \"@webstudio/instance/v0.1\";\n\nconst InstanceData = WebstudioFragment.extend({\n  instanceSelector: z.array(z.string()),\n});\n\ntype InstanceData = z.infer<typeof InstanceData>;\n\nconst getTreeData = (instanceSelector: InstanceSelector) => {\n  const instances = $instances.get();\n  const [targetInstanceId] = instanceSelector;\n  const instance = instances.get(targetInstanceId);\n  if (instance && !isComponentDetachable(instance.component)) {\n    toast.error(\n      \"This instance can not be moved outside of its parent component.\"\n    );\n    return;\n  }\n\n  // @todo tell user they can't copy or cut root\n  if (instanceSelector.length === 1) {\n    return;\n  }\n\n  return {\n    instanceSelector,\n    ...extractWebstudioFragment(getWebstudioData(), targetInstanceId),\n  };\n};\n\nconst stringify = (data: InstanceData) => {\n  return JSON.stringify({ [version]: data });\n};\n\nconst ClipboardData = z.object({ [version]: InstanceData });\n\nconst parse = (clipboardData: string): InstanceData | undefined => {\n  try {\n    const data = ClipboardData.parse(JSON.parse(clipboardData));\n    return data[version];\n  } catch {\n    return;\n  }\n};\n\nconst getPortalFragmentSelector = (\n  instances: Instances,\n  instanceSelector: InstanceSelector\n) => {\n  const instance = instances.get(instanceSelector[0]);\n  if (\n    instance?.component !== portalComponent ||\n    instance.children.length === 0 ||\n    instance.children[0].type !== \"id\"\n  ) {\n    return;\n  }\n  // first portal child is always fragment\n  return [instance.children[0].value, ...instanceSelector];\n};\n\nconst findPasteTarget = (data: InstanceData): undefined | Insertable => {\n  const instances = $instances.get();\n\n  const instanceSelector = $selectedInstanceSelector.get();\n\n  // paste after selected instance\n  if (\n    instanceSelector &&\n    shallowEqual(instanceSelector, data.instanceSelector)\n  ) {\n    // body is not allowed to copy\n    // so clipboard always have at least two level instance selector\n    const [currentInstanceId, parentInstanceId] = instanceSelector;\n    const parentInstance = instances.get(parentInstanceId);\n    if (parentInstance === undefined) {\n      return;\n    }\n    const indexWithinChildren = parentInstance.children.findIndex(\n      (child) => child.type === \"id\" && child.value === currentInstanceId\n    );\n    return {\n      parentSelector: instanceSelector.slice(1),\n      position: indexWithinChildren + 1,\n    };\n  }\n\n  const insertable = findClosestInsertable(data);\n  if (insertable === undefined) {\n    return;\n  }\n\n  if (data.instances.length === 0) {\n    return;\n  }\n\n  const newInstances: Instances = new Map();\n  for (const instance of data.instances) {\n    newInstances.set(instance.id, instance);\n  }\n  const newInstanceIds = findTreeInstanceIdsExcludingSlotDescendants(\n    newInstances,\n    data.instances[0].id\n  );\n  const preservedChildIds = new Set<Instance[\"id\"]>();\n  for (const instance of data.instances) {\n    for (const child of instance.children) {\n      if (child.type === \"id\" && newInstanceIds.has(child.value) === false) {\n        preservedChildIds.add(child.value);\n      }\n    }\n  }\n\n  // portal descendants ids are preserved\n  // so need to prevent pasting portal inside its copies\n  // to avoid circular tree\n  const dropTargetSelector =\n    // consider portal fragment when check for cycles to avoid cases\n    // like pasting portal directly into portal\n    getPortalFragmentSelector(instances, insertable.parentSelector) ??\n    insertable.parentSelector;\n  for (const instanceId of dropTargetSelector) {\n    if (preservedChildIds.has(instanceId)) {\n      return;\n    }\n  }\n\n  return insertable;\n};\n\nconst onPaste = async (clipboardData: string) => {\n  const project = $project.get();\n  const fragment = parse(clipboardData);\n  if (fragment === undefined || project === undefined) {\n    return false;\n  }\n\n  const pasteTarget = findPasteTarget(fragment);\n  if (pasteTarget === undefined) {\n    return false;\n  }\n\n  try {\n    const conflicts = detectFragmentTokenConflicts({ fragment });\n    const conflictResolution =\n      conflicts.length > 0\n        ? await builderApi.showTokenConflictDialog(conflicts)\n        : \"theirs\";\n    updateWebstudioData((data) => {\n      const { newInstanceIds } = insertWebstudioFragmentCopy({\n        data,\n        fragment,\n        availableVariables: findAvailableVariables({\n          ...data,\n          startingInstanceId: pasteTarget.parentSelector[0],\n        }),\n        projectId: project.id,\n        conflictResolution,\n      });\n      const newRootInstanceId = newInstanceIds.get(fragment.instances[0].id);\n      if (newRootInstanceId === undefined) {\n        return;\n      }\n      const children: Instance[\"children\"] = [\n        { type: \"id\", value: newRootInstanceId },\n      ];\n      insertInstanceChildrenMutable(data, children, pasteTarget);\n    });\n  } catch (error) {\n    // User cancelled\n    return false;\n  }\n\n  return true;\n};\n\nconst onCopy = () => {\n  const selectedInstanceSelector = $selectedInstanceSelector.get();\n  if (selectedInstanceSelector === undefined) {\n    return;\n  }\n  const data = getTreeData(selectedInstanceSelector);\n  if (data === undefined) {\n    return;\n  }\n  return stringify(data);\n};\n\nconst onCut = () => {\n  const instancePath = $selectedInstancePath.get();\n  if (instancePath === undefined) {\n    return;\n  }\n  // @todo tell user they can't delete root\n  if (instancePath.length === 1) {\n    return;\n  }\n  const data = getTreeData(instancePath[0].instanceSelector);\n  if (data === undefined) {\n    return;\n  }\n  updateWebstudioData((data) => {\n    deleteInstanceMutable(data, instancePath);\n  });\n  if (data === undefined) {\n    return;\n  }\n  return stringify(data);\n};\n\nexport const instanceText: Plugin = {\n  name: \"instance-text\",\n  mimeType: \"text/plain\",\n  onCopy,\n  onCut,\n  onPaste,\n};\n\nexport const instanceJson: Plugin = {\n  name: \"instance-json\",\n  mimeType: \"application/json\",\n  onPaste,\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-markdown.test.tsx",
    "content": "import { test, expect } from \"vitest\";\nimport { $, renderTemplate, ws } from \"@webstudio-is/template\";\nimport { __testing__ } from \"./plugin-markdown\";\n\nconst { parse } = __testing__;\n\ntest(\"paragraph\", () => {\n  expect(parse(\"xyz\")).toEqual(\n    renderTemplate(<ws.element ws:tag=\"p\">xyz</ws.element>)\n  );\n});\n\ntest(\"h1\", () => {\n  expect(parse(\"# heading\")).toEqual(\n    renderTemplate(<ws.element ws:tag=\"h1\">heading</ws.element>)\n  );\n});\n\ntest(\"h6\", () => {\n  expect(parse(\"###### heading\")).toEqual(\n    renderTemplate(<ws.element ws:tag=\"h6\">heading</ws.element>)\n  );\n});\n\ntest(\"bold 1\", () => {\n  expect(parse(\"__bold__\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"strong\">bold</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"bold 2\", () => {\n  expect(parse(\"**bold**\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"strong\">bold</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"italic 1\", () => {\n  expect(parse(\"_italic_\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"em\">italic</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"italic 2\", () => {\n  expect(parse(\"*italic*\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"em\">italic</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"link\", () => {\n  expect(parse('[link](/uri \"Title\")')).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"a\" href=\"/uri\" title=\"Title\">\n          link\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"autolink\", () => {\n  expect(parse(\"https://github.com\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"a\" href=\"https://github.com\">\n          https://github.com\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"image\", () => {\n  expect(parse('![foo](/url \"title\")')).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <$.Image src=\"/url\" alt=\"foo\" title=\"title\" />\n      </ws.element>\n    )\n  );\n});\n\ntest(\"hard line break\", () => {\n  expect(\n    parse(`foo  \n      baz`)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"span\">foo</ws.element>\n        <ws.element ws:tag=\"br\" />\n        <ws.element ws:tag=\"span\"> baz</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"soft line break\", () => {\n  expect(\n    parse(`foo\n      baz`)\n  ).toEqual(renderTemplate(<ws.element ws:tag=\"p\">foo baz</ws.element>));\n});\n\ntest(\"blockquote\", () => {\n  expect(parse(\"> bar\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"blockquote\">\n        <ws.element ws:tag=\"p\">bar</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"inline code\", () => {\n  expect(parse(\"`foo` and `bar`\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"p\">\n        <ws.element ws:tag=\"code\">foo</ws.element>\n        <ws.element ws:tag=\"span\"> and </ws.element>\n        <ws.element ws:tag=\"code\">bar</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"code\", () => {\n  expect(parse(\"```js meta\\nfoo\\nbar\\n```\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"pre\">\n        <ws.element ws:tag=\"code\" class=\"language-js\">\n          {\"foo\\nbar\\n\"}\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"list unordered\", () => {\n  expect(parse(\"- one\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"ul\">\n        <ws.element ws:tag=\"li\">one</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"list ordered\", () => {\n  expect(parse(\"3. one\")).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"ol\" start={3}>\n        <ws.element ws:tag=\"li\">one</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"thematic break | separator\", () => {\n  expect(parse(\"---\")).toEqual(renderTemplate(<ws.element ws:tag=\"hr\" />));\n});\n\ntest(\"strikethrough\", () => {\n  expect(parse(\"~One~\\n\\n~~two~~\")).toEqual(\n    renderTemplate(\n      <>\n        <ws.element ws:tag=\"p\">\n          <ws.element ws:tag=\"del\">One</ws.element>\n        </ws.element>\n        <ws.element ws:tag=\"p\">\n          <ws.element ws:tag=\"del\">two</ws.element>\n        </ws.element>\n      </>\n    )\n  );\n});\n\ntest(\"preserve spaces between strong and em\", () => {\n  expect(parse(\"**One** *two* text\")).toEqual(\n    renderTemplate(\n      <>\n        <ws.element ws:tag=\"p\">\n          <ws.element ws:tag=\"strong\">One</ws.element>{\" \"}\n          <ws.element ws:tag=\"em\">two</ws.element>\n          {\" text\"}\n        </ws.element>\n      </>\n    )\n  );\n});\n\ntest(\"table\", () => {\n  expect(\n    parse(`\n| Header 1   | Header 2   |\n|------------|------------|\n| Cell 1.1   | Cell 1.2   |\n| Cell 2.1   | Cell 2.2   |\n`)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"table\">\n        <ws.element ws:tag=\"thead\">\n          <ws.element ws:tag=\"tr\">\n            <ws.element ws:tag=\"th\">Header 1</ws.element>\n            <ws.element ws:tag=\"th\">Header 2</ws.element>\n          </ws.element>\n        </ws.element>\n        <ws.element ws:tag=\"tbody\">\n          <ws.element ws:tag=\"tr\">\n            <ws.element ws:tag=\"td\">Cell 1.1</ws.element>\n            <ws.element ws:tag=\"td\">Cell 1.2</ws.element>\n          </ws.element>\n          <ws.element ws:tag=\"tr\">\n            <ws.element ws:tag=\"td\">Cell 2.1</ws.element>\n            <ws.element ws:tag=\"td\">Cell 2.2</ws.element>\n          </ws.element>\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-markdown.ts",
    "content": "import { gfm, gfmHtml } from \"micromark-extension-gfm\";\nimport { micromark } from \"micromark\";\nimport { insertWebstudioFragmentAt } from \"../instance-utils\";\nimport { denormalizeSrcProps } from \"./asset-upload\";\nimport { generateFragmentFromHtml } from \"../html\";\nimport type { Plugin } from \"./init-copy-paste\";\n\nconst parse = (clipboardData: string) => {\n  const html = micromark(clipboardData, \"utf-8\", {\n    extensions: [gfm()],\n    htmlExtensions: [gfmHtml()],\n  });\n  const { skippedSelectors: _skipped, ...fragment } =\n    generateFragmentFromHtml(html);\n  const instances = new Map(fragment.instances.map((item) => [item.id, item]));\n  for (const instance of fragment.instances) {\n    if (instance.tag === \"img\") {\n      delete instance.tag;\n      instance.component = \"Image\";\n    }\n  }\n  fragment.instances = Array.from(instances.values());\n  return fragment;\n};\n\nexport const markdown: Plugin = {\n  name: \"markdown\",\n  mimeType: \"text/plain\",\n  onPaste: async (clipboardData: string) => {\n    let fragment = parse(clipboardData);\n    if (fragment === undefined) {\n      return false;\n    }\n    fragment = await denormalizeSrcProps(fragment);\n    return insertWebstudioFragmentAt(fragment);\n  },\n};\n\nexport const __testing__ = {\n  parse,\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/__generated__/style-presets.ts",
    "content": "/* eslint-disable */\n/* This file was generated by css-to-ws.ts */\n\nexport const styles = {\n  html: [\n    {\n      selector: \"html\",\n      property: \"textSizeAdjust\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"html\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"sans-serif\",\n      },\n    },\n    {\n      selector: \"html\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"html\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"html\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n  ],\n  body: [\n    {\n      selector: \"body\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"minHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"fontFamily\",\n      value: {\n        type: \"unparsed\",\n        value: \"Arial,sans-serif\",\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"body\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n  ],\n  article: [\n    {\n      selector: \"article\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  aside: [\n    {\n      selector: \"aside\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  details: [\n    {\n      selector: \"details\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  figcaption: [\n    {\n      selector: \"figcaption\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"figcaption\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"figcaption\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n  ],\n  figure: [\n    {\n      selector: \"figure\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"figure\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"figure\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"figure\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"figure\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  footer: [\n    {\n      selector: \"footer\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  header: [\n    {\n      selector: \"header\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  hgroup: [\n    {\n      selector: \"hgroup\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  main: [\n    {\n      selector: \"main\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  menu: [\n    {\n      selector: \"menu\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  nav: [\n    {\n      selector: \"nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  section: [\n    {\n      selector: \"section\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  summary: [\n    {\n      selector: \"summary\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  audio: [\n    {\n      selector: \"audio\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"audio\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"audio\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"audio\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":not\",\n    },\n  ],\n  canvas: [\n    {\n      selector: \"canvas\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"canvas\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  progress: [\n    {\n      selector: \"progress\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"progress\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  video: [\n    {\n      selector: \"video\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"video\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  template: [\n    {\n      selector: \"template\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  a: [\n    {\n      selector: \"a\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"a\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":active\",\n    },\n    {\n      selector: \"a\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":active\",\n    },\n    {\n      selector: \"a\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":active\",\n    },\n    {\n      selector: \"a\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":hover\",\n    },\n    {\n      selector: \"a\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":hover\",\n    },\n    {\n      selector: \"a\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":hover\",\n    },\n  ],\n  abbr: [\n    {\n      selector: \"abbr\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"abbr\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"dotted\",\n      },\n    },\n    {\n      selector: \"abbr\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n  ],\n  b: [\n    {\n      selector: \"b\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n  ],\n  strong: [\n    {\n      selector: \"strong\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n  ],\n  dfn: [\n    {\n      selector: \"dfn\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"italic\",\n      },\n    },\n  ],\n  h1: [\n    {\n      selector: \"h1\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"h1\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"h1\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h1\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"h1\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 38,\n      },\n    },\n    {\n      selector: \"h1\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"h1\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 44,\n      },\n    },\n  ],\n  mark: [\n    {\n      selector: \"mark\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"mark\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 0,\n      },\n    },\n  ],\n  small: [\n    {\n      selector: \"small\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 80,\n      },\n    },\n  ],\n  sub: [\n    {\n      selector: \"sub\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"sub\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 75,\n      },\n    },\n    {\n      selector: \"sub\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"sub\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"sub\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: -0.25,\n      },\n    },\n  ],\n  sup: [\n    {\n      selector: \"sup\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"sup\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 75,\n      },\n    },\n    {\n      selector: \"sup\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"sup\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"sup\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: -0.5,\n      },\n    },\n  ],\n  img: [\n    {\n      selector: \"img\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"middle\",\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"img\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  svg: [\n    {\n      selector: \"svg\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"svg\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n      state: \":not\",\n    },\n  ],\n  hr: [\n    {\n      selector: \"hr\",\n      property: \"boxSizing\",\n      value: {\n        type: \"keyword\",\n        value: \"content-box\",\n      },\n    },\n    {\n      selector: \"hr\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  pre: [\n    {\n      selector: \"pre\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"pre\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"pre\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"monospace\",\n      },\n    },\n    {\n      selector: \"pre\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n  ],\n  code: [\n    {\n      selector: \"code\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"monospace\",\n      },\n    },\n    {\n      selector: \"code\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n  ],\n  kbd: [\n    {\n      selector: \"kbd\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"monospace\",\n      },\n    },\n    {\n      selector: \"kbd\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n  ],\n  samp: [\n    {\n      selector: \"samp\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"monospace\",\n      },\n    },\n    {\n      selector: \"samp\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n  ],\n  button: [\n    {\n      selector: \"button\",\n      property: \"color\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"fontVariantCaps\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"fontStretch\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"fontSize\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"visible\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"visible\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"textTransform\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"appearance\",\n      value: {\n        type: \"keyword\",\n        value: \"button\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"button\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"button\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n  ],\n  input: [\n    {\n      selector: \"input\",\n      property: \"color\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"fontVariantCaps\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"fontStretch\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"fontSize\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"appearance\",\n      value: {\n        type: \"keyword\",\n        value: \"button\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \"::-moz-focus-inner\",\n    },\n    {\n      selector: \"input\",\n      property: \"boxSizing\",\n      value: {\n        type: \"keyword\",\n        value: \"border-box\",\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"input\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n  ],\n  optgroup: [\n    {\n      selector: \"optgroup\",\n      property: \"color\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"fontVariantCaps\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"fontStretch\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"fontSize\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"optgroup\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  select: [\n    {\n      selector: \"select\",\n      property: \"color\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"fontVariantCaps\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"fontStretch\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"fontSize\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"select\",\n      property: \"textTransform\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  textarea: [\n    {\n      selector: \"textarea\",\n      property: \"color\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"fontVariantCaps\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"fontStretch\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"fontSize\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"textarea\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n  ],\n  legend: [\n    {\n      selector: \"legend\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"legend\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  table: [\n    {\n      selector: \"table\",\n      property: \"borderCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"table\",\n      property: \"borderSpacing\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  td: [\n    {\n      selector: \"td\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"td\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"td\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"td\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  th: [\n    {\n      selector: \"th\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"th\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"th\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"th\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-icon-slider-right\": [\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î˜€\"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-slider-right\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-icon-slider-left\": [\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î˜\"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-slider-left\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-icon-nav-menu\": [\n    {\n      selector: \"w-icon-nav-menu\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î˜‚\"',\n      },\n      state: \":before\",\n    },\n  ],\n  \"w-icon-arrow-down\": [\n    {\n      selector: \"w-icon-arrow-down\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î˜ƒ\"',\n      },\n      state: \":before\",\n    },\n  ],\n  \"w-icon-dropdown-toggle\": [\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î˜ƒ\"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-icon-dropdown-toggle\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-icon-file-upload-remove\": [\n    {\n      selector: \"w-icon-file-upload-remove\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î¤€\"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-icon-file-upload-remove\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-remove\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-remove\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-remove\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-remove\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n  ],\n  \"w-icon-file-upload-icon\": [\n    {\n      selector: \"w-icon-file-upload-icon\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"î¤ƒ\"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-icon-file-upload-icon\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-icon\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-icon\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"*\": [\n    {\n      selector: \"*\",\n      property: \"boxSizing\",\n      value: {\n        type: \"keyword\",\n        value: \"border-box\",\n      },\n    },\n  ],\n  \"w-block\": [\n    {\n      selector: \"w-block\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-inline-block\": [\n    {\n      selector: \"w-inline-block\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-inline-block\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"w-clearfix\": [\n    {\n      selector: \"w-clearfix\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-clearfix\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-hidden\": [\n    {\n      selector: \"w-hidden\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-button\": [\n    {\n      selector: \"w-button\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 9,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 15,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 9,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 15,\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-button\",\n      property: \"appearance\",\n      value: {\n        type: \"keyword\",\n        value: \"button\",\n      },\n    },\n  ],\n  \"w-code-block\": [\n    {\n      selector: \"w-code-block\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"unset\",\n      },\n    },\n    {\n      selector: \"w-code-block\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"unset\",\n      },\n    },\n    {\n      selector: \"w-code-block\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"unset\",\n      },\n    },\n    {\n      selector: \"w-code-block\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"unset\",\n      },\n    },\n  ],\n  \"w-webflow-badge\": [\n    {\n      selector: \"w-webflow-badge\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2147483647,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"visibility\",\n      value: {\n        type: \"keyword\",\n        value: \"visible\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"boxSizing\",\n      value: {\n        type: \"keyword\",\n        value: \"border-box\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 1,\n              },\n              {\n                type: \"rgb\",\n                alpha: 0.1,\n                r: 0,\n                g: 0,\n                b: 0,\n              },\n            ],\n          },\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 1,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 3,\n              },\n              {\n                type: \"rgb\",\n                alpha: 0.1,\n                r: 0,\n                g: 0,\n                b: 0,\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"direction\",\n      value: {\n        type: \"keyword\",\n        value: \"ltr\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontFamily\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 170,\n        g: 173,\n        b: 176,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantLigatures\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantCaps\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantAlternates\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantNumeric\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantEastAsian\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantPosition\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"fontVariantEmoji\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"letterSpacing\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textIndent\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textTransform\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"none\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"WebkitFontSmoothing\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"baseline\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"wordBreak\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"wordSpacing\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"wordWrap\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"width\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"minWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"maxWidth\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"minHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"maxHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 6,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 6,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 6,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 6,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"listStyleType\",\n      value: {\n        type: \"keyword\",\n        value: \"disc\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"transitionProperty\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"transitionDuration\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"transitionTimingFunction\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"ease\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"transitionDelay\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"transitionBehavior\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"normal\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"fixed\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"top\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"left\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"visible\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"visible\",\n      },\n    },\n    {\n      selector: \"w-webflow-badge\",\n      property: \"transform\",\n      value: {\n        type: \"tuple\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n  ],\n  h2: [\n    {\n      selector: \"h2\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h2\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"h2\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"h2\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 32,\n      },\n    },\n    {\n      selector: \"h2\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 36,\n      },\n    },\n  ],\n  h3: [\n    {\n      selector: \"h3\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h3\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"h3\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"h3\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 24,\n      },\n    },\n    {\n      selector: \"h3\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 30,\n      },\n    },\n  ],\n  h4: [\n    {\n      selector: \"h4\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h4\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"h4\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h4\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"h4\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 24,\n      },\n    },\n  ],\n  h5: [\n    {\n      selector: \"h5\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h5\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"h5\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h5\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"h5\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n  ],\n  h6: [\n    {\n      selector: \"h6\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h6\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"h6\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"h6\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"h6\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n  ],\n  p: [\n    {\n      selector: \"p\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"p\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n  ],\n  blockquote: [\n    {\n      selector: \"blockquote\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 226,\n        g: 226,\n        b: 226,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"blockquote\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 22,\n      },\n    },\n  ],\n  ul: [\n    {\n      selector: \"ul\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"ul\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"ul\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n  ],\n  ol: [\n    {\n      selector: \"ol\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"ol\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"ol\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n  ],\n  \"w-list-unstyled\": [\n    {\n      selector: \"w-list-unstyled\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-list-unstyled\",\n      property: \"listStylePosition\",\n      value: {\n        type: \"keyword\",\n        value: \"initial\",\n      },\n    },\n    {\n      selector: \"w-list-unstyled\",\n      property: \"listStyleImage\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-list-unstyled\",\n      property: \"listStyleType\",\n      value: {\n        type: \"keyword\",\n        value: \"initial\",\n      },\n    },\n  ],\n  \"w-embed\": [\n    {\n      selector: \"w-embed\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-embed\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-video\": [\n    {\n      selector: \"w-video\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-video\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-video\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-video\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-video\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-video\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  fieldset: [\n    {\n      selector: \"fieldset\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"not-allowed\",\n      },\n    },\n    {\n      selector: \"fieldset\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 238,\n        g: 238,\n        b: 238,\n      },\n    },\n  ],\n  \"w-form\": [\n    {\n      selector: \"w-form\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-form\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-form\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 15,\n      },\n    },\n    {\n      selector: \"w-form\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-form-done\": [\n    {\n      selector: \"w-form-done\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-form-done\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 221,\n        g: 221,\n        b: 221,\n      },\n    },\n    {\n      selector: \"w-form-done\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-form-done\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-form-done\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-form-done\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-form-done\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-form-fail\": [\n    {\n      selector: \"w-form-fail\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 222,\n        b: 222,\n      },\n    },\n    {\n      selector: \"w-form-fail\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-form-fail\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-form-fail\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-form-fail\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-form-fail\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-form-fail\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  label: [\n    {\n      selector: \"label\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"label\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"bold\",\n      },\n    },\n    {\n      selector: \"label\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-input\": [\n    {\n      selector: \"w-input\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"middle\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 238,\n        g: 238,\n        b: 238,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1.42857,\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-input\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 153,\n        g: 153,\n        b: 153,\n      },\n      state: \":-moz-placeholder\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 153,\n        g: 153,\n        b: 153,\n      },\n      state: \"::-moz-placeholder\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \"::-moz-placeholder\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 153,\n        g: 153,\n        b: 153,\n      },\n      state: \"::-webkit-input-placeholder\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-input\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"not-allowed\",\n      },\n    },\n  ],\n  \"w-select\": [\n    {\n      selector: \"w-select\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"middle\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 243,\n        g: 243,\n        b: 243,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1.42857,\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-select\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 153,\n        g: 153,\n        b: 153,\n      },\n      state: \":-moz-placeholder\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 153,\n        g: 153,\n        b: 153,\n      },\n      state: \"::-moz-placeholder\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \"::-moz-placeholder\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 153,\n        g: 153,\n        b: 153,\n      },\n      state: \"::-webkit-input-placeholder\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-select\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"not-allowed\",\n      },\n    },\n  ],\n  \"w-input-disabled\": [\n    {\n      selector: \"w-input-disabled\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 238,\n        g: 238,\n        b: 238,\n      },\n    },\n  ],\n  \"w-form-label\": [\n    {\n      selector: \"w-form-label\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-form-label\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-form-label\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-form-label\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"w-radio\": [\n    {\n      selector: \"w-radio\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w-radio\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-radio\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-radio\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-radio\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-radio-input\": [\n    {\n      selector: \"w-radio-input\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-radio-input\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-radio-input\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-radio-input\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-radio-input\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -20,\n      },\n    },\n    {\n      selector: \"w-radio-input\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n  ],\n  \"w-file-upload\": [\n    {\n      selector: \"w-file-upload\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-file-upload\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-file-upload-input\": [\n    {\n      selector: \"w-file-upload-input\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-input\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: -100,\n      },\n    },\n    {\n      selector: \"w-file-upload-input\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 0.1,\n      },\n    },\n    {\n      selector: \"w-file-upload-input\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 0.1,\n      },\n    },\n    {\n      selector: \"w-file-upload-input\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-file-upload-input\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-file-upload-input\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-file-upload-default\": [\n    {\n      selector: \"w-file-upload-default\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"w-file-upload-default\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-file-upload-uploading\": [\n    {\n      selector: \"w-file-upload-uploading\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-file-upload-success\": [\n    {\n      selector: \"w-file-upload-success\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"w-file-upload-success\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-file-upload-error\": [\n    {\n      selector: \"w-file-upload-error\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-file-upload-error\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-file-upload-uploading-btn\": [\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 250,\n        g: 250,\n        b: 250,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-file-upload-uploading-btn\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"w-file-upload-file\": [\n    {\n      selector: \"w-file-upload-file\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 250,\n        g: 250,\n        b: 250,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"flexGrow\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"justifyContent\",\n      value: {\n        type: \"keyword\",\n        value: \"space-between\",\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 9,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 11,\n      },\n    },\n    {\n      selector: \"w-file-upload-file\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"w-file-upload-file-name\": [\n    {\n      selector: \"w-file-upload-file-name\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"w-file-upload-file-name\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-file-upload-file-name\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-file-remove-link\": [\n    {\n      selector: \"w-file-remove-link\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"width\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-file-remove-link\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-file-upload-error-msg\": [\n    {\n      selector: \"w-file-upload-error-msg\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 234,\n        g: 56,\n        b: 76,\n      },\n    },\n    {\n      selector: \"w-file-upload-error-msg\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-file-upload-error-msg\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-error-msg\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-file-upload-error-msg\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-error-msg\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"w-file-upload-info\": [\n    {\n      selector: \"w-file-upload-info\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-info\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-file-upload-info\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-info\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-file-upload-info\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 38,\n      },\n    },\n    {\n      selector: \"w-file-upload-info\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"w-file-upload-label\": [\n    {\n      selector: \"w-file-upload-label\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 250,\n        g: 250,\n        b: 250,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 204,\n        g: 204,\n        b: 204,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 14,\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"fontWeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-file-upload-label\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"w-icon-file-upload-uploading\": [\n    {\n      selector: \"w-icon-file-upload-uploading\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-uploading\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-uploading\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-icon-file-upload-uploading\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n  ],\n  \"w-container\": [\n    {\n      selector: \"w-container\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 940,\n      },\n    },\n    {\n      selector: \"w-container\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-container\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-container\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 728,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-container\",\n      property: \"maxWidth\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-row\": [\n    {\n      selector: \"w-row\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-row\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col\": [\n    {\n      selector: \"w-col\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-col\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-col\",\n      property: \"minHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-col\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-col\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-col\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-col\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-col\",\n      property: \"left\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-col\",\n      property: \"right\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-col\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-1\": [\n    {\n      selector: \"w-col-1\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 8.33333,\n      },\n    },\n  ],\n  \"w-col-2\": [\n    {\n      selector: \"w-col-2\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 16.6667,\n      },\n    },\n  ],\n  \"w-col-3\": [\n    {\n      selector: \"w-col-3\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 25,\n      },\n    },\n  ],\n  \"w-col-4\": [\n    {\n      selector: \"w-col-4\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 33.3333,\n      },\n    },\n  ],\n  \"w-col-5\": [\n    {\n      selector: \"w-col-5\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 41.6667,\n      },\n    },\n  ],\n  \"w-col-6\": [\n    {\n      selector: \"w-col-6\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n  ],\n  \"w-col-7\": [\n    {\n      selector: \"w-col-7\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 58.3333,\n      },\n    },\n  ],\n  \"w-col-8\": [\n    {\n      selector: \"w-col-8\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 66.6667,\n      },\n    },\n  ],\n  \"w-col-9\": [\n    {\n      selector: \"w-col-9\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 75,\n      },\n    },\n  ],\n  \"w-col-10\": [\n    {\n      selector: \"w-col-10\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 83.3333,\n      },\n    },\n  ],\n  \"w-col-11\": [\n    {\n      selector: \"w-col-11\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 91.6667,\n      },\n    },\n  ],\n  \"w-col-12\": [\n    {\n      selector: \"w-col-12\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n  ],\n  \"w-hidden-main\": [\n    {\n      selector: \"w-hidden-main\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-hidden-main\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-hidden-main\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-hidden-main\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-hidden-medium\": [\n    {\n      selector: \"w-hidden-medium\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-hidden-medium\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-hidden-medium\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-medium-1\": [\n    {\n      selector: \"w-col-medium-1\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 8.33333,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-2\": [\n    {\n      selector: \"w-col-medium-2\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 16.6667,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-3\": [\n    {\n      selector: \"w-col-medium-3\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 25,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-4\": [\n    {\n      selector: \"w-col-medium-4\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 33.3333,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-5\": [\n    {\n      selector: \"w-col-medium-5\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 41.6667,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-6\": [\n    {\n      selector: \"w-col-medium-6\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-7\": [\n    {\n      selector: \"w-col-medium-7\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 58.3333,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-8\": [\n    {\n      selector: \"w-col-medium-8\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 66.6667,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-9\": [\n    {\n      selector: \"w-col-medium-9\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 75,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-10\": [\n    {\n      selector: \"w-col-medium-10\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 83.3333,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-11\": [\n    {\n      selector: \"w-col-medium-11\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 91.6667,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-medium-12\": [\n    {\n      selector: \"w-col-medium-12\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-col-stack\": [\n    {\n      selector: \"w-col-stack\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-col-stack\",\n      property: \"left\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-col-stack\",\n      property: \"right\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n  ],\n  \"w-hidden-small\": [\n    {\n      selector: \"w-hidden-small\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-hidden-small\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-small-1\": [\n    {\n      selector: \"w-col-small-1\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 8.33333,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-2\": [\n    {\n      selector: \"w-col-small-2\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 16.6667,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-3\": [\n    {\n      selector: \"w-col-small-3\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 25,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-4\": [\n    {\n      selector: \"w-col-small-4\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 33.3333,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-5\": [\n    {\n      selector: \"w-col-small-5\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 41.6667,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-6\": [\n    {\n      selector: \"w-col-small-6\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-7\": [\n    {\n      selector: \"w-col-small-7\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 58.3333,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-8\": [\n    {\n      selector: \"w-col-small-8\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 66.6667,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-9\": [\n    {\n      selector: \"w-col-small-9\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 75,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-10\": [\n    {\n      selector: \"w-col-small-10\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 83.3333,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-11\": [\n    {\n      selector: \"w-col-small-11\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 91.6667,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-col-small-12\": [\n    {\n      selector: \"w-col-small-12\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-hidden-tiny\": [\n    {\n      selector: \"w-hidden-tiny\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-1\": [\n    {\n      selector: \"w-col-tiny-1\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 8.33333,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-2\": [\n    {\n      selector: \"w-col-tiny-2\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 16.6667,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-3\": [\n    {\n      selector: \"w-col-tiny-3\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 25,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-4\": [\n    {\n      selector: \"w-col-tiny-4\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 33.3333,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-5\": [\n    {\n      selector: \"w-col-tiny-5\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 41.6667,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-6\": [\n    {\n      selector: \"w-col-tiny-6\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-7\": [\n    {\n      selector: \"w-col-tiny-7\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 58.3333,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-8\": [\n    {\n      selector: \"w-col-tiny-8\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 66.6667,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-9\": [\n    {\n      selector: \"w-col-tiny-9\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 75,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-10\": [\n    {\n      selector: \"w-col-tiny-10\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 83.3333,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-11\": [\n    {\n      selector: \"w-col-tiny-11\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 91.6667,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-col-tiny-12\": [\n    {\n      selector: \"w-col-tiny-12\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-widget\": [\n    {\n      selector: \"w-widget\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-widget-map\": [\n    {\n      selector: \"w-widget-map\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-widget-map\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 400,\n      },\n    },\n  ],\n  \"w-widget-twitter\": [\n    {\n      selector: \"w-widget-twitter\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-widget-twitter-count-shim\": [\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 76,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 22,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -9,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"pointerEvents\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -5,\n      },\n      state: \":not\",\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"pointerEvents\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-widget-twitter-count-shim\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -4,\n      },\n    },\n  ],\n  \"w--large\": [\n    {\n      selector: \"w--large\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 76,\n      },\n    },\n    {\n      selector: \"w--large\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 28,\n      },\n    },\n    {\n      selector: \"w--large\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 6,\n      },\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -10,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -5,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--large\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -9,\n      },\n      state: \":after\",\n    },\n  ],\n  \"w--vertical\": [\n    {\n      selector: \"w--vertical\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -9,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"pointerEvents\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 76,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 33,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -5,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"pointerEvents\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"pointerEvents\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 93,\n        g: 108,\n        b: 123,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 117,\n        g: 134,\n        b: 150,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 117,\n        g: 134,\n        b: 150,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 117,\n        g: 134,\n        b: 150,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -5,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w--vertical\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -4,\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-background-video\": [\n    {\n      selector: \"w-background-video\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-background-video\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 500,\n      },\n    },\n    {\n      selector: \"w-background-video\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-background-video\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-background-video\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-background-video--control\": [\n    {\n      selector: \"w-background-video--control\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-background-video--control\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n  ],\n  \"w-slider\": [\n    {\n      selector: \"w-slider\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 221,\n        g: 221,\n        b: 221,\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 300,\n      },\n    },\n    {\n      selector: \"w-slider\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-slider-mask\": [\n    {\n      selector: \"w-slider-mask\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-slider-mask\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-slide\": [\n    {\n      selector: \"w-slide\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"wrap\",\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-slide\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-slider-nav\": [\n    {\n      selector: \"w-slider-nav\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.2,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"top\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"fontSize\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"inherit\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"width\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.5,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.2,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.5,\n      },\n    },\n    {\n      selector: \"w-slider-nav\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 3,\n              },\n              {\n                type: \"rgb\",\n                alpha: 0.4,\n                r: 51,\n                g: 51,\n                b: 51,\n              },\n            ],\n          },\n        ],\n      },\n    },\n  ],\n  \"w-slider-nav-invert\": [\n    {\n      selector: \"w-slider-nav-invert\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n  ],\n  \"w-active\": [\n    {\n      selector: \"w-active\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-active\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"none\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n  ],\n  \"w-slider-dot\": [\n    {\n      selector: \"w-slider-dot\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.5,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"transitionProperty\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unparsed\",\n            value: \"background-color\",\n          },\n          {\n            type: \"unparsed\",\n            value: \"color\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"transitionDuration\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0.1,\n          },\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0.1,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"transitionTimingFunction\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"ease\",\n          },\n          {\n            type: \"keyword\",\n            value: \"ease\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"transitionDelay\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0,\n          },\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"transitionBehavior\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"normal\",\n          },\n          {\n            type: \"keyword\",\n            value: \"normal\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"keyword\",\n        value: \"medium\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-dot\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"none\",\n              },\n            ],\n          },\n        ],\n      },\n      state: \":focus\",\n    },\n  ],\n  \"w-slider-arrow-left\": [\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"userSelect\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 80,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"right\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-arrow-left\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 3,\n      },\n    },\n  ],\n  \"w-slider-arrow-right\": [\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"userSelect\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 80,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"marginTop\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"marginBottom\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"left\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-slider-arrow-right\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 4,\n      },\n    },\n  ],\n  \"w-slider-aria-label\": [\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"clip\",\n      value: {\n        type: \"unparsed\",\n        value: \"rect(0 0 0 0)\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -1,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -1,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -1,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -1,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-slider-aria-label\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-slider-force-show\": [\n    {\n      selector: \"w-slider-force-show\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-dropdown\": [\n    {\n      selector: \"w-dropdown\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-dropdown\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 900,\n      },\n    },\n    {\n      selector: \"w-dropdown\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-dropdown\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-dropdown-btn\": [\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 34,\n        g: 34,\n        b: 34,\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-dropdown-btn\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-dropdown-toggle\": [\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 34,\n        g: 34,\n        b: 34,\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"userSelect\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-dropdown-toggle\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n  ],\n  \"w-dropdown-link\": [\n    {\n      selector: \"w-dropdown-link\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 0,\n        g: 130,\n        b: 243,\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-dropdown-link\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n  ],\n  \"w-dropdown-list\": [\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 221,\n        g: 221,\n        b: 221,\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"minWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-dropdown-list\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n  ],\n  \"w--open\": [\n    {\n      selector: \"w--open\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w--open\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w--open\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 200,\n        g: 200,\n        b: 200,\n      },\n    },\n  ],\n  \"w--current\": [\n    {\n      selector: \"w--current\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 0,\n        g: 130,\n        b: 243,\n      },\n    },\n    {\n      selector: \"w--current\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 200,\n        g: 200,\n        b: 200,\n      },\n    },\n  ],\n  \"w-nav-brand\": [\n    {\n      selector: \"w-nav-brand\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-nav-brand\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-nav-brand\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 51,\n        g: 51,\n        b: 51,\n      },\n    },\n    {\n      selector: \"w-nav-brand\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-nav-brand\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-nav-brand\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-nav-brand\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-lightbox-backdrop\": [\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"letterSpacing\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"textIndent\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"textShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"none\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"textTransform\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"visibility\",\n      value: {\n        type: \"keyword\",\n        value: \"visible\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"wrap\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"wordBreak\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"wordSpacing\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"wordWrap\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2000,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"userSelect\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"keyword\",\n        value: \"transparent\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0.9,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"fontFamily\",\n      value: {\n        type: \"unparsed\",\n        value: \"Helvetica Neue,Helvetica,Ubuntu,Segoe UI,Verdana,sans-serif\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 17,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"fontStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"fontWeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 300,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1.2,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"listStylePosition\",\n      value: {\n        type: \"keyword\",\n        value: \"initial\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"listStyleImage\",\n      value: {\n        type: \"keyword\",\n        value: \"initial\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"listStyleType\",\n      value: {\n        type: \"keyword\",\n        value: \"disc\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"fixed\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"transform\",\n      value: {\n        type: \"tuple\",\n        value: [\n          {\n            type: \"function\",\n            args: {\n              type: \"layers\",\n              value: [\n                {\n                  type: \"unit\",\n                  unit: \"number\",\n                  value: 0,\n                },\n              ],\n            },\n            name: \"translate\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"-webkit-overflow-scrolling\",\n      value: {\n        type: \"keyword\",\n        value: \"touch\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-lightbox-backdrop\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n  ],\n  \"w-lightbox-container\": [\n    {\n      selector: \"w-lightbox-container\",\n      property: \"-webkit-overflow-scrolling\",\n      value: {\n        type: \"keyword\",\n        value: \"touch\",\n      },\n    },\n    {\n      selector: \"w-lightbox-container\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-container\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-lightbox-container\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n  ],\n  \"w-lightbox-content\": [\n    {\n      selector: \"w-lightbox-content\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-content\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-lightbox-content\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-lightbox-content\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-lightbox-content\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 96,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n    {\n      selector: \"w-lightbox-content\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 2,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n  ],\n  \"w-lightbox-view\": [\n    {\n      selector: \"w-lightbox-view\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"vw\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"\"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 100,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"middle\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 96,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n    {\n      selector: \"w-lightbox-view\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 96,\n      },\n      breakpoint: \"(min-width:768px)\",\n      state: \":before\",\n    },\n  ],\n  \"w-lightbox-group\": [\n    {\n      selector: \"w-lightbox-group\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 86,\n      },\n    },\n    {\n      selector: \"w-lightbox-group\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 84,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n  ],\n  \"w-lightbox-frame\": [\n    {\n      selector: \"w-lightbox-frame\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"middle\",\n      },\n    },\n    {\n      selector: \"w-lightbox-frame\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n  ],\n  \"w-lightbox-figure\": [\n    {\n      selector: \"w-lightbox-figure\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-figure\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-figure\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-figure\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-figure\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-lightbox-img\": [\n    {\n      selector: \"w-lightbox-img\",\n      property: \"width\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-lightbox-img\",\n      property: \"maxWidth\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-img\",\n      property: \"height\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n  ],\n  \"w-lightbox-image\": [\n    {\n      selector: \"w-lightbox-image\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-image\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"vw\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-image\",\n      property: \"maxHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-image\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-lightbox-image\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"vw\",\n        value: 96,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n    {\n      selector: \"w-lightbox-image\",\n      property: \"maxHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 96,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n  ],\n  \"w-lightbox-caption\": [\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"textOverflow\",\n      value: {\n        type: \"keyword\",\n        value: \"ellipsis\",\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0.4,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.5,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 0.5,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-lightbox-caption\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-lightbox-embed\": [\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-embed\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-lightbox-control\": [\n    {\n      selector: \"w-lightbox-control\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"center\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"center\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"no-repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"px\",\n            value: 24,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"transitionProperty\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"all\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"transitionDuration\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0.3,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"transitionTimingFunction\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"ease\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"transitionDelay\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"s\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"transitionBehavior\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"normal\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-control\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      breakpoint: \"(min-width:768px)\",\n      state: \":hover\",\n    },\n  ],\n  \"w-lightbox-left\": [\n    {\n      selector: \"w-lightbox-left\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yMCAwIDI0IDQwIiB3aWR0aD0iMjQiIGhlaWdodD0iNDAiPjxnIHRyYW5zZm9ybT0icm90YXRlKDQ1KSI+PHBhdGggZD0ibTAgMGg1djIzaDIzdjVoLTI4eiIgb3BhY2l0eT0iLjQiLz48cGF0aCBkPSJtMSAxaDN2MjNoMjN2M2gtMjZ6IiBmaWxsPSIjZmZmIi8+PC9nPjwvc3ZnPg==\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-left\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-left\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-left\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-left\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0.5,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n    {\n      selector: \"w-lightbox-left\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n  ],\n  \"w-lightbox-right\": [\n    {\n      selector: \"w-lightbox-right\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii00IDAgMjQgNDAiIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIj48cGF0aCBkPSJtMC0waDI4djI4aC01di0yM2gtMjN6IiBvcGFjaXR5PSIuNCIvPjxwYXRoIGQ9Im0xIDFoMjZ2MjZoLTN2LTIzaC0yM3oiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-right\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-right\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-right\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-right\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0.5,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n    {\n      selector: \"w-lightbox-right\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n  ],\n  \"w-lightbox-close\": [\n    {\n      selector: \"w-lightbox-close\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: \"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii00IDAgMTggMTciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxNyI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIj48cGF0aCBkPSJtMCAwaDd2LTdoNXY3aDd2NWgtN3Y3aC01di03aC03eiIgb3BhY2l0eT0iLjQiLz48cGF0aCBkPSJtMSAxaDd2LTdoM3Y3aDd2M2gtN3Y3aC0zdi03aC03eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4=\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-close\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"px\",\n            value: 18,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-close\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"em\",\n        value: 2.6,\n      },\n    },\n    {\n      selector: \"w-lightbox-close\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-close\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0.8,\n      },\n      breakpoint: \"(min-width:768px)\",\n    },\n  ],\n  \"w-lightbox-strip\": [\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"collapse\",\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"nowrap\",\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"lineHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-lightbox-strip\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-lightbox-item\": [\n    {\n      selector: \"w-lightbox-item\",\n      property: \"boxSizing\",\n      value: {\n        type: \"keyword\",\n        value: \"content-box\",\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-lightbox-item\",\n      property: \"transform\",\n      value: {\n        type: \"tuple\",\n        value: [\n          {\n            type: \"function\",\n            args: {\n              type: \"layers\",\n              value: [\n                {\n                  type: \"unit\",\n                  unit: \"number\",\n                  value: 0,\n                },\n                {\n                  type: \"unit\",\n                  unit: \"number\",\n                  value: 0,\n                },\n                {\n                  type: \"unit\",\n                  unit: \"number\",\n                  value: 0,\n                },\n              ],\n            },\n            name: \"translate3d\",\n          },\n        ],\n      },\n    },\n  ],\n  \"w-lightbox-active\": [\n    {\n      selector: \"w-lightbox-active\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0.3,\n      },\n    },\n  ],\n  \"w-lightbox-thumbnail\": [\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 34,\n        g: 34,\n        b: 34,\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-lightbox-thumbnail-image\": [\n    {\n      selector: \"w-lightbox-thumbnail-image\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail-image\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-thumbnail-image\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-lightbox-spinner\": [\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"boxSizing\",\n      value: {\n        type: \"keyword\",\n        value: \"border-box\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0.4,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0.4,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0.4,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0.4,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 40,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -20,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -20,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationDuration\",\n      value: {\n        type: \"unit\",\n        unit: \"s\",\n        value: 0.8,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationTimingFunction\",\n      value: {\n        type: \"keyword\",\n        value: \"linear\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationDelay\",\n      value: {\n        type: \"unit\",\n        unit: \"s\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationIterationCount\",\n      value: {\n        type: \"keyword\",\n        value: \"infinite\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationDirection\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationFillMode\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationPlayState\",\n      value: {\n        type: \"keyword\",\n        value: \"running\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationName\",\n      value: {\n        type: \"unparsed\",\n        value: \"spin\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationTimeline\",\n      value: {\n        type: \"unparsed\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationRangeStart\",\n      value: {\n        type: \"unparsed\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"animationRangeEnd\",\n      value: {\n        type: \"unparsed\",\n        value: \"normal\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"\"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 3,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"bottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -4,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-lightbox-spinner\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -4,\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-lightbox-hide\": [\n    {\n      selector: \"w-lightbox-hide\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-lightbox-noscroll\": [\n    {\n      selector: \"w-lightbox-noscroll\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-lightbox-noscroll\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-lightbox-inactive\": [\n    {\n      selector: \"w-lightbox-inactive\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-lightbox-inactive\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":hover\",\n    },\n  ],\n  \"w-richtext\": [\n    {\n      selector: \"w-richtext\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"whiteSpaceCollapse\",\n      value: {\n        type: \"keyword\",\n        value: \"initial\",\n      },\n    },\n    {\n      selector: \"w-richtext\",\n      property: \"textWrapMode\",\n      value: {\n        type: \"keyword\",\n        value: \"initial\",\n      },\n    },\n  ],\n  \"w-richtext-figcaption-placeholder\": [\n    {\n      selector: \"w-richtext-figcaption-placeholder\",\n      property: \"opacity\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0.6,\n      },\n    },\n  ],\n  \"w-richtext-figure-type-image\": [\n    {\n      selector: \"w-richtext-figure-type-image\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n    },\n  ],\n  \"w-richtext-figure-type-video\": [\n    {\n      selector: \"w-richtext-figure-type-video\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 60,\n      },\n    },\n    {\n      selector: \"w-richtext-figure-type-video\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n  ],\n  \"w-richtext-align-center\": [\n    {\n      selector: \"w-richtext-align-center\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-center\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-center\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-center\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n  ],\n  \"w-richtext-align-normal\": [\n    {\n      selector: \"w-richtext-align-normal\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n    },\n  ],\n  \"w-richtext-align-fullwidth\": [\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-fullwidth\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-richtext-align-floatleft\": [\n    {\n      selector: \"w-richtext-align-floatleft\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-floatleft\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-floatleft\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 15,\n      },\n    },\n  ],\n  \"w-richtext-align-floatright\": [\n    {\n      selector: \"w-richtext-align-floatright\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"right\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-floatright\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-richtext-align-floatright\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 15,\n      },\n    },\n  ],\n  \"w-nav\": [\n    {\n      selector: \"w-nav\",\n      property: \"zIndex\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"none\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 0,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n              {\n                type: \"keyword\",\n                value: \"auto\",\n              },\n            ],\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundAttachment\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"scroll\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundOrigin\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"padding-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundClip\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"border-box\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 221,\n        g: 221,\n        b: 221,\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"width\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"right\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"left\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n    {\n      selector: \"w-nav\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-nav-link\": [\n    {\n      selector: \"w-nav-link\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 0,\n        g: 130,\n        b: 243,\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-nav-link\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-nav-menu\": [\n    {\n      selector: \"w-nav-menu\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"right\",\n      },\n    },\n    {\n      selector: \"w-nav-menu\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w--nav-link-open\": [\n    {\n      selector: \"w--nav-link-open\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w--nav-link-open\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-nav-overlay\": [\n    {\n      selector: \"w-nav-overlay\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"absolute\",\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"top\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"left\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"right\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-nav-overlay\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-nav-button\": [\n    {\n      selector: \"w-nav-button\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"right\",\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"rgb\",\n        alpha: 0,\n        r: 0,\n        g: 0,\n        b: 0,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"userSelect\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 18,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"fontSize\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 24,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 255,\n        g: 255,\n        b: 255,\n      },\n    },\n    {\n      selector: \"w-nav-button\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 200,\n        g: 200,\n        b: 200,\n      },\n    },\n  ],\n  \"w--nav-dropdown-open\": [\n    {\n      selector: \"w--nav-dropdown-open\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w--nav-dropdown-toggle-open\": [\n    {\n      selector: \"w--nav-dropdown-toggle-open\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w--nav-dropdown-list-open\": [\n    {\n      selector: \"w--nav-dropdown-list-open\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"static\",\n      },\n    },\n  ],\n  \"w-tabs\": [\n    {\n      selector: \"w-tabs\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-tabs\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-tab-menu\": [\n    {\n      selector: \"w-tab-menu\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w-tab-link\": [\n    {\n      selector: \"w-tab-link\",\n      property: \"verticalAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"top\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"cursor\",\n      value: {\n        type: \"keyword\",\n        value: \"pointer\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"color\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 34,\n        g: 34,\n        b: 34,\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 200,\n        g: 200,\n        b: 200,\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 9,\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 30,\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 9,\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 30,\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"textDecorationLine\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"textDecorationStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"textDecorationColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"inline-block\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"outlineColor\",\n      value: {\n        type: \"keyword\",\n        value: \"currentcolor\",\n      },\n      state: \":focus\",\n    },\n    {\n      selector: \"w-tab-link\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n      breakpoint: \"(max-width:479px)\",\n    },\n  ],\n  \"w-tab-content\": [\n    {\n      selector: \"w-tab-content\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-tab-content\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-tab-content\",\n      property: \"overflowX\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n    {\n      selector: \"w-tab-content\",\n      property: \"overflowY\",\n      value: {\n        type: \"keyword\",\n        value: \"hidden\",\n      },\n    },\n  ],\n  \"w-tab-pane\": [\n    {\n      selector: \"w-tab-pane\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n    {\n      selector: \"w-tab-pane\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n  ],\n  \"w--tab-active\": [\n    {\n      selector: \"w--tab-active\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n  \"w-ix-emptyfix\": [\n    {\n      selector: \"w-ix-emptyfix\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\"\"',\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-dyn-empty\": [\n    {\n      selector: \"w-dyn-empty\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 221,\n        g: 221,\n        b: 221,\n      },\n    },\n    {\n      selector: \"w-dyn-empty\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-dyn-empty\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-dyn-empty\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n    {\n      selector: \"w-dyn-empty\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      },\n    },\n  ],\n  \"w-dyn-hide\": [\n    {\n      selector: \"w-dyn-hide\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-dyn-bind-empty\": [\n    {\n      selector: \"w-dyn-bind-empty\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"w-condition-invisible\": [\n    {\n      selector: \"w-condition-invisible\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n    },\n  ],\n  \"wf-layout-layout\": [\n    {\n      selector: \"wf-layout-layout\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"grid\",\n      },\n    },\n  ],\n  \"w-layout-blockcontainer\": [\n    {\n      selector: \"w-layout-blockcontainer\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 940,\n      },\n    },\n    {\n      selector: \"w-layout-blockcontainer\",\n      property: \"marginLeft\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-layout-blockcontainer\",\n      property: \"marginRight\",\n      value: {\n        type: \"keyword\",\n        value: \"auto\",\n      },\n    },\n    {\n      selector: \"w-layout-blockcontainer\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-layout-blockcontainer\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 728,\n      },\n      breakpoint: \"(max-width:991px)\",\n    },\n    {\n      selector: \"w-layout-blockcontainer\",\n      property: \"maxWidth\",\n      value: {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      breakpoint: \"(max-width:767px)\",\n    },\n  ],\n  \"w-layout-layout\": [\n    {\n      selector: \"w-layout-layout\",\n      property: \"rowGap\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"columnGap\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"gridAutoColumns\",\n      value: {\n        type: \"unit\",\n        unit: \"fr\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"justifyContent\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"paddingTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-layout-layout\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n  ],\n  \"w-layout-cell\": [\n    {\n      selector: \"w-layout-cell\",\n      property: \"flexDirection\",\n      value: {\n        type: \"keyword\",\n        value: \"column\",\n      },\n    },\n    {\n      selector: \"w-layout-cell\",\n      property: \"justifyContent\",\n      value: {\n        type: \"keyword\",\n        value: \"flex-start\",\n      },\n    },\n    {\n      selector: \"w-layout-cell\",\n      property: \"alignItems\",\n      value: {\n        type: \"keyword\",\n        value: \"flex-start\",\n      },\n    },\n    {\n      selector: \"w-layout-cell\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"w-layout-vflex\": [\n    {\n      selector: \"w-layout-vflex\",\n      property: \"flexDirection\",\n      value: {\n        type: \"keyword\",\n        value: \"column\",\n      },\n    },\n    {\n      selector: \"w-layout-vflex\",\n      property: \"alignItems\",\n      value: {\n        type: \"keyword\",\n        value: \"flex-start\",\n      },\n    },\n    {\n      selector: \"w-layout-vflex\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"w-layout-hflex\": [\n    {\n      selector: \"w-layout-hflex\",\n      property: \"flexDirection\",\n      value: {\n        type: \"keyword\",\n        value: \"row\",\n      },\n    },\n    {\n      selector: \"w-layout-hflex\",\n      property: \"alignItems\",\n      value: {\n        type: \"keyword\",\n        value: \"flex-start\",\n      },\n    },\n    {\n      selector: \"w-layout-hflex\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"w-embed-youtubevideo\": [\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: \"https://d3e54v103j8qbb.cloudfront.net/static/youtube-placeholder.2b05e7d68d.svg\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 50,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"center\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"cover\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"paddingRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"position\",\n      value: {\n        type: \"keyword\",\n        value: \"relative\",\n      },\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"minHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 75,\n      },\n      state: \":empty\",\n    },\n    {\n      selector: \"w-embed-youtubevideo\",\n      property: \"paddingBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 56.25,\n      },\n      state: \":empty\",\n    },\n  ],\n  \"w-checkbox\": [\n    {\n      selector: \"w-checkbox\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 5,\n      },\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"paddingLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 20,\n      },\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":before\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"content\",\n      value: {\n        type: \"unparsed\",\n        value: '\" \"',\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"clear\",\n      value: {\n        type: \"keyword\",\n        value: \"both\",\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridRowStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridColumnStart\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 1,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridRowEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"gridColumnEnd\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 2,\n      },\n      state: \":after\",\n    },\n    {\n      selector: \"w-checkbox\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"table\",\n      },\n      state: \":after\",\n    },\n  ],\n  \"w-checkbox-input\": [\n    {\n      selector: \"w-checkbox-input\",\n      property: \"float\",\n      value: {\n        type: \"keyword\",\n        value: \"left\",\n      },\n    },\n    {\n      selector: \"w-checkbox-input\",\n      property: \"marginTop\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-checkbox-input\",\n      property: \"marginRight\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-checkbox-input\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"number\",\n        value: 0,\n      },\n    },\n    {\n      selector: \"w-checkbox-input\",\n      property: \"marginLeft\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: -20,\n      },\n    },\n    {\n      selector: \"w-checkbox-input\",\n      property: \"lineHeight\",\n      value: {\n        type: \"keyword\",\n        value: \"normal\",\n      },\n    },\n  ],\n  \"w-checkbox-input--inputType-custom\": [\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: \"https://d3e54v103j8qbb.cloudfront.net/static/custom-checkbox-checkmark.589d534424.svg\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 50,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"center\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"no-repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"cover\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-checkbox-input--inputType-custom\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 3,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 1,\n              },\n              {\n                type: \"rgb\",\n                alpha: 1,\n                r: 56,\n                g: 152,\n                b: 236,\n              },\n            ],\n          },\n        ],\n      },\n    },\n  ],\n  \"w--redirected-checked\": [\n    {\n      selector: \"w--redirected-checked\",\n      property: \"backgroundColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: \"https://d3e54v103j8qbb.cloudfront.net/static/custom-checkbox-checkmark.589d534424.svg\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"backgroundPositionX\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"%\",\n            value: 50,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"backgroundPositionY\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"center\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"backgroundRepeat\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"no-repeat\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"backgroundSize\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"cover\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w--redirected-checked\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n  ],\n  \"w--redirected-focus\": [\n    {\n      selector: \"w--redirected-focus\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 3,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 1,\n              },\n              {\n                type: \"rgb\",\n                alpha: 1,\n                r: 56,\n                g: 152,\n                b: 236,\n              },\n            ],\n          },\n        ],\n      },\n    },\n  ],\n  \"w-form-formradioinput--inputType-custom\": [\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderTopWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderRightWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderBottomWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderLeftWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 4,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderTopStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderRightStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderBottomStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderLeftStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderTopColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderRightColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderBottomColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderLeftColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 56,\n        g: 152,\n        b: 236,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 12,\n      },\n    },\n    {\n      selector: \"w-form-formradioinput--inputType-custom\",\n      property: \"boxShadow\",\n      value: {\n        type: \"layers\",\n        value: [\n          {\n            type: \"tuple\",\n            value: [\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"number\",\n                value: 0,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 3,\n              },\n              {\n                type: \"unit\",\n                unit: \"px\",\n                value: 1,\n              },\n              {\n                type: \"rgb\",\n                alpha: 1,\n                r: 56,\n                g: 152,\n                b: 236,\n              },\n            ],\n          },\n        ],\n      },\n    },\n  ],\n  \"w-form-formrecaptcha\": [\n    {\n      selector: \"w-form-formrecaptcha\",\n      property: \"marginBottom\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 8,\n      },\n    },\n  ],\n  \"w-backgroundvideo-backgroundvideoplaypausebutton\": [\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"outlineOffset\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n      state: \":focus-visible\",\n    },\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"borderTopLeftRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":focus-visible\",\n    },\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"borderTopRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":focus-visible\",\n    },\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"borderBottomRightRadius\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 50,\n      },\n      state: \":focus-visible\",\n    },\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"outlineWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 2,\n      },\n      state: \":focus-visible\",\n    },\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"outlineStyle\",\n      value: {\n        type: \"keyword\",\n        value: \"solid\",\n      },\n      state: \":focus-visible\",\n    },\n    {\n      selector: \"w-backgroundvideo-backgroundvideoplaypausebutton\",\n      property: \"outlineColor\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 59,\n        g: 121,\n        b: 195,\n      },\n      state: \":focus-visible\",\n    },\n  ],\n  \"w-layout-grid\": [\n    {\n      selector: \"w-layout-grid\",\n      property: \"rowGap\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 16,\n      },\n    },\n    {\n      selector: \"w-layout-grid\",\n      property: \"columnGap\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 16,\n      },\n    },\n    {\n      selector: \"w-layout-grid\",\n      property: \"gridTemplateRows\",\n      value: {\n        type: \"tuple\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"auto\",\n          },\n          {\n            type: \"keyword\",\n            value: \"auto\",\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-layout-grid\",\n      property: \"gridTemplateColumns\",\n      value: {\n        type: \"tuple\",\n        value: [\n          {\n            type: \"unit\",\n            unit: \"fr\",\n            value: 1,\n          },\n          {\n            type: \"unit\",\n            unit: \"fr\",\n            value: 1,\n          },\n        ],\n      },\n    },\n    {\n      selector: \"w-layout-grid\",\n      property: \"gridAutoColumns\",\n      value: {\n        type: \"unit\",\n        unit: \"fr\",\n        value: 1,\n      },\n    },\n    {\n      selector: \"w-layout-grid\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"grid\",\n      },\n    },\n  ],\n  \"utility-page-wrap\": [\n    {\n      selector: \"utility-page-wrap\",\n      property: \"justifyContent\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"utility-page-wrap\",\n      property: \"alignItems\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"utility-page-wrap\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"vw\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"utility-page-wrap\",\n      property: \"maxWidth\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"utility-page-wrap\",\n      property: \"height\",\n      value: {\n        type: \"unit\",\n        unit: \"vh\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"utility-page-wrap\",\n      property: \"maxHeight\",\n      value: {\n        type: \"unit\",\n        unit: \"%\",\n        value: 100,\n      },\n    },\n    {\n      selector: \"utility-page-wrap\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"utility-page-content\": [\n    {\n      selector: \"utility-page-content\",\n      property: \"textAlign\",\n      value: {\n        type: \"keyword\",\n        value: \"center\",\n      },\n    },\n    {\n      selector: \"utility-page-content\",\n      property: \"flexDirection\",\n      value: {\n        type: \"keyword\",\n        value: \"column\",\n      },\n    },\n    {\n      selector: \"utility-page-content\",\n      property: \"width\",\n      value: {\n        type: \"unit\",\n        unit: \"px\",\n        value: 260,\n      },\n    },\n    {\n      selector: \"utility-page-content\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n  \"utility-page-form\": [\n    {\n      selector: \"utility-page-form\",\n      property: \"flexDirection\",\n      value: {\n        type: \"keyword\",\n        value: \"column\",\n      },\n    },\n    {\n      selector: \"utility-page-form\",\n      property: \"alignItems\",\n      value: {\n        type: \"keyword\",\n        value: \"stretch\",\n      },\n    },\n    {\n      selector: \"utility-page-form\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"flex\",\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/instances-properties.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport type { Instance, Prop, WebstudioFragment } from \"@webstudio-is/sdk\";\nimport type { WfElementNode, WfNode } from \"./schema\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\n\nconst toFragment = (\n  wfNode: WfElementNode,\n  instanceId: Instance[\"id\"]\n): WebstudioFragment | undefined => {\n  const fragment: WebstudioFragment = {\n    children: [],\n    instances: [],\n    props: [],\n    dataSources: [],\n    styleSourceSelections: [],\n    styleSources: [],\n    styles: [],\n    assets: [],\n    breakpoints: [],\n    resources: [],\n  };\n  const component = wfNode.type;\n\n  const addProp = (\n    name: Prop[\"name\"],\n    value: Prop[\"value\"],\n    type: Prop[\"type\"] = \"string\"\n  ) => {\n    const prop = { id: nanoid(), instanceId };\n\n    if (type === \"string\" && typeof value === \"string\") {\n      fragment.props.push({\n        ...prop,\n        type,\n        name,\n        value,\n      });\n      return;\n    }\n    if (typeof value === \"number\") {\n      fragment.props.push({\n        ...prop,\n        type: \"number\",\n        name,\n        value,\n      });\n      return;\n    }\n\n    if (typeof value === \"boolean\") {\n      fragment.props.push({\n        ...prop,\n        type: \"boolean\",\n        name,\n        value,\n      });\n      return;\n    }\n  };\n\n  const addInstance = ({\n    children = [],\n    ...instance\n  }: {\n    component: Instance[\"component\"];\n    tag?: string;\n    children?: Instance[\"children\"];\n    label?: string;\n  }) => {\n    fragment.instances.push({\n      id: instanceId,\n      type: \"instance\",\n      children,\n      ...instance,\n    });\n  };\n\n  if (wfNode.data?.attr?.id) {\n    addProp(\"id\", wfNode.data.attr.id);\n  }\n\n  // Webflow will have conditions: [false, true] when condition is custom and depends on the collection value\n  // We only support condition that has a single value.\n  const conditions = wfNode.data?.visibility?.conditions ?? [];\n  if (conditions.length === 1 && conditions[0] === false) {\n    addProp(showAttribute, false);\n  }\n\n  switch (component) {\n    case \"LineBreak\": {\n      return fragment;\n    }\n    case \"Heading\": {\n      addInstance({ component, tag: wfNode.tag });\n      return fragment;\n    }\n    case \"List\": {\n      if (wfNode.tag === \"ol\") {\n        addProp(\"ordered\", true);\n      }\n      addInstance({ component });\n      return fragment;\n    }\n    case \"ListItem\":\n    case \"Paragraph\":\n    case \"Superscript\":\n    case \"Subscript\":\n    case \"Blockquote\":\n    case \"Span\": {\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Block\": {\n      const component = wfNode.data?.text ? \"Text\" : \"Box\";\n      addInstance({ component, tag: wfNode.tag });\n      return fragment;\n    }\n\n    case \"Link\": {\n      const component = \"Link\";\n\n      const data = wfNode.data;\n      if (\"url\" in data.link) {\n        addProp(\"href\", data.link.url);\n      }\n      if (\"target\" in data.link) {\n        addProp(\"target\", data.link.target);\n      }\n      if (\"href\" in data.link) {\n        addProp(\"href\", data.link.href);\n      }\n      if (\"email\" in data.link) {\n        const subject = data.link.subject\n          ? `?subject=${data.link.subject}`\n          : \"\";\n        addProp(\"href\", `mailto:${data.link.email}${subject}`);\n      }\n      if (\"tel\" in data.link) {\n        addProp(\"href\", `tel:${data.link.tel}`);\n      }\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Section\": {\n      const component = \"Box\";\n      addInstance({ component, tag: wfNode.tag });\n      return fragment;\n    }\n    case \"RichText\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Strong\": {\n      const component = \"Bold\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Emphasized\": {\n      const component = \"Italic\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Container\":\n    case \"BlockContainer\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Layout\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Cell\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"VFlex\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"HFlex\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Grid\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Row\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Column\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"CodeBlock\": {\n      const component = \"CodeText\";\n      const data = wfNode.data;\n      addProp(\"lang\", data.language);\n      addProp(\"code\", data.code);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"HtmlEmbed\": {\n      addProp(\"code\", wfNode.v);\n      addProp(\"clientOnly\", true);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"Image\": {\n      const data = wfNode.data;\n\n      if (\n        data.attr.alt &&\n        // This is how they tell it when alt comes from image meta during publishing\n        data.attr.alt !== \"__wf_reserved_inherit\" &&\n        // This is how they tell it to use alt=\"\", which is our default anyways\n        data.attr.alt !== \"__wf_reserved_decorative\"\n      ) {\n        addProp(\"alt\", data.attr.alt);\n      }\n\n      if (data.attr.loading === \"eager\" || data.attr.loading === \"lazy\") {\n        addProp(\"loading\", data.attr.loading);\n      }\n\n      if (data.attr.width && data.attr.width !== \"auto\") {\n        addProp(\"width\", data.attr.width);\n      }\n\n      if (data.attr.height && data.attr.height !== \"auto\") {\n        addProp(\"height\", data.attr.height);\n      }\n      if (data.attr.src) {\n        addProp(\"src\", data.attr.src);\n      }\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormWrapper\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormForm\": {\n      const component = \"Box\";\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormErrorMessage\":\n    case \"FormSuccessMessage\": {\n      return;\n    }\n    case \"FormButton\": {\n      const component = \"Button\";\n      const data = wfNode.data;\n      addInstance({\n        component,\n        children: [\n          {\n            type: \"text\" as const,\n            value: data.attr.value,\n          },\n        ],\n      });\n      return fragment;\n    }\n    case \"FormTextInput\": {\n      const data = wfNode.data;\n      const component = \"Input\";\n      addProp(\"name\", data.attr.name);\n      addProp(\"maxLength\", data.attr.maxlength);\n      addProp(\"placeholder\", data.attr.placeholder);\n      addProp(\"disabled\", data.attr.disabled);\n      addProp(\"type\", data.attr.type);\n      addProp(\"required\", data.attr.required);\n      addProp(\"autoFocus\", data.attr.autofocus);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormTextarea\": {\n      const data = wfNode.data;\n      const component = \"Textarea\";\n      addProp(\"name\", data.attr.name);\n      addProp(\"maxLength\", data.attr.maxlength);\n      addProp(\"placeholder\", data.attr.placeholder);\n      addProp(\"required\", data.attr.required);\n      addProp(\"autoFocus\", data.attr.autofocus);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormBlockLabel\": {\n      const data = wfNode.data;\n      const component = \"Label\";\n      addProp(\"htmlFor\", data.attr.for);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormCheckboxWrapper\": {\n      const component = \"Label\";\n      addInstance({ component, label: \"Checkbox Field\" });\n      return fragment;\n    }\n    case \"FormCheckboxInput\": {\n      const component = \"Checkbox\";\n      const data = wfNode.data;\n      addProp(\"name\", data.attr.name);\n      addProp(\"required\", data.attr.required);\n      addProp(\"defaultChecked\", data.attr.checked);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormInlineLabel\": {\n      const component = \"Text\";\n      addInstance({ component, tag: \"span\", label: \"Label\" });\n      return fragment;\n    }\n    case \"FormRadioWrapper\": {\n      const component = \"Label\";\n      addInstance({ component, label: \"Radio Field\" });\n      return fragment;\n    }\n    case \"FormRadioInput\": {\n      const component = \"RadioButton\";\n      const data = wfNode.data;\n      addProp(\"name\", data.attr.name);\n      addProp(\"required\", data.attr.required);\n      addProp(\"value\", data.attr.value);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"FormSelect\": {\n      // @todo https://github.com/webstudio-is/webstudio/issues/3601\n      const component = \"Input\";\n      const data = wfNode.data;\n      addProp(\"name\", data.attr.name);\n      addProp(\"required\", data.attr.required);\n      addProp(\"multiple\", data.attr.multiple);\n      addInstance({ component });\n      return fragment;\n    }\n    case \"LightboxWrapper\": {\n      addProp(\"href\", wfNode.data?.attr?.href);\n      addInstance({ component: \"Box\", tag: wfNode.tag, label: component });\n      return fragment;\n    }\n    case \"NavbarMenu\": {\n      addProp(\"role\", wfNode.data?.attr?.role);\n      addInstance({ component: \"Box\", tag: wfNode.tag, label: component });\n      return fragment;\n    }\n    case \"NavbarContainer\":\n    case \"NavbarButton\":\n    case \"NavbarWrapper\": {\n      addInstance({ component: \"Box\", label: component });\n      return fragment;\n    }\n\n    case \"NavbarBrand\":\n    case \"NavbarLink\": {\n      const data = wfNode.data;\n      if (\"url\" in data.link) {\n        addProp(\"href\", data.link.url);\n      }\n      if (\"target\" in data.link) {\n        addProp(\"target\", data.link.target);\n      }\n      if (\"href\" in data.link) {\n        addProp(\"href\", data.link.href);\n      }\n      if (\"email\" in data.link) {\n        const subject = data.link.subject\n          ? `?subject=${data.link.subject}`\n          : \"\";\n        addProp(\"href\", `mailto:${data.link.email}${subject}`);\n      }\n      if (\"tel\" in data.link) {\n        addProp(\"href\", `tel:${data.link.tel}`);\n      }\n      addInstance({ component: \"Link\", label: component });\n      return fragment;\n    }\n\n    case \"Icon\": {\n      // We don't have access to widget icons\n      addInstance({ component: \"Box\", label: component });\n      return fragment;\n    }\n    default:\n      (component) satisfies never;\n  }\n};\n\nconst addCustomAttributes = (\n  wfNode: WfElementNode,\n  instanceId: Instance[\"id\"],\n  props: Array<Prop>\n) => {\n  if (wfNode.data?.xattr) {\n    for (const attribute of wfNode.data.xattr) {\n      props.push({\n        type: \"string\",\n        id: nanoid(),\n        instanceId,\n        name: attribute.name,\n        value: attribute.value,\n      });\n    }\n  }\n};\n\nexport const addInstanceAndProperties = (\n  wfNode: WfNode,\n  doneNodes: Map<WfNode[\"_id\"], Instance[\"id\"] | false>,\n  wfNodes: Map<WfNode[\"_id\"], WfNode>,\n  fragment: WebstudioFragment\n) => {\n  if (\n    doneNodes.has(wfNode._id) ||\n    \"text\" in wfNode ||\n    \"type\" in wfNode === false\n  ) {\n    return;\n  }\n  const instanceId = nanoid();\n  const nextFragment = toFragment(wfNode, instanceId);\n\n  if (nextFragment === undefined) {\n    // Skip this node and its children.\n    doneNodes.set(wfNode._id, false);\n    for (const childId of wfNode.children) {\n      doneNodes.set(childId, false);\n    }\n    return;\n  }\n\n  const children: Instance[\"children\"] = [];\n  for (const wfChildId of wfNode.children) {\n    const wfChildNode = wfNodes.get(wfChildId);\n    if (wfChildNode === undefined) {\n      continue;\n    }\n    const value =\n      \"text\" in wfChildNode\n        ? wfChildNode.v\n        : wfChildNode.type === \"LineBreak\"\n          ? \"\\n\"\n          : undefined;\n\n    if (value !== undefined) {\n      children.push({\n        type: \"text\",\n        value,\n      });\n      doneNodes.set(wfChildId, instanceId);\n      continue;\n    }\n\n    const childInstanceId = addInstanceAndProperties(\n      wfChildNode,\n      doneNodes,\n      wfNodes,\n      fragment\n    );\n    if (childInstanceId !== undefined) {\n      children.push({\n        type: \"id\",\n        value: childInstanceId,\n      });\n    }\n  }\n\n  fragment.instances.push(...nextFragment.instances);\n  fragment.props.push(...nextFragment.props);\n  const instance = fragment.instances.find(\n    (instance) => instance.id === instanceId\n  );\n\n  if (instance === undefined) {\n    return;\n  }\n  instance.children.push(...children);\n  doneNodes.set(wfNode._id, instanceId);\n  addCustomAttributes(wfNode, instanceId, fragment.props);\n\n  return instanceId;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.test.tsx",
    "content": "import type { JSX } from \"react\";\nimport { test, expect, describe, beforeEach } from \"vitest\";\nimport { nanoid } from \"nanoid\";\nimport {\n  type NestingRule,\n  createRegularStyleSheet,\n} from \"@webstudio-is/css-engine\";\nimport {\n  initialBreakpoints,\n  type WebstudioFragment,\n  type Instance,\n} from \"@webstudio-is/sdk\";\nimport { $, renderData } from \"@webstudio-is/template\";\nimport * as defaultMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport { __testing__ } from \"./plugin-webflow\";\nimport {\n  $breakpoints,\n  $project,\n  $registeredComponentMetas,\n  $styleSources,\n  $styles,\n} from \"../../nano-states\";\nimport invariant from \"tiny-invariant\";\nimport { WfData } from \"./schema\";\n\nconst { toWebstudioFragment } = __testing__;\n\nconst equalFragment = (fragment: WebstudioFragment, jsx: JSX.Element) => {\n  const fragmentInstances = new Map();\n  fragment.instances.forEach((instance: Instance) => {\n    const newId = expect.any(String) as unknown as string;\n    fragmentInstances.set(newId, {\n      ...instance,\n      id: newId,\n      children: instance.children.map((child) => {\n        if (child.type === \"text\") {\n          return child;\n        }\n        return { type: \"id\", value: expect.any(String) as unknown as string };\n      }),\n    });\n  });\n\n  const expected = renderData(jsx);\n\n  const expectedInstances = new Map();\n  for (const instance of expected.instances.values()) {\n    expectedInstances.set(instance.id, instance);\n  }\n  const fragmentProps = new Map();\n  for (const prop of fragment.props) {\n    prop.id = expect.any(String) as unknown as string;\n    prop.instanceId = expect.any(String) as unknown as string;\n    fragmentProps.set(prop.id, prop);\n  }\n  const expectedProps = new Map();\n  for (const prop of expected.props.values()) {\n    prop.id = expect.any(String) as unknown as string;\n    prop.instanceId = expect.any(String) as unknown as string;\n    expectedProps.set(prop.id, prop);\n  }\n  //console.dir(fragmentInstances, { depth: null });\n  //console.dir(expectedInstances, { depth: null });\n  //console.dir(expectedProps, { depth: null });\n  expect(fragmentInstances).toEqual(expectedInstances);\n  expect(fragmentProps).toEqual(expectedProps);\n};\n\nconst toCss = (fragment: WebstudioFragment) => {\n  const sheet = createRegularStyleSheet();\n  for (const breakpoint of fragment.breakpoints) {\n    sheet.addMediaRule(breakpoint.id, breakpoint);\n  }\n  const ruleByStyleSourceId = new Map<string, NestingRule>();\n  for (const styleDecl of fragment.styleSources) {\n    const name = styleDecl.type === \"token\" ? styleDecl.name : \"Local\";\n    const rule = sheet.addNestingRule(name);\n    ruleByStyleSourceId.set(styleDecl.id, rule);\n  }\n  for (const styleDecl of fragment.styles) {\n    const rule = ruleByStyleSourceId.get(styleDecl.styleSourceId);\n    rule?.setDeclaration({\n      breakpoint: styleDecl.breakpointId,\n      selector: styleDecl.state ?? \"\",\n      property: styleDecl.property,\n      value: styleDecl.value,\n    });\n  }\n  return sheet.cssText;\n};\n\nbeforeEach(() => {\n  const defaultMetasMap = new Map(Object.entries(defaultMetas));\n  $registeredComponentMetas.set(defaultMetasMap);\n  $project.set({\n    id: \"test\",\n    createdAt: \"\",\n    domain: \"\",\n    title: \"\",\n    userId: \"\",\n    isDeleted: false,\n    previewImageAsset: null,\n    marketplaceApprovalStatus: \"PENDING\",\n    latestStaticBuild: null,\n    domainsVirtual: [],\n    latestBuildVirtual: null,\n    previewImageAssetId: null,\n    tags: [],\n  });\n\n  $breakpoints.set(\n    new Map(\n      initialBreakpoints.map((breakpoint) => {\n        const id = nanoid();\n        return [id, { ...breakpoint, id }];\n      })\n    )\n  );\n});\n\ntest(\"Heading\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"97d91be2-3bba-d340-0f13-a84e975b7497\",\n          type: \"Heading\",\n          tag: \"h1\",\n          children: [\"97d91be2-3bba-d340-0f13-a84e975b7498\"],\n          classes: [],\n        },\n        {\n          _id: \"97d91be2-3bba-d340-0f13-a84e975b7498\",\n          v: \"Turtle in the sea\",\n          text: true,\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Heading ws:tag=\"h1\">Turtle in the sea</$.Heading>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      h1 {\n        margin-top: 20px;\n        margin-right: 0;\n        margin-bottom: 10px;\n        margin-left: 0;\n        font-size: 38px;\n        font-weight: bold;\n        line-height: 44px\n      }\n    }\"\n  `);\n});\n\ntest(\"Link Block, Button, Text Link\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"97539676-c2ca-2e8f-55f3-6c4a3104a5c0\",\n          type: \"Link\",\n          tag: \"a\",\n          classes: [],\n          children: [],\n          data: {\n            link: {\n              url: \"https://webstudio.is\",\n              target: \"_blank\",\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Link href=\"https://webstudio.is\" target=\"_blank\" />\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      a {\n        background-color: rgb(0 0 0 / 0)\n      }\n      a:active {\n        outline: 0 none currentcolor\n      }\n      a:hover {\n        outline: 0 none currentcolor\n      }\n    }\"\n  `);\n});\n\ntest(\"List and ListItem\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"7e11a800-c8e2-9b14-37cf-09a9e94754ad\",\n          type: \"List\",\n          tag: \"ul\",\n          classes: [],\n          children: [\n            \"7e11a800-c8e2-9b14-37cf-09a9e94754ae\",\n            \"7e11a800-c8e2-9b14-37cf-09a9e94754af\",\n            \"7e11a800-c8e2-9b14-37cf-09a9e94754b0\",\n          ],\n        },\n        {\n          _id: \"7e11a800-c8e2-9b14-37cf-09a9e94754ae\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [],\n        },\n        {\n          _id: \"7e11a800-c8e2-9b14-37cf-09a9e94754af\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [],\n        },\n        {\n          _id: \"7e11a800-c8e2-9b14-37cf-09a9e94754b0\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.List>\n      <$.ListItem />\n      <$.ListItem />\n      <$.ListItem />\n    </$.List>\n  );\n\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      ul {\n        margin-top: 0;\n        margin-bottom: 10px;\n        padding-left: 40px\n      }\n    }\"\n  `);\n});\n\ntest(\"Paragraph\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"dfab64ae-6624-b6db-a909-b85588aa3f8d\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"dfab64ae-6624-b6db-a909-b85588aa3f8e\"],\n        },\n        {\n          _id: \"dfab64ae-6624-b6db-a909-b85588aa3f8e\",\n          text: true,\n          v: \"Text in a paragraph\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Paragraph>Text in a paragraph</$.Paragraph>);\n\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      p {\n        margin-top: 0;\n        margin-bottom: 10px\n      }\n    }\"\n  `);\n});\n\ntest(\"Text\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"adea2109-96eb-63e0-c27f-632a7f40bce8\",\n          type: \"Block\",\n          tag: \"div\",\n          classes: [],\n          children: [\"adea2109-96eb-63e0-c27f-632a7f40bce9\"],\n          data: {\n            text: true,\n          },\n        },\n        {\n          _id: \"adea2109-96eb-63e0-c27f-632a7f40bce9\",\n          text: true,\n          v: \"This is some text inside of a div block.\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Text ws:tag=\"div\">This is some text inside of a div block.</$.Text>\n  );\n\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\"\"`);\n});\n\ntest(\"Blockquote\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Blockquote\",\n          tag: \"blockquote\",\n          classes: [],\n          children: [\"25ffefdf-c015-5edd-7673-933b41a25329\"],\n        },\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25329\",\n          text: true,\n          v: \"Block Quote\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Blockquote>Block Quote</$.Blockquote>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      blockquote {\n        margin-top: 0;\n        margin-right: 0;\n        margin-bottom: 10px;\n        margin-left: 0;\n        padding-top: 10px;\n        padding-right: 20px;\n        padding-bottom: 10px;\n        padding-left: 20px;\n        font-size: 18px;\n        line-height: 22px;\n        border-left: 5px solid rgb(226 226 226 / 1)\n      }\n    }\"\n  `);\n});\n\ntest(\"Strong\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Strong\",\n          tag: \"strong\",\n          classes: [],\n          children: [\"25ffefdf-c015-5edd-7673-933b41a25329\"],\n        },\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25329\",\n          text: true,\n          v: \"Bold Text\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Bold>Bold Text</$.Bold>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      strong {\n        font-weight: bold\n      }\n    }\"\n  `);\n});\n\ntest(\"Emphasized\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Emphasized\",\n          tag: \"em\",\n          classes: [],\n          children: [\"25ffefdf-c015-5edd-7673-933b41a25329\"],\n        },\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25329\",\n          text: true,\n          v: \"Emphasis\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(fragment, <$.Italic>Emphasis</$.Italic>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\"\"`);\n});\n\ntest(\"Superscript\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Superscript\",\n          tag: \"sup\",\n          classes: [],\n          children: [\"25ffefdf-c015-5edd-7673-933b41a25329\"],\n        },\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25329\",\n          text: true,\n          v: \"Superscript\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Superscript>Superscript</$.Superscript>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      sup {\n        vertical-align: baseline;\n        font-size: 75%;\n        line-height: 0;\n        position: relative;\n        top: -0.5em\n      }\n    }\"\n  `);\n});\n\ntest(\"Subscript\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Subscript\",\n          tag: \"sub\",\n          classes: [],\n          children: [\"25ffefdf-c015-5edd-7673-933b41a25329\"],\n        },\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25329\",\n          text: true,\n          v: \"Subscript\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Subscript>Subscript</$.Subscript>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      sub {\n        vertical-align: baseline;\n        font-size: 75%;\n        line-height: 0;\n        position: relative;\n        bottom: -0.25em\n      }\n    }\"\n  `);\n});\n\ntest(\"Section\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Section\",\n          tag: \"section\",\n          classes: [],\n          children: [\"25ffefdf-c015-5edd-7673-933b41a25329\"],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Box ws:tag=\"section\" />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      section {\n        display: block\n      }\n    }\"\n  `);\n});\n\ntest(\"Figure\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"7c6bc1fd-128d-514b-167b-605a910e435c\",\n          type: \"Block\",\n          tag: \"figure\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Box ws:tag=\"figure\" />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      figure {\n        display: block;\n        margin-top: 0;\n        margin-right: 0;\n        margin-bottom: 10px;\n        margin-left: 0\n      }\n    }\"\n  `);\n});\n\ntest(\"BlockContainer\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"BlockContainer\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(fragment, <$.Box />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-layout-blockcontainer {\n        max-width: 940px;\n        margin-left: auto;\n        margin-right: auto;\n        display: block\n      }\n      w-container {\n        max-width: 940px;\n        margin-left: auto;\n        margin-right: auto\n      }\n      w-container:after {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table;\n        clear: both\n      }\n      w-container:before {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table\n      }\n    }\n    @media all and (max-width: 991px) {\n      w-layout-blockcontainer {\n        max-width: 728px\n      }\n      w-container {\n        max-width: 728px\n      }\n    }\n    @media all and (max-width: 767px) {\n      w-layout-blockcontainer {\n        max-width: none\n      }\n    }\n    @media all and (max-width: 479px) {\n      w-container {\n        max-width: none\n      }\n    }\"\n  `);\n});\n\ntest(\"Block\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Block\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Box ws:tag=\"div\" />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\"\"`);\n});\n\ntest(\"V Flex\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"VFlex\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(fragment, <$.Box />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-layout-vflex {\n        flex-direction: column;\n        align-items: flex-start;\n        display: flex\n      }\n    }\"\n  `);\n});\n\ntest(\"H Flex\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"HFlex\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(fragment, <$.Box />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-layout-hflex {\n        flex-direction: row;\n        align-items: flex-start;\n        display: flex\n      }\n    }\"\n  `);\n});\n\ntest(\"QuickStack with instance styles\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"aeb3895f-67c9-b5a0-69b4-7960b893ec04\",\n          type: \"Layout\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"ca264069-ff87-c2e5-7dc0-a4c0ec0e9146\",\n            \"ec30cdfd-1a60-eeb0-1bfa-1df6f1956ed5\",\n          ],\n          data: {\n            style: {\n              base: {\n                main: {\n                  noPseudo: {\n                    gridTemplateColumns: \"1fr 1fr\",\n                    gridTemplateRows: \"auto\",\n                    order: 1,\n                  },\n                },\n              },\n            },\n          },\n        },\n        {\n          _id: \"ca264069-ff87-c2e5-7dc0-a4c0ec0e9146\",\n          type: \"Cell\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n        {\n          _id: \"ec30cdfd-1a60-eeb0-1bfa-1df6f1956ed5\",\n          type: \"Cell\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(\n    fragment,\n    <$.Box>\n      <$.Box />\n      <$.Box />\n    </$.Box>\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-layout-layout {\n        row-gap: 20px;\n        column-gap: 20px;\n        grid-auto-columns: 1fr;\n        justify-content: center;\n        padding: 20px\n      }\n      wf-layout-layout {\n        display: grid\n      }\n      Local {\n        grid-template-columns: 1fr 1fr;\n        grid-template-rows: auto;\n        order: 1\n      }\n      w-layout-cell {\n        flex-direction: column;\n        justify-content: flex-start;\n        align-items: flex-start;\n        display: flex\n      }\n    }\"\n  `);\n});\n\ntest(\"Grid\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"25ffefdf-c015-5edd-7673-933b41a25328\",\n          type: \"Grid\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Box />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-layout-grid {\n        row-gap: 16px;\n        column-gap: 16px;\n        grid-template-rows: auto auto;\n        grid-template-columns: 1fr 1fr;\n        grid-auto-columns: 1fr;\n        display: grid\n      }\n    }\"\n  `);\n});\n\ntest(\"Columns\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"08fb88d6-f6ec-5169-f4d4-8dac98df2b58\",\n          type: \"Row\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"08fb88d6-f6ec-5169-f4d4-8dac98df2b59\",\n            \"08fb88d6-f6ec-5169-f4d4-8dac98df2b5a\",\n          ],\n        },\n        {\n          _id: \"08fb88d6-f6ec-5169-f4d4-8dac98df2b59\",\n          type: \"Column\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n        {\n          _id: \"08fb88d6-f6ec-5169-f4d4-8dac98df2b5a\",\n          type: \"Column\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Box>\n      <$.Box />\n      <$.Box />\n    </$.Box>\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-row:after {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table;\n        clear: both\n      }\n      w-row:before {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table\n      }\n      w-col {\n        float: left;\n        width: 100%;\n        min-height: 1px;\n        padding-left: 10px;\n        padding-right: 10px;\n        position: relative\n      }\n    }\n    @media all and (max-width: 767px) {\n      w-row {\n        margin-left: 0;\n        margin-right: 0\n      }\n      w-col {\n        width: 100%;\n        left: auto;\n        right: auto\n      }\n    }\n    @media all and (max-width: 479px) {\n      w-col {\n        width: 100%\n      }\n    }\"\n  `);\n});\n\ntest(\"Image\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"3c0b6a7a-830f-4b4a-48c5-4215f9c9389a\",\n          type: \"Image\",\n          tag: \"img\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              src: \"https://test.com/image.jpg\",\n              loading: \"eager\",\n              width: \"200\",\n              height: \"auto\",\n              alt: \"Test\",\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Image\n      alt=\"Test\"\n      loading=\"eager\"\n      width=\"200\"\n      src=\"https://test.com/image.jpg\"\n    />\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      img {\n        vertical-align: middle;\n        max-width: 100%;\n        display: inline-block;\n        border: 0 none currentcolor\n      }\n    }\"\n  `);\n});\n\ntest(\"HtmlEmbed\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"88131b38-7a58-8085-38d2-dc51c5ce887e\",\n          type: \"HtmlEmbed\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n          v: \"some html\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(fragment, <$.HtmlEmbed code=\"some html\" clientOnly={true} />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-embed {\n        display: block\n      }\n      w-embed:after {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table;\n        clear: both\n      }\n      w-embed:before {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table\n      }\n    }\"\n  `);\n});\n\ntest(\"CodeBlock\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"f06b6679-6414-3592-a6e3-b59196420d7f\",\n          type: \"CodeBlock\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n          data: {\n            code: \"test\",\n            language: \"javascript\",\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.CodeText lang=\"javascript\" code=\"test\" />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-code-block {\n        margin-top: unset;\n        margin-right: unset;\n        margin-bottom: unset;\n        margin-left: unset\n      }\n    }\"\n  `);\n});\n\ntest(\"RichText\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"33178bae-2d99-7d1f-ffa5-101356769dad\",\n          type: \"RichText\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"8418336b-918d-3160-1d76-8c9de474cb43\",\n            \"63929533-a4de-17c6-3b13-351aa16c6e10\",\n            \"7724e6e0-9e3c-4526-35a9-f11dcd628c35\",\n            \"d760a6fd-5da7-7d24-4f4c-24e68a651758\",\n            \"49c71aa2-c61b-4823-dfdb-dc3badea814b\",\n            \"28784747-282c-fe17-a81b-c36c9212b78e\",\n            \"6e4c4ff0-0ebe-1de0-d94d-6f18f08f0762\",\n            \"88e7fd8c-98ff-49f7-aff1-3286e2e0e211\",\n            \"63202791-9c33-bc97-b49e-71a2af805488\",\n            \"b386cecd-1697-e7e8-6c7b-66f36261fec2\",\n            \"98b880aa-7ba5-c754-8069-b83cb198ec32\",\n            \"2e3bf379-e9ee-182c-9f92-1e1409ba1dd7\",\n            \"43d15a61-5f5d-d4bd-4a8c-04b80371e808\",\n            \"676d66b8-3df9-45a5-b2a4-62b75b623443\",\n            \"57268510-0593-b1df-7786-f1f3b3d02830\",\n            \"cd959e82-0fe8-a4a7-6b9f-c5fab19aa6ec\",\n            \"10389bae-2b31-83d5-919f-45fc50689288\",\n          ],\n        },\n        {\n          _id: \"8418336b-918d-3160-1d76-8c9de474cb43\",\n          type: \"Heading\",\n          tag: \"h1\",\n          classes: [],\n          children: [\"c1759a8a-6657-5c74-4e95-33c8a721caab\"],\n        },\n        {\n          _id: \"c1759a8a-6657-5c74-4e95-33c8a721caab\",\n          text: true,\n          v: \"Heading 1\",\n        },\n        {\n          _id: \"63929533-a4de-17c6-3b13-351aa16c6e10\",\n          type: \"Heading\",\n          tag: \"h2\",\n          classes: [],\n          children: [\"d04d84bb-f382-5af4-482a-e6a0aaab168b\"],\n        },\n        {\n          _id: \"d04d84bb-f382-5af4-482a-e6a0aaab168b\",\n          text: true,\n          v: \"Heading 2\",\n        },\n        {\n          _id: \"7724e6e0-9e3c-4526-35a9-f11dcd628c35\",\n          type: \"Heading\",\n          tag: \"h3\",\n          classes: [],\n          children: [\"409d9647-ea51-e048-2bfd-cbbee2efb0c6\"],\n        },\n        {\n          _id: \"409d9647-ea51-e048-2bfd-cbbee2efb0c6\",\n          text: true,\n          v: \"Heading 3\",\n        },\n        {\n          _id: \"d760a6fd-5da7-7d24-4f4c-24e68a651758\",\n          type: \"Heading\",\n          tag: \"h4\",\n          classes: [],\n          children: [\"4ce7fd9b-c7e9-186c-853a-931c507e5d1d\"],\n        },\n        {\n          _id: \"4ce7fd9b-c7e9-186c-853a-931c507e5d1d\",\n          text: true,\n          v: \"Heading 4\",\n        },\n        {\n          _id: \"49c71aa2-c61b-4823-dfdb-dc3badea814b\",\n          type: \"Heading\",\n          tag: \"h5\",\n          classes: [],\n          children: [\"f28abd5f-e21c-08f5-c6be-ec335166114d\"],\n        },\n        {\n          _id: \"f28abd5f-e21c-08f5-c6be-ec335166114d\",\n          text: true,\n          v: \"Heading 5\",\n        },\n        {\n          _id: \"28784747-282c-fe17-a81b-c36c9212b78e\",\n          type: \"Heading\",\n          tag: \"h6\",\n          classes: [],\n          children: [\"6f753bba-a1d9-5092-e605-2f1756fc4a3d\"],\n        },\n        {\n          _id: \"6f753bba-a1d9-5092-e605-2f1756fc4a3d\",\n          text: true,\n          v: \"Heading 6\",\n        },\n        {\n          _id: \"6e4c4ff0-0ebe-1de0-d94d-6f18f08f0762\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"27e847ff-962c-0782-1278-2f4ff342aaad\"],\n        },\n        {\n          _id: \"27e847ff-962c-0782-1278-2f4ff342aaad\",\n          text: true,\n          v: \"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\",\n        },\n        {\n          _id: \"88e7fd8c-98ff-49f7-aff1-3286e2e0e211\",\n          type: \"Blockquote\",\n          tag: \"blockquote\",\n          classes: [],\n          children: [\"9c1d38a3-512b-4f90-e6c4-cadf585cb608\"],\n        },\n        {\n          _id: \"9c1d38a3-512b-4f90-e6c4-cadf585cb608\",\n          text: true,\n          v: \"Block quote\",\n        },\n        {\n          _id: \"63202791-9c33-bc97-b49e-71a2af805488\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"3080a095-fc1a-9de8-e6d1-21c801aed169\"],\n        },\n        {\n          _id: \"3080a095-fc1a-9de8-e6d1-21c801aed169\",\n          text: true,\n          v: \"Ordered list\",\n        },\n        {\n          _id: \"b386cecd-1697-e7e8-6c7b-66f36261fec2\",\n          type: \"List\",\n          tag: \"ol\",\n          classes: [],\n          children: [\n            \"aafe8235-0ff4-c6f4-8382-40fc34900b75\",\n            \"0fd98471-33b6-e8d2-76f1-6e373849e6ba\",\n            \"2ccfda89-a200-d286-440a-f94699163329\",\n          ],\n        },\n        {\n          _id: \"aafe8235-0ff4-c6f4-8382-40fc34900b75\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [\"bf312343-69b6-bec7-5ee1-e19154d85b62\"],\n        },\n        {\n          _id: \"bf312343-69b6-bec7-5ee1-e19154d85b62\",\n          text: true,\n          v: \"Item 1\",\n        },\n        {\n          _id: \"0fd98471-33b6-e8d2-76f1-6e373849e6ba\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [\"161683ac-eed6-7a68-64f7-706c42984c65\"],\n        },\n        {\n          _id: \"161683ac-eed6-7a68-64f7-706c42984c65\",\n          text: true,\n          v: \"Item 2\",\n        },\n        {\n          _id: \"2ccfda89-a200-d286-440a-f94699163329\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [\"59f4270b-72e6-99b1-041a-5ab836481b5c\"],\n        },\n        {\n          _id: \"59f4270b-72e6-99b1-041a-5ab836481b5c\",\n          text: true,\n          v: \"Item 3\",\n        },\n        {\n          _id: \"98b880aa-7ba5-c754-8069-b83cb198ec32\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"b6755218-5a0d-c537-43c0-071245d7a472\"],\n        },\n        {\n          _id: \"b6755218-5a0d-c537-43c0-071245d7a472\",\n          text: true,\n          v: \"Unordered list\",\n        },\n        {\n          _id: \"2e3bf379-e9ee-182c-9f92-1e1409ba1dd7\",\n          type: \"List\",\n          tag: \"ul\",\n          classes: [],\n          children: [\n            \"a90c5835-61e2-e1e6-dfd3-0b02a34ad857\",\n            \"748eb739-12d7-26fc-b8ee-f2c460f35170\",\n            \"54178296-1686-9b14-a341-42ce51958ce5\",\n          ],\n        },\n        {\n          _id: \"a90c5835-61e2-e1e6-dfd3-0b02a34ad857\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [\"8557d8cf-09de-52c0-4631-c2123fc056d1\"],\n        },\n        {\n          _id: \"8557d8cf-09de-52c0-4631-c2123fc056d1\",\n          text: true,\n          v: \"Item A\",\n        },\n        {\n          _id: \"748eb739-12d7-26fc-b8ee-f2c460f35170\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [\"666b664d-8023-50bb-453a-36017c7a3a38\"],\n        },\n        {\n          _id: \"666b664d-8023-50bb-453a-36017c7a3a38\",\n          text: true,\n          v: \"Item B\",\n        },\n        {\n          _id: \"54178296-1686-9b14-a341-42ce51958ce5\",\n          type: \"ListItem\",\n          tag: \"li\",\n          classes: [],\n          children: [\"15647123-fd1c-5928-3894-d666bc6f3c84\"],\n        },\n        {\n          _id: \"15647123-fd1c-5928-3894-d666bc6f3c84\",\n          text: true,\n          v: \"Item C\",\n        },\n        {\n          _id: \"43d15a61-5f5d-d4bd-4a8c-04b80371e808\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"db8cce1a-40ff-ccb7-5f18-ebb811777409\"],\n        },\n        {\n          _id: \"db8cce1a-40ff-ccb7-5f18-ebb811777409\",\n          type: \"Link\",\n          tag: \"a\",\n          classes: [],\n          children: [\"a8af1cd8-157c-4b92-c27e-bca1ae5e6668\"],\n          data: {\n            button: false,\n            block: \"\",\n            link: {\n              url: \"https://university.webflow.com/lesson/add-and-nest-text-links-in-webflow\",\n            },\n          },\n        },\n        {\n          _id: \"a8af1cd8-157c-4b92-c27e-bca1ae5e6668\",\n          text: true,\n          v: \"Text link\",\n        },\n        {\n          _id: \"676d66b8-3df9-45a5-b2a4-62b75b623443\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"4f0b961b-5774-f441-e97f-e84a75efcf08\"],\n        },\n        {\n          _id: \"4f0b961b-5774-f441-e97f-e84a75efcf08\",\n          type: \"Strong\",\n          tag: \"strong\",\n          classes: [],\n          children: [\"5839c9b9-4281-f778-f7f9-c4844452295e\"],\n        },\n        {\n          _id: \"5839c9b9-4281-f778-f7f9-c4844452295e\",\n          text: true,\n          v: \"Bold text\",\n        },\n        {\n          _id: \"57268510-0593-b1df-7786-f1f3b3d02830\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"db4e59e1-b859-d2bc-ee4a-08c638f088db\"],\n        },\n        {\n          _id: \"db4e59e1-b859-d2bc-ee4a-08c638f088db\",\n          type: \"Emphasized\",\n          tag: \"em\",\n          classes: [],\n          children: [\"c2661284-144d-5052-447a-ffe78f3b8bbe\"],\n        },\n        {\n          _id: \"c2661284-144d-5052-447a-ffe78f3b8bbe\",\n          text: true,\n          v: \"Emphasis\",\n        },\n        {\n          _id: \"cd959e82-0fe8-a4a7-6b9f-c5fab19aa6ec\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"debd47f8-ddd2-8ead-af6c-5e42a16e15d0\"],\n        },\n        {\n          _id: \"debd47f8-ddd2-8ead-af6c-5e42a16e15d0\",\n          type: \"Superscript\",\n          tag: \"sup\",\n          classes: [],\n          children: [\"e6640190-f224-5a57-6181-55a793c00da8\"],\n        },\n        {\n          _id: \"e6640190-f224-5a57-6181-55a793c00da8\",\n          text: true,\n          v: \"Superscript\",\n        },\n        {\n          _id: \"10389bae-2b31-83d5-919f-45fc50689288\",\n          type: \"Paragraph\",\n          tag: \"p\",\n          classes: [],\n          children: [\"ec162f35-9226-0e8a-c0c2-32685ef444c1\"],\n        },\n        {\n          _id: \"ec162f35-9226-0e8a-c0c2-32685ef444c1\",\n          type: \"Subscript\",\n          tag: \"sub\",\n          classes: [],\n          children: [\"dcdc8f43-3cdc-1522-e397-60968e378b92\"],\n        },\n        {\n          _id: \"dcdc8f43-3cdc-1522-e397-60968e378b92\",\n          text: true,\n          v: \"Subscript\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Box>\n      <$.Heading ws:tag=\"h1\">Heading 1</$.Heading>\n      <$.Heading ws:tag=\"h2\">Heading 2</$.Heading>\n      <$.Heading ws:tag=\"h3\">Heading 3</$.Heading>\n      <$.Heading ws:tag=\"h4\">Heading 4</$.Heading>\n      <$.Heading ws:tag=\"h5\">Heading 5</$.Heading>\n      <$.Heading ws:tag=\"h6\">Heading 6</$.Heading>\n      <$.Paragraph>\n        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\n        tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim\n        veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea\n        commodo consequat. Duis aute irure dolor in reprehenderit in voluptate\n        velit esse cillum dolore eu fugiat nulla pariatur.\n      </$.Paragraph>\n      <$.Blockquote>Block quote</$.Blockquote>\n      <$.Paragraph>Ordered list</$.Paragraph>\n      <$.List ordered={true}>\n        <$.ListItem>Item 1</$.ListItem>\n        <$.ListItem>Item 2</$.ListItem>\n        <$.ListItem>Item 3</$.ListItem>\n      </$.List>\n      <$.Paragraph>Unordered list</$.Paragraph>\n      <$.List>\n        <$.ListItem>Item A</$.ListItem>\n        <$.ListItem>Item B</$.ListItem>\n        <$.ListItem>Item C</$.ListItem>\n      </$.List>\n      <$.Paragraph>\n        <$.Link href=\"https://university.webflow.com/lesson/add-and-nest-text-links-in-webflow\">\n          Text link\n        </$.Link>\n      </$.Paragraph>\n      <$.Paragraph>\n        <$.Bold>Bold text</$.Bold>\n      </$.Paragraph>\n      <$.Paragraph>\n        <$.Italic>Emphasis</$.Italic>\n      </$.Paragraph>\n      <$.Paragraph>\n        <$.Superscript>Superscript</$.Superscript>\n      </$.Paragraph>\n      <$.Paragraph>\n        <$.Subscript>Subscript</$.Subscript>\n      </$.Paragraph>\n    </$.Box>\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      h1 {\n        margin-top: 20px;\n        margin-right: 0;\n        margin-bottom: 10px;\n        margin-left: 0;\n        font-size: 38px;\n        font-weight: bold;\n        line-height: 44px\n      }\n      h2 {\n        margin-bottom: 10px;\n        font-weight: bold;\n        margin-top: 20px;\n        font-size: 32px;\n        line-height: 36px\n      }\n      h3 {\n        margin-bottom: 10px;\n        font-weight: bold;\n        margin-top: 20px;\n        font-size: 24px;\n        line-height: 30px\n      }\n      h4 {\n        margin-bottom: 10px;\n        font-weight: bold;\n        margin-top: 10px;\n        font-size: 18px;\n        line-height: 24px\n      }\n      h5 {\n        margin-bottom: 10px;\n        font-weight: bold;\n        margin-top: 10px;\n        font-size: 14px;\n        line-height: 20px\n      }\n      h6 {\n        margin-bottom: 10px;\n        font-weight: bold;\n        margin-top: 10px;\n        font-size: 12px;\n        line-height: 18px\n      }\n      p {\n        margin-top: 0;\n        margin-bottom: 10px\n      }\n      blockquote {\n        margin-top: 0;\n        margin-right: 0;\n        margin-bottom: 10px;\n        margin-left: 0;\n        padding-top: 10px;\n        padding-right: 20px;\n        padding-bottom: 10px;\n        padding-left: 20px;\n        font-size: 18px;\n        line-height: 22px;\n        border-left: 5px solid rgb(226 226 226 / 1)\n      }\n      ol {\n        margin-top: 0;\n        margin-bottom: 10px;\n        padding-left: 40px\n      }\n      ul {\n        margin-top: 0;\n        margin-bottom: 10px;\n        padding-left: 40px\n      }\n      a {\n        background-color: rgb(0 0 0 / 0)\n      }\n      a:active {\n        outline: 0 none currentcolor\n      }\n      a:hover {\n        outline: 0 none currentcolor\n      }\n      strong {\n        font-weight: bold\n      }\n      sup {\n        vertical-align: baseline;\n        font-size: 75%;\n        line-height: 0;\n        position: relative;\n        top: -0.5em\n      }\n      sub {\n        vertical-align: baseline;\n        font-size: 75%;\n        line-height: 0;\n        position: relative;\n        bottom: -0.25em\n      }\n    }\"\n  `);\n});\n\ntest(\"Form\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"f19c775e-a9ef-fd0c-2d16-aa53c69e9da4\",\n          type: \"FormWrapper\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"11c85957-89b5-e4f8-da11-f1acb4223a9f\",\n            \"492633d9-7425-ba09-036f-e43213b9875b\",\n            \"a0877638-7523-ac57-7ed9-19f5469e79d1\",\n          ],\n        },\n        {\n          _id: \"11c85957-89b5-e4f8-da11-f1acb4223a9f\",\n          type: \"FormForm\",\n          tag: \"form\",\n          classes: [],\n          children: [\"eeb469a5-8b03-8002-7dec-586856365387\"],\n        },\n        {\n          _id: \"eeb469a5-8b03-8002-7dec-586856365387\",\n          type: \"FormTextInput\",\n          tag: \"input\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              id: \"name\",\n              name: \"name\",\n              maxlength: 256,\n              placeholder: \"\",\n              disabled: false,\n              type: \"text\",\n              required: false,\n              autofocus: false,\n            },\n          },\n        },\n        {\n          _id: \"492633d9-7425-ba09-036f-e43213b9875b\",\n          type: \"FormSuccessMessage\",\n          tag: \"div\",\n          classes: [],\n          children: [\"442d7e86-2c77-961c-ab1b-431bd34abcf7\"],\n        },\n        {\n          _id: \"442d7e86-2c77-961c-ab1b-431bd34abcf7\",\n          type: \"Block\",\n          tag: \"div\",\n          classes: [],\n          children: [\"875baf7f-6c98-b379-0cbe-59b5fdd2c112\"],\n        },\n        {\n          _id: \"875baf7f-6c98-b379-0cbe-59b5fdd2c112\",\n          text: true,\n          v: \"Thank you! Your submission has been received!\",\n        },\n        {\n          _id: \"a0877638-7523-ac57-7ed9-19f5469e79d1\",\n          type: \"FormErrorMessage\",\n          tag: \"div\",\n          classes: [],\n          children: [\"9eaf0a19-b2fa-7aef-4825-41f6319d790d\"],\n        },\n        {\n          _id: \"9eaf0a19-b2fa-7aef-4825-41f6319d790d\",\n          type: \"Block\",\n          tag: \"div\",\n          classes: [],\n          children: [\"a87685fe-24b2-31ff-e229-40c71f6155dd\"],\n        },\n        {\n          _id: \"a87685fe-24b2-31ff-e229-40c71f6155dd\",\n          text: true,\n          v: \"Oops! Something went wrong while submitting the form.\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Box>\n      <$.Box>\n        <$.Input\n          id=\"name\"\n          name=\"name\"\n          maxLength={256}\n          placeholder=\"\"\n          disabled={false}\n          type=\"text\"\n          required={false}\n          autoFocus={false}\n        />\n      </$.Box>\n    </$.Box>\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-form {\n        margin-top: 0;\n        margin-right: 0;\n        margin-bottom: 15px;\n        margin-left: 0\n      }\n      input {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: normal;\n        font-family: inherit;\n        appearance: button;\n        cursor: pointer;\n        box-sizing: border-box;\n        height: auto;\n        margin: 0;\n        padding: 0\n      }\n      input::-moz-focus-inner {\n        border: 0 none currentcolor;\n        padding: 0\n      }\n      w-input {\n        color: rgb(51 51 51 / 1);\n        vertical-align: middle;\n        background-color: rgb(238 238 238 / 1);\n        width: 100%;\n        height: auto;\n        margin-bottom: 10px;\n        padding-top: 8px;\n        padding-right: 12px;\n        padding-bottom: 8px;\n        padding-left: 12px;\n        font-size: 14px;\n        line-height: 1.42857;\n        display: block;\n        cursor: not-allowed;\n        border: 1px solid rgb(204 204 204 / 1)\n      }\n      w-input:-moz-placeholder {\n        color: rgb(153 153 153 / 1)\n      }\n      w-input::-moz-placeholder {\n        color: rgb(153 153 153 / 1);\n        opacity: 1\n      }\n      w-input::-webkit-input-placeholder {\n        color: rgb(153 153 153 / 1)\n      }\n      w-input:focus {\n        border-top-color: rgb(56 152 236 / 1);\n        border-right-color: rgb(56 152 236 / 1);\n        border-bottom-color: rgb(56 152 236 / 1);\n        border-left-color: rgb(56 152 236 / 1);\n        outline: 0 none currentcolor\n      }\n    }\"\n  `);\n});\n\ntest(\"FormButton\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"b7d4b56c-77eb-a79d-4b73-7802d6c5f74a\",\n          type: \"FormButton\",\n          tag: \"input\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              value: \"Submit\",\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Button>Submit</$.Button>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      input {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: normal;\n        font-family: inherit;\n        appearance: button;\n        cursor: pointer;\n        box-sizing: border-box;\n        height: auto;\n        margin: 0;\n        padding: 0\n      }\n      input::-moz-focus-inner {\n        border: 0 none currentcolor;\n        padding: 0\n      }\n      w-button {\n        color: rgb(255 255 255 / 1);\n        line-height: inherit;\n        cursor: pointer;\n        background-color: rgb(56 152 236 / 1);\n        border-top-left-radius: 0;\n        border-top-right-radius: 0;\n        border-bottom-right-radius: 0;\n        padding-top: 9px;\n        padding-right: 15px;\n        padding-bottom: 9px;\n        padding-left: 15px;\n        text-decoration-line: none;\n        text-decoration-style: solid;\n        text-decoration-color: currentcolor;\n        display: inline-block;\n        appearance: button;\n        border: 0 none currentcolor\n      }\n    }\"\n  `);\n});\n\ntest(\"FormTextInput\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"3b60c6c7-296c-f98b-0557-a55dcac084b3\",\n          type: \"FormTextInput\",\n          tag: \"input\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              id: \"email\",\n              name: \"email\",\n              maxlength: 256,\n              placeholder: \"\",\n              disabled: false,\n              type: \"email\",\n              required: true,\n              autofocus: false,\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Input\n      id=\"email\"\n      name=\"email\"\n      maxLength={256}\n      placeholder=\"\"\n      disabled={false}\n      type=\"email\"\n      required={true}\n      autoFocus={false}\n    />\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      input {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: normal;\n        font-family: inherit;\n        appearance: button;\n        cursor: pointer;\n        box-sizing: border-box;\n        height: auto;\n        margin: 0;\n        padding: 0\n      }\n      input::-moz-focus-inner {\n        border: 0 none currentcolor;\n        padding: 0\n      }\n      w-input {\n        color: rgb(51 51 51 / 1);\n        vertical-align: middle;\n        background-color: rgb(238 238 238 / 1);\n        width: 100%;\n        height: auto;\n        margin-bottom: 10px;\n        padding-top: 8px;\n        padding-right: 12px;\n        padding-bottom: 8px;\n        padding-left: 12px;\n        font-size: 14px;\n        line-height: 1.42857;\n        display: block;\n        cursor: not-allowed;\n        border: 1px solid rgb(204 204 204 / 1)\n      }\n      w-input:-moz-placeholder {\n        color: rgb(153 153 153 / 1)\n      }\n      w-input::-moz-placeholder {\n        color: rgb(153 153 153 / 1);\n        opacity: 1\n      }\n      w-input::-webkit-input-placeholder {\n        color: rgb(153 153 153 / 1)\n      }\n      w-input:focus {\n        border-top-color: rgb(56 152 236 / 1);\n        border-right-color: rgb(56 152 236 / 1);\n        border-bottom-color: rgb(56 152 236 / 1);\n        border-left-color: rgb(56 152 236 / 1);\n        outline: 0 none currentcolor\n      }\n    }\"\n  `);\n});\n\ntest(\"FormBlockLabel\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"0fdc679b-9891-dfb3-698a-0114d0c6c729\",\n          type: \"FormBlockLabel\",\n          tag: \"label\",\n          classes: [],\n          children: [\"7827c7aa-cd90-8bda-4570-605f2a1a0d61\"],\n          data: {\n            attr: {\n              for: \"email\",\n            },\n          },\n        },\n        {\n          _id: \"7827c7aa-cd90-8bda-4570-605f2a1a0d61\",\n          text: true,\n          v: \"Email Address\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Label htmlFor=\"email\">Email Address</$.Label>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      label {\n        margin-bottom: 5px;\n        font-weight: bold;\n        display: block\n      }\n    }\"\n  `);\n});\n\ntest(\"FormTextarea\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"ff81988e-1cc2-8c93-d264-d702d846c30a\",\n          type: \"FormTextarea\",\n          tag: \"textarea\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              id: \"field\",\n              name: \"field\",\n              maxlength: 5000,\n              placeholder: \"Example Text\",\n              required: false,\n              autofocus: false,\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Textarea\n      id=\"field\"\n      name=\"field\"\n      maxLength={5000}\n      placeholder=\"Example Text\"\n      required={false}\n      autoFocus={false}\n    />\n  );\n\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      textarea {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: inherit;\n        font-family: inherit;\n        overflow-x: auto;\n        overflow-y: auto;\n        height: auto;\n        margin: 0\n      }\n      w-input {\n        color: rgb(51 51 51 / 1);\n        vertical-align: middle;\n        background-color: rgb(238 238 238 / 1);\n        width: 100%;\n        height: auto;\n        margin-bottom: 10px;\n        padding-top: 8px;\n        padding-right: 12px;\n        padding-bottom: 8px;\n        padding-left: 12px;\n        font-size: 14px;\n        line-height: 1.42857;\n        display: block;\n        cursor: not-allowed;\n        border: 1px solid rgb(204 204 204 / 1)\n      }\n      w-input:-moz-placeholder {\n        color: rgb(153 153 153 / 1)\n      }\n      w-input::-moz-placeholder {\n        color: rgb(153 153 153 / 1);\n        opacity: 1\n      }\n      w-input::-webkit-input-placeholder {\n        color: rgb(153 153 153 / 1)\n      }\n      w-input:focus {\n        border-top-color: rgb(56 152 236 / 1);\n        border-right-color: rgb(56 152 236 / 1);\n        border-bottom-color: rgb(56 152 236 / 1);\n        border-left-color: rgb(56 152 236 / 1);\n        outline: 0 none currentcolor\n      }\n    }\"\n  `);\n});\n\ntest(\"FormBlockLabel\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"0fdc679b-9891-dfb3-698a-0114d0c6c729\",\n          type: \"FormBlockLabel\",\n          tag: \"label\",\n          classes: [],\n          children: [\"7827c7aa-cd90-8bda-4570-605f2a1a0d61\"],\n          data: {\n            attr: {\n              for: \"email\",\n            },\n          },\n        },\n        {\n          _id: \"7827c7aa-cd90-8bda-4570-605f2a1a0d61\",\n          text: true,\n          v: \"Email Address\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(fragment, <$.Label htmlFor=\"email\">Email Address</$.Label>);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      label {\n        margin-bottom: 5px;\n        font-weight: bold;\n        display: block\n      }\n    }\"\n  `);\n});\n\ntest(\"FormCheckboxWrapper, FormCheckboxInput, FormInlineLabel\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"4b49f914-6011-9030-5727-232bbddc7d56\",\n          type: \"FormCheckboxWrapper\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"2e32acc8-7027-1741-1be1-cc72606b57cf\",\n            \"3f68e88d-5c03-698e-c4e2-a6ad6ff1a00c\",\n          ],\n        },\n        {\n          _id: \"2e32acc8-7027-1741-1be1-cc72606b57cf\",\n          type: \"FormCheckboxInput\",\n          tag: \"input\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              type: \"checkbox\",\n              name: \"checkbox\",\n              id: \"checkbox\",\n              required: false,\n              checked: false,\n            },\n          },\n        },\n        {\n          _id: \"3f68e88d-5c03-698e-c4e2-a6ad6ff1a00c\",\n          type: \"FormInlineLabel\",\n          tag: \"label\",\n          classes: [],\n          children: [\"18550f45-3a50-27d6-4524-b91a226ded60\"],\n        },\n        {\n          _id: \"18550f45-3a50-27d6-4524-b91a226ded60\",\n          text: true,\n          v: \"Checkbox\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Label ws:label=\"Checkbox Field\">\n      <$.Checkbox\n        id=\"checkbox\"\n        name=\"checkbox\"\n        required={false}\n        defaultChecked={false}\n      />\n      <$.Text ws:tag=\"span\" ws:label=\"Label\">\n        Checkbox\n      </$.Text>\n    </$.Label>\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-checkbox {\n        margin-bottom: 5px;\n        padding-left: 20px;\n        display: block\n      }\n      w-checkbox:after {\n        content: \" \";\n        clear: both;\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table\n      }\n      w-checkbox:before {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table\n      }\n      input {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: normal;\n        font-family: inherit;\n        appearance: button;\n        cursor: pointer;\n        box-sizing: border-box;\n        height: auto;\n        margin: 0;\n        padding: 0\n      }\n      input::-moz-focus-inner {\n        border: 0 none currentcolor;\n        padding: 0\n      }\n      w-checkbox-input {\n        float: left;\n        margin-top: 4px;\n        margin-right: 0;\n        margin-bottom: 0;\n        margin-left: -20px;\n        line-height: normal\n      }\n      label {\n        margin-bottom: 5px;\n        font-weight: bold;\n        display: block\n      }\n      w-form-label {\n        cursor: pointer;\n        margin-bottom: 0;\n        font-weight: normal;\n        display: inline-block\n      }\n    }\"\n  `);\n});\n\ntest(\"FormRadioWrapper, FormRadioInput, FormInlineLabel\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"712915f3-84e7-6e7a-e1a1-050e12d3b401\",\n          type: \"FormRadioWrapper\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"2ee7bcbb-e2ec-3c21-2cb0-c76171ff84e0\",\n            \"99874ce0-e582-a5ea-9934-f6e42266a80d\",\n          ],\n        },\n        {\n          _id: \"2ee7bcbb-e2ec-3c21-2cb0-c76171ff84e0\",\n          type: \"FormRadioInput\",\n          tag: \"input\",\n          classes: [],\n          children: [],\n          data: {\n            attr: {\n              type: \"radio\",\n              name: \"radio\",\n              id: \"radio\",\n              value: \"Radio\",\n              required: false,\n            },\n          },\n        },\n        {\n          _id: \"99874ce0-e582-a5ea-9934-f6e42266a80d\",\n          type: \"FormInlineLabel\",\n          tag: \"label\",\n          classes: [],\n          children: [\"9e6a0451-a55e-a063-9acc-ee22ed5eac94\"],\n        },\n        {\n          _id: \"9e6a0451-a55e-a063-9acc-ee22ed5eac94\",\n          text: true,\n          v: \"Radio\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Label ws:label=\"Radio Field\">\n      <$.RadioButton id=\"radio\" name=\"radio\" required={false} value=\"Radio\" />\n      <$.Text ws:tag=\"span\" ws:label=\"Label\">\n        Radio\n      </$.Text>\n    </$.Label>\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      w-radio {\n        margin-bottom: 5px;\n        padding-left: 20px;\n        display: block\n      }\n      w-radio:after {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table;\n        clear: both\n      }\n      w-radio:before {\n        content: \" \";\n        grid-row-start: 1;\n        grid-column-start: 1;\n        grid-row-end: 2;\n        grid-column-end: 2;\n        display: table\n      }\n      input {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: normal;\n        font-family: inherit;\n        appearance: button;\n        cursor: pointer;\n        box-sizing: border-box;\n        height: auto;\n        margin: 0;\n        padding: 0\n      }\n      input::-moz-focus-inner {\n        border: 0 none currentcolor;\n        padding: 0\n      }\n      w-radio-input {\n        float: left;\n        margin-top: 3px;\n        margin-right: 0;\n        margin-bottom: 0;\n        margin-left: -20px;\n        line-height: normal\n      }\n      label {\n        margin-bottom: 5px;\n        font-weight: bold;\n        display: block\n      }\n      w-form-label {\n        cursor: pointer;\n        margin-bottom: 0;\n        font-weight: normal;\n        display: inline-block\n      }\n    }\"\n  `);\n});\n\ntest(\"FormSelect\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"a68c34ca-e4c3-3dbc-0036-2554387bd7a4\",\n          type: \"FormSelect\",\n          tag: \"select\",\n          classes: [],\n          children: [],\n          data: {\n            form: {\n              opts: [\n                {\n                  t: \"Select one...\",\n                  v: \"\",\n                },\n                {\n                  t: \"First choice\",\n                  v: \"First\",\n                },\n                {\n                  t: \"Second choice\",\n                  v: \"Second\",\n                },\n                {\n                  t: \"Third choice\",\n                  v: \"Third\",\n                },\n              ],\n            },\n            attr: {\n              id: \"field-3\",\n              name: \"field-3\",\n              required: false,\n              multiple: false,\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Input id=\"field-3\" name=\"field-3\" required={false} multiple={false} />\n  );\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\n    \"@media all {\n      select {\n        color: inherit;\n        font-style: inherit;\n        font-variant-caps: inherit;\n        font-weight: inherit;\n        font-stretch: inherit;\n        font-size: inherit;\n        line-height: inherit;\n        font-family: inherit;\n        text-transform: none;\n        margin: 0\n      }\n    }\"\n  `);\n});\n\ntest(\"Multiline text\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"da7b8e40-a038-735c-bde8-0016079a5502\",\n          type: \"Block\",\n          tag: \"div\",\n          classes: [],\n          children: [\n            \"629fa602-de2f-b70b-ba5e-a74de823cfff\",\n            \"b6a2a455-8ece-d70b-c88b-2f7b7dd39445\",\n            \"49a4d697-0430-6739-9179-e75c1dbec765\",\n          ],\n        },\n        {\n          _id: \"629fa602-de2f-b70b-ba5e-a74de823cfff\",\n          text: true,\n          v: \"a\",\n        },\n        {\n          _id: \"b6a2a455-8ece-d70b-c88b-2f7b7dd39445\",\n          type: \"LineBreak\",\n          tag: \"br\",\n          classes: [],\n          children: [],\n        },\n        {\n          _id: \"49a4d697-0430-6739-9179-e75c1dbec765\",\n          text: true,\n          v: \"b\",\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n\n  equalFragment(\n    fragment,\n    <$.Box ws:tag=\"div\">\n      {\"a\"}\n      {\"\\n\"}\n      {\"b\"}\n    </$.Box>\n  );\n\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\"\"`);\n});\n\ndescribe(\"Custom attributes\", () => {\n  test(\"Basic\", async () => {\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"249f235e-91b6-bd0f-bc42-00993479e637\",\n            type: \"Heading\",\n            tag: \"h1\",\n            classes: [],\n            children: [],\n            data: {\n              xattr: [\n                {\n                  name: \"at\",\n                  value: \"b\",\n                },\n              ],\n            },\n          },\n        ],\n        styles: [],\n        assets: [],\n      },\n    });\n    equalFragment(fragment, <$.Heading ws:tag=\"h1\" at=\"b\" />);\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        h1 {\n          margin-top: 20px;\n          margin-right: 0;\n          margin-bottom: 10px;\n          margin-left: 0;\n          font-size: 38px;\n          font-weight: bold;\n          line-height: 44px\n        }\n      }\"\n    `);\n  });\n});\n\ntest(\"Set show false when visibility's only condition is false\", async () => {\n  const fragment = await toWebstudioFragment({\n    type: \"@webflow/XscpData\",\n    payload: {\n      nodes: [\n        {\n          _id: \"b35ac1a9-5a38-56c6-03ba-3196d421b95e\",\n          type: \"Block\",\n          tag: \"div\",\n          classes: [],\n          children: [],\n          data: {\n            visibility: {\n              conditions: [false],\n            },\n          },\n        },\n      ],\n      styles: [],\n      assets: [],\n    },\n  });\n  equalFragment(fragment, <$.Box data-ws-show={false} ws:tag=\"div\" />);\n  expect(toCss(fragment)).toMatchInlineSnapshot(`\"\"`);\n});\n\ndescribe(\"Styles\", () => {\n  test(\"Single class\", async () => {\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"97d91be2-3bba-d340-0f13-a84e975b7497\",\n            type: \"Heading\",\n            tag: \"h1\",\n            classes: [\"a7bff598-b719-1edb-067b-a90a54d68605\"],\n            children: [],\n          },\n        ],\n        styles: [\n          {\n            _id: \"a7bff598-b719-1edb-067b-a90a54d68605\",\n            type: \"class\",\n            name: \"Heading\",\n            styleLess: \"color: hsla(0, 80.00%, 47.78%, 1.00);\",\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    expect(fragment.styleSources).toEqual([\n      {\n        type: \"token\",\n        id: expect.any(String),\n        name: \"h1\",\n      },\n      {\n        type: \"token\",\n        id: expect.any(String),\n        name: \"Heading\",\n      },\n    ]);\n    expect(fragment.styleSourceSelections).toEqual([\n      {\n        instanceId: expect.any(String),\n        values: [expect.any(String), expect.any(String)],\n      },\n    ]);\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        h1 {\n          margin-top: 20px;\n          margin-right: 0;\n          margin-bottom: 10px;\n          margin-left: 0;\n          font-size: 38px;\n          font-weight: bold;\n          line-height: 44px\n        }\n        Heading {\n          color: hsl(0 80% 47.78% / 1)\n        }\n      }\"\n    `);\n  });\n\n  test(\"Combo class\", async () => {\n    $styleSources.set(\n      new Map([\n        [\n          \"uuBw1PRC_uE8RhTmwxaH8\",\n          {\n            id: \"uuBw1PRC_uE8RhTmwxaH8\",\n            type: \"token\",\n            name: \"button\",\n          },\n        ],\n        [\n          \"uuORexg4BOrRXBJZgB80_\",\n          {\n            id: \"uuORexg4BOrRXBJZgB80_\",\n            type: \"token\",\n            name: \"button.is-small\",\n          },\n        ],\n      ])\n    );\n    $styles.set(\n      new Map([\n        [\n          \"uuBw1PRC_uE8RhTmwxaH8\",\n          {\n            styleSourceId: \"uuBw1PRC_uE8RhTmwxaH8\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"green\" },\n          },\n        ],\n        [\n          \"uuORexg4BOrRXBJZgB80_0\",\n          {\n            styleSourceId: \"uuORexg4BOrRXBJZgB80_\",\n            breakpointId: \"base\",\n            property: \"paddingTop\",\n            value: { type: \"unit\", unit: \"rem\", value: 1 },\n          },\n        ],\n      ])\n    );\n\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"5f7ab979-89b3-c705-6ab9-35f77dfb209f\",\n            type: \"Link\",\n            tag: \"a\",\n            classes: [\n              \"194e7d07-469d-6ffa-3925-1f51bdad7e44\",\n              \"194e7d07-469d-6ffa-3925-1f51bdad7e47\",\n              \"194e7d07-469d-6ffa-3925-1f51bdad7e46\",\n            ],\n            children: [],\n            data: {\n              link: {\n                url: \"#\",\n              },\n            },\n          },\n        ],\n        styles: [\n          {\n            _id: \"194e7d07-469d-6ffa-3925-1f51bdad7e44\",\n            type: \"class\",\n            name: \"button\",\n            styleLess: \"text-align: center;\",\n            children: [\n              \"194e7d07-469d-6ffa-3925-1f51bdad7e47\",\n              \"194e7d07-469d-6ffa-3925-1f51bdad7e46\",\n            ],\n          },\n          {\n            _id: \"194e7d07-469d-6ffa-3925-1f51bdad7e47\",\n            type: \"class\",\n            name: \"is-small\",\n            comb: \"&\",\n            styleLess: \"padding: 1rem;\",\n          },\n          {\n            _id: \"194e7d07-469d-6ffa-3925-1f51bdad7e46\",\n            type: \"class\",\n            name: \"is-secondary\",\n            comb: \"&\",\n            styleLess: \"background-color: transparent;\",\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    expect(fragment.styleSources).toEqual([\n      {\n        type: \"token\",\n        id: \"uu1p3Xdvlq_AZOxnzDvAv\",\n        name: \"a\",\n      },\n      {\n        type: \"token\",\n        id: \"uumXb7vHOnzTr-4SIW-wJ\",\n        name: \"button.is-small.is-secondary\",\n      },\n    ]);\n    expect(fragment.styleSourceSelections).toEqual([\n      {\n        instanceId: expect.any(String),\n        values: [\n          \"uu1p3Xdvlq_AZOxnzDvAv\",\n          \"uuBw1PRC_uE8RhTmwxaH8\",\n          \"uumXb7vHOnzTr-4SIW-wJ\",\n          \"uuORexg4BOrRXBJZgB80_\",\n        ],\n      },\n    ]);\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        a {\n          background-color: rgb(0 0 0 / 0)\n        }\n        a:active {\n          outline: 0 none currentcolor\n        }\n        a:hover {\n          outline: 0 none currentcolor\n        }\n        button.is-small.is-secondary {\n          text-align: center;\n          background-color: transparent;\n          padding: 1rem\n        }\n      }\"\n    `);\n  });\n\n  test(\"Skip empty combo class\", async () => {\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"56ede23d-6dcc-a320-0af4-3a803d8efd88\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\"08c3532a-4e88-1106-9fda-e4dcd66f93f0\"],\n            children: [\n              \"d06c01f8-da59-b31e-a910-55a6d33898f0\",\n              \"b67fb4c4-ce52-b441-1d89-55b165282c99\",\n            ],\n          },\n          {\n            _id: \"d06c01f8-da59-b31e-a910-55a6d33898f0\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\"6569b562-7de4-bd09-3d2d-59d7c11ec988\"],\n            children: [],\n          },\n          {\n            _id: \"b67fb4c4-ce52-b441-1d89-55b165282c99\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\n              \"08c3532a-4e88-1106-9fda-e4dcd66f93f0\",\n              \"c4851546-799d-c6bf-fb17-0b5765f48d72\",\n            ],\n            children: [],\n          },\n        ],\n        styles: [\n          {\n            _id: \"08c3532a-4e88-1106-9fda-e4dcd66f93f0\",\n            fake: false,\n            type: \"class\",\n            name: \"d1\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess: \"color: hsla(0, 70.45%, 48.11%, 1.00);\",\n            variants: {},\n            children: [\"c4851546-799d-c6bf-fb17-0b5765f48d72\"],\n          },\n          {\n            _id: \"6569b562-7de4-bd09-3d2d-59d7c11ec988\",\n            fake: false,\n            type: \"class\",\n            name: \"d2\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess: \"font-size: 200px;\",\n            variants: {},\n            children: [],\n          },\n          {\n            _id: \"c4851546-799d-c6bf-fb17-0b5765f48d72\",\n            fake: false,\n            type: \"class\",\n            name: \"d2\",\n            namespace: \"\",\n            comb: \"&\",\n            styleLess: \"\",\n            variants: {},\n            children: [],\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        d1 {\n          color: hsl(0 70.45% 48.11% / 1)\n        }\n        d2 {\n          font-size: 200px\n        }\n      }\"\n    `);\n  });\n\n  test(\"Webflow @variable syntax used by Relume (legacy?)\", async () => {\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"5f7ab979-89b3-c705-6ab9-35f77dfb209f\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\"194e7d07-469d-6ffa-3925-1f51bdad7e44\"],\n            children: [],\n          },\n        ],\n        styles: [\n          {\n            _id: \"194e7d07-469d-6ffa-3925-1f51bdad7e44\",\n            type: \"class\",\n            name: \"block\",\n            styleLess: \"color: @var_relume-variable-color-neutral-1\",\n            children: [],\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        block {\n          color: unset\n        }\n      }\"\n    `);\n  });\n\n  test(\"States\", async () => {\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"5c66aeeb-941d-c52f-8e14-c54312878021\",\n            type: \"Link\",\n            tag: \"a\",\n            classes: [\"81cf331a-ed88-7a5f-3538-8915c7788aea\"],\n            children: [],\n            data: {\n              link: {\n                url: \"#\",\n              },\n            },\n          },\n        ],\n        styles: [\n          {\n            _id: \"81cf331a-ed88-7a5f-3538-8915c7788aea\",\n            fake: false,\n            type: \"class\",\n            name: \"x\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess: \"transform: translate3d(7px, 74px, 16px);\",\n            variants: {\n              main_hover: {\n                styleLess: \"background-color: hsla(0, 85.19%, 42.12%, 1.00);\",\n              },\n              tiny_focus: {\n                styleLess: \"background-color: hsla(264, 73.75%, 38.37%, 1.00);\",\n              },\n              tiny_active: {\n                styleLess: \"background-color: hsla(0, 46.06%, 51.80%, 1.00);\",\n              },\n              \"tiny_focus-visible\": {\n                styleLess: \"background-color: hsla(264, 36.36%, 41.56%, 1.00);\",\n              },\n              tiny_visited: {\n                styleLess: \"background-color: hsla(0, 83.67%, 42.10%, 1.00);\",\n              },\n              tiny_hover: {\n                styleLess: \"background-color: hsla(0, 60.99%, 58.99%, 1.00);\",\n              },\n            },\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    expect(fragment.styleSources).toEqual([\n      {\n        type: \"token\",\n        id: expect.any(String),\n        name: \"a\",\n      },\n      {\n        type: \"token\",\n        id: expect.any(String),\n        name: \"x\",\n      },\n    ]);\n    expect(fragment.styleSourceSelections).toEqual([\n      {\n        instanceId: expect.any(String),\n        values: [expect.any(String), expect.any(String)],\n      },\n    ]);\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        a {\n          background-color: rgb(0 0 0 / 0)\n        }\n        a:active {\n          outline: 0 none currentcolor\n        }\n        a:hover {\n          outline: 0 none currentcolor\n        }\n        x {\n          transform: translate3d(7px, 74px, 16px)\n        }\n        x:hover {\n          background-color: hsl(0 85.19% 42.12% / 1)\n        }\n      }\n      @media all and (max-width: 479px) {\n        x:active {\n          background-color: hsl(0 46.06% 51.8% / 1)\n        }\n        x:focus {\n          background-color: hsl(264 73.75% 38.37% / 1)\n        }\n        x:focus-visible {\n          background-color: hsl(264 36.36% 41.56% / 1)\n        }\n        x:hover {\n          background-color: hsl(0 60.99% 58.99% / 1)\n        }\n        x:visited {\n          background-color: hsl(0 83.67% 42.1% / 1)\n        }\n      }\"\n    `);\n  });\n\n  test(\"Breakpoints\", async () => {\n    const fragment = await toWebstudioFragment({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"c06c94aa-e2cd-fa7a-d8f8-574b474a20fa\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\"81fbefba-d2de-9cc2-81bf-3a929d4eb219\"],\n            children: [],\n          },\n        ],\n        styles: [\n          {\n            _id: \"81fbefba-d2de-9cc2-81bf-3a929d4eb219\",\n            fake: false,\n            type: \"class\",\n            name: \"Div Block 2\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess: \"background-color: hsla(191, 100.00%, 50.00%, 1.00);\",\n            variants: {\n              large: {\n                styleLess:\n                  \"background-color: hsla(150, 100.00%, 50.00%, 1.00);\",\n              },\n              xl: {\n                styleLess: \"\",\n              },\n              medium: {\n                styleLess:\n                  \"background-color: hsla(256, 100.00%, 50.00%, 1.00);\",\n              },\n              small: {\n                styleLess:\n                  \"background-color: hsla(308, 100.00%, 50.00%, 1.00);\",\n              },\n              tiny: {\n                styleLess:\n                  \"background-color: hsla(359, 100.00%, 50.00%, 1.00);\",\n              },\n            },\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    expect(fragment.breakpoints).toEqual([\n      { id: expect.any(String), label: \"base\" },\n      { id: expect.any(String), label: \"large\", minWidth: 1280 },\n      { id: expect.any(String), label: \"medium\", maxWidth: 991 },\n      { id: expect.any(String), label: \"small\", maxWidth: 767 },\n      { id: expect.any(String), label: \"tiny\", maxWidth: 479 },\n    ]);\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        Div Block 2 {\n          background-color: hsl(191 100% 50% / 1)\n        }\n      }\n      @media all and (max-width: 991px) {\n        Div Block 2 {\n          background-color: hsl(256 100% 50% / 1)\n        }\n      }\n      @media all and (max-width: 767px) {\n        Div Block 2 {\n          background-color: hsl(308 100% 50% / 1)\n        }\n      }\n      @media all and (max-width: 479px) {\n        Div Block 2 {\n          background-color: hsl(359 100% 50% / 1)\n        }\n      }\n      @media all and (min-width: 1280px) {\n        Div Block 2 {\n          background-color: hsl(150 100% 50% / 1)\n        }\n      }\"\n    `);\n  });\n\n  test(\"background images\", async () => {\n    const input = WfData.parse({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"2e9842a4-ac18-9d21-894b-026c6eb20441\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\"98133834-439c-9a8c-7e9f-c3186f2fa45f\"],\n            children: [],\n            data: {\n              text: false,\n            },\n          },\n        ],\n        styles: [\n          {\n            _id: \"98133834-439c-9a8c-7e9f-c3186f2fa45f\",\n            fake: false,\n            type: \"class\",\n            name: \"Div Block\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess:\n              \"height: 400px; background-image: linear-gradient(180deg, hsla(0, 0.00%, 0.00%, 0.11), white), @img_667d0b7769e0cc3754b584f6, @img_667d0fe180995eadc1534a26, @img_example_bg; background-position: 0px 0px, 550px 0px, 0px 0px,0px 0px; background-size: auto, contain, auto, auto; background-repeat: repeat, no-repeat, repeat,repeat; background-attachment: scroll, fixed, scroll, fixed;\",\n            variants: {},\n            children: [],\n          },\n        ],\n        assets: [\n          {\n            cdnUrl:\n              \"https://uploads-ssl.webflow.com/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png\",\n            siteId: \"667c32290bd6159c18dca9a0\",\n            width: 800,\n            height: 600,\n            fileName: \"IMG_2882 (1).png\",\n            createdOn: \"2024-06-27T06:49:27.100Z\",\n            origFileName: \"IMG_2882 (1).png\",\n            fileHash: \"36f49907757795f0a4ecfcfdfc483115\",\n            variants: [\n              {\n                origFileName: \"IMG_2882%20(1)-p-500.png\",\n                fileName: \"667d0b7769e0cc3754b584f6_IMG_2882%20(1)-p-500.png\",\n                format: \"png\",\n                size: 192728,\n                width: 500,\n                quality: 100,\n                cdnUrl:\n                  \"https://daks2k3a4ib2z.cloudfront.net/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1)-p-500.png\",\n                s3Url:\n                  \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1)-p-500.png\",\n              },\n            ],\n            mimeType: \"image/png\",\n            s3Url:\n              \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png\",\n            thumbUrl: \"\",\n            _id: \"667d0b7769e0cc3754b584f6\",\n            markedAsDeleted: false,\n            fileSize: 862053,\n          },\n          {\n            cdnUrl:\n              \"https://uploads-ssl.webflow.com/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp\",\n            siteId: \"667c32290bd6159c18dca9a0\",\n            width: 1024,\n            height: 1024,\n            fileName: \"Привет Мир : %2F .webp\",\n            createdOn: \"2024-06-27T07:08:17.010Z\",\n            origFileName: \"Привет Мир : %2F .webp\",\n            fileHash: \"d86e52a94c04120f455b276effa59046\",\n            variants: [\n              {\n                origFileName:\n                  \"%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp\",\n                fileName:\n                  \"667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp\",\n                format: \"webp\",\n                size: 26992,\n                width: 500,\n                quality: 100,\n                cdnUrl:\n                  \"https://daks2k3a4ib2z.cloudfront.net/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp\",\n                s3Url:\n                  \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-500.webp\",\n              },\n              {\n                origFileName:\n                  \"%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp\",\n                fileName:\n                  \"667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp\",\n                format: \"webp\",\n                size: 45964,\n                width: 800,\n                quality: 100,\n                cdnUrl:\n                  \"https://daks2k3a4ib2z.cloudfront.net/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp\",\n                s3Url:\n                  \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20-p-800.webp\",\n              },\n            ],\n            mimeType: \"image/webp\",\n            s3Url:\n              \"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp\",\n            thumbUrl: \"\",\n            _id: \"667d0fe180995eadc1534a26\",\n            markedAsDeleted: false,\n            fileSize: 191270,\n          },\n        ],\n      },\n    });\n\n    const fragment = await toWebstudioFragment(input);\n\n    const bgStyle = fragment.styles.find(\n      (style) => style.property === \"backgroundImage\"\n    );\n\n    expect(bgStyle).toBeDefined();\n    expect(bgStyle?.value.type).toEqual(\"layers\");\n\n    const layers = bgStyle?.value;\n\n    invariant(layers?.type === \"layers\");\n\n    const imgA = layers.value[1];\n    const imgB = layers.value[2];\n    const noneLayer = layers.value[3];\n\n    invariant(imgA.type === \"image\");\n    invariant(imgA.value.type === \"url\");\n    invariant(imgB.type === \"image\");\n    invariant(imgB.value.type === \"url\");\n\n    expect(imgA.value.url).toEqual(input.payload.assets[0].s3Url);\n    expect(imgB.value.url).toEqual(input.payload.assets[1].s3Url);\n\n    expect(noneLayer.type).toEqual(\"keyword\");\n    invariant(noneLayer.type === \"keyword\");\n    expect(noneLayer.value).toEqual(\"none\");\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        Div Block {\n          height: 400px;\n          background-image: linear-gradient(180deg,hsla(0,0.00%,0.00%,0.11),white), url(\"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0b7769e0cc3754b584f6_IMG_2882%20(1).png\"), url(\"https://s3.amazonaws.com/webflow-prod-assets/667c32290bd6159c18dca9a0/667d0fe180995eadc1534a26_%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%20%D0%9C%D0%B8%D1%80%20%3A%20%252F%20.webp\"), none;\n          background-size: auto, contain, auto, auto;\n          background-repeat: repeat, no-repeat, repeat, repeat;\n          background-attachment: scroll, fixed, scroll, fixed;\n          background-position: 0px 0px, 550px 0px, 0px 0px, 0px 0px\n        }\n      }\"\n  `);\n  });\n\n  test(\"@raw webflow custom properties\", async () => {\n    const input = WfData.parse({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"89ed49d8-2d32-5bab-22e1-264acb8d2d84\",\n            type: \"Block\",\n            tag: \"div\",\n            classes: [\"393ac121-a395-7678-e598-994e30fb7880\"],\n            children: [],\n            data: { text: false, tag: \"div\" },\n          },\n        ],\n        styles: [\n          {\n            _id: \"393ac121-a395-7678-e598-994e30fb7880\",\n            fake: false,\n            type: \"class\",\n            name: \"Div Block\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess: \"color: @raw<|black|>; background-color: @raw<|red|>;\",\n            variants: {},\n            children: [],\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    const fragment = await toWebstudioFragment(input);\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        Div Block {\n          color: black;\n          background-color: red\n        }\n      }\"\n    `);\n  });\n\n  test(\"append transparent color when background-clip is used\", async () => {\n    const input = WfData.parse({\n      type: \"@webflow/XscpData\",\n      payload: {\n        nodes: [\n          {\n            _id: \"15c3fb65-b871-abe4-f9a4-3747c8a882e0\",\n            type: \"Heading\",\n            tag: \"h2\",\n            classes: [\"069649be-a33a-1a9a-3763-c0bd9d1f3a3d\"],\n            children: [\"b69a5869-f046-5a0c-151e-9b134a6852aa\"],\n            data: {\n              tag: \"h2\",\n              devlink: { runtimeProps: {}, slot: \"\" },\n              displayName: \"\",\n              attr: { id: \"\" },\n              xattr: [],\n              search: { exclude: false },\n              visibility: { conditions: [] },\n            },\n          },\n          {\n            _id: \"b69a5869-f046-5a0c-151e-9b134a6852aa\",\n            text: true,\n            v: \"Protect your systems securely with Prism\",\n          },\n        ],\n        styles: [\n          {\n            _id: \"069649be-a33a-1a9a-3763-c0bd9d1f3a3d\",\n            fake: false,\n            type: \"class\",\n            name: \"H2 Heading 2\",\n            namespace: \"\",\n            comb: \"\",\n            styleLess:\n              \"background-image: linear-gradient(350deg, hsl(256.3636363636363 72.13% 23.92% / 0.00), hsl(256.2162162162162 72.55% 80.00% / 1.00) 49%, #bba7f1); color: hsl(0 0.00% 100.00% / 1.00); background-clip: text;\",\n            variants: {},\n            children: [],\n            createdBy: \"58b4b8186ceb395341fcf640\",\n            origin: null,\n            selector: null,\n          },\n        ],\n        assets: [],\n      },\n    });\n\n    const fragment = await toWebstudioFragment(input);\n\n    expect(toCss(fragment)).toMatchInlineSnapshot(`\n      \"@media all {\n        h2 {\n          margin-bottom: 10px;\n          font-weight: bold;\n          margin-top: 20px;\n          font-size: 32px;\n          line-height: 36px\n        }\n        H2 Heading 2 {\n          background-image: linear-gradient(350deg,hsl(256.3636363636363 72.13% 23.92%/0.00),hsl(256.2162162162162 72.55% 80.00%/1.00) 49%,#bba7f1);\n          -webkit-background-clip: text;\n          background-clip: text;\n          color: transparent\n        }\n      }\"\n    `);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/plugin-webflow.ts",
    "content": "import type { Instance, WebstudioFragment } from \"@webstudio-is/sdk\";\nimport {\n  findClosestInsertable,\n  insertInstanceChildrenMutable,\n  insertWebstudioFragmentCopy,\n  updateWebstudioData,\n} from \"../../instance-utils\";\nimport { $project } from \"../../nano-states\";\nimport {\n  WfData,\n  wfNodeTypes,\n  type WfNode,\n  type WfStyle,\n  type WfAsset,\n} from \"./schema\";\nimport { addInstanceAndProperties } from \"./instances-properties\";\nimport { addStyles } from \"./styles\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport { denormalizeSrcProps } from \"../asset-upload\";\nimport { nanoHash } from \"~/shared/nano-hash\";\nimport { findAvailableVariables } from \"~/shared/data-variables\";\nimport type { Plugin } from \"../init-copy-paste\";\n\nconst { toast } = builderApi;\n\nconst toWebstudioFragment = async (wfData: WfData) => {\n  const fragment: WebstudioFragment = {\n    children: [],\n    instances: [],\n    props: [],\n    breakpoints: [],\n    styles: [],\n    styleSources: [],\n    styleSourceSelections: [],\n    dataSources: [],\n    resources: [],\n    assets: [],\n  };\n\n  const wfNodes = new Map<WfNode[\"_id\"], WfNode>();\n  for (const node of wfData.payload.nodes) {\n    if (\"type\" in node || \"text\" in node) {\n      wfNodes.set(node._id, node);\n    }\n  }\n  const wfStyles = new Map<WfStyle[\"_id\"], WfStyle>(\n    wfData.payload.styles.map((style: WfStyle) => [style._id, style])\n  );\n\n  const wfAssets = new Map<WfAsset[\"_id\"], WfAsset>(\n    wfData.payload.assets.map((asset: WfAsset) => [asset._id, asset])\n  );\n\n  // False value used to skip a node.\n  const doneNodes = new Map<WfNode[\"_id\"], Instance[\"id\"] | false>();\n  for (const wfNode of wfNodes.values()) {\n    addInstanceAndProperties(wfNode, doneNodes, wfNodes, fragment);\n  }\n\n  /**\n   * Generates deterministic style IDs based on sourceId or unique data.\n   * This simplifies merging and deduplicating styles from different sources.\n   */\n  const generateStyleSourceId = async (sourceData: string) => {\n    // We are using projectId here to avoid id collisions between different projects.\n    const projectId = $project.get()?.id;\n    if (projectId === undefined) {\n      throw new Error(\"Project id is not set\");\n    }\n    return nanoHash(`${projectId}-${sourceData}`);\n  };\n\n  await addStyles({\n    wfNodes,\n    wfStyles,\n    wfAssets,\n    doneNodes,\n    fragment,\n    generateStyleSourceId,\n  });\n  // First node should be always the root node in theory, if not\n  // we need to find a node that is not a child of any other node.\n  const rootWfNode = wfData.payload.nodes[0];\n  const rootInstanceId = doneNodes.get(rootWfNode._id);\n  if (rootInstanceId === false) {\n    return fragment;\n  }\n  if (rootInstanceId === undefined) {\n    console.error(`No root instance id found for node ${rootWfNode._id}`);\n    return fragment;\n  }\n  fragment.children = [\n    {\n      type: \"id\",\n      value: rootInstanceId,\n    },\n  ];\n  return fragment;\n};\n\nconst parse = (clipboardData: string) => {\n  let data;\n  try {\n    data = JSON.parse(clipboardData);\n  } catch {\n    return;\n  }\n\n  if (data.type !== \"@webflow/XscpData\") {\n    return;\n  }\n\n  const unsupportedNodeTypes: Set<string> = new Set(\n    data.payload.nodes\n      .filter((node: { type: string }) => {\n        return (\n          node.type !== undefined &&\n          wfNodeTypes.includes(node.type as (typeof wfNodeTypes)[number]) ===\n            false\n        );\n      })\n      .map((node: { type: string }) => node.type)\n  );\n\n  if (unsupportedNodeTypes.size !== 0) {\n    const message = `Skipping unsupported elements: ${[...unsupportedNodeTypes.values()].join(\", \")}`;\n    toast.info(message);\n    console.info(message);\n  }\n\n  const result = WfData.safeParse(data);\n\n  if (result.success) {\n    const unpasedTypes = new Set<string>();\n\n    for (let i = 0; i !== result.data.payload.nodes.length; ++i) {\n      if (\"type\" in result.data.payload.nodes[i]) {\n        continue;\n      }\n\n      if (data.payload.nodes[i].type === undefined) {\n        continue;\n      }\n\n      const probablyUnparsedType = data.payload.nodes[i].type;\n\n      if (unsupportedNodeTypes.has(probablyUnparsedType)) {\n        continue;\n      }\n\n      unpasedTypes.add(probablyUnparsedType);\n    }\n\n    if (unpasedTypes.size !== 0) {\n      const message = `The following types were skipped due to a parsing error: ${[...unpasedTypes.values()].join(\", \")}`;\n      toast.info(message);\n      console.info(message);\n    }\n\n    return result.data;\n  }\n\n  toast.error(result.error.message);\n  console.error(result.error.message);\n};\n\nconst onPaste = async (clipboardData: string) => {\n  const project = $project.get();\n  const wfData = parse(clipboardData);\n  if (wfData === undefined || project === undefined) {\n    return false;\n  }\n\n  let fragment = await toWebstudioFragment(wfData);\n  if (fragment === undefined) {\n    return false;\n  }\n  fragment = await denormalizeSrcProps(fragment);\n\n  const insertable = findClosestInsertable(fragment);\n  if (insertable === undefined) {\n    return false;\n  }\n\n  updateWebstudioData((data) => {\n    const { newInstanceIds } = insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: findAvailableVariables({\n        ...data,\n        startingInstanceId: insertable.parentSelector[0],\n      }),\n      projectId: project.id,\n    });\n\n    const children = fragment.children\n      .map((child) => {\n        if (child.type === \"id\") {\n          const value = newInstanceIds.get(child.value);\n          if (value) {\n            return { type: \"id\" as const, value };\n          }\n        }\n      })\n      .filter(<T>(value: T): value is NonNullable<T> => value !== undefined);\n\n    insertInstanceChildrenMutable(data, children, insertable);\n  });\n\n  return true;\n};\n\nexport const webflow: Plugin = {\n  name: \"webflow\",\n  mimeType: \"application/json\",\n  onPaste,\n};\n\nexport const __testing__ = {\n  toWebstudioFragment,\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/schema.ts",
    "content": "import { z } from \"zod\";\n\nconst Attr = z\n  .object({ id: z.string(), role: z.string(), href: z.string() })\n  .partial();\n\nconst styleBase = z.string();\n\nconst styleBreakpoint = z.string();\n\nconst stylePseudo = z.string();\n\nconst styleProperty = z.string();\n\nconst styleValue = z.unknown();\n\nconst WfNodeData = z.object({\n  attr: Attr.optional(),\n  xattr: z.array(z.object({ name: z.string(), value: z.string() })).optional(),\n  visibility: z\n    .object({\n      conditions: z.array(z.boolean()),\n    })\n    .optional(),\n  style: z\n    .record(\n      styleBase,\n      z.record(\n        styleBreakpoint,\n        z.record(stylePseudo, z.record(styleProperty, styleValue))\n      )\n    )\n    .optional(),\n});\n\nconst WfBaseNode = z.object({\n  _id: z.string(),\n  tag: z.string(),\n  children: z.array(z.string()),\n  classes: z.array(z.string()),\n  data: WfNodeData.optional(),\n  attr: Attr.optional(),\n});\n\nconst WfTextNode = z.object({\n  _id: z.string(),\n  v: z.string(),\n  text: z.boolean(),\n});\n\nconst WfLinkData = WfNodeData.extend({\n  attr: Attr.optional(),\n  block: z.enum([\"inline\", \"block\", \"\"]).optional(),\n  button: z.boolean().optional(),\n  link: z.union([\n    // External link\n    z.object({\n      url: z.string(),\n      target: z.string().optional(),\n    }),\n    // Page link and section link\n    z.object({\n      href: z.string(),\n    }),\n    // Email link\n    z.object({\n      email: z.string(),\n      subject: z.string().optional(),\n    }),\n    // Phone link\n    z.object({\n      tel: z.string(),\n    }),\n    z.object({\n      mode: z.string(),\n    }),\n  ]),\n});\n\nexport const wfNodeTypes = [\n  \"Heading\",\n  \"Block\",\n  \"List\",\n  \"ListItem\",\n  \"Link\",\n  \"Paragraph\",\n  \"Blockquote\",\n  \"RichText\",\n  \"Strong\",\n  \"Emphasized\",\n  \"Superscript\",\n  \"Subscript\",\n  \"Section\",\n  \"BlockContainer\",\n  \"Container\",\n  \"Layout\",\n  \"Cell\",\n  \"VFlex\",\n  \"HFlex\",\n  \"Grid\",\n  \"Row\",\n  \"Column\",\n  \"CodeBlock\",\n  \"HtmlEmbed\",\n  \"Image\",\n  \"FormWrapper\",\n  \"FormForm\",\n  \"FormSuccessMessage\",\n  \"FormErrorMessage\",\n  \"FormButton\",\n  \"FormTextInput\",\n  \"FormTextarea\",\n  \"FormBlockLabel\",\n  \"FormCheckboxWrapper\",\n  \"FormCheckboxInput\",\n  \"FormInlineLabel\",\n  \"FormRadioWrapper\",\n  \"FormRadioInput\",\n  \"FormSelect\",\n  \"LineBreak\",\n  \"Span\",\n  \"NavbarMenu\",\n  \"NavbarWrapper\",\n  \"NavbarBrand\",\n  \"NavbarLink\",\n  \"NavbarButton\",\n  \"NavbarContainer\",\n  \"Icon\",\n  \"LightboxWrapper\",\n] as const;\n\nconst WfElementNode = z.union([\n  WfBaseNode.extend({\n    type: z.enum([\"Icon\"]),\n    data: WfNodeData.extend({\n      widget: z\n        .object({\n          type: z.string(),\n          icon: z.string(),\n        })\n        .optional(),\n    }),\n  }),\n\n  WfBaseNode.extend({ type: z.enum([\"LightboxWrapper\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"NavbarMenu\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"NavbarContainer\"]) }),\n\n  WfBaseNode.extend({ type: z.enum([\"NavbarWrapper\"]) }),\n\n  WfBaseNode.extend({\n    type: z.enum([\"NavbarBrand\"]),\n    data: WfLinkData,\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"NavbarLink\"]),\n    data: WfLinkData,\n  }),\n\n  WfBaseNode.extend({ type: z.enum([\"NavbarButton\"]) }),\n\n  WfBaseNode.extend({ type: z.enum([\"Heading\"]) }),\n  WfBaseNode.extend({\n    type: z.enum([\"Block\"]),\n    data: WfNodeData.extend({\n      attr: Attr.optional(),\n      text: z.boolean().optional(),\n    }).optional(),\n  }),\n  WfBaseNode.extend({ type: z.enum([\"List\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"ListItem\"]) }),\n  WfBaseNode.extend({\n    type: z.enum([\"Link\"]),\n    data: WfLinkData,\n  }),\n  WfBaseNode.extend({ type: z.enum([\"Paragraph\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Blockquote\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"RichText\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Strong\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Emphasized\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Superscript\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Subscript\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Section\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"BlockContainer\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Container\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Layout\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Cell\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"VFlex\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"HFlex\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Grid\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Row\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Column\"]) }),\n  WfBaseNode.extend({\n    type: z.enum([\"CodeBlock\"]),\n    data: WfNodeData.extend({\n      attr: Attr.optional(),\n      language: z.string().optional(),\n      code: z.string(),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"HtmlEmbed\"]),\n    v: z.string(),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"Image\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        alt: z.string().optional(),\n        loading: z.enum([\"lazy\", \"eager\", \"auto\"]),\n        src: z.string(),\n        width: z.string(),\n        height: z.string(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({ type: z.enum([\"FormWrapper\"]) }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormForm\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        action: z.string(),\n        method: z.string(),\n        name: z.string(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({ type: z.enum([\"FormSuccessMessage\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"FormErrorMessage\"]) }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormButton\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        value: z.string(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormTextInput\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        name: z.string(),\n        maxlength: z.number(),\n        placeholder: z.string(),\n        disabled: z.boolean(),\n        type: z.string(),\n        required: z.boolean(),\n        autofocus: z.boolean(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormTextarea\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        name: z.string(),\n        maxlength: z.number(),\n        placeholder: z.string(),\n        required: z.boolean(),\n        autofocus: z.boolean(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormBlockLabel\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        for: z.string().optional(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormCheckboxWrapper\"]),\n  }),\n\n  WfBaseNode.extend({\n    type: z.enum([\"FormCheckboxInput\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        type: z.enum([\"checkbox\"]),\n        name: z.string(),\n        required: z.boolean(),\n        checked: z.boolean(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormInlineLabel\"]),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormRadioWrapper\"]),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormRadioInput\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        type: z.enum([\"radio\"]),\n        name: z.string(),\n        required: z.boolean(),\n        value: z.string(),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({\n    type: z.enum([\"FormSelect\"]),\n    data: WfNodeData.extend({\n      attr: Attr.extend({\n        name: z.string(),\n        required: z.boolean(),\n        multiple: z.boolean(),\n      }),\n      form: z.object({\n        opts: z.array(\n          z.object({\n            t: z.string(),\n            v: z.string(),\n          })\n        ),\n      }),\n    }),\n  }),\n  WfBaseNode.extend({ type: z.enum([\"LineBreak\"]) }),\n  WfBaseNode.extend({ type: z.enum([\"Span\"]) }),\n]);\n\nexport type WfElementNode = z.infer<typeof WfElementNode>;\n\n[...wfNodeTypes] as const satisfies WfElementNode[\"type\"][];\n\n//@todo verify the other way around too\n//(typeof WfElementNode)[\"type\"] satisfies typeof wfNodeTypes[number]\n\nconst WfNode = z.union([WfElementNode, WfTextNode]);\nexport type WfNode = z.infer<typeof WfNode>;\n\nconst WfStyle = z.object({\n  _id: z.string(),\n  type: z.enum([\"class\"]),\n  name: z.string(),\n  styleLess: z.string(),\n  fake: z.boolean().optional(),\n  comb: z.string().optional(),\n  namespace: z.string().optional(),\n  variants: z\n    .record(z.string(), z.object({ styleLess: z.string() }))\n    .optional(),\n  children: z.array(z.string()).optional(),\n});\nexport type WfStyle = z.infer<typeof WfStyle>;\n\nconst WfErrorAssetVariant = z.object({\n  origFileName: z.string(),\n  fileName: z.string(),\n  format: z.string(),\n  size: z.number().optional(),\n  width: z.number().optional(),\n  height: z.number().optional(),\n  quality: z.number().optional(),\n  error: z.string(),\n  _id: z.string(),\n});\n\nconst WfAssetVariant = z.object({\n  origFileName: z.string(),\n  fileName: z.string(),\n  format: z.string(),\n  size: z.number().optional(),\n  width: z.number().optional(),\n  height: z.number().optional(),\n  quality: z.number().optional(),\n  cdnUrl: z.string().url(),\n  s3Url: z.string().url(),\n});\n\nconst WfAsset = z.object({\n  cdnUrl: z.string().url(),\n  siteId: z.string(),\n  width: z.number().optional(),\n  height: z.number().optional(),\n  fileName: z.string(),\n  createdOn: z.string(),\n  origFileName: z.string(),\n  fileHash: z.string(),\n  variants: z.array(z.union([WfAssetVariant, WfErrorAssetVariant])).optional(),\n  mimeType: z.string(),\n  s3Url: z.string().url(),\n  thumbUrl: z.string().optional(),\n  _id: z.string(),\n  markedAsDeleted: z.boolean().optional(),\n  fileSize: z.number(),\n});\n\nexport type WfAsset = z.infer<typeof WfAsset>;\n\nexport const WfData = z.object({\n  type: z.literal(\"@webflow/XscpData\"),\n  payload: z.object({\n    // Using WfBaseNode here just so we can skip a node with unknown node.type.\n    nodes: z.array(z.union([WfNode, WfBaseNode])),\n    styles: z.array(WfStyle),\n    assets: z.array(WfAsset),\n  }),\n});\nexport type WfData = z.infer<typeof WfData>;\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/style-presets-overrides.ts",
    "content": "import type { ParsedStyleDecl } from \"@webstudio-is/css-data\";\nimport { styles } from \"./__generated__/style-presets\";\n\nconst _stylePresets = {\n  ...styles,\n  \"w-embed\": [\n    ...styles[\"w-embed\"],\n    {\n      selector: \"w-embed\",\n      property: \"display\",\n      value: {\n        type: \"keyword\",\n        value: \"block\",\n      },\n    },\n  ],\n};\n\ntype Key = keyof typeof _stylePresets;\n\ntype WfIcons = Record<`w-icon-${string}`, ParsedStyleDecl>;\n\nexport type WfStylePresets = Record<Key, Array<ParsedStyleDecl>> & WfIcons;\nexport const stylePresets = _stylePresets as WfStylePresets;\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/style-presets.css",
    "content": "html {\n  -webkit-text-size-adjust: 100%;\n  -ms-text-size-adjust: 100%;\n  font-family: sans-serif;\n}\n\nbody {\n  margin: 0;\n}\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n  display: block;\n}\n\naudio,\ncanvas,\nprogress,\nvideo {\n  vertical-align: baseline;\n  display: inline-block;\n}\n\naudio:not([controls]) {\n  height: 0;\n  display: none;\n}\n\n[hidden],\ntemplate {\n  display: none;\n}\n\na {\n  background-color: rgba(0, 0, 0, 0);\n}\n\na:active,\na:hover {\n  outline: 0;\n}\n\nabbr[title] {\n  border-bottom: 1px dotted;\n}\n\nb,\nstrong {\n  font-weight: bold;\n}\n\ndfn {\n  font-style: italic;\n}\n\nh1 {\n  margin: 0.67em 0;\n  font-size: 2em;\n}\n\nmark {\n  color: #000;\n  background: #ff0;\n}\n\nsmall {\n  font-size: 80%;\n}\n\nsub,\nsup {\n  vertical-align: baseline;\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n}\n\nsup {\n  top: -0.5em;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nimg {\n  border: 0;\n}\n\nsvg:not(:root) {\n  overflow: hidden;\n}\n\nhr {\n  box-sizing: content-box;\n  height: 0;\n}\n\npre {\n  overflow: auto;\n}\n\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace;\n  font-size: 1em;\n}\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  color: inherit;\n  font: inherit;\n  margin: 0;\n}\n\nbutton {\n  overflow: visible;\n}\n\nbutton,\nselect {\n  text-transform: none;\n}\n\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"] {\n  -webkit-appearance: button;\n  cursor: pointer;\n}\n\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n  border: 0;\n  padding: 0;\n}\n\ninput {\n  line-height: normal;\n}\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n  box-sizing: border-box;\n  padding: 0;\n}\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\ninput[type=\"search\"] {\n  -webkit-appearance: none;\n}\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\nlegend {\n  border: 0;\n  padding: 0;\n}\n\ntextarea {\n  overflow: auto;\n}\n\noptgroup {\n  font-weight: bold;\n}\n\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\ntd,\nth {\n  padding: 0;\n}\n\n@font-face {\n  font-family: webflow-icons;\n  src: url(\"data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBiUAAAC8AAAAYGNtYXDpP+a4AAABHAAAAFxnYXNwAAAAEAAAAXgAAAAIZ2x5ZmhS2XEAAAGAAAADHGhlYWQTFw3HAAAEnAAAADZoaGVhCXYFgQAABNQAAAAkaG10eCe4A1oAAAT4AAAAMGxvY2EDtALGAAAFKAAAABptYXhwABAAPgAABUQAAAAgbmFtZSoCsMsAAAVkAAABznBvc3QAAwAAAAAHNAAAACAAAwP4AZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpAwPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAQAAAAAwACAACAAQAAQAg5gPpA//9//8AAAAAACDmAOkA//3//wAB/+MaBBcIAAMAAQAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEBIAAAAyADgAAFAAAJAQcJARcDIP5AQAGA/oBAAcABwED+gP6AQAABAOAAAALgA4AABQAAEwEXCQEH4AHAQP6AAYBAAcABwED+gP6AQAAAAwDAAOADQALAAA8AHwAvAAABISIGHQEUFjMhMjY9ATQmByEiBh0BFBYzITI2PQE0JgchIgYdARQWMyEyNj0BNCYDIP3ADRMTDQJADRMTDf3ADRMTDQJADRMTDf3ADRMTDQJADRMTAsATDSANExMNIA0TwBMNIA0TEw0gDRPAEw0gDRMTDSANEwAAAAABAJ0AtAOBApUABQAACQIHCQEDJP7r/upcAXEBcgKU/usBFVz+fAGEAAAAAAL//f+9BAMDwwAEAAkAABcBJwEXAwE3AQdpA5ps/GZsbAOabPxmbEMDmmz8ZmwDmvxmbAOabAAAAgAA/8AEAAPAAB0AOwAABSInLgEnJjU0Nz4BNzYzMTIXHgEXFhUUBw4BBwYjNTI3PgE3NjU0Jy4BJyYjMSIHDgEHBhUUFx4BFxYzAgBqXV6LKCgoKIteXWpqXV6LKCgoKIteXWpVSktvICEhIG9LSlVVSktvICEhIG9LSlVAKCiLXl1qal1eiygoKCiLXl1qal1eiygoZiEgb0tKVVVKS28gISEgb0tKVVVKS28gIQABAAABwAIAA8AAEgAAEzQ3PgE3NjMxFSIHDgEHBhUxIwAoKIteXWpVSktvICFmAcBqXV6LKChmISBvS0pVAAAAAgAA/8AFtgPAADIAOgAAARYXHgEXFhUUBw4BBwYHIxUhIicuAScmNTQ3PgE3NjMxOAExNDc+ATc2MzIXHgEXFhcVATMJATMVMzUEjD83NlAXFxYXTjU1PQL8kz01Nk8XFxcXTzY1PSIjd1BQWlJJSXInJw3+mdv+2/7c25MCUQYcHFg5OUA/ODlXHBwIAhcXTzY1PTw1Nk8XF1tQUHcjIhwcYUNDTgL+3QFt/pOTkwABAAAAAQAAmM7nP18PPPUACwQAAAAAANciZKUAAAAA1yJkpf/9/70FtgPDAAAACAACAAAAAAAAAAEAAAPA/8AAAAW3//3//QW2AAEAAAAAAAAAAAAAAAAAAAAMBAAAAAAAAAAAAAAAAgAAAAQAASAEAADgBAAAwAQAAJ0EAP/9BAAAAAQAAAAFtwAAAAAAAAAKABQAHgAyAEYAjACiAL4BFgE2AY4AAAABAAAADAA8AAMAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEADQAAAAEAAAAAAAIABwCWAAEAAAAAAAMADQBIAAEAAAAAAAQADQCrAAEAAAAAAAUACwAnAAEAAAAAAAYADQBvAAEAAAAAAAoAGgDSAAMAAQQJAAEAGgANAAMAAQQJAAIADgCdAAMAAQQJAAMAGgBVAAMAAQQJAAQAGgC4AAMAAQQJAAUAFgAyAAMAAQQJAAYAGgB8AAMAAQQJAAoANADsd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzVmVyc2lvbiAxLjAAVgBlAHIAcwBpAG8AbgAgADEALgAwd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzUmVndWxhcgBSAGUAZwB1AGwAYQByd2ViZmxvdy1pY29ucwB3AGUAYgBmAGwAbwB3AC0AaQBjAG8AbgBzRm9udCBnZW5lcmF0ZWQgYnkgSWNvTW9vbi4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\")\n    format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n}\n\n[class^=\"w-icon-\"],\n[class*=\" w-icon-\"] {\n  speak: none;\n  font-variant: normal;\n  text-transform: none;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  font-style: normal;\n  font-weight: normal;\n  line-height: 1;\n  font-family: webflow-icons !important;\n}\n\n.w-icon-slider-right:before {\n  content: \"î˜€\";\n}\n\n.w-icon-slider-left:before {\n  content: \"î˜\";\n}\n\n.w-icon-nav-menu:before {\n  content: \"î˜‚\";\n}\n\n.w-icon-arrow-down:before,\n.w-icon-dropdown-toggle:before {\n  content: \"î˜ƒ\";\n}\n\n.w-icon-file-upload-remove:before {\n  content: \"î¤€\";\n}\n\n.w-icon-file-upload-icon:before {\n  content: \"î¤ƒ\";\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  height: 100%;\n}\n\nbody {\n  color: #333;\n  background-color: #fff;\n  min-height: 100%;\n  margin: 0;\n  font-family: Arial, sans-serif;\n  font-size: 14px;\n  line-height: 20px;\n}\n\nimg {\n  vertical-align: middle;\n  max-width: 100%;\n  display: inline-block;\n}\n\nhtml.w-mod-touch * {\n  background-attachment: scroll !important;\n}\n\n.w-block {\n  display: block;\n}\n\n.w-inline-block {\n  max-width: 100%;\n  display: inline-block;\n}\n\n.w-clearfix:before,\n.w-clearfix:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-clearfix:after {\n  clear: both;\n}\n\n.w-hidden {\n  display: none;\n}\n\n.w-button {\n  color: #fff;\n  line-height: inherit;\n  cursor: pointer;\n  background-color: #3898ec;\n  border: 0;\n  border-radius: 0;\n  padding: 9px 15px;\n  text-decoration: none;\n  display: inline-block;\n}\n\ninput.w-button {\n  -webkit-appearance: button;\n}\n\nhtml[data-w-dynpage] [data-w-cloak] {\n  color: rgba(0, 0, 0, 0) !important;\n}\n\n.w-code-block {\n  margin: unset;\n}\n\npre.w-code-block code {\n  all: inherit;\n}\n\n.w-webflow-badge,\n.w-webflow-badge * {\n  z-index: auto;\n  visibility: visible;\n  box-sizing: border-box;\n  float: none;\n  clear: none;\n  box-shadow: none;\n  opacity: 1;\n  direction: ltr;\n  font-family: inherit;\n  font-weight: inherit;\n  color: inherit;\n  font-size: inherit;\n  line-height: inherit;\n  font-style: inherit;\n  font-variant: inherit;\n  text-align: inherit;\n  letter-spacing: inherit;\n  -webkit-text-decoration: inherit;\n  text-decoration: inherit;\n  text-indent: 0;\n  text-transform: inherit;\n  text-shadow: none;\n  font-smoothing: auto;\n  vertical-align: baseline;\n  cursor: inherit;\n  white-space: inherit;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  background: none;\n  border: 0 rgba(0, 0, 0, 0);\n  border-radius: 0;\n  width: auto;\n  min-width: 0;\n  max-width: none;\n  height: auto;\n  min-height: 0;\n  max-height: none;\n  margin: 0;\n  padding: 0;\n  list-style-type: disc;\n  transition: none;\n  display: block;\n  position: static;\n  top: auto;\n  bottom: auto;\n  left: auto;\n  right: auto;\n  overflow: visible;\n  transform: none;\n}\n\n.w-webflow-badge {\n  white-space: nowrap;\n  cursor: pointer;\n  box-shadow:\n    0 0 0 1px rgba(0, 0, 0, 0.1),\n    0 1px 3px rgba(0, 0, 0, 0.1);\n  visibility: visible !important;\n  z-index: 2147483647 !important;\n  color: #aaadb0 !important;\n  opacity: 1 !important;\n  background-color: #fff !important;\n  border-radius: 3px !important;\n  width: auto !important;\n  height: auto !important;\n  margin: 0 !important;\n  padding: 6px !important;\n  font-size: 12px !important;\n  line-height: 14px !important;\n  text-decoration: none !important;\n  display: inline-block !important;\n  position: fixed !important;\n  top: auto !important;\n  bottom: 12px !important;\n  left: auto !important;\n  right: 12px !important;\n  overflow: visible !important;\n  transform: none !important;\n}\n\n.w-webflow-badge > img {\n  visibility: visible !important;\n  opacity: 1 !important;\n  vertical-align: middle !important;\n  display: inline-block !important;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  margin-bottom: 10px;\n  font-weight: bold;\n}\n\nh1 {\n  margin-top: 20px;\n  font-size: 38px;\n  line-height: 44px;\n}\n\nh2 {\n  margin-top: 20px;\n  font-size: 32px;\n  line-height: 36px;\n}\n\nh3 {\n  margin-top: 20px;\n  font-size: 24px;\n  line-height: 30px;\n}\n\nh4 {\n  margin-top: 10px;\n  font-size: 18px;\n  line-height: 24px;\n}\n\nh5 {\n  margin-top: 10px;\n  font-size: 14px;\n  line-height: 20px;\n}\n\nh6 {\n  margin-top: 10px;\n  font-size: 12px;\n  line-height: 18px;\n}\n\np {\n  margin-top: 0;\n  margin-bottom: 10px;\n}\n\nblockquote {\n  border-left: 5px solid #e2e2e2;\n  margin: 0 0 10px;\n  padding: 10px 20px;\n  font-size: 18px;\n  line-height: 22px;\n}\n\nfigure {\n  margin: 0 0 10px;\n}\n\nfigcaption {\n  text-align: center;\n  margin-top: 5px;\n}\n\nul,\nol {\n  margin-top: 0;\n  margin-bottom: 10px;\n  padding-left: 40px;\n}\n\n.w-list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n.w-embed:before,\n.w-embed:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-embed:after {\n  clear: both;\n}\n\n.w-video {\n  width: 100%;\n  padding: 0;\n  position: relative;\n}\n\n.w-video iframe,\n.w-video object,\n.w-video embed {\n  border: none;\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\nfieldset {\n  border: 0;\n  margin: 0;\n  padding: 0;\n}\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"] {\n  cursor: pointer;\n  -webkit-appearance: button;\n  border: 0;\n}\n\n.w-form {\n  margin: 0 0 15px;\n}\n\n.w-form-done {\n  text-align: center;\n  background-color: #ddd;\n  padding: 20px;\n  display: none;\n}\n\n.w-form-fail {\n  background-color: #ffdede;\n  margin-top: 10px;\n  padding: 10px;\n  display: none;\n}\n\nlabel {\n  margin-bottom: 5px;\n  font-weight: bold;\n  display: block;\n}\n\n.w-input,\n.w-select {\n  color: #333;\n  vertical-align: middle;\n  background-color: #fff;\n  border: 1px solid #ccc;\n  width: 100%;\n  height: 38px;\n  margin-bottom: 10px;\n  padding: 8px 12px;\n  font-size: 14px;\n  line-height: 1.42857;\n  display: block;\n}\n\n.w-input:-moz-placeholder,\n.w-select:-moz-placeholder {\n  color: #999;\n}\n\n.w-input::-moz-placeholder,\n.w-select::-moz-placeholder {\n  color: #999;\n  opacity: 1;\n}\n\n.w-input::-webkit-input-placeholder,\n.w-select::-webkit-input-placeholder {\n  color: #999;\n}\n\n.w-input:focus,\n.w-select:focus {\n  border-color: #3898ec;\n  outline: 0;\n}\n\n.w-input[disabled],\n.w-select[disabled],\n.w-input[readonly],\n.w-select[readonly],\nfieldset[disabled] .w-input,\nfieldset[disabled] .w-select {\n  cursor: not-allowed;\n}\n\n.w-input[disabled]:not(.w-input-disabled),\n.w-select[disabled]:not(.w-input-disabled),\n.w-input[readonly],\n.w-select[readonly],\nfieldset[disabled]:not(.w-input-disabled) .w-input,\nfieldset[disabled]:not(.w-input-disabled) .w-select {\n  background-color: #eee;\n}\n\ntextarea.w-input,\ntextarea.w-select {\n  height: auto;\n}\n\n.w-select {\n  background-color: #f3f3f3;\n}\n\n.w-select[multiple] {\n  height: auto;\n}\n\n.w-form-label {\n  cursor: pointer;\n  margin-bottom: 0;\n  font-weight: normal;\n  display: inline-block;\n}\n\n.w-radio {\n  margin-bottom: 5px;\n  padding-left: 20px;\n  display: block;\n}\n\n.w-radio:before,\n.w-radio:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-radio:after {\n  clear: both;\n}\n\n.w-radio-input {\n  float: left;\n  margin: 3px 0 0 -20px;\n  line-height: normal;\n}\n\n.w-file-upload {\n  margin-bottom: 10px;\n  display: block;\n}\n\n.w-file-upload-input {\n  opacity: 0;\n  z-index: -100;\n  width: 0.1px;\n  height: 0.1px;\n  position: absolute;\n  overflow: hidden;\n}\n\n.w-file-upload-default,\n.w-file-upload-uploading,\n.w-file-upload-success {\n  color: #333;\n  display: inline-block;\n}\n\n.w-file-upload-error {\n  margin-top: 10px;\n  display: block;\n}\n\n.w-file-upload-default.w-hidden,\n.w-file-upload-uploading.w-hidden,\n.w-file-upload-error.w-hidden,\n.w-file-upload-success.w-hidden {\n  display: none;\n}\n\n.w-file-upload-uploading-btn {\n  cursor: pointer;\n  background-color: #fafafa;\n  border: 1px solid #ccc;\n  margin: 0;\n  padding: 8px 12px;\n  font-size: 14px;\n  font-weight: normal;\n  display: flex;\n}\n\n.w-file-upload-file {\n  background-color: #fafafa;\n  border: 1px solid #ccc;\n  flex-grow: 1;\n  justify-content: space-between;\n  margin: 0;\n  padding: 8px 9px 8px 11px;\n  display: flex;\n}\n\n.w-file-upload-file-name {\n  font-size: 14px;\n  font-weight: normal;\n  display: block;\n}\n\n.w-file-remove-link {\n  cursor: pointer;\n  width: auto;\n  height: auto;\n  margin-top: 3px;\n  margin-left: 10px;\n  padding: 3px;\n  display: block;\n}\n\n.w-icon-file-upload-remove {\n  margin: auto;\n  font-size: 10px;\n}\n\n.w-file-upload-error-msg {\n  color: #ea384c;\n  padding: 2px 0;\n  display: inline-block;\n}\n\n.w-file-upload-info {\n  padding: 0 12px;\n  line-height: 38px;\n  display: inline-block;\n}\n\n.w-file-upload-label {\n  cursor: pointer;\n  background-color: #fafafa;\n  border: 1px solid #ccc;\n  margin: 0;\n  padding: 8px 12px;\n  font-size: 14px;\n  font-weight: normal;\n  display: inline-block;\n}\n\n.w-icon-file-upload-icon,\n.w-icon-file-upload-uploading {\n  width: 20px;\n  margin-right: 8px;\n  display: inline-block;\n}\n\n.w-icon-file-upload-uploading {\n  height: 20px;\n}\n\n.w-container {\n  max-width: 940px;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.w-container:before,\n.w-container:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-container:after {\n  clear: both;\n}\n\n.w-container .w-row {\n  margin-left: -10px;\n  margin-right: -10px;\n}\n\n.w-row:before,\n.w-row:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-row:after {\n  clear: both;\n}\n\n.w-row .w-row {\n  margin-left: 0;\n  margin-right: 0;\n}\n\n.w-col {\n  float: left;\n  width: 100%;\n  min-height: 1px;\n  padding-left: 10px;\n  padding-right: 10px;\n  position: relative;\n}\n\n.w-col .w-col {\n  padding-left: 0;\n  padding-right: 0;\n}\n\n.w-col-1 {\n  width: 8.33333%;\n}\n\n.w-col-2 {\n  width: 16.6667%;\n}\n\n.w-col-3 {\n  width: 25%;\n}\n\n.w-col-4 {\n  width: 33.3333%;\n}\n\n.w-col-5 {\n  width: 41.6667%;\n}\n\n.w-col-6 {\n  width: 50%;\n}\n\n.w-col-7 {\n  width: 58.3333%;\n}\n\n.w-col-8 {\n  width: 66.6667%;\n}\n\n.w-col-9 {\n  width: 75%;\n}\n\n.w-col-10 {\n  width: 83.3333%;\n}\n\n.w-col-11 {\n  width: 91.6667%;\n}\n\n.w-col-12 {\n  width: 100%;\n}\n\n.w-hidden-main {\n  display: none !important;\n}\n\n@media screen and (max-width: 991px) {\n  .w-container {\n    max-width: 728px;\n  }\n\n  .w-hidden-main {\n    display: inherit !important;\n  }\n\n  .w-hidden-medium {\n    display: none !important;\n  }\n\n  .w-col-medium-1 {\n    width: 8.33333%;\n  }\n\n  .w-col-medium-2 {\n    width: 16.6667%;\n  }\n\n  .w-col-medium-3 {\n    width: 25%;\n  }\n\n  .w-col-medium-4 {\n    width: 33.3333%;\n  }\n\n  .w-col-medium-5 {\n    width: 41.6667%;\n  }\n\n  .w-col-medium-6 {\n    width: 50%;\n  }\n\n  .w-col-medium-7 {\n    width: 58.3333%;\n  }\n\n  .w-col-medium-8 {\n    width: 66.6667%;\n  }\n\n  .w-col-medium-9 {\n    width: 75%;\n  }\n\n  .w-col-medium-10 {\n    width: 83.3333%;\n  }\n\n  .w-col-medium-11 {\n    width: 91.6667%;\n  }\n\n  .w-col-medium-12 {\n    width: 100%;\n  }\n\n  .w-col-stack {\n    width: 100%;\n    left: auto;\n    right: auto;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .w-hidden-main,\n  .w-hidden-medium {\n    display: inherit !important;\n  }\n\n  .w-hidden-small {\n    display: none !important;\n  }\n\n  .w-row,\n  .w-container .w-row {\n    margin-left: 0;\n    margin-right: 0;\n  }\n\n  .w-col {\n    width: 100%;\n    left: auto;\n    right: auto;\n  }\n\n  .w-col-small-1 {\n    width: 8.33333%;\n  }\n\n  .w-col-small-2 {\n    width: 16.6667%;\n  }\n\n  .w-col-small-3 {\n    width: 25%;\n  }\n\n  .w-col-small-4 {\n    width: 33.3333%;\n  }\n\n  .w-col-small-5 {\n    width: 41.6667%;\n  }\n\n  .w-col-small-6 {\n    width: 50%;\n  }\n\n  .w-col-small-7 {\n    width: 58.3333%;\n  }\n\n  .w-col-small-8 {\n    width: 66.6667%;\n  }\n\n  .w-col-small-9 {\n    width: 75%;\n  }\n\n  .w-col-small-10 {\n    width: 83.3333%;\n  }\n\n  .w-col-small-11 {\n    width: 91.6667%;\n  }\n\n  .w-col-small-12 {\n    width: 100%;\n  }\n}\n\n@media screen and (max-width: 479px) {\n  .w-container {\n    max-width: none;\n  }\n\n  .w-hidden-main,\n  .w-hidden-medium,\n  .w-hidden-small {\n    display: inherit !important;\n  }\n\n  .w-hidden-tiny {\n    display: none !important;\n  }\n\n  .w-col {\n    width: 100%;\n  }\n\n  .w-col-tiny-1 {\n    width: 8.33333%;\n  }\n\n  .w-col-tiny-2 {\n    width: 16.6667%;\n  }\n\n  .w-col-tiny-3 {\n    width: 25%;\n  }\n\n  .w-col-tiny-4 {\n    width: 33.3333%;\n  }\n\n  .w-col-tiny-5 {\n    width: 41.6667%;\n  }\n\n  .w-col-tiny-6 {\n    width: 50%;\n  }\n\n  .w-col-tiny-7 {\n    width: 58.3333%;\n  }\n\n  .w-col-tiny-8 {\n    width: 66.6667%;\n  }\n\n  .w-col-tiny-9 {\n    width: 75%;\n  }\n\n  .w-col-tiny-10 {\n    width: 83.3333%;\n  }\n\n  .w-col-tiny-11 {\n    width: 91.6667%;\n  }\n\n  .w-col-tiny-12 {\n    width: 100%;\n  }\n}\n\n.w-widget {\n  position: relative;\n}\n\n.w-widget-map {\n  width: 100%;\n  height: 400px;\n}\n\n.w-widget-map label {\n  width: auto;\n  display: inline;\n}\n\n.w-widget-map img {\n  max-width: inherit;\n}\n\n.w-widget-map .gm-style-iw {\n  text-align: center;\n}\n\n.w-widget-map .gm-style-iw > button {\n  display: none !important;\n}\n\n.w-widget-twitter {\n  overflow: hidden;\n}\n\n.w-widget-twitter-count-shim {\n  vertical-align: top;\n  text-align: center;\n  background: #fff;\n  border: 1px solid #758696;\n  border-radius: 3px;\n  width: 28px;\n  height: 20px;\n  display: inline-block;\n  position: relative;\n}\n\n.w-widget-twitter-count-shim * {\n  pointer-events: none;\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n\n.w-widget-twitter-count-shim .w-widget-twitter-count-inner {\n  text-align: center;\n  color: #999;\n  font-family: serif;\n  font-size: 15px;\n  line-height: 12px;\n  position: relative;\n}\n\n.w-widget-twitter-count-shim .w-widget-twitter-count-clear {\n  display: block;\n  position: relative;\n}\n\n.w-widget-twitter-count-shim.w--large {\n  width: 36px;\n  height: 28px;\n}\n\n.w-widget-twitter-count-shim.w--large .w-widget-twitter-count-inner {\n  font-size: 18px;\n  line-height: 18px;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical) {\n  margin-left: 5px;\n  margin-right: 8px;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical).w--large {\n  margin-left: 6px;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical):before,\n.w-widget-twitter-count-shim:not(.w--vertical):after {\n  content: \" \";\n  pointer-events: none;\n  border: solid rgba(0, 0, 0, 0);\n  width: 0;\n  height: 0;\n  position: absolute;\n  top: 50%;\n  left: 0;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical):before {\n  border-width: 4px;\n  border-color: rgba(117, 134, 150, 0) #5d6c7b rgba(117, 134, 150, 0)\n    rgba(117, 134, 150, 0);\n  margin-top: -4px;\n  margin-left: -9px;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical).w--large:before {\n  border-width: 5px;\n  margin-top: -5px;\n  margin-left: -10px;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical):after {\n  border-width: 4px;\n  border-color: rgba(255, 255, 255, 0) #fff rgba(255, 255, 255, 0)\n    rgba(255, 255, 255, 0);\n  margin-top: -4px;\n  margin-left: -8px;\n}\n\n.w-widget-twitter-count-shim:not(.w--vertical).w--large:after {\n  border-width: 5px;\n  margin-top: -5px;\n  margin-left: -9px;\n}\n\n.w-widget-twitter-count-shim.w--vertical {\n  width: 61px;\n  height: 33px;\n  margin-bottom: 8px;\n}\n\n.w-widget-twitter-count-shim.w--vertical:before,\n.w-widget-twitter-count-shim.w--vertical:after {\n  content: \" \";\n  pointer-events: none;\n  border: solid rgba(0, 0, 0, 0);\n  width: 0;\n  height: 0;\n  position: absolute;\n  top: 100%;\n  left: 50%;\n}\n\n.w-widget-twitter-count-shim.w--vertical:before {\n  border-width: 5px;\n  border-color: #5d6c7b rgba(117, 134, 150, 0) rgba(117, 134, 150, 0);\n  margin-left: -5px;\n}\n\n.w-widget-twitter-count-shim.w--vertical:after {\n  border-width: 4px;\n  border-color: #fff rgba(255, 255, 255, 0) rgba(255, 255, 255, 0);\n  margin-left: -4px;\n}\n\n.w-widget-twitter-count-shim.w--vertical .w-widget-twitter-count-inner {\n  font-size: 18px;\n  line-height: 22px;\n}\n\n.w-widget-twitter-count-shim.w--vertical.w--large {\n  width: 76px;\n}\n\n.w-background-video {\n  color: #fff;\n  height: 500px;\n  position: relative;\n  overflow: hidden;\n}\n\n.w-background-video > video {\n  object-fit: cover;\n  z-index: -100;\n  background-position: 50%;\n  background-size: cover;\n  width: 100%;\n  height: 100%;\n  margin: auto;\n  position: absolute;\n  top: -100%;\n  bottom: -100%;\n  left: -100%;\n  right: -100%;\n}\n\n.w-background-video > video::-webkit-media-controls-start-playback-button {\n  -webkit-appearance: none;\n  display: none !important;\n}\n\n.w-background-video--control {\n  background-color: rgba(0, 0, 0, 0);\n  padding: 0;\n  position: absolute;\n  bottom: 1em;\n  right: 1em;\n}\n\n.w-background-video--control > [hidden] {\n  display: none !important;\n}\n\n.w-slider {\n  text-align: center;\n  clear: both;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n  tap-highlight-color: rgba(0, 0, 0, 0);\n  background: #ddd;\n  height: 300px;\n  position: relative;\n}\n\n.w-slider-mask {\n  z-index: 1;\n  white-space: nowrap;\n  height: 100%;\n  display: block;\n  position: relative;\n  left: 0;\n  right: 0;\n  overflow: hidden;\n}\n\n.w-slide {\n  vertical-align: top;\n  white-space: normal;\n  text-align: left;\n  width: 100%;\n  height: 100%;\n  display: inline-block;\n  position: relative;\n}\n\n.w-slider-nav {\n  z-index: 2;\n  text-align: center;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n  tap-highlight-color: rgba(0, 0, 0, 0);\n  height: 40px;\n  margin: auto;\n  padding-top: 10px;\n  position: absolute;\n  top: auto;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n\n.w-slider-nav.w-round > div {\n  border-radius: 100%;\n}\n\n.w-slider-nav.w-num > div {\n  font-size: inherit;\n  line-height: inherit;\n  width: auto;\n  height: auto;\n  padding: 0.2em 0.5em;\n}\n\n.w-slider-nav.w-shadow > div {\n  box-shadow: 0 0 3px rgba(51, 51, 51, 0.4);\n}\n\n.w-slider-nav-invert {\n  color: #fff;\n}\n\n.w-slider-nav-invert > div {\n  background-color: rgba(34, 34, 34, 0.4);\n}\n\n.w-slider-nav-invert > div.w-active {\n  background-color: #222;\n}\n\n.w-slider-dot {\n  cursor: pointer;\n  background-color: rgba(255, 255, 255, 0.4);\n  width: 1em;\n  height: 1em;\n  margin: 0 3px 0.5em;\n  transition:\n    background-color 0.1s,\n    color 0.1s;\n  display: inline-block;\n  position: relative;\n}\n\n.w-slider-dot.w-active {\n  background-color: #fff;\n}\n\n.w-slider-dot:focus {\n  outline: none;\n  box-shadow: 0 0 0 2px #fff;\n}\n\n.w-slider-dot:focus.w-active {\n  box-shadow: none;\n}\n\n.w-slider-arrow-left,\n.w-slider-arrow-right {\n  cursor: pointer;\n  color: #fff;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n  tap-highlight-color: rgba(0, 0, 0, 0);\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  width: 80px;\n  margin: auto;\n  font-size: 40px;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  overflow: hidden;\n}\n\n.w-slider-arrow-left [class^=\"w-icon-\"],\n.w-slider-arrow-right [class^=\"w-icon-\"],\n.w-slider-arrow-left [class*=\" w-icon-\"],\n.w-slider-arrow-right [class*=\" w-icon-\"] {\n  position: absolute;\n}\n\n.w-slider-arrow-left:focus,\n.w-slider-arrow-right:focus {\n  outline: 0;\n}\n\n.w-slider-arrow-left {\n  z-index: 3;\n  right: auto;\n}\n\n.w-slider-arrow-right {\n  z-index: 4;\n  left: auto;\n}\n\n.w-icon-slider-left,\n.w-icon-slider-right {\n  width: 1em;\n  height: 1em;\n  margin: auto;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n\n.w-slider-aria-label {\n  clip: rect(0 0 0 0);\n  border: 0;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  position: absolute;\n  overflow: hidden;\n}\n\n.w-slider-force-show {\n  display: block !important;\n}\n\n.w-dropdown {\n  text-align: left;\n  z-index: 900;\n  margin-left: auto;\n  margin-right: auto;\n  display: inline-block;\n  position: relative;\n}\n\n.w-dropdown-btn,\n.w-dropdown-toggle,\n.w-dropdown-link {\n  vertical-align: top;\n  color: #222;\n  text-align: left;\n  white-space: nowrap;\n  margin-left: auto;\n  margin-right: auto;\n  padding: 20px;\n  text-decoration: none;\n  position: relative;\n}\n\n.w-dropdown-toggle {\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  cursor: pointer;\n  padding-right: 40px;\n  display: inline-block;\n}\n\n.w-dropdown-toggle:focus {\n  outline: 0;\n}\n\n.w-icon-dropdown-toggle {\n  width: 1em;\n  height: 1em;\n  margin: auto 20px auto auto;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  right: 0;\n}\n\n.w-dropdown-list {\n  background: #ddd;\n  min-width: 100%;\n  display: none;\n  position: absolute;\n}\n\n.w-dropdown-list.w--open {\n  display: block;\n}\n\n.w-dropdown-link {\n  color: #222;\n  padding: 10px 20px;\n  display: block;\n}\n\n.w-dropdown-link.w--current {\n  color: #0082f3;\n}\n\n.w-dropdown-link:focus {\n  outline: 0;\n}\n\n@media screen and (max-width: 767px) {\n  .w-nav-brand {\n    padding-left: 10px;\n  }\n}\n\n.w-lightbox-backdrop {\n  cursor: auto;\n  letter-spacing: normal;\n  text-indent: 0;\n  text-shadow: none;\n  text-transform: none;\n  visibility: visible;\n  white-space: normal;\n  word-break: normal;\n  word-spacing: normal;\n  word-wrap: normal;\n  color: #fff;\n  text-align: center;\n  z-index: 2000;\n  opacity: 0;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  background: rgba(0, 0, 0, 0.9);\n  outline: 0;\n  font-family:\n    Helvetica Neue,\n    Helvetica,\n    Ubuntu,\n    Segoe UI,\n    Verdana,\n    sans-serif;\n  font-size: 17px;\n  font-style: normal;\n  font-weight: 300;\n  line-height: 1.2;\n  list-style: disc;\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  -webkit-transform: translate(0);\n}\n\n.w-lightbox-backdrop,\n.w-lightbox-container {\n  -webkit-overflow-scrolling: touch;\n  height: 100%;\n  overflow: auto;\n}\n\n.w-lightbox-content {\n  height: 100vh;\n  position: relative;\n  overflow: hidden;\n}\n\n.w-lightbox-view {\n  opacity: 0;\n  width: 100vw;\n  height: 100vh;\n  position: absolute;\n}\n\n.w-lightbox-view:before {\n  content: \"\";\n  height: 100vh;\n}\n\n.w-lightbox-group,\n.w-lightbox-group .w-lightbox-view,\n.w-lightbox-group .w-lightbox-view:before {\n  height: 86vh;\n}\n\n.w-lightbox-frame,\n.w-lightbox-view:before {\n  vertical-align: middle;\n  display: inline-block;\n}\n\n.w-lightbox-figure {\n  margin: 0;\n  position: relative;\n}\n\n.w-lightbox-group .w-lightbox-figure {\n  cursor: pointer;\n}\n\n.w-lightbox-img {\n  width: auto;\n  max-width: none;\n  height: auto;\n}\n\n.w-lightbox-image {\n  float: none;\n  max-width: 100vw;\n  max-height: 100vh;\n  display: block;\n}\n\n.w-lightbox-group .w-lightbox-image {\n  max-height: 86vh;\n}\n\n.w-lightbox-caption {\n  text-align: left;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  background: rgba(0, 0, 0, 0.4);\n  padding: 0.5em 1em;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  overflow: hidden;\n}\n\n.w-lightbox-embed {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n\n.w-lightbox-control {\n  cursor: pointer;\n  background-position: center;\n  background-repeat: no-repeat;\n  background-size: 24px;\n  width: 4em;\n  transition: all 0.3s;\n  position: absolute;\n  top: 0;\n}\n\n.w-lightbox-left {\n  background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yMCAwIDI0IDQwIiB3aWR0aD0iMjQiIGhlaWdodD0iNDAiPjxnIHRyYW5zZm9ybT0icm90YXRlKDQ1KSI+PHBhdGggZD0ibTAgMGg1djIzaDIzdjVoLTI4eiIgb3BhY2l0eT0iLjQiLz48cGF0aCBkPSJtMSAxaDN2MjNoMjN2M2gtMjZ6IiBmaWxsPSIjZmZmIi8+PC9nPjwvc3ZnPg==\");\n  display: none;\n  bottom: 0;\n  left: 0;\n}\n\n.w-lightbox-right {\n  background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii00IDAgMjQgNDAiIHdpZHRoPSIyNCIgaGVpZ2h0PSI0MCI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIj48cGF0aCBkPSJtMC0waDI4djI4aC01di0yM2gtMjN6IiBvcGFjaXR5PSIuNCIvPjxwYXRoIGQ9Im0xIDFoMjZ2MjZoLTN2LTIzaC0yM3oiIGZpbGw9IiNmZmYiLz48L2c+PC9zdmc+\");\n  display: none;\n  bottom: 0;\n  right: 0;\n}\n\n.w-lightbox-close {\n  background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii00IDAgMTggMTciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxNyI+PGcgdHJhbnNmb3JtPSJyb3RhdGUoNDUpIj48cGF0aCBkPSJtMCAwaDd2LTdoNXY3aDd2NWgtN3Y3aC01di03aC03eiIgb3BhY2l0eT0iLjQiLz48cGF0aCBkPSJtMSAxaDd2LTdoM3Y3aDd2M2gtN3Y3aC0zdi03aC03eiIgZmlsbD0iI2ZmZiIvPjwvZz48L3N2Zz4=\");\n  background-size: 18px;\n  height: 2.6em;\n  right: 0;\n}\n\n.w-lightbox-strip {\n  white-space: nowrap;\n  padding: 0 1vh;\n  line-height: 0;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  overflow-x: auto;\n  overflow-y: hidden;\n}\n\n.w-lightbox-item {\n  box-sizing: content-box;\n  cursor: pointer;\n  width: 10vh;\n  padding: 2vh 1vh;\n  display: inline-block;\n  -webkit-transform: translate3d(0, 0, 0);\n}\n\n.w-lightbox-active {\n  opacity: 0.3;\n}\n\n.w-lightbox-thumbnail {\n  background: #222;\n  height: 10vh;\n  position: relative;\n  overflow: hidden;\n}\n\n.w-lightbox-thumbnail-image {\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.w-lightbox-thumbnail .w-lightbox-tall {\n  width: 100%;\n  top: 50%;\n  transform: translate(0, -50%);\n}\n\n.w-lightbox-thumbnail .w-lightbox-wide {\n  height: 100%;\n  left: 50%;\n  transform: translate(-50%);\n}\n\n.w-lightbox-spinner {\n  box-sizing: border-box;\n  border: 5px solid rgba(0, 0, 0, 0.4);\n  border-radius: 50%;\n  width: 40px;\n  height: 40px;\n  margin-top: -20px;\n  margin-left: -20px;\n  animation: 0.8s linear infinite spin;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n}\n\n.w-lightbox-spinner:after {\n  content: \"\";\n  border: 3px solid rgba(0, 0, 0, 0);\n  border-bottom-color: #fff;\n  border-radius: 50%;\n  position: absolute;\n  top: -4px;\n  bottom: -4px;\n  left: -4px;\n  right: -4px;\n}\n\n.w-lightbox-hide {\n  display: none;\n}\n\n.w-lightbox-noscroll {\n  overflow: hidden;\n}\n\n@media (min-width: 768px) {\n  .w-lightbox-content {\n    height: 96vh;\n    margin-top: 2vh;\n  }\n\n  .w-lightbox-view,\n  .w-lightbox-view:before {\n    height: 96vh;\n  }\n\n  .w-lightbox-group,\n  .w-lightbox-group .w-lightbox-view,\n  .w-lightbox-group .w-lightbox-view:before {\n    height: 84vh;\n  }\n\n  .w-lightbox-image {\n    max-width: 96vw;\n    max-height: 96vh;\n  }\n\n  .w-lightbox-group .w-lightbox-image {\n    max-width: 82.3vw;\n    max-height: 84vh;\n  }\n\n  .w-lightbox-left,\n  .w-lightbox-right {\n    opacity: 0.5;\n    display: block;\n  }\n\n  .w-lightbox-close {\n    opacity: 0.8;\n  }\n\n  .w-lightbox-control:hover {\n    opacity: 1;\n  }\n}\n\n.w-lightbox-inactive,\n.w-lightbox-inactive:hover {\n  opacity: 0;\n}\n\n.w-richtext:before,\n.w-richtext:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-richtext:after {\n  clear: both;\n}\n\n.w-richtext[contenteditable=\"true\"]:before,\n.w-richtext[contenteditable=\"true\"]:after {\n  white-space: initial;\n}\n\n.w-richtext ol,\n.w-richtext ul {\n  overflow: hidden;\n}\n\n.w-richtext .w-richtext-figure-selected.w-richtext-figure-type-video div:after,\n.w-richtext .w-richtext-figure-selected[data-rt-type=\"video\"] div:after,\n.w-richtext .w-richtext-figure-selected.w-richtext-figure-type-image div,\n.w-richtext .w-richtext-figure-selected[data-rt-type=\"image\"] div {\n  outline: 2px solid #2895f7;\n}\n\n.w-richtext figure.w-richtext-figure-type-video > div:after,\n.w-richtext figure[data-rt-type=\"video\"] > div:after {\n  content: \"\";\n  display: none;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n}\n\n.w-richtext figure {\n  max-width: 60%;\n  position: relative;\n}\n\n.w-richtext figure > div:before {\n  cursor: default !important;\n}\n\n.w-richtext figure img {\n  width: 100%;\n}\n\n.w-richtext figure figcaption.w-richtext-figcaption-placeholder {\n  opacity: 0.6;\n}\n\n.w-richtext figure div {\n  color: rgba(0, 0, 0, 0);\n  font-size: 0;\n}\n\n.w-richtext figure.w-richtext-figure-type-image,\n.w-richtext figure[data-rt-type=\"image\"] {\n  display: table;\n}\n\n.w-richtext figure.w-richtext-figure-type-image > div,\n.w-richtext figure[data-rt-type=\"image\"] > div {\n  display: inline-block;\n}\n\n.w-richtext figure.w-richtext-figure-type-image > figcaption,\n.w-richtext figure[data-rt-type=\"image\"] > figcaption {\n  caption-side: bottom;\n  display: table-caption;\n}\n\n.w-richtext figure.w-richtext-figure-type-video,\n.w-richtext figure[data-rt-type=\"video\"] {\n  width: 60%;\n  height: 0;\n}\n\n.w-richtext figure.w-richtext-figure-type-video iframe,\n.w-richtext figure[data-rt-type=\"video\"] iframe {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.w-richtext figure.w-richtext-figure-type-video > div,\n.w-richtext figure[data-rt-type=\"video\"] > div {\n  width: 100%;\n}\n\n.w-richtext figure.w-richtext-align-center {\n  clear: both;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.w-richtext figure.w-richtext-align-center.w-richtext-figure-type-image > div,\n.w-richtext figure.w-richtext-align-center[data-rt-type=\"image\"] > div {\n  max-width: 100%;\n}\n\n.w-richtext figure.w-richtext-align-normal {\n  clear: both;\n}\n\n.w-richtext figure.w-richtext-align-fullwidth {\n  text-align: center;\n  clear: both;\n  width: 100%;\n  max-width: 100%;\n  margin-left: auto;\n  margin-right: auto;\n  display: block;\n}\n\n.w-richtext figure.w-richtext-align-fullwidth > div {\n  padding-bottom: inherit;\n  display: inline-block;\n}\n\n.w-richtext figure.w-richtext-align-fullwidth > figcaption {\n  display: block;\n}\n\n.w-richtext figure.w-richtext-align-floatleft {\n  float: left;\n  clear: none;\n  margin-right: 15px;\n}\n\n.w-richtext figure.w-richtext-align-floatright {\n  float: right;\n  clear: none;\n  margin-left: 15px;\n}\n\n.w-nav {\n  z-index: 1000;\n  background: #ddd;\n  position: relative;\n}\n\n.w-nav:before,\n.w-nav:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-nav:after {\n  clear: both;\n}\n\n.w-nav-brand {\n  float: left;\n  color: #333;\n  text-decoration: none;\n  position: relative;\n}\n\n.w-nav-link {\n  vertical-align: top;\n  color: #222;\n  text-align: left;\n  margin-left: auto;\n  margin-right: auto;\n  padding: 20px;\n  text-decoration: none;\n  display: inline-block;\n  position: relative;\n}\n\n.w-nav-link.w--current {\n  color: #0082f3;\n}\n\n.w-nav-menu {\n  float: right;\n  position: relative;\n}\n\n[data-nav-menu-open] {\n  text-align: center;\n  background: #c8c8c8;\n  min-width: 200px;\n  position: absolute;\n  top: 100%;\n  left: 0;\n  right: 0;\n  overflow: visible;\n  display: block !important;\n}\n\n.w--nav-link-open {\n  display: block;\n  position: relative;\n}\n\n.w-nav-overlay {\n  width: 100%;\n  display: none;\n  position: absolute;\n  top: 100%;\n  left: 0;\n  right: 0;\n  overflow: hidden;\n}\n\n.w-nav-overlay [data-nav-menu-open] {\n  top: 0;\n}\n\n.w-nav[data-animation=\"over-left\"] .w-nav-overlay {\n  width: auto;\n}\n\n.w-nav[data-animation=\"over-left\"] .w-nav-overlay,\n.w-nav[data-animation=\"over-left\"] [data-nav-menu-open] {\n  z-index: 1;\n  top: 0;\n  right: auto;\n}\n\n.w-nav[data-animation=\"over-right\"] .w-nav-overlay {\n  width: auto;\n}\n\n.w-nav[data-animation=\"over-right\"] .w-nav-overlay,\n.w-nav[data-animation=\"over-right\"] [data-nav-menu-open] {\n  z-index: 1;\n  top: 0;\n  left: auto;\n}\n\n.w-nav-button {\n  float: right;\n  cursor: pointer;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n  tap-highlight-color: rgba(0, 0, 0, 0);\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  padding: 18px;\n  font-size: 24px;\n  display: none;\n  position: relative;\n}\n\n.w-nav-button:focus {\n  outline: 0;\n}\n\n.w-nav-button.w--open {\n  color: #fff;\n  background-color: #c8c8c8;\n}\n\n.w-nav[data-collapse=\"all\"] .w-nav-menu {\n  display: none;\n}\n\n.w-nav[data-collapse=\"all\"] .w-nav-button,\n.w--nav-dropdown-open,\n.w--nav-dropdown-toggle-open {\n  display: block;\n}\n\n.w--nav-dropdown-list-open {\n  position: static;\n}\n\n@media screen and (max-width: 991px) {\n  .w-nav[data-collapse=\"medium\"] .w-nav-menu {\n    display: none;\n  }\n\n  .w-nav[data-collapse=\"medium\"] .w-nav-button {\n    display: block;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .w-nav[data-collapse=\"small\"] .w-nav-menu {\n    display: none;\n  }\n\n  .w-nav[data-collapse=\"small\"] .w-nav-button {\n    display: block;\n  }\n\n  .w-nav-brand {\n    padding-left: 10px;\n  }\n}\n\n@media screen and (max-width: 479px) {\n  .w-nav[data-collapse=\"tiny\"] .w-nav-menu {\n    display: none;\n  }\n\n  .w-nav[data-collapse=\"tiny\"] .w-nav-button {\n    display: block;\n  }\n}\n\n.w-tabs {\n  position: relative;\n}\n\n.w-tabs:before,\n.w-tabs:after {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-tabs:after {\n  clear: both;\n}\n\n.w-tab-menu {\n  position: relative;\n}\n\n.w-tab-link {\n  vertical-align: top;\n  text-align: left;\n  cursor: pointer;\n  color: #222;\n  background-color: #ddd;\n  padding: 9px 30px;\n  text-decoration: none;\n  display: inline-block;\n  position: relative;\n}\n\n.w-tab-link.w--current {\n  background-color: #c8c8c8;\n}\n\n.w-tab-link:focus {\n  outline: 0;\n}\n\n.w-tab-content {\n  display: block;\n  position: relative;\n  overflow: hidden;\n}\n\n.w-tab-pane {\n  display: none;\n  position: relative;\n}\n\n.w--tab-active {\n  display: block;\n}\n\n@media screen and (max-width: 479px) {\n  .w-tab-link {\n    display: block;\n  }\n}\n\n.w-ix-emptyfix:after {\n  content: \"\";\n}\n\n@keyframes spin {\n  0% {\n    transform: rotate(0);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.w-dyn-empty {\n  background-color: #ddd;\n  padding: 10px;\n}\n\n.w-dyn-hide,\n.w-dyn-bind-empty,\n.w-condition-invisible {\n  display: none !important;\n}\n\n.wf-layout-layout {\n  display: grid;\n}\n\n.w-code-component > * {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0;\n  left: 0;\n}\n\n.w-layout-blockcontainer {\n  max-width: 940px;\n  margin-left: auto;\n  margin-right: auto;\n  display: block;\n}\n\n.w-layout-layout {\n  grid-row-gap: 20px;\n  grid-column-gap: 20px;\n  grid-auto-columns: 1fr;\n  justify-content: center;\n  padding: 20px;\n}\n\n.w-layout-cell {\n  flex-direction: column;\n  justify-content: flex-start;\n  align-items: flex-start;\n  display: flex;\n}\n\n.w-layout-vflex {\n  flex-direction: column;\n  align-items: flex-start;\n  display: flex;\n}\n\n.w-layout-hflex {\n  flex-direction: row;\n  align-items: flex-start;\n  display: flex;\n}\n\n.w-embed-youtubevideo {\n  background-image: url(\"https://d3e54v103j8qbb.cloudfront.net/static/youtube-placeholder.2b05e7d68d.svg\");\n  background-position: 50%;\n  background-size: cover;\n  width: 100%;\n  padding-bottom: 0;\n  padding-left: 0;\n  padding-right: 0;\n  position: relative;\n}\n\n.w-embed-youtubevideo:empty {\n  min-height: 75px;\n  padding-bottom: 56.25%;\n}\n\n.w-checkbox {\n  margin-bottom: 5px;\n  padding-left: 20px;\n  display: block;\n}\n\n.w-checkbox:before {\n  content: \" \";\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-checkbox:after {\n  content: \" \";\n  clear: both;\n  grid-area: 1 / 1 / 2 / 2;\n  display: table;\n}\n\n.w-checkbox-input {\n  float: left;\n  margin: 4px 0 0 -20px;\n  line-height: normal;\n}\n\n.w-checkbox-input--inputType-custom {\n  border: 1px solid #ccc;\n  border-radius: 2px;\n  width: 12px;\n  height: 12px;\n}\n\n.w-checkbox-input--inputType-custom.w--redirected-checked {\n  background-color: #3898ec;\n  background-image: url(\"https://d3e54v103j8qbb.cloudfront.net/static/custom-checkbox-checkmark.589d534424.svg\");\n  background-position: 50%;\n  background-repeat: no-repeat;\n  background-size: cover;\n  border-color: #3898ec;\n}\n\n.w-checkbox-input--inputType-custom.w--redirected-focus {\n  box-shadow: 0 0 3px 1px #3898ec;\n}\n\n.w-form-formradioinput--inputType-custom {\n  border: 1px solid #ccc;\n  border-radius: 50%;\n  width: 12px;\n  height: 12px;\n}\n\n.w-form-formradioinput--inputType-custom.w--redirected-focus {\n  box-shadow: 0 0 3px 1px #3898ec;\n}\n\n.w-form-formradioinput--inputType-custom.w--redirected-checked {\n  border-width: 4px;\n  border-color: #3898ec;\n}\n\n.w-form-formrecaptcha {\n  margin-bottom: 8px;\n}\n\n.w-backgroundvideo-backgroundvideoplaypausebutton:focus-visible {\n  outline-offset: 2px;\n  border-radius: 50%;\n  outline: 2px solid #3b79c3;\n}\n\n.w-layout-grid {\n  grid-row-gap: 16px;\n  grid-column-gap: 16px;\n  grid-template-rows: auto auto;\n  grid-template-columns: 1fr 1fr;\n  grid-auto-columns: 1fr;\n  display: grid;\n}\n\n@media screen and (max-width: 991px) {\n  .w-layout-blockcontainer {\n    max-width: 728px;\n  }\n}\n\n@media screen and (max-width: 767px) {\n  .w-layout-blockcontainer {\n    max-width: none;\n  }\n}\n\n.utility-page-wrap {\n  justify-content: center;\n  align-items: center;\n  width: 100vw;\n  max-width: 100%;\n  height: 100vh;\n  max-height: 100%;\n  display: flex;\n}\n\n.utility-page-content {\n  text-align: center;\n  flex-direction: column;\n  width: 260px;\n  display: flex;\n}\n\n.utility-page-form {\n  flex-direction: column;\n  align-items: stretch;\n  display: flex;\n}\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste/plugin-webflow/styles.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { url } from \"css-tree\";\nimport type {\n  Breakpoint,\n  Instance,\n  WebstudioFragment,\n} from \"@webstudio-is/sdk\";\nimport { equalMedia, hyphenateProperty } from \"@webstudio-is/css-engine\";\nimport {\n  camelCaseProperty,\n  parseCss,\n  pseudoElements,\n  type ParsedStyleDecl,\n} from \"@webstudio-is/css-data\";\nimport { $styleSources } from \"~/shared/sync/data-stores\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport { mapGroupBy } from \"~/shared/shim\";\nimport type { WfStylePresets } from \"./style-presets-overrides\";\nimport type { WfAsset, WfElementNode, WfNode, WfStyle } from \"./schema\";\n\nconst { toast } = builderApi;\n\ntype WfBreakpoint = { minWidth?: number; maxWidth?: number };\n\ntype WfBreakpointName =\n  | \"base\"\n  | \"xxl\"\n  | \"xl\"\n  | \"large\"\n  | \"medium\"\n  | \"small\"\n  | \"tiny\";\n\nconst wfBreakpoints = new Map<WfBreakpointName, WfBreakpoint>([\n  [\"base\", {}],\n  [\"xxl\", { minWidth: 1920 }],\n  [\"xl\", { minWidth: 1440 }],\n  [\"large\", { minWidth: 1280 }],\n  [\"medium\", { maxWidth: 991 }],\n  [\"small\", { maxWidth: 767 }],\n  [\"tiny\", { maxWidth: 479 }],\n]);\nconst wfBreakpointByMediaQuery = new Map<undefined | string, WfBreakpointName>([\n  [undefined, \"base\"],\n  [\"(min-width:1920px)\", \"xxl\"],\n  [\"(min-width:1440px)\", \"xl\"],\n  [\"(min-width:1280px)\", \"large\"],\n  [\"(max-width:991px)\", \"medium\"],\n  [\"(max-width:767px)\", \"small\"],\n  [\"(max-width:479px)\", \"tiny\"],\n]);\n\nconst parseVariantName = (variant: string) => {\n  let [breakpointName, state = \"\"] = variant.split(\"_\");\n  if (state) {\n    const separator = pseudoElements.includes(\n      state as (typeof pseudoElements)[number]\n    )\n      ? \"::\"\n      : \":\";\n    state = separator + state;\n  }\n  if (wfBreakpoints.has(breakpointName as WfBreakpointName) === false) {\n    if (breakpointName !== \"main\") {\n      console.error(`Invalid breakpoint name: ${breakpointName}`);\n    }\n    breakpointName = \"base\";\n  }\n\n  return {\n    breakpointName: breakpointName as WfBreakpointName,\n    state,\n  };\n};\n\n// Apparently webflow supports this variable notation: `color: @var_relume-variable-color-neutral-1`\n// Until actual variables are supported, we need to replace it with \"unset\",\n// otherwise property will be skipped and that will result in an inherited value, which is more problematic.\n// @todo use CSS variables once this is done https://github.com/webstudio-is/webstudio/issues/3399\nconst replaceAtVariables = (styles: string) => {\n  return styles.replace(/@var_[\\w-]+/g, \"unset\");\n};\n\n// Converts webflow asset references like `@img_667d0b7769e0cc3754b584f6` to valid urls like\n// url(\"https://667d0b7769e0cc3754b584f6\") to not break csstree parser\nconst replaceAtImages = (\n  styles: string,\n  wfAssets: Map<WfAsset[\"_id\"], WfAsset>\n) => {\n  return styles.replace(/@img_[\\w-]+/g, (match) => {\n    const assetId = match.slice(5);\n    const asset = wfAssets.get(assetId);\n\n    if (asset === undefined) {\n      if (assetId !== \"example_bg\") {\n        console.error(`Asset not found: ${assetId}`);\n      }\n      return `none`;\n    }\n\n    return url.encode(asset.s3Url);\n  });\n};\n\nconst processStyles = (parsedStyles: ParsedStyleDecl[]) => {\n  const styles = new Map<string, ParsedStyleDecl>();\n  for (const parsedStyleDecl of parsedStyles) {\n    const { breakpoint, selector, state, property } = parsedStyleDecl;\n    const key = `${breakpoint}:${selector}:${state}:${property}`;\n    styles.set(key, parsedStyleDecl);\n  }\n  for (const parsedStyleDecl of styles.values()) {\n    const { breakpoint, selector, state, property } = parsedStyleDecl;\n    const key = `${breakpoint}:${selector}:${state}:${property}`;\n    styles.set(key, parsedStyleDecl);\n    if (property === \"background-clip\") {\n      const colorKey = `${breakpoint}:${selector}:${state}:color`;\n      styles.delete(colorKey);\n      styles.set(colorKey, {\n        ...parsedStyleDecl,\n        property: \"color\",\n        value: { type: \"keyword\", value: \"transparent\" },\n      });\n    }\n  }\n  return Array.from(styles.values());\n};\n\ntype UnparsedVariants = Map<string, string | Array<ParsedStyleDecl>>;\n\n// Variants value can be wf styleLess string which is a styles block\n// or it can be an array of EmbedTemplateStyleDecl.\n// If its an array, convert it to ws style decl.\nconst toParsedVariants = (variants: UnparsedVariants) => {\n  const parsedVariants = new Map<WfBreakpointName, Array<ParsedStyleDecl>>();\n  for (const [variant, styles] of variants) {\n    const { breakpointName, state } = parseVariantName(variant);\n    if (typeof styles === \"string\") {\n      try {\n        const sanitizedStyles = styles.replaceAll(/@raw<\\|([^@]+)\\|>/g, \"$1\");\n        const parsed = processStyles(\n          parseCss(`.styles${state} {${sanitizedStyles}}`) ?? []\n        );\n        const allBreakpointStyles = parsedVariants.get(breakpointName) ?? [];\n        allBreakpointStyles.push(...parsed);\n        parsedVariants.set(breakpointName, allBreakpointStyles);\n      } catch (error) {\n        console.error(\"Failed to parse style\", error, breakpointName, styles);\n      }\n      continue;\n    }\n    parsedVariants.set(breakpointName, styles);\n  }\n\n  return parsedVariants;\n};\n\nconst addNodeStyles = ({\n  styleSourceId,\n  variants,\n  fragment,\n}: {\n  styleSourceId: string;\n  variants: UnparsedVariants;\n  fragment: WebstudioFragment;\n}) => {\n  const parsedVariants = toParsedVariants(variants);\n  for (const [wfBreakpointName, styles] of parsedVariants) {\n    if (styles.length === 0) {\n      // We don't want to add breakpoints if there are no styles defined on them\n      continue;\n    }\n    const wfBreakpoint = wfBreakpoints.get(wfBreakpointName);\n    if (wfBreakpoint === undefined) {\n      console.error(`Unknown breakpoint \"${wfBreakpointName}\"`);\n      continue;\n    }\n    let breakpoint = fragment.breakpoints.find((breakpoint) => {\n      return equalMedia(breakpoint, wfBreakpoint);\n    });\n\n    if (breakpoint === undefined) {\n      breakpoint = {\n        id: nanoid(),\n        label: wfBreakpointName,\n        ...(wfBreakpoint.minWidth !== undefined && {\n          minWidth: wfBreakpoint.minWidth,\n        }),\n        ...(wfBreakpoint.maxWidth !== undefined && {\n          maxWidth: wfBreakpoint.maxWidth,\n        }),\n      } satisfies Breakpoint;\n      fragment.breakpoints.push(breakpoint);\n    }\n\n    for (const style of styles) {\n      fragment.styles.push({\n        styleSourceId,\n        breakpointId: breakpoint.id,\n        property: camelCaseProperty(style.property),\n        value: style.value,\n        state: style.state,\n      });\n      if (style.value.type === \"invalid\") {\n        const error = `Invalid style value: Local \"${style.property}: ${style.value.value}\"`;\n        toast.error(error);\n        console.error(error);\n      }\n    }\n  }\n};\n\nconst addStyleSource = (\n  styleSourceId: string,\n  instanceId: Instance[\"id\"],\n  fragment: WebstudioFragment\n) => {\n  let styleSourceSelection = fragment.styleSourceSelections.find(\n    (selection) => selection.instanceId === instanceId\n  );\n  if (styleSourceSelection === undefined) {\n    styleSourceSelection = { instanceId, values: [] };\n    fragment.styleSourceSelections.push(styleSourceSelection);\n  }\n\n  if (styleSourceSelection.values.includes(styleSourceId) === false) {\n    styleSourceSelection.values.push(styleSourceId);\n  }\n};\n\nconst addNodeTokenStyles = ({\n  styleSourceId,\n  name,\n  variants,\n  instanceId,\n  fragment,\n}: {\n  styleSourceId: string;\n  name: string;\n  variants: UnparsedVariants;\n  instanceId: Instance[\"id\"];\n  fragment: WebstudioFragment;\n}) => {\n  fragment.styleSources.push({\n    type: \"token\",\n    id: styleSourceId,\n    name,\n  });\n\n  addStyleSource(styleSourceId, instanceId, fragment);\n\n  addNodeStyles({\n    styleSourceId,\n    variants,\n    fragment,\n  });\n};\n\nconst addNodeLocalStyles = ({\n  styleSourceId,\n  style,\n  instanceId,\n  fragment,\n}: {\n  styleSourceId: string;\n  style: NonNullable<WfElementNode[\"data\"]>[\"style\"];\n  instanceId: Instance[\"id\"];\n  fragment: WebstudioFragment;\n}) => {\n  let hasLocalStyles = false;\n  const instanceDataVariants = new Map<string, string>();\n  // @todo we don't know what is base, maybe the opposite of cringe?\n  for (const baseStyle of Object.values(style ?? {})) {\n    for (const [breakpoint, breakpointStyle] of Object.entries(baseStyle)) {\n      let css = ``;\n      // @todo we don't know format for pseudo\n      // because webflow does not set styles on pseudo\n      // in quick stack instance\n      for (const pseudoStyle of Object.values(breakpointStyle)) {\n        for (const [property, value] of Object.entries(pseudoStyle)) {\n          hasLocalStyles = true;\n          css += `${hyphenateProperty(property)}: ${value};`;\n        }\n      }\n      instanceDataVariants.set(breakpoint, css);\n    }\n  }\n  if (hasLocalStyles === false) {\n    return;\n  }\n\n  fragment.styleSources.push({\n    type: \"local\",\n    id: styleSourceId,\n  });\n  let styleSourceSelection = fragment.styleSourceSelections.find(\n    (selection) => selection.instanceId === instanceId\n  );\n  if (styleSourceSelection === undefined) {\n    styleSourceSelection = { instanceId, values: [] };\n    fragment.styleSourceSelections.push(styleSourceSelection);\n  }\n  styleSourceSelection.values.push(styleSourceId);\n\n  addNodeStyles({\n    styleSourceId,\n    variants: instanceDataVariants,\n    fragment,\n  });\n};\n\nconst mapComponentAndPresetStyles = (\n  wfNode: WfElementNode,\n  stylePresets: WfStylePresets\n) => {\n  const component = wfNode.type;\n  const presetStyles: Array<keyof WfStylePresets> = [];\n\n  if (wfNode.tag in stylePresets) {\n    presetStyles.push(wfNode.tag as keyof typeof stylePresets);\n  }\n\n  switch (component) {\n    case \"Link\": {\n      const data = wfNode.data;\n      if (data.button) {\n        presetStyles.push(\"w-button\");\n      }\n      if (data.block === \"inline\") {\n        presetStyles.push(\"w-inline-block\");\n      }\n      return presetStyles;\n    }\n    case \"CodeBlock\": {\n      presetStyles.push(\"w-code-block\");\n      return presetStyles;\n    }\n    case \"HtmlEmbed\": {\n      presetStyles.push(\"w-embed\");\n      return presetStyles;\n    }\n    case \"BlockContainer\": {\n      presetStyles.push(\"w-layout-blockcontainer\");\n      presetStyles.push(\"w-container\");\n      return presetStyles;\n    }\n    case \"Row\": {\n      presetStyles.push(\"w-row\");\n      return presetStyles;\n    }\n    case \"Cell\": {\n      presetStyles.push(\"w-layout-cell\");\n      return presetStyles;\n    }\n    case \"Column\": {\n      // @todo wf has w-col-1 etc in grid\n      presetStyles.push(\"w-col\");\n      return presetStyles;\n    }\n    case \"Grid\": {\n      presetStyles.push(\"w-layout-grid\");\n      return presetStyles;\n    }\n    case \"Layout\": {\n      presetStyles.push(\"w-layout-layout\");\n      presetStyles.push(\"wf-layout-layout\");\n      return presetStyles;\n    }\n    case \"HFlex\": {\n      presetStyles.push(\"w-layout-hflex\");\n      return presetStyles;\n    }\n    case \"VFlex\": {\n      presetStyles.push(\"w-layout-vflex\");\n      return presetStyles;\n    }\n    case \"FormWrapper\": {\n      presetStyles.push(\"w-form\");\n      return presetStyles;\n    }\n    case \"FormTextInput\":\n    case \"FormTextarea\": {\n      presetStyles.push(\"w-input\");\n      return presetStyles;\n    }\n    case \"FormButton\": {\n      presetStyles.push(\"w-button\");\n      return presetStyles;\n    }\n    case \"FormCheckboxWrapper\": {\n      presetStyles.push(\"w-checkbox\");\n      return presetStyles;\n    }\n    case \"FormCheckboxInput\": {\n      presetStyles.push(\"w-checkbox-input\");\n      return presetStyles;\n    }\n    case \"FormInlineLabel\": {\n      presetStyles.push(\"w-form-label\");\n      return presetStyles;\n    }\n    case \"FormRadioWrapper\": {\n      presetStyles.push(\"w-radio\");\n      return presetStyles;\n    }\n    case \"FormRadioInput\": {\n      presetStyles.push(\"w-radio-input\");\n      return presetStyles;\n    }\n\n    case \"Icon\": {\n      const data = wfNode.data;\n\n      if (data.widget?.icon) {\n        presetStyles.push(`w-icon-${data.widget.icon}`);\n      }\n      return presetStyles;\n    }\n\n    case \"NavbarMenu\":\n      presetStyles.push(\"w-nav-menu\");\n      return presetStyles;\n\n    case \"NavbarContainer\":\n      presetStyles.push(\"w-container\");\n      return presetStyles;\n\n    case \"NavbarWrapper\":\n      presetStyles.push(\"w-nav\");\n      return presetStyles;\n\n    case \"NavbarBrand\":\n      presetStyles.push(\"w-nav-brand\");\n      return presetStyles;\n\n    case \"NavbarLink\":\n      presetStyles.push(\"w-nav-link\");\n      return presetStyles;\n\n    case \"NavbarButton\":\n      presetStyles.push(\"w-nav-button\");\n      return presetStyles;\n  }\n\n  return presetStyles;\n};\n\n// Merges wf styles that are combo classes into a single style.\n// Checks if a style source with that name already exists and the new one has new styles and is not empty - if so, adds a number to a name.\nconst mergeComboStyles = (wfStyles: Array<WfStyle>) => {\n  const classes = new Set<string>();\n  const skip = new Set<string>();\n  let mergedStyle;\n  for (const wfStyle of wfStyles) {\n    const { name } = wfStyle;\n\n    classes.add(name);\n\n    if (mergedStyle === undefined) {\n      mergedStyle = { variants: {}, ...wfStyle, name };\n      continue;\n    }\n\n    const comboClass = mergedStyle.name + \".\" + name;\n\n    // We need to avoid creating combo classes when they have no additional styles.\n    if (wfStyle.comb === \"&\" && wfStyle.styleLess === \"\") {\n      skip.add(comboClass);\n      continue;\n    }\n\n    mergedStyle.styleLess += wfStyle.styleLess;\n    mergedStyle.name = comboClass;\n    for (const key in wfStyle.variants) {\n      if (key in mergedStyle.variants === false) {\n        mergedStyle.variants[key] = { styleLess: \"\" };\n      }\n      mergedStyle.variants[key].styleLess += wfStyle.variants[key];\n    }\n  }\n\n  const classesArray = Array.from(classes);\n  // Produce all possible combinations of combo classes so we can check later if they alredy exist.\n  // This is needed to achieve the same end-result as with combo-classes in webflow.\n  // Example .a.b.c -> .a, .b, .c, .a.b, .a.c, .b.c, .a.b.c\n  const comboClasses = classesArray\n    .flatMap((name1) => classesArray.map((name2) => `${name1}.${name2}`))\n    .filter((name) => skip.has(name) === false);\n\n  return {\n    mergedStyle,\n    classes: classesArray,\n    comboClasses,\n  };\n};\n\nexport const addStyles = async ({\n  wfNodes,\n  wfStyles,\n  wfAssets,\n  doneNodes,\n  fragment,\n  generateStyleSourceId,\n}: {\n  wfNodes: Map<WfNode[\"_id\"], WfNode>;\n  wfStyles: Map<WfStyle[\"_id\"], WfStyle>;\n  wfAssets: Map<WfAsset[\"_id\"], WfAsset>;\n  doneNodes: Map<WfNode[\"_id\"], Instance[\"id\"] | false>;\n  fragment: WebstudioFragment;\n  generateStyleSourceId: (sourceData: string) => Promise<string>;\n}) => {\n  const { stylePresets } = await import(\"./style-presets-overrides\");\n\n  for (const wfNode of wfNodes.values()) {\n    if (\"text\" in wfNode) {\n      continue;\n    }\n    const instanceId = doneNodes.get(wfNode._id);\n    if (instanceId === false) {\n      continue;\n    }\n    if (instanceId === undefined) {\n      console.error(`No instance id found for node ${wfNode._id}`);\n      continue;\n    }\n\n    for (const name of mapComponentAndPresetStyles(wfNode, stylePresets)) {\n      addNodeTokenStyles({\n        styleSourceId: await generateStyleSourceId(name),\n        name,\n        variants: new Map(\n          Array.from(\n            mapGroupBy(\n              stylePresets[name] as Array<ParsedStyleDecl>,\n              (item) => item.breakpoint\n            ),\n            ([mediaQuery, value]) => [\n              wfBreakpointByMediaQuery.get(mediaQuery) ?? \"base\",\n              value,\n            ]\n          )\n        ),\n        instanceId,\n        fragment,\n      });\n    }\n\n    addNodeLocalStyles({\n      styleSourceId: await generateStyleSourceId(wfNode._id),\n      style: wfNode.data?.style,\n      instanceId,\n      fragment,\n    });\n\n    const instance = fragment.instances.find(\n      (instance) => instance.id === instanceId\n    );\n\n    if (instance === undefined) {\n      console.error(`No instance found for ${instanceId}`);\n      continue;\n    }\n\n    const wfNodeStyles = wfNode.classes\n      .map((classId) => wfStyles.get(classId))\n      .filter(<T>(value: T): value is NonNullable<T> => value !== undefined);\n\n    const stylesMerge = mergeComboStyles(wfNodeStyles);\n    const wfStyle = stylesMerge.mergedStyle;\n    if (wfStyle === undefined) {\n      continue;\n    }\n    if (instance && instance.label === undefined) {\n      instance.label = wfStyle.name;\n    }\n\n    const variants = new Map();\n    variants.set(\n      \"base\",\n      replaceAtImages(replaceAtVariables(wfStyle.styleLess), wfAssets)\n    );\n    const wfVariants = wfStyle.variants;\n    Object.keys(wfVariants).forEach((breakpointName) => {\n      const variant = wfVariants[breakpointName as keyof typeof wfVariants];\n      if (variant && \"styleLess\" in variant) {\n        variants.set(\n          breakpointName,\n          replaceAtImages(replaceAtVariables(variant.styleLess), wfAssets)\n        );\n      }\n    });\n\n    // We need to see if the individual classes have already existed in the system\n    // and if so, add them to the selection, because webflow relies on combo class logic and we merge the combo into one.\n    const addExistingStyleSources = async (classes: Array<string>) => {\n      for (const className of classes) {\n        const styleSourceId = await generateStyleSourceId(className);\n        if ($styleSources.get().has(styleSourceId)) {\n          addStyleSource(styleSourceId, instanceId, fragment);\n        }\n      }\n    };\n\n    // First go individual classes: .a, .b, .c\n    await addExistingStyleSources(stylesMerge.classes);\n\n    // Second goes merged combo class .a.b.c\n    addNodeTokenStyles({\n      styleSourceId: await generateStyleSourceId(wfStyle.name),\n      name: wfStyle.name,\n      variants,\n      instanceId,\n      fragment,\n    });\n\n    // Third goes .a.b, .b.c\n    // This worder is arbitrary and seems to be most likely to match webflow,\n    // but it can be wrong sometimes.\n    // Source order specificity in Webflow is a mess.\n    await addExistingStyleSources(stylesMerge.comboClasses);\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/copy-paste.test.tsx",
    "content": "import { describe, expect, test } from \"vitest\";\nimport stripIndent from \"strip-indent\";\nimport { createRegularStyleSheet } from \"@webstudio-is/css-engine\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport {\n  encodeDataVariableId,\n  getStyleDeclKey,\n  ROOT_INSTANCE_ID,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport {\n  $,\n  ws,\n  css,\n  renderData,\n  type TemplateStyleDecl,\n  expression,\n  Variable,\n  ResourceValue,\n  ActionValue,\n  renderTemplate,\n  Parameter,\n} from \"@webstudio-is/template\";\nimport type { Project } from \"@webstudio-is/project\";\nimport {\n  extractWebstudioFragment,\n  insertWebstudioFragmentCopy,\n} from \"./instance-utils\";\nimport { $project } from \"./nano-states\";\nimport { findAvailableVariables } from \"./data-variables\";\nimport { camelCaseProperty } from \"@webstudio-is/css-data\";\n\n$project.set({ id: \"current_project\" } as Project);\n\nconst createStub = (element: JSX.Element) => {\n  const project = {\n    pages: createDefaultPages({ rootInstanceId: \"\" }),\n    ...renderData(element),\n  };\n  // global root instance is never stored in data\n  project.instances.delete(ROOT_INSTANCE_ID);\n  return project;\n};\n\nconst toCss = (data: WebstudioData) => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"base\");\n  for (const { instanceId, values } of data.styleSourceSelections.values()) {\n    for (const styleSourceId of values) {\n      const styleSource = data.styleSources.get(styleSourceId);\n      let name;\n      if (styleSource?.type === \"local\") {\n        name = `${instanceId}:local`;\n      }\n      if (styleSource?.type === \"token\") {\n        name = `${instanceId}:token(${styleSource.name})`;\n      }\n      if (name) {\n        const rule = sheet.addNestingRule(name);\n        for (const styleDecl of data.styles.values()) {\n          if (styleDecl.styleSourceId === styleSourceId) {\n            rule.setDeclaration({\n              breakpoint: styleDecl.breakpointId,\n              selector: styleDecl.state ?? \"\",\n              property: styleDecl.property,\n              value: styleDecl.value,\n            });\n          }\n        }\n      }\n    }\n  }\n  return sheet.cssText;\n};\n\nconst insertStyles = ({\n  data,\n  breakpointId,\n  styleSourceId,\n  style,\n}: {\n  data: WebstudioData;\n  breakpointId: string;\n  styleSourceId: string;\n  style: TemplateStyleDecl[];\n}) => {\n  for (const { state, property, value } of style) {\n    const newStyleDecl = {\n      breakpointId,\n      styleSourceId,\n      state,\n      property: camelCaseProperty(property),\n      value,\n    };\n    data.styles.set(getStyleDeclKey(newStyleDecl), newStyleDecl);\n  }\n};\n\ntest(\"extract the instance by id and all its descendants including slot instances\", () => {\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <$.Box ws:id=\"boxId\">\n        <$.Slot>\n          <$.Fragment></$.Fragment>\n        </$.Slot>\n      </$.Box>\n      <$.Text ws:id=\"textId\"></$.Text>\n    </$.Body>\n  );\n  const { instances } = extractWebstudioFragment(data, \"boxId\");\n  expect(instances).toEqual([\n    expect.objectContaining({ component: \"Box\" }),\n    expect.objectContaining({ component: \"Slot\" }),\n    expect.objectContaining({ component: \"Fragment\" }),\n  ]);\n});\n\ntest(\"insert instances with slots\", () => {\n  const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n  const fragment = renderTemplate(\n    <$.Slot ws:id=\"slotId\">\n      <$.Fragment ws:id=\"fragmentId\">\n        <$.Box ws:id=\"boxId\"></$.Box>\n      </$.Fragment>\n    </$.Slot>\n  );\n  expect(data.instances.size).toEqual(1);\n  insertWebstudioFragmentCopy({\n    data,\n    fragment,\n    availableVariables: [],\n    projectId: \"\",\n  });\n  expect(data.instances.size).toEqual(4);\n  insertWebstudioFragmentCopy({\n    data,\n    fragment,\n    availableVariables: [],\n    projectId: \"\",\n  });\n  expect(data.instances.size).toEqual(5);\n  expect(Array.from(data.instances.values())).toEqual([\n    expect.objectContaining({ component: \"Body\" }),\n    // id of slot instances are preserved\n    expect.objectContaining({ component: \"Fragment\", id: \"fragmentId\" }),\n    expect.objectContaining({ component: \"Box\", id: \"boxId\" }),\n    expect.objectContaining({ component: \"Slot\" }),\n    expect.objectContaining({ component: \"Slot\" }),\n  ]);\n});\n\ntest(\"insert instances with multiple roots\", () => {\n  const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n  const fragment = renderTemplate(\n    <>\n      <$.Box>\n        <$.Text></$.Text>\n      </$.Box>\n      <$.Box>\n        <$.Text></$.Text>\n      </$.Box>\n    </>\n  );\n  expect(data.instances.size).toEqual(1);\n  insertWebstudioFragmentCopy({\n    data,\n    fragment,\n    availableVariables: [],\n    projectId: \"\",\n  });\n  expect(data.instances.size).toEqual(5);\n});\n\ntest(\"should add :root local styles\", () => {\n  const oldProject = createStub(\n    <ws.root\n      ws:id={ROOT_INSTANCE_ID}\n      ws:style={css`\n        color: red;\n      `}\n    >\n      <$.Body></$.Body>\n    </ws.root>\n  );\n  const newProject = createStub(<$.Body></$.Body>);\n  const fragment = extractWebstudioFragment(oldProject, ROOT_INSTANCE_ID);\n  insertWebstudioFragmentCopy({\n    data: newProject,\n    fragment,\n    availableVariables: [],\n    projectId: \"\",\n  });\n  expect(toCss(newProject)).toEqual(\n    stripIndent(`\n      @media all {\n        :root:local {\n          color: red\n        }\n      }\n    `).trim()\n  );\n});\n\ntest(\"should merge :root local styles\", () => {\n  const oldProject = createStub(\n    <ws.root\n      ws:id={ROOT_INSTANCE_ID}\n      ws:style={css`\n        color: red;\n      `}\n    >\n      <$.Body></$.Body>\n    </ws.root>\n  );\n  const newProject = createStub(\n    <ws.root\n      ws:id={ROOT_INSTANCE_ID}\n      ws:style={css`\n        font-size: medium;\n      `}\n    >\n      <$.Body></$.Body>\n    </ws.root>\n  );\n  const fragment = extractWebstudioFragment(oldProject, ROOT_INSTANCE_ID);\n  insertWebstudioFragmentCopy({\n    data: newProject,\n    fragment,\n    availableVariables: [],\n    projectId: \"\",\n  });\n  expect(toCss(newProject)).toEqual(\n    stripIndent(`\n      @media all {\n        :root:local {\n          font-size: medium;\n          color: red\n        }\n      }\n    `).trim()\n  );\n});\n\ntest(\"should copy local styles of duplicated instance\", () => {\n  const project = createStub(\n    <$.Body>\n      <$.Box\n        ws:id=\"boxId\"\n        ws:style={css`\n          color: red;\n        `}\n      ></$.Box>\n    </$.Body>\n  );\n  const fragment = extractWebstudioFragment(project, \"boxId\");\n  insertWebstudioFragmentCopy({\n    data: project,\n    fragment,\n    availableVariables: [],\n    projectId: \"\",\n  });\n  const newInstanceId = Array.from(project.instances.keys()).at(-1);\n  expect(toCss(project)).toEqual(\n    stripIndent(`\n      @media all {\n        boxId:local {\n          color: red\n        }\n        ${newInstanceId}:local {\n          color: red\n        }\n      }\n    `).trim()\n  );\n  // modify original style\n  insertStyles({\n    data: project,\n    breakpointId: \"base\",\n    styleSourceId: project.styleSourceSelections.get(\"boxId\")?.values[0] ?? \"\",\n    style: css`\n      font-size: medium;\n    `,\n  });\n  expect(toCss(project)).toEqual(\n    stripIndent(`\n      @media all {\n        boxId:local {\n          color: red;\n          font-size: medium\n        }\n        ${newInstanceId}:local {\n          color: red\n        }\n      }\n    `).trim()\n  );\n});\n\ndescribe(\"props\", () => {\n  test(\"extract all props bound to fragment instances\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" data-body=\"\">\n        <$.Box ws:id=\"boxId\" data-box=\"\">\n          <$.Text ws:id=\"textId\" data-text=\"\"></$.Text>\n        </$.Box>\n      </$.Body>\n    );\n    const { props } = extractWebstudioFragment(data, \"boxId\");\n    expect(props).toEqual([\n      expect.objectContaining({ name: \"data-box\" }),\n      expect.objectContaining({ name: \"data-text\" }),\n    ]);\n  });\n\n  test(\"insert props with new ids\", () => {\n    const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    const fragment = renderTemplate(\n      <$.Box ws:id=\"boxId\" data-box=\"\">\n        <$.Text ws:id=\"textId\" data-text=\"\"></$.Text>\n      </$.Box>\n    );\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.props.values())).toEqual([\n      expect.objectContaining({\n        id: expect.toSatisfy((value) => value !== fragment.props[0].id),\n        name: \"data-box\",\n      }),\n      expect.objectContaining({\n        id: expect.toSatisfy((value) => value !== fragment.props[1].id),\n        name: \"data-text\",\n      }),\n    ]);\n  });\n\n  test(\"preserve ids when insert props from slots\", () => {\n    const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    const fragment = renderTemplate(\n      <$.Slot>\n        <$.Fragment>\n          <$.Box ws:id=\"boxId\" data-box=\"\">\n            <$.Text ws:id=\"textId\" data-text=\"\"></$.Text>\n          </$.Box>\n        </$.Fragment>\n      </$.Slot>\n    );\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.props.values())).toEqual([\n      expect.objectContaining({\n        id: fragment.props[0].id,\n        name: \"data-box\",\n      }),\n      expect.objectContaining({\n        id: fragment.props[1].id,\n        name: \"data-text\",\n      }),\n    ]);\n  });\n});\n\ndescribe(\"variables\", () => {\n  test(\"extract variable\", () => {\n    const boxVariable = new Variable(\"Box Variable\", \"\");\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    expect(fragment.dataSources).toEqual([\n      expect.objectContaining({ id: \"0\", type: \"variable\" }),\n    ]);\n    expect(fragment.props).toEqual([\n      expect.objectContaining({\n        instanceId: \"boxId\",\n        value: \"$ws$dataSource$0\",\n      }),\n    ]);\n  });\n\n  test(\"unset variable outside of scope\", () => {\n    const bodyVariable = new Variable(\"Body Variable\", \"\");\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Box\n          ws:id=\"boxId\"\n          vars={expression`${bodyVariable}`}\n          action={\n            new ActionValue([\"state\"], expression`${bodyVariable} = state`)\n          }\n        >\n          {expression`${bodyVariable}`}\n        </$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    expect(fragment.dataSources).toEqual([]);\n    expect(fragment.props).toEqual([\n      expect.objectContaining({\n        instanceId: \"boxId\",\n        value: \"Body$32$Variable\",\n      }),\n      expect.objectContaining({\n        instanceId: \"boxId\",\n        value: [\n          {\n            type: \"execute\",\n            args: [\"state\"],\n            code: \"Body$32$Variable = state\",\n          },\n        ],\n      }),\n    ]);\n    expect(fragment.instances).toEqual([\n      expect.objectContaining({\n        id: \"boxId\",\n        children: [{ type: \"expression\", value: \"Body$32$Variable\" }],\n      }),\n    ]);\n  });\n\n  test(\"insert variables with new ids\", () => {\n    const boxParameter = new Parameter(\"My Parameter\");\n    const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    const fragment = renderTemplate(\n      <$.Box\n        ws:id=\"boxId\"\n        vars={expression`${boxParameter}`}\n        action={new ActionValue([], expression`${boxParameter}`)}\n        parameter={boxParameter}\n      >\n        {expression`${boxParameter}`}\n      </$.Box>\n    );\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: [],\n      projectId: \"\",\n    });\n    const [newDataSourceId] = data.dataSources.keys();\n    expect(Array.from(data.dataSources.values())).toEqual([\n      expect.objectContaining({\n        id: expect.toSatisfy((value) => value !== fragment.dataSources[0].id),\n        name: \"My Parameter\",\n      }),\n    ]);\n    expect(Array.from(data.props.values())).toEqual([\n      expect.objectContaining({\n        name: \"vars\",\n        value: encodeDataVariableId(newDataSourceId),\n      }),\n      expect.objectContaining({\n        name: \"action\",\n        value: [\n          {\n            type: \"execute\",\n            args: [],\n            code: encodeDataVariableId(newDataSourceId),\n          },\n        ],\n      }),\n      expect.objectContaining({\n        name: \"parameter\",\n        value: newDataSourceId,\n      }),\n    ]);\n  });\n\n  test(\"preserve ids when insert variables from portals\", () => {\n    const boxParameter = new Parameter(\"My Parameter\");\n    const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    const fragment = renderTemplate(\n      <$.Slot>\n        <$.Fragment>\n          <$.Box\n            ws:id=\"boxId\"\n            vars={expression`${boxParameter}`}\n            action={new ActionValue([], expression`${boxParameter}`)}\n            parameter={boxParameter}\n          >\n            {expression`${boxParameter}`}\n          </$.Box>\n        </$.Fragment>\n      </$.Slot>\n    );\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.dataSources.values())).toEqual([\n      expect.objectContaining({\n        id: fragment.dataSources[0].id,\n        name: \"My Parameter\",\n      }),\n    ]);\n    expect(Array.from(data.props.values())).toEqual([\n      expect.objectContaining({\n        name: \"vars\",\n        value: encodeDataVariableId(fragment.dataSources[0].id),\n      }),\n      expect.objectContaining({\n        name: \"action\",\n        value: [\n          {\n            type: \"execute\",\n            args: [],\n            code: encodeDataVariableId(fragment.dataSources[0].id),\n          },\n        ],\n      }),\n      expect.objectContaining({\n        name: \"parameter\",\n        value: fragment.dataSources[0].id,\n      }),\n    ]);\n  });\n\n  test(\"restore unset variables when insert fragment\", () => {\n    const bodyVariable = new Variable(\"Body Variable\", \"\");\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Box\n          ws:id=\"boxId\"\n          vars={expression`${bodyVariable} + unknownVariable`}\n          action={\n            new ActionValue([\"state\"], expression`${bodyVariable} = state`)\n          }\n        >\n          {expression`${bodyVariable}`}\n        </$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: findAvailableVariables({\n        ...data,\n        startingInstanceId: \"bodyId\",\n      }),\n      projectId: \"\",\n    });\n    const newInstanceId = Array.from(data.instances.keys()).at(-1) ?? \"\";\n    expect(newInstanceId).not.toEqual(\"boxId\");\n    expect(data.instances.get(newInstanceId)?.children).toEqual([\n      { type: \"expression\", value: \"$ws$dataSource$0\" },\n    ]);\n    expect(\n      Array.from(data.props.values()).filter(\n        (item) => item.instanceId === newInstanceId\n      )\n    ).toEqual([\n      expect.objectContaining({\n        value: \"$ws$dataSource$0 + unknownVariable\",\n      }),\n      expect.objectContaining({\n        value: [expect.objectContaining({ code: \"$ws$dataSource$0 = state\" })],\n      }),\n    ]);\n  });\n});\n\ndescribe(\"resources\", () => {\n  test(\"extract resource variable with dependant variables\", () => {\n    const boxVariable = new Variable(\"Box Variable\", \"\");\n    const resourceVariable = new ResourceValue(\"Box Resource\", {\n      url: expression`${boxVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${boxVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${boxVariable}` }],\n      body: expression`${boxVariable}`,\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" vars={expression`${resourceVariable}`}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    expect(fragment.dataSources).toEqual([\n      expect.objectContaining({ id: \"1\", type: \"variable\" }),\n      expect.objectContaining({ id: \"0\", type: \"resource\" }),\n    ]);\n    expect(fragment.resources).toEqual([\n      expect.objectContaining({\n        url: \"$ws$dataSource$1\",\n        searchParams: [{ name: \"filter\", value: \"$ws$dataSource$1\" }],\n        headers: [{ name: \"auth\", value: \"$ws$dataSource$1\" }],\n        body: \"$ws$dataSource$1\",\n      }),\n    ]);\n  });\n\n  test(\"extract resource variable and unset variables outside of scope\", () => {\n    const bodyVariable = new Variable(\"Body Variable\", \"\");\n    const resourceVariable = new ResourceValue(\"Box Resource\", {\n      url: expression`${bodyVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n      body: expression`${bodyVariable}`,\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Box ws:id=\"boxId\" vars={expression`${resourceVariable}`}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    expect(fragment.dataSources).toEqual([\n      expect.objectContaining({ id: \"1\", type: \"resource\" }),\n    ]);\n    expect(fragment.resources).toEqual([\n      expect.objectContaining({\n        url: \"Body$32$Variable\",\n        searchParams: [{ name: \"filter\", value: \"Body$32$Variable\" }],\n        headers: [{ name: \"auth\", value: \"Body$32$Variable\" }],\n        body: \"Body$32$Variable\",\n      }),\n    ]);\n  });\n\n  test(\"restore unset variables in resource variable\", () => {\n    const bodyVariable = new Variable(\"Body Variable\", \"\");\n    const resourceVariable = new ResourceValue(\"Box Resource\", {\n      url: expression`${bodyVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n      body: expression`${bodyVariable}`,\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Box ws:id=\"boxId\" vars={expression`${resourceVariable}`}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: findAvailableVariables({\n        ...data,\n        startingInstanceId: \"bodyId\",\n      }),\n      projectId: \"\",\n    });\n    const newInstanceId = Array.from(data.instances.keys()).at(-1);\n    expect(newInstanceId).not.toEqual(\"boxId\");\n    expect(Array.from(data.resources.values())).toEqual([\n      expect.objectContaining({\n        url: \"$ws$dataSource$0\",\n        searchParams: [{ name: \"filter\", value: \"$ws$dataSource$0\" }],\n        headers: [{ name: \"auth\", value: \"$ws$dataSource$0\" }],\n        body: \"$ws$dataSource$0\",\n      }),\n      expect.objectContaining({\n        url: \"$ws$dataSource$0\",\n        searchParams: [{ name: \"filter\", value: \"$ws$dataSource$0\" }],\n        headers: [{ name: \"auth\", value: \"$ws$dataSource$0\" }],\n        body: \"$ws$dataSource$0\",\n      }),\n    ]);\n  });\n\n  test(\"extract resource prop with dependant variables\", () => {\n    const boxVariable = new Variable(\"Box Variable\", \"\");\n    const resourceProp = new ResourceValue(\"Box Resource\", {\n      url: expression`${boxVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${boxVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${boxVariable}` }],\n      body: expression`${boxVariable}`,\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" resource={resourceProp}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    expect(fragment.dataSources).toEqual([\n      expect.objectContaining({ id: \"1\", type: \"variable\" }),\n    ]);\n    expect(fragment.resources).toEqual([\n      expect.objectContaining({\n        url: \"$ws$dataSource$1\",\n        searchParams: [{ name: \"filter\", value: \"$ws$dataSource$1\" }],\n        headers: [{ name: \"auth\", value: \"$ws$dataSource$1\" }],\n        body: \"$ws$dataSource$1\",\n      }),\n    ]);\n  });\n\n  test(\"extract resource prop and unset variables outside of scope\", () => {\n    const bodyVariable = new Variable(\"Body Variable\", \"\");\n    const resourceProp = new ResourceValue(\"Box Resource\", {\n      url: expression`${bodyVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n      body: expression`${bodyVariable}`,\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Box ws:id=\"boxId\" resource={resourceProp}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    expect(fragment.dataSources).toEqual([]);\n    expect(fragment.resources).toEqual([\n      expect.objectContaining({\n        url: \"Body$32$Variable\",\n        searchParams: [{ name: \"filter\", value: \"Body$32$Variable\" }],\n        headers: [{ name: \"auth\", value: \"Body$32$Variable\" }],\n        body: \"Body$32$Variable\",\n      }),\n    ]);\n  });\n\n  test(\"restore unset variables in resource prop\", () => {\n    const bodyVariable = new Variable(\"Body Variable\", \"\");\n    const resourceProp = new ResourceValue(\"Box Resource\", {\n      url: expression`${bodyVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n      body: expression`${bodyVariable}`,\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Box ws:id=\"boxId\" resource={resourceProp}></$.Box>\n      </$.Body>\n    );\n    const fragment = extractWebstudioFragment(data, \"boxId\");\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: findAvailableVariables({\n        ...data,\n        startingInstanceId: \"bodyId\",\n      }),\n      projectId: \"\",\n    });\n    const newInstanceId = Array.from(data.instances.keys()).at(-1);\n    expect(newInstanceId).not.toEqual(\"boxId\");\n    expect(Array.from(data.resources.values())).toEqual([\n      expect.objectContaining({\n        url: \"$ws$dataSource$0\",\n        searchParams: [{ name: \"filter\", value: \"$ws$dataSource$0\" }],\n        headers: [{ name: \"auth\", value: \"$ws$dataSource$0\" }],\n        body: \"$ws$dataSource$0\",\n      }),\n      expect.objectContaining({\n        url: \"$ws$dataSource$0\",\n        searchParams: [{ name: \"filter\", value: \"$ws$dataSource$0\" }],\n        headers: [{ name: \"auth\", value: \"$ws$dataSource$0\" }],\n        body: \"$ws$dataSource$0\",\n      }),\n    ]);\n  });\n\n  test(\"insert resources with new ids\", () => {\n    const boxVariable = new Variable(\"Box Variable\", \"\");\n    const resourceProp = new ResourceValue(\"Box Resource\", {\n      url: expression`${boxVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${boxVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${boxVariable}` }],\n      body: expression`${boxVariable}`,\n    });\n    const resourceVariable = new ResourceValue(\"Box Resource\", {\n      url: expression`${boxVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${boxVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${boxVariable}` }],\n      body: expression`${boxVariable}`,\n    });\n    const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    const fragment = renderTemplate(\n      <$.Box\n        ws:id=\"boxId\"\n        action={resourceProp}\n        vars={expression`${resourceVariable}`}\n      ></$.Box>\n    );\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: [],\n      projectId: \"\",\n    });\n    const [newPropResourceId, newVariableResourceId] = data.resources.keys();\n    const [newBoxVariableId] = data.dataSources.keys();\n    const newVariableIdentifier = encodeDataVariableId(newBoxVariableId);\n    expect(Array.from(data.dataSources.values())).toEqual([\n      expect.objectContaining({\n        name: \"Box Variable\",\n      }),\n      expect.objectContaining({\n        name: \"Box Resource\",\n        resourceId: newVariableResourceId,\n      }),\n    ]);\n    expect(Array.from(data.resources.values())).toEqual([\n      expect.objectContaining({\n        id: expect.toSatisfy((value) => value !== fragment.resources[0].id),\n        url: newVariableIdentifier,\n        searchParams: [{ name: \"filter\", value: newVariableIdentifier }],\n        headers: [{ name: \"auth\", value: newVariableIdentifier }],\n        body: newVariableIdentifier,\n      }),\n      expect.objectContaining({\n        id: expect.toSatisfy((value) => value !== fragment.resources[1].id),\n        url: newVariableIdentifier,\n        searchParams: [{ name: \"filter\", value: newVariableIdentifier }],\n        headers: [{ name: \"auth\", value: newVariableIdentifier }],\n        body: newVariableIdentifier,\n      }),\n    ]);\n    expect(Array.from(data.props.values())).toEqual([\n      expect.objectContaining({\n        name: \"action\",\n        value: newPropResourceId,\n      }),\n      expect.objectContaining({ name: \"vars\" }),\n    ]);\n  });\n\n  test(\"preserve ids when insert resource from slot\", () => {\n    const boxVariable = new Variable(\"Box Variable\", \"\");\n    const resourceProp = new ResourceValue(\"Box Resource\", {\n      url: expression`${boxVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${boxVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${boxVariable}` }],\n      body: expression`${boxVariable}`,\n    });\n    const resourceVariable = new ResourceValue(\"Box Resource\", {\n      url: expression`${boxVariable}`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: expression`${boxVariable}` }],\n      headers: [{ name: \"auth\", value: expression`${boxVariable}` }],\n      body: expression`${boxVariable}`,\n    });\n    const data = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    const fragment = renderTemplate(\n      <$.Slot ws:id=\"slotId\">\n        <$.Fragment ws:id=\"fragmentId\">\n          <$.Box\n            ws:id=\"boxId\"\n            action={resourceProp}\n            vars={expression`${resourceVariable}`}\n          ></$.Box>\n        </$.Fragment>\n      </$.Slot>\n    );\n    insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.dataSources.values())).toEqual([\n      expect.objectContaining({\n        name: \"Box Variable\",\n      }),\n      expect.objectContaining({\n        name: \"Box Resource\",\n        resourceId: fragment.resources[1].id,\n      }),\n    ]);\n    const oldVariableIdentifier = encodeDataVariableId(\n      fragment.dataSources[0].id\n    );\n    expect(Array.from(data.resources.values())).toEqual([\n      expect.objectContaining({\n        id: fragment.resources[0].id,\n        url: oldVariableIdentifier,\n        searchParams: [{ name: \"filter\", value: oldVariableIdentifier }],\n        headers: [{ name: \"auth\", value: oldVariableIdentifier }],\n        body: oldVariableIdentifier,\n      }),\n      expect.objectContaining({\n        id: fragment.resources[1].id,\n        url: oldVariableIdentifier,\n        searchParams: [{ name: \"filter\", value: oldVariableIdentifier }],\n        headers: [{ name: \"auth\", value: oldVariableIdentifier }],\n        body: oldVariableIdentifier,\n      }),\n    ]);\n    expect(Array.from(data.props.values())).toEqual([\n      expect.objectContaining({\n        name: \"action\",\n        value: fragment.resources[0].id,\n      }),\n      expect.objectContaining({ name: \"vars\" }),\n    ]);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/copy-to-clipboard.stories.tsx",
    "content": "import { Button, Flex, StorySection } from \"@webstudio-is/design-system\";\nimport { CopyToClipboard as CopyToClipboardComponent } from \"./copy-to-clipboard\";\n\nexport default {\n  title: \"Copy To Clipboard\",\n  component: CopyToClipboardComponent,\n};\n\nexport const CopyToClipboard = () => (\n  <StorySection title=\"Copy To Clipboard\">\n    <Flex gap=\"3\">\n      <CopyToClipboardComponent text=\"Hello, world!\">\n        <Button>Click to copy</Button>\n      </CopyToClipboardComponent>\n      <CopyToClipboardComponent\n        text=\"some-secret-token-123\"\n        copyText=\"Copy token\"\n        copiedText=\"Token copied!\"\n      >\n        <Button>Copy token</Button>\n      </CopyToClipboardComponent>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/shared/copy-to-clipboard.tsx",
    "content": "import { Tooltip } from \"@webstudio-is/design-system\";\nimport { useState } from \"react\";\n\nexport const CopyToClipboard = ({\n  text,\n  copyText = \"Copy to clipboard\",\n  copiedText = \"Copied\",\n  children,\n}: {\n  text: string;\n  copyText?: React.ReactNode;\n  copiedText?: React.ReactNode;\n  children: React.ReactNode;\n}) => {\n  const [isCopied, setIsCopied] = useState(false);\n\n  return (\n    <div\n      style={{ display: \"contents\" }}\n      onClick={(event) => {\n        event.preventDefault();\n        navigator.clipboard.writeText(text);\n        setIsCopied(true);\n      }}\n    >\n      <Tooltip\n        // Tooltip sometimes receives onOpenChange with isOpen=false immediately after a click.\n        // Changing the key seems like a workaround to address this issue.\n        key={isCopied ? \"copied\" : \"copy\"}\n        disableHoverableContent\n        variant=\"wrapped\"\n        content={isCopied ? copiedText : copyText}\n        open={isCopied === true ? true : undefined}\n        onOpenChange={(isOpen) => {\n          if (isOpen === false) {\n            setIsCopied(false);\n          }\n        }}\n      >\n        {children}\n      </Tooltip>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/csrf.client.ts",
    "content": "export let csrfToken: string | undefined = undefined;\n\nexport const updateCsrfToken = (token: string) => {\n  csrfToken = token;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/data-variables.test.tsx",
    "content": "import { expect, test, vi } from \"vitest\";\nimport {\n  $,\n  ActionValue,\n  expression,\n  Parameter,\n  renderData,\n  ResourceValue,\n  Variable,\n  ws,\n} from \"@webstudio-is/template\";\nimport {\n  encodeDataVariableId,\n  ROOT_INSTANCE_ID,\n  SYSTEM_VARIABLE_ID,\n} from \"@webstudio-is/sdk\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport {\n  computeExpression,\n  decodeDataVariableName,\n  encodeDataVariableName,\n  findUnsetVariableNames,\n  restoreExpressionVariables,\n  rebindTreeVariablesMutable,\n  unsetExpressionVariables,\n  deleteVariableMutable,\n  findAvailableVariables,\n  findVariableUsagesByInstance,\n} from \"./data-variables\";\n\ntest(\"encode data variable name when necessary\", () => {\n  expect(encodeDataVariableName(\"formState\")).toEqual(\"formState\");\n  expect(encodeDataVariableName(\"Collection Item\")).toEqual(\n    \"Collection$32$Item\"\n  );\n  expect(encodeDataVariableName(\"$my$Variable\")).toEqual(\"$36$my$36$Variable\");\n});\n\ntest(\"dencode data variable name\", () => {\n  expect(decodeDataVariableName(encodeDataVariableName(\"formState\"))).toEqual(\n    \"formState\"\n  );\n  expect(\n    decodeDataVariableName(encodeDataVariableName(\"Collection Item\"))\n  ).toEqual(\"Collection Item\");\n});\n\ntest(\"dencode data variable name with dollar sign\", () => {\n  expect(\n    decodeDataVariableName(encodeDataVariableName(\"$my$Variable\"))\n  ).toEqual(\"$my$Variable\");\n  expect(decodeDataVariableName(\"$my$Variable\")).toEqual(\"$my$Variable\");\n});\n\ntest(\"find available variables\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const boxVariable = new Variable(\"boxVariable\", \"\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n    </$.Body>\n  );\n  expect(\n    findAvailableVariables({ ...data, startingInstanceId: \"boxId\" })\n  ).toEqual([\n    expect.objectContaining({ name: \"system\", id: SYSTEM_VARIABLE_ID }),\n    expect.objectContaining({ name: \"bodyVariable\" }),\n    expect.objectContaining({ name: \"boxVariable\" }),\n  ]);\n});\n\ntest(\"find masked variables\", () => {\n  const bodyVariable = new Variable(\"myVariable\", \"\");\n  const boxVariable = new Variable(\"myVariable\", \"\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n    </$.Body>\n  );\n  expect(\n    findAvailableVariables({ ...data, startingInstanceId: \"boxId\" })\n  ).toEqual([\n    expect.objectContaining({ name: \"system\", id: SYSTEM_VARIABLE_ID }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\", name: \"myVariable\" }),\n  ]);\n});\n\ntest(\"find global variables\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const boxVariable = new Variable(\"boxVariable\", \"\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  expect(\n    findAvailableVariables({ ...data, startingInstanceId: \"boxId\" })\n  ).toEqual([\n    expect.objectContaining({ name: \"system\", id: SYSTEM_VARIABLE_ID }),\n    expect.objectContaining({ name: \"globalVariable\" }),\n    expect.objectContaining({ name: \"boxVariable\" }),\n  ]);\n});\n\ntest(\"find global variables in slots\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const boxVariable = new Variable(\"boxVariable\", \"\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Slot ws:id=\"slotId\">\n          <$.Fragment ws:id=\"fragmentId\">\n            <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n          </$.Fragment>\n        </$.Slot>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  expect(\n    findAvailableVariables({ ...data, startingInstanceId: \"boxId\" })\n  ).toEqual([\n    expect.objectContaining({ name: \"system\", id: SYSTEM_VARIABLE_ID }),\n    expect.objectContaining({ name: \"globalVariable\" }),\n    expect.objectContaining({ name: \"boxVariable\" }),\n  ]);\n});\n\ntest(\"unset expression variables\", () => {\n  expect(\n    unsetExpressionVariables({\n      expression: `$ws$dataSource$myId + arbitaryVariable`,\n      unsetNameById: new Map([[\"myId\", \"My Variable\"]]),\n    })\n  ).toEqual(\"My$32$Variable + arbitaryVariable\");\n});\n\ntest(\"ignore not existing variables in expressions\", () => {\n  expect(\n    unsetExpressionVariables({\n      expression: `$ws$dataSource$myId + arbitaryVariable`,\n      unsetNameById: new Map(),\n    })\n  ).toEqual(\"$ws$dataSource$myId + arbitaryVariable\");\n});\n\ntest(\"restore expression variables\", () => {\n  expect(\n    restoreExpressionVariables({\n      expression: `My$32$Variable + missingVariable`,\n      maskedIdByName: new Map([[\"My Variable\", \"myId\"]]),\n    })\n  ).toEqual(\"$ws$dataSource$myId + missingVariable\");\n});\n\ntest(\"compute expression with decoded ids\", () => {\n  expect(\n    computeExpression(\"$ws$dataSource$myId\", new Map([[\"myId\", \"value\"]]))\n  ).toEqual(\"value\");\n});\n\ntest(\"compute expression with decoded names\", () => {\n  expect(\n    computeExpression(\"My$32$Name\", new Map([[\"My Name\", \"value\"]]))\n  ).toEqual(\"value\");\n});\n\ntest(\"compute expression when invalid syntax\", () => {\n  // prevent error message in test report\n  const spy = vi.spyOn(console, \"error\");\n  spy.mockImplementationOnce(() => {});\n  expect(computeExpression(\"https://github.com\", new Map())).toEqual(undefined);\n  expect(spy).toHaveBeenCalledOnce();\n  spy.mockRestore();\n});\n\ntest(\"compute expression with nested field of undefined without error\", () => {\n  const spy = vi.spyOn(console, \"error\");\n  const variables = new Map([[\"myVariable\", undefined]]);\n  expect(computeExpression(\"myVariable.field\", variables)).toEqual(undefined);\n  expect(spy).not.toHaveBeenCalled();\n  spy.mockRestore();\n});\n\ntest(\"compute literal expression when variable is json object\", () => {\n  const jsonObject = { hello: \"world\", subObject: { world: \"hello\" } };\n  const variables = new Map([[\"jsonVariable\", jsonObject]]);\n  expect(computeExpression(\"`${jsonVariable}`\", variables)).toEqual(\n    `{\"hello\":\"world\",\"subObject\":{\"world\":\"hello\"}}`\n  );\n  expect(computeExpression(\"`${jsonVariable.subObject}`\", variables)).toEqual(\n    `{\"world\":\"hello\"}`\n  );\n});\n\ntest(\"compute literal expression when object is frozen\", () => {\n  const jsonObject = Object.freeze({\n    hello: \"world\",\n    subObject: { world: \"hello\" },\n  });\n  const variables = new Map([[\"jsonVariable\", jsonObject]]);\n  expect(computeExpression(\"`${jsonVariable.subObject}`\", variables)).toEqual(\n    `{\"world\":\"hello\"}`\n  );\n});\n\ntest(\"compute unset variables as undefined\", () => {\n  expect(computeExpression(`a`, new Map())).toEqual(undefined);\n  expect(computeExpression(\"`${a}`\", new Map())).toEqual(\"undefined\");\n});\n\ntest(\"find unset variable names\", () => {\n  const resourceVariable = new ResourceValue(\"resourceVariable\", {\n    url: expression`six`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`seven` }],\n    headers: [{ name: \"auth\", value: expression`eight` }],\n    body: expression`nine`,\n  });\n  const resourceProp = new ResourceValue(\"resourceProp\", {\n    url: expression`ten`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`eleven` }],\n    headers: [{ name: \"auth\", value: expression`twelve` }],\n    body: expression`thirteen`,\n  });\n  const data = renderData(\n    <$.Body ws:id=\"body\" data-prop={expression`two`}>\n      <$.Box ws:id=\"box\" data-prop={expression`three`}>\n        <$.Text\n          ws:id=\"text\"\n          data-variables={expression`${resourceVariable}`}\n          data-resource={resourceProp}\n          data-action={new ActionValue([\"five\"], expression`four + five`)}\n        >{expression`one`}</$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  expect(\n    findUnsetVariableNames({ startingInstanceId: \"body\", ...data })\n  ).toEqual([\n    \"one\",\n    \"two\",\n    \"three\",\n    \"four\",\n    \"six\",\n    \"eight\",\n    \"seven\",\n    \"nine\",\n    \"ten\",\n    \"twelve\",\n    \"eleven\",\n    \"thirteen\",\n  ]);\n});\n\ntest(\"restore tree variables in children\", () => {\n  const bodyVariable = new Variable(\"one\", \"one value of body\");\n  const boxVariable = new Variable(\"one\", \"one value of box\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" data-vars={expression`${boxVariable}`}>\n        <$.Text ws:id=\"textId\">{expression`one`}</$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"boxId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n  ]);\n  const [_bodyVariableId, boxVariableId] = data.dataSources.keys();\n  const boxIdentifier = encodeDataVariableId(boxVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: boxIdentifier },\n  ]);\n});\n\ntest(\"restore tree variables in props\", () => {\n  const oneBody = new Variable(\"one\", \"one value of body\");\n  const oneBox = new Variable(\"one\", \"one value of box\");\n  const twoBox = new Variable(\"two\", \"two value of box\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${oneBody}`}>\n      <$.Box\n        ws:id=\"boxId\"\n        data-box-vars={expression`${oneBox} ${twoBox}`}\n        data-one={expression`one`}\n        data-action={new ActionValue([\"one\"], expression`one + two + three`)}\n      >\n        <$.Text ws:id=\"text\" data-two={expression`one + two + three`}></$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"boxId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  const [_bodyVariableId, boxOneVariableId, boxTwoVariableId] =\n    data.dataSources.keys();\n  const boxOneIdentifier = encodeDataVariableId(boxOneVariableId);\n  const boxTwoIdentifier = encodeDataVariableId(boxTwoVariableId);\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ name: \"one\", scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ name: \"one\", scopeInstanceId: \"boxId\" }),\n    expect.objectContaining({ name: \"two\", scopeInstanceId: \"boxId\" }),\n  ]);\n  expect(Array.from(data.props.values())).toEqual([\n    expect.objectContaining({ name: \"data-body-vars\" }),\n    expect.objectContaining({ name: \"data-box-vars\" }),\n    expect.objectContaining({ name: \"data-one\", value: boxOneIdentifier }),\n    expect.objectContaining({\n      name: \"data-action\",\n      value: [\n        {\n          type: \"execute\",\n          args: [\"one\"],\n          code: `one + ${boxTwoIdentifier} + three`,\n        },\n      ],\n    }),\n    expect.objectContaining({\n      name: \"data-two\",\n      value: `${boxOneIdentifier} + ${boxTwoIdentifier} + three`,\n    }),\n  ]);\n});\n\ntest(\"rebind tree variables in props and children\", () => {\n  const bodyVariable = new Variable(\"one\", \"one value of body\");\n  const boxVariable = new Variable(\"one\", \"one value of box\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" data-box-vars={expression`${boxVariable}`}>\n        <$.Text\n          ws:id=\"textId\"\n          data-text-vars={expression`${bodyVariable}`}\n          data-action={new ActionValue([], expression`${bodyVariable}`)}\n        >\n          {expression`${bodyVariable}`}\n        </$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"boxId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n  ]);\n  const [_bodyVariableId, boxVariableId] = data.dataSources.keys();\n  const boxIdentifier = encodeDataVariableId(boxVariableId);\n  expect(Array.from(data.props.values())).toEqual([\n    expect.objectContaining({ name: \"data-body-vars\" }),\n    expect.objectContaining({ name: \"data-box-vars\", value: boxIdentifier }),\n    expect.objectContaining({ name: \"data-text-vars\", value: boxIdentifier }),\n    expect.objectContaining({\n      name: \"data-action\",\n      value: [{ type: \"execute\", args: [], code: boxIdentifier }],\n    }),\n  ]);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: boxIdentifier },\n  ]);\n});\n\ntest(\"preserve nested variables with the same name when rebind\", () => {\n  const bodyVariable = new Variable(\"one\", \"one value of body\");\n  const textVariable = new Variable(\"one\", \"one value of box\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Text ws:id=\"textId\" data-text-vars={expression`${textVariable}`}>\n        {expression`${textVariable}`}\n      </$.Text>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"bodyId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"textId\" }),\n  ]);\n  const [_bodyVariableId, textVariableId] = data.dataSources.keys();\n  const textIdentifier = encodeDataVariableId(textVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: textIdentifier },\n  ]);\n});\n\ntest(\"restore tree variables in resources\", () => {\n  const bodyVariable = new Variable(\"one\", \"one value of body\");\n  const boxVariable = new Variable(\"one\", \"one value of box\");\n  const resourceVariable = new ResourceValue(\"resourceVariable\", {\n    url: expression`one + 1`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`one + 1` }],\n    headers: [{ name: \"auth\", value: expression`one + 1` }],\n    body: expression`one + 1`,\n  });\n  const resourceProp = new ResourceValue(\"resourceProp\", {\n    url: expression`one + 2`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`one + 2` }],\n    headers: [{ name: \"auth\", value: expression`one + 2` }],\n    body: expression`one + 2`,\n  });\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" data-vars={expression`${boxVariable}`}>\n        <$.Text\n          ws:id=\"text\"\n          data-vars={expression`${resourceVariable}`}\n          data-resource={resourceProp}\n        ></$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"boxId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n    expect.objectContaining({ type: \"resource\" }),\n  ]);\n  const [_bodyVariableId, boxVariableId] = data.dataSources.keys();\n  const boxIdentifier = encodeDataVariableId(boxVariableId);\n  expect(Array.from(data.resources.values())).toEqual([\n    expect.objectContaining({\n      url: `${boxIdentifier} + 1`,\n      method: \"post\",\n      searchParams: [{ name: \"filter\", value: `${boxIdentifier} + 1` }],\n      headers: [{ name: \"auth\", value: `${boxIdentifier} + 1` }],\n      body: `${boxIdentifier} + 1`,\n    }),\n    expect.objectContaining({\n      url: `${boxIdentifier} + 2`,\n      method: \"post\",\n      searchParams: [{ name: \"filter\", value: `${boxIdentifier} + 2` }],\n      headers: [{ name: \"auth\", value: `${boxIdentifier} + 2` }],\n      body: `${boxIdentifier} + 2`,\n    }),\n  ]);\n});\n\ntest(\"rebind tree variables in resources\", () => {\n  const bodyVariable = new Variable(\"one\", \"one value of body\");\n  const boxVariable = new Variable(\"one\", \"one value of box\");\n  const resourceVariable = new ResourceValue(\"resourceVariable\", {\n    url: expression`${bodyVariable}`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n    headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n    body: expression`${bodyVariable}`,\n  });\n  const resourceProp = new ResourceValue(\"resourceProp\", {\n    url: expression`${bodyVariable}`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n    headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n    body: expression`${bodyVariable}`,\n  });\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" data-box-vars={expression`${boxVariable}`}>\n        <$.Text\n          ws:id=\"textId\"\n          data-text-vars={expression`${resourceVariable}`}\n          data-action={resourceProp}\n        ></$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"boxId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n    expect.objectContaining({ type: \"resource\" }),\n  ]);\n  const [_bodyVariableId, boxVariableId] = data.dataSources.keys();\n  const boxIdentifier = encodeDataVariableId(boxVariableId);\n  expect(Array.from(data.resources.values())).toEqual([\n    expect.objectContaining({\n      url: boxIdentifier,\n      method: \"post\",\n      searchParams: [{ name: \"filter\", value: boxIdentifier }],\n      headers: [{ name: \"auth\", value: boxIdentifier }],\n      body: boxIdentifier,\n    }),\n    expect.objectContaining({\n      url: boxIdentifier,\n      method: \"post\",\n      searchParams: [{ name: \"filter\", value: boxIdentifier }],\n      headers: [{ name: \"auth\", value: boxIdentifier }],\n      body: boxIdentifier,\n    }),\n  ]);\n});\n\ntest(\"rebind global variables in resources\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} data-vars={expression`${globalVariable}`}>\n      <$.Body ws:id=\"bodyId\">\n        <$.Text ws:id=\"textId\">{expression`globalVariable`}</$.Text>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  rebindTreeVariablesMutable({\n    startingInstanceId: ROOT_INSTANCE_ID,\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: ROOT_INSTANCE_ID }),\n  ]);\n  const [globalVariableId] = data.dataSources.keys();\n  const globalIdentifier = encodeDataVariableId(globalVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: globalIdentifier },\n  ]);\n});\n\ntest(\"preserve other variables when rebind\", () => {\n  const bodyVariable = new Variable(\"globalVariable\", \"\");\n  const textVariable = new Variable(\"textVariable\", \"\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-vars={expression`${bodyVariable}`}>\n      <$.Text ws:id=\"textId\">{expression`${textVariable}`}</$.Text>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"bodyId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"textId\" }),\n  ]);\n  const [_globalVariableId, textVariableId] = data.dataSources.keys();\n  const textIdentifier = encodeDataVariableId(textVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: textIdentifier },\n  ]);\n});\n\ntest(\"prevent rebinding tree variables from slots\", () => {\n  const bodyVariable = new Variable(\"myVariable\", \"one value of body\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Slot ws:id=\"slotId\">\n        <$.Fragment ws:id=\"fragmentId\">\n          <$.Box ws:id=\"boxId\">{expression`myVariable`}</$.Box>\n        </$.Fragment>\n      </$.Slot>\n    </$.Body>\n  );\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"boxId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(data.instances.get(\"boxId\")?.children).toEqual([\n    { type: \"expression\", value: \"myVariable\" },\n  ]);\n});\n\ntest(\"prevent rebinding with nested collection item\", () => {\n  const collectionItem = new Parameter(\"collectionITem\");\n  const nestedCollectionItem = new Parameter(\"collectionITem\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <ws.collection ws:id=\"collectionId\" data={[]} item={collectionItem}>\n        <ws.collection\n          ws:id=\"nestedCollectionId\"\n          data={expression`${collectionItem}`}\n          item={nestedCollectionItem}\n        >\n          <ws.element ws:id=\"divId\" ws:tag=\"div\">\n            {expression`${nestedCollectionItem}`}\n          </ws.element>\n        </ws.collection>\n      </ws.collection>\n    </$.Body>\n  );\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"collectionId\" }),\n    expect.objectContaining({ scopeInstanceId: \"nestedCollectionId\" }),\n  ]);\n  const [collectionItemId, nestedCollectionItemId] = data.dataSources.keys();\n  rebindTreeVariablesMutable({\n    startingInstanceId: \"bodyId\",\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...data,\n  });\n  expect(Array.from(data.props.values())).toEqual([\n    expect.objectContaining({\n      instanceId: \"collectionId\",\n      name: \"data\",\n    }),\n    expect.objectContaining({\n      instanceId: \"collectionId\",\n      name: \"item\",\n      value: collectionItemId,\n    }),\n    expect.objectContaining({\n      instanceId: \"nestedCollectionId\",\n      name: \"data\",\n      value: encodeDataVariableId(collectionItemId),\n    }),\n    expect.objectContaining({\n      instanceId: \"nestedCollectionId\",\n      name: \"item\",\n      value: nestedCollectionItemId,\n    }),\n  ]);\n  expect(data.instances.get(\"divId\")?.children).toEqual([\n    { type: \"expression\", value: encodeDataVariableId(nestedCollectionItemId) },\n  ]);\n});\n\ntest(\"delete variable and unset it in expressions\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"one value of body\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Box\n        ws:id=\"boxId\"\n        data-action={new ActionValue([], expression`${bodyVariable}`)}\n      >\n        {expression`${bodyVariable}`}\n      </$.Box>\n    </$.Body>\n  );\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n  ]);\n  const [bodyVariableId] = data.dataSources.keys();\n  deleteVariableMutable(data, bodyVariableId);\n  expect(Array.from(data.props.values())).toEqual([\n    expect.objectContaining({ name: \"data-body-vars\", value: \"bodyVariable\" }),\n    expect.objectContaining({\n      name: \"data-action\",\n      value: [{ type: \"execute\", args: [], code: \"bodyVariable\" }],\n    }),\n  ]);\n  expect(data.instances.get(\"boxId\")?.children).toEqual([\n    { type: \"expression\", value: \"bodyVariable\" },\n  ]);\n});\n\ntest(\"delete variable and unset it in resources\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"one value of body\");\n  const resourceVariable = new ResourceValue(\"resourceVariable\", {\n    url: expression`${bodyVariable}`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n    headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n    body: expression`${bodyVariable}`,\n  });\n  const resourceProp = new ResourceValue(\"resourceProp\", {\n    url: expression`${bodyVariable}`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`${bodyVariable}` }],\n    headers: [{ name: \"auth\", value: expression`${bodyVariable}` }],\n    body: expression`${bodyVariable}`,\n  });\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Box\n        ws:id=\"boxId\"\n        data-box-vars={expression`${resourceVariable}`}\n        data-resource={resourceProp}\n      ></$.Box>\n    </$.Body>\n  );\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n  ]);\n  const [bodyVariableId] = data.dataSources.keys();\n  deleteVariableMutable(data, bodyVariableId);\n  expect(Array.from(data.resources.values())).toEqual([\n    expect.objectContaining({\n      url: \"bodyVariable\",\n      method: \"post\",\n      searchParams: [{ name: \"filter\", value: \"bodyVariable\" }],\n      headers: [{ name: \"auth\", value: \"bodyVariable\" }],\n      body: \"bodyVariable\",\n    }),\n    expect.objectContaining({\n      url: \"bodyVariable\",\n      method: \"post\",\n      searchParams: [{ name: \"filter\", value: \"bodyVariable\" }],\n      headers: [{ name: \"auth\", value: \"bodyVariable\" }],\n      body: \"bodyVariable\",\n    }),\n  ]);\n});\n\ntest(\"rebind expressions with parent variable when delete variable on child\", () => {\n  const bodyVariable = new Variable(\"myVariable\", \"one value of body\");\n  const boxVariable = new Variable(\"myVariable\", \"one value of body\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" data-box-vars={expression`${boxVariable}`}>\n        <$.Text ws:id=\"textId\">{expression`${boxVariable}`}</$.Text>\n      </$.Box>\n    </$.Body>\n  );\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n  ]);\n  const [bodyVariableId, boxVariableId] = data.dataSources.keys();\n  deleteVariableMutable(data, boxVariableId);\n  const bodyIdentifier = encodeDataVariableId(bodyVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: bodyIdentifier },\n  ]);\n});\n\ntest(\"prevent rebinding with variables outside of slot content scope\", () => {\n  const bodyVariable = new Variable(\"myVariable\", \"one value of body\");\n  const boxVariable = new Variable(\"myVariable\", \"one value of body\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-body-vars={expression`${bodyVariable}`}>\n      <$.Slot>\n        <$.Fragment>\n          <$.Box ws:id=\"boxId\" data-box-vars={expression`${boxVariable}`}>\n            <$.Text ws:id=\"textId\">{expression`${boxVariable}`}</$.Text>\n          </$.Box>\n        </$.Fragment>\n      </$.Slot>\n    </$.Body>\n  );\n  expect(Array.from(data.dataSources.values())).toEqual([\n    expect.objectContaining({ scopeInstanceId: \"bodyId\" }),\n    expect.objectContaining({ scopeInstanceId: \"boxId\" }),\n  ]);\n  const [_bodyVariableId, boxVariableId] = data.dataSources.keys();\n  deleteVariableMutable(data, boxVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: \"myVariable\" },\n  ]);\n});\n\ntest(\"unset global variables on all pages when delete\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const pages = createDefaultPages({ rootInstanceId: \"homeBodyId\" });\n  pages.pages.push({\n    id: \"\",\n    name: \"\",\n    path: \"\",\n    title: \"\",\n    meta: {},\n    rootInstanceId: \"aboutBodyId\",\n  });\n  const data = {\n    pages,\n    ...renderData(\n      <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n        <$.Body ws:id=\"homeBodyId\">\n          <$.Text ws:id=\"homeTextId\">{expression`${globalVariable}`}</$.Text>\n        </$.Body>\n        <$.Body ws:id=\"aboutBodyId\">\n          <$.Text ws:id=\"aboutTextId\">{expression`${globalVariable}`}</$.Text>\n        </$.Body>\n      </ws.root>\n    ),\n  };\n  data.instances.delete(ROOT_INSTANCE_ID);\n  expect(data.dataSources.size).toEqual(1);\n  const [globalVariableId] = data.dataSources.keys();\n  deleteVariableMutable(data, globalVariableId);\n  expect(data.instances.get(\"homeTextId\")?.children).toEqual([\n    { type: \"expression\", value: \"globalVariable\" },\n  ]);\n  expect(data.instances.get(\"aboutTextId\")?.children).toEqual([\n    { type: \"expression\", value: \"globalVariable\" },\n  ]);\n});\n\ntest(\"unset global variables in slots when delete\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...renderData(\n      <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n        <$.Body ws:id=\"bodyId\">\n          <$.Slot ws:id=\"slotId\">\n            <$.Fragment ws:id=\"fragmentId\">\n              <$.Text ws:id=\"textId\">{expression`${globalVariable}`}</$.Text>\n            </$.Fragment>\n          </$.Slot>\n        </$.Body>\n      </ws.root>\n    ),\n  };\n  data.instances.delete(ROOT_INSTANCE_ID);\n  expect(data.dataSources.size).toEqual(1);\n  const [globalVariableId] = data.dataSources.keys();\n  deleteVariableMutable(data, globalVariableId);\n  expect(data.instances.get(\"textId\")?.children).toEqual([\n    { type: \"expression\", value: \"globalVariable\" },\n  ]);\n});\n\ntest(\"unset body variables in page meta when delete\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}></$.Body>\n    ),\n  };\n  expect(data.dataSources.size).toEqual(1);\n  const [bodyVariableId] = data.dataSources.keys();\n  const bodyIdentifier = encodeDataVariableId(bodyVariableId);\n  data.pages.homePage.title = bodyIdentifier;\n  data.pages.homePage.meta = {\n    description: bodyIdentifier,\n    excludePageFromSearch: bodyIdentifier,\n    socialImageUrl: bodyIdentifier,\n    language: bodyIdentifier,\n    status: bodyIdentifier,\n    redirect: bodyIdentifier,\n    custom: [{ property: \"auth\", content: bodyIdentifier }],\n  };\n  deleteVariableMutable(data, bodyVariableId);\n  expect(data.pages.homePage.title).toEqual(`bodyVariable`);\n  expect(data.pages.homePage.meta.description).toEqual(`bodyVariable`);\n  expect(data.pages.homePage.meta.excludePageFromSearch).toEqual(\n    `bodyVariable`\n  );\n  expect(data.pages.homePage.meta.socialImageUrl).toEqual(`bodyVariable`);\n  expect(data.pages.homePage.meta.language).toEqual(`bodyVariable`);\n  expect(data.pages.homePage.meta.status).toEqual(`bodyVariable`);\n  expect(data.pages.homePage.meta.redirect).toEqual(`bodyVariable`);\n  expect(data.pages.homePage.meta.custom?.[0].content).toEqual(`bodyVariable`);\n});\n\ntest(\"unset global variables in all pages meta when delete\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"homeBodyId\" }),\n    ...renderData(\n      <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n        <$.Body ws:id=\"homeBodyId\"></$.Body>\n        <$.Body ws:id=\"aboutBodyId\"></$.Body>\n      </ws.root>\n    ),\n  };\n  data.instances.delete(ROOT_INSTANCE_ID);\n  data.pages.pages.push({\n    id: \"\",\n    name: \"\",\n    path: \"\",\n    title: \"\",\n    meta: {},\n    rootInstanceId: \"aboutBodyId\",\n  });\n  expect(data.dataSources.size).toEqual(1);\n  const [globalVariableId] = data.dataSources.keys();\n  const globalIdentifier = encodeDataVariableId(globalVariableId);\n  for (const page of [data.pages.homePage, ...data.pages.pages]) {\n    page.title = globalIdentifier;\n    page.meta = {\n      description: globalIdentifier,\n      excludePageFromSearch: globalIdentifier,\n      socialImageUrl: globalIdentifier,\n      language: globalIdentifier,\n      status: globalIdentifier,\n      redirect: globalIdentifier,\n      custom: [{ property: \"auth\", content: globalIdentifier }],\n    };\n  }\n  deleteVariableMutable(data, globalVariableId);\n  for (const page of [data.pages.homePage, ...data.pages.pages]) {\n    expect(page.title).toEqual(`globalVariable`);\n    expect(page.meta.description).toEqual(`globalVariable`);\n    expect(page.meta.excludePageFromSearch).toEqual(`globalVariable`);\n    expect(page.meta.socialImageUrl).toEqual(`globalVariable`);\n    expect(page.meta.language).toEqual(`globalVariable`);\n    expect(page.meta.status).toEqual(`globalVariable`);\n    expect(page.meta.redirect).toEqual(`globalVariable`);\n    expect(page.meta.custom?.[0].content).toEqual(`globalVariable`);\n  }\n});\n\ntest(\"find variable usages by instance\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const boxVariable = new Variable(\"boxVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...renderData(\n      <$.Body\n        ws:id=\"bodyId\"\n        vars={expression`${bodyVariable}`}\n        data-prop={expression`${bodyVariable} + \"test\"`}\n      >\n        <$.Box\n          ws:id=\"boxId\"\n          vars={expression`${boxVariable}`}\n          data-prop={expression`${boxVariable} + ${bodyVariable}`}\n        >\n          <$.Text ws:id=\"textId\">{expression`${bodyVariable}`}</$.Text>\n        </$.Box>\n      </$.Body>\n    ),\n  };\n  const bodyVariableId = [...data.dataSources.values()].find(\n    (ds) => ds.name === \"bodyVariable\"\n  )?.id;\n  const boxVariableId = [...data.dataSources.values()].find(\n    (ds) => ds.name === \"boxVariable\"\n  )?.id;\n\n  const usagesByInstance = findVariableUsagesByInstance({\n    ...data,\n    startingInstanceId: \"bodyId\",\n  });\n\n  expect(usagesByInstance.get(bodyVariableId!)).toEqual(\n    new Set([\"bodyId\", \"boxId\", \"textId\"])\n  );\n  expect(usagesByInstance.get(boxVariableId!)).toEqual(new Set([\"boxId\"]));\n});\n\ntest(\"find variable usages from root includes all pages\", () => {\n  const globalVariable = new Variable(\"globalVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"homeBodyId\" }),\n    ...renderData(\n      <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n        <$.Body ws:id=\"homeBodyId\">\n          <$.Box ws:id=\"homeBoxId\" data-prop={expression`${globalVariable}`} />\n        </$.Body>\n        <$.Body ws:id=\"aboutBodyId\">\n          <$.Box ws:id=\"aboutBoxId\" data-prop={expression`${globalVariable}`} />\n        </$.Body>\n      </ws.root>\n    ),\n  };\n  data.instances.delete(ROOT_INSTANCE_ID);\n  data.pages.pages.push({\n    id: \"aboutPage\",\n    name: \"About\",\n    path: \"/about\",\n    title: \"\",\n    meta: {},\n    rootInstanceId: \"aboutBodyId\",\n  });\n\n  const [globalVariableId] = data.dataSources.keys();\n\n  const usagesByInstance = findVariableUsagesByInstance({\n    ...data,\n    startingInstanceId: ROOT_INSTANCE_ID,\n  });\n\n  // Global variable is used in expressions on both boxes and the root (where it's defined)\n  expect(usagesByInstance.get(globalVariableId)).toEqual(\n    new Set([ROOT_INSTANCE_ID, \"homeBoxId\", \"aboutBoxId\"])\n  );\n});\n\ntest(\"find variable usages in page meta\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}></$.Body>\n    ),\n  };\n  const [bodyVariableId] = data.dataSources.keys();\n  const bodyIdentifier = encodeDataVariableId(bodyVariableId);\n\n  data.pages.homePage.title = bodyIdentifier;\n  data.pages.homePage.meta = {\n    description: bodyIdentifier,\n    excludePageFromSearch: bodyIdentifier,\n    socialImageUrl: bodyIdentifier,\n  };\n\n  const usagesByInstance = findVariableUsagesByInstance({\n    ...data,\n    startingInstanceId: \"bodyId\",\n  });\n\n  // Page meta expressions are attributed to the page's root instance\n  expect(usagesByInstance.get(bodyVariableId)).toEqual(new Set([\"bodyId\"]));\n});\n\ntest(\"find variable usages counts unique instances not expressions\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const data = {\n    pages: createDefaultPages({ rootInstanceId: \"bodyId\" }),\n    ...renderData(\n      <$.Body\n        ws:id=\"bodyId\"\n        vars={expression`${bodyVariable}`}\n        data-prop1={expression`${bodyVariable}`}\n        data-prop2={expression`${bodyVariable}`}\n        data-prop3={expression`${bodyVariable}`}\n      ></$.Body>\n    ),\n  };\n  const [bodyVariableId] = data.dataSources.keys();\n\n  const usagesByInstance = findVariableUsagesByInstance({\n    ...data,\n    startingInstanceId: \"bodyId\",\n  });\n\n  // Even though bodyVariable is used 4 times (in 4 different expressions),\n  // it should only appear once in the set since it's all on the same instance\n  expect(usagesByInstance.get(bodyVariableId)).toEqual(new Set([\"bodyId\"]));\n  expect(usagesByInstance.get(bodyVariableId)?.size).toBe(1);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/data-variables.ts",
    "content": "import {\n  type DataSource,\n  type DataSources,\n  type Instance,\n  type Instances,\n  type Props,\n  type Resource,\n  type Resources,\n  type WebstudioData,\n  Pages,\n  ROOT_INSTANCE_ID,\n  SYSTEM_VARIABLE_ID,\n  collectionComponent,\n  decodeDataVariableId,\n  encodeDataVariableId,\n  findTreeInstanceIds,\n  findTreeInstanceIdsExcludingSlotDescendants,\n  getExpressionIdentifiers,\n  systemParameter,\n  transpileExpression,\n} from \"@webstudio-is/sdk\";\nimport {\n  createJsonStringifyProxy,\n  isPlainObject,\n} from \"@webstudio-is/sdk/runtime\";\nimport { setUnion } from \"./shim\";\n\nconst allowedJsChars = /[A-Za-z_]/;\n\n/**\n * variable names can contain any characters and\n * this utility encodes data variable name into valid js identifier\n * for example\n * \"Collection Item\" -> \"Collection$20$Item\"\n */\nexport const encodeDataVariableName = (name: string) => {\n  let encodedName = \"\";\n  for (let index = 0; index < name.length; index += 1) {\n    const char = name[index];\n    encodedName += allowedJsChars.test(char)\n      ? char\n      : `$${char.codePointAt(0)}$`;\n  }\n  return encodedName;\n};\n\n/**\n * Variable name should be restorable from encoded js identifier\n */\nexport const decodeDataVariableName = (identifier: string) => {\n  const name = identifier.replaceAll(/\\$(\\d+)\\$/g, (_match, code) =>\n    String.fromCodePoint(code)\n  );\n  return name;\n};\n\n/**\n * replace all encoded ids with encoded names\n * to make expression transferrable\n */\nexport const unsetExpressionVariables = ({\n  expression,\n  unsetNameById,\n}: {\n  expression: string;\n  unsetNameById: Map<DataSource[\"id\"], DataSource[\"name\"]>;\n}) => {\n  try {\n    return transpileExpression({\n      expression,\n      replaceVariable: (identifier) => {\n        const id = decodeDataVariableId(identifier);\n        if (id) {\n          const name = unsetNameById.get(id);\n          if (name) {\n            return encodeDataVariableName(name);\n          }\n        }\n        return identifier;\n      },\n    });\n  } catch {\n    return expression;\n  }\n};\n\n/**\n * restore variable ids by js identifiers\n */\nexport const restoreExpressionVariables = ({\n  expression,\n  maskedIdByName,\n}: {\n  expression: string;\n  maskedIdByName: Map<DataSource[\"name\"], DataSource[\"id\"]>;\n}) => {\n  try {\n    return transpileExpression({\n      expression,\n      replaceVariable: (identifier) => {\n        const name = decodeDataVariableName(identifier);\n        if (name) {\n          const id = maskedIdByName.get(name);\n          if (id) {\n            return encodeDataVariableId(id);\n          }\n        }\n        return identifier;\n      },\n    });\n  } catch {\n    return expression;\n  }\n};\n\nexport const computeExpression = (\n  expression: string,\n  variables: Map<DataSource[\"name\"], unknown>\n) => {\n  try {\n    const usedVariables = new Map();\n    const transpiled = transpileExpression({\n      expression,\n      executable: true,\n      replaceVariable: (identifier) => {\n        const id = decodeDataVariableId(identifier);\n        if (id) {\n          usedVariables.set(identifier, id);\n        } else {\n          // access all variable values from specified map\n          const name = decodeDataVariableName(identifier);\n          usedVariables.set(identifier, name);\n        }\n      },\n    });\n    let code = \"\";\n    // add only used variables in expression and get values\n    // from variables map without additional serializing of these values\n    for (const [identifier, name] of usedVariables) {\n      code += `let ${identifier} = _variables.get(${JSON.stringify(name)});\\n`;\n    }\n    code += `return (${transpiled})`;\n\n    /**\n     *\n     * We are using structuredClone on frozen values because, for some reason,\n     * the Proxy example below throws a cryptic error:\n     * TypeError: 'get' on proxy: property 'data' is a read-only and non-configurable\n     * data property on the proxy target, but the proxy did not return its actual value\n     * (expected '[object Array]' but got '[object Array]').\n     *\n     * ```\n     * const createJsonStringifyProxy = (target) => {\n     *   return new Proxy(target, {\n     *     get(target, prop, receiver) {\n     *\n     *       console.log((prop in target), prop)\n     *\n     *       const value = Reflect.get(target, prop, receiver);\n     *\n     *       if (typeof value === \"object\" && value !== null) {\n     *         return createJsonStringifyProxy(value);\n     *       }\n     *\n     *       return value;\n     *     },\n     *   });\n     * };\n     * const obj = Object.freeze({ data: [1, 2, 3, 4] });\n     * const proxy = createJsonStringifyProxy(obj)\n     * proxy.data\n     *\n     * ```\n     */\n    const proxiedVariables = new Map(\n      [...variables.entries()].map(([key, value]) => [\n        key,\n        isPlainObject(value)\n          ? createJsonStringifyProxy(\n              Object.isFrozen(value) ? structuredClone(value) : value\n            )\n          : value,\n      ])\n    );\n\n    const result = new Function(\"_variables\", code)(proxiedVariables);\n    return result;\n  } catch (error) {\n    console.error(error);\n  }\n};\n\nconst getParentInstanceById = (instances: Instances) => {\n  const parentInstanceById = new Map<Instance[\"id\"], Instance[\"id\"]>();\n  for (const instance of instances.values()) {\n    // interrupt lookup because slot variables cannot be passed to slot content\n    if (instance.component === \"Slot\") {\n      continue;\n    }\n    for (const child of instance.children) {\n      if (child.type === \"id\") {\n        parentInstanceById.set(child.value, instance.id);\n      }\n    }\n  }\n  return parentInstanceById;\n};\n\nconst findMaskedVariablesByInstanceId = ({\n  startingInstanceId,\n  parentInstanceById,\n  instances,\n  dataSources,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  parentInstanceById: Map<Instance[\"id\"], Instance[\"id\"]>;\n  instances: Instances;\n  dataSources: DataSources;\n}) => {\n  let currentId: undefined | string = startingInstanceId;\n  const instanceIdsPath: Instance[\"id\"][] = [];\n  while (currentId) {\n    instanceIdsPath.push(currentId);\n    currentId = parentInstanceById.get(currentId);\n  }\n  // allow accessing global variables everywhere\n  instanceIdsPath.push(ROOT_INSTANCE_ID);\n  const maskedVariables = new Map<DataSource[\"name\"], DataSource[\"id\"]>();\n  // global system variable always present\n  maskedVariables.set(\"system\", SYSTEM_VARIABLE_ID);\n  // start from the root to descendant\n  // so child variables override parent variables\n  for (const instanceId of instanceIdsPath.reverse()) {\n    const instance = instances.get(instanceId);\n    for (const dataSource of dataSources.values()) {\n      if (dataSource.scopeInstanceId === instanceId) {\n        // when current instance is collection\n        // ignore its collection item parameter\n        // when rebind variables\n        if (\n          instanceId === startingInstanceId &&\n          instance?.component === collectionComponent &&\n          dataSource.type === \"parameter\"\n        ) {\n          continue;\n        }\n        maskedVariables.set(dataSource.name, dataSource.id);\n      }\n    }\n  }\n  return maskedVariables;\n};\n\nexport const findAvailableVariables = ({\n  startingInstanceId,\n  instances,\n  dataSources,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  instances: Instances;\n  dataSources: DataSources;\n}) => {\n  const maskedVariables = findMaskedVariablesByInstanceId({\n    startingInstanceId,\n    parentInstanceById: getParentInstanceById(instances),\n    instances,\n    dataSources,\n  });\n  const availableVariables: DataSource[] = [];\n  for (const dataSourceId of maskedVariables.values()) {\n    const dataSource = dataSources.get(dataSourceId);\n    if (dataSource) {\n      availableVariables.push(dataSource);\n    }\n    if (dataSourceId === SYSTEM_VARIABLE_ID) {\n      availableVariables.push(systemParameter);\n    }\n  }\n  return availableVariables;\n};\n\nconst traverseExpressions = ({\n  startingInstanceId,\n  pages,\n  instances,\n  props,\n  dataSources,\n  resources,\n  update,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  pages: undefined | Pages;\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  resources: Resources;\n  update: (\n    expression: string,\n    instanceId: Instance[\"id\"],\n    args?: string[]\n  ) => void | string;\n}) => {\n  const pagesList = pages ? [pages.homePage, ...pages.pages] : [];\n\n  let instanceIds = findTreeInstanceIdsExcludingSlotDescendants(\n    instances,\n    startingInstanceId\n  );\n  for (const page of pagesList) {\n    // global variables can be accessed on all pages and inside of slots\n    if (startingInstanceId === ROOT_INSTANCE_ID) {\n      instanceIds = setUnion(\n        instanceIds,\n        findTreeInstanceIds(instances, page.rootInstanceId)\n      );\n    }\n\n    // global and body variables can be accessed in pages meta\n    if (\n      startingInstanceId === page.rootInstanceId ||\n      startingInstanceId === ROOT_INSTANCE_ID\n    ) {\n      const { rootInstanceId } = page;\n      page.title = update(page.title, rootInstanceId) ?? page.title;\n      if (page.meta.description) {\n        page.meta.description =\n          update(page.meta.description, rootInstanceId) ??\n          page.meta.description;\n      }\n      if (page.meta.excludePageFromSearch) {\n        page.meta.excludePageFromSearch =\n          update(page.meta.excludePageFromSearch, rootInstanceId) ??\n          page.meta.excludePageFromSearch;\n      }\n      if (page.meta.socialImageUrl) {\n        page.meta.socialImageUrl =\n          update(page.meta.socialImageUrl, rootInstanceId) ??\n          page.meta.socialImageUrl;\n      }\n      if (page.meta.language) {\n        page.meta.language =\n          update(page.meta.language, rootInstanceId) ?? page.meta.language;\n      }\n      if (page.meta.status) {\n        page.meta.status =\n          update(page.meta.status, rootInstanceId) ?? page.meta.status;\n      }\n      if (page.meta.redirect) {\n        page.meta.redirect =\n          update(page.meta.redirect, rootInstanceId) ?? page.meta.redirect;\n      }\n      if (page.meta.custom) {\n        for (const item of page.meta.custom) {\n          item.content = update(item.content, rootInstanceId) ?? item.content;\n        }\n      }\n    }\n  }\n  const instanceIdByResourceId = new Map<Resource[\"id\"], Instance[\"id\"]>();\n\n  for (const instance of instances.values()) {\n    if (instanceIds.has(instance.id) === false) {\n      continue;\n    }\n    for (const child of instance.children) {\n      if (child.type === \"expression\") {\n        child.value = update(child.value, instance.id) ?? child.value;\n      }\n    }\n  }\n\n  for (const prop of props.values()) {\n    if (instanceIds.has(prop.instanceId) === false) {\n      continue;\n    }\n    if (prop.type === \"expression\") {\n      prop.value = update(prop.value, prop.instanceId) ?? prop.value;\n      continue;\n    }\n    if (prop.type === \"action\") {\n      for (const action of prop.value) {\n        action.code =\n          update(action.code, prop.instanceId, action.args) ?? action.code;\n      }\n      continue;\n    }\n    if (prop.type === \"resource\") {\n      instanceIdByResourceId.set(prop.value, prop.instanceId);\n      continue;\n    }\n  }\n\n  for (const dataSource of dataSources.values()) {\n    const instanceId = dataSource.scopeInstanceId ?? \"\";\n    if (instanceIds.has(instanceId) && dataSource.type === \"resource\") {\n      instanceIdByResourceId.set(dataSource.resourceId, instanceId);\n    }\n  }\n\n  for (const resource of resources.values()) {\n    const instanceId = instanceIdByResourceId.get(resource.id);\n    if (instanceId === undefined) {\n      continue;\n    }\n    resource.url = update(resource.url, instanceId) ?? resource.url;\n    for (const header of resource.headers) {\n      header.value = update(header.value, instanceId) ?? header.value;\n    }\n    if (resource.searchParams) {\n      for (const searchParam of resource.searchParams) {\n        searchParam.value =\n          update(searchParam.value, instanceId) ?? searchParam.value;\n      }\n    }\n    if (resource.body) {\n      resource.body = update(resource.body, instanceId) ?? resource.body;\n    }\n  }\n};\n\nexport const findUnsetVariableNames = ({\n  startingInstanceId,\n  instances,\n  props,\n  dataSources,\n  resources,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  resources: Resources;\n}) => {\n  const unsetVariables = new Set<DataSource[\"name\"]>();\n  traverseExpressions({\n    startingInstanceId,\n    pages: undefined,\n    instances,\n    props,\n    dataSources,\n    resources,\n    update: (expression, _instanceId, args = []) => {\n      transpileExpression({\n        expression,\n        replaceVariable: (identifier) => {\n          const id = decodeDataVariableId(identifier);\n          if (id === undefined && args.includes(identifier) === false) {\n            unsetVariables.add(decodeDataVariableName(identifier));\n          }\n        },\n      });\n    },\n  });\n  return Array.from(unsetVariables);\n};\n\nexport const findUsedVariables = ({\n  startingInstanceId,\n  pages,\n  instances,\n  props,\n  dataSources,\n  resources,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  pages: undefined | Pages;\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  resources: Resources;\n}) => {\n  const usedVariables = new Map<DataSource[\"id\"], number>();\n  traverseExpressions({\n    startingInstanceId,\n    pages,\n    instances,\n    props,\n    dataSources,\n    resources,\n    update: (expression) => {\n      const identifiers = getExpressionIdentifiers(expression);\n      for (const identifier of identifiers) {\n        const id = decodeDataVariableId(identifier);\n        if (id !== undefined) {\n          const count = usedVariables.get(id) ?? 0;\n          usedVariables.set(id, count + 1);\n        }\n      }\n    },\n  });\n  return usedVariables;\n};\n\nexport const findVariableUsagesByInstance = ({\n  startingInstanceId,\n  pages,\n  instances,\n  props,\n  dataSources,\n  resources,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  pages: undefined | Pages;\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  resources: Resources;\n}) => {\n  const usedIn = new Map<DataSource[\"id\"], Set<Instance[\"id\"]>>();\n  traverseExpressions({\n    startingInstanceId,\n    pages,\n    instances,\n    props,\n    dataSources,\n    resources,\n    update: (expression, instanceId) => {\n      const identifiers = getExpressionIdentifiers(expression);\n      for (const identifier of identifiers) {\n        const id = decodeDataVariableId(identifier);\n        if (id !== undefined) {\n          if (!usedIn.has(id)) {\n            usedIn.set(id, new Set());\n          }\n          usedIn.get(id)?.add(instanceId);\n        }\n      }\n    },\n  });\n  return usedIn;\n};\n\nexport const rebindTreeVariablesMutable = ({\n  startingInstanceId,\n  pages,\n  instances,\n  props,\n  dataSources,\n  resources,\n}: {\n  startingInstanceId: Instance[\"id\"];\n  pages: undefined | Pages;\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  resources: Resources;\n}) => {\n  // unset all variables\n  const unsetNameById = new Map<DataSource[\"id\"], DataSource[\"name\"]>();\n  for (const dataSource of dataSources.values()) {\n    unsetNameById.set(dataSource.id, dataSource.name);\n  }\n  // precompute parent instances outside of traverse\n  const parentInstanceById = getParentInstanceById(instances);\n  traverseExpressions({\n    startingInstanceId,\n    pages,\n    instances,\n    props,\n    dataSources,\n    resources,\n    update: (expression, instanceId, args) => {\n      // restore all masked variables of current scope\n      const maskedVariables = findMaskedVariablesByInstanceId({\n        startingInstanceId: instanceId,\n        parentInstanceById,\n        instances,\n        dataSources,\n      });\n      let maskedIdByName = new Map(maskedVariables);\n      if (args) {\n        maskedIdByName = new Map(maskedIdByName);\n        for (const arg of args) {\n          maskedIdByName.delete(arg);\n        }\n      }\n      expression = unsetExpressionVariables({\n        expression,\n        unsetNameById,\n      });\n      expression = restoreExpressionVariables({\n        expression,\n        maskedIdByName,\n      });\n      return expression;\n    },\n  });\n};\n\nexport const deleteVariableMutable = (\n  data: Omit<WebstudioData, \"pages\"> & { pages?: Pages },\n  variableId: DataSource[\"id\"]\n) => {\n  const dataSource = data.dataSources.get(variableId);\n  if (dataSource === undefined) {\n    return;\n  }\n  data.dataSources.delete(variableId);\n  if (dataSource.type === \"resource\") {\n    data.resources.delete(dataSource.resourceId);\n  }\n  const unsetNameById = new Map<DataSource[\"id\"], DataSource[\"name\"]>();\n  unsetNameById.set(dataSource.id, dataSource.name);\n  const startingInstanceId = dataSource.scopeInstanceId ?? \"\";\n  const maskedIdByName = findMaskedVariablesByInstanceId({\n    startingInstanceId,\n    parentInstanceById: getParentInstanceById(data.instances),\n    instances: data.instances,\n    dataSources: data.dataSources,\n  });\n  // unset deleted variable in expressions\n  traverseExpressions({\n    ...data,\n    pages: data.pages,\n    startingInstanceId,\n    update: (expression) => {\n      expression = unsetExpressionVariables({\n        expression,\n        unsetNameById,\n      });\n      expression = restoreExpressionVariables({\n        expression,\n        maskedIdByName,\n      });\n      return expression;\n    },\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/db/canvas.server.ts",
    "content": "import type { Data } from \"@webstudio-is/http-client\";\nimport { loadBuildById } from \"@webstudio-is/project-build/index.server\";\nimport { loadAssetsByProject } from \"@webstudio-is/asset-uploader/index.server\";\nimport type { AppContext } from \"@webstudio-is/trpc-interface/index.server\";\nimport { findPageByIdOrPath, getStyleDeclKey } from \"@webstudio-is/sdk\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\n\nconst getPair = <Item extends { id: string }>(item: Item): [string, Item] => [\n  item.id,\n  item,\n];\n\nexport const loadProductionCanvasData = async (\n  buildId: string,\n  context: AppContext\n): Promise<Data> => {\n  const build = await loadBuildById(context, buildId);\n\n  if (build === undefined) {\n    throw new Error(\"The project is not published\");\n  }\n\n  const { deployment } = build;\n\n  if (deployment === undefined) {\n    throw new Error(\"The project is not published\");\n  }\n\n  const project = await projectApi.loadById(build.projectId, context);\n\n  const currentProjectDomains = project.domainsVirtual;\n\n  // Check that build deployment domains are still active and verified\n  // for examle: redeploy created few days later\n  if (deployment.destination !== \"static\") {\n    deployment.domains = deployment.domains.filter(\n      (domain) =>\n        project.domain === domain ||\n        currentProjectDomains.some(\n          (projectDomain) =>\n            projectDomain.domain === domain &&\n            projectDomain.status === \"ACTIVE\" &&\n            projectDomain.verified\n        )\n    );\n  }\n\n  const page = findPageByIdOrPath(\"/\", build.pages);\n\n  if (page === undefined) {\n    throw new Error(`Page / not found`);\n  }\n\n  const allAssets = await loadAssetsByProject(build.projectId, context);\n\n  // Find all fonts referenced in styles\n  const fontFamilySet = new Set<string>();\n  for (const { value } of build.styles) {\n    if (value.type === \"fontFamily\") {\n      for (const fontFamily of value.value) {\n        fontFamilySet.add(fontFamily);\n      }\n    }\n  }\n\n  // Filter unused font assets but include all other asset types (images, videos, audio, documents)\n  const assets = allAssets.filter(\n    (asset) =>\n      asset.type === \"image\" ||\n      asset.type === \"file\" ||\n      (asset.type === \"font\" && fontFamilySet.has(asset.meta.family))\n  );\n\n  return {\n    build: {\n      id: build.id,\n      projectId: build.projectId,\n      version: build.version,\n      createdAt: build.createdAt,\n      updatedAt: build.updatedAt,\n      pages: build.pages,\n      breakpoints: build.breakpoints.map(getPair),\n      styles: build.styles.map((item) => [getStyleDeclKey(item), item]),\n      styleSources: build.styleSources.map(getPair),\n      styleSourceSelections: build.styleSourceSelections.map((item) => [\n        item.instanceId,\n        item,\n      ]),\n      props: build.props.map(getPair),\n      dataSources: build.dataSources.map(getPair),\n      resources: build.resources.map(getPair),\n      instances: build.instances.map(getPair),\n      deployment,\n    },\n    page,\n    pages: [build.pages.homePage, ...build.pages.pages],\n    assets,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/db/index.ts",
    "content": "export * as user from \"./user.server\";\n// @todo make the export consistent with the rest\nexport * from \"./canvas.server\";\n"
  },
  {
    "path": "apps/builder/app/shared/db/user-plan-features.server.ts",
    "content": "import type { AppContext } from \"@webstudio-is/trpc-interface/index.server\";\nimport env from \"~/env/env.server\";\n\nexport type UserPlanFeatures = NonNullable<AppContext[\"userPlanFeatures\"]>;\n\nexport const getUserPlanFeatures = async (\n  userId: string,\n  postgrest: AppContext[\"postgrest\"]\n): Promise<UserPlanFeatures> => {\n  const userProductsResult = await postgrest.client\n    .from(\"UserProduct\")\n    .select(\"customerId, subscriptionId, productId\")\n    .eq(\"userId\", userId);\n\n  if (userProductsResult.error) {\n    console.error(userProductsResult.error);\n    throw new Error(\"Failed to fetch user products\");\n  }\n\n  const userProducts = userProductsResult.data;\n\n  const productsResult = await postgrest.client\n    .from(\"Product\")\n    .select(\"id, name, meta\")\n    .in(\n      \"id\",\n      userProducts.map(({ productId }) => productId ?? \"\")\n    );\n\n  if (productsResult.error) {\n    console.error(productsResult.error);\n    throw new Error(\"Failed to fetch products\");\n  }\n\n  const products = productsResult.data;\n\n  // Create a map of productId -> product name for quick lookup\n  const productIdToName = new Map<string, string>();\n  for (const product of products) {\n    productIdToName.set(product.id, product.name);\n  }\n\n  // Build purchases array - includes all products (subscriptions and LTDs)\n  // subscriptionId only set for recurring subscriptions (manageable in Stripe)\n  const purchases: Array<{\n    planName: string;\n    subscriptionId?: string;\n  }> = [];\n  for (const userProduct of userProducts) {\n    if (userProduct.productId) {\n      const planName = productIdToName.get(userProduct.productId) ?? \"Pro\";\n      purchases.push({\n        planName,\n        subscriptionId: userProduct.subscriptionId ?? undefined,\n      });\n    }\n  }\n\n  if (userProducts.length > 0) {\n    const productMetas = products.map((product) => {\n      return {\n        allowAdditionalPermissions: true,\n        allowDynamicData: true,\n        allowContentMode: true,\n        allowStagingPublish: true,\n        maxContactEmails: 5,\n        maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER,\n        maxPublishesAllowedPerUser: Number.MAX_SAFE_INTEGER,\n        ...(product.meta as Partial<UserPlanFeatures>),\n      };\n    });\n    return {\n      allowAdditionalPermissions: productMetas.some(\n        (item) => item.allowAdditionalPermissions\n      ),\n      allowDynamicData: productMetas.some((item) => item.allowDynamicData),\n      allowContentMode: productMetas.some((item) => item.allowContentMode),\n      allowStagingPublish: productMetas.some(\n        (item) => item.allowStagingPublish\n      ),\n      maxContactEmails: Math.max(\n        ...productMetas.map((item) => item.maxContactEmails)\n      ),\n      maxDomainsAllowedPerUser: Math.max(\n        ...productMetas.map((item) => item.maxDomainsAllowedPerUser)\n      ),\n      maxPublishesAllowedPerUser: Math.max(\n        ...productMetas.map((item) => item.maxPublishesAllowedPerUser)\n      ),\n      purchases,\n    };\n  }\n\n  if (env.USER_PLAN === \"pro\") {\n    return {\n      allowAdditionalPermissions: true,\n      allowDynamicData: true,\n      allowContentMode: true,\n      allowStagingPublish: true,\n      maxContactEmails: 5,\n      maxDomainsAllowedPerUser: Number.MAX_SAFE_INTEGER,\n      maxPublishesAllowedPerUser: Number.MAX_SAFE_INTEGER,\n      purchases: [{ planName: \"env.USER_PLAN Pro\" }],\n    };\n  }\n\n  return {\n    allowAdditionalPermissions: false,\n    allowDynamicData: false,\n    allowContentMode: false,\n    allowStagingPublish: false,\n    maxContactEmails: 0,\n    maxDomainsAllowedPerUser: 0,\n    maxPublishesAllowedPerUser: 10,\n    purchases: [],\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/db/user.server.ts",
    "content": "import type { Database } from \"@webstudio-is/postgrest/index.server\";\nimport {\n  AuthorizationError,\n  type AppContext,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport type { GitHubProfile } from \"remix-auth-github\";\nimport type { GoogleProfile } from \"remix-auth-google\";\nimport { z } from \"zod\";\n\nexport type User = Omit<\n  Database[\"public\"][\"Tables\"][\"User\"][\"Row\"],\n  \"projectsTags\"\n> & {\n  projectsTags: Array<ProjectTag>;\n};\n\nconst formatUser = (user: Database[\"public\"][\"Tables\"][\"User\"][\"Row\"]) => {\n  return {\n    ...user,\n    projectsTags: (user.projectsTags || []) as User[\"projectsTags\"],\n  };\n};\n\nexport const getUserById = async (context: AppContext, id: User[\"id\"]) => {\n  const dbUser = await context.postgrest.client\n    .from(\"User\")\n    .select()\n    .eq(\"id\", id)\n    .single();\n\n  if (dbUser.error) {\n    console.error(dbUser.error);\n    throw new Error(\"User not found\");\n  }\n\n  return formatUser(dbUser.data);\n};\n\nconst genericCreateAccount = async (\n  context: AppContext,\n  userData: {\n    email: string;\n    username: string;\n    image: string;\n    provider: string;\n  }\n): Promise<User> => {\n  const dbUser = await context.postgrest.client\n    .from(\"User\")\n    .select()\n    .eq(\"email\", userData.email)\n    .single();\n\n  if (dbUser.error == null) {\n    return formatUser(dbUser.data);\n  }\n\n  // https://github.com/PostgREST/postgrest/blob/bfbd033c6e9f38cfbc8b1cfe19ee009a9379e3dd/docs/references/errors.rst#L234\n  if (dbUser.error.code !== \"PGRST116\") {\n    console.error(dbUser.error);\n    throw new Error(\"User not found\");\n  }\n\n  const newUser = await context.postgrest.client\n    .from(\"User\")\n    .insert({\n      id: crypto.randomUUID(),\n      ...userData,\n    })\n    .select()\n    .single();\n\n  if (newUser.error) {\n    console.error(newUser.error);\n    throw new Error(\"Failed to create user\");\n  }\n\n  return formatUser(newUser.data);\n};\n\nexport const createOrLoginWithOAuth = async (\n  context: AppContext,\n  profile: GoogleProfile | GitHubProfile\n): Promise<User> => {\n  const userData = {\n    email: (profile.emails ?? [])[0]?.value,\n    username: profile.displayName,\n    image: (profile.photos ?? [])[0]?.value,\n    provider: profile.provider,\n  };\n  const newUser = await genericCreateAccount(context, userData);\n  return newUser;\n};\n\nexport const createOrLoginWithDev = async (\n  context: AppContext,\n  email: string\n): Promise<User> => {\n  const userData = {\n    email,\n    username: \"admin\",\n    image: \"\",\n    provider: \"dev\",\n  };\n\n  const newUser = await genericCreateAccount(context, userData);\n  return newUser;\n};\n\nexport const userProjectTagSchema = z.object({\n  id: z.string(),\n  label: z.string().min(1).max(100),\n});\n\nexport type ProjectTag = z.infer<typeof userProjectTagSchema>;\n\nexport const updateUserProjectsTags = async (\n  { tags }: { tags: ProjectTag[] },\n  context: AppContext\n) => {\n  if (context.authorization.type !== \"user\") {\n    throw new AuthorizationError(\n      \"Only logged in users can update project tags\"\n    );\n  }\n  const result = await context.postgrest.client\n    .from(\"User\")\n    .update({ projectsTags: tags })\n    .eq(\"id\", context.authorization.userId)\n    .select()\n    .single();\n\n  if (result.error) {\n    throw result.error;\n  }\n  return result.data.projectsTags as ProjectTag[];\n};\n"
  },
  {
    "path": "apps/builder/app/shared/debug-track.ts",
    "content": "import { shallowEqual } from \"shallow-equal\";\nimport warnOnce from \"warn-once\";\n\nconst trackers: Record<string, (args: Record<string, unknown>) => void> = {};\n\n/**\n * Debug-only: Tracks changes between consecutive calls.\n * Usage: `track('label')({ a, b, c })` logs changed keys on subsequent calls.\n */\nexport const trackChanges = (label: string) => {\n  if (process.env.NODE_ENV !== \"development\") {\n    warnOnce(true, \"track should not be used in production\");\n    return () => {};\n  }\n\n  if (!trackers[label]) {\n    let previousArgs: Record<string, unknown> | undefined;\n\n    trackers[label] = (currentArgs: Record<string, unknown>) => {\n      if (!previousArgs) {\n        previousArgs = currentArgs;\n        return;\n      }\n\n      if (!shallowEqual(previousArgs, currentArgs)) {\n        const changedKeys = Object.keys(currentArgs).filter(\n          (key) => previousArgs![key] !== currentArgs[key]\n        );\n        console.info(label, changedKeys);\n        previousArgs = currentArgs;\n      }\n    };\n  }\n\n  return trackers[label];\n};\n"
  },
  {
    "path": "apps/builder/app/shared/debug.ts",
    "content": "import createDebugRaw from \"debug\";\n\nconst getModuleName = (url: string) => {\n  const nameParts = url.split(\"/\").pop()?.split(\".\") ?? [];\n  if (nameParts?.length === 0) {\n    return \"unknown\";\n  }\n\n  const knownExtensions = [\"ts\", \"tsx\", \"js\", \"jsx\"];\n\n  if (knownExtensions.includes(nameParts[nameParts.length - 1])) {\n    nameParts.pop();\n  }\n\n  return nameParts.join(\".\");\n};\n\nexport const createDebug = (namespaceOrUrl: string) =>\n  createDebugRaw(\"ws\").extend(getModuleName(namespaceOrUrl));\n"
  },
  {
    "path": "apps/builder/app/shared/dom-hooks/index.ts",
    "content": "export * from \"./use-window-resize\";\nexport * from \"./use-content-editable\";\n"
  },
  {
    "path": "apps/builder/app/shared/dom-hooks/use-content-editable.ts",
    "content": "import {\n  useRef,\n  type FocusEvent,\n  type KeyboardEvent,\n  type KeyboardEventHandler,\n  useEffect,\n} from \"react\";\n\nexport const useContentEditable = ({\n  isEditable,\n  isEditing,\n  onChangeEditing,\n  onChangeValue,\n  value,\n}: {\n  isEditable: boolean;\n  isEditing: boolean;\n  onChangeEditing: (isEditing: boolean) => void;\n  onChangeValue: (value: string) => void;\n  value: string;\n}) => {\n  const elementRef = useRef<HTMLDivElement | null>(null);\n  const getValue = () => elementRef.current?.textContent ?? \"\";\n\n  useEffect(() => {\n    const element = elementRef.current;\n    if (element === null) {\n      return;\n    }\n\n    // Nothing changed, do nothing\n    if (element.hasAttribute(\"contenteditable\") === isEditing) {\n      return;\n    }\n\n    if (isEditing) {\n      element.setAttribute(\"contenteditable\", \"plaintext-only\");\n      // the next frame is necessary when newly created element\n      // need to get focus, for example after duplicate operation\n      requestAnimationFrame(() => {\n        element.focus();\n        getSelection()?.selectAllChildren(element);\n      });\n      return;\n    }\n\n    element.removeAttribute(\"contenteditable\");\n  }, [isEditing]);\n\n  const handleEnd = (event: KeyboardEvent<Element> | FocusEvent<Element>) => {\n    event.preventDefault();\n    if (isEditing) {\n      onChangeEditing(false);\n    }\n  };\n\n  const handleComplete = (\n    event: KeyboardEvent<Element> | FocusEvent<Element>\n  ) => {\n    event.preventDefault();\n    if (isEditing === false) {\n      return;\n    }\n    const nextValue = getValue();\n    handleEnd(event);\n    onChangeValue(nextValue);\n  };\n\n  const handleKeyDown: KeyboardEventHandler = (event) => {\n    if (isEditing === false) {\n      return;\n    }\n    // prevent keyboard navigation on parent elements\n    event.stopPropagation();\n    if (event.key === \"Enter\") {\n      handleComplete(event);\n    }\n    if (event.key === \"Escape\" && elementRef.current !== null) {\n      elementRef.current.textContent = value;\n      handleEnd(event);\n    }\n  };\n\n  const handleDoubleClick = () => {\n    if (isEditable) {\n      onChangeEditing(true);\n    }\n  };\n\n  const handlers = {\n    onKeyDown: handleKeyDown,\n    onBlur: handleComplete,\n    onDoubleClick: handleDoubleClick,\n  };\n\n  return { ref: elementRef, handlers };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/dom-hooks/use-window-resize.ts",
    "content": "import { useEffect } from \"react\";\nimport { createNanoEvents } from \"nanoevents\";\n\nconst emitter = createNanoEvents<{\n  resizeStart: () => void;\n  resize: () => void;\n  resizeEnd: () => void;\n}>();\n\nif (typeof window === \"object\") {\n  let timeoutId = 0;\n  let isResizing = false;\n  window.addEventListener(\n    \"resize\",\n    () => {\n      if (isResizing === false) {\n        emitter.emit(\"resizeStart\");\n      }\n      emitter.emit(\"resize\");\n      isResizing = true;\n      clearTimeout(timeoutId);\n      timeoutId = window.setTimeout(() => {\n        if (isResizing === false) {\n          return;\n        }\n        isResizing = false;\n        emitter.emit(\"resizeEnd\");\n      }, 150);\n    },\n    false\n  );\n}\n\nconst noop = () => {};\n\ntype UseWindowResize = {\n  onResizeStart?: () => void;\n  onResize?: () => void;\n  onResizeEnd?: () => void;\n};\n\nexport const subscribeWindowResize = ({\n  onResizeStart = noop,\n  onResize = noop,\n  onResizeEnd = noop,\n}: UseWindowResize) => {\n  const unsubscribeResizeStart = emitter.on(\"resizeStart\", onResizeStart);\n  const unsubscribeResize = emitter.on(\"resize\", onResize);\n  const unsubscribeResizeEnd = emitter.on(\"resizeEnd\", onResizeEnd);\n\n  return () => {\n    unsubscribeResizeStart();\n    unsubscribeResize();\n    unsubscribeResizeEnd();\n  };\n};\n\n/**\n * Subscribe to DOM resize event only once and then notify all listeners over js only.\n * This allows us to subscribe to resize by many listeners without perf issues, since its going to be the same resize event.\n * TODO: We can add throttling and RAF if needed.\n */\nexport const useWindowResize = (onResize: () => void) => {\n  useEffect(() => {\n    const unsubscribeResize = emitter.on(\"resize\", onResize);\n    return () => {\n      unsubscribeResize();\n    };\n  }, [onResize]);\n};\n\nexport const useWindowResizeDebounced = (onResize: () => void) => {\n  useEffect(() => {\n    // Call on leading\n    const unsubscribeResizeStart = emitter.on(\"resizeStart\", onResize);\n    // and trailing edge\n    const unsubscribeResizeEnd = emitter.on(\"resizeEnd\", onResize);\n    return () => {\n      unsubscribeResizeStart();\n      unsubscribeResizeEnd();\n    };\n  }, [onResize]);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/dom-utils.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport { scrollIntoView } from \"./dom-utils\";\n\nexport default {\n  title: \"DOM utils/Scroll into view\",\n};\n\nconst handleClick = (selector: string) => {\n  const elements = document.querySelectorAll(selector);\n\n  const element = elements[0];\n  if (element === undefined) {\n    return;\n  }\n\n  if (false === element instanceof HTMLElement) {\n    return;\n  }\n\n  scrollIntoView(element, element.getBoundingClientRect());\n};\n\nconst ToolbarStory = () => {\n  return (\n    <StorySection title=\"Toolbar\">\n      <div>\n        <button onClick={() => handleClick(\".element\")}>\n          Test Scroll Into Scrollable\n        </button>\n        <button onClick={() => handleClick(\".viewport\")}>\n          Test Scroll Into Viewport\n        </button>\n        <h1\n          style={{\n            marginTop: \"1000px\",\n          }}\n        >\n          Viewport Matrix Fiztures\n        </h1>\n        <div\n          style={{\n            marginTop: \"20px\",\n            width: \"200px\",\n            height: \"200px\",\n            overflow: \"auto\",\n          }}\n        >\n          <h1 style={{ marginTop: \"2400px\" }} className=\"element\">\n            Inside scrollable\n          </h1>\n          <div style={{ height: \"1000px\" }}></div>\n        </div>\n        <h2 className=\"viewport\">Inside viewport</h2>\n        <div style={{ height: \"1000px\" }}></div>\n        <button onClick={() => handleClick(\".element\")}>\n          Test Scroll Into Scrollable\n        </button>\n        <button onClick={() => handleClick(\".viewport\")}>\n          Test Scroll Into Viewport\n        </button>\n      </div>\n    </StorySection>\n  );\n};\n\nexport { ToolbarStory as Toolbar };\n"
  },
  {
    "path": "apps/builder/app/shared/dom-utils.test.ts",
    "content": "/**\n * @vitest-environment jsdom\n */\n\nimport { test, expect, beforeEach } from \"vitest\";\nimport {\n  getAllElementsByInstanceSelector,\n  getVisibleElementsByInstanceSelector,\n} from \"./dom-utils\";\n\nbeforeEach(() => {\n  document.body.innerHTML = `\n  <div data-ws-selector=\"box,body\">\n    <select data-ws-selector=\"select,box,body\">\n      <option data-ws-selector=\"option,select,box,body\">test</option>\n    </select>\n    <div style=\"display:fixed;\" data-ws-selector=\"box-fixed,body\"></div>\n    <div style=\"display:none;\" data-ws-selector=\"box-none,body\"></div>\n  </div>`;\n\n  document.body.setAttribute(\"data-ws-selector\", \"body\");\n});\n\ntest(\"Select body element\", () => {\n  expect(getVisibleElementsByInstanceSelector([\"body\"])).toEqual([\n    document.body,\n  ]);\n});\n\ntest(\"getVisibleElementsByInstanceSelector selects select element\", () => {\n  expect(\n    getVisibleElementsByInstanceSelector([\"option\", \"select\", \"box\", \"body\"])\n  ).toEqual([document.querySelector(\"select\")]);\n});\n\ntest(\"getAllElementsByInstanceSelector selects option element\", () => {\n  expect(\n    getAllElementsByInstanceSelector([\"option\", \"select\", \"box\", \"body\"])\n  ).toEqual([document.querySelector(\"option\")]);\n});\n\ntest(\"getVisibleElementsByInstanceSelector selects parent of box-none element\", () => {\n  expect(getVisibleElementsByInstanceSelector([\"box-none\", \"body\"])).toEqual([\n    document.querySelector(\"[data-ws-selector='box,body']\"),\n  ]);\n});\n\ntest(\"getAllElementsByInstanceSelector selects box-none element\", () => {\n  expect(getAllElementsByInstanceSelector([\"box-none\", \"body\"])).toEqual([\n    document.querySelector(\"[data-ws-selector='box-none,body']\"),\n  ]);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/dom-utils.ts",
    "content": "import type { Instance } from \"@webstudio-is/sdk\";\nimport { idAttribute, selectorIdAttribute } from \"@webstudio-is/react-sdk\";\nimport type { InstanceSelector } from \"./tree-utils\";\nimport { getIsVisuallyHidden } from \"./visually-hidden\";\n\nexport const getInstanceIdFromElement = (\n  element: Element\n): Instance[\"id\"] | undefined => {\n  return element.getAttribute(idAttribute) ?? undefined;\n};\n\n// traverse dom to the root and find all instances\nexport const getInstanceSelectorFromElement = (element: Element) => {\n  // Change logic to support Portals\n  const matched: undefined | Element =\n    element.closest(`[${idAttribute}]`) ?? undefined;\n\n  const instanceSelector: InstanceSelector =\n    matched?.getAttribute(selectorIdAttribute)?.split(\",\") ?? [];\n\n  if (instanceSelector.length === 0) {\n    return;\n  }\n  return instanceSelector;\n};\n\nexport const getElementByInstanceSelector = (\n  instanceSelector: InstanceSelector | Readonly<InstanceSelector>\n) => {\n  return (\n    document.querySelector<HTMLElement>(\n      `[${selectorIdAttribute}=\"${instanceSelector.join(\",\")}\"]`\n    ) ?? undefined\n  );\n};\n\n// Determine if the element is detached, or lacks visual layout.\n// We want to exclude elements that are display: none, option tags, or are not in the DOM\nconst hasLayout = (element: HTMLElement) => {\n  // Detached element\n  if (false === document.documentElement.contains(element)) {\n    return false;\n  }\n\n  if (element.tagName.toLowerCase() === \"option\") {\n    return false;\n  }\n\n  // Display none\n  if (getComputedStyle(element)?.display?.toLowerCase() === \"none\") {\n    return false;\n  }\n\n  return true;\n};\n\nexport const getVisibleElementsByInstanceSelector = (\n  instanceSelector: InstanceSelector | Readonly<InstanceSelector>\n) => {\n  return getElementsByInstanceSelector(instanceSelector, true);\n};\n\nexport const getAllElementsByInstanceSelector = (\n  instanceSelector: InstanceSelector | Readonly<InstanceSelector>\n) => {\n  return getElementsByInstanceSelector(instanceSelector, false);\n};\n\n/**\n * Get root visible elements, even if instance\n **/\nconst getElementsByInstanceSelector = (\n  instanceSelector: InstanceSelector | Readonly<InstanceSelector>,\n  skipHidden: boolean\n) => {\n  const descendantsOrSelf = [\n    ...document.querySelectorAll<HTMLElement>(\n      `[${selectorIdAttribute}$=\"${instanceSelector.join(\",\")}\"]`\n    ),\n  ].filter((element) =>\n    skipHidden ? getIsVisuallyHidden(element) === false : true\n  );\n\n  const visibleIdSelectors = descendantsOrSelf.map(\n    (element) => element.getAttribute(selectorIdAttribute) ?? \"\"\n  );\n\n  // Find root selectors (i.e. selectors that are not descendants of other selectors)\n  let rootSelectors = [...visibleIdSelectors];\n  const isDescendant = (testSelector: string, selector: string) =>\n    testSelector.endsWith(`,${selector}`);\n\n  for (const selector of visibleIdSelectors) {\n    rootSelectors = rootSelectors.filter(\n      (rootSelector) => isDescendant(rootSelector, selector) === false\n    );\n  }\n\n  const rootSelectorSet = new Set(rootSelectors);\n\n  const rootElements = descendantsOrSelf.filter((element) =>\n    rootSelectorSet.has(element.getAttribute(selectorIdAttribute) ?? \"\")\n  );\n\n  return rootElements.map((element) => {\n    let elementResult: HTMLElement = element;\n\n    while (\n      skipHidden &&\n      false === hasLayout(elementResult) &&\n      elementResult.parentElement !== null\n    ) {\n      elementResult = elementResult.parentElement;\n    }\n\n    return elementResult;\n  });\n};\n\ntype Rect = {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n};\n\nconst sumRects = (first: Rect, second: Rect) => {\n  return {\n    top: Math.min(first.top, second.top),\n    right: Math.max(first.right, second.right),\n    bottom: Math.max(first.bottom, second.bottom),\n    left: Math.min(first.left, second.left),\n  };\n};\n\nexport const getAllElementsBoundingBox = (\n  elements: Element[],\n  depth: number = 0\n): DOMRect => {\n  const rects: Rect[] = [];\n\n  if (elements.length === 0) {\n    return DOMRect.fromRect({ width: 0, height: 0, x: 0, y: 0 });\n  }\n\n  for (const element of elements) {\n    const rect = element.getBoundingClientRect();\n\n    // possible display: contents\n    if (rect.width !== 0 || rect.height !== 0) {\n      rects.push(rect);\n      continue;\n    }\n\n    if (element.children.length === 0) {\n      const textNode = element.firstChild;\n\n      if (textNode?.nodeType === Node.TEXT_NODE) {\n        // Create a range object\n        const range = document.createRange();\n        // Set the range to encompass the text node\n        range.selectNodeContents(textNode);\n        // Get the bounding rectangle\n        const rect = range.getBoundingClientRect();\n\n        if (rect.width !== 0 || rect.height !== 0) {\n          rects.push(rect);\n          range.detach();\n          continue;\n        }\n        range.detach();\n      }\n    }\n\n    if (element.children.length > 0) {\n      const childRect = getAllElementsBoundingBox(\n        [...element.children],\n        depth + 1\n      );\n      if (childRect.width !== 0 || childRect.height !== 0) {\n        const { top, right, bottom, left } = childRect;\n        rects.push({ top, right, bottom, left });\n        continue;\n      }\n    }\n\n    if (depth > 0) {\n      continue;\n    }\n\n    // We here, let's try ancestor size\n    const parentElement = element.parentElement;\n    if (parentElement === null) {\n      continue;\n    }\n    const parentRect = getAllElementsBoundingBox([parentElement]);\n\n    if (parentRect.width !== 0 || parentRect.height !== 0) {\n      const { top, right, bottom, left } = parentRect;\n      rects.push({ top, right, bottom, left });\n      continue;\n    }\n  }\n\n  if (rects.length === 0) {\n    // To preserve position even if width/height is 0\n    return elements[0].getBoundingClientRect();\n  }\n\n  const { top, right, bottom, left } = rects.reduce(sumRects);\n\n  return DOMRect.fromRect({\n    x: left,\n    y: top,\n    height: bottom - top,\n    width: right - left,\n  });\n};\n\nconst doNotTrackMutationAttribute = \"data-ws-do-not-track-mutation\";\n\nexport const doNotTrackMutation = (element: Element) => {\n  element.setAttribute(doNotTrackMutationAttribute, \"true\");\n};\n\nexport const hasDoNotTrackMutationAttribute = (element: Element) => {\n  return element.hasAttribute(doNotTrackMutationAttribute);\n};\n\nexport const hasDoNotTrackMutationRecord = (\n  mutationRecords: MutationRecord[]\n) => {\n  return mutationRecords.some((record) =>\n    record.type === \"childList\"\n      ? [...record.addedNodes.values()].some(\n          (node) =>\n            node instanceof Element &&\n            node.hasAttribute(doNotTrackMutationAttribute)\n        )\n      : false\n  );\n};\n\n/**\n * Get a DOMMatrix mapping the container's local coords to viewport coords.\n * This uses one test DIV (width=100, height=100) placed at (0,0) in container space.\n */\nconst getLocalToViewportMatrix = (container: Element): DOMMatrix => {\n  const rectSize = 100;\n\n  const testDiv = document.createElement(\"div\");\n  Object.assign(testDiv.style, {\n    position: \"absolute\",\n    left: \"0px\",\n    top: \"0px\",\n    width: `${rectSize}px`,\n    height: `${rectSize}px`,\n    pointerEvents: \"none\",\n    background: \"transparent\",\n    visibility: \"hidden\",\n  });\n  container.appendChild(testDiv);\n  testDiv.setAttribute(doNotTrackMutationAttribute, \"true\");\n\n  const { left, top, width, height } = testDiv.getBoundingClientRect();\n  container.removeChild(testDiv);\n\n  const x1 = left;\n  const y1 = top;\n  const x2 = left + width;\n  const y2 = top;\n  const x3 = left;\n  const y3 = top + height;\n\n  const a = (x2 - x1) / rectSize;\n  const b = (y2 - y1) / rectSize;\n  const c = (x3 - x1) / rectSize;\n  const d = (y3 - y1) / rectSize;\n  const e = x1;\n  const f = y1;\n\n  return new DOMMatrix([a, b, c, d, e, f]);\n};\n\nconst getViewportToLocalMatrix = (container: Element): DOMMatrix => {\n  return getLocalToViewportMatrix(container).inverse();\n};\n\nconst transformDOMRect = (rect: DOMRect, matrix: DOMMatrix) => {\n  const topLeft = new DOMPoint(rect.x, rect.y).matrixTransform(matrix);\n  const topRight = new DOMPoint(rect.x + rect.width, rect.y).matrixTransform(\n    matrix\n  );\n  const bottomLeft = new DOMPoint(rect.x, rect.y + rect.height).matrixTransform(\n    matrix\n  );\n  const bottomRight = new DOMPoint(\n    rect.x + rect.width,\n    rect.y + rect.height\n  ).matrixTransform(matrix);\n\n  const xs = [topLeft.x, topRight.x, bottomLeft.x, bottomRight.x];\n  const ys = [topLeft.y, topRight.y, bottomLeft.y, bottomRight.y];\n  const minX = Math.min(...xs);\n  const maxX = Math.max(...xs);\n  const minY = Math.min(...ys);\n  const maxY = Math.max(...ys);\n\n  return new DOMRect(minX, minY, maxX - minX, maxY - minY);\n};\n\n/**\n * `overflow: hidden` can be scrolled\n */\nconst isScrollable = (\n  node: HTMLElement,\n  checkForOverflow: boolean\n): boolean => {\n  if (!node) {\n    return false;\n  }\n  const style = window.getComputedStyle(node);\n\n  let isScrollable = /(auto|scroll|hidden)/.test(\n    style.overflow + style.overflowX + style.overflowY\n  );\n\n  if (isScrollable && checkForOverflow) {\n    isScrollable =\n      node.scrollHeight !== node.clientHeight ||\n      node.scrollWidth !== node.clientWidth;\n  }\n\n  return isScrollable;\n};\n\nconst getScrollParent = (\n  node: HTMLElement,\n  checkForOverflow: boolean\n): HTMLElement | null => {\n  for (\n    let scrollableNode = node.parentElement;\n    scrollableNode !== null;\n    scrollableNode = scrollableNode.parentElement\n  ) {\n    if (isScrollable(scrollableNode, checkForOverflow)) {\n      return scrollableNode;\n    }\n  }\n\n  return document.documentElement;\n};\n\n/**\n * We scroll using rectangle and anchor calculations because `scrollIntoView` does not work\n * reliably for certain elements, such as those with `display: contents`.\n * For these elements, we display a selected or hovered outline on the canvas using the\n * bounding rectangles of their children or the selection range.\n * Here, we ensure scrolling works for these elements as well.\n */\nexport const scrollIntoView = (anchor: HTMLElement, rect: DOMRect) => {\n  const scrollParent = getScrollParent(anchor, true);\n\n  if (false === scrollParent instanceof HTMLElement) {\n    return;\n  }\n\n  requestAnimationFrame(() => {\n    const savedPosition = (scrollParent as HTMLElement).style.position;\n    // avoid updating <html> to prevent full page repaint and freeze on big projects\n    if (scrollParent.tagName !== \"HTML\") {\n      (scrollParent as HTMLElement).style.position = \"relative\";\n    }\n\n    const matrix = getViewportToLocalMatrix(scrollParent);\n\n    const transformedRect = transformDOMRect(rect, matrix);\n\n    const scrollDiv = document.createElement(\"div\");\n\n    Object.assign(scrollDiv.style, {\n      position: \"absolute\",\n      left: `${transformedRect.left}px`,\n      top: `${transformedRect.top}px`,\n      width: `${transformedRect.width}px`,\n      height: `${transformedRect.height}px`,\n      pointerEvents: \"none\",\n      background: \"transparent\",\n      scrollMargin: \"20px\",\n    });\n    scrollParent.appendChild(scrollDiv);\n\n    scrollDiv.scrollIntoView({ behavior: \"smooth\", block: \"nearest\" });\n\n    scrollParent.removeChild(scrollDiv);\n\n    // avoid updating <html> to prevent full page repaint and freeze on big projects\n    if (scrollParent.tagName !== \"HTML\") {\n      (scrollParent as HTMLElement).style.position = savedPosition;\n    }\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/empty.ts",
    "content": "// This module is intentionally left empty to prevent the import of certain libraries.\n// Specific libraries have been excluded from the project using Vite's alias configuration.\n// Any import that resolves to this module will return an empty object instead of the actual library.\n\nexport default {};\n"
  },
  {
    "path": "apps/builder/app/shared/entri/entri-api.server.ts",
    "content": "import { z } from \"zod\";\nimport env from \"~/env/env.server\";\n\nconst EntryResponse = z.object({\n  auth_token: z.string(),\n});\n\n/**\n * Short lived JWT token for Entri API\n *  https://developers.entri.com/docs/install#3-fetch-the-json-web-token-jwt\n */\nconst getEntriToken = async () => {\n  const { ENTRI_APPLICATION_ID, ENTRI_SECRET } = env;\n\n  if (ENTRI_SECRET === undefined) {\n    throw new Error(`ENTRI_SECRET is not defined`);\n  }\n\n  const entryResponse = await fetch(\"https://api.goentri.com/token\", {\n    method: \"POST\",\n    body: JSON.stringify({\n      // These values come from the Entri dashboard\n      applicationId: ENTRI_APPLICATION_ID,\n      secret: ENTRI_SECRET,\n    }),\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Accept: \"application/json\",\n    },\n  });\n\n  if (entryResponse.ok === false) {\n    throw new Error(`Entri API error: ${await entryResponse.text()}`);\n  }\n\n  const entryJson = await entryResponse.json();\n  const responseData = EntryResponse.parse(entryJson);\n\n  return {\n    token: responseData.auth_token,\n    applicationId: ENTRI_APPLICATION_ID,\n  } as const;\n};\n\nexport const entryApi = {\n  getEntriToken,\n};\n"
  },
  {
    "path": "apps/builder/app/shared/error/error-boundary.tsx",
    "content": "import { useRouteError } from \"@remix-run/react\";\nimport { ClientOnly } from \"../client-only\";\nimport { lazy } from \"react\";\nimport { parseError } from \"./error-parse\";\n\nconst ErrorMessage = lazy(async () => {\n  const { ErrorMessage } = await import(\"./error-message.client\");\n  return { default: ErrorMessage };\n});\n\nexport const ErrorBoundary = () => {\n  const rawError = useRouteError();\n\n  const error = parseError(rawError);\n\n  return (\n    <ClientOnly>\n      <ErrorMessage error={error} />\n    </ClientOnly>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/error/error-message.client.tsx",
    "content": "import {\n  AccessibleIcon,\n  css,\n  Grid,\n  Link,\n  Text,\n  theme,\n  buttonStyle,\n} from \"@webstudio-is/design-system\";\nimport { WebstudioIcon } from \"@webstudio-is/icons\";\n\nconst pageStyle = css({\n  position: \"fixed\",\n  justifyItems: \"center\",\n  alignContent: \"start\",\n  inset: 0,\n  background: theme.colors.brandBackgroundDashboard,\n  paddingTop: \"10vh\",\n  // prevent global root styles override error color\n  color: theme.colors.black,\n});\n\nexport const ErrorMessage = ({\n  error,\n}: {\n  error: {\n    status: number;\n    statusText?: string;\n    message: string;\n    description?: string;\n  };\n}) => {\n  return (\n    <Grid className={pageStyle()} justify={\"center\"} gap={6}>\n      <AccessibleIcon label=\"Logo\">\n        <WebstudioIcon size=\"60\" />\n      </AccessibleIcon>\n      <div />\n      <div />\n      <Text\n        css={{\n          fontSize: theme.spacing[21],\n          lineHeight: 1,\n        }}\n        variant={\"bigTitle\"}\n      >\n        {error.status}\n      </Text>\n\n      <Grid\n        css={{\n          justifyItems: \"center\",\n          marginInline: theme.spacing[10],\n          maxWidth: 600,\n        }}\n        gap={5}\n      >\n        <Grid\n          css={{\n            background: theme.colors.backgroundPanel,\n            padding: theme.spacing[7],\n            borderRadius: theme.spacing[5],\n            minWidth: theme.spacing[34],\n          }}\n          gap=\"3\"\n        >\n          <Text\n            css={{\n              display: \"-webkit-box\",\n              \"-webkit-line-clamp\": 4,\n              \"-webkit-box-orient\": \"vertical\",\n              overflow: \"hidden\",\n              textOverflow: \"ellipsis\",\n              wordBreak: \"break-word\",\n            }}\n            variant=\"brandSectionTitle\"\n            userSelect=\"text\"\n          >\n            {error.message ?? error.statusText}\n          </Text>\n\n          {error.description && (\n            <Text\n              css={{ wordBreak: \"break-word\", whiteSpace: \"pre-line\" }}\n              userSelect=\"text\"\n              variant=\"brandRegular\"\n            >\n              {error.description}\n            </Text>\n          )}\n        </Grid>\n        <Link\n          href=\"/\"\n          className={buttonStyle()}\n          color=\"contrast\"\n          underline=\"none\"\n        >\n          Go Home\n        </Link>\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/error/error-message.stories.tsx",
    "content": "import { Flex, StorySection } from \"@webstudio-is/design-system\";\nimport { ErrorMessage as ErrorMessageComponent } from \"./error-message.client\";\n\nexport default {\n  title: \"Error Message\",\n  component: ErrorMessageComponent,\n};\n\nexport const ErrorMessage = () => (\n  <StorySection title=\"Error Message\">\n    <Flex direction=\"column\" gap=\"9\">\n      <ErrorMessageComponent\n        error={{\n          status: 404,\n          statusText: \"Not Found\",\n          message: \"The page you're looking for doesn't exist.\",\n        }}\n      />\n      <ErrorMessageComponent\n        error={{\n          status: 500,\n          statusText: \"Internal Server Error\",\n          message: \"Something went wrong on our end.\",\n          description:\n            \"Please try again later or contact support if the problem persists.\",\n        }}\n      />\n      <ErrorMessageComponent\n        error={{\n          status: 403,\n          statusText: \"Forbidden\",\n          message: \"You don't have permission to access this resource.\",\n        }}\n      />\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/shared/error/error-parse.ts",
    "content": "import { isRouteErrorResponse } from \"@remix-run/react\";\nimport { z } from \"zod\";\n\n// Currently, we have multiple error formats, and not all of them are covered here.\n// We should consolidate these formats into a single, unified format for consistency.\nconst PageError = z.union([\n  z.string().transform((message) => ({ message, description: undefined })),\n  z\n    .object({\n      message: z.string(),\n      details: z.string(),\n      hint: z.string().nullable(),\n      code: z.string(),\n    })\n    .transform(({ message, details, hint, code }) => ({\n      message,\n      description: `details: ${details}; hint: ${hint}; code: ${code}`,\n    })),\n  z.object({\n    message: z.string(),\n    description: z.string().optional(),\n  }),\n]);\n\nexport const parseError = (\n  error: unknown\n): {\n  status: number;\n  statusText?: string;\n  message: string;\n  description?: string;\n} => {\n  if (error instanceof Error) {\n    return {\n      message: error.message,\n      status: 500,\n    };\n  }\n\n  if (isRouteErrorResponse(error)) {\n    const parsed = PageError.safeParse(error.data);\n\n    if (parsed.success) {\n      return {\n        message: parsed.data.message,\n        description: parsed.data.description,\n        status: error.status,\n        statusText: error.statusText,\n      };\n    }\n\n    return {\n      message: error.data ? JSON.stringify(error.data) : \"unknown error\",\n      status: error.status,\n      statusText: error.statusText,\n    };\n  }\n\n  const parsed = PageError.safeParse(error);\n  if (parsed.success) {\n    return {\n      message: parsed.data.message,\n      description: parsed.data.description,\n      status: 1001,\n    };\n  }\n\n  return {\n    message: JSON.stringify(error ?? \"unknown error\"),\n    status: 1001,\n    statusText: undefined,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/error/index.ts",
    "content": "export * from \"./error-message.client\";\n"
  },
  {
    "path": "apps/builder/app/shared/error/toast-error.tsx",
    "content": "import { toast } from \"@webstudio-is/design-system\";\nimport { useEffect } from \"react\";\nimport { $toastErrors } from \"../nano-states\";\n\nlet toastErrorsIndex = 0;\n\n/**\n * To show errors exposed from the canvas | builder\n */\nexport const useToastErrors = () => {\n  useEffect(() => {\n    return $toastErrors.subscribe((toastErrors) => {\n      for (\n        let errorIndex = toastErrorsIndex;\n        errorIndex < toastErrors.length;\n        errorIndex++\n      ) {\n        toast.error(toastErrors[errorIndex]);\n      }\n\n      toastErrorsIndex = toastErrors.length;\n    });\n  }, []);\n};\n\nexport const toastError = (_error: string) => {\n  $toastErrors.set([\n    ...$toastErrors.get(),\n    \"Copying has been disabled by the project owner\",\n  ]);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/event-utils.test.ts",
    "content": "import { describe, test, expect, vi } from \"vitest\";\nimport { composeEventHandlers } from \"./event-utils\";\n\ndescribe(\"composeEventHandlers\", () => {\n  test(\"executes handlers in sequence\", () => {\n    const handler1 = vi.fn();\n    const handler2 = vi.fn();\n    const event = {};\n\n    const composed = composeEventHandlers([handler1, handler2]);\n    composed(event);\n\n    expect(handler1).toHaveBeenCalledWith(event);\n    expect(handler2).toHaveBeenCalledWith(event);\n  });\n\n  test(\"stops execution if event.defaultPrevented is true\", () => {\n    const handler1 = vi.fn((event) => {\n      event.defaultPrevented = true;\n    });\n    const handler2 = vi.fn();\n    const event = {};\n\n    const composed = composeEventHandlers([handler1, handler2]);\n    composed(event);\n\n    expect(handler1).toHaveBeenCalled();\n    expect(handler2).not.toHaveBeenCalled();\n  });\n\n  test(\"continues execution when checkForDefaultPrevented is false\", () => {\n    const handler1 = vi.fn((event) => {\n      event.defaultPrevented = true;\n    });\n    const handler2 = vi.fn();\n    const event = {};\n\n    const composed = composeEventHandlers([handler1, handler2], {\n      checkForDefaultPrevented: false,\n    });\n    composed(event);\n\n    expect(handler1).toHaveBeenCalled();\n    expect(handler2).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/event-utils.ts",
    "content": "/*\n * Inspired by\n * https://github.com/radix-ui/primitives/blob/main/packages/core/primitive/src/primitive.tsx\n */\nexport const composeEventHandlers = <CustomEvent>(\n  handlers: Array<(event: CustomEvent) => void>,\n  { checkForDefaultPrevented = true } = {}\n) => {\n  return function handleEvent(event: CustomEvent) {\n    for (const handler of handlers) {\n      handler?.(event);\n      if (\n        checkForDefaultPrevented &&\n        (event as unknown as Event).defaultPrevented\n      ) {\n        break;\n      }\n    }\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/fetch.client.ts",
    "content": "import { toast } from \"@webstudio-is/design-system\";\nimport { csrfToken } from \"./csrf.client\";\nimport { $authToken } from \"./nano-states\";\n\n/**\n * To avoid fetch interception from the canvas, i.e., `globalThis.fetch = () => console.log('INTERCEPTED');`,\n */\nconst _fetch = globalThis.fetch;\n\n/**\n * To avoid fetch interception from the canvas, i.e., `globalThis.fetch = () => console.log('INTERCEPTED');`,\n * To add csrf token to the headers.\n */\nexport const fetch: typeof globalThis.fetch = (requestInfo, requestInit) => {\n  if (csrfToken === undefined) {\n    toast.error(\"CSRF token is not set.\");\n    throw new Error(\"CSRF token is not set.\");\n  }\n\n  const headers = new Headers(requestInit?.headers);\n\n  headers.set(\"X-CSRF-Token\", csrfToken);\n\n  const authToken = $authToken.get();\n\n  // Do not override the existing x-auth-token header.\n  // As some mutations are queue based and they need to be authenticated with the same token as in the queue.\n  if (authToken !== undefined && headers.get(\"x-auth-token\") === null) {\n    headers.set(\"x-auth-token\", authToken);\n  }\n\n  const modifiedInit: RequestInit = {\n    ...requestInit,\n    headers,\n  };\n\n  return _fetch(requestInfo, modifiedInit);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/form-utils/index.ts",
    "content": "export * from \"./use-ids\";\n"
  },
  {
    "path": "apps/builder/app/shared/form-utils/use-ids.ts",
    "content": "import { useId } from \"react\";\n\nexport const useIds = <FieldName extends string>(\n  fieldNames: readonly FieldName[]\n) => {\n  const id = useId();\n  return Object.fromEntries(\n    fieldNames.map((fieldName) => [fieldName, `${id}-${fieldName}`])\n  ) as Record<FieldName, string>;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/help.tsx",
    "content": "import { ContentIcon, DiscordIcon, YoutubeIcon } from \"@webstudio-is/icons\";\n\nexport const help = [\n  {\n    label: \"Video tutorials\",\n    url: \"https://wstd.us/101\",\n    icon: <YoutubeIcon />,\n  },\n  {\n    label: \"Docs\",\n    url: \"https://docs.webstudio.is/\",\n    icon: <ContentIcon />,\n  },\n  {\n    label: \"Community\",\n    url: \"https://wstd.us/community\",\n    icon: <DiscordIcon />,\n  },\n] as const;\n"
  },
  {
    "path": "apps/builder/app/shared/hook-utils/effect-event.ts",
    "content": "import { useRef, useInsertionEffect, useCallback } from \"react\";\n\nconst useSafeInsertionEffect =\n  typeof window !== \"undefined\"\n    ? useInsertionEffect\n    : (fn: () => void) => {\n        fn();\n      };\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\nexport const useEffectEvent = <T extends Function>(callback?: T) => {\n  const ref = useRef(callback);\n\n  useSafeInsertionEffect(() => {\n    ref.current = callback;\n  });\n\n  return useCallback<T>(\n    ((...args: unknown[]) => ref.current?.(...args)) as never,\n    []\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/hook-utils/use-interval.ts",
    "content": "import { useEffect, useRef } from \"react\";\n\ntype Timeout = ReturnType<typeof setTimeout>;\n\nexport const useInterval = (callback: (id: Timeout) => void, delay: number) => {\n  const savedCallback = useRef(callback);\n\n  useEffect(() => {\n    savedCallback.current = callback;\n  });\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      savedCallback.current(id);\n    }, delay);\n    return () => clearInterval(id);\n  }, [delay]);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/hook-utils/use-mount.ts",
    "content": "import { useRef } from \"react\";\nimport { type EffectCallback, useEffect } from \"react\";\n\nexport const useMount = (effect: EffectCallback) => {\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useEffect(effect, []);\n};\n\nexport const useUnmount = (fn: () => void): void => {\n  const fnRef = useRef(fn);\n  // update the ref each render so if it change the newest callback will be invoked\n  fnRef.current = fn;\n  useMount(() => {\n    return () => fnRef.current();\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/html.test.tsx",
    "content": "import { expect, test, describe } from \"vitest\";\nimport { $, css, renderTemplate, token, ws } from \"@webstudio-is/template\";\nimport { generateFragmentFromHtml as _generateFragmentFromHtml } from \"./html\";\n\n// Wrapper that strips skippedSelectors for tests that only compare fragment shape\nconst generateFragmentFromHtml = (html: string) => {\n  const { skippedSelectors: _skipped, ...fragment } =\n    _generateFragmentFromHtml(html);\n  return fragment;\n};\n\ntest(\"generate instances from html\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <main>\n        <section>\n          <h1>It works!</h1>\n          <p>Webstudio is great.</p>\n          <ul>\n            <li>one</li>\n            <li>two</li>\n          </ul>\n        </section>\n      </main>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"main\">\n        <ws.element ws:tag=\"section\">\n          <ws.element ws:tag=\"h1\">It works!</ws.element>\n          <ws.element ws:tag=\"p\">Webstudio is great.</ws.element>\n          <ws.element ws:tag=\"ul\">\n            <ws.element ws:tag=\"li\">one</ws.element>\n            <ws.element ws:tag=\"li\">two</ws.element>\n          </ws.element>\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"generate multiple root instances from html\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <section>\n        <h1>One</h1>\n      </section>\n      <section>\n        <h1>Two</h1>\n      </section>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <>\n        <ws.element ws:tag=\"section\">\n          <ws.element ws:tag=\"h1\">One</ws.element>\n        </ws.element>\n        <ws.element ws:tag=\"section\">\n          <ws.element ws:tag=\"h1\">Two</ws.element>\n        </ws.element>\n      </>\n    )\n  );\n});\n\ntest(\"handle broken html\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <section>\n        <h1>One</h1>\n      </section attribute=\"value\">\n   `)\n  ).toEqual(\n    renderTemplate(\n      <>\n        <ws.element ws:tag=\"section\">\n          <ws.element ws:tag=\"h1\">One</ws.element>\n        </ws.element>\n      </>\n    )\n  );\n});\n\ntest(\"handle non-html\", () => {\n  expect(generateFragmentFromHtml(\"\")).toEqual(renderTemplate(<></>));\n  expect(generateFragmentFromHtml(\"It works!\")).toEqual(renderTemplate(<></>));\n});\n\ntest(\"ignore custom elements\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <custom-element>\n        <div></div>\n      </custom-element>\n      <section></section>\n   `)\n  ).toEqual(renderTemplate(<ws.element ws:tag=\"section\"></ws.element>));\n});\n\ntest(\"ignore not allowed tags\", () => {\n  expect(\n    generateFragmentFromHtml(`<div><marquee>test</marquee></div>`)\n  ).toEqual(renderTemplate(<ws.element ws:tag=\"div\"></ws.element>));\n});\n\ntest(\"generate props from html attributes\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <form action=\"/my-action\">\n        <button class=\"my-button\">My Button</button>\n      </form>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"form\" action=\"/my-action\">\n        <ws.element ws:tag=\"button\" class=\"my-button\">\n          My Button\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"generate props from number and boolean html attributes\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <button autofocus tabindex=\"-1\">My Button</button>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"button\" autofocus={true} tabindex={-1}>\n        My Button\n      </ws.element>\n    )\n  );\n});\n\ntest(\"generate props from number and boolean aria attributes\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <button aria-expanded aria-valuemin=\"100\">My Button</button>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"button\" aria-expanded={true} aria-valuemin={100}>\n        My Button\n      </ws.element>\n    )\n  );\n});\n\ntest(\"wrap text with span when spotted outside of rich text\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>div<article>article</article></div>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        <ws.element ws:tag=\"span\">div</ws.element>\n        <ws.element ws:tag=\"article\">article</ws.element>\n      </ws.element>\n    )\n  );\n  expect(\n    generateFragmentFromHtml(`\n      <div>div<b><br></b></div>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        <ws.element ws:tag=\"span\">div</ws.element>\n        <ws.element ws:tag=\"b\">\n          <ws.element ws:tag=\"br\"></ws.element>\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"do not wrap text with span when spotted near link\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>div<a>link</a></div>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        div\n        <ws.element ws:tag=\"a\">link</ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"collapse any spacing characters inside text\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>\n        line\n        another line\n      </div>\n   `)\n  ).toEqual(\n    renderTemplate(<ws.element ws:tag=\"div\">{\"line another line\"}</ws.element>)\n  );\n});\n\ntest(\"collapse any spacing characters inside rich text\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>\n        <i> line </i>\n        <b> another line </b>\n        text\n      </div>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        <ws.element ws:tag=\"i\">line</ws.element>{\" \"}\n        <ws.element ws:tag=\"b\">another line</ws.element> text\n      </ws.element>\n    )\n  );\n});\n\ntest(\"generate style attribute as local styles\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div style=\"display: inline\"></div>\n   `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          display: inline;\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"script as html embed\", () => {\n  expect(generateFragmentFromHtml(`<script>a;</script>`)).toEqual(\n    renderTemplate(\n      <$.HtmlEmbed\n        ws:label=\"Script\"\n        clientOnly={true}\n        code={`<script>a;</script>`}\n      />\n    )\n  );\n});\n\ntest(\"style as html embed\", () => {\n  expect(generateFragmentFromHtml(`<style>a;</style>`)).toEqual(\n    renderTemplate(<$.HtmlEmbed code={`<style>a;</style>`} ws:label=\"Style\" />)\n  );\n});\n\ntest(\"generate textarea element\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>\n        <textarea>\n          my text\n        </textarea>\n      </div>\n    `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        <ws.element ws:tag=\"textarea\" value=\"my text\" />\n      </ws.element>\n    )\n  );\n});\n\ntest(\"generate select element\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>\n        <select>\n          <option value=\"one\">One</option>\n          <option value=\"two\" selected>Two</option>\n        </select>\n        <select>\n          <option>One</option>\n          <option selected>Two</option>\n        </select>\n      </div>\n    `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        <ws.element ws:tag=\"select\" value=\"two\">\n          <ws.element ws:tag=\"option\" value=\"one\">\n            One\n          </ws.element>\n          <ws.element ws:tag=\"option\" value=\"two\">\n            Two\n          </ws.element>\n        </ws.element>\n        <ws.element ws:tag=\"select\" value=\"Two\">\n          <ws.element ws:tag=\"option\">One</ws.element>\n          <ws.element ws:tag=\"option\">Two</ws.element>\n        </ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"generate Image component instead of img element\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <div>\n        <img src=\"./my-url\">\n      </div>\n    `)\n  ).toEqual(\n    renderTemplate(\n      <ws.element ws:tag=\"div\">\n        <$.Image src=\"./my-url\" />\n      </ws.element>\n    )\n  );\n});\n\ntest(\"strip unsupported attribute names\", () => {\n  expect(\n    generateFragmentFromHtml(`\n      <button @click=\"open = true\">Expand</button>\n      <button x-on:click=\"open = !open\">\n        Toggle\n      </button>\n    `)\n  ).toEqual(\n    renderTemplate(\n      <>\n        <ws.element ws:tag=\"button\">Expand</ws.element>\n        <ws.element ws:tag=\"button\" x-on:click=\"open = !open\">\n          Toggle\n        </ws.element>\n      </>\n    )\n  );\n});\n\ndescribe(\"style tag to tokens\", () => {\n  test(\"extract single class from style tag as token\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <div class=\"card\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[cardToken]}>\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"extract multiple classes from style tag as tokens\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n        padding: 16px;\n      `\n    );\n    const titleToken = token(\n      \"title\",\n      css`\n        font-size: 24px;\n        font-weight: bold;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; padding: 16px; }\n          .title { font-size: 24px; font-weight: bold; }\n        </style>\n        <div class=\"card\">\n          <h1 class=\"title\">Hello</h1>\n        </div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[cardToken]}>\n          <ws.element ws:tag=\"h1\" ws:tokens={[titleToken]}>\n            Hello\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"reuse same token when class appears on multiple elements\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <div class=\"card\">\n          <section class=\"card\">Hello</section>\n        </div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[cardToken]}>\n          <ws.element ws:tag=\"section\" ws:tokens={[cardToken]}>\n            Hello\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"preserve unresolved class names as class prop\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <div class=\"card external-lib\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"external-lib\" ws:tokens={[cardToken]}>\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"remove class prop entirely when all classes resolved to tokens\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>.card { display: flex; }</style>\n      <div class=\"card\">Hello</div>\n    `);\n    expect(fragment.props.find((p) => p.name === \"class\")).toBeUndefined();\n  });\n\n  test(\"combine inline style attribute with class token\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <div class=\"card\" style=\"color: red\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            color: red;\n          `}\n          ws:tokens={[cardToken]}\n        >\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"no html embed when all style rules are class rules\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>\n        .card { display: flex; }\n        .title { font-size: 24px; }\n      </style>\n      <div class=\"card\">\n        <h1 class=\"title\">Hello</h1>\n      </div>\n    `);\n    expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n      false\n    );\n  });\n\n  test(\"keep style tag as html embed when it has no class rules\", () => {\n    expect(\n      generateFragmentFromHtml(`\n        <style>#hero { color: red; }</style>\n        <div>Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <>\n          <$.HtmlEmbed\n            ws:label=\"Style\"\n            code={`<style>#hero { color: red; }</style>`}\n          />\n          <ws.element ws:tag=\"div\">Hello</ws.element>\n        </>\n      )\n    );\n  });\n\n  test(\"extract class rules and keep remaining rules as html embed\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>\n        .card { display: flex; }\n        #hero { color: red; }\n      </style>\n      <div class=\"card\">Hello</div>\n    `);\n    // token for .card should be created\n    const tokenSource = fragment.styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"card\"\n    );\n    expect(tokenSource).toBeDefined();\n    // remaining #hero rule should stay as HtmlEmbed\n    const htmlEmbed = fragment.instances.find(\n      (i) => i.component === \"HtmlEmbed\"\n    );\n    expect(htmlEmbed).toBeDefined();\n    const codeProp = fragment.props.find(\n      (p) => p.instanceId === htmlEmbed!.id && p.name === \"code\"\n    );\n    expect(codeProp).toBeDefined();\n    expect(codeProp!.type === \"string\" && codeProp!.value).toContain(\"#hero\");\n    // class prop should not be set since .card was resolved\n    expect(fragment.props.find((p) => p.name === \"class\")).toBeUndefined();\n  });\n\n  test(\"extract classes from multiple style tags\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    const titleToken = token(\n      \"title\",\n      css`\n        font-size: 24px;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <style>.title { font-size: 24px; }</style>\n        <div class=\"card\">\n          <h1 class=\"title\">Hello</h1>\n        </div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[cardToken]}>\n          <ws.element ws:tag=\"h1\" ws:tokens={[titleToken]}>\n            Hello\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"resolve nested selectors - descendant\", () => {\n    const cardInnerToken = token(\n      \"card__inner\",\n      css`\n        color: red;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card .inner { color: red; }</style>\n        <div class=\"card\">\n          <span class=\"inner\">Hello</span>\n        </div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"card\">\n          <ws.element ws:tag=\"span\" ws:tokens={[cardInnerToken]}>\n            Hello\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"resolve nested selectors - child combinator\", () => {\n    const parentChildToken = token(\n      \"parent__child\",\n      css`\n        margin: 0;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.parent > .child { margin: 0; }</style>\n        <div class=\"parent\"><span class=\"child\">Hello</span></div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"parent\">\n          <ws.element ws:tag=\"span\" ws:tokens={[parentChildToken]}>\n            Hello\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"ignore element selectors in style tag\", () => {\n    // element selectors like `div { }` should not become tokens\n    expect(\n      generateFragmentFromHtml(`\n        <style>div { color: red; }</style>\n        <div>Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <>\n          <$.HtmlEmbed\n            ws:label=\"Style\"\n            code={`<style>div { color: red; }</style>`}\n          />\n          <ws.element ws:tag=\"div\">Hello</ws.element>\n        </>\n      )\n    );\n  });\n\n  test(\"handle multiple classes on one element with multiple tokens\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    const largeToken = token(\n      \"large\",\n      css`\n        font-size: 32px;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          .large { font-size: 32px; }\n        </style>\n        <div class=\"card large\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[cardToken, largeToken]}>\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"handle class with multiple properties\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n        gap: 16px;\n        padding: 24px;\n        border-radius: 8px;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>\n          .card {\n            display: flex;\n            gap: 16px;\n            padding: 24px;\n            border-radius: 8px;\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[cardToken]}>\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"empty style tag produces no html embed\", () => {\n    expect(\n      generateFragmentFromHtml(`\n        <style></style>\n        <div>Hello</div>\n      `)\n    ).toEqual(renderTemplate(<ws.element ws:tag=\"div\">Hello</ws.element>));\n  });\n\n  test(\"class attribute without any style tag stays as prop\", () => {\n    // existing behavior: class is just a prop when no style tag is present\n    expect(\n      generateFragmentFromHtml(`\n        <div class=\"my-class\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"my-class\">\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"style tag with only whitespace produces no html embed\", () => {\n    expect(\n      generateFragmentFromHtml(`\n        <style>   </style>\n        <div>Hello</div>\n      `)\n    ).toEqual(renderTemplate(<ws.element ws:tag=\"div\">Hello</ws.element>));\n  });\n\n  test(\"extract token and apply to nested structure\", () => {\n    const wrapperToken = token(\n      \"wrapper\",\n      css`\n        max-width: 1200px;\n        margin-left: auto;\n        margin-right: auto;\n      `\n    );\n    const gridToken = token(\n      \"grid\",\n      css`\n        display: grid;\n        gap: 16px;\n      `\n    );\n    const itemToken = token(\n      \"item\",\n      css`\n        padding: 8px;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>\n          .wrapper { max-width: 1200px; margin-left: auto; margin-right: auto; }\n          .grid { display: grid; gap: 16px; }\n          .item { padding: 8px; }\n        </style>\n        <div class=\"wrapper\">\n          <div class=\"grid\">\n            <div class=\"item\">One</div>\n            <div class=\"item\">Two</div>\n            <div class=\"item\">Three</div>\n          </div>\n        </div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" ws:tokens={[wrapperToken]}>\n          <ws.element ws:tag=\"div\" ws:tokens={[gridToken]}>\n            <ws.element ws:tag=\"div\" ws:tokens={[itemToken]}>\n              One\n            </ws.element>\n            <ws.element ws:tag=\"div\" ws:tokens={[itemToken]}>\n              Two\n            </ws.element>\n            <ws.element ws:tag=\"div\" ws:tokens={[itemToken]}>\n              Three\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"element with both resolved and unresolved classes plus inline style\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <div class=\"card external\" style=\"color: red\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          class=\"external\"\n          ws:style={css`\n            color: red;\n          `}\n          ws:tokens={[cardToken]}\n        >\n          Hello\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"token names preserve original class names\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>\n        .my-component { display: flex; }\n        .text-lg { font-size: 18px; }\n        .bg_primary { background-color: blue; }\n      </style>\n      <div class=\"my-component\">\n        <span class=\"text-lg bg_primary\">Hello</span>\n      </div>\n    `);\n    const tokenNames = fragment.styleSources\n      .filter((s) => s.type === \"token\")\n      .map((s) => (s as { name: string }).name);\n    expect(tokenNames).toContain(\"my-component\");\n    expect(tokenNames).toContain(\"text-lg\");\n    expect(tokenNames).toContain(\"bg_primary\");\n  });\n\n  test(\"class defined in style tag but not used by any element\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>\n        .used { display: flex; }\n        .unused { color: red; }\n      </style>\n      <div class=\"used\">Hello</div>\n    `);\n    // all class rules become tokens, even if not referenced by elements\n    const tokenNames = fragment.styleSources\n      .filter((s) => s.type === \"token\")\n      .map((s) => (s as { name: string }).name);\n    expect(tokenNames).toContain(\"used\");\n    expect(tokenNames).toContain(\"unused\");\n    // only \"used\" is assigned to the instance\n    const divInstance = fragment.instances.find((i) => i.tag === \"div\");\n    const sel = fragment.styleSourceSelections.find(\n      (s) => s.instanceId === divInstance?.id\n    );\n    const usedToken = fragment.styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"used\"\n    );\n    const unusedToken = fragment.styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"unused\"\n    );\n    expect(sel?.values).toContain(usedToken?.id);\n    expect(sel?.values).not.toContain(unusedToken?.id);\n    // no HtmlEmbed since all rules are class rules\n    expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n      false\n    );\n  });\n\n  test(\"duplicate class rules across multiple style tags are merged\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>.card { display: flex; }</style>\n      <style>.card { padding: 16px; }</style>\n      <div class=\"card\">Hello</div>\n    `);\n    // should create a single token with both properties\n    const tokens = fragment.styleSources.filter((s) => s.type === \"token\");\n    expect(tokens).toHaveLength(1);\n    const tokenId = tokens[0].id;\n    const tokenStyles = fragment.styles.filter(\n      (s) => s.styleSourceId === tokenId\n    );\n    const properties = tokenStyles.map((s) => s.property);\n    expect(properties).toContain(\"display\");\n    expect(properties).toContain(\"paddingTop\");\n  });\n\n  test(\"script tag still becomes html embed alongside token extraction\", () => {\n    const cardToken = token(\n      \"card\",\n      css`\n        display: flex;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>.card { display: flex; }</style>\n        <script>console.log(\"hi\");</script>\n        <div class=\"card\">Hello</div>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <>\n          <$.HtmlEmbed\n            ws:label=\"Script\"\n            clientOnly={true}\n            code={`<script>console.log(\"hi\");</script>`}\n          />\n          <ws.element ws:tag=\"div\" ws:tokens={[cardToken]}>\n            Hello\n          </ws.element>\n        </>\n      )\n    );\n  });\n\n  test(\"style tag with pseudo-class creates token with state\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>\n        .btn { background-color: blue; }\n        .btn:hover { background-color: darkblue; }\n      </style>\n      <button class=\"btn\">Click me</button>\n    `);\n    const tokenSource = fragment.styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"btn\"\n    );\n    expect(tokenSource).toBeDefined();\n    // base style\n    const baseStyle = fragment.styles.find(\n      (s) =>\n        s.styleSourceId === tokenSource!.id &&\n        s.property === \"backgroundColor\" &&\n        s.state === undefined\n    );\n    expect(baseStyle).toBeDefined();\n    // hover state style\n    const hoverStyle = fragment.styles.find(\n      (s) =>\n        s.styleSourceId === tokenSource!.id &&\n        s.property === \"backgroundColor\" &&\n        s.state === \":hover\"\n    );\n    expect(hoverStyle).toBeDefined();\n  });\n\n  test(\"style tag with media query creates token with breakpoint styles\", () => {\n    const fragment = generateFragmentFromHtml(`\n      <style>\n        .container { padding: 8px; }\n        @media (min-width: 768px) {\n          .container { padding: 16px; }\n        }\n      </style>\n      <div class=\"container\">Hello</div>\n    `);\n    const tokenSource = fragment.styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"container\"\n    );\n    expect(tokenSource).toBeDefined();\n    // should have styles on different breakpoints\n    const tokenStyles = fragment.styles.filter(\n      (s) => s.styleSourceId === tokenSource!.id\n    );\n    const breakpointIds = new Set(tokenStyles.map((s) => s.breakpointId));\n    // should have at least 2 different breakpoints (base + media query)\n    expect(breakpointIds.size).toBeGreaterThanOrEqual(2);\n  });\n\n  test(\"handle real-world html snippet with style tag\", () => {\n    const heroToken = token(\n      \"hero\",\n      css`\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        padding: 64px;\n      `\n    );\n    const heroTitleToken = token(\n      \"hero-title\",\n      css`\n        font-size: 48px;\n        font-weight: bold;\n        line-height: 1.2;\n      `\n    );\n    const heroTextToken = token(\n      \"hero-text\",\n      css`\n        font-size: 18px;\n        color: gray;\n      `\n    );\n    expect(\n      generateFragmentFromHtml(`\n        <style>\n          .hero {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            padding: 64px;\n          }\n          .hero-title {\n            font-size: 48px;\n            font-weight: bold;\n            line-height: 1.2;\n          }\n          .hero-text {\n            font-size: 18px;\n            color: gray;\n          }\n        </style>\n        <section class=\"hero\">\n          <h1 class=\"hero-title\">Welcome</h1>\n          <p class=\"hero-text\">Build something amazing</p>\n        </section>\n      `)\n    ).toEqual(\n      renderTemplate(\n        <ws.element ws:tag=\"section\" ws:tokens={[heroToken]}>\n          <ws.element ws:tag=\"h1\" ws:tokens={[heroTitleToken]}>\n            Welcome\n          </ws.element>\n          <ws.element ws:tag=\"p\" ws:tokens={[heroTextToken]}>\n            Build something amazing\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"create tokens from style-only paste without HTML elements\", () => {\n    const result = generateFragmentFromHtml(`\n      <style>.card { display: flex; padding: 16px; }</style>\n    `);\n    expect(result.children).toEqual([]);\n    expect(result.instances).toEqual([]);\n    expect(result.styleSources).toEqual([\n      { type: \"token\", id: expect.any(String), name: \"card\" },\n    ]);\n    expect(result.styles).toEqual([\n      {\n        styleSourceId: result.styleSources[0].id,\n        breakpointId: expect.any(String),\n        property: \"display\",\n        value: { type: \"keyword\", value: \"flex\" },\n      },\n      {\n        styleSourceId: result.styleSources[0].id,\n        breakpointId: expect.any(String),\n        property: \"paddingTop\",\n        value: { type: \"unit\", unit: \"px\", value: 16 },\n      },\n      {\n        styleSourceId: result.styleSources[0].id,\n        breakpointId: expect.any(String),\n        property: \"paddingRight\",\n        value: { type: \"unit\", unit: \"px\", value: 16 },\n      },\n      {\n        styleSourceId: result.styleSources[0].id,\n        breakpointId: expect.any(String),\n        property: \"paddingBottom\",\n        value: { type: \"unit\", unit: \"px\", value: 16 },\n      },\n      {\n        styleSourceId: result.styleSources[0].id,\n        breakpointId: expect.any(String),\n        property: \"paddingLeft\",\n        value: { type: \"unit\", unit: \"px\", value: 16 },\n      },\n    ]);\n    expect(result.styleSourceSelections).toEqual([]);\n  });\n\n  test(\"create multiple tokens from style-only paste\", () => {\n    const result = generateFragmentFromHtml(`\n      <style>\n        .card { display: flex; }\n        .title { font-weight: bold; }\n      </style>\n    `);\n    expect(result.children).toEqual([]);\n    expect(result.styleSources).toEqual([\n      { type: \"token\", id: expect.any(String), name: \"card\" },\n      { type: \"token\", id: expect.any(String), name: \"title\" },\n    ]);\n    expect(result.styles).toHaveLength(2);\n  });\n\n  test(\"create tokens with media queries from style-only paste\", () => {\n    const result = generateFragmentFromHtml(`\n      <style>\n        .card { display: flex; }\n        @media (min-width: 768px) {\n          .card { flex-direction: row; }\n        }\n      </style>\n    `);\n    expect(result.children).toEqual([]);\n    expect(result.styleSources).toEqual([\n      { type: \"token\", id: expect.any(String), name: \"card\" },\n    ]);\n    // base style + breakpoint style\n    expect(result.styles).toHaveLength(2);\n    expect(result.breakpoints).toHaveLength(2); // base + 768px\n  });\n\n  test(\"style-only paste with non-class rules creates leftover embed\", () => {\n    const result = generateFragmentFromHtml(`\n      <style>\n        .card { display: flex; }\n        div { color: red; }\n      </style>\n    `);\n    // card token created\n    expect(result.styleSources).toEqual([\n      { type: \"token\", id: expect.any(String), name: \"card\" },\n    ]);\n    // leftover non-class rule kept as HtmlEmbed\n    expect(result.instances).toHaveLength(1);\n    expect(result.instances[0].component).toBe(\"HtmlEmbed\");\n  });\n\n  describe(\"media queries\", () => {\n    test(\"extract class with min-width media query into breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          @media (min-width: 768px) {\n            .card { flex-direction: row; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // base style\n      const baseStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"display\" &&\n          s.state === undefined\n      );\n      expect(baseStyle).toBeDefined();\n      // breakpoint style\n      const bpStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id && s.property === \"flexDirection\"\n      );\n      expect(bpStyle).toBeDefined();\n      expect(bpStyle!.breakpointId).not.toBe(baseStyle!.breakpointId);\n      // breakpoint should be created with minWidth\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === bpStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.minWidth).toBe(768);\n    });\n\n    test(\"extract class with max-width media query into breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .container { padding: 32px; }\n          @media (max-width: 480px) {\n            .container { padding: 16px; }\n          }\n        </style>\n        <div class=\"container\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"container\"\n      );\n      expect(tokenSource).toBeDefined();\n      const bpStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"paddingTop\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(bpStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === bpStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.maxWidth).toBe(480);\n    });\n\n    test(\"extract class across multiple media queries\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { font-size: 14px; }\n          @media (min-width: 768px) {\n            .card { font-size: 16px; }\n          }\n          @media (min-width: 1024px) {\n            .card { font-size: 18px; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      const tokenStyles = fragment.styles.filter(\n        (s) => s.styleSourceId === tokenSource!.id\n      );\n      const breakpointIds = new Set(tokenStyles.map((s) => s.breakpointId));\n      // base + 768px + 1024px = 3 breakpoints\n      expect(breakpointIds.size).toBe(3);\n    });\n\n    test(\"media query with non-class selectors stays as html embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @media (min-width: 768px) {\n            #hero { font-size: 24px; }\n          }\n        </style>\n        <div>Hello</div>\n      `);\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        true\n      );\n      expect(\n        fragment.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n    });\n\n    test(\"media query with mixed class and non-class selectors\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @media (min-width: 768px) {\n            .card { display: flex; }\n            #hero { color: red; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      // .card in media query should become token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // #hero in media query should remain as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n    });\n\n    test(\"same class in base and media query produces single token\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn { padding: 8px; }\n          @media (min-width: 768px) {\n            .btn { padding: 16px; }\n          }\n        </style>\n        <button class=\"btn\">Click</button>\n      `);\n      const tokens = fragment.styleSources.filter((s) => s.type === \"token\");\n      expect(tokens).toHaveLength(1);\n      expect(tokens[0].type === \"token\" && tokens[0].name).toBe(\"btn\");\n    });\n\n    test(\"multiple classes in same media query\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          .title { font-size: 16px; }\n          @media (min-width: 768px) {\n            .card { flex-direction: row; }\n            .title { font-size: 24px; }\n          }\n        </style>\n        <div class=\"card\">\n          <h1 class=\"title\">Hello</h1>\n        </div>\n      `);\n      const tokens = fragment.styleSources.filter((s) => s.type === \"token\");\n      expect(tokens).toHaveLength(2);\n      const cardToken = tokens.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      const titleToken = tokens.find(\n        (s) => s.type === \"token\" && s.name === \"title\"\n      );\n      expect(cardToken).toBeDefined();\n      expect(titleToken).toBeDefined();\n    });\n  });\n\n  describe(\"pseudo-classes and pseudo-elements\", () => {\n    test(\"extract :hover pseudo-class as state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .link { color: blue; }\n          .link:hover { color: darkblue; }\n        </style>\n        <a class=\"link\">Click</a>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"link\"\n      );\n      expect(tokenSource).toBeDefined();\n      const hoverStyle = fragment.styles.find(\n        (s) => s.styleSourceId === tokenSource!.id && s.state === \":hover\"\n      );\n      expect(hoverStyle).toBeDefined();\n    });\n\n    test(\"extract :focus pseudo-class as state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .input { border-color: gray; }\n          .input:focus { border-color: blue; }\n        </style>\n        <input class=\"input\" />\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"input\"\n      );\n      expect(tokenSource).toBeDefined();\n      const focusStyle = fragment.styles.find(\n        (s) => s.styleSourceId === tokenSource!.id && s.state === \":focus\"\n      );\n      expect(focusStyle).toBeDefined();\n    });\n\n    test(\"extract multiple pseudo-classes on same class\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn { background: blue; }\n          .btn:hover { background: darkblue; }\n          .btn:focus { outline-color: blue; }\n          .btn:active { background: navy; }\n        </style>\n        <button class=\"btn\">Click</button>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"btn\"\n      );\n      expect(tokenSource).toBeDefined();\n      const states = fragment.styles\n        .filter((s) => s.styleSourceId === tokenSource!.id && s.state)\n        .map((s) => s.state);\n      expect(states).toContain(\":hover\");\n      expect(states).toContain(\":focus\");\n      expect(states).toContain(\":active\");\n    });\n\n    test(\"extract ::before pseudo-element as state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .decorated { position: relative; }\n          .decorated::before { content: \"★\"; }\n        </style>\n        <div class=\"decorated\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"decorated\"\n      );\n      expect(tokenSource).toBeDefined();\n      const beforeStyle = fragment.styles.find(\n        (s) => s.styleSourceId === tokenSource!.id && s.state === \"::before\"\n      );\n      expect(beforeStyle).toBeDefined();\n    });\n\n    test(\"extract ::after pseudo-element as state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .clearfix::after {\n            content: \"\";\n            display: table;\n            clear: both;\n          }\n        </style>\n        <div class=\"clearfix\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"clearfix\"\n      );\n      expect(tokenSource).toBeDefined();\n      const afterStyles = fragment.styles.filter(\n        (s) => s.styleSourceId === tokenSource!.id && s.state === \"::after\"\n      );\n      expect(afterStyles.length).toBeGreaterThanOrEqual(1);\n    });\n\n    test(\"pseudo-class on non-class selector ignored (stays in embed)\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          a:hover { color: red; }\n        </style>\n        <a>Link</a>\n      `);\n      expect(\n        fragment.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        true\n      );\n    });\n\n    test(\"pseudo-class combined with media query\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn { background: blue; }\n          .btn:hover { background: darkblue; }\n          @media (min-width: 768px) {\n            .btn { padding: 16px; }\n            .btn:hover { background: royalblue; }\n          }\n        </style>\n        <button class=\"btn\">Click</button>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"btn\"\n      );\n      expect(tokenSource).toBeDefined();\n      // should have hover styles on base breakpoint\n      const baseHover = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.state === \":hover\" &&\n          s.breakpointId === \"base\"\n      );\n      expect(baseHover).toBeDefined();\n      // should have hover styles on 768px breakpoint\n      const bpHover = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.state === \":hover\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(bpHover).toBeDefined();\n    });\n\n    test(\"::placeholder pseudo-element as state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .input { color: black; }\n          .input::placeholder { color: gray; }\n        </style>\n        <input class=\"input\" />\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"input\"\n      );\n      expect(tokenSource).toBeDefined();\n      const placeholderStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id && s.state === \"::placeholder\"\n      );\n      expect(placeholderStyle).toBeDefined();\n    });\n  });\n\n  describe(\"at-rules and special CSS\", () => {\n    test(\"@keyframes stays as html embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @keyframes spin {\n            from { transform: rotate(0deg); }\n            to { transform: rotate(360deg); }\n          }\n          .spinner { animation: spin 1s infinite; }\n        </style>\n        <div class=\"spinner\">Loading</div>\n      `);\n      // .spinner class should still become a token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"spinner\"\n      );\n      expect(tokenSource).toBeDefined();\n      // @keyframes should remain as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n      const codeProp = fragment.props.find(\n        (p) => p.instanceId === htmlEmbed!.id && p.name === \"code\"\n      );\n      expect(codeProp).toBeDefined();\n      expect(codeProp!.type === \"string\" && codeProp!.value).toContain(\n        \"@keyframes\"\n      );\n    });\n\n    test(\"@font-face stays as html embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @font-face {\n            font-family: \"MyFont\";\n            src: url(\"font.woff2\");\n          }\n          .text { font-family: \"MyFont\"; }\n        </style>\n        <p class=\"text\">Hello</p>\n      `);\n      // .text class should become a token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"text\"\n      );\n      expect(tokenSource).toBeDefined();\n      // @font-face should remain as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n      const codeProp = fragment.props.find(\n        (p) => p.instanceId === htmlEmbed!.id && p.name === \"code\"\n      );\n      expect(codeProp).toBeDefined();\n      expect(codeProp!.type === \"string\" && codeProp!.value).toContain(\n        \"@font-face\"\n      );\n    });\n\n    test(\"@media print stays as html embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          @media print {\n            .card { display: block; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      // base .card should become a token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // @media print should remain as html embed since parseCss ignores print\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n    });\n\n    test(\"grouped class selectors like .a, .b are extracted as separate tokens\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .primary, .secondary { font-weight: bold; }\n        </style>\n        <span class=\"primary\">One</span>\n        <span class=\"secondary\">Two</span>\n      `);\n      const primaryToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"primary\"\n      );\n      const secondaryToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"secondary\"\n      );\n      expect(primaryToken).toBeDefined();\n      expect(secondaryToken).toBeDefined();\n      // both should have the same style\n      const primaryStyle = fragment.styles.find(\n        (s) => s.styleSourceId === primaryToken!.id\n      );\n      const secondaryStyle = fragment.styles.find(\n        (s) => s.styleSourceId === secondaryToken!.id\n      );\n      expect(primaryStyle?.property).toBe(\"fontWeight\");\n      expect(secondaryStyle?.property).toBe(\"fontWeight\");\n    });\n\n    test(\"grouped selector with class + non-class stays partially in embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card, #hero { display: flex; }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      // .card should become token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // #hero part should remain as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n    });\n  });\n\n  describe(\"condition breakpoints\", () => {\n    test(\"@media (prefers-color-scheme: dark) creates condition breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { background: white; color: black; }\n          @media (prefers-color-scheme: dark) {\n            .card { background: black; color: white; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // should have base styles\n      const baseStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"backgroundColor\" &&\n          s.state === undefined\n      );\n      expect(baseStyle).toBeDefined();\n      // should have dark mode styles on condition breakpoint\n      const darkStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"backgroundColor\" &&\n          s.breakpointId !== baseStyle!.breakpointId\n      );\n      expect(darkStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === darkStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.condition).toBe(\"prefers-color-scheme:dark\");\n      expect(bp!.minWidth).toBeUndefined();\n      expect(bp!.maxWidth).toBeUndefined();\n    });\n\n    test(\"@media (orientation: portrait) creates condition breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .layout { flex-direction: row; }\n          @media (orientation: portrait) {\n            .layout { flex-direction: column; }\n          }\n        </style>\n        <div class=\"layout\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"layout\"\n      );\n      expect(tokenSource).toBeDefined();\n      const portraitStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"flexDirection\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(portraitStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === portraitStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.condition).toBe(\"orientation:portrait\");\n    });\n\n    test(\"@media (hover: hover) creates condition breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .interactive { cursor: default; }\n          @media (hover: hover) {\n            .interactive { cursor: pointer; }\n          }\n        </style>\n        <div class=\"interactive\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"interactive\"\n      );\n      expect(tokenSource).toBeDefined();\n      const hoverStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"cursor\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(hoverStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === hoverStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.condition).toBe(\"hover:hover\");\n    });\n\n    test(\"@media (prefers-reduced-motion: reduce) creates condition breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .animated { transition: transform 0.3s; }\n          @media (prefers-reduced-motion: reduce) {\n            .animated { transition: none; }\n          }\n        </style>\n        <div class=\"animated\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"animated\"\n      );\n      expect(tokenSource).toBeDefined();\n      const reduceStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"transitionProperty\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(reduceStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === reduceStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.condition).toBe(\"prefers-reduced-motion:reduce\");\n    });\n\n    test(\"combined min-width and max-width uses range breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .tablet-only { display: none; }\n          @media (min-width: 768px) and (max-width: 1024px) {\n            .tablet-only { display: block; }\n          }\n        </style>\n        <div class=\"tablet-only\">Tablet content</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"tablet-only\"\n      );\n      expect(tokenSource).toBeDefined();\n      const tabletStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"display\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(tabletStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === tabletStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      // Combined width queries use both minWidth and maxWidth\n      expect(bp!.minWidth).toBe(768);\n      expect(bp!.maxWidth).toBe(1024);\n      expect(bp!.condition).toBeUndefined();\n    });\n\n    test(\"overlapping min/max-width breakpoints get rounded to non-overlapping values\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 600px) and (max-width: 900px) {\n            .a { font-size: 16px; }\n          }\n          @media (min-width: 800px) and (max-width: 1200px) {\n            .a { font-size: 18px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"a\"\n      );\n      expect(tokenSource).toBeDefined();\n      const tokenStyles = fragment.styles.filter(\n        (s) => s.styleSourceId === tokenSource!.id && s.property === \"fontSize\"\n      );\n      // base + 2 range breakpoints = 3 styles\n      expect(tokenStyles).toHaveLength(3);\n      const rangeBreakpoints = tokenStyles\n        .filter((s) => s.breakpointId !== \"base\")\n        .map((s) => fragment.breakpoints.find((b) => b.id === s.breakpointId))\n        .filter(Boolean)\n        .sort((a, b) => (a!.minWidth ?? 0) - (b!.minWidth ?? 0));\n      expect(rangeBreakpoints).toHaveLength(2);\n      // ranges should not overlap: first.maxWidth < second.minWidth\n      const first = rangeBreakpoints[0]!;\n      const second = rangeBreakpoints[1]!;\n      expect(first.maxWidth).toBeDefined();\n      expect(second.minWidth).toBeDefined();\n      expect(first.maxWidth!).toBeLessThan(second.minWidth!);\n    });\n\n    test(\"non-overlapping range breakpoints stay unchanged\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 400px) and (max-width: 700px) {\n            .a { font-size: 16px; }\n          }\n          @media (min-width: 800px) and (max-width: 1200px) {\n            .a { font-size: 18px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"a\"\n      );\n      expect(tokenSource).toBeDefined();\n      const rangeBreakpoints = fragment.breakpoints\n        .filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)\n        .sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));\n      expect(rangeBreakpoints).toHaveLength(2);\n      // values should remain exactly as authored since they don't overlap\n      expect(rangeBreakpoints[0].minWidth).toBe(400);\n      expect(rangeBreakpoints[0].maxWidth).toBe(700);\n      expect(rangeBreakpoints[1].minWidth).toBe(800);\n      expect(rangeBreakpoints[1].maxWidth).toBe(1200);\n    });\n\n    test(\"adjacent range breakpoints (touching) stay unchanged\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 400px) and (max-width: 799px) {\n            .a { font-size: 16px; }\n          }\n          @media (min-width: 800px) and (max-width: 1200px) {\n            .a { font-size: 18px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const rangeBreakpoints = fragment.breakpoints\n        .filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)\n        .sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));\n      expect(rangeBreakpoints).toHaveLength(2);\n      // touching but not overlapping — keep as is\n      expect(rangeBreakpoints[0].maxWidth).toBe(799);\n      expect(rangeBreakpoints[1].minWidth).toBe(800);\n    });\n\n    test(\"overlapping range: first.maxWidth rounded down to second.minWidth - 1\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 600px) and (max-width: 1000px) {\n            .a { font-size: 16px; }\n          }\n          @media (min-width: 900px) and (max-width: 1400px) {\n            .a { font-size: 18px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const rangeBreakpoints = fragment.breakpoints\n        .filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)\n        .sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));\n      expect(rangeBreakpoints).toHaveLength(2);\n      // first range's maxWidth should be adjusted to not overlap second's minWidth\n      expect(rangeBreakpoints[0].minWidth).toBe(600);\n      expect(rangeBreakpoints[0].maxWidth!).toBeLessThan(\n        rangeBreakpoints[1].minWidth!\n      );\n      // second range should keep its original values\n      expect(rangeBreakpoints[1].minWidth).toBe(900);\n      expect(rangeBreakpoints[1].maxWidth).toBe(1400);\n    });\n\n    test(\"three overlapping ranges all get adjusted\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 400px) and (max-width: 800px) {\n            .a { font-size: 16px; }\n          }\n          @media (min-width: 700px) and (max-width: 1100px) {\n            .a { font-size: 18px; }\n          }\n          @media (min-width: 1000px) and (max-width: 1400px) {\n            .a { font-size: 20px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const rangeBreakpoints = fragment.breakpoints\n        .filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)\n        .sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));\n      expect(rangeBreakpoints).toHaveLength(3);\n      // no pair should overlap\n      expect(rangeBreakpoints[0].maxWidth!).toBeLessThan(\n        rangeBreakpoints[1].minWidth!\n      );\n      expect(rangeBreakpoints[1].maxWidth!).toBeLessThan(\n        rangeBreakpoints[2].minWidth!\n      );\n    });\n\n    test(\"exact duplicate range breakpoints get deduplicated\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          .b { color: red; }\n          @media (min-width: 768px) and (max-width: 1024px) {\n            .a { font-size: 18px; }\n          }\n          @media (min-width: 768px) and (max-width: 1024px) {\n            .b { color: blue; }\n          }\n        </style>\n        <div class=\"a\"><span class=\"b\">Hello</span></div>\n      `);\n      // same range should reuse a single breakpoint\n      const rangeBreakpoints = fragment.breakpoints.filter(\n        (b) => b.minWidth !== undefined && b.maxWidth !== undefined\n      );\n      expect(rangeBreakpoints).toHaveLength(1);\n      expect(rangeBreakpoints[0].minWidth).toBe(768);\n      expect(rangeBreakpoints[0].maxWidth).toBe(1024);\n    });\n\n    test(\"range breakpoint overlapping with simple min-width breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 768px) {\n            .a { font-size: 18px; }\n          }\n          @media (min-width: 600px) and (max-width: 900px) {\n            .a { font-size: 16px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"a\"\n      );\n      expect(tokenSource).toBeDefined();\n      const tokenStyles = fragment.styles.filter(\n        (s) => s.styleSourceId === tokenSource!.id && s.property === \"fontSize\"\n      );\n      // base + min-width + range = 3 breakpoints\n      expect(tokenStyles).toHaveLength(3);\n      // simple min-width breakpoint should exist\n      const minWidthBp = fragment.breakpoints.find(\n        (b) =>\n          b.minWidth !== undefined &&\n          b.maxWidth === undefined &&\n          b.condition === undefined\n      );\n      expect(minWidthBp).toBeDefined();\n      // range breakpoint should exist and not overlap min-width\n      const rangeBp = fragment.breakpoints.find(\n        (b) => b.minWidth !== undefined && b.maxWidth !== undefined\n      );\n      expect(rangeBp).toBeDefined();\n      // range's maxWidth must be less than the min-width breakpoint's minWidth\n      // so they don't overlap\n      expect(rangeBp!.maxWidth!).toBeLessThan(minWidthBp!.minWidth!);\n    });\n\n    test(\"range breakpoint overlapping with simple max-width breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (max-width: 600px) {\n            .a { font-size: 12px; }\n          }\n          @media (min-width: 400px) and (max-width: 900px) {\n            .a { font-size: 16px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"a\"\n      );\n      expect(tokenSource).toBeDefined();\n      const tokenStyles = fragment.styles.filter(\n        (s) => s.styleSourceId === tokenSource!.id && s.property === \"fontSize\"\n      );\n      // base + max-width + range = 3 breakpoints\n      expect(tokenStyles).toHaveLength(3);\n      // simple max-width breakpoint should exist\n      const maxWidthBp = fragment.breakpoints.find(\n        (b) =>\n          b.maxWidth !== undefined &&\n          b.minWidth === undefined &&\n          b.condition === undefined\n      );\n      expect(maxWidthBp).toBeDefined();\n      // range breakpoint should exist and not overlap max-width\n      const rangeBp = fragment.breakpoints.find(\n        (b) => b.minWidth !== undefined && b.maxWidth !== undefined\n      );\n      expect(rangeBp).toBeDefined();\n      // range's minWidth must be greater than the max-width breakpoint's maxWidth\n      expect(rangeBp!.minWidth!).toBeGreaterThan(maxWidthBp!.maxWidth!);\n    });\n\n    test(\"completely contained range gets clamped\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a { font-size: 14px; }\n          @media (min-width: 400px) and (max-width: 1200px) {\n            .a { font-size: 16px; }\n          }\n          @media (min-width: 600px) and (max-width: 900px) {\n            .a { font-size: 18px; }\n          }\n        </style>\n        <div class=\"a\">Hello</div>\n      `);\n      const rangeBreakpoints = fragment.breakpoints\n        .filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)\n        .sort((a, b) => (a.minWidth ?? 0) - (b.minWidth ?? 0));\n      expect(rangeBreakpoints).toHaveLength(2);\n      // the outer range should be split/adjusted so inner doesn't overlap\n      expect(rangeBreakpoints[0].maxWidth!).toBeLessThan(\n        rangeBreakpoints[1].minWidth!\n      );\n    });\n\n    test(\"combined width and feature uses condition breakpoint\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .widget { font-size: 14px; }\n          @media (min-width: 768px) and (orientation: landscape) {\n            .widget { font-size: 18px; }\n          }\n        </style>\n        <div class=\"widget\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"widget\"\n      );\n      expect(tokenSource).toBeDefined();\n      const landscapeStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"fontSize\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(landscapeStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === landscapeStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      // Mixed width + feature query becomes a condition\n      expect(bp!.condition).toBeDefined();\n    });\n\n    test(\"multiple condition features combined\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .accessible { font-size: 16px; }\n          @media (prefers-color-scheme: dark) and (prefers-contrast: more) {\n            .accessible { font-size: 18px; }\n          }\n        </style>\n        <div class=\"accessible\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"accessible\"\n      );\n      expect(tokenSource).toBeDefined();\n      const conditionStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"fontSize\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(conditionStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === conditionStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.condition).toBeDefined();\n      // condition should contain both features\n      expect(bp!.condition).toContain(\"prefers-color-scheme\");\n      expect(bp!.condition).toContain(\"prefers-contrast\");\n    });\n\n    test(\"same condition breakpoint reused for multiple classes\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { background: white; }\n          .text { color: black; }\n          @media (prefers-color-scheme: dark) {\n            .card { background: black; }\n            .text { color: white; }\n          }\n        </style>\n        <div class=\"card\">\n          <p class=\"text\">Hello</p>\n        </div>\n      `);\n      const cardToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      const textToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"text\"\n      );\n      expect(cardToken).toBeDefined();\n      expect(textToken).toBeDefined();\n      // both dark styles should reference the same breakpoint\n      const cardDarkStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === cardToken!.id &&\n          s.property === \"backgroundColor\" &&\n          s.breakpointId !== \"base\"\n      );\n      const textDarkStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === textToken!.id &&\n          s.property === \"color\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(cardDarkStyle).toBeDefined();\n      expect(textDarkStyle).toBeDefined();\n      expect(cardDarkStyle!.breakpointId).toBe(textDarkStyle!.breakpointId);\n    });\n  });\n\n  describe(\"nested at-rules\", () => {\n    test(\"nested @media rules extract class with flattened condition\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          @media (min-width: 768px) {\n            @media (prefers-color-scheme: dark) {\n              .card { background: black; }\n            }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // nested @media should flatten into a combined condition\n      const nestedStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"backgroundColor\"\n      );\n      expect(nestedStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === nestedStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      // flattened condition should include both queries\n      expect(bp!.condition).toBeDefined();\n    });\n\n    test(\"deeply nested @media still extracts class tokens\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { padding: 8px; }\n          @media (min-width: 768px) {\n            @media (orientation: landscape) {\n              @media (hover: hover) {\n                .card { padding: 32px; }\n              }\n            }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      const deepStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"paddingTop\" &&\n          s.breakpointId !== \"base\"\n      );\n      expect(deepStyle).toBeDefined();\n      const bp = fragment.breakpoints.find(\n        (b) => b.id === deepStyle!.breakpointId\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.condition).toBeDefined();\n    });\n\n    test(\"nested @media with same feature type combines correctly\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .responsive { font-size: 14px; }\n          @media (min-width: 768px) {\n            .responsive { font-size: 16px; }\n            @media (min-width: 1024px) {\n              .responsive { font-size: 20px; }\n            }\n          }\n        </style>\n        <div class=\"responsive\">Hello</div>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"responsive\"\n      );\n      expect(tokenSource).toBeDefined();\n      // outer @media (min-width: 768px) should have simple breakpoint\n      const outerStyle = fragment.styles.find(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"fontSize\" &&\n          s.breakpointId !== \"base\" &&\n          fragment.breakpoints.find(\n            (b) => b.id === s.breakpointId && b.minWidth === 768\n          )\n      );\n      expect(outerStyle).toBeDefined();\n      // nested @media (min-width: 1024px) inside @media (min-width: 768px)\n      // should still produce a breakpoint (the effective min-width is 1024px)\n      const innerStyles = fragment.styles.filter(\n        (s) =>\n          s.styleSourceId === tokenSource!.id &&\n          s.property === \"fontSize\" &&\n          s.breakpointId !== \"base\" &&\n          s.breakpointId !== outerStyle!.breakpointId\n      );\n      expect(innerStyles).toHaveLength(1);\n    });\n\n    test(\"@supports nested inside @media keeps class in embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .grid { display: flex; }\n          @supports (display: grid) {\n            .grid { display: grid; }\n          }\n        </style>\n        <div class=\"grid\">Hello</div>\n      `);\n      // base class should become token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"grid\"\n      );\n      expect(tokenSource).toBeDefined();\n      // @supports should remain as html embed (not a media query)\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n    });\n  });\n\n  describe(\"selector edge cases\", () => {\n    test(\"class with compound selector .card.active is extracted as combo token\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card.active { opacity: 1; }\n        </style>\n        <div class=\"card active\">Hello</div>\n      `);\n      // compound selectors become combo tokens (Webflow-style naming)\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card.active\"\n      );\n      expect(tokenSource).toBeDefined();\n      // no leftover for pure class rule\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        false\n      );\n    });\n\n    test(\"class with attribute selector .btn[disabled] is extracted with state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn[disabled] { opacity: 0.5; }\n        </style>\n        <button class=\"btn\" disabled>Click</button>\n      `);\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"btn\"\n      );\n      expect(tokenSource).toBeDefined();\n      // the style should have [disabled] state\n      const style = fragment.styles.find(\n        (s) => s.styleSourceId === tokenSource!.id\n      );\n      expect(style).toBeDefined();\n      expect(style!.state).toBe(\"[disabled]\");\n      // no leftover\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        false\n      );\n    });\n\n    test(\"class with sibling combinator .a + .b is not extracted\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a + .b { margin-top: 8px; }\n        </style>\n        <div class=\"a\">A</div>\n        <div class=\"b\">B</div>\n      `);\n      expect(\n        fragment.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n    });\n\n    test(\"class with general sibling .a ~ .b is not extracted\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a ~ .b { color: red; }\n        </style>\n        <div class=\"a\">A</div>\n        <div class=\"b\">B</div>\n      `);\n      expect(\n        fragment.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n    });\n\n    test(\":root selector stays as html embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          :root { --color: blue; }\n          .card { color: var(--color); }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      // .card should become token\n      const tokenSource = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(tokenSource).toBeDefined();\n      // :root should stay as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n    });\n\n    test(\"universal selector * stays as html embed\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          * { box-sizing: border-box; }\n        </style>\n        <div>Hello</div>\n      `);\n      expect(\n        fragment.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        true\n      );\n    });\n  });\n\n  describe(\"compound class selectors\", () => {\n    test(\"compound .card.active extracts as combo token when element has both classes\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          .card.active { opacity: 1; }\n        </style>\n        <div class=\"card active\">Hello</div>\n      `);\n      const cardToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      const comboToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card.active\"\n      );\n      expect(cardToken).toBeDefined();\n      expect(comboToken).toBeDefined();\n      // instance should reference both tokens\n      const sel = fragment.styleSourceSelections.find(\n        (s) =>\n          s.values.includes(cardToken!.id) && s.values.includes(comboToken!.id)\n      );\n      expect(sel).toBeDefined();\n    });\n\n    test(\"compound .a.b.c extracts as triple combo token\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a.b.c { color: red; }\n        </style>\n        <div class=\"a b c\">Hello</div>\n      `);\n      const comboToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"a.b.c\"\n      );\n      expect(comboToken).toBeDefined();\n      const style = fragment.styles.find(\n        (s) => s.styleSourceId === comboToken!.id && s.property === \"color\"\n      );\n      expect(style).toBeDefined();\n    });\n\n    test(\"compound class not matched when element is missing a class\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card.active { opacity: 1; }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      // token is created for all class rules, even if not applied to elements\n      const comboToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card.active\"\n      );\n      expect(comboToken).toBeDefined();\n      // but NOT assigned to the div (missing \"active\" class)\n      const divInstance = fragment.instances.find((i) => i.tag === \"div\");\n      const sel = fragment.styleSourceSelections.find(\n        (s) => s.instanceId === divInstance?.id\n      );\n      // No selection for this instance, or if one exists, it shouldn't contain the combo token\n      expect(sel?.values ?? []).not.toContain(comboToken?.id);\n    });\n\n    test(\"compound class with :hover pseudo extracts correctly\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn.primary:hover { background: blue; }\n        </style>\n        <button class=\"btn primary\">Click</button>\n      `);\n      const comboToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"btn.primary\"\n      );\n      expect(comboToken).toBeDefined();\n      const style = fragment.styles.find(\n        (s) => s.styleSourceId === comboToken!.id\n      );\n      expect(style).toBeDefined();\n      expect(style!.state).toBe(\":hover\");\n    });\n\n    test(\"compound class inside @media query creates breakpoint style\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card.featured { display: block; }\n          @media (min-width: 768px) {\n            .card.featured { display: flex; }\n          }\n        </style>\n        <div class=\"card featured\">Hello</div>\n      `);\n      const comboToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card.featured\"\n      );\n      expect(comboToken).toBeDefined();\n      // Should have both base and breakpoint styles\n      const comboStyles = fragment.styles.filter(\n        (s) => s.styleSourceId === comboToken!.id\n      );\n      expect(comboStyles.length).toBe(2);\n      const bpIds = new Set(comboStyles.map((s) => s.breakpointId));\n      expect(bpIds.size).toBe(2);\n    });\n  });\n\n  describe(\"attribute selectors as state\", () => {\n    test(\".btn[disabled] extracts with [disabled] state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn { padding: 8px; }\n          .btn[disabled] { opacity: 0.5; }\n        </style>\n        <button class=\"btn\" disabled>Click</button>\n      `);\n      const btnToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"btn\"\n      );\n      expect(btnToken).toBeDefined();\n      const baseStyle = fragment.styles.find(\n        (s) => s.styleSourceId === btnToken!.id && s.state === undefined\n      );\n      expect(baseStyle).toBeDefined();\n      expect(baseStyle!.property).toBe(\"paddingTop\");\n      const stateStyle = fragment.styles.find(\n        (s) => s.styleSourceId === btnToken!.id && s.state === \"[disabled]\"\n      );\n      expect(stateStyle).toBeDefined();\n      expect(stateStyle!.property).toBe(\"opacity\");\n    });\n\n    test(\".input[readonly] extracts with [readonly] state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .input[readonly] { background: #eee; }\n        </style>\n        <input class=\"input\" readonly>\n      `);\n      const inputToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"input\"\n      );\n      expect(inputToken).toBeDefined();\n      const stateStyle = fragment.styles.find(\n        (s) => s.styleSourceId === inputToken!.id && s.state === \"[readonly]\"\n      );\n      expect(stateStyle).toBeDefined();\n    });\n\n    test(\"attribute selector combined with pseudo keeps pseudo state\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .btn:hover { color: blue; }\n          .btn[disabled] { opacity: 0.5; }\n        </style>\n        <button class=\"btn\" disabled>Click</button>\n      `);\n      const btnToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"btn\"\n      );\n      expect(btnToken).toBeDefined();\n      const hoverStyle = fragment.styles.find(\n        (s) => s.styleSourceId === btnToken!.id && s.state === \":hover\"\n      );\n      expect(hoverStyle).toBeDefined();\n      const disabledStyle = fragment.styles.find(\n        (s) => s.styleSourceId === btnToken!.id && s.state === \"[disabled]\"\n      );\n      expect(disabledStyle).toBeDefined();\n    });\n  });\n\n  describe(\"mixed comma selectors (partial extraction)\", () => {\n    test(\".card, div {} extracts .card token, keeps div in leftover\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card, div { display: flex; }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const cardToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      expect(cardToken).toBeDefined();\n      // div part should stay as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n      const codeProp = fragment.props.find(\n        (p) => p.instanceId === htmlEmbed!.id && p.name === \"code\"\n      );\n      expect(codeProp).toBeDefined();\n      // The leftover should only contain the div selector, not .card\n      expect(codeProp!.type === \"string\" && codeProp!.value).toContain(\"div\");\n      expect(codeProp!.type === \"string\" && codeProp!.value).not.toMatch(\n        /\\.card/\n      );\n    });\n\n    test(\".a, .b, #id {} extracts .a and .b, keeps #id in leftover\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a, .b, #id { color: red; }\n        </style>\n        <div class=\"a\">A</div>\n        <div class=\"b\">B</div>\n      `);\n      const tokenA = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"a\"\n      );\n      const tokenB = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"b\"\n      );\n      expect(tokenA).toBeDefined();\n      expect(tokenB).toBeDefined();\n      // #id should stay in embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n    });\n\n    test(\".a, .b {} with all class selectors produces no leftover\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .a, .b { padding: 4px; }\n        </style>\n        <div class=\"a\">A</div>\n        <div class=\"b\">B</div>\n      `);\n      // Both should become tokens\n      expect(\n        fragment.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(2);\n      // No leftover embed\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        false\n      );\n    });\n  });\n\n  describe(\"breakpoint labels\", () => {\n    test(\"condition breakpoint gets human-readable label\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @media (prefers-color-scheme: dark) {\n            .card { background: black; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const condBp = fragment.breakpoints.find((b) => b.condition);\n      expect(condBp).toBeDefined();\n      // capitalCase transforms \"prefers-color-scheme:dark\" → \"Prefers Color Scheme Dark\"\n      expect(condBp!.label).toBe(\"Prefers Color Scheme Dark\");\n    });\n\n    test(\"width breakpoint gets readable label\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @media (min-width: 768px) {\n            .card { display: flex; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const bp = fragment.breakpoints.find((b) => b.minWidth === 768);\n      expect(bp).toBeDefined();\n      expect(bp!.label).toBe(\"≥ 768px\");\n    });\n\n    test(\"range breakpoint gets combined label\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @media (min-width: 768px) and (max-width: 1024px) {\n            .card { display: flex; }\n          }\n        </style>\n        <div class=\"card\">Hello</div>\n      `);\n      const bp = fragment.breakpoints.find(\n        (b) => b.minWidth === 768 && b.maxWidth === 1024\n      );\n      expect(bp).toBeDefined();\n      expect(bp!.label).toBe(\"≥ 768px and ≤ 1024px\");\n    });\n  });\n\n  describe(\"complex real-world scenarios\", () => {\n    test(\"responsive card component with hover states\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .card {\n            display: flex;\n            flex-direction: column;\n            padding: 16px;\n            border-radius: 8px;\n          }\n          .card:hover {\n            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n          }\n          @media (min-width: 768px) {\n            .card {\n              flex-direction: row;\n              padding: 24px;\n            }\n          }\n          .card-title {\n            font-size: 18px;\n            font-weight: bold;\n          }\n          @media (min-width: 768px) {\n            .card-title {\n              font-size: 24px;\n            }\n          }\n        </style>\n        <div class=\"card\">\n          <h2 class=\"card-title\">Title</h2>\n          <p>Description</p>\n        </div>\n      `);\n      // both tokens should be created\n      const cardToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card\"\n      );\n      const titleToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card-title\"\n      );\n      expect(cardToken).toBeDefined();\n      expect(titleToken).toBeDefined();\n      // card should have hover state\n      expect(\n        fragment.styles.some(\n          (s) => s.styleSourceId === cardToken!.id && s.state === \":hover\"\n        )\n      ).toBe(true);\n      // card should have breakpoint styles\n      const cardBreakpoints = new Set(\n        fragment.styles\n          .filter((s) => s.styleSourceId === cardToken!.id && !s.state)\n          .map((s) => s.breakpointId)\n      );\n      expect(cardBreakpoints.size).toBeGreaterThanOrEqual(2);\n      // title should have breakpoint styles\n      const titleBreakpoints = new Set(\n        fragment.styles\n          .filter((s) => s.styleSourceId === titleToken!.id)\n          .map((s) => s.breakpointId)\n      );\n      expect(titleBreakpoints.size).toBeGreaterThanOrEqual(2);\n      // no HtmlEmbed (all rules are class-based)\n      expect(fragment.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        false\n      );\n    });\n\n    test(\"html with mixed class rules, id rules, keyframes, and media queries\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          @keyframes fadeIn {\n            from { opacity: 0; }\n            to { opacity: 1; }\n          }\n          .fade-in { animation: fadeIn 0.3s ease-in; }\n          .card { display: flex; }\n          #app { min-height: 100vh; }\n          @media (min-width: 768px) {\n            .card { flex-direction: row; }\n          }\n        </style>\n        <div class=\"card fade-in\">\n          <p>Hello</p>\n        </div>\n      `);\n      // class tokens should be extracted\n      expect(\n        fragment.styleSources.find(\n          (s) => s.type === \"token\" && s.name === \"card\"\n        )\n      ).toBeDefined();\n      expect(\n        fragment.styleSources.find(\n          (s) => s.type === \"token\" && s.name === \"fade-in\"\n        )\n      ).toBeDefined();\n      // @keyframes and #app should remain as html embed\n      const htmlEmbed = fragment.instances.find(\n        (i) => i.component === \"HtmlEmbed\"\n      );\n      expect(htmlEmbed).toBeDefined();\n      const codeProp = fragment.props.find(\n        (p) => p.instanceId === htmlEmbed!.id && p.name === \"code\"\n      );\n      expect(codeProp!.type === \"string\" && codeProp!.value).toContain(\n        \"@keyframes\"\n      );\n      expect(codeProp!.type === \"string\" && codeProp!.value).toContain(\"#app\");\n    });\n\n    test(\"navigation component with multiple states and breakpoints\", () => {\n      const fragment = generateFragmentFromHtml(`\n        <style>\n          .nav { display: flex; gap: 8px; }\n          .nav-link { color: gray; text-decoration: none; }\n          .nav-link:hover { color: blue; }\n          .nav-link:focus { outline: 2px solid blue; }\n          .nav-link::after { content: \"\"; display: block; height: 2px; }\n          @media (min-width: 768px) {\n            .nav { gap: 16px; }\n            .nav-link { font-size: 16px; }\n          }\n        </style>\n        <nav class=\"nav\">\n          <a class=\"nav-link\">Home</a>\n          <a class=\"nav-link\">About</a>\n        </nav>\n      `);\n      const navToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"nav\"\n      );\n      const linkToken = fragment.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"nav-link\"\n      );\n      expect(navToken).toBeDefined();\n      expect(linkToken).toBeDefined();\n      // nav-link should have hover, focus, and ::after states\n      const linkStates = new Set(\n        fragment.styles\n          .filter((s) => s.styleSourceId === linkToken!.id && s.state)\n          .map((s) => s.state)\n      );\n      expect(linkStates.has(\":hover\")).toBe(true);\n      expect(linkStates.has(\":focus\")).toBe(true);\n      expect(linkStates.has(\"::after\")).toBe(true);\n      // both should have breakpoint styles\n      const navBreakpoints = new Set(\n        fragment.styles\n          .filter((s) => s.styleSourceId === navToken!.id)\n          .map((s) => s.breakpointId)\n      );\n      expect(navBreakpoints.size).toBeGreaterThanOrEqual(2);\n      // nav-link token should be reused for both <a> elements\n      const linkSelections = fragment.styleSourceSelections.filter((sel) =>\n        sel.values.includes(linkToken!.id)\n      );\n      expect(linkSelections).toHaveLength(2);\n    });\n  });\n\n  describe(\"nested selectors\", () => {\n    test(\"resolve descendant selector to matching element\", () => {\n      const cardInnerToken = token(\n        \"card__inner\",\n        css`\n          color: red;\n        `\n      );\n      expect(\n        generateFragmentFromHtml(`\n          <style>.card .inner { color: red; }</style>\n          <div class=\"card\"><span class=\"inner\">Hello</span></div>\n        `)\n      ).toEqual(\n        renderTemplate(\n          <ws.element ws:tag=\"div\" class=\"card\">\n            <ws.element ws:tag=\"span\" ws:tokens={[cardInnerToken]}>\n              Hello\n            </ws.element>\n          </ws.element>\n        )\n      );\n    });\n\n    test(\"resolve child combinator to direct child\", () => {\n      const parentChildToken = token(\n        \"parent__child\",\n        css`\n          margin: 0;\n        `\n      );\n      expect(\n        generateFragmentFromHtml(`\n          <style>.parent > .child { margin: 0; }</style>\n          <div class=\"parent\"><span class=\"child\">Hello</span></div>\n        `)\n      ).toEqual(\n        renderTemplate(\n          <ws.element ws:tag=\"div\" class=\"parent\">\n            <ws.element ws:tag=\"span\" ws:tokens={[parentChildToken]}>\n              Hello\n            </ws.element>\n          </ws.element>\n        )\n      );\n    });\n\n    test(\"child combinator does not match non-direct child\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>.parent > .child { color: red; }</style>\n        <div class=\"parent\"><div><span class=\"child\">Hello</span></div></div>\n      `);\n      // .child is nested inside an intermediate div, not a direct child of .parent\n      expect(result.skippedSelectors).toEqual([\".parent > .child\"]);\n      // no token created for the nested rule\n      expect(\n        result.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n    });\n\n    test(\"skip unresolved nested selector and report it\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>.card .title { color: red; }</style>\n        <div class=\"card\"><span>No title class here</span></div>\n      `);\n      expect(result.skippedSelectors).toEqual([\".card .title\"]);\n      expect(\n        result.styleSources.filter((s) => s.type === \"token\")\n      ).toHaveLength(0);\n    });\n\n    test(\"style-only paste with nested selector — all skipped\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>.card .title { color: red; }</style>\n      `);\n      expect(result.skippedSelectors).toEqual([\".card .title\"]);\n      expect(result.styleSources).toHaveLength(0);\n    });\n\n    test(\"mixed simple and nested selectors\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>\n          .card { display: flex; }\n          .card .title { font-weight: bold; }\n        </style>\n        <div class=\"card\"><h1 class=\"title\">Hello</h1></div>\n      `);\n      expect(result.skippedSelectors).toEqual([]);\n      // .card token + .card__title nested token\n      const tokenNames = result.styleSources\n        .filter((s) => s.type === \"token\")\n        .map((s) => s.name);\n      expect(tokenNames).toContain(\"card\");\n      expect(tokenNames).toContain(\"card__title\");\n    });\n\n    test(\"multiple ancestors: .a .b .c\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>.a .b .c { color: red; }</style>\n        <div class=\"a\"><div class=\"b\"><span class=\"c\">Hi</span></div></div>\n      `);\n      expect(result.skippedSelectors).toEqual([]);\n      const tokenNames = result.styleSources\n        .filter((s) => s.type === \"token\")\n        .map((s) => s.name);\n      expect(tokenNames).toContain(\"a__b__c\");\n    });\n\n    test(\"nested selector with media query\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>\n          .card .title { color: red; }\n          @media (min-width: 768px) {\n            .card .title { color: blue; }\n          }\n        </style>\n        <div class=\"card\"><h1 class=\"title\">Hello</h1></div>\n      `);\n      expect(result.skippedSelectors).toEqual([]);\n      const nestedToken = result.styleSources.find(\n        (s) => s.type === \"token\" && s.name === \"card__title\"\n      );\n      expect(nestedToken).toBeDefined();\n      // should have styles at base breakpoint and 768px breakpoint\n      const nestedStyles = result.styles.filter(\n        (s) => s.styleSourceId === nestedToken!.id\n      );\n      const breakpointIds = new Set(nestedStyles.map((s) => s.breakpointId));\n      expect(breakpointIds.size).toBe(2);\n    });\n\n    test(\"sibling combinators stay as leftover\", () => {\n      const result = _generateFragmentFromHtml(`\n        <style>.a + .b { color: red; }</style>\n        <div class=\"a\">A</div><div class=\"b\">B</div>\n      `);\n      // sibling selector is not class-based → stays as HtmlEmbed\n      expect(result.instances.some((i) => i.component === \"HtmlEmbed\")).toBe(\n        true\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/html.ts",
    "content": "import {\n  type DefaultTreeAdapterMap,\n  defaultTreeAdapter,\n  parseFragment,\n} from \"parse5\";\nimport {\n  type WebstudioFragment,\n  type Instance,\n  elementComponent,\n  Prop,\n  tags,\n  StyleDecl,\n  Breakpoint,\n  StyleSource,\n  StyleSourceSelection,\n} from \"@webstudio-is/sdk\";\nimport { ariaAttributes, attributesByTag } from \"@webstudio-is/html-data\";\nimport {\n  camelCaseProperty,\n  parseCss,\n  parseClassBasedSelector,\n  parseMediaQuery,\n  type ParsedStyleDecl,\n  type ParsedClassSelector,\n} from \"@webstudio-is/css-data\";\nimport { richTextContentTags } from \"./content-model\";\nimport { setIsSubsetOf } from \"./shim\";\nimport { isAttributeNameSafe } from \"@webstudio-is/react-sdk\";\nimport { capitalCase } from \"change-case\";\nimport * as csstree from \"css-tree\";\n\ntype ElementNode = DefaultTreeAdapterMap[\"element\"];\n\nconst spaceRegex = /^\\s*$/;\n\nconst getAttributeType = (\n  attribute: (typeof ariaAttributes)[number]\n): \"string\" | \"boolean\" | \"number\" => {\n  if (\n    attribute.type === \"string\" ||\n    attribute.type === \"select\" ||\n    attribute.type === \"url\"\n  ) {\n    return \"string\";\n  }\n  if (attribute.type === \"number\" || attribute.type === \"boolean\") {\n    return attribute.type;\n  }\n  attribute.type satisfies never;\n  throw Error(\"Unknown type\");\n};\n\nconst getAttributeTypes = () => {\n  const attributeTypes = new Map<string, \"string\" | \"number\" | \"boolean\">();\n  for (const attribute of ariaAttributes) {\n    attributeTypes.set(attribute.name, getAttributeType(attribute));\n  }\n  for (const attribute of attributesByTag[\"*\"] ?? []) {\n    attributeTypes.set(attribute.name, getAttributeType(attribute));\n  }\n  for (const [tag, attributes] of Object.entries(attributesByTag)) {\n    if (attributes) {\n      for (const attribute of attributes) {\n        attributeTypes.set(\n          `${tag}:${attribute.name}`,\n          getAttributeType(attribute)\n        );\n      }\n    }\n  }\n  return attributeTypes;\n};\n\nconst findContentTags = (element: ElementNode, tags = new Set<string>()) => {\n  for (const childNode of element.childNodes) {\n    if (defaultTreeAdapter.isElementNode(childNode)) {\n      tags.add(childNode.tagName);\n      findContentTags(childNode, tags);\n    }\n  }\n  return tags;\n};\n\n/**\n * Collect all text content from <style> elements in the parsed HTML tree\n */\nconst collectStyleTexts = (\n  documentFragment: DefaultTreeAdapterMap[\"documentFragment\"]\n): string[] => {\n  const texts: string[] = [];\n  const walk = (node: DefaultTreeAdapterMap[\"parentNode\"]) => {\n    for (const child of node.childNodes) {\n      if (\n        defaultTreeAdapter.isElementNode(child) &&\n        child.tagName === \"style\"\n      ) {\n        const textContent = child.childNodes\n          .filter((c) => defaultTreeAdapter.isTextNode(c))\n          .map((c) => (c as DefaultTreeAdapterMap[\"textNode\"]).value)\n          .join(\"\");\n        texts.push(textContent);\n      }\n      if (defaultTreeAdapter.isElementNode(child)) {\n        walk(child);\n      }\n    }\n  };\n  walk(documentFragment);\n  return texts;\n};\n\n/**\n * Get class names from a parse5 element's class attribute.\n */\nconst getElementClasses = (element: ElementNode): Set<string> => {\n  const classAttr = element.attrs?.find((a) => a.name === \"class\");\n  if (!classAttr) {\n    return new Set();\n  }\n  return new Set(classAttr.value.split(/\\s+/).filter(Boolean));\n};\n\n/**\n * Get the parent element node, or undefined if the parent is a document/fragment.\n */\nconst getParentElementNode = (node: ElementNode): ElementNode | undefined => {\n  const parent = node.parentNode;\n  return parent && defaultTreeAdapter.isElementNode(parent)\n    ? parent\n    : undefined;\n};\n\n/**\n * Check whether an element satisfies the ancestor constraints of a nested\n * selector. Walks up the parse5 tree from the element checking each ancestor\n * constraint from innermost to outermost.\n */\nconst elementMatchesAncestors = (\n  element: ElementNode,\n  ancestors: NonNullable<ParsedClassSelector[\"ancestors\"]>\n): boolean => {\n  let current: ElementNode | undefined = getParentElementNode(element);\n\n  // Check ancestors from innermost (last in array) to outermost (first)\n  for (let i = ancestors.length - 1; i >= 0; i--) {\n    const { classNames: requiredClasses, combinator } = ancestors[i];\n    const requiredSet = new Set(requiredClasses);\n\n    if (combinator === \"child\") {\n      // Direct parent must match\n      if (!current) {\n        return false;\n      }\n      if (!setIsSubsetOf(requiredSet, getElementClasses(current))) {\n        return false;\n      }\n      current = getParentElementNode(current);\n    } else {\n      // \"descendant\" — search up the tree for any matching ancestor\n      let found = false;\n      while (current) {\n        if (setIsSubsetOf(requiredSet, getElementClasses(current))) {\n          current = getParentElementNode(current);\n          found = true;\n          break;\n        }\n        current = getParentElementNode(current);\n      }\n      if (!found) {\n        return false;\n      }\n    }\n  }\n\n  return true;\n};\n\ntype NestedClassRule = {\n  parsed: ParsedClassSelector;\n  decls: ParsedStyleDecl[];\n  selector: string;\n};\n\n/**\n * From a list of parsed style declarations, determine which selectors are\n * class-based selectors (extractable as tokens) and which are not.\n *\n * Simple class selectors go into classRules. Nested selectors (with\n * descendant/child combinators) go into nestedClassRules for tree matching.\n */\nconst classifyRules = (\n  decls: ParsedStyleDecl[]\n): {\n  classRules: Map<string, ParsedStyleDecl[]>;\n  nestedClassRules: Map<string, NestedClassRule>;\n  hasNonClassRules: boolean;\n} => {\n  const classRules = new Map<string, ParsedStyleDecl[]>();\n  const nestedClassRules = new Map<string, NestedClassRule>();\n  let hasNonClassRules = false;\n\n  for (const decl of decls) {\n    const parsed = parseClassBasedSelector(decl.selector);\n    if (parsed !== undefined) {\n      const selectorState = parsed.states?.[0];\n      const effectiveDecl = selectorState\n        ? { ...decl, state: decl.state ?? selectorState }\n        : decl;\n\n      if (parsed.ancestors && parsed.ancestors.length > 0) {\n        // Nested selector — needs tree matching\n        let entry = nestedClassRules.get(parsed.tokenName);\n        if (!entry) {\n          entry = { parsed, decls: [], selector: decl.selector };\n          nestedClassRules.set(parsed.tokenName, entry);\n        }\n        entry.decls.push(effectiveDecl);\n      } else {\n        // Simple class selector\n        let rules = classRules.get(parsed.tokenName);\n        if (!rules) {\n          rules = [];\n          classRules.set(parsed.tokenName, rules);\n        }\n        rules.push(effectiveDecl);\n      }\n    } else {\n      hasNonClassRules = true;\n    }\n  }\n  return { classRules, nestedClassRules, hasNonClassRules };\n};\n\n/**\n * Build the leftover CSS (non-class rules) from the original style text.\n * We re-parse with css-tree and walk rules to classify them:\n * - Class-based selectors (simple and compound) are removed (extracted as tokens)\n * - Non-class selectors are kept as leftover\n * - For mixed comma-separated selectors (.card, div {}), only the non-class\n *   portions are kept\n */\nconst buildLeftoverCss = (cssText: string): string => {\n  const ast = csstree.parse(cssText);\n  const parts: string[] = [];\n\n  /** Re-use parseClassBasedSelector as single source of truth */\n  const isClassBasedSelector = (selector: csstree.CssNode): boolean =>\n    selector.type === \"Selector\" &&\n    parseClassBasedSelector(csstree.generate(selector)) !== undefined;\n\n  /**\n   * Process a Rule: if all selectors are class-based, skip entirely.\n   * If none are, keep entirely. If mixed, keep only non-class selectors.\n   */\n  const getLeftoverRule = (node: csstree.Rule): string | undefined => {\n    if (node.prelude?.type !== \"SelectorList\") {\n      return csstree.generate(node);\n    }\n    const selectors = node.prelude.children.toArray();\n    const nonClassSelectors = selectors.filter(\n      (sel) => !isClassBasedSelector(sel)\n    );\n    if (nonClassSelectors.length === selectors.length) {\n      // No class selectors found — keep entire rule\n      return csstree.generate(node);\n    }\n    if (nonClassSelectors.length === 0) {\n      // All selectors are class-based — skip entirely\n      return undefined;\n    }\n    // Mixed: rebuild rule with only non-class selectors\n    const selectorStrs = nonClassSelectors.map((sel) => csstree.generate(sel));\n    const body = node.block ? csstree.generate(node.block) : \"{}\";\n    return `${selectorStrs.join(\",\")}${body}`;\n  };\n\n  // Walk only top-level children of the stylesheet\n  if (ast.type === \"StyleSheet\") {\n    for (const node of ast.children) {\n      if (node.type === \"Rule\") {\n        const leftover = getLeftoverRule(node);\n        if (leftover) {\n          parts.push(leftover);\n        }\n      } else if (node.type === \"Atrule\") {\n        if (node.name === \"media\") {\n          // Check if this media query is unsupported by parseCss (print, non-px units)\n          // If so, keep the entire block as leftover\n          let isUnsupportedMedia = false;\n          if (node.prelude) {\n            csstree.walk(node.prelude, (walkNode) => {\n              if (\n                walkNode.type === \"MediaQuery\" &&\n                walkNode.mediaType === \"print\"\n              ) {\n                isUnsupportedMedia = true;\n              }\n            });\n          }\n          if (isUnsupportedMedia) {\n            parts.push(csstree.generate(node));\n          } else {\n            // For @media, check children — keep non-class rules\n            const leftoverRules: string[] = [];\n            if (node.block) {\n              for (const child of node.block.children) {\n                if (child.type === \"Rule\") {\n                  const leftover = getLeftoverRule(child);\n                  if (leftover) {\n                    leftoverRules.push(leftover);\n                  }\n                } else if (child.type === \"Atrule\") {\n                  // Nested at-rule within @media — keep if it contains non-class rules\n                  const nestedLeftover = collectNonClassFromAtrule(\n                    child,\n                    getLeftoverRule\n                  );\n                  if (nestedLeftover) {\n                    leftoverRules.push(nestedLeftover);\n                  }\n                }\n              }\n            }\n            if (leftoverRules.length > 0) {\n              const prelude = node.prelude\n                ? csstree.generate(node.prelude)\n                : \"\";\n              parts.push(`@media ${prelude}{${leftoverRules.join(\"\")}}`);\n            }\n          }\n        } else {\n          // @keyframes, @font-face, @supports, etc. — always keep\n          parts.push(csstree.generate(node));\n        }\n      }\n    }\n  }\n  return parts.join(\"\");\n};\n\nconst collectNonClassFromAtrule = (\n  node: csstree.Atrule,\n  getLeftoverRule: (rule: csstree.Rule) => string | undefined\n): string | undefined => {\n  if (node.name === \"media\" && node.block) {\n    // Check if this nested @media is unsupported (e.g. print)\n    let isUnsupportedMedia = false;\n    if (node.prelude) {\n      csstree.walk(node.prelude, (walkNode) => {\n        if (walkNode.type === \"MediaQuery\" && walkNode.mediaType === \"print\") {\n          isUnsupportedMedia = true;\n        }\n      });\n    }\n    if (isUnsupportedMedia) {\n      return csstree.generate(node);\n    }\n    const leftoverRules: string[] = [];\n    for (const child of node.block.children) {\n      if (child.type === \"Rule\") {\n        const leftover = getLeftoverRule(child);\n        if (leftover) {\n          leftoverRules.push(leftover);\n        }\n      } else if (child.type === \"Atrule\") {\n        const nested = collectNonClassFromAtrule(child, getLeftoverRule);\n        if (nested) {\n          leftoverRules.push(nested);\n        }\n      }\n    }\n    if (leftoverRules.length > 0) {\n      const prelude = node.prelude ? csstree.generate(node.prelude) : \"\";\n      return `@media ${prelude}{${leftoverRules.join(\"\")}}`;\n    }\n    return undefined;\n  }\n  // Non-media at-rules (like @supports nested in @media) — always keep\n  return csstree.generate(node);\n};\n\n/**\n * Resolve overlapping range breakpoints by adjusting maxWidth values.\n * Range breakpoints are those that have both minWidth and maxWidth.\n * When ranges overlap, the earlier range's maxWidth is adjusted to\n * be less than the next range's minWidth.\n */\nconst resolveOverlappingBreakpoints = (breakpoints: Breakpoint[]) => {\n  // Collect all breakpoints that have both minWidth and maxWidth (range breakpoints)\n  // plus simple minWidth-only and maxWidth-only breakpoints\n  const widthBreakpoints = breakpoints.filter(\n    (b) =>\n      b.condition === undefined &&\n      (b.minWidth !== undefined || b.maxWidth !== undefined)\n  );\n\n  if (widthBreakpoints.length < 2) {\n    return;\n  }\n\n  // Sort range breakpoints by minWidth\n  const rangeBreakpoints = widthBreakpoints\n    .filter((b) => b.minWidth !== undefined && b.maxWidth !== undefined)\n    .sort((a, b) => a.minWidth! - b.minWidth!);\n\n  // Also consider simple min-width breakpoints as potential overlap targets\n  const minOnlyBreakpoints = widthBreakpoints.filter(\n    (b) => b.minWidth !== undefined && b.maxWidth === undefined\n  );\n  const maxOnlyBreakpoints = widthBreakpoints.filter(\n    (b) => b.maxWidth !== undefined && b.minWidth === undefined\n  );\n\n  // Adjust range vs range overlaps\n  for (let i = 0; i < rangeBreakpoints.length - 1; i++) {\n    const current = rangeBreakpoints[i];\n    const next = rangeBreakpoints[i + 1];\n    if (current.maxWidth! >= next.minWidth!) {\n      current.maxWidth = next.minWidth! - 1;\n    }\n  }\n\n  // Adjust range vs min-width-only overlaps\n  for (const range of rangeBreakpoints) {\n    for (const minBp of minOnlyBreakpoints) {\n      if (range.maxWidth! >= minBp.minWidth!) {\n        range.maxWidth = minBp.minWidth! - 1;\n      }\n    }\n  }\n\n  // Adjust range vs max-width-only overlaps\n  for (const range of rangeBreakpoints) {\n    for (const maxBp of maxOnlyBreakpoints) {\n      if (range.minWidth! <= maxBp.maxWidth!) {\n        range.minWidth = maxBp.maxWidth! + 1;\n      }\n    }\n  }\n};\n\nexport const generateFragmentFromHtml = (\n  html: string\n): WebstudioFragment & { skippedSelectors: string[] } => {\n  const attributeTypes = getAttributeTypes();\n  const instances = new Map<Instance[\"id\"], Instance>();\n  const styleSourceSelections: StyleSourceSelection[] = [];\n  const styleSources: StyleSource[] = [];\n  const styles: StyleDecl[] = [];\n  const breakpoints: Breakpoint[] = [];\n  const props: Prop[] = [];\n  let lastId = -1;\n  const getNewId = () => {\n    lastId += 1;\n    return lastId.toString();\n  };\n\n  let baseBreakpoint: undefined | Breakpoint;\n  const getBaseBreakpointId = () => {\n    if (baseBreakpoint) {\n      return baseBreakpoint.id;\n    }\n    baseBreakpoint = { id: \"base\", label: \"\" };\n    breakpoints.push(baseBreakpoint);\n    return baseBreakpoint.id;\n  };\n\n  // Breakpoint deduplication by key — breakpoints use their own ID counter\n  let lastBreakpointId = -1;\n  const getNewBreakpointId = () => {\n    lastBreakpointId += 1;\n    return lastBreakpointId.toString();\n  };\n  const breakpointByKey = new Map<string, Breakpoint>();\n  const getOrCreateBreakpoint = (parsed: {\n    minWidth?: number;\n    maxWidth?: number;\n    condition?: string;\n  }): string => {\n    const key = JSON.stringify(parsed);\n    let bp = breakpointByKey.get(key);\n    if (!bp) {\n      const id = getNewBreakpointId();\n      let label: string;\n      if (parsed.condition) {\n        // e.g. \"prefers-color-scheme:dark\" → \"Prefers Color Scheme Dark\"\n        label = capitalCase(parsed.condition);\n      } else {\n        const parts: string[] = [];\n        if (parsed.minWidth !== undefined) {\n          parts.push(`≥ ${parsed.minWidth}px`);\n        }\n        if (parsed.maxWidth !== undefined) {\n          parts.push(`≤ ${parsed.maxWidth}px`);\n        }\n        label = parts.join(\" and \");\n      }\n      bp = { id, label, ...parsed };\n      breakpointByKey.set(key, bp);\n      breakpoints.push(bp);\n    }\n    return bp.id;\n  };\n\n  const createLocalStyles = (instanceId: string, css: string) => {\n    const localStyleSource: StyleSource = {\n      type: \"local\",\n      id: `${instanceId}:ws:style`,\n    };\n    styleSources.push(localStyleSource);\n    styleSourceSelections.push({ instanceId, values: [localStyleSource.id] });\n    for (const { property, value } of parseCss(`.styles{${css}}`)) {\n      styles.push({\n        styleSourceId: localStyleSource.id,\n        breakpointId: getBaseBreakpointId(),\n        property: camelCaseProperty(property),\n        value,\n      });\n    }\n  };\n\n  // ---- Pre-parse all <style> tags to extract class-based tokens ----\n  const documentFragment = parseFragment(html, {\n    scriptingEnabled: false,\n    sourceCodeLocationInfo: true,\n  });\n\n  // Collect style tag texts and their nodes\n  const styleTexts = collectStyleTexts(documentFragment);\n  const allCssText = styleTexts.join(\"\\n\");\n\n  // Parse all CSS and classify rules\n  const allDecls = parseCss(allCssText);\n  const { classRules, nestedClassRules } = classifyRules(allDecls);\n\n  // Track which class names are used by elements — IDs will be assigned later\n  const usedClassNames = new Set<string>();\n  // Track which nested token names were resolved against the tree\n  const usedNestedTokenNames = new Set<string>();\n\n  // Classify each style tag: \"all-class\" (skip embed), \"no-class\" (keep original),\n  // \"mixed\" (regenerate leftover), or \"empty\" (skip)\n  type StyleTagAction =\n    | { type: \"skip\" }\n    | { type: \"keep-original\" }\n    | { type: \"leftover\"; css: string };\n\n  const styleTagActions: StyleTagAction[] = [];\n  for (const text of styleTexts) {\n    if (spaceRegex.test(text) || text.length === 0) {\n      styleTagActions.push({ type: \"skip\" });\n      continue;\n    }\n    const parsedDecls = parseCss(text);\n    const {\n      classRules: tagClassRules,\n      nestedClassRules: tagNestedRules,\n      hasNonClassRules: tagHasNonClass,\n    } = classifyRules(parsedDecls);\n\n    if (\n      parsedDecls.length === 0 &&\n      tagClassRules.size === 0 &&\n      tagNestedRules.size === 0\n    ) {\n      // Unparseable CSS — keep original\n      styleTagActions.push({ type: \"keep-original\" });\n    } else if (tagClassRules.size === 0 && tagNestedRules.size === 0) {\n      // Only non-class rules — keep original\n      styleTagActions.push({ type: \"keep-original\" });\n    } else if (!tagHasNonClass) {\n      // Only class rules — also check for unsupported media like @media print\n      const leftover = buildLeftoverCss(text);\n      if (leftover.length > 0) {\n        styleTagActions.push({ type: \"leftover\", css: leftover });\n      } else {\n        styleTagActions.push({ type: \"skip\" });\n      }\n    } else {\n      // Mixed — regenerate leftover\n      const leftover = buildLeftoverCss(text);\n      if (leftover.length > 0) {\n        styleTagActions.push({ type: \"leftover\", css: leftover });\n      } else {\n        styleTagActions.push({ type: \"skip\" });\n      }\n    }\n  }\n\n  let styleTagIndex = 0;\n\n  // Map of instanceId → resolved class names (for post-processing token assignments)\n  const instanceTokenClasses = new Map<string, string[]>();\n\n  const convertElementToInstance = (node: ElementNode) => {\n    if (node.tagName === \"script\" && node.sourceCodeLocation) {\n      const { startCol, startOffset, endOffset } = node.sourceCodeLocation;\n      const indent = startCol - 1;\n      const htmlFragment = html\n        .slice(startOffset, endOffset)\n        .split(\"\\n\")\n        .map((line, index) => {\n          if (index > 0 && /^\\s+$/.test(line.slice(0, indent))) {\n            return line.slice(indent);\n          }\n          return line;\n        })\n        .join(\"\\n\");\n      const instance: Instance = {\n        type: \"instance\",\n        id: getNewId(),\n        component: \"HtmlEmbed\",\n        label: \"Script\",\n        children: [],\n      };\n      instances.set(instance.id, instance);\n      props.push({\n        id: `${instance.id}:clientOnly`,\n        instanceId: instance.id,\n        name: \"clientOnly\",\n        type: \"boolean\",\n        value: true,\n      });\n      props.push({\n        id: `${instance.id}:code`,\n        instanceId: instance.id,\n        name: \"code\",\n        type: \"string\",\n        value: htmlFragment,\n      });\n      return { type: \"id\" as const, value: instance.id };\n    }\n    if (node.tagName === \"style\" && node.sourceCodeLocation) {\n      const tagIdx = styleTagIndex++;\n      const action = styleTagActions[tagIdx] ?? { type: \"skip\" };\n\n      if (action.type === \"keep-original\") {\n        // Preserve original formatting — use original HTML fragment\n        const { startCol, startOffset, endOffset } = node.sourceCodeLocation;\n        const indent = startCol - 1;\n        const htmlFragment = html\n          .slice(startOffset, endOffset)\n          .split(\"\\n\")\n          .map((line, index) => {\n            if (index > 0 && /^\\s+$/.test(line.slice(0, indent))) {\n              return line.slice(indent);\n            }\n            return line;\n          })\n          .join(\"\\n\");\n        const instance: Instance = {\n          type: \"instance\",\n          id: getNewId(),\n          component: \"HtmlEmbed\",\n          label: \"Style\",\n          children: [],\n        };\n        instances.set(instance.id, instance);\n        props.push({\n          id: `${instance.id}:code`,\n          instanceId: instance.id,\n          name: \"code\",\n          type: \"string\",\n          value: htmlFragment,\n        });\n        return { type: \"id\" as const, value: instance.id };\n      }\n\n      if (action.type === \"leftover\") {\n        const instance: Instance = {\n          type: \"instance\",\n          id: getNewId(),\n          component: \"HtmlEmbed\",\n          label: \"Style\",\n          children: [],\n        };\n        instances.set(instance.id, instance);\n        props.push({\n          id: `${instance.id}:code`,\n          instanceId: instance.id,\n          name: \"code\",\n          type: \"string\",\n          value: `<style>${action.css}</style>`,\n        });\n        return { type: \"id\" as const, value: instance.id };\n      }\n\n      // action.type === \"skip\" — all rules were class rules (or empty)\n      return undefined;\n    }\n    if (!tags.includes(node.tagName)) {\n      return;\n    }\n    const instance: Instance = {\n      type: \"instance\",\n      id: getNewId(),\n      component: elementComponent,\n      tag: node.tagName,\n      children: [],\n    };\n    // users expect to get optimized images by default\n    // though still able to create raw img element\n    if (node.tagName === \"img\") {\n      instance.component = \"Image\";\n      delete instance.tag;\n    }\n    instances.set(instance.id, instance);\n    for (const attr of node.attrs) {\n      // skip attributes which cannot be rendered in jsx\n      if (!isAttributeNameSafe(attr.name)) {\n        continue;\n      }\n      const id = `${instance.id}:${attr.name}`;\n      const instanceId = instance.id;\n      const name = attr.name;\n      // cast props to types extracted from html and aria specs\n      const type =\n        attributeTypes.get(`${node.tagName}:${name}`) ??\n        attributeTypes.get(name) ??\n        \"string\";\n      // ignore style attribute to not conflict with react\n      if (attr.name === \"style\") {\n        createLocalStyles(instanceId, attr.value);\n        continue;\n      }\n      // resolve class attribute against class rules from style tags\n      if (\n        attr.name === \"class\" &&\n        (classRules.size > 0 || nestedClassRules.size > 0)\n      ) {\n        const classNames = attr.value.split(/\\s+/).filter(Boolean);\n        const resolvedTokenNames: string[] = [];\n        const unresolvedClasses: string[] = [];\n\n        // First: match individual class names as simple tokens\n        for (const className of classNames) {\n          if (classRules.has(className)) {\n            resolvedTokenNames.push(className);\n            usedClassNames.add(className);\n          } else {\n            unresolvedClasses.push(className);\n          }\n        }\n\n        // Second: match compound class combinations (e.g., class=\"card active\"\n        // matches token \"card.active\" from .card.active {} selector)\n        if (classNames.length >= 2) {\n          const classSet = new Set(classNames);\n          for (const tokenName of classRules.keys()) {\n            if (!tokenName.includes(\".\")) {\n              continue;\n            }\n            const parts = tokenName.split(\".\");\n            if (parts.every((part) => classSet.has(part))) {\n              resolvedTokenNames.push(tokenName);\n              usedClassNames.add(tokenName);\n            }\n          }\n        }\n\n        // Third: match nested class rules (e.g., .card .title)\n        if (nestedClassRules.size > 0) {\n          const classSet = new Set(classNames);\n          for (const [tokenName, { parsed }] of nestedClassRules) {\n            const targetRequired = new Set(parsed.classNames);\n            if (!setIsSubsetOf(targetRequired, classSet)) {\n              continue;\n            }\n            if (\n              parsed.ancestors &&\n              !elementMatchesAncestors(node, parsed.ancestors)\n            ) {\n              continue;\n            }\n            resolvedTokenNames.push(tokenName);\n            usedNestedTokenNames.add(tokenName);\n            // Remove matched target class names from unresolved\n            for (const cn of parsed.classNames) {\n              const idx = unresolvedClasses.indexOf(cn);\n              if (idx >= 0) {\n                unresolvedClasses.splice(idx, 1);\n              }\n            }\n          }\n        }\n\n        // Token style source selections will be created after all instances\n        if (resolvedTokenNames.length > 0) {\n          // Store the resolved token names for post-processing\n          instanceTokenClasses.set(instanceId, resolvedTokenNames);\n        }\n        // Keep unresolved class names as class prop\n        if (unresolvedClasses.length > 0) {\n          props.push({\n            id,\n            instanceId,\n            name: \"class\",\n            type: \"string\",\n            value: unresolvedClasses.join(\" \"),\n          });\n        }\n        continue;\n      }\n      // selected option is represented as fake value attribute on select element\n      if (node.tagName === \"option\" && attr.name === \"selected\") {\n        continue;\n      }\n      if (type === \"string\") {\n        props.push({ id, instanceId, name, type, value: attr.value });\n        continue;\n      }\n      if (type === \"number\") {\n        props.push({ id, instanceId, name, type, value: Number(attr.value) });\n        continue;\n      }\n      if (type === \"boolean\") {\n        props.push({ id, instanceId, name, type, value: true });\n        continue;\n      }\n      (type) satisfies never;\n    }\n    const contentTags = findContentTags(node);\n    const hasNonRichTextContent = !setIsSubsetOf(\n      contentTags,\n      richTextContentTags\n    );\n    if (node.tagName === \"select\") {\n      for (const childNode of node.childNodes) {\n        if (defaultTreeAdapter.isElementNode(childNode)) {\n          if (\n            childNode.tagName === \"option\" &&\n            childNode.attrs.find((attr) => attr.name === \"selected\")\n          ) {\n            const valueAttr = childNode.attrs.find(\n              (attr) => attr.name === \"value\"\n            );\n            // if value attribute is omitted, the value is taken from the text content of the option element\n            const childText = childNode.childNodes.find((childNode) =>\n              defaultTreeAdapter.isTextNode(childNode)\n            );\n            // selected option is represented as fake value attribute on select element\n            props.push({\n              id: `${instance.id}:value`,\n              instanceId: instance.id,\n              name: \"value\",\n              type: \"string\",\n              value: valueAttr?.value ?? childText?.value.trim() ?? \"\",\n            });\n          }\n        }\n      }\n    }\n    for (let index = 0; index < node.childNodes.length; index += 1) {\n      const childNode = node.childNodes[index];\n      if (defaultTreeAdapter.isElementNode(childNode)) {\n        const child = convertElementToInstance(childNode);\n        if (child) {\n          instance.children.push(child);\n        }\n      }\n      if (defaultTreeAdapter.isTextNode(childNode)) {\n        // trim spaces around rich text\n        // do not for code\n        if (spaceRegex.test(childNode.value) && node.tagName !== \"code\") {\n          if (index === 0 || index === node.childNodes.length - 1) {\n            continue;\n          }\n        }\n        let child: Instance[\"children\"][number] = {\n          type: \"text\",\n          value: childNode.value,\n        };\n        if (node.tagName !== \"code\") {\n          // collapse spacing characters inside of text to avoid preserved newlines\n          child.value = child.value.replaceAll(/\\s+/g, \" \");\n          // remove unnecessary spacing in nodes\n          if (index === 0) {\n            child.value = child.value.trimStart();\n          }\n          if (index === node.childNodes.length - 1) {\n            child.value = child.value.trimEnd();\n          }\n        }\n        // textarea content is initial value\n        // and represented with fake value attribute\n        if (node.tagName === \"textarea\") {\n          props.push({\n            id: `${instance.id}:value`,\n            instanceId: instance.id,\n            name: \"value\",\n            type: \"string\",\n            value: child.value.trim(),\n          });\n          continue;\n        }\n        // when element has content elements other than supported by rich text\n        // wrap its text children with span, for example\n        // <div>\n        //   text\n        //   <article></article>\n        // </div>\n        // is converted into\n        // <div>\n        //   <span>text</span>\n        //   <article></article>\n        // </div>\n        if (hasNonRichTextContent) {\n          // remove spaces between elements outside of rich text\n          if (spaceRegex.test(childNode.value)) {\n            continue;\n          }\n          const span: Instance = {\n            type: \"instance\",\n            id: getNewId(),\n            component: elementComponent,\n            tag: \"span\",\n            children: [child],\n          };\n          instances.set(span.id, span);\n          child = { type: \"id\", value: span.id };\n        }\n        instance.children.push(child);\n      }\n    }\n    return { type: \"id\" as const, value: instance.id };\n  };\n\n  const children: Instance[\"children\"] = [];\n  for (const childNode of documentFragment.childNodes) {\n    if (defaultTreeAdapter.isElementNode(childNode)) {\n      const child = convertElementToInstance(childNode);\n      if (child) {\n        children.push(child);\n      }\n    }\n  }\n\n  // ---- Post-processing: create token style sources and styles ----\n  // Now that all instances have been created and IDs assigned,\n  // create token style sources (using the shared getNewId counter)\n  // in the order that matches template rendering (instances first, then tokens)\n\n  // Ensure all simple class rules become tokens, even when no elements reference them\n  // (e.g. pasting just a <style> tag without HTML elements)\n  for (const className of classRules.keys()) {\n    usedClassNames.add(className);\n  }\n  // Add resolved nested tokens\n  for (const tokenName of usedNestedTokenNames) {\n    usedClassNames.add(tokenName);\n  }\n\n  const tokenIdMap = new Map<string, string>(); // className → styleSourceId\n\n  for (const className of usedClassNames) {\n    const styleSourceId = getNewId();\n    tokenIdMap.set(className, styleSourceId);\n    styleSources.push({\n      type: \"token\",\n      id: styleSourceId,\n      name: className,\n    });\n\n    const decls =\n      classRules.get(className) ?? nestedClassRules.get(className)?.decls;\n    if (decls) {\n      for (const decl of decls) {\n        let breakpointId: string;\n        if (decl.breakpoint) {\n          const parsed = parseMediaQuery(decl.breakpoint);\n          if (parsed === undefined) {\n            continue;\n          }\n          const hasWidth =\n            parsed.minWidth !== undefined || parsed.maxWidth !== undefined;\n          const hasCondition = parsed.condition !== undefined;\n          if (hasCondition) {\n            const conditionParts: string[] = [];\n            if (parsed.minWidth !== undefined) {\n              conditionParts.push(`min-width:${parsed.minWidth}px`);\n            }\n            if (parsed.maxWidth !== undefined) {\n              conditionParts.push(`max-width:${parsed.maxWidth}px`);\n            }\n            // hasCondition guarantees parsed.condition is defined\n            conditionParts.push(parsed.condition as string);\n            breakpointId = getOrCreateBreakpoint({\n              condition: conditionParts.join(\" and \"),\n            });\n          } else if (hasWidth) {\n            breakpointId = getOrCreateBreakpoint({\n              ...(parsed.minWidth !== undefined\n                ? { minWidth: parsed.minWidth }\n                : {}),\n              ...(parsed.maxWidth !== undefined\n                ? { maxWidth: parsed.maxWidth }\n                : {}),\n            });\n          } else {\n            continue;\n          }\n        } else {\n          breakpointId = getBaseBreakpointId();\n        }\n        styles.push({\n          styleSourceId,\n          breakpointId,\n          property: camelCaseProperty(decl.property),\n          value: decl.value,\n          ...(decl.state ? { state: decl.state } : {}),\n        });\n      }\n    }\n  }\n\n  // Create style source selections for instances that use tokens\n  const selectionsByInstance = new Map(\n    styleSourceSelections.map((sel) => [sel.instanceId, sel])\n  );\n  for (const [instanceId, classNames] of instanceTokenClasses) {\n    const tokenIds = classNames\n      .map((name) => tokenIdMap.get(name))\n      .filter((id): id is string => id !== undefined);\n    if (tokenIds.length > 0) {\n      const existingSelection = selectionsByInstance.get(instanceId);\n      if (existingSelection) {\n        existingSelection.values.push(...tokenIds);\n      } else {\n        const newSelection: StyleSourceSelection = {\n          instanceId,\n          values: [...tokenIds],\n        };\n        styleSourceSelections.push(newSelection);\n        selectionsByInstance.set(instanceId, newSelection);\n      }\n    }\n  }\n\n  // Resolve overlapping breakpoints\n  resolveOverlappingBreakpoints(breakpoints);\n\n  // Collect skipped nested selectors (no matching elements in the pasted HTML)\n  const skippedSelectors: string[] = [];\n  for (const [tokenName, { selector }] of nestedClassRules) {\n    if (!usedNestedTokenNames.has(tokenName)) {\n      skippedSelectors.push(selector);\n    }\n  }\n\n  return {\n    children,\n    instances: Array.from(instances.values()),\n    props,\n    dataSources: [],\n    resources: [],\n    styleSourceSelections,\n    styleSources,\n    styles,\n    breakpoints,\n    assets: [],\n    skippedSelectors,\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/instance-utils.test.tsx",
    "content": "import { enableMapSet } from \"immer\";\nimport { describe, test, expect, beforeEach } from \"vitest\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport {\n  $,\n  ws,\n  css,\n  expression,\n  renderTemplate,\n  renderData,\n  ResourceValue,\n  token,\n} from \"@webstudio-is/template\";\nimport * as defaultMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport * as radixMetas from \"@webstudio-is/sdk-components-react-radix/metas\";\nimport type {\n  Asset,\n  Breakpoint,\n  Instance,\n  StyleDecl,\n  StyleDeclKey,\n  StyleSource,\n  WebstudioData,\n  WebstudioFragment,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport {\n  coreMetas,\n  portalComponent,\n  elementComponent,\n  ROOT_INSTANCE_ID,\n} from \"@webstudio-is/sdk\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\nimport type { StyleProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  deleteInstanceMutable,\n  extractWebstudioFragment,\n  insertWebstudioFragmentCopy,\n  reparentInstanceMutable,\n  getWebstudioData,\n  insertInstanceChildrenMutable,\n  findClosestInsertable,\n  insertWebstudioFragmentAt,\n  insertWebstudioElementAt,\n  buildInstancePath,\n  wrapInstance,\n  toggleInstanceShow,\n  unwrapInstanceMutable,\n  canUnwrapInstance,\n  canConvertInstance,\n  convertInstance,\n  deleteSelectedInstance,\n  detectPageTokenConflicts,\n} from \"./instance-utils\";\nimport type { InstancePath } from \"./awareness\";\nimport {\n  $assets,\n  $breakpoints,\n  $dataSources,\n  $instances,\n  $pages,\n  $project,\n  $props,\n  $styleSourceSelections,\n  $styleSources,\n  $styles,\n  $registeredComponentMetas,\n  $resources,\n} from \"./nano-states\";\nimport { registerContainers } from \"./sync/sync-stores\";\nimport {\n  $awareness,\n  getInstancePath,\n  selectInstance,\n  selectPage,\n} from \"./awareness\";\n\nenableMapSet();\nregisterContainers();\n\n$pages.set(createDefaultPages({ rootInstanceId: \"\" }));\n\nconst defaultMetasMap = new Map(\n  Object.entries({ ...defaultMetas, ...coreMetas })\n);\n$registeredComponentMetas.set(defaultMetasMap);\n\nconst createFragment = (\n  fragment: Partial<WebstudioFragment>\n): WebstudioFragment => ({\n  children: [],\n  instances: [],\n  styleSourceSelections: [],\n  styleSources: [],\n  breakpoints: [],\n  styles: [],\n  dataSources: [],\n  resources: [],\n  props: [],\n  assets: [],\n  ...fragment,\n});\n\nconst createFakeComponentMetas = (\n  itemMeta: Partial<WsComponentMeta>,\n  anotherItemMeta?: Partial<WsComponentMeta>\n): Map<string, WsComponentMeta> => {\n  const base = {\n    label: \"\",\n    Icon: () => null,\n  };\n  const configs = {\n    Item: { ...base, ...itemMeta },\n    AnotherItem: { ...base, ...anotherItemMeta },\n    Bold: base,\n    Text: base,\n    Form: base,\n    Box: base,\n    Div: base,\n    Body: base,\n  } as const;\n  return new Map(Object.entries(configs)) as Map<string, WsComponentMeta>;\n};\n\nconst getIdValuePair = <T extends { id: string }>(item: T) =>\n  [item.id, item] as const;\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map(getIdValuePair));\n\nconst setDataStores = (data: Omit<WebstudioData, \"pages\">) => {\n  $instances.set(data.instances);\n  $breakpoints.set(data.breakpoints);\n  $styleSources.set(data.styleSources);\n  $styles.set(data.styles);\n  $styleSourceSelections.set(data.styleSourceSelections);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  $assets.set(data.assets);\n  $resources.set(data.resources);\n};\n\nconst createInstance = (\n  id: Instance[\"id\"],\n  component: string,\n  children: Instance[\"children\"]\n): Instance => {\n  return { type: \"instance\", id, component, children };\n};\n\nconst createStyleDecl = (\n  styleSourceId: string,\n  breakpointId: string,\n  property: StyleProperty,\n  value: StyleValue | string\n): StyleDecl => ({\n  styleSourceId,\n  breakpointId,\n  property,\n  value: typeof value === \"string\" ? { type: \"unparsed\", value } : value,\n});\n\nconst createStyleDeclPair = (\n  styleSourceId: string,\n  breakpointId: string,\n  property: StyleProperty,\n  value: StyleValue | string\n): [StyleDeclKey, StyleDecl] => [\n  `${styleSourceId}:${breakpointId}:${property}:`,\n  createStyleDecl(styleSourceId, breakpointId, property, value),\n];\n\nconst createImageAsset = (id: string, name = \"\", projectId = \"\"): Asset => {\n  return {\n    id,\n    type: \"image\",\n    format: \"\",\n    name,\n    description: \"\",\n    projectId,\n    createdAt: \"\",\n    size: 0,\n    meta: { width: 0, height: 0 },\n  };\n};\n\nconst createFontAsset = (id: string, family: string): Asset => {\n  return {\n    id,\n    type: \"font\",\n    format: \"woff\",\n    name: \"\",\n    description: \"\",\n    projectId: \"\",\n    createdAt: \"\",\n    size: 0,\n    meta: { style: \"normal\", family, variationAxes: {} },\n  };\n};\n\ndescribe(\"insert instance children\", () => {\n  test(\"insert instance children into empty target\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\"></ws.element>\n    );\n    const [div] = renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    ).instances;\n    data.instances.set(div.id, div);\n    insertInstanceChildrenMutable(data, [{ type: \"id\", value: \"divId\" }], {\n      parentSelector: [\"bodyId\"],\n      position: \"end\",\n    });\n    expect(data).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"insert instance children into the end of target\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"textId\"></ws.element>\n      </ws.element>\n    );\n    const [div] = renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    ).instances;\n    data.instances.set(div.id, div);\n    insertInstanceChildrenMutable(data, [{ type: \"id\", value: \"divId\" }], {\n      parentSelector: [\"bodyId\"],\n      position: \"end\",\n    });\n    expect(data).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"textId\"></ws.element>\n          <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"insert instance children into the start of target\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"textId\"></ws.element>\n      </ws.element>\n    );\n    const [div] = renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    ).instances;\n    data.instances.set(div.id, div);\n    insertInstanceChildrenMutable(data, [{ type: \"id\", value: \"divId\" }], {\n      parentSelector: [\"bodyId\"],\n      position: 0,\n    });\n    expect(data).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n          <ws.element ws:tag=\"div\" ws:id=\"textId\"></ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"insert instance children at the start of text\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"textId\">\n          text\n        </ws.element>\n      </ws.element>\n    );\n    const [div] = renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    ).instances;\n    data.instances.set(div.id, div);\n    insertInstanceChildrenMutable(data, [{ type: \"id\", value: \"divId\" }], {\n      parentSelector: [\"textId\", \"bodyId\"],\n      position: 0,\n    });\n    const [_bodyId, _textId, _divId, spanId] = data.instances.keys();\n    expect(data).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"textId\">\n            <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n            <ws.element ws:tag=\"span\" ws:id={spanId}>\n              text\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"insert instance children at the end of text\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"textId\">\n          text\n        </ws.element>\n      </ws.element>\n    );\n    const [div] = renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    ).instances;\n    data.instances.set(div.id, div);\n    insertInstanceChildrenMutable(data, [{ type: \"id\", value: \"divId\" }], {\n      parentSelector: [\"textId\", \"bodyId\"],\n      position: \"end\",\n    });\n    const [_bodyId, _textId, _divId, spanId] = data.instances.keys();\n    expect(data).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"textId\">\n            <ws.element ws:tag=\"span\" ws:id={spanId}>\n              text\n            </ws.element>\n            <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n\n  test(\"insert instance children between text children\", () => {\n    const data = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"textId\">\n          <ws.element ws:tag=\"strong\" ws:id=\"strongId\">\n            strong\n          </ws.element>\n          text\n          <ws.element ws:tag=\"em\" ws:id=\"emId\">\n            emphasis\n          </ws.element>\n        </ws.element>\n      </ws.element>\n    );\n    const [div] = renderTemplate(\n      <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n    ).instances;\n    data.instances.set(div.id, div);\n    insertInstanceChildrenMutable(data, [{ type: \"id\", value: \"divId\" }], {\n      parentSelector: [\"textId\", \"bodyId\"],\n      position: 1,\n    });\n    const [\n      _bodyId,\n      _textId,\n      _strongId,\n      _emId,\n      _divId,\n      leftSpanId,\n      rightSpanId,\n    ] = data.instances.keys();\n    expect(data).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"textId\">\n            <ws.element ws:tag=\"span\" ws:id={leftSpanId}>\n              <ws.element ws:tag=\"strong\" ws:id=\"strongId\">\n                strong\n              </ws.element>\n            </ws.element>\n            <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n            <ws.element ws:tag=\"span\" ws:id={rightSpanId}>\n              text\n              <ws.element ws:tag=\"em\" ws:id=\"emId\">\n                emphasis\n              </ws.element>\n            </ws.element>\n          </ws.element>\n        </ws.element>\n      )\n    );\n  });\n});\n\ndescribe(\"insert webstudio element at\", () => {\n  beforeEach(() => {\n    $styleSourceSelections.set(new Map());\n    $styleSources.set(new Map());\n    $breakpoints.set(new Map());\n    $styles.set(new Map());\n    $dataSources.set(new Map());\n    $resources.set(new Map());\n    $props.set(new Map());\n    $assets.set(new Map());\n  });\n\n  test(\"insert element with div tag into body\", () => {\n    $instances.set(renderData(<$.Body ws:id=\"bodyId\"></$.Body>).instances);\n    insertWebstudioElementAt({\n      parentSelector: [\"bodyId\"],\n      position: \"end\",\n    });\n    const [_bodyId, newInstanceId] = $instances.get().keys();\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id={newInstanceId} ws:tag=\"div\" />\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"insert element with li tag into ul\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"listId\" ws:tag=\"ul\"></ws.element>\n        </$.Body>\n      ).instances\n    );\n    insertWebstudioElementAt({\n      parentSelector: [\"listId\", \"bodyId\"],\n      position: \"end\",\n    });\n    const [_bodyId, _listId, newInstanceId] = $instances.get().keys();\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"listId\" ws:tag=\"ul\">\n            <ws.element ws:id={newInstanceId} ws:tag=\"li\" />\n          </ws.element>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"insert element into selected instance\", () => {\n    $pages.set(\n      createDefaultPages({ homePageId: \"homePageId\", rootInstanceId: \"bodyId\" })\n    );\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"divId\" ws:tag=\"div\"></ws.element>\n        </$.Body>\n      ).instances\n    );\n    selectPage(\"homePageId\");\n    selectInstance([\"divId\", \"bodyId\"]);\n    insertWebstudioElementAt();\n    const [_bodyId, _divId, newInstanceId] = $instances.get().keys();\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"divId\" ws:tag=\"div\">\n            <ws.element ws:id={newInstanceId} ws:tag=\"div\" />\n          </ws.element>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"insert element into closest non-textual container\", () => {\n    $pages.set(\n      createDefaultPages({ homePageId: \"homePageId\", rootInstanceId: \"bodyId\" })\n    );\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"divId\" ws:tag=\"div\">\n            text\n          </ws.element>\n          <ws.element ws:id=\"spanId\" ws:tag=\"span\"></ws.element>\n        </$.Body>\n      ).instances\n    );\n    selectPage(\"homePageId\");\n    selectInstance([\"divId\", \"bodyId\"]);\n    insertWebstudioElementAt();\n    const [_bodyId, _divId, _spanId, newInstanceId] = $instances.get().keys();\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"divId\" ws:tag=\"div\">\n            text\n          </ws.element>\n          <ws.element ws:id={newInstanceId} ws:tag=\"div\" />\n          <ws.element ws:id=\"spanId\" ws:tag=\"span\"></ws.element>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"insert element into closest non-empty container\", () => {\n    $pages.set(\n      createDefaultPages({ homePageId: \"homePageId\", rootInstanceId: \"bodyId\" })\n    );\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"imgId\" ws:tag=\"img\"></ws.element>\n          <ws.element ws:id=\"spanId\" ws:tag=\"span\"></ws.element>\n        </$.Body>\n      ).instances\n    );\n    selectPage(\"homePageId\");\n    selectInstance([\"imgId\", \"bodyId\"]);\n    insertWebstudioElementAt();\n    const [_bodyId, _imgId, _spanId, newInstanceId] = $instances.get().keys();\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:id=\"imgId\" ws:tag=\"img\"></ws.element>\n          <ws.element ws:id={newInstanceId} ws:tag=\"div\" />\n          <ws.element ws:id=\"spanId\" ws:tag=\"span\"></ws.element>\n        </$.Body>\n      ).instances\n    );\n  });\n});\n\ndescribe(\"insert webstudio fragment at\", () => {\n  beforeEach(() => {\n    $styleSourceSelections.set(new Map());\n    $styleSources.set(new Map());\n    $breakpoints.set(new Map());\n    $styles.set(new Map());\n    $dataSources.set(new Map());\n    $resources.set(new Map());\n    $props.set(new Map());\n    $assets.set(new Map());\n  });\n\n  test(\"insert multiple instances\", () => {\n    $instances.set(renderData(<$.Body ws:id=\"bodyId\"></$.Body>).instances);\n    insertWebstudioFragmentAt(\n      renderTemplate(\n        <>\n          <$.Heading ws:id=\"headingId\"></$.Heading>\n          <$.Paragraph ws:id=\"paragraphId\"></$.Paragraph>\n        </>\n      ),\n      {\n        parentSelector: [\"bodyId\"],\n        position: \"end\",\n      }\n    );\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Heading ws:id={expect.any(String)}></$.Heading>\n          <$.Paragraph ws:id={expect.any(String)}></$.Paragraph>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"insert fragment after insertable\", () => {\n    $instances.set(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </$.Body>\n      ).instances\n    );\n    insertWebstudioFragmentAt(\n      renderTemplate(<$.Heading ws:id=\"headingId\"></$.Heading>),\n      {\n        parentSelector: [\"boxId\", \"bodyId\"],\n        position: \"after\",\n      }\n    );\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n          <$.Heading ws:id={expect.any(String)}></$.Heading>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"insert fragment inside of body when configured to place after insertable\", () => {\n    $instances.set(renderData(<$.Body ws:id=\"bodyId\"></$.Body>).instances);\n    insertWebstudioFragmentAt(\n      renderTemplate(<$.Heading ws:id=\"headingId\"></$.Heading>),\n      {\n        parentSelector: [\"bodyId\"],\n        position: \"after\",\n      }\n    );\n    expect($instances.get()).toEqual(\n      renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Heading ws:id={expect.any(String)}></$.Heading>\n        </$.Body>\n      ).instances\n    );\n  });\n});\n\ndescribe(\"reparent instance\", () => {\n  test(\"between instances\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\">\n          <$.Text ws:id=\"text\"></$.Text>\n        </$.Box>\n        <$.Button ws:id=\"button\"></$.Button>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"text\", \"box\", \"body\"], {\n      parentSelector: [\"body\"],\n      position: 1,\n    });\n    const newTextId = data.instances.get(\"body\")?.children[1].value as string;\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n          <$.Text ws:id={newTextId}></$.Text>\n          <$.Button ws:id=\"button\"></$.Button>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"to the end\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\">\n          <$.Text ws:id=\"text\"></$.Text>\n        </$.Box>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"text\", \"box\", \"body\"], {\n      parentSelector: [\"body\"],\n      position: \"end\",\n    });\n    const newTextId = data.instances.get(\"body\")?.children[1].value as string;\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box ws:id=\"box\"></$.Box>\n          <$.Text ws:id={newTextId}></$.Text>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"before itself\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Text ws:id=\"text\"></$.Text>\n        <$.Box ws:id=\"box\"></$.Box>\n        <$.Button ws:id=\"button\"></$.Button>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"box\", \"body\"], {\n      parentSelector: [\"body\"],\n      position: 1,\n    });\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Text ws:id=\"text\"></$.Text>\n          <$.Box ws:id=\"box\"></$.Box>\n          <$.Button ws:id=\"button\"></$.Button>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"after itself\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Text ws:id=\"text\"></$.Text>\n        <$.Box ws:id=\"box\"></$.Box>\n        <$.Button ws:id=\"button\"></$.Button>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"box\", \"body\"], {\n      parentSelector: [\"body\"],\n      position: 2,\n    });\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Text ws:id=\"text\"></$.Text>\n          <$.Box ws:id=\"box\"></$.Box>\n          <$.Button ws:id=\"button\"></$.Button>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"wrap with fragment when reparent into empty slot\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Slot ws:id=\"slot\"></$.Slot>\n        <$.Box ws:id=\"box\"></$.Box>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"box\", \"body\"], {\n      parentSelector: [\"slot\", \"body\"],\n      position: \"end\",\n    });\n    const newFragmentId = data.instances.get(\"slot\")?.children[0]\n      .value as string;\n    const newBoxId = data.instances.get(newFragmentId)?.children[0]\n      .value as string;\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Slot ws:id=\"slot\">\n            <$.Fragment ws:id={newFragmentId}>\n              <$.Box ws:id={newBoxId}></$.Box>\n            </$.Fragment>\n          </$.Slot>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"reuse existing fragment when reparent into slot\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Slot ws:id=\"slot\">\n          <$.Fragment ws:id=\"fragment\"></$.Fragment>\n        </$.Slot>\n        <$.Box ws:id=\"box\"></$.Box>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"box\", \"body\"], {\n      parentSelector: [\"slot\", \"body\"],\n      position: \"end\",\n    });\n    const newBoxId = data.instances.get(\"fragment\")?.children[0]\n      .value as string;\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Slot ws:id=\"slot\">\n            <$.Fragment ws:id=\"fragment\">\n              <$.Box ws:id={newBoxId}></$.Box>\n            </$.Fragment>\n          </$.Slot>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"reparent slot child from one instance of this slot into another\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Slot ws:id=\"slot1\">\n          <$.Fragment ws:id=\"fragment\">\n            <$.Box ws:id=\"box\"></$.Box>\n          </$.Fragment>\n        </$.Slot>\n        <$.Slot ws:id=\"slot2\">\n          {/* same ids */}\n          <$.Fragment ws:id=\"fragment\">\n            <$.Box ws:id=\"box\"></$.Box>\n          </$.Fragment>\n        </$.Slot>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"box\", \"fragment\", \"slot1\", \"body\"], {\n      parentSelector: [\"slot2\", \"body\"],\n      position: \"end\",\n    });\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Slot ws:id=\"slot1\">\n            <$.Fragment ws:id=\"fragment\">\n              <$.Box ws:id=\"box\"></$.Box>\n            </$.Fragment>\n          </$.Slot>\n          <$.Slot ws:id=\"slot2\">\n            {/* same ids */}\n            <$.Fragment ws:id=\"fragment\">\n              <$.Box ws:id=\"box\"></$.Box>\n            </$.Fragment>\n          </$.Slot>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"prevent slot reparenting into own children to avoid infinite loop\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Slot ws:id=\"slot\">\n          <$.Fragment ws:id=\"fragment\"></$.Fragment>\n        </$.Slot>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(data, [\"slot\", \"body\"], {\n      parentSelector: [\"fragment\", \"slot\", \"body\"],\n      position: \"end\",\n    });\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Slot ws:id=\"slot\">\n            <$.Fragment ws:id=\"fragment\"></$.Fragment>\n          </$.Slot>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"from collection item\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <ws.collection ws:id=\"collection\">\n          <$.Box ws:id=\"box\"></$.Box>\n        </ws.collection>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(createFakeComponentMetas({}));\n    reparentInstanceMutable(\n      data,\n      [\"box\", \"collection[0]\", \"collection\", \"body\"],\n      { parentSelector: [\"body\"], position: \"end\" }\n    );\n    const newBoxId = data.instances.get(\"body\")?.children[1].value as string;\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <ws.collection ws:id=\"collection\"></ws.collection>\n          <$.Box ws:id={newBoxId}></$.Box>\n        </$.Body>\n      ).instances\n    );\n  });\n\n  test(\"move required child within same instance\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Tooltip ws:id=\"tooltip\">\n          <$.TooltipTrigger ws:id=\"trigger\"></$.TooltipTrigger>\n          <$.TooltipContent ws:id=\"content\"></$.TooltipContent>\n        </$.Tooltip>\n      </$.Body>\n    );\n    $registeredComponentMetas.set(\n      new Map(Object.entries({ ...defaultMetas, ...radixMetas }))\n    );\n    reparentInstanceMutable(data, [\"trigger\", \"tooltip\", \"body\"], {\n      parentSelector: [\"tooltip\", \"body\"],\n      position: \"end\",\n    });\n    expect(data.instances).toEqual(\n      renderData(\n        <$.Body ws:id=\"body\">\n          <$.Tooltip ws:id=\"tooltip\">\n            <$.TooltipContent ws:id=\"content\"></$.TooltipContent>\n            <$.TooltipTrigger ws:id={expect.any(String)}></$.TooltipTrigger>\n          </$.Tooltip>\n        </$.Body>\n      ).instances\n    );\n  });\n});\n\nconst getWebstudioDataStub = (\n  data?: Partial<WebstudioData>\n): WebstudioData => ({\n  pages: createDefaultPages({ rootInstanceId: \"\" }),\n  assets: new Map(),\n  dataSources: new Map(),\n  resources: new Map(),\n  instances: new Map(),\n  props: new Map(),\n  breakpoints: new Map(),\n  styleSourceSelections: new Map(),\n  styleSources: new Map(),\n  styles: new Map(),\n  ...data,\n});\n\ndescribe(\"delete instance\", () => {\n  test(\"delete instance with its children\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\">\n          <$.Box ws:id=\"sectionId\"></$.Box>\n        </$.Box>\n        <$.Box ws:id=\"divId\"></$.Box>\n      </$.Body>\n    );\n    expect(data.instances.size).toEqual(4);\n    expect(data.instances.get(\"bodyId\")?.children.length).toEqual(2);\n    deleteInstanceMutable(\n      data,\n      // clone to make sure data is mutated instead of instance path\n      structuredClone(getInstancePath([\"boxId\", \"bodyId\"], data.instances))\n    );\n    expect(data.instances.size).toEqual(2);\n    expect(data.instances.get(\"bodyId\")?.children.length).toEqual(1);\n  });\n\n  test(\"delete instance from collection\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <ws.collection ws:id=\"collectionId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </ws.collection>\n      </$.Body>\n    );\n    expect(data.instances.size).toEqual(3);\n    expect(data.instances.get(\"collectionId\")?.children.length).toEqual(1);\n    deleteInstanceMutable(\n      data,\n      getInstancePath(\n        [\"boxId\", \"collectionId[0]\", \"collectionId\", \"bodyId\"],\n        data.instances\n      )\n    );\n    expect(data.instances.size).toEqual(2);\n    expect(data.instances.get(\"collectionId\")?.children.length).toEqual(0);\n  });\n\n  test(\"delete instance from collection item\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <ws.collection ws:id=\"collectionId\">\n          <$.Box ws:id=\"boxId\">\n            <$.Text ws:id=\"textId\"></$.Text>\n          </$.Box>\n        </ws.collection>\n      </$.Body>\n    );\n    expect(data.instances.size).toEqual(4);\n    expect(data.instances.get(\"boxId\")?.children.length).toEqual(1);\n    deleteInstanceMutable(\n      data,\n      getInstancePath(\n        [\"textId\", \"boxId\", \"collectionId[0]\", \"collectionId\", \"bodyId\"],\n        data.instances\n      )\n    );\n    expect(data.instances.size).toEqual(3);\n    expect(data.instances.get(\"boxId\")?.children.length).toEqual(0);\n  });\n\n  test(\"delete resource bound to variable\", () => {\n    const myResource = new ResourceValue(\"My Resource\", {\n      url: expression`\"\"`,\n      method: \"get\",\n      searchParams: [],\n      headers: [],\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" vars={expression`${myResource}`}></$.Box>\n      </$.Body>\n    );\n    expect(data.resources.size).toEqual(1);\n    expect(data.dataSources.size).toEqual(1);\n    deleteInstanceMutable(\n      data,\n      getInstancePath([\"boxId\", \"bodyId\"], data.instances)\n    );\n    expect(data.resources.size).toEqual(0);\n    expect(data.dataSources.size).toEqual(0);\n  });\n\n  test(\"delete resource bound to prop\", () => {\n    const myResource = new ResourceValue(\"My Resource\", {\n      url: expression`\"\"`,\n      method: \"get\",\n      searchParams: [],\n      headers: [],\n    });\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" action={myResource}></$.Box>\n      </$.Body>\n    );\n    expect(data.resources.size).toEqual(1);\n    expect(data.props.size).toEqual(1);\n    deleteInstanceMutable(\n      data,\n      getInstancePath([\"boxId\", \"bodyId\"], data.instances)\n    );\n    expect(data.resources.size).toEqual(0);\n    expect(data.props.size).toEqual(0);\n  });\n\n  test(\"delete unknown instance (just in case)\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Invalid ws:id=\"invalidId\"></$.Invalid>\n      </$.Body>\n    );\n    expect(data.instances.size).toEqual(2);\n    deleteInstanceMutable(\n      data,\n      getInstancePath([\"invalidId\", \"bodyId\"], data.instances)\n    );\n    expect(data.instances.size).toEqual(1);\n  });\n\n  test(\"delete slot fragment along with last child\", () => {\n    const data = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Slot ws:id=\"slotId\">\n          <$.Fragment ws:id=\"fragmentId\">\n            <$.Box ws:id=\"boxId\"></$.Box>\n          </$.Fragment>\n        </$.Slot>\n      </$.Body>\n    );\n    expect(data.instances.size).toEqual(4);\n    expect(data.instances.get(\"fragmentId\")?.children.length).toEqual(1);\n    deleteInstanceMutable(\n      data,\n      getInstancePath(\n        [\"boxId\", \"fragmentId\", \"slotId\", \"bodyId\"],\n        data.instances\n      )\n    );\n    expect(data.instances.size).toEqual(2);\n    expect(data.instances.get(\"slotId\")?.children.length).toEqual(0);\n  });\n});\n\ndescribe(\"wrap in\", () => {\n  test(\"wrap instance in link\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"divId\", \"bodyId\"]);\n    wrapInstance(elementComponent, \"a\");\n    expect($instances.get()).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"a\" ws:id={expect.any(String)}>\n            <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ).instances\n    );\n  });\n\n  test(\"wrap image in element\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"img\" ws:id=\"imageId\"></ws.element>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"imageId\", \"bodyId\"]);\n    wrapInstance(elementComponent);\n    expect($instances.get()).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id={expect.any(String)}>\n            <ws.element ws:tag=\"img\" ws:id=\"imageId\"></ws.element>\n          </ws.element>\n        </ws.element>\n      ).instances\n    );\n  });\n\n  test(\"avoid wrapping text with link in link\", () => {\n    const { instances } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"p\" ws:id=\"textId\">\n          <ws.element ws:tag=\"a\" ws:id=\"linkId\"></ws.element>\n        </ws.element>\n      </ws.element>\n    );\n    $instances.set(instances);\n    selectInstance([\"textId\", \"bodyId\"]);\n    wrapInstance(elementComponent, \"a\");\n    // nothing is changed\n    expect($instances.get()).toEqual(instances);\n  });\n\n  test(\"avoid wrapping textual content\", () => {\n    const { instances } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"div\" ws:id=\"textId\">\n          <ws.element ws:tag=\"bold\" ws:id=\"boldId\"></ws.element>\n        </ws.element>\n      </ws.element>\n    );\n    $instances.set(instances);\n    selectInstance([\"boldId\", \"textId\", \"bodyId\"]);\n    wrapInstance(elementComponent);\n    // nothing is changed\n    expect($instances.get()).toEqual(instances);\n  });\n\n  test(\"avoid wrapping list item\", () => {\n    const { instances } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"ul\" ws:id=\"listId\">\n          <ws.element ws:tag=\"li\" ws:id=\"listItemId\"></ws.element>\n        </ws.element>\n      </ws.element>\n    );\n    $instances.set(instances);\n    selectInstance([\"listItemId\", \"listId\", \"bodyId\"]);\n    wrapInstance(elementComponent);\n    // nothing is changed\n    expect($instances.get()).toEqual(instances);\n  });\n});\n\ndescribe(\"extract webstudio fragment\", () => {\n  test(\"collect all styles and breakpoints bound to fragment instances\", () => {\n    // body\n    //   box1\n    //     box2\n    $instances.set(\n      toMap([\n        createInstance(\"body\", \"Body\", [{ type: \"id\", value: \"box1\" }]),\n        createInstance(\"box1\", \"Box\", [{ type: \"id\", value: \"box2\" }]),\n        createInstance(\"box2\", \"Box\", []),\n      ])\n    );\n    $styleSourceSelections.set(\n      new Map([\n        [\"body\", { instanceId: \"box1\", values: [\"localBody\", \"token1\"] }],\n        [\"box1\", { instanceId: \"box1\", values: [\"localBox1\", \"token2\"] }],\n        [\"box2\", { instanceId: \"box2\", values: [\"localBox2\", \"token2\"] }],\n      ])\n    );\n    $styleSources.set(\n      new Map([\n        [\"localBody\", { id: \"localBody\", type: \"local\" }],\n        [\"localBox1\", { id: \"localBox1\", type: \"local\" }],\n        [\"localBox2\", { id: \"localBox2\", type: \"local\" }],\n        [\"token1\", { id: \"token1\", type: \"token\", name: \"token1\" }],\n        [\"token2\", { id: \"token2\", type: \"token\", name: \"token2\" }],\n      ])\n    );\n    $styles.set(\n      new Map([\n        createStyleDeclPair(\"localBody1\", \"base\", \"color\", \"red\"),\n        createStyleDeclPair(\"localBox1\", \"base\", \"color\", \"green\"),\n        createStyleDeclPair(\"localBox2\", \"base\", \"color\", \"blue\"),\n        createStyleDeclPair(\"token1\", \"base\", \"color\", \"yellow\"),\n        createStyleDeclPair(\"token2\", \"base\", \"color\", \"orange\"),\n      ])\n    );\n    $breakpoints.set(\n      new Map([\n        [\"base\", { id: \"base\", label: \"base\" }],\n        [\"big\", { id: \"big\", label: \"big\", minWidth: 768 }],\n      ])\n    );\n    const { styleSources, styleSourceSelections, styles, breakpoints } =\n      extractWebstudioFragment(getWebstudioData(), \"box1\");\n\n    // exclude localBody and token1 bound to body\n    expect(styleSources).toEqual([\n      { id: \"localBox1\", type: \"local\" },\n      { id: \"token2\", type: \"token\", name: \"token2\" },\n      { id: \"localBox2\", type: \"local\" },\n    ]);\n    expect(styleSourceSelections).toEqual([\n      { instanceId: \"box1\", values: [\"localBox1\", \"token2\"] },\n      { instanceId: \"box2\", values: [\"localBox2\", \"token2\"] },\n    ]);\n    expect(styles).toEqual([\n      createStyleDecl(\"localBox1\", \"base\", \"color\", \"green\"),\n      createStyleDecl(\"localBox2\", \"base\", \"color\", \"blue\"),\n      createStyleDecl(\"token2\", \"base\", \"color\", \"orange\"),\n    ]);\n    expect(breakpoints).toEqual([{ id: \"base\", label: \"base\" }]);\n  });\n\n  test(\"collect assets from props and styles withiin fragment instances\", () => {\n    // body\n    //   box1\n    //     box2\n    $instances.set(\n      toMap([\n        createInstance(\"body\", \"Body\", [{ type: \"id\", value: \"box1\" }]),\n        createInstance(\"box1\", \"Box\", [{ type: \"id\", value: \"box2\" }]),\n        createInstance(\"box2\", \"Box\", []),\n      ])\n    );\n    $props.set(\n      new Map([\n        [\n          \"bodyProp\",\n          {\n            id: \"bodyProp\",\n            instanceId: \"body\",\n            name: \"data-body\",\n            type: \"asset\",\n            value: \"asset1\",\n          },\n        ],\n        [\n          \"box1Prop\",\n          {\n            id: \"box1Prop\",\n            instanceId: \"box1\",\n            name: \"data-box1\",\n            type: \"asset\",\n            value: \"asset2\",\n          },\n        ],\n      ])\n    );\n    $styleSourceSelections.set(\n      new Map([\n        [\"body\", { instanceId: \"box1\", values: [\"localBody\"] }],\n        [\"box1\", { instanceId: \"box1\", values: [\"localBox1\"] }],\n      ])\n    );\n    $styleSources.set(\n      new Map([\n        [\"localBody\", { id: \"localBody\", type: \"local\" }],\n        [\"localBox1\", { id: \"localBox1\", type: \"local\" }],\n      ])\n    );\n    $styles.set(\n      new Map([\n        createStyleDeclPair(\"localBody1\", \"base\", \"fontFamily\", {\n          type: \"fontFamily\",\n          value: [\"font1\"],\n        }),\n        createStyleDeclPair(\"localBody1\", \"base\", \"backgroundImage\", {\n          type: \"image\",\n          value: { type: \"asset\", value: \"asset3\" },\n        }),\n        createStyleDeclPair(\"localBox1\", \"base\", \"fontFamily\", {\n          type: \"fontFamily\",\n          value: [\"font2\"],\n        }),\n        createStyleDeclPair(\"localBox1\", \"base\", \"color\", {\n          type: \"image\",\n          value: { type: \"asset\", value: \"asset4\" },\n        }),\n      ])\n    );\n    $breakpoints.set(new Map([[\"base\", { id: \"base\", label: \"base\" }]]));\n    $assets.set(\n      toMap([\n        createImageAsset(\"asset1\"),\n        createImageAsset(\"asset2\"),\n        createImageAsset(\"asset3\"),\n        createImageAsset(\"asset4\"),\n        createFontAsset(\"asset5\", \"font1\"),\n        createFontAsset(\"asset6\", \"font2\"),\n      ])\n    );\n    const { assets } = extractWebstudioFragment(getWebstudioData(), \"box1\");\n\n    expect(assets).toEqual([\n      createImageAsset(\"asset2\"),\n      createImageAsset(\"asset4\"),\n      createFontAsset(\"asset6\", \"font2\"),\n    ]);\n  });\n});\n\ndescribe(\"insert webstudio fragment copy\", () => {\n  const emptyFragment = {\n    children: [],\n    instances: [\n      {\n        id: \"body\",\n        type: \"instance\",\n        component: \"Body\",\n        children: [],\n      } satisfies Instance,\n    ],\n    styleSourceSelections: [],\n    styleSources: [],\n    breakpoints: [],\n    styles: [],\n    dataSources: [],\n    resources: [],\n    props: [],\n    assets: [],\n  };\n\n  $project.set({ id: \"current_project\" } as Project);\n\n  beforeEach(() => {\n    $assets.set(new Map());\n    $breakpoints.set(new Map());\n    $styleSourceSelections.set(new Map());\n    $styleSources.set(new Map());\n    $styles.set(new Map());\n    $instances.set(new Map());\n    $props.set(new Map());\n    $dataSources.set(new Map());\n  });\n\n  test(\"insert assets with same ids if missing\", () => {\n    const data = getWebstudioDataStub();\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        assets: [createImageAsset(\"asset1\", \"name\", \"another_project\")],\n      },\n      availableVariables: [],\n      projectId: \"current_project\",\n    });\n    expect(Array.from(data.assets.values())).toEqual([\n      createImageAsset(\"asset1\", \"name\", \"current_project\"),\n    ]);\n\n    data.assets = toMap([\n      createImageAsset(\"asset1\", \"changed_name\", \"current_project\"),\n    ]);\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        assets: [\n          createImageAsset(\"asset1\", \"name\", \"another_project\"),\n          createImageAsset(\"asset2\", \"another_name\", \"another_project\"),\n        ],\n      },\n      availableVariables: [],\n      projectId: \"current_project\",\n    });\n    expect(Array.from(data.assets.values())).toEqual([\n      // preserve any user changes\n      createImageAsset(\"asset1\", \"changed_name\", \"current_project\"),\n      // add new assets while preserving old ones\n      createImageAsset(\"asset2\", \"another_name\", \"current_project\"),\n    ]);\n  });\n\n  test(\"merge breakpoints with existing ones\", () => {\n    const breakpoints = toMap<Breakpoint>([\n      { id: \"existing_base\", label: \"base\" },\n      { id: \"existing_small\", label: \"small\", minWidth: 768 },\n    ]);\n    const data = getWebstudioDataStub({ breakpoints });\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [\n          { id: \"new_base\", label: \"Base\" },\n          {\n            id: \"new_small\",\n            label: \"Small\",\n            minWidth: 768,\n          },\n          {\n            id: \"new_large\",\n            label: \"Large\",\n            minWidth: 1200,\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.breakpoints.values())).toEqual([\n      { id: \"existing_base\", label: \"base\" },\n      { id: \"existing_small\", label: \"small\", minWidth: 768 },\n      { id: \"new_large\", label: \"Large\", minWidth: 1200 },\n    ]);\n  });\n\n  // Case 2: Same styles AND same name -> reuse existing token\n  test(\"token with same styles and same name reuses existing token\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"existingToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDeclPair(\"existingToken\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Same name \"primaryColor\", same styles, different id\n          { id: \"newToken\", type: \"token\", name: \"primaryColor\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"newToken\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n          {\n            styleSourceId: \"newToken\",\n            breakpointId: \"base\",\n            property: \"fontSize\",\n            value: { type: \"unit\", value: 16, unit: \"px\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should reuse existing token, not create a new one\n    expect(Array.from(data.styleSources.values())).toEqual([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    // No new styles should be added\n    expect(Array.from(data.styles.values())).toEqual([\n      {\n        styleSourceId: \"existingToken\",\n        breakpointId: \"base\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n      {\n        styleSourceId: \"existingToken\",\n        breakpointId: \"base\",\n        property: \"fontSize\",\n        value: { type: \"unit\", value: 16, unit: \"px\" },\n      },\n    ]);\n  });\n\n  // Case 3: Same styles but different name -> insert new token with original name\n  test(\"token with same styles but different name inserts new token\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Different name \"accentColor\", same styles\n          { id: \"token2\", type: \"token\", name: \"accentColor\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"token2\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should insert new token with its original name\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"accentColor\" });\n    expect(tokens[1].id).not.toBe(\"token2\"); // Should have new ID\n\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(2);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"token1\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(tokenStyles[1]).toMatchObject({\n      styleSourceId: tokens[1].id, // Should reference new token ID\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n  });\n\n  // Case 4: Different styles but same name -> add counter suffix\n  test(\"token with different styles but same name adds counter suffix\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"myToken\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Same name \"myToken\", different styles\n          { id: \"token2\", type: \"token\", name: \"myToken\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"token2\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should add counter suffix to the new token\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({ id: \"token1\", type: \"token\", name: \"myToken\" });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"myToken-1\" });\n    expect(tokens[1].id).not.toBe(\"token2\"); // Should have new ID\n\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(2);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"token1\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    expect(tokenStyles[1]).toMatchObject({\n      styleSourceId: tokens[1].id,\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n  });\n\n  // Case 4b: Multiple counter suffixes\n  test(\"token with name conflict increments counter correctly\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"myToken\" },\n      { id: \"token2\", type: \"token\", name: \"myToken-1\" },\n      { id: \"token3\", type: \"token\", name: \"myToken-2\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n      createStyleDeclPair(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"green\",\n      }),\n      createStyleDeclPair(\"token3\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"yellow\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [{ id: \"token4\", type: \"token\", name: \"myToken\" }],\n        styles: [\n          {\n            styleSourceId: \"token4\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should use counter 3\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(4);\n    expect(tokens[0]).toEqual({ id: \"token1\", type: \"token\", name: \"myToken\" });\n    expect(tokens[1]).toEqual({\n      id: \"token2\",\n      type: \"token\",\n      name: \"myToken-1\",\n    });\n    expect(tokens[2]).toEqual({\n      id: \"token3\",\n      type: \"token\",\n      name: \"myToken-2\",\n    });\n    expect(tokens[3]).toMatchObject({ type: \"token\", name: \"myToken-3\" });\n    expect(tokens[3].id).not.toBe(\"token4\"); // Should have new ID\n  });\n\n  // Case 6: Different styles and different name -> insert as-is\n  test(\"token with different styles and different name inserts normally\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [{ id: \"token2\", type: \"token\", name: \"secondaryColor\" }],\n        styles: [\n          {\n            styleSourceId: \"token2\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should insert new token normally\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"secondaryColor\" });\n    expect(tokens[1].id).not.toBe(\"token2\"); // Should have new ID\n\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(2);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"token1\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    expect(tokenStyles[1]).toMatchObject({\n      styleSourceId: tokens[1].id,\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n  });\n\n  // Test that instance with matching token gets the token reference updated\n  test(\"instance with reused token updates styleSourceSelection to existing token id\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"existingToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"box\",\n            component: \"Box\",\n            children: [],\n          },\n        ],\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Same name and same styles as existingToken\n          { id: \"newToken\", type: \"token\", name: \"primaryColor\" },\n        ],\n        styleSourceSelections: [{ instanceId: \"box\", values: [\"newToken\"] }],\n        styles: [\n          {\n            styleSourceId: \"newToken\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should reuse existing token\n    expect(Array.from(data.styleSources.values())).toEqual([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n\n    // The instance should reference the existing token, not the new one\n    const newBoxId = Array.from(data.instances.keys())[0];\n    expect(data.styleSourceSelections.get(newBoxId)).toEqual({\n      instanceId: newBoxId,\n      values: [\"existingToken\"], // Should use existing token id, not \"newToken\"\n    });\n  });\n\n  // Case 3 safeguard: Same styles but different name gets suffix when name conflicts\n  test(\"token with same styles but different name adds suffix when name already exists\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n      { id: \"token2\", type: \"token\", name: \"accentColor\" }, // This name is taken\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDeclPair(\"token2\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Same styles as token1, but wants to use name \"accentColor\" which is already taken\n          { id: \"token3\", type: \"token\", name: \"accentColor\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"token3\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should add counter suffix to prevent duplicate name\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(3);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toEqual({\n      id: \"token2\",\n      type: \"token\",\n      name: \"accentColor\",\n    });\n    expect(tokens[2]).toMatchObject({ type: \"token\", name: \"accentColor-1\" });\n    expect(tokens[2].id).not.toBe(\"token3\"); // Should have new ID\n\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(3);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"token1\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(tokenStyles[1]).toEqual({\n      styleSourceId: \"token2\",\n      breakpointId: \"base\",\n      property: \"fontSize\",\n      value: { type: \"unit\", value: 16, unit: \"px\" },\n    });\n    expect(tokenStyles[2]).toMatchObject({\n      styleSourceId: tokens[2].id,\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n  });\n\n  // Case 6 safeguard: Different styles and different name gets suffix when name conflicts\n  test(\"token with different styles and name adds suffix when name already exists\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n      { id: \"token2\", type: \"token\", name: \"secondaryColor\" }, // This name is taken\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n      createStyleDeclPair(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"green\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Different styles from both existing tokens, but wants name \"secondaryColor\"\n          { id: \"token3\", type: \"token\", name: \"secondaryColor\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"token3\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should add counter suffix to prevent duplicate name\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(3);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toEqual({\n      id: \"token2\",\n      type: \"token\",\n      name: \"secondaryColor\",\n    });\n    expect(tokens[2]).toMatchObject({\n      type: \"token\",\n      name: \"secondaryColor-1\",\n    });\n    expect(tokens[2].id).not.toBe(\"token3\"); // Should have new ID\n\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(3);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"token1\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    expect(tokenStyles[1]).toEqual({\n      styleSourceId: \"token2\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    });\n    expect(tokenStyles[2]).toMatchObject({\n      styleSourceId: tokens[2].id,\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n  });\n\n  // Test that existing token with same styles but different name stays untouched\n  test(\"existing token with matching styles but different name stays untouched when inserting new token\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"existingToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDeclPair(\"existingToken\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Different name \"accentColor\", same styles as existingToken\n          { id: \"newToken\", type: \"token\", name: \"accentColor\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"newToken\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n          {\n            styleSourceId: \"newToken\",\n            breakpointId: \"base\",\n            property: \"fontSize\",\n            value: { type: \"unit\", value: 16, unit: \"px\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should insert new token with its own name, leaving existing one untouched\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"existingToken\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"accentColor\" });\n    expect(tokens[1].id).not.toBe(\"newToken\"); // Should have new ID\n\n    // Both tokens should have their own styles\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(4);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"existingToken\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(tokenStyles[1]).toEqual({\n      styleSourceId: \"existingToken\",\n      breakpointId: \"base\",\n      property: \"fontSize\",\n      value: { type: \"unit\", value: 16, unit: \"px\" },\n    });\n    expect(tokenStyles[2]).toMatchObject({\n      styleSourceId: tokens[1].id,\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(tokenStyles[3]).toMatchObject({\n      styleSourceId: tokens[1].id,\n      breakpointId: \"base\",\n      property: \"fontSize\",\n      value: { type: \"unit\", value: 16, unit: \"px\" },\n    });\n  });\n\n  // Critical test: inserting base name when suffixed version exists\n  test(\"inserting token 'bbb' when 'bbb-1' with same styles exists inserts both tokens\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"bbb-1\" },\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"existingToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources, styles });\n\n    // Add an instance that uses the existing token\n    const existingInstance = createInstance(\"existingInstance\", \"Box\", []);\n    data.instances.set(\"existingInstance\", existingInstance);\n    data.styleSourceSelections.set(\"existingInstance\", {\n      instanceId: \"existingInstance\",\n      values: [\"existingToken\"],\n    });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"box\",\n            component: \"Box\",\n            children: [],\n          },\n        ],\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSources: [\n          // Inserting \"bbb\" with same styles as \"bbb-1\"\n          { id: \"newToken\", type: \"token\", name: \"bbb\" },\n        ],\n        styleSourceSelections: [{ instanceId: \"box\", values: [\"newToken\"] }],\n        styles: [\n          {\n            styleSourceId: \"newToken\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"blue\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Both tokens should exist\n    const tokens = Array.from(data.styleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"existingToken\",\n      type: \"token\",\n      name: \"bbb-1\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"bbb\" }); // Different name, so inserted as-is\n    expect(tokens[1].id).not.toBe(\"newToken\"); // Should have new ID\n\n    // Both should have their own styles\n    const tokenStyles = Array.from(data.styles.values());\n    expect(tokenStyles).toHaveLength(2);\n    expect(tokenStyles[0]).toEqual({\n      styleSourceId: \"existingToken\",\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    expect(tokenStyles[1]).toMatchObject({\n      styleSourceId: tokens[1].id,\n      breakpointId: \"base\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n\n    // The EXISTING instance should still reference \"bbb-1\" (existingToken)\n    expect(data.styleSourceSelections.get(\"existingInstance\")).toEqual({\n      instanceId: \"existingInstance\",\n      values: [\"existingToken\"],\n    });\n\n    // The new instance should reference the new token \"bbb\" (tokens[1].id)\n    const newBoxId = Array.from(data.instances.keys()).find(\n      (id) => id !== \"existingInstance\"\n    );\n    expect(data.styleSourceSelections.get(newBoxId!)).toEqual({\n      instanceId: newBoxId,\n      values: [tokens[1].id],\n    });\n  });\n\n  test(\"insert local styles with new ids and use merged breakpoint ids\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const data = getWebstudioDataStub({ breakpoints });\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"box\",\n            component: \"Box\",\n            children: [],\n          },\n        ],\n        breakpoints: [{ id: \"new_base\", label: \"Base\" }],\n        styleSourceSelections: [\n          { instanceId: \"box\", values: [\"localId\", \"tokenId\"] },\n          { instanceId: \"unknown\", values: [] },\n        ],\n        styleSources: [{ id: \"localId\", type: \"local\" }],\n        styles: [\n          {\n            styleSourceId: \"localId\",\n            breakpointId: \"new_base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n          {\n            styleSourceId: \"tokenId\",\n            breakpointId: \"new_base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"green\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.styleSourceSelections.values())).toEqual([\n      {\n        instanceId: expect.not.stringMatching(\"box\"),\n        values: [expect.not.stringMatching(\"localId\"), \"tokenId\"],\n      },\n    ]);\n    expect(Array.from(data.styleSources.values())).toEqual([\n      { id: expect.not.stringMatching(\"localId\"), type: \"local\" },\n    ]);\n    expect(Array.from(data.styles.values())).toEqual([\n      {\n        styleSourceId: expect.not.stringMatching(\"localId\"),\n        breakpointId: \"base\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"insert local styles from portal and use merged breakpoint ids\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const data = getWebstudioDataStub({ breakpoints });\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        // portal\n        //   fragment\n        instances: [\n          {\n            type: \"instance\",\n            id: \"portal\",\n            component: portalComponent,\n            children: [{ type: \"id\", value: \"fragment\" }],\n          },\n          {\n            type: \"instance\",\n            id: \"fragment\",\n            component: \"Fragment\",\n            children: [{ type: \"id\", value: \"box\" }],\n          },\n        ],\n        breakpoints: [{ id: \"new_base\", label: \"Base\" }],\n        styleSourceSelections: [\n          { instanceId: \"fragment\", values: [\"localId\", \"tokenId\"] },\n          { instanceId: \"unknown\", values: [] },\n        ],\n        styleSources: [{ id: \"localId\", type: \"local\" }],\n        styles: [\n          {\n            styleSourceId: \"localId\",\n            breakpointId: \"new_base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n          {\n            styleSourceId: \"tokenId\",\n            breakpointId: \"new_base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"green\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n    expect(Array.from(data.styleSourceSelections.values())).toEqual([\n      { instanceId: \"fragment\", values: [\"localId\", \"tokenId\"] },\n    ]);\n    expect(Array.from(data.styleSources.values())).toEqual([\n      { id: \"localId\", type: \"local\" },\n    ]);\n    expect(Array.from(data.styles.values())).toEqual([\n      {\n        styleSourceId: \"localId\",\n        breakpointId: \"base\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"merge local styles into existing ROOT_INSTANCE_ID local source\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"existingLocal\", type: \"local\" },\n    ]);\n    const styleSourceSelections = new Map([\n      [\n        ROOT_INSTANCE_ID,\n        { instanceId: ROOT_INSTANCE_ID, values: [\"existingLocal\"] },\n      ],\n    ]);\n    const styles = new Map([\n      createStyleDeclPair(\"existingLocal\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ]);\n    const data = getWebstudioDataStub({\n      breakpoints,\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: ROOT_INSTANCE_ID,\n            component: \"Body\",\n            children: [],\n          },\n        ],\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSourceSelections: [\n          { instanceId: ROOT_INSTANCE_ID, values: [\"newLocal\"] },\n        ],\n        styleSources: [{ id: \"newLocal\", type: \"local\" }],\n        styles: [\n          {\n            styleSourceId: \"newLocal\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should merge into existing local source\n    expect(Array.from(data.styleSources.values())).toEqual([\n      { id: \"existingLocal\", type: \"local\" },\n    ]);\n    expect(data.styleSourceSelections.get(ROOT_INSTANCE_ID)).toEqual({\n      instanceId: ROOT_INSTANCE_ID,\n      values: [\"existingLocal\"],\n    });\n    // Both styles should be present under the same source\n    expect(Array.from(data.styles.values())).toEqual([\n      {\n        styleSourceId: \"existingLocal\",\n        breakpointId: \"base\",\n        property: \"fontSize\",\n        value: { type: \"unit\", value: 16, unit: \"px\" },\n      },\n      {\n        styleSourceId: \"existingLocal\",\n        breakpointId: \"base\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"insert instances with new ids and update child references\", () => {\n    const data = getWebstudioDataStub();\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"parent\",\n            component: \"Box\",\n            children: [\n              { type: \"id\", value: \"child1\" },\n              { type: \"id\", value: \"child2\" },\n            ],\n          },\n          {\n            type: \"instance\",\n            id: \"child1\",\n            component: \"Text\",\n            children: [],\n          },\n          {\n            type: \"instance\",\n            id: \"child2\",\n            component: \"Text\",\n            children: [],\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    const newInstances = Array.from(data.instances.values());\n    expect(newInstances).toHaveLength(3);\n\n    const parentInstance = newInstances.find((i) => i.component === \"Box\");\n    const childInstances = newInstances.filter((i) => i.component === \"Text\");\n\n    expect(parentInstance).toBeDefined();\n    expect(childInstances).toHaveLength(2);\n\n    // Verify parent's children reference the new child ids\n    expect(parentInstance?.children).toEqual([\n      { type: \"id\", value: childInstances[0].id },\n      { type: \"id\", value: childInstances[1].id },\n    ]);\n\n    // Verify new ids were generated (not same as original)\n    expect(parentInstance?.id).not.toBe(\"parent\");\n    expect(childInstances[0].id).not.toBe(\"child1\");\n    expect(childInstances[1].id).not.toBe(\"child2\");\n  });\n\n  test(\"skip portal content that already exists\", () => {\n    const instances = new Map([\n      [\n        \"existingPortalContent\",\n        {\n          type: \"instance\" as const,\n          id: \"existingPortalContent\",\n          component: \"Box\",\n          children: [{ type: \"text\" as const, value: \"existing\" }],\n        },\n      ],\n    ]);\n    const data = getWebstudioDataStub({ instances });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"portal\",\n            component: portalComponent,\n            children: [{ type: \"id\", value: \"existingPortalContent\" }],\n          },\n          {\n            type: \"instance\",\n            id: \"existingPortalContent\",\n            component: \"Box\",\n            children: [{ type: \"text\", value: \"new version\" }],\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should preserve existing portal content\n    const existingInstance = data.instances.get(\"existingPortalContent\");\n    expect(existingInstance?.children).toEqual([\n      { type: \"text\", value: \"existing\" },\n    ]);\n  });\n\n  test(\"insert instance with expression child and update variable references\", () => {\n    const data = getWebstudioDataStub();\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"box\",\n            component: \"Box\",\n            children: [{ type: \"expression\", value: \"$ws$dataSource$oldVar\" }],\n          },\n        ],\n        dataSources: [\n          {\n            id: \"oldVar\",\n            scopeInstanceId: \"box\",\n            type: \"variable\",\n            name: \"myVar\",\n            value: { type: \"string\", value: \"hello\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    const newBoxId = Array.from(data.instances.keys())[0];\n    const newBox = data.instances.get(newBoxId);\n    const newVarId = Array.from(data.dataSources.keys())[0];\n\n    // Verify the expression was updated with the new variable id\n    expect(newBox?.children[0].type).toBe(\"expression\");\n    if (newBox?.children[0].type === \"expression\") {\n      // The encoding replaces - with __DASH__\n      const encodedId = newVarId.replace(/-/g, \"__DASH__\");\n      expect(newBox.children[0].value).toBe(`$ws$dataSource$${encodedId}`);\n    }\n    expect(newVarId).not.toBe(\"oldVar\");\n  });\n\n  test(\"skip global variables that already exist by id\", () => {\n    const dataSources = new Map([\n      [\n        \"globalVar1\",\n        {\n          id: \"globalVar1\",\n          scopeInstanceId: ROOT_INSTANCE_ID,\n          type: \"variable\" as const,\n          name: \"Global Var\",\n          value: { type: \"string\" as const, value: \"existing\" },\n        },\n      ],\n    ]);\n    const data = getWebstudioDataStub({ dataSources });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        dataSources: [\n          {\n            id: \"globalVar1\",\n            scopeInstanceId: ROOT_INSTANCE_ID,\n            type: \"variable\",\n            name: \"Global Var\",\n            value: { type: \"string\", value: \"new value\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    // Should preserve existing global variable\n    const existingVar = data.dataSources.get(\"globalVar1\");\n    expect(existingVar?.type).toBe(\"variable\");\n    if (existingVar?.type === \"variable\") {\n      expect(existingVar.value).toEqual({\n        type: \"string\",\n        value: \"existing\",\n      });\n    }\n  });\n\n  test(\"skip global variables that have conflicting names with availableVariables\", () => {\n    const data = getWebstudioDataStub();\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        dataSources: [\n          {\n            id: \"newGlobal\",\n            scopeInstanceId: ROOT_INSTANCE_ID,\n            type: \"variable\",\n            name: \"conflictingName\",\n            value: { type: \"string\", value: \"value\" },\n          },\n        ],\n      },\n      availableVariables: [\n        {\n          id: \"existingId\",\n          scopeInstanceId: ROOT_INSTANCE_ID,\n          type: \"variable\",\n          name: \"conflictingName\",\n          value: { type: \"string\", value: \"existing\" },\n        },\n      ],\n      projectId: \"\",\n    });\n\n    // Should not insert the global variable due to name conflict\n    expect(data.dataSources.has(\"newGlobal\")).toBe(false);\n  });\n\n  test(\"handle mixed token and local style sources in styleSourceSelections\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"Primary\" },\n    ]);\n    const data = getWebstudioDataStub({ breakpoints, styleSources });\n\n    insertWebstudioFragmentCopy({\n      data,\n      fragment: {\n        ...emptyFragment,\n        instances: [\n          {\n            type: \"instance\",\n            id: \"box\",\n            component: \"Box\",\n            children: [],\n          },\n        ],\n        breakpoints: [{ id: \"base\", label: \"base\" }],\n        styleSourceSelections: [\n          {\n            instanceId: \"box\",\n            values: [\"localStyle1\", \"existingToken\", \"localStyle2\"],\n          },\n        ],\n        styleSources: [\n          { id: \"localStyle1\", type: \"local\" },\n          { id: \"localStyle2\", type: \"local\" },\n        ],\n        styles: [\n          {\n            styleSourceId: \"localStyle1\",\n            breakpointId: \"base\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"red\" },\n          },\n          {\n            styleSourceId: \"localStyle2\",\n            breakpointId: \"base\",\n            property: \"fontSize\",\n            value: { type: \"unit\", value: 16, unit: \"px\" },\n          },\n        ],\n      },\n      availableVariables: [],\n      projectId: \"\",\n    });\n\n    const newBoxId = Array.from(data.instances.keys())[0];\n    const selection = data.styleSourceSelections.get(newBoxId);\n\n    expect(selection?.values).toHaveLength(3);\n    expect(selection?.values[1]).toBe(\"existingToken\"); // Token preserved\n    expect(selection?.values[0]).not.toBe(\"localStyle1\"); // Local regenerated\n    expect(selection?.values[2]).not.toBe(\"localStyle2\"); // Local regenerated\n\n    // Verify both local styles were created with new ids\n    const localStyles = Array.from(data.styleSources.values()).filter(\n      (s) => s.type === \"local\"\n    );\n    expect(localStyles).toHaveLength(2);\n  });\n});\n\ndescribe(\"find closest insertable\", () => {\n  const newBoxFragment = createFragment({\n    children: [{ type: \"id\", value: \"newBoxId\" }],\n    instances: [\n      { type: \"instance\", id: \"newBoxId\", component: \"Box\", children: [] },\n    ],\n  });\n\n  beforeEach(() => {\n    $pages.set(\n      createDefaultPages({\n        homePageId: \"homePageId\",\n        rootInstanceId: \"\",\n      })\n    );\n    $awareness.set({\n      pageId: \"homePageId\",\n      instanceSelector: [\"collectionId[1]\", \"collectionId\", \"bodyId\"],\n    });\n    $registeredComponentMetas.set(defaultMetasMap);\n  });\n\n  test(\"puts in the end if closest instance is container\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\">\n          <$.Paragraph ws:id=\"paragraphId\">\n            <$.Bold ws:id=\"boldId\"></$.Bold>\n          </$.Paragraph>\n        </$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"boxId\", \"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"boxId\", \"bodyId\"],\n      position: \"end\",\n    });\n  });\n\n  test(\"puts in the end of root instance\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Paragraph ws:id=\"paragraphId\"></$.Paragraph>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"bodyId\"],\n      position: \"end\",\n    });\n  });\n\n  test(\"puts in the end of root instance when page root only has text\", () => {\n    const { instances } = renderData(<$.Body ws:id=\"bodyId\">text</$.Body>);\n    $instances.set(instances);\n    selectInstance([\"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"bodyId\"],\n      position: \"end\",\n    });\n  });\n\n  test(\"finds closest container and puts after its child within selection\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Paragraph ws:id=\"paragraphId\">\n          <$.Bold ws:id=\"boldId\"></$.Bold>\n        </$.Paragraph>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"boldId\", \"paragraphId\", \"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"bodyId\"],\n      position: 1,\n    });\n  });\n\n  test(\"finds closest container that doesn't have an expression as a child\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"box1Id\"></$.Box>\n        <$.Paragraph ws:id=\"paragraphId\">{expression`\"bla\"`}</$.Paragraph>\n        <$.Box ws:id=\"box2Id\"></$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"paragraphId\", \"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"bodyId\"],\n      position: 2,\n    });\n  });\n\n  test(\"finds closest container without textual placeholder\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Paragraph ws:id=\"paragraphId\"></$.Paragraph>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"paragraphId\", \"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"bodyId\"],\n      position: 1,\n    });\n  });\n\n  test(\"finds closest container even with when parent has placeholder\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <$.Paragraph ws:id=\"paragraphId\">\n          <$.Image ws:id=\"imageId\"></$.Image>\n        </$.Paragraph>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"imageId\", \"paragraphId\", \"bodyId\"]);\n    expect(\n      findClosestInsertable(renderTemplate(<$.Box ws:tag=\"span\"></$.Box>))\n    ).toEqual({\n      parentSelector: [\"paragraphId\", \"bodyId\"],\n      position: 1,\n    });\n  });\n\n  test(\"forbids inserting into :root\", () => {\n    const { instances } = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    $instances.set(instances);\n    selectInstance([\":root\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual(undefined);\n  });\n\n  test(\"allow inserting into collection item\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"bodyId\">\n        <ws.collection ws:id=\"collectionId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </ws.collection>\n      </$.Body>\n    );\n    $instances.set(instances);\n    selectInstance([\"collectionId[1]\", \"collectionId\", \"bodyId\"]);\n    expect(findClosestInsertable(newBoxFragment)).toEqual({\n      parentSelector: [\"collectionId\", \"bodyId\"],\n      position: \"end\",\n    });\n  });\n\n  test(\"allow inserting list item in body even though validation fails\", () => {\n    const { instances } = renderData(<$.Body ws:id=\"bodyId\"></$.Body>);\n    $instances.set(instances);\n    selectInstance([\"bodyId\"]);\n    const newListItemFragment = renderTemplate(\n      <$.ListItem ws:id=\"newListItemId\"></$.ListItem>\n    );\n    expect(findClosestInsertable(newListItemFragment)).toEqual({\n      parentSelector: [\"bodyId\"],\n      position: \"end\",\n    });\n  });\n});\n\ntest(\"get undefined instead of instance path when no instances found\", () => {\n  expect(getInstancePath([\"boxId\"], new Map())).toEqual(undefined);\n});\n\ndescribe(\"buildInstancePath\", () => {\n  const createPages = () =>\n    createDefaultPages({\n      homePageId: \"homePageId\",\n      rootInstanceId: \"rootId\",\n      systemDataSourceId: \"systemId\",\n    });\n\n  test(\"returns empty array when instance has no selector\", () => {\n    const pages = createPages();\n    const instances = new Map();\n\n    const result = buildInstancePath(\"nonexistent\", pages, instances);\n    expect(result).toEqual([]);\n  });\n\n  test(\"returns empty array for root instance (no ancestors)\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"rootId\">\n        <$.Box ws:id=\"boxId\"></$.Box>\n      </$.Body>\n    );\n    const pages = createPages();\n\n    const result = buildInstancePath(\"rootId\", pages, instances);\n    expect(result).toEqual([]);\n  });\n\n  test(\"builds path for single-level nesting\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"rootId\">\n        <$.Box ws:id=\"boxId\"></$.Box>\n      </$.Body>\n    );\n    const pages = createPages();\n\n    const result = buildInstancePath(\"boxId\", pages, instances);\n    expect(result).toEqual([\"Body\"]);\n  });\n\n  test(\"builds path for multi-level nesting\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"rootId\">\n        <$.Box ws:id=\"containerId\">\n          <$.Heading ws:id=\"headingId\"></$.Heading>\n        </$.Box>\n      </$.Body>\n    );\n    const pages = createPages();\n\n    const result = buildInstancePath(\"headingId\", pages, instances);\n    expect(result).toEqual([\"Body\", \"Box\"]);\n  });\n\n  test(\"builds path for deeply nested instance\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"rootId\">\n        <$.Box ws:id=\"sectionId\">\n          <$.Box ws:id=\"articleId\">\n            <$.Box ws:id=\"divId\">\n              <$.Text ws:id=\"textId\"></$.Text>\n            </$.Box>\n          </$.Box>\n        </$.Box>\n      </$.Body>\n    );\n    const pages = createPages();\n\n    const result = buildInstancePath(\"textId\", pages, instances);\n    expect(result).toEqual([\"Body\", \"Box\", \"Box\", \"Box\"]);\n  });\n\n  test(\"handles instances with custom labels\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"rootId\" ws:label=\"Main Body\">\n        <$.Box ws:id=\"navId\" ws:label=\"Navigation\">\n          <$.Link ws:id=\"linkId\" ws:label=\"Home Link\"></$.Link>\n        </$.Box>\n      </$.Body>\n    );\n    const pages = createPages();\n\n    const result = buildInstancePath(\"linkId\", pages, instances);\n    expect(result).toEqual([\"Main Body\", \"Navigation\"]);\n  });\n});\n\ndescribe(\"toggleInstanceShow\", () => {\n  test(\"creates show prop with false value when it doesn't exist\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\"></$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    $props.set(new Map());\n    $pages.set(createDefaultPages({ rootInstanceId: \"body\" }));\n\n    toggleInstanceShow(\"box\");\n\n    const props = $props.get();\n    const showProp = Array.from(props.values()).find(\n      (prop) => prop.instanceId === \"box\" && prop.name === showAttribute\n    );\n    expect(showProp).toEqual({\n      id: expect.any(String),\n      instanceId: \"box\",\n      name: showAttribute,\n      type: \"boolean\",\n      value: false,\n    });\n  });\n\n  test(\"toggles show prop from true to false\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\" ws:show={true}></$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    $props.set(props);\n    $pages.set(createDefaultPages({ rootInstanceId: \"body\" }));\n\n    toggleInstanceShow(\"box\");\n\n    const updatedProps = $props.get();\n    const showProp = Array.from(updatedProps.values()).find(\n      (prop) => prop.instanceId === \"box\" && prop.name === showAttribute\n    );\n    expect(showProp?.type).toBe(\"boolean\");\n    if (showProp?.type === \"boolean\") {\n      expect(showProp.value).toBe(false);\n    }\n  });\n\n  test(\"toggles show prop from false to true\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\" ws:show={false}></$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    $props.set(props);\n    $pages.set(createDefaultPages({ rootInstanceId: \"body\" }));\n\n    toggleInstanceShow(\"box\");\n\n    const updatedProps = $props.get();\n    const showProp = Array.from(updatedProps.values()).find(\n      (prop) => prop.instanceId === \"box\" && prop.name === showAttribute\n    );\n    expect(showProp?.type).toBe(\"boolean\");\n    if (showProp?.type === \"boolean\") {\n      expect(showProp.value).toBe(true);\n    }\n  });\n});\n\ndescribe(\"unwrap instance\", () => {\n  test(\"unwraps instance and moves children to parent\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Box ws:id=\"wrapper\">\n            <$.Box ws:id=\"child1\"></$.Box>\n            <$.Box ws:id=\"child2\"></$.Box>\n          </$.Box>\n        </$.Box>\n      </$.Body>\n    );\n\n    const selectedItem = {\n      instanceSelector: [\"wrapper\", \"parent\", \"body\"],\n      instance: instances.get(\"wrapper\")!,\n    };\n    const parentItem = {\n      instanceSelector: [\"parent\", \"body\"],\n      instance: instances.get(\"parent\")!,\n    };\n\n    const result = unwrapInstanceMutable({\n      instances,\n      props,\n      metas: defaultMetasMap,\n      selectedItem,\n      parentItem,\n    });\n\n    expect(result.success).toBe(true);\n    expect(instances.has(\"parent\")).toBe(false);\n    const bodyInstance = instances.get(\"body\")!;\n    expect(bodyInstance.children).toEqual([{ type: \"id\", value: \"wrapper\" }]);\n  });\n\n  test(\"fails to unwrap textual instance\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\">\n          <$.Paragraph ws:id=\"paragraph\">\n            <$.Bold ws:id=\"bold\">text</$.Bold>\n          </$.Paragraph>\n        </$.Box>\n      </$.Body>\n    );\n\n    const selectedItem = {\n      instanceSelector: [\"bold\", \"paragraph\", \"box\", \"body\"],\n      instance: instances.get(\"bold\")!,\n    };\n    const parentItem = {\n      instanceSelector: [\"paragraph\", \"box\", \"body\"],\n      instance: instances.get(\"paragraph\")!,\n    };\n\n    const result = unwrapInstanceMutable({\n      instances,\n      props,\n      metas: defaultMetasMap,\n      selectedItem,\n      parentItem,\n    });\n\n    expect(result.success).toBe(false);\n    expect(result.error).toBe(\"Cannot unwrap textual instance\");\n  });\n\n  test(\"fails to unwrap if content model is violated\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"outerBox\">\n          <$.Form ws:id=\"form\">\n            <$.Box ws:id=\"innerBox\">\n              <$.Form ws:id=\"nestedForm\"></$.Form>\n            </$.Box>\n          </$.Form>\n        </$.Box>\n      </$.Body>\n    );\n\n    const selectedItem = {\n      instanceSelector: [\"form\", \"outerBox\", \"body\"],\n      instance: instances.get(\"form\")!,\n    };\n    const parentItem = {\n      instanceSelector: [\"outerBox\", \"body\"],\n      instance: instances.get(\"outerBox\")!,\n    };\n\n    const result = unwrapInstanceMutable({\n      instances,\n      props,\n      metas: defaultMetasMap,\n      selectedItem,\n      parentItem,\n    });\n\n    expect(result.success).toBe(false);\n    expect(result.error).toBe(\"Cannot unwrap instance\");\n  });\n\n  test(\"unwrapping replaces parent with selected in grandparent\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Box ws:id=\"child\"></$.Box>\n        </$.Box>\n      </$.Body>\n    );\n\n    const selectedItem = {\n      instanceSelector: [\"child\", \"parent\", \"body\"],\n      instance: instances.get(\"child\")!,\n    };\n    const parentItem = {\n      instanceSelector: [\"parent\", \"body\"],\n      instance: instances.get(\"parent\")!,\n    };\n\n    const result = unwrapInstanceMutable({\n      instances,\n      props,\n      metas: defaultMetasMap,\n      selectedItem,\n      parentItem,\n    });\n\n    expect(result.success).toBe(true);\n    expect(instances.has(\"parent\")).toBe(false);\n    expect(instances.has(\"child\")).toBe(true);\n    const bodyInstance = instances.get(\"body\")!;\n    expect(bodyInstance.children).toEqual([{ type: \"id\", value: \"child\" }]);\n  });\n\n  test(\"unwrapping removes selected from parent and moves it to grandparent\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Image ws:id=\"image\"></$.Image>\n          <$.Link ws:id=\"link\"></$.Link>\n        </$.Box>\n      </$.Body>\n    );\n\n    const selectedItem = {\n      instanceSelector: [\"image\", \"parent\", \"body\"],\n      instance: instances.get(\"image\")!,\n    };\n    const parentItem = {\n      instanceSelector: [\"parent\", \"body\"],\n      instance: instances.get(\"parent\")!,\n    };\n\n    const result = unwrapInstanceMutable({\n      instances,\n      props,\n      metas: defaultMetasMap,\n      selectedItem,\n      parentItem,\n    });\n\n    expect(result.success).toBe(true);\n    expect(instances.has(\"parent\")).toBe(true); // Parent still exists\n    expect(instances.has(\"image\")).toBe(true);\n    expect(instances.has(\"link\")).toBe(true);\n\n    const bodyInstance = instances.get(\"body\")!;\n    expect(bodyInstance.children).toEqual([\n      { type: \"id\", value: \"parent\" },\n      { type: \"id\", value: \"image\" },\n    ]);\n\n    const parentInstance = instances.get(\"parent\")!;\n    expect(parentInstance.children).toEqual([{ type: \"id\", value: \"link\" }]);\n  });\n});\n\ndescribe(\"canUnwrapInstance\", () => {\n  beforeEach(() => {\n    $project.set({ id: \"projectId\" } as Project);\n    $registeredComponentMetas.set(defaultMetasMap);\n  });\n\n  test(\"returns true for unwrappable instance\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Box ws:id=\"wrapper\">\n            <$.Box ws:id=\"child\"></$.Box>\n          </$.Box>\n        </$.Box>\n      </$.Body>\n    );\n\n    $instances.set(instances);\n    $props.set(props);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n\n    const instancePath = [\n      {\n        instanceSelector: [\"wrapper\", \"parent\", \"body\"],\n        instance: instances.get(\"wrapper\")!,\n      },\n      {\n        instanceSelector: [\"parent\", \"body\"],\n        instance: instances.get(\"parent\")!,\n      },\n      {\n        instanceSelector: [\"body\"],\n        instance: instances.get(\"body\")!,\n      },\n    ] satisfies InstancePath;\n\n    expect(canUnwrapInstance(instancePath)).toBe(true);\n  });\n\n  test(\"returns false if parent is root instance\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\"></$.Box>\n      </$.Body>\n    );\n\n    $instances.set(instances);\n    $props.set(props);\n    $registeredComponentMetas.set(defaultMetasMap);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n\n    const instancePath = [\n      {\n        instanceSelector: [\"box\", \"body\"],\n        instance: instances.get(\"box\")!,\n      },\n      {\n        instanceSelector: [\"body\"],\n        instance: instances.get(\"body\")!,\n      },\n    ] satisfies InstancePath;\n\n    expect(canUnwrapInstance(instancePath)).toBe(false);\n  });\n\n  test(\"returns false for textual instance\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Paragraph ws:id=\"paragraph\">\n          <$.Bold ws:id=\"bold\">text</$.Bold>\n        </$.Paragraph>\n      </$.Body>\n    );\n\n    $instances.set(instances);\n    $props.set(props);\n    $registeredComponentMetas.set(defaultMetasMap);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n\n    const instancePath = [\n      {\n        instanceSelector: [\"bold\", \"paragraph\", \"body\"],\n        instance: instances.get(\"bold\")!,\n      },\n      {\n        instanceSelector: [\"paragraph\", \"body\"],\n        instance: instances.get(\"paragraph\")!,\n      },\n    ] satisfies InstancePath;\n\n    expect(canUnwrapInstance(instancePath)).toBe(false);\n  });\n\n  test(\"returns true for Body > div > a scenario\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <ws.element ws:tag=\"div\" ws:id=\"div\">\n          <ws.element ws:tag=\"a\" ws:id=\"link\">\n            Link text\n          </ws.element>\n        </ws.element>\n      </$.Body>\n    );\n\n    $instances.set(instances);\n    $props.set(props);\n    $registeredComponentMetas.set(defaultMetasMap);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n\n    const instancePath = [\n      {\n        instanceSelector: [\"link\", \"div\", \"body\"],\n        instance: instances.get(\"link\")!,\n      },\n      {\n        instanceSelector: [\"div\", \"body\"],\n        instance: instances.get(\"div\")!,\n      },\n      {\n        instanceSelector: [\"body\"],\n        instance: instances.get(\"body\")!,\n      },\n    ] satisfies InstancePath;\n\n    // Should be able to unwrap the link from the div\n    expect(canUnwrapInstance(instancePath)).toBe(true);\n  });\n\n  test(\"unwrapInstanceMutable works for Body > div > a scenario\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <ws.element ws:tag=\"div\" ws:id=\"div\">\n          <ws.element ws:tag=\"a\" ws:id=\"link\">\n            Link text\n          </ws.element>\n        </ws.element>\n      </$.Body>\n    );\n\n    const result = unwrapInstanceMutable({\n      instances,\n      props,\n      metas: defaultMetasMap,\n      selectedItem: {\n        instanceSelector: [\"link\", \"div\", \"body\"],\n        instance: instances.get(\"link\")!,\n      },\n      parentItem: {\n        instanceSelector: [\"div\", \"body\"],\n        instance: instances.get(\"div\")!,\n      },\n    });\n\n    expect(result.success).toBe(true);\n\n    // Verify the link is now a direct child of body\n    const body = instances.get(\"body\")!;\n    expect(body.children).toContainEqual({ type: \"id\", value: \"link\" });\n\n    // Verify the div was deleted since it has no more children\n    expect(instances.has(\"div\")).toBe(false);\n  });\n});\n\ndescribe(\"canConvertInstance\", () => {\n  test(\"returns true for valid conversion\", () => {\n    const { instances, props } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"body\">\n        <$.Box ws:id=\"box\"></$.Box>\n      </ws.element>\n    );\n\n    const result = canConvertInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      elementComponent,\n      \"div\",\n      instances,\n      props,\n      defaultMetasMap\n    );\n\n    expect(result).toBe(true);\n  });\n\n  test(\"returns false for non-existent instance\", () => {\n    const { instances, props } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"body\"></ws.element>\n    );\n\n    const result = canConvertInstance(\n      \"nonexistent\",\n      [\"nonexistent\", \"body\"],\n      elementComponent,\n      \"div\",\n      instances,\n      props,\n      defaultMetasMap\n    );\n\n    expect(result).toBe(false);\n  });\n\n  test(\"returns true when converting Box to Heading\", () => {\n    const { instances, props } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"body\">\n        <$.Box ws:id=\"box\"></$.Box>\n      </ws.element>\n    );\n\n    const result = canConvertInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"@webstudio-is/sdk-components-react:Heading\",\n      undefined,\n      instances,\n      props,\n      defaultMetasMap\n    );\n\n    expect(result).toBe(true);\n  });\n\n  test(\"uses preset tag when available\", () => {\n    const { instances, props } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"body\">\n        <$.Box ws:id=\"box\"></$.Box>\n      </ws.element>\n    );\n\n    const result = canConvertInstance(\n      \"box\",\n      [\"box\", \"body\"],\n      \"@webstudio-is/sdk-components-react:Heading\",\n      undefined,\n      instances,\n      props,\n      defaultMetasMap\n    );\n\n    expect(result).toBe(true);\n  });\n});\n\ndescribe(\"convertInstance\", () => {\n  beforeEach(() => {\n    $registeredComponentMetas.set(defaultMetasMap);\n  });\n\n  test(\"converts legacy tag to element\", () => {\n    const { instances, props } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <$.Box tag=\"article\" ws:id=\"articleId\"></$.Box>\n      </ws.element>\n    );\n    $instances.set(instances);\n    $props.set(props);\n    selectInstance([\"articleId\", \"bodyId\"]);\n    convertInstance(elementComponent);\n    const { instances: newInstances, props: newProps } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element ws:tag=\"article\" ws:id=\"articleId\"></ws.element>\n      </ws.element>\n    );\n    expect({ instances: $instances.get(), props: $props.get() }).toEqual({\n      instances: newInstances,\n      props: newProps,\n    });\n  });\n\n  test(\"migrates legacy properties\", () => {\n    const { instances, props } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <$.Box\n          ws:tag=\"div\"\n          ws:id=\"divId\"\n          className=\"my-class\"\n          htmlFor=\"my-id\"\n        ></$.Box>\n      </ws.element>\n    );\n    $instances.set(instances);\n    $props.set(props);\n    selectInstance([\"divId\", \"bodyId\"]);\n    convertInstance(elementComponent);\n    const { instances: newInstances, props: newProps } = renderData(\n      <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n        <ws.element\n          ws:tag=\"div\"\n          ws:id=\"divId\"\n          class=\"my-class\"\n          for=\"my-id\"\n        ></ws.element>\n      </ws.element>\n    );\n    expect({ instances: $instances.get(), props: $props.get() }).toEqual({\n      instances: newInstances,\n      props: newProps,\n    });\n  });\n\n  test(\"preserves currently specified tag\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.Box ws:tag=\"article\" ws:id=\"articleId\"></$.Box>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"articleId\", \"bodyId\"]);\n    convertInstance(elementComponent);\n    expect($instances.get()).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"article\" ws:id=\"articleId\"></ws.element>\n        </ws.element>\n      ).instances\n    );\n  });\n\n  test(\"converts to first tag from presets\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.Heading ws:id=\"headingId\"></$.Heading>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"headingId\", \"bodyId\"]);\n    convertInstance(elementComponent);\n    expect($instances.get()).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"h1\" ws:id=\"headingId\"></ws.element>\n        </ws.element>\n      ).instances\n    );\n  });\n\n  test(\"falls back to div\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.Box ws:id=\"divId\"></$.Box>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"divId\", \"bodyId\"]);\n    convertInstance(elementComponent);\n    expect($instances.get()).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"div\" ws:id=\"divId\"></ws.element>\n        </ws.element>\n      ).instances\n    );\n  });\n\n  test(\"converts with specific tag\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.Box ws:id=\"divId\"></$.Box>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"divId\", \"bodyId\"]);\n    convertInstance(elementComponent, \"a\");\n    expect($instances.get()).toEqual(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"a\" ws:id=\"divId\"></ws.element>\n        </ws.element>\n      ).instances\n    );\n  });\n\n  test(\"converts between components\", () => {\n    $instances.set(\n      renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </ws.element>\n      ).instances\n    );\n    selectInstance([\"boxId\", \"bodyId\"]);\n    convertInstance(\"@webstudio-is/sdk-components-react:Heading\");\n    const result = $instances.get();\n    const boxInstance = result.get(\"boxId\");\n    expect(boxInstance?.component).toBe(\n      \"@webstudio-is/sdk-components-react:Heading\"\n    );\n  });\n\n  test(\"prevents converting root instance\", () => {\n    const initialInstances = renderData(\n      <ws.element ws:tag=\"html\" ws:id=\"rootId\">\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\"></ws.element>\n      </ws.element>\n    ).instances;\n    $instances.set(initialInstances);\n    selectInstance([\"rootId\"]);\n    convertInstance(elementComponent, \"div\");\n    // Should not change\n    expect($instances.get()).toEqual(initialInstances);\n  });\n\n  test(\"prevents converting body instance\", () => {\n    const pages = createDefaultPages({ rootInstanceId: \"bodyId\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n\n    const initialInstances = renderData(\n      <ws.element ws:tag=\"html\" ws:id=\"rootId\">\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <$.Box ws:id=\"boxId\"></$.Box>\n        </ws.element>\n      </ws.element>\n    ).instances;\n    $instances.set(initialInstances);\n    selectInstance([\"bodyId\", \"rootId\"]);\n    convertInstance(elementComponent, \"div\");\n    // Should not change\n    expect($instances.get()).toEqual(initialInstances);\n  });\n});\n\ndescribe(\"deleteSelectedInstance\", () => {\n  test(\"delete selected instance and select next one\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Box ws:id=\"child1\"></$.Box>\n          <$.Box ws:id=\"child2\"></$.Box>\n          <$.Box ws:id=\"child3\"></$.Box>\n        </$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n    selectInstance([\"child2\", \"parent\", \"body\"]);\n    deleteSelectedInstance();\n    expect($awareness.get()?.instanceSelector).toEqual([\n      \"child3\",\n      \"parent\",\n      \"body\",\n    ]);\n  });\n\n  test(\"delete selected instance and select previous one\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Box ws:id=\"child1\"></$.Box>\n          <$.Box ws:id=\"child2\"></$.Box>\n          <$.Box ws:id=\"child3\"></$.Box>\n        </$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n    selectInstance([\"child3\", \"parent\", \"body\"]);\n    deleteSelectedInstance();\n    expect($awareness.get()?.instanceSelector).toEqual([\n      \"child2\",\n      \"parent\",\n      \"body\",\n    ]);\n  });\n\n  test(\"delete selected instance and select parent one\", () => {\n    const { instances } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"parent\">\n          <$.Box ws:id=\"child1\"></$.Box>\n        </$.Box>\n      </$.Body>\n    );\n    $instances.set(instances);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n    selectInstance([\"child1\", \"parent\", \"body\"]);\n    deleteSelectedInstance();\n    expect($awareness.get()?.instanceSelector).toEqual([\"parent\", \"body\"]);\n  });\n});\n\ndescribe(\"insertWebstudioFragmentAt with conflictResolution\", () => {\n  beforeEach(() => {\n    $project.set({ id: \"project-id\" } as Project);\n  });\n\n  test(\"uses conflictResolution='theirs' by default (creates new token with suffix)\", () => {\n    // Existing project with a \"primary\" token (used by existing-box)\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box\n          ws:id=\"existing-box\"\n          ws:tokens={[\n            token(\n              \"primary\",\n              css`\n                color: blue;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n\n    // Create fragment with token that has same name but different styles\n    const fragment = renderTemplate(\n      <$.Box\n        ws:id=\"box\"\n        ws:tokens={[\n          token(\n            \"primary\",\n            css`\n              color: red;\n            `\n          ),\n        ]}\n      ></$.Box>\n    );\n\n    setDataStores(data);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n    selectInstance([\"body\"]);\n\n    // Insert without explicit conflictResolution (defaults to \"theirs\")\n    insertWebstudioFragmentAt(fragment, {\n      parentSelector: [\"body\"],\n      position: \"end\",\n    });\n\n    // The existing token should still have blue color (unchanged)\n    const styles = Array.from($styles.get().values());\n    const styleSources = Array.from($styleSources.get().values());\n    const existingToken = styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"primary\"\n    );\n    const existingTokenStyle = styles.find(\n      (s) =>\n        s.styleSourceId === existingToken?.id &&\n        s.property === \"color\" &&\n        s.breakpointId === \"base\"\n    );\n    expect(existingTokenStyle?.value).toEqual({\n      type: \"keyword\",\n      value: \"blue\",\n    });\n\n    // A new token \"primary-1\" should be created with red color\n    const newToken = styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"primary-1\"\n    );\n    expect(newToken).toBeDefined();\n    const newTokenStyle = styles.find(\n      (s) =>\n        s.styleSourceId === newToken?.id &&\n        s.property === \"color\" &&\n        s.breakpointId === \"base\"\n    );\n    expect(newTokenStyle?.value).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"uses conflictResolution='ours' to keep existing token styles\", () => {\n    // Existing project with a \"primary\" token (used by existing-box)\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box\n          ws:id=\"existing-box\"\n          ws:tokens={[\n            token(\n              \"primary\",\n              css`\n                color: blue;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n\n    const fragment = renderTemplate(\n      <$.Box\n        ws:id=\"box\"\n        ws:tokens={[\n          token(\n            \"primary\",\n            css`\n              color: red;\n            `\n          ),\n        ]}\n      ></$.Box>\n    );\n\n    setDataStores(data);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n    selectInstance([\"body\"]);\n\n    // Insert with conflictResolution=\"ours\" to keep existing styles\n    insertWebstudioFragmentAt(\n      fragment,\n      {\n        parentSelector: [\"body\"],\n        position: \"end\",\n      },\n      \"ours\"\n    );\n\n    // The existing token should still have blue color (kept original)\n    const styles = Array.from($styles.get().values());\n    const styleSources = Array.from($styleSources.get().values());\n    const existingToken = styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"primary\"\n    );\n    const primaryTokenStyle = styles.find(\n      (s) =>\n        s.styleSourceId === existingToken?.id &&\n        s.property === \"color\" &&\n        s.breakpointId === \"base\"\n    );\n    expect(primaryTokenStyle?.value).toEqual({\n      type: \"keyword\",\n      value: \"blue\",\n    });\n\n    // No new token should be created\n    const newToken = styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"primary-1\"\n    );\n    expect(newToken).toBeUndefined();\n  });\n\n  test(\"uses conflictResolution='merge' to merge styles (theirs overrides)\", () => {\n    // Existing project with a \"primary\" token that has color and fontSize\n    const data = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box\n          ws:id=\"existing-box\"\n          ws:tokens={[\n            token(\n              \"primary\",\n              css`\n                color: blue;\n                font-size: 16px;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n\n    // Fragment with same \"primary\" token but different color and new property\n    const fragment = renderTemplate(\n      <$.Box\n        ws:id=\"box\"\n        ws:tokens={[\n          token(\n            \"primary\",\n            css`\n              color: red;\n              font-weight: bold;\n            `\n          ),\n        ]}\n      ></$.Box>\n    );\n\n    setDataStores(data);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n    $awareness.set({ pageId: pages.homePage.id });\n    selectInstance([\"body\"]);\n\n    // Insert with conflictResolution=\"merge\"\n    insertWebstudioFragmentAt(\n      fragment,\n      {\n        parentSelector: [\"body\"],\n        position: \"end\",\n      },\n      \"merge\"\n    );\n\n    // The existing token should now have red color (overridden by fragment)\n    const styles = Array.from($styles.get().values());\n    const styleSources = Array.from($styleSources.get().values());\n    const existingToken = styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"primary\"\n    );\n    const colorStyle = styles.find(\n      (s) =>\n        s.styleSourceId === existingToken?.id &&\n        s.property === \"color\" &&\n        s.breakpointId === \"base\"\n    );\n    expect(colorStyle?.value).toEqual({ type: \"keyword\", value: \"red\" });\n\n    // fontSize should still be there (not in fragment, so kept)\n    const fontSizeStyle = styles.find(\n      (s) =>\n        s.styleSourceId === existingToken?.id &&\n        s.property === \"fontSize\" &&\n        s.breakpointId === \"base\"\n    );\n    expect(fontSizeStyle?.value).toEqual({\n      type: \"unit\",\n      value: 16,\n      unit: \"px\",\n    });\n\n    // fontWeight should be added from fragment\n    const fontWeightStyle = styles.find(\n      (s) =>\n        s.styleSourceId === existingToken?.id &&\n        s.property === \"fontWeight\" &&\n        s.breakpointId === \"base\"\n    );\n    expect(fontWeightStyle?.value).toEqual({ type: \"keyword\", value: \"bold\" });\n\n    // No new token should be created\n    const newToken = styleSources.find(\n      (s) => s.type === \"token\" && s.name === \"primary-1\"\n    );\n    expect(newToken).toBeUndefined();\n  });\n});\n\ndescribe(\"detectPageTokenConflicts\", () => {\n  beforeEach(() => {\n    $project.set({ id: \"project-id\" } as Project);\n  });\n\n  test(\"returns empty array when no conflicts exist\", () => {\n    // Set up target project data (no tokens)\n    const targetData = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"existing-box\"></$.Box>\n      </$.Body>\n    );\n    setDataStores(targetData);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n\n    // Create source data with a token\n    const sourceData = renderData(\n      <$.Body ws:id=\"source-body\">\n        <$.Box\n          ws:id=\"source-box\"\n          ws:tokens={[\n            token(\n              \"primary\",\n              css`\n                color: red;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n    const sourcePages = createDefaultPages({ rootInstanceId: \"source-body\" });\n    const sourceWebstudioData: WebstudioData = {\n      ...sourceData,\n      pages: sourcePages,\n    };\n\n    const conflicts = detectPageTokenConflicts({\n      sourceData: sourceWebstudioData,\n      pageId: sourcePages.homePage.id,\n    });\n\n    // No conflicts\n    expect(conflicts).toEqual([]);\n  });\n\n  test(\"returns conflicts when token conflicts exist\", () => {\n    // Set up target project with a \"primary\" token\n    const targetData = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box\n          ws:id=\"existing-box\"\n          ws:tokens={[\n            token(\n              \"primary\",\n              css`\n                color: blue;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n    setDataStores(targetData);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n\n    // Create source data with same \"primary\" token but different styles\n    const sourceData = renderData(\n      <$.Body ws:id=\"source-body\">\n        <$.Box\n          ws:id=\"source-box\"\n          ws:tokens={[\n            token(\n              \"primary\",\n              css`\n                color: red;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n    const sourcePages = createDefaultPages({ rootInstanceId: \"source-body\" });\n    const sourceWebstudioData: WebstudioData = {\n      ...sourceData,\n      pages: sourcePages,\n    };\n\n    const conflicts = detectPageTokenConflicts({\n      sourceData: sourceWebstudioData,\n      pageId: sourcePages.homePage.id,\n    });\n\n    // Should return conflicts\n    expect(conflicts.length).toBeGreaterThan(0);\n    expect(conflicts[0].tokenName).toBe(\"primary\");\n  });\n\n  test(\"detects conflicts from ROOT_INSTANCE tokens\", () => {\n    // Set up target project with a \"header\" token\n    const targetData = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box\n          ws:id=\"existing-box\"\n          ws:tokens={[\n            token(\n              \"header\",\n              css`\n                background: white;\n              `\n            ),\n          ]}\n        ></$.Box>\n      </$.Body>\n    );\n    setDataStores(targetData);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n\n    // Create source data with a ROOT_INSTANCE that has conflicting \"header\" token\n    const sourceBodyData = renderData(\n      <$.Body ws:id=\"source-body\">\n        <$.Box ws:id=\"source-box\"></$.Box>\n      </$.Body>\n    );\n    // Add a global \"header\" token with different styles (simulating ROOT_INSTANCE content)\n    const headerTokenId = \"header-token-id\";\n    sourceBodyData.styleSources.set(headerTokenId, {\n      type: \"token\",\n      id: headerTokenId,\n      name: \"header\",\n    });\n    const baseBreakpointId = Array.from(\n      sourceBodyData.breakpoints.values()\n    ).find((b) => b.minWidth === undefined)?.id;\n    if (baseBreakpointId) {\n      sourceBodyData.styles.set(`${headerTokenId}:base:backgroundColor`, {\n        styleSourceId: headerTokenId,\n        breakpointId: baseBreakpointId,\n        property: \"backgroundColor\",\n        value: { type: \"keyword\", value: \"black\" },\n      });\n    }\n    // Add ROOT_INSTANCE with the header token\n    sourceBodyData.instances.set(ROOT_INSTANCE_ID, {\n      type: \"instance\",\n      id: ROOT_INSTANCE_ID,\n      component: \"Box\",\n      children: [],\n    });\n    sourceBodyData.styleSourceSelections.set(ROOT_INSTANCE_ID, {\n      instanceId: ROOT_INSTANCE_ID,\n      values: [headerTokenId],\n    });\n\n    const sourcePages = createDefaultPages({ rootInstanceId: \"source-body\" });\n    const sourceWebstudioData: WebstudioData = {\n      ...sourceBodyData,\n      pages: sourcePages,\n    };\n\n    const conflicts = detectPageTokenConflicts({\n      sourceData: sourceWebstudioData,\n      pageId: sourcePages.homePage.id,\n    });\n\n    // Should detect conflict from ROOT_INSTANCE token\n    expect(conflicts.length).toBeGreaterThan(0);\n    expect(conflicts[0].tokenName).toBe(\"header\");\n  });\n\n  test(\"throws error when page not found\", () => {\n    const targetData = renderData(<$.Body ws:id=\"body\"></$.Body>);\n    setDataStores(targetData);\n    const pages = createDefaultPages({ rootInstanceId: \"body\" });\n    $pages.set(pages);\n\n    const sourceData = renderData(<$.Body ws:id=\"source-body\"></$.Body>);\n    const sourcePages = createDefaultPages({ rootInstanceId: \"source-body\" });\n    const sourceWebstudioData: WebstudioData = {\n      ...sourceData,\n      pages: sourcePages,\n    };\n\n    expect(() =>\n      detectPageTokenConflicts({\n        sourceData: sourceWebstudioData,\n        pageId: \"non-existent-page-id\",\n      })\n    ).toThrow(\"Page not found\");\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/instance-utils.ts",
    "content": "import { current, isDraft } from \"immer\";\nimport { nanoid } from \"nanoid\";\nimport { toast } from \"@webstudio-is/design-system\";\nimport { builderApi } from \"~/shared/builder-api\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\nimport {\n  type Instances,\n  type Instance,\n  type StyleDecl,\n  type Asset,\n  type Breakpoints,\n  type DataSources,\n  type DataSource,\n  type WebstudioFragment,\n  type WebstudioData,\n  type Resource,\n  type WsComponentMeta,\n  type Pages,\n  getStyleDeclKey,\n  findTreeInstanceIds,\n  findTreeInstanceIdsExcludingSlotDescendants,\n  decodeDataSourceVariable,\n  encodeDataSourceVariable,\n  transpileExpression,\n  ROOT_INSTANCE_ID,\n  portalComponent,\n  collectionComponent,\n  Prop,\n  Props,\n  elementComponent,\n  tags,\n  blockTemplateComponent,\n  isComponentDetachable,\n} from \"@webstudio-is/sdk\";\nimport { detectTokenConflicts } from \"./style-source-utils\";\nimport { type ConflictResolution } from \"./token-conflict-dialog\";\nimport { buildMergedBreakpointIds } from \"./breakpoints-utils\";\nimport {\n  $props,\n  $styles,\n  $styleSourceSelections,\n  $styleSources,\n  $instances,\n  $registeredComponentMetas,\n  $dataSources,\n  $assets,\n  $breakpoints,\n  $pages,\n  $resources,\n  $registeredTemplates,\n  $project,\n  $isPreviewMode,\n  $textEditingInstanceSelector,\n  $isContentMode,\n  findBlockSelector,\n} from \"./nano-states\";\nimport {\n  type DroppableTarget,\n  type InstanceSelector,\n  findLocalStyleSourcesWithinInstances,\n  getReparentDropTargetMutable,\n  getInstanceOrCreateFragmentIfNecessary,\n  wrapEditableChildrenAroundDropTargetMutable,\n} from \"./tree-utils\";\nimport {\n  insertStyleSources,\n  insertPortalLocalStyleSources,\n  insertLocalStyleSourcesWithNewIds,\n  deleteLocalStyleSourcesMutable,\n  collectStyleSourcesFromInstances,\n} from \"./style-source-utils\";\nimport { removeByMutable } from \"./array-utils\";\nimport { serverSyncStore } from \"./sync/sync-stores\";\nimport { setDifference, setUnion } from \"./shim\";\nimport { breakCyclesMutable, findCycles } from \"@webstudio-is/project-build\";\nimport {\n  $awareness,\n  $selectedInstancePath,\n  $selectedPage,\n  findAwarenessByInstanceId,\n  getInstancePath,\n  selectInstance,\n  type InstancePath,\n} from \"./awareness\";\nimport { findClosestInstanceMatchingFragment } from \"./matcher\";\nimport {\n  findAvailableVariables,\n  restoreExpressionVariables,\n  unsetExpressionVariables,\n} from \"./data-variables\";\nimport {\n  findClosestNonTextualContainer,\n  isRichTextTree,\n  isTreeSatisfyingContentModel,\n  isRichTextContent,\n} from \"./content-model\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { getInstanceLabel } from \"~/builder/shared/instance-label\";\nimport { $instanceTags } from \"~/builder/features/style-panel/shared/model\";\nimport { reactPropsToStandardAttributes } from \"@webstudio-is/react-sdk\";\n\n/**\n * structuredClone can be invoked on draft and throw error\n * extract current snapshot before cloning\n */\nexport const unwrap = <Value>(value: Value) =>\n  isDraft(value) ? current(value) : value;\n\nexport const updateWebstudioData = (mutate: (data: WebstudioData) => void) => {\n  serverSyncStore.createTransaction(\n    [\n      $pages,\n      $instances,\n      $props,\n      $breakpoints,\n      $styleSourceSelections,\n      $styleSources,\n      $styles,\n      $dataSources,\n      $resources,\n      $assets,\n    ],\n    (\n      pages,\n      instances,\n      props,\n      breakpoints,\n      styleSourceSelections,\n      styleSources,\n      styles,\n      dataSources,\n      resources,\n      assets\n    ) => {\n      // @todo normalize pages\n      if (pages === undefined) {\n        return;\n      }\n      mutate({\n        pages,\n        instances,\n        props,\n        dataSources,\n        resources,\n        breakpoints,\n        styleSourceSelections,\n        styleSources,\n        styles,\n        assets,\n      });\n\n      const cycles = findCycles(instances.values());\n\n      // Detect and fix cycles in the instance tree, then report\n      if (cycles.length > 0) {\n        toast.info(\"Detected and fixed cycles in the instance tree.\");\n\n        breakCyclesMutable(\n          instances.values(),\n          (node) => node.component === \"Slot\"\n        );\n      }\n    }\n  );\n};\n\nexport const getWebstudioData = (): WebstudioData => {\n  const pages = $pages.get();\n  if (pages === undefined) {\n    throw Error(`Cannot get webstudio data with empty pages`);\n  }\n  return {\n    pages,\n    instances: $instances.get(),\n    props: $props.get(),\n    dataSources: $dataSources.get(),\n    resources: $resources.get(),\n    breakpoints: $breakpoints.get(),\n    styleSourceSelections: $styleSourceSelections.get(),\n    styleSources: $styleSources.get(),\n    styles: $styles.get(),\n    assets: $assets.get(),\n  };\n};\n\nexport const findAllEditableInstanceSelector = ({\n  instanceSelector,\n  instances,\n  props,\n  metas,\n  results,\n}: {\n  instanceSelector: InstanceSelector;\n  instances: Instances;\n  props: Props;\n  metas: Map<string, WsComponentMeta>;\n  results: InstanceSelector[];\n}) => {\n  const [instanceId] = instanceSelector;\n\n  if (instanceId === undefined) {\n    return;\n  }\n\n  // Check if current instance is text editing instance\n  if (isRichTextTree({ instanceId, instances, props, metas })) {\n    results.push(instanceSelector);\n    return;\n  }\n\n  const instance = instances.get(instanceId);\n  if (instance) {\n    for (const child of instance.children) {\n      if (child.type === \"id\") {\n        findAllEditableInstanceSelector({\n          instanceSelector: [child.value, ...instanceSelector],\n          instances,\n          props,\n          metas,\n          results,\n        });\n      }\n    }\n  }\n};\n\nexport const insertInstanceChildrenMutable = (\n  data: Omit<WebstudioData, \"pages\">,\n  children: Instance[\"children\"],\n  insertTarget: Insertable\n) => {\n  const dropTarget: DroppableTarget = {\n    parentSelector: insertTarget.parentSelector,\n    position: insertTarget.position === \"after\" ? \"end\" : insertTarget.position,\n  };\n  const metas = $registeredComponentMetas.get();\n  insertTarget =\n    getInstanceOrCreateFragmentIfNecessary(data.instances, dropTarget) ??\n    insertTarget;\n  insertTarget =\n    wrapEditableChildrenAroundDropTargetMutable(\n      data.instances,\n      data.props,\n      metas,\n      dropTarget\n    ) ?? insertTarget;\n  const [parentInstanceId] = insertTarget.parentSelector;\n  const parentInstance = data.instances.get(parentInstanceId);\n  if (parentInstance === undefined) {\n    return;\n  }\n  if (dropTarget.position === \"end\") {\n    parentInstance.children.push(...children);\n  } else {\n    parentInstance.children.splice(dropTarget.position, 0, ...children);\n  }\n};\n\nexport const insertWebstudioElementAt = (insertable?: Insertable) => {\n  const instances = $instances.get();\n  const props = $props.get();\n  const metas = $registeredComponentMetas.get();\n  // find closest container and try to match new element with it\n  if (insertable === undefined) {\n    const instancePath = $selectedInstancePath.get();\n    if (instancePath === undefined) {\n      return false;\n    }\n    const [{ instanceSelector }] = instancePath;\n    const containerSelector = findClosestNonTextualContainer({\n      instances,\n      props,\n      metas,\n      instanceSelector,\n    });\n    const insertableIndex = instanceSelector.length - containerSelector.length;\n    if (insertableIndex === 0) {\n      insertable = {\n        parentSelector: containerSelector,\n        position: \"end\",\n      };\n    } else {\n      const containerInstance = instances.get(containerSelector[0]);\n      if (containerInstance === undefined) {\n        return false;\n      }\n      const lastChildInstanceId = instanceSelector[insertableIndex - 1];\n      const lastChildPosition = containerInstance.children.findIndex(\n        (child) => child.type === \"id\" && child.value === lastChildInstanceId\n      );\n      insertable = {\n        parentSelector: containerSelector,\n        position: lastChildPosition + 1,\n      };\n    }\n  }\n  // create element and find matching tag\n  const element: Instance = {\n    type: \"instance\",\n    id: nanoid(),\n    component: elementComponent,\n    children: [],\n  };\n  const newInstances = new Map(instances);\n  newInstances.set(element.id, element);\n  let matchingTag: undefined | string;\n  for (const tag of tags) {\n    element.tag = tag;\n    const isSatisfying = isTreeSatisfyingContentModel({\n      instances: newInstances,\n      props,\n      metas,\n      instanceSelector: [element.id, ...insertable.parentSelector],\n    });\n    if (isSatisfying) {\n      matchingTag = tag;\n      break;\n    }\n  }\n  if (matchingTag === undefined) {\n    return false;\n  }\n  // insert element\n  updateWebstudioData((data) => {\n    data.instances.set(element.id, element);\n    const children: Instance[\"children\"] = [{ type: \"id\", value: element.id }];\n    insertInstanceChildrenMutable(data, children, insertable);\n  });\n  selectInstance([element.id, ...insertable.parentSelector]);\n  return true;\n};\n\nexport const insertWebstudioFragmentAt = (\n  fragment: WebstudioFragment,\n  insertable?: Insertable,\n  conflictResolution?: ConflictResolution\n): boolean => {\n  const hasChildren = fragment.children.length > 0;\n  const hasTokens = fragment.styleSources.length > 0;\n  if (!hasChildren && !hasTokens) {\n    return false;\n  }\n  // Tokens-only fragment: insert tokens/breakpoints/styles without instances\n  if (!hasChildren && hasTokens) {\n    const project = $project.get();\n    if (project === undefined) {\n      return false;\n    }\n    updateWebstudioData((data) => {\n      insertWebstudioFragmentCopy({\n        data,\n        fragment,\n        availableVariables: [],\n        projectId: project.id,\n        conflictResolution,\n      });\n    });\n    return true;\n  }\n  const project = $project.get();\n  insertable = findClosestInsertable(fragment, insertable) ?? insertable;\n  if (project === undefined || insertable === undefined) {\n    return false;\n  }\n  let newInstanceSelector: undefined | InstanceSelector;\n  updateWebstudioData((data) => {\n    const instancePath = getInstancePath(\n      insertable.parentSelector,\n      data.instances\n    );\n    if (instancePath === undefined) {\n      return;\n    }\n    const { newInstanceIds } = insertWebstudioFragmentCopy({\n      data,\n      fragment,\n      availableVariables: findAvailableVariables({\n        ...data,\n        startingInstanceId: instancePath[0].instance.id,\n      }),\n      projectId: project.id,\n      conflictResolution,\n    });\n    const children: Instance[\"children\"] = fragment.children.map((child) => {\n      if (child.type === \"id\") {\n        return {\n          type: \"id\",\n          value: newInstanceIds.get(child.value) ?? child.value,\n        };\n      }\n      return child;\n    });\n    let parentSelector;\n    let position: number | \"end\";\n    if (insertable.position === \"after\") {\n      if (instancePath.length === 1) {\n        parentSelector = insertable.parentSelector;\n        position = \"end\";\n      } else {\n        parentSelector = instancePath[1].instanceSelector;\n        const [{ instance }, { instance: parentInstance }] = instancePath;\n        const index = parentInstance.children.findIndex(\n          (child) => child.type === \"id\" && child.value === instance.id\n        );\n        position = 1 + index;\n      }\n    } else {\n      parentSelector = insertable.parentSelector;\n      position = insertable.position;\n    }\n    insertInstanceChildrenMutable(data, children, {\n      parentSelector,\n      position,\n    });\n    newInstanceSelector = [children[0].value, ...parentSelector];\n  });\n  if (newInstanceSelector) {\n    selectInstance(newInstanceSelector);\n  }\n  return true;\n};\n\nexport const getComponentTemplateData = (\n  componentOrTemplate: string\n): WebstudioFragment => {\n  const templates = $registeredTemplates.get();\n  const templateMeta = templates.get(componentOrTemplate);\n  if (templateMeta) {\n    return templateMeta.template;\n  }\n  const newInstance: Instance = {\n    id: nanoid(),\n    type: \"instance\",\n    component: componentOrTemplate,\n    children: [],\n  };\n  return {\n    children: [{ type: \"id\", value: newInstance.id }],\n    instances: [newInstance],\n    props: [],\n    dataSources: [],\n    styleSourceSelections: [],\n    styleSources: [],\n    styles: [],\n    breakpoints: [],\n    assets: [],\n    resources: [],\n  };\n};\n\nexport const reparentInstanceMutable = (\n  data: Omit<WebstudioData, \"pages\">,\n  sourceInstanceSelector: InstanceSelector,\n  dropTarget: DroppableTarget\n) => {\n  const project = $project.get();\n  if (project === undefined) {\n    return;\n  }\n  const [rootInstanceId] = sourceInstanceSelector;\n  // delect is target is one of own descendants\n  // prevent reparenting to avoid infinite loop\n  const instanceDescendants = findTreeInstanceIds(\n    data.instances,\n    rootInstanceId\n  );\n  for (const instanceId of instanceDescendants) {\n    if (dropTarget.parentSelector.includes(instanceId)) {\n      return;\n    }\n  }\n  // try to use slot fragment as target instead of slot itself\n  const parentInstance = data.instances.get(dropTarget.parentSelector[0]);\n  if (\n    parentInstance?.component === portalComponent &&\n    parentInstance.children.length > 0 &&\n    parentInstance.children[0].type === \"id\"\n  ) {\n    const fragmentId = parentInstance.children[0].value;\n    dropTarget = {\n      parentSelector: [fragmentId, ...dropTarget.parentSelector],\n      position: dropTarget.position,\n    };\n  }\n  // move within same parent\n  if (sourceInstanceSelector[1] === dropTarget.parentSelector[0]) {\n    const [parentId] = dropTarget.parentSelector;\n    const parent = data.instances.get(parentId);\n    if (parent === undefined) {\n      return;\n    }\n    const prevPosition = parent.children.findIndex(\n      (child) => child.type === \"id\" && child.value === rootInstanceId\n    );\n    const child = parent.children[prevPosition];\n    parent?.children.splice(prevPosition, 1);\n    if (dropTarget.position === \"end\") {\n      parent?.children.push(child);\n    } else {\n      // when parent is the same, we need to adjust the position\n      // to account for the removal of the instance.\n      let nextPosition = dropTarget.position;\n      if (prevPosition < nextPosition) {\n        nextPosition -= 1;\n      }\n      parent?.children.splice(nextPosition, 0, child);\n    }\n    return sourceInstanceSelector;\n  }\n  // move into another parent\n  const fragment = extractWebstudioFragment(data, rootInstanceId);\n  deleteInstanceMutable(\n    data,\n    getInstancePath(sourceInstanceSelector, data.instances)\n  );\n  // prepare drop target after deleting instance to recreate new slot fragment\n  dropTarget =\n    getReparentDropTargetMutable(\n      data.instances,\n      data.props,\n      $registeredComponentMetas.get(),\n      dropTarget\n    ) ?? dropTarget;\n  const { newInstanceIds } = insertWebstudioFragmentCopy({\n    data,\n    fragment,\n    availableVariables: findAvailableVariables({\n      ...data,\n      startingInstanceId: dropTarget.parentSelector[0],\n    }),\n    projectId: project.id,\n  });\n  const [newParentId] = dropTarget.parentSelector;\n  const newRootInstanceId =\n    newInstanceIds.get(rootInstanceId) ?? rootInstanceId;\n  const newParent = data.instances.get(newParentId);\n  const newChild = { type: \"id\" as const, value: newRootInstanceId };\n  if (dropTarget.position === \"end\") {\n    newParent?.children.push(newChild);\n  } else {\n    newParent?.children.splice(dropTarget.position, 0, newChild);\n  }\n  return [newRootInstanceId, ...dropTarget.parentSelector];\n};\n\nexport const reparentInstance = (\n  sourceInstanceSelector: InstanceSelector,\n  dropTarget: DroppableTarget\n) => {\n  updateWebstudioData((data) => {\n    const newSelector = reparentInstanceMutable(\n      data,\n      sourceInstanceSelector,\n      dropTarget\n    );\n    selectInstance(newSelector);\n  });\n};\n\nexport const deleteInstanceMutable = (\n  data: Omit<WebstudioData, \"pages\">,\n  instancePath: undefined | InstancePath\n) => {\n  if (instancePath === undefined) {\n    return false;\n  }\n  const {\n    instances,\n    props,\n    styleSourceSelections,\n    styleSources,\n    styles,\n    dataSources,\n    resources,\n  } = data;\n  let targetInstance = instancePath[0].instance;\n  let parentInstance =\n    instancePath.length > 1 ? instancePath[1]?.instance : undefined;\n  const grandparentInstance =\n    instancePath.length > 2 ? instancePath[2]?.instance : undefined;\n\n  // delete parent fragment too if its last child is going to be deleted\n  // use case for slots: slot became empty and remove display: contents\n  // to be displayed properly on canvas\n  if (\n    parentInstance?.component === \"Fragment\" &&\n    parentInstance.children.length === 1 &&\n    grandparentInstance\n  ) {\n    targetInstance = parentInstance;\n    parentInstance = grandparentInstance;\n  }\n\n  const instanceIds = findTreeInstanceIdsExcludingSlotDescendants(\n    instances,\n    targetInstance.id\n  );\n  const localStyleSourceIds = findLocalStyleSourcesWithinInstances(\n    styleSources.values(),\n    styleSourceSelections.values(),\n    instanceIds\n  );\n\n  // mutate instances from data instead of instance path\n  parentInstance = data.instances.get(parentInstance?.id as string);\n  // may not exist when delete root\n  if (parentInstance) {\n    removeByMutable(\n      parentInstance.children,\n      (child) => child.type === \"id\" && child.value === targetInstance.id\n    );\n  }\n\n  for (const instanceId of instanceIds) {\n    instances.delete(instanceId);\n  }\n  // delete props, data sources and styles of deleted instance and its descendants\n  for (const prop of props.values()) {\n    if (instanceIds.has(prop.instanceId)) {\n      props.delete(prop.id);\n      if (prop.type === \"resource\") {\n        resources.delete(prop.value);\n      }\n    }\n  }\n  for (const dataSource of dataSources.values()) {\n    if (instanceIds.has(dataSource.scopeInstanceId ?? \"\")) {\n      dataSources.delete(dataSource.id);\n      if (dataSource.type === \"resource\") {\n        resources.delete(dataSource.resourceId);\n      }\n    }\n  }\n  for (const instanceId of instanceIds) {\n    styleSourceSelections.delete(instanceId);\n  }\n  deleteLocalStyleSourcesMutable({\n    localStyleSourceIds,\n    styleSources,\n    styles,\n  });\n  return true;\n};\n\nexport const unwrapInstanceMutable = ({\n  instances,\n  props,\n  metas,\n  selectedItem,\n  parentItem,\n}: {\n  instances: Map<string, Instance>;\n  props: Props;\n  metas: Map<string, WsComponentMeta>;\n  selectedItem: {\n    instanceSelector: InstanceSelector;\n    instance: { id: string };\n  };\n  parentItem: { instanceSelector: InstanceSelector; instance: { id: string } };\n}): { success: boolean; error?: string } => {\n  // Check if the selected instance is rich text content (like Bold, Italic in Paragraph)\n  if (\n    isRichTextContent({\n      instanceSelector: selectedItem.instanceSelector,\n      instances,\n      props,\n      metas,\n    })\n  ) {\n    return { success: false, error: \"Cannot unwrap textual instance\" };\n  }\n\n  const parentInstance = instances.get(parentItem.instance.id);\n  const selectedInstance = instances.get(selectedItem.instance.id);\n  if (!parentInstance || !selectedInstance) {\n    return { success: false, error: \"Instance not found\" };\n  }\n\n  // Get grandparent to replace parent with selected\n  const grandparentId = parentItem.instanceSelector[1];\n  if (!grandparentId) {\n    return { success: false, error: \"Cannot unwrap instance at root level\" };\n  }\n  const grandparentInstance = instances.get(grandparentId);\n  if (!grandparentInstance) {\n    return { success: false, error: \"Grandparent instance not found\" };\n  }\n\n  // Remove selected instance from parent's children\n  const selectedIndexInParent = parentInstance.children.findIndex(\n    (child) => child.type === \"id\" && child.value === selectedItem.instance.id\n  );\n  if (selectedIndexInParent !== -1) {\n    parentInstance.children.splice(selectedIndexInParent, 1);\n  }\n\n  // If parent has no more children, delete it\n  if (parentInstance.children.length === 0) {\n    instances.delete(parentItem.instance.id);\n  }\n\n  // Add selected instance to grandparent at parent's position\n  const parentIndex = grandparentInstance.children.findIndex(\n    (child) => child.type === \"id\" && child.value === parentItem.instance.id\n  );\n  if (parentIndex !== -1) {\n    if (parentInstance.children.length === 0) {\n      // Replace parent with selected if parent is now empty\n      grandparentInstance.children[parentIndex] = {\n        type: \"id\",\n        value: selectedItem.instance.id,\n      };\n    } else {\n      // Insert selected after parent if parent still has children\n      grandparentInstance.children.splice(parentIndex + 1, 0, {\n        type: \"id\",\n        value: selectedItem.instance.id,\n      });\n    }\n  }\n\n  const matches = isTreeSatisfyingContentModel({\n    instances,\n    props,\n    metas,\n    instanceSelector: [\n      selectedItem.instance.id,\n      ...parentItem.instanceSelector.slice(1),\n    ],\n  });\n  if (matches === false) {\n    return { success: false, error: \"Cannot unwrap instance\" };\n  }\n\n  return { success: true };\n};\n\nexport const canUnwrapInstance = (instancePath: InstancePath) => {\n  // Need at least 3 levels: selected, parent, and grandparent\n  // Can't unwrap if there's no grandparent to move the selected instance to\n  if (instancePath.length < 3) {\n    return false;\n  }\n  const [selectedItem, parentItem] = instancePath;\n\n  // Prevent unwrapping if parent is the root instance (e.g., Body)\n  const rootInstanceId = $selectedPage.get()?.rootInstanceId;\n  if (\n    rootInstanceId !== undefined &&\n    parentItem.instance.id === rootInstanceId\n  ) {\n    return false;\n  }\n\n  // Check if the selected instance is rich text content (like Bold, Italic in Paragraph)\n  const instances = $instances.get();\n  const props = $props.get();\n  const metas = $registeredComponentMetas.get();\n\n  if (\n    isRichTextContent({\n      instanceSelector: selectedItem.instanceSelector,\n      instances,\n      props,\n      metas,\n    })\n  ) {\n    return false;\n  }\n\n  return true;\n};\n\nexport const toggleInstanceShow = (instanceId: Instance[\"id\"]) => {\n  serverSyncStore.createTransaction([$props], (props) => {\n    const allProps = Array.from(props.values());\n    const instanceProps = allProps.filter(\n      (prop) => prop.instanceId === instanceId\n    );\n    let showProp = instanceProps.find((prop) => prop.name === showAttribute);\n\n    // Toggle the show value\n    const newValue = showProp?.type === \"boolean\" ? !showProp.value : false;\n\n    if (showProp === undefined) {\n      showProp = {\n        id: nanoid(),\n        instanceId,\n        name: showAttribute,\n        type: \"boolean\",\n        value: newValue,\n      };\n    }\n    if (showProp.type === \"boolean\") {\n      props.set(showProp.id, { ...showProp, value: newValue });\n    }\n  });\n};\n\nexport const wrapInstance = (component: string, tag?: string) => {\n  const instancePath = $selectedInstancePath.get();\n  // global root or body are selected\n  if (instancePath === undefined || instancePath.length === 1) {\n    return;\n  }\n  const [selectedItem, parentItem] = instancePath;\n  const selectedInstance = selectedItem.instance;\n  const newInstanceId = nanoid();\n  const newInstanceSelector = [newInstanceId, ...parentItem.instanceSelector];\n  const metas = $registeredComponentMetas.get();\n\n  try {\n    updateWebstudioData((data) => {\n      const isContent = isRichTextContent({\n        instanceSelector: selectedItem.instanceSelector,\n        instances: data.instances,\n        props: data.props,\n        metas,\n      });\n      if (isContent) {\n        toast.error(`Cannot wrap textual content`);\n        throw Error(\"Abort transaction\");\n      }\n      const newInstance: Instance = {\n        type: \"instance\",\n        id: newInstanceId,\n        component,\n        children: [{ type: \"id\", value: selectedInstance.id }],\n      };\n\n      if (tag || component === elementComponent) {\n        newInstance.tag = tag ?? \"div\";\n      }\n      const parentInstance = data.instances.get(parentItem.instance.id);\n      data.instances.set(newInstanceId, newInstance);\n      if (parentInstance) {\n        for (const child of parentInstance.children) {\n          if (child.type === \"id\" && child.value === selectedInstance.id) {\n            child.value = newInstanceId;\n          }\n        }\n      }\n\n      const isSatisfying = isTreeSatisfyingContentModel({\n        instances: data.instances,\n        props: data.props,\n        metas,\n        instanceSelector: newInstanceSelector,\n      });\n\n      if (isSatisfying === false) {\n        const label = getInstanceLabel({ component, tag });\n        toast.error(`Cannot wrap in ${label}`);\n        throw Error(\"Abort transaction\");\n      }\n    });\n    selectInstance(newInstanceSelector);\n  } catch {\n    // do nothing\n  }\n};\n\n// Check if an instance can be converted to a specific component or tag\nexport const canConvertInstance = (\n  selectedInstanceId: string,\n  selectedInstanceSelector: string[],\n  component: string,\n  tag: string | undefined,\n  instances: Instances,\n  props: Props,\n  metas: Map<Instance[\"component\"], WsComponentMeta>\n): boolean => {\n  const selectedInstance = instances.get(selectedInstanceId);\n\n  if (!selectedInstance) {\n    return false;\n  }\n\n  // Create a test instance with the new component/tag\n  const testInstance: Instance = {\n    ...selectedInstance,\n    component,\n  };\n\n  if (tag || component === elementComponent) {\n    testInstance.tag = tag ?? \"div\";\n  } else {\n    // For components with presetStyle (like Heading, Box), infer default tag\n    const meta = metas.get(component);\n    const defaultTag = Object.keys(\n      (meta as { presetStyle?: Record<string, unknown> })?.presetStyle ?? {}\n    ).at(0);\n    if (defaultTag) {\n      testInstance.tag = defaultTag;\n    }\n  }\n\n  const newInstances = new Map(instances);\n  newInstances.set(testInstance.id, testInstance);\n\n  // Validate the converted instance satisfies content model\n  return isTreeSatisfyingContentModel({\n    instances: newInstances,\n    props,\n    metas,\n    instanceSelector: selectedInstanceSelector,\n  });\n};\n\nexport const convertInstance = (component: string, tag?: string) => {\n  const instancePath = $selectedInstancePath.get();\n  // global root or body are selected\n  if (instancePath === undefined || instancePath.length === 1) {\n    return;\n  }\n  const [selectedItem] = instancePath;\n  const selectedInstance = selectedItem.instance;\n  const selectedInstanceSelector = selectedItem.instanceSelector;\n  const metas = $registeredComponentMetas.get();\n  const instanceTags = $instanceTags.get();\n  try {\n    updateWebstudioData((data) => {\n      const instance = data.instances.get(selectedInstance.id);\n      if (instance === undefined) {\n        return;\n      }\n      instance.component = component;\n      // convert to specified tag or with currently used\n      if (tag || component === elementComponent) {\n        instance.tag = tag ?? instanceTags.get(selectedInstance.id) ?? \"div\";\n        // delete legacy tag prop if specified\n        for (const prop of data.props.values()) {\n          if (prop.instanceId !== selectedInstance.id) {\n            continue;\n          }\n          if (prop.name === \"tag\") {\n            data.props.delete(prop.id);\n            continue;\n          }\n          const newName = reactPropsToStandardAttributes[prop.name];\n          if (newName) {\n            const newId = `${prop.instanceId}:${newName}`;\n            data.props.delete(prop.id);\n            data.props.set(newId, { ...prop, id: newId, name: newName });\n          }\n        }\n      }\n      const isSatisfying = isTreeSatisfyingContentModel({\n        instances: data.instances,\n        props: data.props,\n        metas,\n        instanceSelector: selectedInstanceSelector,\n      });\n      if (isSatisfying === false) {\n        const label = getInstanceLabel({ component, tag });\n        toast.error(`Cannot convert to ${label}`);\n        throw Error(\"Abort transaction\");\n      }\n    });\n  } catch {\n    // do nothing\n  }\n};\n\nexport const unwrapInstance = () => {\n  const instancePath = $selectedInstancePath.get();\n  if (instancePath === undefined || !canUnwrapInstance(instancePath)) {\n    return;\n  }\n\n  const [selectedItem, parentItem] = instancePath;\n\n  try {\n    updateWebstudioData((data) => {\n      const result = unwrapInstanceMutable({\n        instances: data.instances,\n        props: data.props,\n        metas: $registeredComponentMetas.get(),\n        selectedItem,\n        parentItem,\n      });\n\n      if (!result.success) {\n        toast.error(result.error ?? \"Cannot unwrap instance\");\n        throw Error(\"Abort transaction\");\n      }\n    });\n    // After unwrap, select the child that replaced the parent\n    selectInstance([\n      selectedItem.instance.id,\n      ...parentItem.instanceSelector.slice(1),\n    ]);\n  } catch {\n    // do nothing\n  }\n};\n\nexport const deleteSelectedInstance = () => {\n  if ($isPreviewMode.get()) {\n    return;\n  }\n  const textEditingInstanceSelector = $textEditingInstanceSelector.get();\n  const instancePath = $selectedInstancePath.get();\n  // cannot delete instance while editing\n  if (textEditingInstanceSelector) {\n    return;\n  }\n  if (instancePath === undefined || instancePath.length === 1) {\n    return;\n  }\n  const [selectedItem, parentItem] = instancePath;\n  const selectedInstanceSelector = selectedItem.instanceSelector;\n  const instances = $instances.get();\n  if (!isComponentDetachable(selectedItem.instance.component)) {\n    toast.error(\n      \"This instance can not be moved outside of its parent component.\"\n    );\n    return false;\n  }\n\n  if ($isContentMode.get()) {\n    // In content mode we are allowing to delete childen of the editable block\n    const editableInstanceSelector = findBlockSelector(\n      selectedInstanceSelector,\n      instances\n    );\n    if (editableInstanceSelector === undefined) {\n      builderApi.toast.info(\"You can't delete this instance in conent mode.\");\n      return;\n    }\n\n    const isChildOfBlock =\n      selectedInstanceSelector.length - editableInstanceSelector.length === 1;\n\n    const isTemplateInstance =\n      instances.get(selectedInstanceSelector[0])?.component ===\n      blockTemplateComponent;\n\n    if (isTemplateInstance) {\n      builderApi.toast.info(\"You can't delete this instance in content mode.\");\n      return;\n    }\n\n    if (!isChildOfBlock) {\n      builderApi.toast.info(\"You can't delete this instance in content mode.\");\n      return;\n    }\n  }\n\n  // find next selected instance\n  let newSelectedInstanceSelector: undefined | InstanceSelector;\n  const parentInstanceSelector = parentItem.instanceSelector;\n  const siblingIds = parentItem.instance.children\n    .filter((child) => child.type === \"id\")\n    .map((child) => child.value);\n  const position = siblingIds.indexOf(selectedItem.instance.id);\n  const siblingId = siblingIds[position + 1] ?? siblingIds[position - 1];\n  if (siblingId) {\n    // select next or previous sibling if possible\n    newSelectedInstanceSelector = [siblingId, ...parentInstanceSelector];\n  } else {\n    // fallback to parent\n    newSelectedInstanceSelector = parentInstanceSelector;\n  }\n  updateWebstudioData((data) => {\n    if (deleteInstanceMutable(data, instancePath)) {\n      selectInstance(newSelectedInstanceSelector);\n    }\n  });\n};\n\nconst traverseStyleValue = (\n  value: StyleValue,\n  callback: (value: StyleValue) => void\n) => {\n  if (\n    value.type === \"fontFamily\" ||\n    value.type === \"image\" ||\n    value.type === \"unit\" ||\n    value.type === \"keyword\" ||\n    value.type === \"unparsed\" ||\n    value.type === \"invalid\" ||\n    value.type === \"unset\" ||\n    value.type === \"color\" ||\n    value.type === \"rgb\" ||\n    value.type === \"function\" ||\n    value.type === \"guaranteedInvalid\"\n  ) {\n    callback(value);\n    return;\n  }\n  if (value.type === \"var\") {\n    if (value.fallback) {\n      traverseStyleValue(value.fallback, callback);\n    }\n    return;\n  }\n  if (value.type === \"tuple\" || value.type === \"layers\") {\n    for (const item of value.value) {\n      traverseStyleValue(item, callback);\n    }\n    return;\n  }\n  if (value.type === \"shadow\") {\n    traverseStyleValue(value.offsetX, callback);\n    traverseStyleValue(value.offsetY, callback);\n    if (value.blur) {\n      traverseStyleValue(value.blur, callback);\n    }\n    if (value.spread) {\n      traverseStyleValue(value.spread, callback);\n    }\n    if (value.color) {\n      traverseStyleValue(value.color, callback);\n    }\n    return;\n  }\n  value satisfies never;\n};\n\nexport const extractWebstudioFragment = (\n  data: Omit<WebstudioData, \"pages\">,\n  rootInstanceId: string,\n  options: { unsetVariables?: Set<DataSource[\"id\"]> } = {}\n): WebstudioFragment => {\n  const {\n    assets,\n    instances,\n    dataSources,\n    resources,\n    props,\n    styleSourceSelections,\n    styleSources,\n    breakpoints,\n    styles,\n  } = data;\n\n  // collect the instance by id and all its descendants including portal instances\n  const fragmentInstanceIds = findTreeInstanceIds(instances, rootInstanceId);\n  let fragmentInstances: Instance[] = [];\n\n  // Collect style sources and selections from instances\n  const {\n    styleSourceSelectionsArray: fragmentStyleSourceSelections,\n    styleSourcesMap: fragmentStyleSources,\n    stylesArray: fragmentStyles,\n  } = collectStyleSourcesFromInstances({\n    instanceIds: fragmentInstanceIds,\n    styleSourceSelections,\n    styleSources,\n    styles,\n  });\n\n  for (const instanceId of fragmentInstanceIds) {\n    const instance = instances.get(instanceId);\n    if (instance) {\n      fragmentInstances.push(instance);\n    }\n  }\n\n  const fragmentAssetIds = new Set<Asset[\"id\"]>();\n  const fragmentFontFamilies = new Set<string>();\n\n  // collect breakpoints and assets from styles\n  const fragmentBreapoints: Breakpoints = new Map();\n  for (const styleDecl of fragmentStyles) {\n    // collect breakpoints\n    if (fragmentBreapoints.has(styleDecl.breakpointId) === false) {\n      const breakpoint = breakpoints.get(styleDecl.breakpointId);\n      if (breakpoint) {\n        fragmentBreapoints.set(styleDecl.breakpointId, breakpoint);\n      }\n    }\n\n    // collect assets including fonts\n    traverseStyleValue(styleDecl.value, (value) => {\n      if (value.type === \"fontFamily\") {\n        for (const fontFamily of value.value) {\n          fragmentFontFamilies.add(fontFamily);\n        }\n      }\n      if (value.type === \"image\") {\n        if (value.value.type === \"asset\") {\n          fragmentAssetIds.add(value.value.value);\n        }\n      }\n    });\n  }\n\n  // collect variables scoped to fragment instances\n  // and variables outside of scope to unset\n  const fragmentDataSources: DataSources = new Map();\n  const fragmentResourceIds = new Set<Resource[\"id\"]>();\n  const unsetNameById = new Map<DataSource[\"id\"], DataSource[\"name\"]>();\n  const unsetVariables = options.unsetVariables ?? new Set();\n  for (const dataSource of dataSources.values()) {\n    if (\n      fragmentInstanceIds.has(dataSource.scopeInstanceId ?? \"\") &&\n      unsetVariables.has(dataSource.id) === false\n    ) {\n      fragmentDataSources.set(dataSource.id, dataSource);\n      if (dataSource.type === \"resource\") {\n        fragmentResourceIds.add(dataSource.resourceId);\n      }\n    } else {\n      unsetNameById.set(dataSource.id, dataSource.name);\n    }\n  }\n\n  // unset variables outside of scope\n  fragmentInstances = fragmentInstances.map((instance) => {\n    instance = structuredClone(unwrap(instance));\n    for (const child of instance.children) {\n      if (child.type === \"expression\") {\n        const expression = child.value;\n        child.value = unsetExpressionVariables({ expression, unsetNameById });\n      }\n    }\n    return instance;\n  });\n\n  // collect props bound to these instances\n  // and unset variables outside of scope\n  const fragmentProps: Prop[] = [];\n  for (const prop of props.values()) {\n    if (fragmentInstanceIds.has(prop.instanceId) === false) {\n      continue;\n    }\n\n    if (prop.type === \"expression\") {\n      const newProp = structuredClone(unwrap(prop));\n      const expression = prop.value;\n      newProp.value = unsetExpressionVariables({ expression, unsetNameById });\n      fragmentProps.push(newProp);\n      continue;\n    }\n\n    if (prop.type === \"action\") {\n      const newProp = structuredClone(unwrap(prop));\n      for (const value of newProp.value) {\n        const expression = value.code;\n        value.code = unsetExpressionVariables({ expression, unsetNameById });\n      }\n      fragmentProps.push(newProp);\n      continue;\n    }\n\n    fragmentProps.push(prop);\n\n    // collect assets\n    if (prop.type === \"asset\") {\n      fragmentAssetIds.add(prop.value);\n    }\n\n    // collect resources from props\n    if (prop.type === \"resource\") {\n      fragmentResourceIds.add(prop.value);\n    }\n  }\n\n  // collect resources bound to all fragment data sources\n  // and unset variables which are defined outside of scope\n  // and used in resource\n  const fragmentResources: Resource[] = [];\n  for (const resourceId of fragmentResourceIds) {\n    const resource = resources.get(resourceId);\n    if (resource === undefined) {\n      continue;\n    }\n    const newResource = structuredClone(unwrap(resource));\n    newResource.url = unsetExpressionVariables({\n      expression: newResource.url,\n      unsetNameById,\n    });\n    for (const header of newResource.headers) {\n      header.value = unsetExpressionVariables({\n        expression: header.value,\n        unsetNameById,\n      });\n    }\n    if (newResource.searchParams) {\n      for (const searchParam of newResource.searchParams) {\n        searchParam.value = unsetExpressionVariables({\n          expression: searchParam.value,\n          unsetNameById,\n        });\n      }\n    }\n    if (newResource.body) {\n      newResource.body = unsetExpressionVariables({\n        expression: newResource.body,\n        unsetNameById,\n      });\n    }\n    fragmentResources.push(newResource);\n  }\n\n  const fragmentAssets: Asset[] = [];\n  for (const asset of assets.values()) {\n    if (\n      fragmentAssetIds.has(asset.id) ||\n      (asset.type === \"font\" && fragmentFontFamilies.has(asset.meta.family))\n    ) {\n      fragmentAssets.push(asset);\n    }\n  }\n\n  return {\n    children: [{ type: \"id\", value: rootInstanceId }],\n    instances: fragmentInstances,\n    styleSourceSelections: fragmentStyleSourceSelections,\n    styleSources: Array.from(fragmentStyleSources.values()),\n    breakpoints: Array.from(fragmentBreapoints.values()),\n    styles: fragmentStyles,\n    dataSources: Array.from(fragmentDataSources.values()),\n    resources: fragmentResources,\n    props: fragmentProps,\n    assets: fragmentAssets,\n  };\n};\n\nconst replaceDataSources = (\n  code: string,\n  replacements: Map<DataSource[\"id\"], DataSource[\"id\"]>\n) => {\n  return transpileExpression({\n    expression: code,\n    replaceVariable: (identifier) => {\n      const dataSourceId = decodeDataSourceVariable(identifier);\n      if (dataSourceId === undefined) {\n        return;\n      }\n      return encodeDataSourceVariable(\n        replacements.get(dataSourceId) ?? dataSourceId\n      );\n    },\n  });\n};\n\nexport const insertWebstudioFragmentCopy = ({\n  data,\n  fragment,\n  availableVariables,\n  projectId,\n  conflictResolution = \"theirs\",\n}: {\n  data: Omit<WebstudioData, \"pages\">;\n  fragment: WebstudioFragment;\n  availableVariables: DataSource[];\n  projectId: Project[\"id\"];\n  conflictResolution?: ConflictResolution;\n}) => {\n  const newInstanceIds = new Map<Instance[\"id\"], Instance[\"id\"]>();\n  const newDataSourceIds = new Map<DataSource[\"id\"], DataSource[\"id\"]>();\n  const newDataIds = {\n    newInstanceIds,\n    newDataSourceIds,\n  };\n\n  const fragmentInstances: Instances = new Map();\n  const portalContentRootIds = new Set<Instance[\"id\"]>();\n  for (const instance of fragment.instances) {\n    fragmentInstances.set(instance.id, instance);\n    if (instance.component === portalComponent) {\n      for (const child of instance.children) {\n        if (child.type === \"id\") {\n          portalContentRootIds.add(child.value);\n        }\n      }\n    }\n  }\n\n  const {\n    assets,\n    instances,\n    resources,\n    dataSources,\n    props,\n    breakpoints,\n    styleSources,\n    styles,\n    styleSourceSelections,\n  } = data;\n\n  /**\n   * insert reusables without changing their ids to not bloat data\n   * and catch up with user changes\n   * - assets\n   * - breakpoints\n   * - token styles\n   * - portals\n   *\n   * breakpoints behave slightly differently and merged with existing ones\n   * and those ids are used instead\n   */\n\n  // insert assets\n\n  for (const asset of fragment.assets) {\n    // asset can be already present if pasting to the same project\n    if (assets.has(asset.id) === false) {\n      // we use the same asset.id so the references are preserved\n      assets.set(asset.id, { ...asset, projectId });\n    }\n  }\n\n  // merge breakpoints\n\n  const mergedBreakpointIds = buildMergedBreakpointIds(\n    fragment.breakpoints,\n    breakpoints\n  );\n  for (const newBreakpoint of fragment.breakpoints) {\n    if (mergedBreakpointIds.has(newBreakpoint.id) === false) {\n      breakpoints.set(newBreakpoint.id, newBreakpoint);\n    }\n  }\n\n  // insert tokens with their styles\n\n  const { styleSourceIds, styleSourceIdMap, updatedStyleSources } =\n    insertStyleSources({\n      fragmentStyleSources: fragment.styleSources,\n      fragmentStyles: fragment.styles,\n      existingStyleSources: styleSources,\n      existingStyles: styles,\n      breakpoints,\n      mergedBreakpointIds,\n      conflictResolution,\n    });\n\n  // Update styleSources map with the new tokens\n  for (const [id, styleSource] of updatedStyleSources) {\n    styleSources.set(id, styleSource);\n  }\n\n  for (const styleDecl of fragment.styles) {\n    if (styleSourceIds.has(styleDecl.styleSourceId)) {\n      const { breakpointId } = styleDecl;\n      const newStyleDecl: StyleDecl = {\n        ...styleDecl,\n        breakpointId: mergedBreakpointIds.get(breakpointId) ?? breakpointId,\n        // Remap the styleSourceId to the new token ID\n        styleSourceId:\n          styleSourceIdMap.get(styleDecl.styleSourceId) ??\n          styleDecl.styleSourceId,\n      };\n      styles.set(getStyleDeclKey(newStyleDecl), newStyleDecl);\n    }\n  }\n\n  let portalContentIds = new Set<Instance[\"id\"]>();\n\n  // insert portal contents\n  // - instances\n  // - data sources\n  // - props\n  // - local styles\n  for (const rootInstanceId of portalContentRootIds) {\n    const instanceIds = findTreeInstanceIdsExcludingSlotDescendants(\n      fragmentInstances,\n      rootInstanceId\n    );\n    portalContentIds = setUnion(portalContentIds, instanceIds);\n\n    // prevent reinserting portals which could be already changed by user\n    if (instances.has(rootInstanceId)) {\n      continue;\n    }\n\n    const usedResourceIds = new Set<Resource[\"id\"]>();\n    for (const dataSource of fragment.dataSources) {\n      // insert only data sources within portal content\n      if (instanceIds.has(dataSource.scopeInstanceId ?? \"\")) {\n        dataSources.set(dataSource.id, dataSource);\n        if (dataSource.type === \"resource\") {\n          usedResourceIds.add(dataSource.resourceId);\n        }\n      }\n    }\n\n    for (const prop of fragment.props) {\n      if (instanceIds.has(prop.instanceId)) {\n        props.set(prop.id, prop);\n        if (prop.type === \"resource\") {\n          usedResourceIds.add(prop.value);\n        }\n      }\n    }\n\n    for (const resource of fragment.resources) {\n      if (usedResourceIds.has(resource.id)) {\n        resources.set(resource.id, resource);\n      }\n    }\n\n    for (const instance of fragment.instances) {\n      if (instanceIds.has(instance.id)) {\n        instances.set(instance.id, instance);\n      }\n    }\n\n    // insert local style sources with their styles\n\n    insertPortalLocalStyleSources({\n      fragmentStyleSources: fragment.styleSources,\n      fragmentStyleSourceSelections: fragment.styleSourceSelections,\n      fragmentStyles: fragment.styles,\n      instanceIds,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n  }\n\n  /**\n   * inserting unique content is structurally similar to inserting portal content\n   * but all ids are regenerated to support duplicating existing content\n   * - instances\n   * - data sources\n   * - props\n   * - local styles\n   */\n\n  // generate new ids only instances outside of portals\n  const fragmentInstanceIds = setDifference(\n    new Set(fragmentInstances.keys()),\n    portalContentIds\n  );\n  for (const instanceId of fragmentInstanceIds) {\n    newInstanceIds.set(instanceId, nanoid());\n  }\n  fragmentInstanceIds.add(ROOT_INSTANCE_ID);\n  newInstanceIds.set(ROOT_INSTANCE_ID, ROOT_INSTANCE_ID);\n\n  const maskedIdByName = new Map<DataSource[\"name\"], DataSource[\"id\"]>();\n  for (const dataSource of availableVariables) {\n    maskedIdByName.set(dataSource.name, dataSource.id);\n  }\n  const newResourceIds = new Map<Resource[\"id\"], Resource[\"id\"]>();\n  for (let dataSource of fragment.dataSources) {\n    const scopeInstanceId = dataSource.scopeInstanceId ?? \"\";\n    if (scopeInstanceId === ROOT_INSTANCE_ID) {\n      // add global variable only if not exist already\n      if (\n        dataSources.has(dataSource.id) === false &&\n        maskedIdByName.has(dataSource.name) === false\n      ) {\n        dataSources.set(dataSource.id, dataSource);\n      }\n      continue;\n    }\n    // insert only data sources within portal content\n    if (fragmentInstanceIds.has(scopeInstanceId)) {\n      const newDataSourceId = nanoid();\n      newDataSourceIds.set(dataSource.id, newDataSourceId);\n      dataSource = structuredClone(unwrap(dataSource));\n      dataSource.id = newDataSourceId;\n      dataSource.scopeInstanceId =\n        newInstanceIds.get(scopeInstanceId) ?? scopeInstanceId;\n      if (dataSource.type === \"resource\") {\n        const newResourceId = nanoid();\n        newResourceIds.set(dataSource.resourceId, newResourceId);\n        dataSource.resourceId = newResourceId;\n      }\n      dataSources.set(dataSource.id, dataSource);\n    }\n  }\n\n  for (let prop of fragment.props) {\n    if (fragmentInstanceIds.has(prop.instanceId) === false) {\n      continue;\n    }\n    prop = structuredClone(unwrap(prop));\n    prop.id = nanoid();\n    prop.instanceId = newInstanceIds.get(prop.instanceId) ?? prop.instanceId;\n    if (prop.type === \"expression\") {\n      prop.value = restoreExpressionVariables({\n        expression: prop.value,\n        maskedIdByName,\n      });\n      prop.value = replaceDataSources(prop.value, newDataSourceIds);\n    }\n    if (prop.type === \"action\") {\n      for (const value of prop.value) {\n        value.code = restoreExpressionVariables({\n          expression: value.code,\n          maskedIdByName,\n        });\n        value.code = replaceDataSources(value.code, newDataSourceIds);\n      }\n    }\n    if (prop.type === \"parameter\") {\n      prop.value = newDataSourceIds.get(prop.value) ?? prop.value;\n    }\n    if (prop.type === \"resource\") {\n      const newResourceId = nanoid();\n      newResourceIds.set(prop.value, newResourceId);\n      prop.value = newResourceId;\n    }\n    props.set(prop.id, prop);\n  }\n\n  for (let resource of fragment.resources) {\n    if (newResourceIds.has(resource.id) === false) {\n      continue;\n    }\n    resource = structuredClone(unwrap(resource));\n    resource.id = newResourceIds.get(resource.id) ?? resource.id;\n    resource.url = restoreExpressionVariables({\n      expression: resource.url,\n      maskedIdByName,\n    });\n    resource.url = replaceDataSources(resource.url, newDataSourceIds);\n    for (const header of resource.headers) {\n      header.value = restoreExpressionVariables({\n        expression: header.value,\n        maskedIdByName,\n      });\n      header.value = replaceDataSources(header.value, newDataSourceIds);\n    }\n    if (resource.searchParams) {\n      for (const searchParam of resource.searchParams) {\n        searchParam.value = restoreExpressionVariables({\n          expression: searchParam.value,\n          maskedIdByName,\n        });\n        searchParam.value = replaceDataSources(\n          searchParam.value,\n          newDataSourceIds\n        );\n      }\n    }\n    if (resource.body) {\n      resource.body = restoreExpressionVariables({\n        expression: resource.body,\n        maskedIdByName,\n      });\n      resource.body = replaceDataSources(resource.body, newDataSourceIds);\n    }\n    resources.set(resource.id, resource);\n  }\n\n  for (let instance of fragment.instances) {\n    if (fragmentInstanceIds.has(instance.id)) {\n      instance = structuredClone(unwrap(instance));\n      instance.id = newInstanceIds.get(instance.id) ?? instance.id;\n      for (const child of instance.children) {\n        if (child.type === \"id\") {\n          child.value = newInstanceIds.get(child.value) ?? child.value;\n        }\n        if (child.type === \"expression\") {\n          child.value = restoreExpressionVariables({\n            expression: child.value,\n            maskedIdByName,\n          });\n          child.value = replaceDataSources(child.value, newDataSourceIds);\n        }\n      }\n      instances.set(instance.id, instance);\n    }\n  }\n\n  // insert local styles with new ids\n\n  insertLocalStyleSourcesWithNewIds({\n    fragmentStyleSources: fragment.styleSources,\n    fragmentStyleSourceSelections: fragment.styleSourceSelections,\n    fragmentStyles: fragment.styles,\n    fragmentInstanceIds,\n    newInstanceIds,\n    styleSourceIdMap,\n    styleSources,\n    styleSourceSelections,\n    styles,\n    mergedBreakpointIds,\n  });\n\n  return newDataIds;\n};\n\nexport const findClosestSlot = (\n  instances: Instances,\n  instanceSelector: InstanceSelector\n) => {\n  for (const instanceId of instanceSelector) {\n    const instance = instances.get(instanceId);\n    if (instance?.component === \"Slot\") {\n      return instance;\n    }\n  }\n};\n\nexport type Insertable = {\n  parentSelector: InstanceSelector;\n  position: number | \"end\" | \"after\";\n};\n\nexport const findClosestInsertable = (\n  fragment: WebstudioFragment,\n  from?: Insertable\n): undefined | Insertable => {\n  const selectedPage = $selectedPage.get();\n  const awareness = $awareness.get();\n  if (selectedPage === undefined) {\n    return;\n  }\n  // paste to the page root if nothing is selected\n  const instanceSelector = from?.parentSelector ??\n    awareness?.instanceSelector ?? [selectedPage.rootInstanceId];\n  if (instanceSelector[0] === ROOT_INSTANCE_ID) {\n    toast.error(`Cannot insert into Global root`);\n    return;\n  }\n  const metas = $registeredComponentMetas.get();\n  const instances = $instances.get();\n  const props = $props.get();\n  const containerSelector = findClosestNonTextualContainer({\n    metas,\n    props,\n    instances,\n    instanceSelector,\n  });\n  const closestContainerIndex =\n    instanceSelector.length - containerSelector.length;\n  if (closestContainerIndex === -1) {\n    return;\n  }\n  let insertableIndex = findClosestInstanceMatchingFragment({\n    metas,\n    instances,\n    props,\n    instanceSelector: instanceSelector.slice(closestContainerIndex),\n    fragment,\n    onError: (message) => {\n      const component = fragment.instances[0].component;\n      const label = getInstanceLabel({ component });\n      toast.warn(message || `\"${label}\" has no place here`);\n    },\n  });\n  if (insertableIndex === -1) {\n    // fallback to closest container to always insert something\n    // even when validation fails\n    insertableIndex = 0;\n  }\n\n  // adjust with container lookup\n  insertableIndex = insertableIndex + closestContainerIndex;\n  const parentSelector = instanceSelector.slice(insertableIndex);\n  if (insertableIndex === 0) {\n    return from ?? { parentSelector, position: \"end\" };\n  }\n  const instance = instances.get(instanceSelector[insertableIndex]);\n  if (instance === undefined) {\n    return;\n  }\n  // skip collection item when inserting something and go straight into collection instance\n  if (instance?.component === collectionComponent && insertableIndex === 1) {\n    return {\n      parentSelector,\n      position: \"end\",\n    };\n  }\n  const lastChildInstanceId = instanceSelector[insertableIndex - 1];\n  const lastChildPosition = instance.children.findIndex(\n    (child) => child.type === \"id\" && child.value === lastChildInstanceId\n  );\n  return {\n    parentSelector,\n    position: lastChildPosition + 1,\n  };\n};\n\n/**\n * Build the ancestor path array for an instance.\n * Returns an array of labels for all ancestors from root to parent.\n */\nexport const buildInstancePath = (\n  instanceId: Instance[\"id\"],\n  pages: Pages,\n  instances: Instances\n): string[] => {\n  const awareness = findAwarenessByInstanceId(pages, instances, instanceId);\n  if (!awareness.instanceSelector) {\n    return [];\n  }\n\n  const instancePath = getInstancePath(\n    awareness.instanceSelector,\n    instances,\n    undefined,\n    undefined\n  );\n\n  if (!instancePath) {\n    return [];\n  }\n\n  return instancePath\n    .slice()\n    .reverse()\n    .slice(0, -1) // Remove the instance itself (last element after reverse), keep only ancestors\n    .map(({ instance }) => getInstanceLabel(instance));\n};\n\n/**\n * Detects token conflicts for a fragment insertion.\n *\n * @param fragment - The fragment to check for conflicts\n * @returns Array of token conflicts (empty if no conflicts)\n */\nexport const detectFragmentTokenConflicts = ({\n  fragment,\n}: {\n  fragment: WebstudioFragment;\n}) => {\n  const data = getWebstudioData();\n\n  const mergedBreakpointIds = buildMergedBreakpointIds(\n    fragment.breakpoints,\n    data.breakpoints\n  );\n\n  return detectTokenConflicts({\n    fragmentStyleSources: fragment.styleSources,\n    fragmentStyles: fragment.styles,\n    existingStyleSources: data.styleSources,\n    existingStyles: data.styles,\n    breakpoints: data.breakpoints,\n    mergedBreakpointIds,\n  });\n};\n\n/**\n * Detects token conflicts for a page insertion.\n * Combines fragments from ROOT_INSTANCE and page body for conflict detection.\n *\n * @param sourceData - The source webstudio data containing the page\n * @param pageId - The page ID to check for conflicts\n * @returns Array of token conflicts (empty if no conflicts)\n */\nexport const detectPageTokenConflicts = ({\n  sourceData,\n  pageId,\n}: {\n  sourceData: WebstudioData;\n  pageId: string;\n}) => {\n  const data = getWebstudioData();\n\n  const page = sourceData.pages.pages.find((p) => p.id === pageId);\n  if (page === undefined && sourceData.pages.homePage.id !== pageId) {\n    throw new Error(\"Page not found\");\n  }\n  const targetPage = page ?? sourceData.pages.homePage;\n\n  // Extract fragments for both ROOT_INSTANCE and page body\n  const rootFragment = extractWebstudioFragment(sourceData, ROOT_INSTANCE_ID);\n  const pageFragment = extractWebstudioFragment(\n    sourceData,\n    targetPage.rootInstanceId\n  );\n\n  // Combine style sources and styles from both fragments\n  const combinedStyleSources = [\n    ...rootFragment.styleSources,\n    ...pageFragment.styleSources,\n  ];\n  const combinedStyles = [...rootFragment.styles, ...pageFragment.styles];\n  const combinedBreakpoints = [\n    ...rootFragment.breakpoints,\n    ...pageFragment.breakpoints,\n  ];\n\n  const mergedBreakpointIds = buildMergedBreakpointIds(\n    combinedBreakpoints,\n    data.breakpoints\n  );\n\n  return detectTokenConflicts({\n    fragmentStyleSources: combinedStyleSources,\n    fragmentStyles: combinedStyles,\n    existingStyleSources: data.styleSources,\n    existingStyles: data.styles,\n    breakpoints: data.breakpoints,\n    mergedBreakpointIds,\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/logout.client.tsx",
    "content": "import { Text, Grid } from \"@webstudio-is/design-system\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useEffectEvent } from \"./hook-utils/effect-event\";\nimport { fetch } from \"~/shared/fetch.client\";\nimport { z } from \"zod\";\nimport { restLogoutPath } from \"./router-utils\";\n\nexport type LogoutProps = {\n  logoutUrls: string[];\n  onFinish: (failedProjects?: string[]) => void;\n};\n\nconst MAX_RETRIES = 3;\n\nconst Logout = (props: LogoutProps) => {\n  const [logoutState, setLogoutState] = useState({\n    retries: MAX_RETRIES,\n    logoutUrls: props.logoutUrls,\n  });\n\n  useEffect(() => {\n    if (logoutState.retries === 0) {\n      if (logoutState.retries === 0) {\n        // Show error message\n        props.onFinish(logoutState.logoutUrls);\n      }\n\n      return;\n    }\n\n    Promise.allSettled(\n      logoutState.logoutUrls.map(async (url) => {\n        await new Promise((resolve) =>\n          setTimeout(resolve, (MAX_RETRIES - logoutState.retries) * 1000)\n        );\n\n        const response = await fetch(url, {\n          method: \"POST\",\n          credentials: \"include\",\n          headers: { \"content-type\": \"application/json\" },\n        });\n\n        if (response.ok) {\n          return response;\n        }\n\n        console.error(`Logout failed for URL: ${url}`, response);\n        throw new Error(`Logout failed for URL: ${url}`);\n      })\n    ).then((results) => {\n      const failedUrls: string[] = [];\n\n      results.forEach((result, index) => {\n        if (result.status === \"rejected\") {\n          failedUrls.push(logoutState.logoutUrls[index]);\n        }\n      });\n\n      if (failedUrls.length === 0) {\n        props.onFinish();\n        return;\n      }\n\n      setLogoutState({\n        retries: logoutState.retries - 1,\n        logoutUrls: failedUrls,\n      });\n    });\n  }, [logoutState, props]);\n\n  return (\n    <Grid\n      gap={2}\n      css={{\n        position: \"fixed\",\n        inset: 0,\n        alignContent: \"center\",\n        justifyItems: \"center\",\n      }}\n    >\n      <Text variant={\"bigTitle\"}>Logging out ...</Text>\n    </Grid>\n  );\n};\n\nexport type LogoutPageProps = {\n  logoutUrls: string[];\n};\n\nconst LogoutResponse = z.object({\n  redirectTo: z.string(),\n});\n\nexport const LogoutPage = (props: LogoutPageProps) => {\n  const refForm = useRef<HTMLFormElement>(null);\n\n  const handleLogout = useEffectEvent(async (formData: FormData) => {\n    const response = await fetch(restLogoutPath(), {\n      method: \"POST\",\n      body: JSON.stringify(Object.fromEntries(formData.entries())),\n      headers: { \"content-type\": \"application/json\" },\n      redirect: \"manual\",\n    });\n\n    if (false === response.ok) {\n      throw {\n        message: \"Logout failed. Please try again later.\",\n        description: `Logout request failed with status ${response.status}: ${await response.text()}`,\n      };\n    }\n\n    const data = await response.json();\n    const parsedData = LogoutResponse.safeParse(data);\n\n    if (false === parsedData.success) {\n      throw {\n        message: \"Logout failed. Please try again later\",\n        description: \"Logout request failed: Unsupported endpoint response\",\n      };\n    }\n\n    if (formData.get(\"error\") !== null) {\n      const value = formData.get(\"error\")!.toString();\n\n      const failedProjects = (JSON.parse(value) as string[])\n        .map((project) => `- ${new URL(project).origin}`)\n        .join(\"\\n\");\n\n      throw {\n        message: \"Logout failed. Please try again later\",\n        description: `Something went wrong during the projects logout. Please try again later.\\nProjects failed to logout:\\n${failedProjects}`,\n      };\n    }\n\n    window.location.href = parsedData.data.redirectTo;\n    return;\n  });\n\n  const handleFinish = useEffectEvent((failedProjects?: string[]) => {\n    if (failedProjects !== undefined) {\n      const elt = document.createElement(\"button\");\n      elt.type = \"submit\";\n      elt.name = \"error\";\n      elt.value = JSON.stringify(failedProjects);\n      elt.style.display = \"none\";\n      refForm.current?.appendChild(elt);\n      refForm.current?.requestSubmit(elt);\n      return;\n    }\n\n    refForm.current?.requestSubmit();\n  });\n\n  return (\n    <form ref={refForm} action={handleLogout}>\n      <Logout logoutUrls={props.logoutUrls} onFinish={handleFinish} />\n    </form>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/marketplace/db.server.ts",
    "content": "import { MarketplaceProduct } from \"@webstudio-is/project-build\";\nimport type { MarketplaceOverviewItem } from \"./types\";\nimport {\n  loadApprovedProdBuildByProjectId,\n  parseConfig,\n} from \"@webstudio-is/project-build/index.server\";\nimport type { AppContext } from \"@webstudio-is/trpc-interface/index.server\";\nimport type { Project } from \"@webstudio-is/project\";\nimport { loadAssetsByProject } from \"@webstudio-is/asset-uploader/index.server\";\n\nexport const getBuildProdData = async (\n  { projectId }: { projectId: Project[\"id\"] },\n  context: AppContext\n) => {\n  const build = await loadApprovedProdBuildByProjectId(context, projectId);\n\n  const assets = await loadAssetsByProject(projectId, context, {\n    skipPermissionsCheck: true,\n  });\n\n  return {\n    ...build,\n    assets,\n  };\n};\n\nexport const getItems = async (\n  context: AppContext\n): Promise<Array<MarketplaceOverviewItem>> => {\n  const approvedMarketplaceProducts = await context.postgrest.client\n    .from(\"ApprovedMarketplaceProduct\")\n    .select();\n  if (approvedMarketplaceProducts.error) {\n    throw approvedMarketplaceProducts.error;\n  }\n\n  const items: MarketplaceOverviewItem[] = [];\n\n  for (const product of approvedMarketplaceProducts.data) {\n    if (product.marketplaceProduct === null || product.projectId === null) {\n      continue;\n    }\n    const parsedProduct = MarketplaceProduct.safeParse(\n      parseConfig(product.marketplaceProduct)\n    );\n\n    if (parsedProduct.success === false) {\n      console.error(parsedProduct.error.formErrors.fieldErrors);\n      continue;\n    }\n\n    items.push({\n      projectId: product.projectId,\n      authorizationToken: product.authorizationToken ?? undefined,\n      ...parsedProduct.data,\n    });\n  }\n  const assetIds = items\n    .map((item) => item.thumbnailAssetId)\n    .filter((value): value is string => value != null);\n\n  const assets = new Map<string, string>();\n  if (assetIds.length > 0) {\n    const data = await context.postgrest.client\n      .from(\"Asset\")\n      .select()\n      .in(\"id\", assetIds);\n    if (data.error) {\n      throw data.error;\n    }\n    for (const asset of data.data) {\n      assets.set(asset.id, asset.name);\n    }\n  }\n\n  return items.map((item) => {\n    return {\n      ...item,\n      thumbnailAssetName: assets.get(item.thumbnailAssetId),\n    };\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/marketplace/router.server.ts",
    "content": "import { z } from \"zod\";\nimport {\n  procedure,\n  router,\n  createCacheMiddleware,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { getItems, getBuildProdData } from \"./db.server\";\n\nconst cacheMiddleware = createCacheMiddleware(60 * 3); // 60 * 3 = 3 minutes cache\nconst cachedProcedure = procedure.use(cacheMiddleware);\n\nexport const marketplaceRouter = router({\n  getItems: cachedProcedure.query(async ({ ctx }) => {\n    return await getItems(ctx);\n  }),\n  getBuildData: cachedProcedure\n    .input(\n      z.object({\n        projectId: z.string(),\n      })\n    )\n    .query(async ({ input, ctx }) => {\n      return await getBuildProdData(input, ctx);\n    }),\n});\n"
  },
  {
    "path": "apps/builder/app/shared/marketplace/types.ts",
    "content": "import type { MarketplaceProduct } from \"@webstudio-is/project-build\";\nimport type { Asset } from \"@webstudio-is/sdk\";\n\nexport type MarketplaceOverviewItem = MarketplaceProduct & {\n  projectId: string;\n  authorizationToken?: string | undefined;\n  thumbnailAssetName?: Asset[\"name\"];\n};\n"
  },
  {
    "path": "apps/builder/app/shared/matcher.test.tsx",
    "content": "import { describe, expect, test, vi } from \"vitest\";\nimport { $, renderTemplate, renderData } from \"@webstudio-is/template\";\nimport { coreMetas } from \"@webstudio-is/sdk\";\nimport * as baseMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport { findClosestInstanceMatchingFragment } from \"./matcher\";\n\nconst metas = new Map(Object.entries({ ...coreMetas, ...baseMetas }));\n\ndescribe(\"find closest instance matching fragment\", () => {\n  test(\"finds closest list with list item fragment\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.List ws:id=\"list\">\n          <$.ListItem ws:id=\"listitem\"></$.ListItem>\n        </$.List>\n      </$.Body>\n    );\n    const fragment = renderTemplate(<$.ListItem ws:id=\"new\"></$.ListItem>);\n    expect(\n      findClosestInstanceMatchingFragment({\n        metas,\n        instances,\n        props,\n        instanceSelector: [\"list\", \"body\"],\n        fragment,\n      })\n    ).toEqual(0);\n    expect(\n      findClosestInstanceMatchingFragment({\n        metas,\n        instances,\n        props,\n        // looks up until list parent is reached\n        instanceSelector: [\"listitem\", \"list\", \"body\"],\n        fragment,\n      })\n    ).toEqual(1);\n    expect(\n      findClosestInstanceMatchingFragment({\n        metas,\n        instances,\n        props,\n        instanceSelector: [\"body\"],\n        fragment,\n      })\n    ).toEqual(-1);\n  });\n\n  test(\"finds button parent with button fragment\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Button ws:id=\"button\"></$.Button>\n      </$.Body>\n    );\n    const fragment = renderTemplate(<$.Button ws:id=\"new\"></$.Button>);\n    expect(\n      findClosestInstanceMatchingFragment({\n        metas,\n        instances,\n        props,\n        instanceSelector: [\"button\", \"body\"],\n        fragment,\n      })\n    ).toEqual(1);\n  });\n\n  test(\"finds button parent with button+span fragment\", () => {\n    const { instances, props } = renderData(\n      <$.Body ws:id=\"body\">\n        <$.Button ws:id=\"button\"></$.Button>\n      </$.Body>\n    );\n    const fragment = renderTemplate(\n      <>\n        <$.Button ws:id=\"new-button\"></$.Button>\n        <$.Text ws:id=\"new-text\"></$.Text>\n      </>\n    );\n    expect(\n      findClosestInstanceMatchingFragment({\n        metas,\n        instances,\n        props,\n        instanceSelector: [\"button\", \"body\"],\n        fragment,\n      })\n    ).toEqual(1);\n  });\n\n  test(\"report first error\", () => {\n    const onError = vi.fn();\n    const { instances, props } = renderData(<$.Body ws:id=\"body\"></$.Body>);\n    const fragment = renderTemplate(<$.ListItem ws:id=\"listitem\"></$.ListItem>);\n    findClosestInstanceMatchingFragment({\n      metas,\n      instances,\n      props,\n      instanceSelector: [\"body\"],\n      fragment,\n      onError,\n    });\n    expect(onError).toHaveBeenLastCalledWith(\n      \"Placing <li> element inside a <body> violates HTML spec.\"\n    );\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/matcher.ts",
    "content": "import type {\n  Props,\n  Instances,\n  WebstudioFragment,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport type { InstanceSelector } from \"./tree-utils\";\nimport { isTreeSatisfyingContentModel } from \"./content-model\";\n\nexport const findClosestInstanceMatchingFragment = ({\n  instances,\n  props,\n  metas,\n  instanceSelector,\n  fragment,\n  onError,\n}: {\n  instances: Instances;\n  props: Props;\n  metas: Map<string, WsComponentMeta>;\n  instanceSelector: InstanceSelector;\n  // require only subset of fragment\n  fragment: Pick<WebstudioFragment, \"children\" | \"instances\" | \"props\">;\n  onError?: (message: string) => void;\n}) => {\n  const mergedInstances = new Map(instances);\n  for (const instance of fragment.instances) {\n    mergedInstances.set(instance.id, instance);\n  }\n  const mergedProps = new Map(props);\n  for (const prop of fragment.props) {\n    mergedProps.set(prop.id, prop);\n  }\n  let firstError = \"\";\n  for (let index = 0; index < instanceSelector.length; index += 1) {\n    const instanceId = instanceSelector[index];\n    const instance = instances.get(instanceId);\n    // collection item can be undefined\n    if (instance === undefined) {\n      continue;\n    }\n    const meta = metas.get(instance.component);\n    if (meta === undefined) {\n      continue;\n    }\n    let matches = true;\n    for (const child of fragment.children) {\n      if (child.type === \"id\") {\n        const childInstanceSelector = [\n          child.value,\n          ...instanceSelector.slice(index),\n        ];\n        matches &&= isTreeSatisfyingContentModel({\n          instances: mergedInstances,\n          props: mergedProps,\n          metas,\n          instanceSelector: childInstanceSelector,\n          onError: (message) => {\n            if (firstError === \"\") {\n              firstError = message;\n            }\n          },\n        });\n      }\n    }\n    if (matches) {\n      return index;\n    }\n  }\n  onError?.(firstError);\n  return -1;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/math-utils.ts",
    "content": "export const clamp = (value: number, min: number, max: number) => {\n  return Math.min(Math.max(value, min), max);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/nano-hash.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { nanoHash } from \"./nano-hash\";\n\ntest(\"NanoHash Equal\", async () => {\n  expect(await nanoHash(\"hello\")).toEqual(await nanoHash(\"hello\"));\n});\n\ntest(\"NanoHash Not Equal\", async () => {\n  expect(await nanoHash(\"hello\")).not.toEqual(await nanoHash(\"world\"));\n});\n"
  },
  {
    "path": "apps/builder/app/shared/nano-hash.ts",
    "content": "import { customRandom } from \"nanoid\";\n\n// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js\nconst alphabet =\n  \"useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict\";\nconst NANOID_DEFAULT_SIZE = 21;\n\n// Generates hash strings using the same alphabet as nanoid\nexport const nanoHash = async (data: string) => {\n  const encoder = new TextEncoder();\n  const dataBuffer = encoder.encode(data);\n  const hashBuffer = await crypto.subtle.digest(\"SHA-256\", dataBuffer);\n  return customRandom(\n    alphabet,\n    NANOID_DEFAULT_SIZE,\n    () => new Uint8Array(hashBuffer)\n  )();\n};\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/breakpoints.ts",
    "content": "import { atom, computed } from \"nanostores\";\nimport type { Breakpoint } from \"@webstudio-is/sdk\";\nimport { $breakpoints } from \"../sync/data-stores\";\nimport { isBaseBreakpoint } from \"../breakpoints-utils\";\n\n// Re-export for backward compatibility\nexport { $breakpoints };\nexport { isBaseBreakpoint };\n\nexport const $selectedBreakpointId = atom<undefined | Breakpoint[\"id\"]>(\n  undefined\n);\n\nexport const $selectedBreakpoint = computed(\n  [$breakpoints, $selectedBreakpointId],\n  (breakpoints, selectedBreakpointId) => {\n    const selectedBreakpoint =\n      selectedBreakpointId === undefined\n        ? undefined\n        : breakpoints.get(selectedBreakpointId);\n\n    if (breakpoints.size === 0) {\n      return;\n    }\n    const breakpointsArray = Array.from(breakpoints.values());\n    return (\n      selectedBreakpoint ??\n      breakpointsArray.find(isBaseBreakpoint) ??\n      breakpointsArray[0] ??\n      undefined\n    );\n  }\n);\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/canvas.ts",
    "content": "import { atom, computed } from \"nanostores\";\nimport type { Instance, Instances } from \"@webstudio-is/sdk\";\nimport { blockComponent, blockTemplateComponent } from \"@webstudio-is/sdk\";\nimport type { FontWeight } from \"@webstudio-is/fonts\";\nimport { $instances } from \"./instances\";\nimport type { InstanceSelector } from \"../tree-utils\";\n\nexport type TextToolbarState = {\n  selectionRect: undefined | DOMRect;\n  isBold: boolean;\n  isItalic: boolean;\n  isSuperscript: boolean;\n  isSubscript: boolean;\n  isLink: boolean;\n  isSpan: boolean;\n};\n\nexport const $textToolbar = atom<undefined | TextToolbarState>(undefined);\n\ntype InstanceOutline = {\n  instanceId: Instance[\"id\"];\n  rect: DOMRect;\n};\n\nexport type BlockChildOutline = {\n  selector: InstanceSelector;\n  rect: DOMRect;\n};\n\nconst getInstanceOutlineAndInstance = (\n  instances: Instances,\n  instanceOutline: undefined | InstanceOutline\n) => {\n  if (instanceOutline === undefined) {\n    return;\n  }\n  const { instanceId, rect } = instanceOutline;\n  const instance = instances.get(instanceId);\n  if (instance === undefined) {\n    return;\n  }\n  return {\n    instance,\n    rect,\n  };\n};\n\nexport const $selectedInstanceOutline = atom<undefined | InstanceOutline>(\n  undefined\n);\n\nexport const $selectedInstanceOutlineAndInstance = computed(\n  [$instances, $selectedInstanceOutline],\n  getInstanceOutlineAndInstance\n);\n\nexport const $hoveredInstanceOutline = atom<undefined | InstanceOutline>(\n  undefined\n);\n\nexport const $hoveredInstanceOutlineAndInstance = computed(\n  [$instances, $hoveredInstanceOutline],\n  getInstanceOutlineAndInstance\n);\n\nexport const $collaborativeInstanceSelector = atom<\n  undefined | InstanceSelector\n>(undefined);\n\nexport const $collaborativeInstanceRect = atom<undefined | DOMRect>(undefined);\n\nexport const $blockChildOutline = atom<undefined | BlockChildOutline>(\n  undefined\n);\n\nexport type InstanceContextMenu = {\n  position: { x: number; y: number };\n  instanceSelector: InstanceSelector;\n};\n\nexport const $instanceContextMenu = atom<undefined | InstanceContextMenu>(\n  undefined\n);\n\nexport const findBlockChildSelector = (instanceSelector: InstanceSelector) => {\n  const instances = $instances.get();\n  let blockChildSelector: InstanceSelector | undefined = undefined;\n\n  for (let i = 1; i < instanceSelector.length; ++i) {\n    const instance = instances.get(instanceSelector[i]);\n    if (instance?.component === blockComponent) {\n      blockChildSelector = instanceSelector.slice(i - 1);\n\n      return blockChildSelector;\n    }\n  }\n\n  if (instances.get(instanceSelector[0])?.component === blockComponent) {\n    return instanceSelector;\n  }\n};\n\nexport const findBlockSelector = (\n  anchor: InstanceSelector,\n  instances: Instances\n) => {\n  if (anchor === undefined) {\n    return;\n  }\n\n  if (anchor.length === 0) {\n    return;\n  }\n\n  let blockInstanceSelector: InstanceSelector | undefined = undefined;\n\n  for (let i = 0; i < anchor.length; ++i) {\n    const instanceId = anchor[i];\n\n    const instance = instances.get(instanceId);\n    if (instance === undefined) {\n      return;\n    }\n\n    if (instance.component === blockComponent) {\n      blockInstanceSelector = anchor.slice(i);\n      break;\n    }\n  }\n\n  if (blockInstanceSelector === undefined) {\n    return;\n  }\n\n  return blockInstanceSelector;\n};\n\nexport const findTemplates = (\n  anchor: InstanceSelector,\n  instances: Instances\n) => {\n  const blockInstanceSelector = findBlockSelector(anchor, instances);\n  if (blockInstanceSelector === undefined) {\n    return;\n  }\n\n  const blockInstance = instances.get(blockInstanceSelector[0]);\n\n  if (blockInstance === undefined) {\n    return;\n  }\n\n  const templateInstanceId = blockInstance.children.find(\n    (child) =>\n      child.type === \"id\" &&\n      instances.get(child.value)?.component === blockTemplateComponent\n  )?.value;\n\n  if (templateInstanceId === undefined) {\n    return;\n  }\n\n  const templateInstance = instances.get(templateInstanceId);\n\n  if (templateInstance === undefined) {\n    return;\n  }\n\n  const result: [instance: Instance, instanceSelector: InstanceSelector][] =\n    templateInstance.children\n      .filter((child) => child.type === \"id\")\n      .map((child) => child.value)\n      .map((childId) => instances.get(childId))\n      .filter((child) => child !== undefined)\n      .map((child) => [\n        child,\n        [child.id, templateInstanceId, ...blockInstanceSelector],\n      ]);\n\n  return result;\n};\n\nexport const $canvasIframeState = atom<\"idle\" | \"ready\">(\"idle\");\n\nexport const $detectedFontsWeights = atom<Map<string, Array<FontWeight>>>(\n  new Map()\n);\n\nexport type GridCellData = {\n  instanceId: Instance[\"id\"];\n  columnCount: number;\n  rowCount: number;\n  // Raw bounding client rect (post-transform). The builder recovers the\n  // untransformed position by probing a hidden element with the same CSS.\n  bcr: { top: number; left: number };\n  // Untransformed border-box dimensions from offsetWidth/offsetHeight.\n  untransformedWidth: number;\n  untransformedHeight: number;\n  // CSS text built from getComputedStyle on the canvas element.\n  // Applied verbatim to the builder overlay mirror div via style.cssText.\n  // Adding a new synced property = one line in the whitelist array\n  // in grid-guide-utils.ts.\n  resolvedCssText: string;\n  // Index of the first implicit column track (0-based), or columnCount\n  // when all tracks are explicit.\n  implicitColumnStart: number;\n  // Index of the first implicit row track (0-based), or rowCount\n  // when all tracks are explicit.\n  implicitRowStart: number;\n};\n\nexport const $gridCellData = atom<GridCellData | undefined>(undefined);\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/components.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { shallowEqual } from \"shallow-equal\";\nimport type { ExoticComponent } from \"react\";\nimport { atom, computed } from \"nanostores\";\nimport type {\n  AnyComponent,\n  Hook,\n  HookContext,\n  InstanceData,\n} from \"@webstudio-is/react-sdk\";\nimport {\n  getIndexesWithinAncestors,\n  type Instance,\n  type WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport type { InstanceSelector } from \"../tree-utils\";\nimport { $memoryProps, $props } from \"./misc\";\nimport { $instances } from \"./instances\";\nimport { $awareness, $selectedPage, getInstanceKey } from \"../awareness\";\nimport {\n  renderTemplate,\n  type GeneratedTemplateMeta,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\n\nconst createHookContext = (): HookContext => {\n  const metas = $registeredComponentMetas.get();\n  const instances = $instances.get();\n  const page = $selectedPage.get();\n  const indexesWithinAncestors = getIndexesWithinAncestors(\n    metas,\n    instances,\n    page ? [page.rootInstanceId] : []\n  );\n\n  return {\n    indexesWithinAncestors,\n\n    getPropValue: ({ id }, propName) => {\n      const props = $props.get();\n      for (const prop of props.values()) {\n        if (prop.instanceId === id && prop.name === propName) {\n          if (\n            prop.type === \"string\" ||\n            prop.type === \"number\" ||\n            prop.type === \"boolean\" ||\n            prop.type === \"string[]\"\n          ) {\n            return prop.value;\n          }\n        }\n      }\n    },\n\n    setMemoryProp: (instanceData: InstanceData, propName, value) => {\n      const { instanceKey } = instanceData;\n      const props = new Map($memoryProps.get());\n\n      const newProps = props.get(instanceKey) ?? new Map();\n\n      const propBase = {\n        id: nanoid(),\n        instanceId: instanceData.id,\n        name: propName,\n      };\n\n      if (value !== undefined) {\n        switch (typeof value) {\n          case \"string\":\n            newProps.set(propName, {\n              ...propBase,\n              type: \"string\",\n              value,\n            });\n            break;\n          case \"number\":\n            newProps.set(propName, {\n              ...propBase,\n              type: \"number\",\n              value,\n            });\n            break;\n          case \"boolean\":\n            newProps.set(propName, {\n              ...propBase,\n              type: \"boolean\",\n              value,\n            });\n            break;\n          default:\n            throw new Error(`Unsupported type ${typeof value}`);\n        }\n      } else {\n        newProps.delete(propName);\n      }\n\n      props.set(instanceKey, newProps);\n\n      $memoryProps.set(props);\n    },\n  };\n};\n\nconst $instanceSelector = computed(\n  $awareness,\n  (awareness) => awareness?.instanceSelector\n);\n\n// subscribe builder events and invoke all component hooks\nexport const subscribeComponentHooks = () => {\n  let lastInstanceSelector: undefined | InstanceSelector = undefined;\n  const unsubscribeSelectedInstanceSelector = $instanceSelector.subscribe(\n    (instanceSelector) => {\n      // prevent executing hooks when selected instance is not changed\n      if (shallowEqual(lastInstanceSelector, instanceSelector)) {\n        return;\n      }\n      const hooks = $registeredComponentHooks.get();\n      const instances = $instances.get();\n      if (lastInstanceSelector) {\n        for (const hook of hooks) {\n          hook.onNavigatorUnselect?.(createHookContext(), {\n            instancePath: lastInstanceSelector.flatMap((id, index, array) => {\n              const instance = instances.get(id);\n              if (instance === undefined) {\n                return [];\n              }\n              return {\n                id: instance.id,\n                instanceKey: getInstanceKey(array.slice(index)),\n                component: instance.component,\n                tag: instance.tag,\n              };\n            }),\n          });\n        }\n      }\n\n      if (instanceSelector) {\n        for (const hook of hooks) {\n          hook.onNavigatorSelect?.(createHookContext(), {\n            instancePath: instanceSelector.flatMap((id, index, array) => {\n              const instance = instances.get(id);\n              if (instance === undefined) {\n                return [];\n              }\n              return {\n                id: instance.id,\n                instanceKey: getInstanceKey(array.slice(index)),\n                component: instance.component,\n                tag: instance.tag,\n              };\n            }),\n          });\n        }\n      }\n\n      // store converts values to readonly\n      lastInstanceSelector = instanceSelector as InstanceSelector;\n    }\n  );\n\n  return () => {\n    unsubscribeSelectedInstanceSelector();\n  };\n};\n\nexport const $registeredComponents = atom(new Map<string, AnyComponent>());\n\nexport const $registeredComponentHooks = atom<Hook[]>([]);\n\nexport const $registeredComponentMetas = atom(\n  new Map<string, WsComponentMeta>()\n);\n\nexport const $registeredTemplates = atom(\n  new Map<string, GeneratedTemplateMeta>()\n);\n\nexport const registerComponentLibrary = ({\n  namespace,\n  components,\n  metas,\n  hooks,\n  templates,\n}: {\n  namespace?: string;\n  // simplify adding component libraries\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  components: Record<Instance[\"component\"], ExoticComponent<any>>;\n  metas: Record<Instance[\"component\"], WsComponentMeta>;\n  hooks?: Hook[];\n  templates: Record<Instance[\"component\"], TemplateMeta>;\n}) => {\n  const prefix = namespace === undefined ? \"\" : `${namespace}:`;\n\n  const prevComponents = $registeredComponents.get();\n  const nextComponents = new Map(prevComponents);\n  for (const [componentName, component] of Object.entries(components)) {\n    nextComponents.set(`${prefix}${componentName}`, component);\n  }\n  $registeredComponents.set(nextComponents);\n\n  const prevMetas = $registeredComponentMetas.get();\n  const nextMetas = new Map(prevMetas);\n  for (const [componentName, meta] of Object.entries(metas)) {\n    nextMetas.set(`${prefix}${componentName}`, meta);\n  }\n  $registeredComponentMetas.set(nextMetas);\n\n  const prevTemplates = $registeredTemplates.get();\n  const nextTemplates = new Map(prevTemplates);\n  for (const [componentName, meta] of Object.entries(templates)) {\n    const { template, ...generatedMeta } = meta;\n    nextTemplates.set(`${prefix}${componentName}`, {\n      ...generatedMeta,\n      template: renderTemplate(template),\n    });\n  }\n  $registeredTemplates.set(nextTemplates);\n\n  if (hooks) {\n    const prevHooks = $registeredComponentHooks.get();\n    const nextHooks = [...prevHooks, ...hooks];\n    $registeredComponentHooks.set(nextHooks);\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/index.ts",
    "content": "// Re-export data stores that were moved to sync/data-stores\nexport {\n  $project,\n  $publisherHost,\n  $dataSources,\n  $resources,\n  $props,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n  $assets,\n  $marketplaceProduct,\n  $pages,\n  $instances,\n  $breakpoints,\n} from \"../sync/data-stores\";\n\nexport * from \"./misc\";\nexport * from \"./breakpoints\";\nexport * from \"./instances\";\nexport * from \"./props\";\nexport * from \"./canvas\";\nexport * from \"./pages\";\nexport * from \"./variables\";\nexport * from \"./components\";\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/instances.ts",
    "content": "import { atom } from \"nanostores\";\nimport type { InstanceSelector } from \"../tree-utils\";\nimport { $instances } from \"../sync/data-stores\";\n\n// Re-export for backward compatibility\nexport { $instances };\n\nexport const $isResizingCanvas = atom(false);\n\nexport const $selectedInstanceSelector = atom<undefined | InstanceSelector>(\n  undefined\n);\n\nexport const $editingItemSelector = atom<undefined | InstanceSelector>(\n  undefined\n);\n\nexport const $textEditingInstanceSelector = atom<\n  | undefined\n  | {\n      selector: InstanceSelector;\n      reason: \"right\" | \"left\" | \"enter\";\n    }\n  | {\n      selector: InstanceSelector;\n      reason: \"new\";\n    }\n  | {\n      selector: InstanceSelector;\n      reason: \"click\";\n      mouseX: number;\n      mouseY: number;\n    }\n  | {\n      selector: InstanceSelector;\n      reason: \"up\" | \"down\";\n      cursorX: number;\n    }\n>();\n\nexport const $textEditorContextMenu = atom<\n  | {\n      cursorRect: DOMRect;\n    }\n  | undefined\n>(undefined);\n\ntype ContextMenuCommand =\n  | {\n      type: \"filter\";\n      value: string;\n    }\n  | { type: \"selectNext\" }\n  | { type: \"selectPrevious\" }\n  | { type: \"enter\" };\n\nexport const $textEditorContextMenuCommand = atom<\n  undefined | ContextMenuCommand\n>(undefined);\n\nexport const execTextEditorContextMenuCommand = (\n  command: ContextMenuCommand\n) => {\n  $textEditorContextMenuCommand.set(command);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/misc.ts",
    "content": "import type { Simplify } from \"type-fest\";\nimport { atom, computed, onSet } from \"nanostores\";\nimport { nanoid } from \"nanoid\";\nimport type { AuthPermit } from \"@webstudio-is/trpc-interface/index.server\";\nimport { toast, type Placement } from \"@webstudio-is/design-system\";\nimport type {\n  Instance,\n  Prop,\n  Props,\n  StyleDecl,\n  StyleSource,\n} from \"@webstudio-is/sdk\";\nimport type { CssProperty, UnitValue } from \"@webstudio-is/css-engine\";\nimport type { TokenPermissions } from \"@webstudio-is/authorization-token\";\nimport type { AssetType } from \"@webstudio-is/asset-uploader\";\nimport type { DragStartPayload } from \"~/canvas/shared/use-drag-drop\";\nimport { type InstanceSelector } from \"../tree-utils\";\nimport type { ChildrenOrientation } from \"@webstudio-is/design-system\";\nimport { $awareness, $selectedInstance } from \"../awareness\";\nimport type { UserPlanFeatures } from \"../db/user-plan-features.server\";\nimport {\n  $project,\n  $publisherHost,\n  $dataSources,\n  $resources,\n  $props,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n  $assets,\n  $marketplaceProduct,\n} from \"../sync/data-stores\";\n\n// Re-export data stores for backward compatibility\nexport {\n  $project,\n  $publisherHost,\n  $dataSources,\n  $resources,\n  $props,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n  $assets,\n  $marketplaceProduct,\n};\n\nexport const $publishedOrigin = computed(\n  [$project, $publisherHost],\n  (project, publisherHost) => `https://${project?.domain}.${publisherHost}`\n);\n\nexport const $memoryProps = atom<Map<string, Props>>(new Map());\n\nexport const $propsIndex = computed($props, (props) => {\n  const propsByInstanceId = new Map<Instance[\"id\"], Prop[]>();\n  for (const prop of props.values()) {\n    const { instanceId } = prop;\n    let instanceProps = propsByInstanceId.get(instanceId);\n    if (instanceProps === undefined) {\n      instanceProps = [];\n      propsByInstanceId.set(instanceId, instanceProps);\n    }\n    instanceProps.push(prop);\n  }\n  return {\n    propsByInstanceId,\n  };\n});\n\nexport type StyleSourceSelector = {\n  styleSourceId: StyleSource[\"id\"];\n  state?: string;\n};\n\nexport const $selectedStyleSources = atom(\n  new Map<Instance[\"id\"], StyleSource[\"id\"]>()\n);\nexport const $selectedStyleState = atom<StyleDecl[\"state\"]>();\n// reset style state whenever selected instance change\nonSet($awareness, () => {\n  $selectedStyleState.set(undefined);\n});\n\n/**\n * Indexed styles data is recomputed on every styles update\n * Compumer should use shallow-equal to check all items in the list\n * are the same to avoid unnecessary rerenders\n *\n * Potential optimization can be maintaining the index as separate state\n * though will require to move away from running immer patches on array\n * of styles\n */\nexport const $stylesIndex = computed(\n  [$styles, $styleSourceSelections],\n  (styles, styleSourceSelections) => {\n    const stylesByStyleSourceId = new Map<StyleSource[\"id\"], StyleDecl[]>();\n    for (const styleDecl of styles.values()) {\n      const { styleSourceId } = styleDecl;\n      let styleSourceStyles = stylesByStyleSourceId.get(styleSourceId);\n      if (styleSourceStyles === undefined) {\n        styleSourceStyles = [];\n        stylesByStyleSourceId.set(styleSourceId, styleSourceStyles);\n      }\n      styleSourceStyles.push(styleDecl);\n    }\n\n    const stylesByInstanceId = new Map<Instance[\"id\"], StyleDecl[]>();\n    for (const { instanceId, values } of styleSourceSelections.values()) {\n      const instanceStyles: StyleDecl[] = [];\n      for (const styleSourceId of values) {\n        const styleSourceStyles = stylesByStyleSourceId.get(styleSourceId);\n        if (styleSourceStyles) {\n          instanceStyles.push(...styleSourceStyles);\n        }\n      }\n      stylesByInstanceId.set(instanceId, instanceStyles);\n    }\n\n    return {\n      stylesByStyleSourceId,\n      stylesByInstanceId,\n    };\n  }\n);\n\nexport type UploadingFileData = Simplify<\n  {\n    // common props\n    assetId: string;\n    type: AssetType;\n    objectURL: string;\n  } & (\n    | {\n        source: \"file\";\n        file: File;\n      }\n    | {\n        source: \"url\";\n        url: string;\n      }\n  )\n>;\n\nexport const $uploadingFilesDataStore = atom<UploadingFileData[]>([]);\n\nexport const convertibleUnits = [\"px\", \"ch\", \"vw\", \"vh\", \"em\", \"rem\"] as const;\n\nexport type ConvertibleUnit = (typeof convertibleUnits)[number];\n\nexport type UnitSizes = Record<ConvertibleUnit, number>;\n\nexport type PropertySizes = Partial<Record<CssProperty, UnitValue>>;\n\n// Init with some defaults to avoid undefined\nexport const $selectedInstanceSizes = atom<{\n  unitSizes: UnitSizes;\n  propertySizes: PropertySizes;\n}>({\n  unitSizes: {\n    ch: 8,\n    vw: 3.2,\n    vh: 4.8,\n    em: 16,\n    rem: 16,\n    px: 1,\n  },\n  propertySizes: {},\n});\n\n/**\n * pending means: previous selected instance unmounted,\n * and we don't know yet whether a new one will mount\n **/\nexport const $selectedInstanceRenderState = atom<\n  \"mounted\" | \"notMounted\" | \"pending\"\n>(\"notMounted\");\n\nexport const $selectedInstanceStatesByStyleSourceId = computed(\n  [$styles, $styleSourceSelections, $selectedInstance],\n  (styles, styleSourceSelections, selectedInstance) => {\n    const statesByStyleSourceId = new Map<StyleSource[\"id\"], string[]>();\n    if (selectedInstance === undefined) {\n      return statesByStyleSourceId;\n    }\n    const styleSourceIds = new Set(\n      styleSourceSelections.get(selectedInstance.id)?.values\n    );\n    for (const styleDecl of styles.values()) {\n      if (\n        styleDecl.state === undefined ||\n        styleSourceIds.has(styleDecl.styleSourceId) === false\n      ) {\n        continue;\n      }\n      let states = statesByStyleSourceId.get(styleDecl.styleSourceId);\n      if (states === undefined) {\n        states = [];\n        statesByStyleSourceId.set(styleDecl.styleSourceId, states);\n      }\n      if (states.includes(styleDecl.state) === false) {\n        states.push(styleDecl.state);\n      }\n    }\n    return statesByStyleSourceId;\n  }\n);\n\nexport const $selectedInstanceStyleSources = computed(\n  [$styleSourceSelections, $styleSources, $selectedInstance],\n  (styleSourceSelections, styleSources, selectedInstance) => {\n    const selectedInstanceStyleSources: StyleSource[] = [];\n    if (selectedInstance === undefined) {\n      return selectedInstanceStyleSources;\n    }\n    const styleSourceIds =\n      styleSourceSelections.get(selectedInstance.id)?.values ?? [];\n    let hasLocal = false;\n    for (const styleSourceId of styleSourceIds) {\n      const styleSource = styleSources.get(styleSourceId);\n      if (styleSource !== undefined) {\n        selectedInstanceStyleSources.push(styleSource);\n        if (styleSource.type === \"local\") {\n          hasLocal = true;\n        }\n      }\n    }\n    // generate style source when selection has not local style sources\n    // it is synchronized whenever styles are updated\n    if (hasLocal === false) {\n      // always put local style source last\n      selectedInstanceStyleSources.push({\n        type: \"local\",\n        id: nanoid(),\n      });\n    }\n    return selectedInstanceStyleSources;\n  }\n);\n\nexport const $selectedOrLastStyleSourceSelector = computed(\n  [\n    $selectedInstanceStyleSources,\n    $selectedStyleSources,\n    $selectedInstance,\n    $selectedStyleState,\n  ],\n  (\n    styleSources,\n    selectedStyleSources,\n    selectedInstance,\n    selectedStyleState\n  ) => {\n    if (selectedInstance === undefined) {\n      return;\n    }\n    const styleSourceId = selectedStyleSources.get(selectedInstance.id);\n    // always fallback to local (the last one) style source\n    const lastStyleSource = styleSources.at(-1);\n    const matchedStyleSource = styleSources.find(\n      (styleSource) => styleSource.id === styleSourceId\n    );\n    const styleSource = matchedStyleSource ?? lastStyleSource;\n    if (styleSource) {\n      return { styleSourceId: styleSource.id, state: selectedStyleState };\n    }\n  }\n);\n\n/**\n * Provide selected style source with fallback\n * to the last style source of selected instance\n */\nexport const $selectedStyleSource = computed(\n  [$selectedInstanceStyleSources, $selectedStyleSources, $selectedInstance],\n  (styleSources, selectedStyleSources, selectedInstance) => {\n    if (selectedInstance === undefined) {\n      return;\n    }\n    const selectedStyleSourceId = selectedStyleSources.get(selectedInstance.id);\n    return (\n      styleSources.find((item) => item.id === selectedStyleSourceId) ??\n      styleSources.at(-1)\n    );\n  }\n);\n\n/**\n * Store the list of active states inferred from dom element\n * to display style values as remote\n */\nexport const $selectedInstanceStates = atom(new Set<string>());\n\nexport const $hoveredInstanceSelector = atom<undefined | InstanceSelector>(\n  undefined\n);\n\n// keep in sync with user-plan-features.server\nexport const $userPlanFeatures = atom<UserPlanFeatures>({\n  allowAdditionalPermissions: false,\n  allowDynamicData: false,\n  allowContentMode: false,\n  allowStagingPublish: false,\n  maxContactEmails: 0,\n  maxDomainsAllowedPerUser: 0,\n  maxPublishesAllowedPerUser: 1,\n  purchases: [],\n});\n\nconst builderModes = [\"design\", \"preview\", \"content\"] as const;\nexport type BuilderMode = (typeof builderModes)[number];\nexport const isBuilderMode = (mode: string | null): mode is BuilderMode =>\n  builderModes.includes(mode as BuilderMode);\nexport const $builderMode = atom<BuilderMode>(\"design\");\n\nexport const $isPreviewMode = computed(\n  $builderMode,\n  (mode) => mode === \"preview\"\n);\nexport const $isContentMode = computed(\n  $builderMode,\n  (mode) => mode === \"content\"\n);\nexport const $isDesignMode = computed(\n  $builderMode,\n  (mode) => mode === \"design\"\n);\n\nexport const $authPermit = atom<AuthPermit>(\"view\");\nexport const $authTokenPermissions = atom<TokenPermissions>({\n  canClone: true,\n  canCopy: true,\n  canPublish: false,\n});\n\nexport const $authToken = atom<string | undefined>(undefined);\n\nexport const $stagingUsername = atom<string | undefined>();\nexport const $stagingPassword = atom<string | undefined>();\n\nexport const $isContentModeAllowed = computed(\n  [$authToken, $userPlanFeatures],\n  (token, userPlanFeatures) => {\n    // In own projects, everyone can edit content\n    if (token === undefined) {\n      return true;\n    }\n\n    // In shared projects, only Pro users can share editable links, so check the plan features of the user who shared the link\n    return userPlanFeatures.allowContentMode === true;\n  }\n);\n\nexport const $isDesignModeAllowed = computed([$authPermit], (authPermit) => {\n  return authPermit !== \"edit\";\n});\n\nlet lastEditableBuilderMode: Exclude<BuilderMode, \"preview\"> | undefined =\n  undefined;\n\nconst getNextEditableMode = (): \"design\" | \"content\" => {\n  if (lastEditableBuilderMode === undefined) {\n    if ($isDesignModeAllowed.get()) {\n      return \"design\";\n    }\n\n    return \"content\";\n  }\n  return lastEditableBuilderMode;\n};\n\n/**\n * - preview, preview -> 'last known editable mode i.e. design or content' ?? 'default editable mode'\n * - preview, design -> design\n * - preview, content -> content\n *\n * - design, design -> preview\n * - design, preview -> preview\n * - design, content -> content\n *\n * - content, content -> preview\n * - content, preview -> preview\n * - content, design -> design\n */\nexport const toggleBuilderMode = (mode: BuilderMode) => {\n  const currentMode = $builderMode.get();\n\n  if (currentMode === mode) {\n    if (mode === \"preview\") {\n      setBuilderMode(getNextEditableMode());\n      return;\n    }\n\n    setBuilderMode(\"preview\");\n    return;\n  }\n\n  setBuilderMode(mode);\n};\n\nexport const setBuilderMode = (mode: BuilderMode | null) => {\n  if (mode === \"content\" && !$isContentModeAllowed.get()) {\n    // This is content link from a non pro user, we don't allow content mode for such links\n    toast.info(\n      \"Content mode is not available for this link. The link’s author must have a Pro plan.\"\n    );\n\n    $builderMode.set(\"preview\");\n    return;\n  }\n\n  if (mode === \"design\" && !$isDesignModeAllowed.get()) {\n    toast.info(\"Design mode is not available for content edit links.\");\n\n    $builderMode.set(\"content\");\n    lastEditableBuilderMode = \"content\";\n    return;\n  }\n\n  const defaultMode = $isDesignModeAllowed.get()\n    ? \"design\"\n    : $isContentModeAllowed.get()\n      ? \"content\"\n      : \"preview\";\n\n  const nextMode = mode ?? defaultMode;\n\n  $builderMode.set(nextMode);\n  if (nextMode !== \"preview\") {\n    lastEditableBuilderMode = nextMode;\n  }\n};\n\nexport const $toastErrors = atom<string[]>([]);\n\nexport const $modifierKeys = atom<{ altKey: boolean }>({ altKey: false });\n\nexport const subscribeModifierKeys = (options: AddEventListenerOptions) => {\n  const handleKeyEvent = (event: MouseEvent | KeyboardEvent) => {\n    const altKey = event.altKey;\n    if ($modifierKeys.get().altKey !== altKey) {\n      $modifierKeys.set({ altKey });\n    }\n  };\n\n  const eventOptions: AddEventListenerOptions = {\n    ...options,\n    capture: true,\n\n    passive: true,\n  };\n\n  document.addEventListener(\"keydown\", handleKeyEvent, eventOptions);\n  document.addEventListener(\"keyup\", handleKeyEvent, eventOptions);\n  document.addEventListener(\"mousemove\", handleKeyEvent, eventOptions);\n};\n\nexport type ItemDropTarget = {\n  itemSelector: InstanceSelector;\n  indexWithinChildren: number;\n  placement: {\n    closestChildIndex: number;\n    indexAdjustment: number;\n    childrenOrientation: ChildrenOrientation;\n  };\n};\n\nexport type DragAndDropState = {\n  isDragging: boolean;\n  dropTarget?: ItemDropTarget;\n  dragPayload?: DragStartPayload;\n  placementIndicator?: Placement;\n};\n\nexport const $dragAndDropState = atom<DragAndDropState>({\n  isDragging: false,\n});\n\nexport const $canvasToolsVisible = atom<boolean>(true);\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/pages.ts",
    "content": "import { atom } from \"nanostores\";\nimport type { Page } from \"@webstudio-is/sdk\";\nimport { $pages } from \"../sync/data-stores\";\n\n// Re-export for backward compatibility\nexport { $pages };\n\nexport const $selectedPageHash = atom<{ hash: string }>({ hash: \"\" });\n\nexport const $editingPageId = atom<undefined | Page[\"id\"]>();\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/project-settings.ts",
    "content": "import { atom } from \"nanostores\";\n\nexport type SectionName =\n  | \"general\"\n  | \"redirects\"\n  | \"publish\"\n  | \"marketplace\"\n  | \"backups\";\n\nexport const $openProjectSettings = atom<SectionName | undefined>();\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/props.test.tsx",
    "content": "import { beforeEach, expect, test } from \"vitest\";\nimport { cleanStores } from \"nanostores\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport { setEnv } from \"@webstudio-is/feature-flags\";\nimport {\n  DataSource,\n  type Instance,\n  ROOT_INSTANCE_ID,\n  Resource,\n  SYSTEM_VARIABLE_ID,\n  collectionComponent,\n} from \"@webstudio-is/sdk\";\nimport { textContentAttribute } from \"@webstudio-is/react-sdk\";\nimport { $instances } from \"./instances\";\nimport {\n  $propValuesByInstanceSelector,\n  $variableValuesByInstanceSelector,\n} from \"./props\";\nimport { $pages } from \"./pages\";\nimport { $assets, $dataSources, $props, $resources } from \"./misc\";\nimport { $dataSourceVariables } from \"./variables\";\nimport { $awareness, getInstanceKey } from \"../awareness\";\nimport {\n  $,\n  expression,\n  Parameter,\n  renderData,\n  ResourceValue,\n  Variable,\n  ws,\n} from \"@webstudio-is/template\";\nimport { $systemDataByPage, updateCurrentSystem } from \"../system\";\nimport { registerContainers } from \"../sync/sync-stores\";\nimport { $resourcesCache, getResourceKey } from \"../resources\";\n\nconst initialSystem = {\n  origin: \"https://undefined.wstd.work\",\n  params: {},\n  pathname: \"/\",\n  search: {},\n};\n\nregisterContainers();\nsetEnv(\"*\");\n\nconst getIdValuePair = <T extends { id: string }>(item: T) =>\n  [item.id, item] as const;\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map(getIdValuePair));\n\nconst setBoxInstance = (id: Instance[\"id\"]) => {\n  $instances.set(\n    toMap([{ id, type: \"instance\", component: \"Box\", children: [] }])\n  );\n};\n\nconst selectPageRoot = (\n  rootInstanceId: Instance[\"id\"],\n  systemDataSourceId?: DataSource[\"id\"]\n) => {\n  const defaultPages = createDefaultPages({\n    homePageId: \"pageId\",\n    rootInstanceId,\n    systemDataSourceId,\n  });\n  $pages.set(defaultPages);\n  $awareness.set({ pageId: defaultPages.homePage.id });\n};\n\nbeforeEach(() => {\n  $instances.set(new Map());\n  $props.set(new Map());\n  $resources.set(new Map());\n  $dataSources.set(new Map());\n  $dataSourceVariables.set(new Map());\n  $resourcesCache.set(new Map());\n});\n\ntest(\"collect prop values\", () => {\n  setBoxInstance(\"box\");\n  selectPageRoot(\"box\");\n  $dataSources.set(new Map());\n  $props.set(\n    toMap([\n      {\n        id: \"prop1\",\n        name: \"first\",\n        instanceId: \"box\",\n        type: \"number\",\n        value: 0,\n      },\n      {\n        id: \"prop2\",\n        name: \"second\",\n        instanceId: \"box\",\n        type: \"json\",\n        value: { name: \"John\" },\n      },\n    ])\n  );\n  expect(\n    $propValuesByInstanceSelector.get().get(getInstanceKey([\"box\"]))\n  ).toEqual(\n    new Map<string, unknown>([\n      [\"first\", 0],\n      [\"second\", { name: \"John\" }],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"compute expression prop values\", () => {\n  setBoxInstance(\"box\");\n  selectPageRoot(\"box\");\n  $dataSources.set(\n    toMap([\n      {\n        id: \"var1\",\n        scopeInstanceId: \"box\",\n        type: \"variable\",\n        name: \"\",\n        value: { type: \"number\", value: 1 },\n      },\n      {\n        id: \"var2\",\n        scopeInstanceId: \"box\",\n        type: \"variable\",\n        name: \"\",\n        value: { type: \"string\", value: \"Hello\" },\n      },\n    ])\n  );\n  $props.set(\n    toMap([\n      {\n        id: \"prop1\",\n        name: \"first\",\n        instanceId: \"box\",\n        type: \"expression\",\n        value: `$ws$dataSource$var1 + 2`,\n      },\n      {\n        id: \"prop2\",\n        name: \"second\",\n        instanceId: \"box\",\n        type: \"expression\",\n        value: `$ws$dataSource$var2 + ' World!'`,\n      },\n      {\n        id: \"prop3\",\n        name: \"third\",\n        instanceId: \"box\",\n        type: \"expression\",\n        // do not fail when access fields of undefined\n        value: `$ws$dataSource$var1.second.third || \"something\"`,\n      },\n    ])\n  );\n  expect(\n    $propValuesByInstanceSelector.get().get(getInstanceKey([\"box\"]))\n  ).toEqual(\n    new Map<string, unknown>([\n      [\"first\", 3],\n      [\"second\", \"Hello World!\"],\n      [\"third\", \"something\"],\n    ])\n  );\n\n  $dataSourceVariables.set(new Map([[\"var1\", 4]]));\n  expect(\n    $propValuesByInstanceSelector.get().get(getInstanceKey([\"box\"]))\n  ).toEqual(\n    new Map<string, unknown>([\n      [\"first\", 6],\n      [\"second\", \"Hello World!\"],\n      [\"third\", \"something\"],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"generate action prop callbacks\", () => {\n  setBoxInstance(\"box\");\n  selectPageRoot(\"box\");\n  $dataSources.set(\n    toMap([\n      {\n        id: \"var\",\n        scopeInstanceId: \"box\",\n        type: \"variable\",\n        name: \"\",\n        value: { type: \"number\", value: 1 },\n      },\n    ])\n  );\n  $props.set(\n    toMap([\n      {\n        id: \"valueId\",\n        name: \"value\",\n        instanceId: \"box\",\n        type: \"expression\",\n        value: `$ws$dataSource$var`,\n      },\n      {\n        id: \"actionId\",\n        name: \"onChange\",\n        instanceId: \"box\",\n        type: \"action\",\n        value: [\n          {\n            type: \"execute\",\n            args: [],\n            code: `$ws$dataSource$var = $ws$dataSource$var + 1`,\n          },\n        ],\n      },\n    ])\n  );\n  const values1 = $propValuesByInstanceSelector\n    .get()\n    .get(getInstanceKey([\"box\"]));\n  expect(values1?.get(\"value\")).toEqual(1);\n\n  (values1?.get(\"onChange\") as () => void)();\n  const values2 = $propValuesByInstanceSelector\n    .get()\n    .get(getInstanceKey([\"box\"]));\n  expect(values2?.get(\"value\")).toEqual(2);\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"resolve asset prop values\", () => {\n  setBoxInstance(\"box\");\n  selectPageRoot(\"box\");\n  $dataSources.set(new Map());\n  $assets.set(\n    toMap([\n      {\n        id: \"assetId\",\n        type: \"image\",\n        name: \"my-file.jpg\",\n        format: \"jpeg\",\n        size: 0,\n        projectId: \"\",\n        createdAt: \"\",\n        meta: { width: 0, height: 0 },\n        description: \"\",\n      },\n    ])\n  );\n  $props.set(\n    toMap([\n      {\n        id: \"propId\",\n        name: \"myAsset\",\n        instanceId: \"box\",\n        type: \"asset\",\n        value: \"assetId\",\n      },\n    ])\n  );\n  expect(\n    $propValuesByInstanceSelector.get().get(getInstanceKey([\"box\"]))\n  ).toEqual(\n    new Map<string, unknown>([\n      [\"$webstudio$canvasOnly$assetId\", \"assetId\"],\n      [\"myAsset\", \"/cgi/asset/my-file.jpg\"],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"resolve page prop values\", () => {\n  setBoxInstance(\"box\");\n  selectPageRoot(\"box\");\n  $dataSources.set(new Map());\n  $props.set(\n    toMap([\n      {\n        id: \"propId\",\n        name: \"myPage\",\n        instanceId: \"box\",\n        type: \"page\",\n        value: \"pageId\",\n      },\n    ])\n  );\n  expect(\n    $propValuesByInstanceSelector.get().get(getInstanceKey([\"box\"]))\n  ).toEqual(new Map<string, unknown>([[\"myPage\", \"/\"]]));\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"compute expression from collection items\", () => {\n  $instances.set(\n    toMap([\n      {\n        id: \"list\",\n        type: \"instance\",\n        component: collectionComponent,\n        children: [{ type: \"id\", value: \"item\" }],\n      },\n      {\n        id: \"item\",\n        type: \"instance\",\n        component: \"Box\",\n        children: [],\n      },\n    ])\n  );\n  selectPageRoot(\"list\");\n  $dataSources.set(\n    toMap([\n      {\n        id: \"itemId\",\n        scopeInstanceId: \"list\",\n        type: \"parameter\",\n        name: \"item\",\n      },\n    ])\n  );\n  $props.set(\n    toMap([\n      {\n        id: \"prop1\",\n        name: \"data\",\n        instanceId: \"list\",\n        type: \"json\",\n        value: [\"orange\", \"apple\", \"banana\"],\n      },\n      {\n        id: \"prop2\",\n        name: \"item\",\n        instanceId: \"list\",\n        type: \"parameter\",\n        value: \"itemId\",\n      },\n      {\n        id: \"prop3\",\n        name: \"ariaLabel\",\n        instanceId: \"item\",\n        type: \"expression\",\n        value: `$ws$dataSource$itemId`,\n      },\n    ])\n  );\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([\"list\"]),\n        new Map<string, unknown>([[\"data\", [\"orange\", \"apple\", \"banana\"]]]),\n      ],\n      [\n        getInstanceKey([\"item\", \"list[0]\", \"list\"]),\n        new Map<string, unknown>([[\"ariaLabel\", \"orange\"]]),\n      ],\n      [\n        getInstanceKey([\"item\", \"list[1]\", \"list\"]),\n        new Map<string, unknown>([[\"ariaLabel\", \"apple\"]]),\n      ],\n      [\n        getInstanceKey([\"item\", \"list[2]\", \"list\"]),\n        new Map<string, unknown>([[\"ariaLabel\", \"banana\"]]),\n      ],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"compute expression from object collection items\", () => {\n  const dataVariable = new Variable(\"dataVariable\", {\n    first: \"orange\",\n    second: \"apple\",\n    third: \"banana\",\n  });\n  const collectionItem = new Parameter(\"Collection Item\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <ws.collection\n        ws:id=\"collectionId\"\n        data={expression`${dataVariable}`}\n        item={collectionItem}\n      >\n        <$.Box ws:id=\"boxId\" ariaLabel={expression`${collectionItem}`}></$.Box>\n      </ws.collection>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  selectPageRoot(\"bodyId\");\n  $dataSourceVariables.set(new Map([]));\n\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [getInstanceKey([\"bodyId\"]), new Map<string, unknown>([])],\n      [\n        getInstanceKey([\"collectionId\", \"bodyId\"]),\n        new Map<string, unknown>([\n          [\"data\", { first: \"orange\", second: \"apple\", third: \"banana\" }],\n        ]),\n      ],\n      [\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[first]\",\n          \"collectionId\",\n          \"bodyId\",\n        ]),\n        new Map<string, unknown>([[\"ariaLabel\", \"orange\"]]),\n      ],\n      [\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[second]\",\n          \"collectionId\",\n          \"bodyId\",\n        ]),\n        new Map<string, unknown>([[\"ariaLabel\", \"apple\"]]),\n      ],\n      [\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[third]\",\n          \"collectionId\",\n          \"bodyId\",\n        ]),\n        new Map<string, unknown>([[\"ariaLabel\", \"banana\"]]),\n      ],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"access parameter value from variables values\", () => {\n  $instances.set(\n    toMap([\n      {\n        id: \"body\",\n        type: \"instance\",\n        component: \"Body\",\n        children: [],\n      },\n    ])\n  );\n  selectPageRoot(\"body\");\n  $dataSources.set(\n    toMap([\n      {\n        id: \"parameterId\",\n        scopeInstanceId: \"body\",\n        type: \"parameter\",\n        name: \"paramName\",\n      },\n    ])\n  );\n  $dataSourceVariables.set(new Map([[\"parameterId\", \"paramValue\"]]));\n  $props.set(\n    toMap([\n      {\n        id: \"parameterPropId\",\n        name: \"param\",\n        instanceId: \"body\",\n        type: \"expression\",\n        value: \"$ws$dataSource$parameterId\",\n      },\n    ])\n  );\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([\"body\"]),\n        new Map<string, unknown>([[\"param\", \"paramValue\"]]),\n      ],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"compute props bound to resource variables\", () => {\n  $instances.set(\n    toMap([{ id: \"body\", type: \"instance\", component: \"Body\", children: [] }])\n  );\n  selectPageRoot(\"body\");\n  $dataSources.set(\n    toMap([\n      {\n        id: \"resourceVariableId\",\n        scopeInstanceId: \"body\",\n        type: \"resource\",\n        name: \"paramName\",\n        resourceId: \"resourceId\",\n      },\n    ])\n  );\n  $resources.set(\n    toMap<Resource>([\n      {\n        id: \"resourceId\",\n        name: \"my-resource\",\n        url: `\"\"`,\n        method: \"get\",\n        headers: [],\n      },\n    ])\n  );\n  const key = getResourceKey({\n    name: \"my-resource\",\n    url: \"\",\n    method: \"get\",\n    headers: [],\n    searchParams: [],\n  });\n  $resourcesCache.set(new Map([[key, \"my-value\"]]));\n  $props.set(\n    toMap([\n      {\n        id: \"resourcePropId\",\n        name: \"resource\",\n        instanceId: \"body\",\n        type: \"expression\",\n        value: \"$ws$dataSource$resourceVariableId\",\n      },\n    ])\n  );\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([\"body\"]),\n        new Map<string, unknown>([[\"resource\", \"my-value\"]]),\n      ],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"compute instance text content when plain text\", () => {\n  $instances.set(\n    toMap([\n      {\n        id: \"body\",\n        type: \"instance\",\n        component: \"Body\",\n        children: [\n          { type: \"id\", value: \"plainBox\" },\n          { type: \"id\", value: \"richBox\" },\n        ],\n      },\n      {\n        id: \"plainBox\",\n        type: \"instance\",\n        component: \"Box\",\n        children: [{ type: \"text\", value: \"plain\" }],\n      },\n      {\n        id: \"richBox\",\n        type: \"instance\",\n        component: \"Box\",\n        children: [\n          { type: \"text\", value: \"plain\" },\n          { type: \"id\", value: \"bold\" },\n        ],\n      },\n      {\n        id: \"bold\",\n        type: \"instance\",\n        component: \"Bold\",\n        children: [{ type: \"text\", value: \"bold\" }],\n      },\n    ])\n  );\n  selectPageRoot(\"body\");\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [getInstanceKey([\"body\"]), new Map<string, unknown>()],\n      [\n        getInstanceKey([\"plainBox\", \"body\"]),\n        new Map<string, unknown>([[textContentAttribute, \"plain\"]]),\n      ],\n      [getInstanceKey([\"richBox\", \"body\"]), new Map<string, unknown>()],\n      [\n        getInstanceKey([\"bold\", \"richBox\", \"body\"]),\n        new Map<string, unknown>([[textContentAttribute, \"bold\"]]),\n      ],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"compute instance text content bound to expression\", () => {\n  $instances.set(\n    toMap([\n      {\n        id: \"body\",\n        type: \"instance\",\n        component: \"Body\",\n        children: [{ type: \"id\", value: \"expressionBox\" }],\n      },\n      {\n        id: \"expressionBox\",\n        type: \"instance\",\n        component: \"Box\",\n        children: [\n          { type: \"expression\", value: `\"Hello \" + $ws$dataSource$world` },\n        ],\n      },\n    ])\n  );\n  $dataSources.set(\n    toMap([\n      {\n        id: \"world\",\n        scopeInstanceId: \"body\",\n        name: \"world\",\n        type: \"variable\",\n        value: { type: \"string\", value: \"world\" },\n      },\n    ])\n  );\n  selectPageRoot(\"body\");\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [getInstanceKey([\"body\"]), new Map<string, unknown>()],\n      [\n        getInstanceKey([\"expressionBox\", \"body\"]),\n        new Map<string, unknown>([[textContentAttribute, \"Hello world\"]]),\n      ],\n    ])\n  );\n\n  cleanStores($propValuesByInstanceSelector);\n});\n\ntest(\"use page system values in props\", () => {\n  const systemParameter = new Parameter(\"system\");\n  const data = renderData(\n    <$.Body\n      ws:id=\"bodyId\"\n      data-origin={expression`${systemParameter}.origin`}\n    ></$.Body>\n  );\n  expect(data.dataSources.size).toEqual(1);\n  const [systemParameterId] = data.dataSources.keys();\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  selectPageRoot(\"bodyId\", systemParameterId);\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([\"bodyId\"]),\n        new Map<string, unknown>([\n          [\"data-origin\", \"https://undefined.wstd.work\"],\n        ]),\n      ],\n    ])\n  );\n});\n\ntest(\"compute props with global variables\", () => {\n  const rootVariable = new Variable(\"rootVariable\", \"root value\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${rootVariable}`}>\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" data-value={expression`${rootVariable}`}></$.Box>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  selectPageRoot(\"bodyId\");\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [getInstanceKey([\"bodyId\"]), new Map<string, unknown>()],\n      [\n        getInstanceKey([\"boxId\", \"bodyId\"]),\n        new Map<string, unknown>([[\"data-value\", \"root value\"]]),\n      ],\n    ])\n  );\n});\n\ntest(\"use global system values in props\", () => {\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" data-origin={expression`$ws$system.origin`}></$.Body>\n  );\n  expect(data.dataSources.size).toEqual(0);\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  selectPageRoot(\"bodyId\");\n  expect($propValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([\"bodyId\"]),\n        new Map<string, unknown>([\n          [\"data-origin\", \"https://undefined.wstd.work\"],\n        ]),\n      ],\n    ])\n  );\n});\n\ntest(\"compute variable values for page root\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"initial\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}></$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [dataSourceId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  $dataSourceVariables.set(new Map([[dataSourceId, \"success\"]]));\n  expect(\n    $variableValuesByInstanceSelector\n      .get()\n      .get(getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]))\n  ).toEqual(\n    new Map<string, unknown>([\n      [SYSTEM_VARIABLE_ID, initialSystem],\n      [dataSourceId, \"success\"],\n    ])\n  );\n});\n\ntest(\"nest variable values from global root to current instance\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"\");\n  const boxVariable = new Variable(\"boxVariable\", \"\");\n  const textVariable = new Variable(\"textVariable\", \"\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" ars={expression`${boxVariable}`}></$.Box>\n      <$.Text ws:id=\"textId\" ars={expression`${textVariable}`}></$.Text>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [bodyVariableId, boxVariableId, textVariableId] =\n    data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  $dataSourceVariables.set(\n    new Map([\n      [bodyVariableId, \"bodyValue\"],\n      [boxVariableId, \"boxValue\"],\n      [textVariableId, \"textValue\"],\n    ])\n  );\n  expect($variableValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([ROOT_INSTANCE_ID]),\n        new Map([[SYSTEM_VARIABLE_ID, initialSystem]]),\n      ],\n      [\n        getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [bodyVariableId, \"bodyValue\"],\n        ]),\n      ],\n      [\n        getInstanceKey([\"boxId\", \"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [bodyVariableId, \"bodyValue\"],\n          [boxVariableId, \"boxValue\"],\n        ]),\n      ],\n      [\n        getInstanceKey([\"textId\", \"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [bodyVariableId, \"bodyValue\"],\n          [textVariableId, \"textValue\"],\n        ]),\n      ],\n    ])\n  );\n});\n\ntest(\"compute item values for collection\", () => {\n  const dataVariable = new Variable(\"dataVariable\", [\n    \"apple\",\n    \"banana\",\n    \"orange\",\n  ]);\n  const collectionItem = new Parameter(\"Collection Item\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <ws.collection\n        ws:id=\"collectionId\"\n        data={expression`${dataVariable}`}\n        item={collectionItem}\n      >\n        <$.Box ws:id=\"boxId\"></$.Box>\n      </ws.collection>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [_dataVariableId, itemParameterId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  $dataSourceVariables.set(new Map([]));\n  const values = $variableValuesByInstanceSelector.get();\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[0]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual(\"apple\");\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[1]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual(\"banana\");\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[2]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual(\"orange\");\n});\n\ntest(\"compute item values for collection with object data\", () => {\n  const dataVariable = new Variable(\"dataVariable\", {\n    first: \"apple\",\n    second: \"banana\",\n    third: \"orange\",\n  });\n  const collectionItem = new Parameter(\"Collection Item\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <ws.collection\n        ws:id=\"collectionId\"\n        data={expression`${dataVariable}`}\n        item={collectionItem}\n      >\n        <$.Box ws:id=\"boxId\"></$.Box>\n      </ws.collection>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [_dataVariableId, itemParameterId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  $dataSourceVariables.set(new Map([]));\n  const values = $variableValuesByInstanceSelector.get();\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[first]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual(\"apple\");\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[second]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual(\"banana\");\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[third]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual(\"orange\");\n});\n\ntest(\"compute item values for collection with nested object data\", () => {\n  const dataVariable = new Variable(\"dataVariable\", {\n    user1: { name: \"Alice\", age: 30 },\n    user2: { name: \"Bob\", age: 25 },\n  });\n  const collectionItem = new Parameter(\"Collection Item\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\">\n      <ws.collection\n        ws:id=\"collectionId\"\n        data={expression`${dataVariable}`}\n        item={collectionItem}\n      >\n        <$.Box ws:id=\"boxId\"></$.Box>\n      </ws.collection>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [_dataVariableId, itemParameterId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  $dataSourceVariables.set(new Map([]));\n  const values = $variableValuesByInstanceSelector.get();\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[user1]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual({ name: \"Alice\", age: 30 });\n  expect(\n    values\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"collectionId[user2]\",\n          \"collectionId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n      ?.get(itemParameterId)\n  ).toEqual({ name: \"Bob\", age: 25 });\n});\n\ntest(\"compute resource variable values\", () => {\n  const resourceVariable = new ResourceValue(\"resourceVariable\", {\n    url: expression`\"\"`,\n    method: \"get\",\n    searchParams: [],\n    headers: [],\n  });\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${resourceVariable}`}></$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $resources.set(data.resources);\n  $props.set(data.props);\n  const [resourceVariableId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  const key = getResourceKey({\n    name: \"resourceVariable\",\n    url: \"\",\n    method: \"get\",\n    headers: [],\n    searchParams: [],\n  });\n  $resourcesCache.set(new Map([[key, \"my-value\"]]));\n  expect(\n    $variableValuesByInstanceSelector\n      .get()\n      .get(getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]))\n      ?.get(resourceVariableId)\n  ).toEqual(\"my-value\");\n});\n\ntest(\"stop variables lookup outside of slots\", () => {\n  const bodyVariable = new Variable(\"bodyVariable\", \"body\");\n  const slotVariable = new Variable(\"slotVariable\", \"slot\");\n  const boxVariable = new Variable(\"boxVariable\", \"box\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n      <$.Slot ws:id=\"slotId\" vars={expression`${slotVariable}`}>\n        <$.Fragment ws:id=\"fragmentId\">\n          <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n        </$.Fragment>\n      </$.Slot>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  selectPageRoot(\"bodyId\");\n  const values = $variableValuesByInstanceSelector.get();\n  expect(\n    values.get(getInstanceKey([\"slotId\", \"bodyId\", ROOT_INSTANCE_ID]))?.size\n  ).toEqual(3);\n  expect(\n    values.get(\n      getInstanceKey([\"fragmentId\", \"slotId\", \"bodyId\", ROOT_INSTANCE_ID])\n    )?.size\n  ).toEqual(1);\n  expect(\n    values.get(\n      getInstanceKey([\n        \"boxId\",\n        \"fragmentId\",\n        \"slotId\",\n        \"bodyId\",\n        ROOT_INSTANCE_ID,\n      ])\n    )?.size\n    // global system and box variable\n  ).toEqual(2);\n});\n\ntest(\"compute parameter and resource variables without values to make it available in scope\", () => {\n  const resourceVariable = new ResourceValue(\"resourceVariable\", {\n    url: expression`\"\"`,\n    method: \"get\",\n    searchParams: [],\n    headers: [],\n  });\n  const parameterVariable = new Parameter(\"parameterVariable\");\n  const data = renderData(\n    <$.Body\n      ws:id=\"bodyId\"\n      vars={expression`${resourceVariable} + ${parameterVariable}`}\n    ></$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [resourceVariableId, parameterVariableId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  const values = $variableValuesByInstanceSelector\n    .get()\n    .get(getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]));\n  expect(values?.get(resourceVariableId)).toEqual(undefined);\n  expect(values?.get(parameterVariableId)).toEqual(undefined);\n});\n\ntest(\"provide page system variable value\", () => {\n  const system = new Parameter(\"system\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${system}`}></$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [systemId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\", systemId);\n  expect(\n    $variableValuesByInstanceSelector\n      .get()\n      .get(getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]))\n      ?.get(systemId)\n  ).toEqual(initialSystem);\n  updateCurrentSystem({\n    params: { slug: \"my-post\" },\n  });\n  expect(\n    $variableValuesByInstanceSelector\n      .get()\n      .get(getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]))\n      ?.get(systemId)\n  ).toEqual({\n    params: { slug: \"my-post\" },\n    pathname: \"/\",\n    search: {},\n    origin: \"https://undefined.wstd.work\",\n  });\n});\n\ntest(\"provide global system variable value\", () => {\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`$ws$system`}></$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  selectPageRoot(\"bodyId\");\n  $systemDataByPage.set(new Map());\n  expect($variableValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([ROOT_INSTANCE_ID]),\n        new Map([[SYSTEM_VARIABLE_ID, initialSystem]]),\n      ],\n      [\n        getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([[SYSTEM_VARIABLE_ID, initialSystem]]),\n      ],\n    ])\n  );\n  updateCurrentSystem({\n    params: { slug: \"my-post\" },\n  });\n  const updatedSystem = {\n    params: { slug: \"my-post\" },\n    pathname: \"/\",\n    search: {},\n    origin: \"https://undefined.wstd.work\",\n  };\n  expect($variableValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([ROOT_INSTANCE_ID]),\n        new Map([[SYSTEM_VARIABLE_ID, updatedSystem]]),\n      ],\n      [\n        getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([[SYSTEM_VARIABLE_ID, updatedSystem]]),\n      ],\n    ])\n  );\n});\n\ntest(\"mask variables with the same name in nested scope\", () => {\n  const bodyVariable = new Variable(\"myVariable\", \"body\");\n  const boxVariable = new Variable(\"myVariable\", \"box\");\n  const data = renderData(\n    <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n      <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n    </$.Body>\n  );\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [bodyVariableId, boxVariableId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  $systemDataByPage.set(new Map());\n  expect($variableValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([ROOT_INSTANCE_ID]),\n        new Map([[SYSTEM_VARIABLE_ID, initialSystem]]),\n      ],\n      [\n        getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [bodyVariableId, \"body\"],\n        ]),\n      ],\n      [\n        getInstanceKey([\"boxId\", \"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [boxVariableId, \"box\"],\n        ]),\n      ],\n    ])\n  );\n});\n\ntest(\"inherit variables from global root\", () => {\n  const rootVariable = new Variable(\"rootVariable\", \"root\");\n  const boxVariable = new Variable(\"myVariable\", \"box\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${rootVariable}`}>\n      <$.Body ws:id=\"bodyId\">\n        <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [rootVariableId, boxVariableId] = data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  expect($variableValuesByInstanceSelector.get()).toEqual(\n    new Map([\n      [\n        getInstanceKey([ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [rootVariableId, \"root\"],\n        ]),\n      ],\n      [\n        getInstanceKey([\"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [rootVariableId, \"root\"],\n        ]),\n      ],\n      [\n        getInstanceKey([\"boxId\", \"bodyId\", ROOT_INSTANCE_ID]),\n        new Map<string, unknown>([\n          [SYSTEM_VARIABLE_ID, initialSystem],\n          [rootVariableId, \"root\"],\n          [boxVariableId, \"box\"],\n        ]),\n      ],\n    ])\n  );\n});\n\ntest(\"inherit variables from global root inside slots\", () => {\n  const rootVariable = new Variable(\"rootVariable\", \"root\");\n  const bodyVariable = new Variable(\"bodyVariable\", \"body\");\n  const boxVariable = new Variable(\"myVariable\", \"box\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${rootVariable}`}>\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}>\n        <$.Slot ws:id=\"slotId\">\n          <$.Fragment ws:id=\"fragmentId\">\n            <$.Box ws:id=\"boxId\" vars={expression`${boxVariable}`}></$.Box>\n          </$.Fragment>\n        </$.Slot>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  $instances.set(data.instances);\n  $dataSources.set(data.dataSources);\n  $props.set(data.props);\n  const [rootVariableId, _bodyVariableId, boxVariableId] =\n    data.dataSources.keys();\n  selectPageRoot(\"bodyId\");\n  expect(\n    $variableValuesByInstanceSelector\n      .get()\n      .get(\n        getInstanceKey([\n          \"boxId\",\n          \"fragmentId\",\n          \"slotId\",\n          \"bodyId\",\n          ROOT_INSTANCE_ID,\n        ])\n      )\n  ).toEqual(\n    new Map<string, unknown>([\n      [SYSTEM_VARIABLE_ID, initialSystem],\n      [rootVariableId, \"root\"],\n      [boxVariableId, \"box\"],\n    ])\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/props.ts",
    "content": "import { computed } from \"nanostores\";\nimport type {\n  DataSource,\n  Instance,\n  Prop,\n  ResourceRequest,\n  ImageAsset,\n} from \"@webstudio-is/sdk\";\nimport {\n  decodeDataSourceVariable,\n  encodeDataSourceVariable,\n  transpileExpression,\n  collectionComponent,\n  portalComponent,\n  ROOT_INSTANCE_ID,\n  SYSTEM_VARIABLE_ID,\n  findTreeInstanceIds,\n} from \"@webstudio-is/sdk\";\nimport {\n  normalizeProps,\n  textContentAttribute,\n  getCollectionEntries,\n} from \"@webstudio-is/react-sdk\";\nimport { mapGroupBy } from \"~/shared/shim\";\nimport { $instances } from \"./instances\";\nimport {\n  $dataSources,\n  $props,\n  $assets,\n  $resources,\n  $uploadingFilesDataStore,\n  $memoryProps,\n  $isPreviewMode,\n} from \"./misc\";\nimport { $pages } from \"./pages\";\nimport type { InstanceSelector } from \"../tree-utils\";\nimport { $dataSourceVariables } from \"./variables\";\nimport { uploadingFileDataToAsset } from \"~/builder/shared/assets/asset-utils\";\nimport { $selectedPage, getInstanceKey } from \"../awareness\";\nimport { computeExpression } from \"../data-variables\";\nimport { $currentSystem } from \"../system\";\nimport {\n  $resourcesCache,\n  computeResourceRequest,\n  getResourceKey,\n  preloadResource,\n} from \"../resources\";\n\nexport const assetBaseUrl = \"/cgi/asset/\";\n\nexport const getIndexedInstanceId = (\n  instanceId: Instance[\"id\"],\n  index: number | string\n) => `${instanceId}[${index}]`;\n\n/**\n * (arg1) => {\n * let $ws$dataSource$id = _getVariable('id')\n * $ws$dataSource$id = $ws$dataSource$id + arg1\n * _setVariable('id', $ws$dataSource$id)\n * }\n */\nconst generateAction = (prop: Extract<Prop, { type: \"action\" }>) => {\n  const getters = new Set<DataSource[\"id\"]>();\n  const setters = new Set<DataSource[\"id\"]>();\n  // important to fallback to empty argumets to render empty function\n  let args: string[] = [];\n  let assignersCode = \"\";\n  for (const value of prop.value) {\n    args = value.args;\n    assignersCode += transpileExpression({\n      expression: value.code,\n      executable: true,\n      replaceVariable: (identifier, assignee) => {\n        if (args?.includes(identifier)) {\n          return;\n        }\n        const depId = decodeDataSourceVariable(identifier);\n        if (depId) {\n          getters.add(depId);\n          if (assignee) {\n            setters.add(depId);\n          }\n        }\n      },\n    });\n    assignersCode += `\\n`;\n  }\n  let gettersCode = \"\";\n  for (const dataSourceId of getters) {\n    const valueName = encodeDataSourceVariable(dataSourceId);\n    gettersCode += `let ${valueName} = _getVariable(\"${dataSourceId}\")\\n`;\n  }\n  let settersCode = \"\";\n  for (const dataSourceId of setters) {\n    const valueName = encodeDataSourceVariable(dataSourceId);\n    settersCode += `_setVariable(\"${dataSourceId}\", ${valueName})\\n`;\n  }\n  let generated = \"\";\n  generated += `return (${args.join(\", \")}) => {\\n`;\n  generated += gettersCode;\n  generated += assignersCode;\n  generated += settersCode;\n  generated += `}`;\n  return generated;\n};\n\nconst getAction = (\n  prop: Extract<Prop, { type: \"action\" }>,\n  values: Map<string, unknown>\n) => {\n  const generatedAction = generateAction(prop);\n  try {\n    const executeFn = new Function(\n      \"_getVariable\",\n      \"_setVariable\",\n      generatedAction\n    );\n    const getVariable = (id: string) => {\n      return values.get(id);\n    };\n    const setVariable = (id: string, value: unknown) => {\n      const dataSourceVariables = new Map($dataSourceVariables.get());\n      dataSourceVariables.set(id, value);\n      $dataSourceVariables.set(dataSourceVariables);\n    };\n    return executeFn(getVariable, setVariable);\n  } catch (error) {\n    console.error(error);\n  }\n};\n\n/**\n * similar to below but should not depend on resource values\n * because these values are used to load resources\n * which result in updated resource values and may trigger\n * circular updates\n */\nconst $resourceVariableValues = computed(\n  [$dataSources, $selectedPage, $currentSystem],\n  (dataSources, selectedPage, system) => {\n    const values = new Map<string, unknown>();\n    values.set(SYSTEM_VARIABLE_ID, system);\n    for (const [dataSourceId, dataSource] of dataSources) {\n      if (dataSource.type === \"variable\") {\n        values.set(dataSourceId, dataSource.value.value);\n      }\n      if (\n        dataSource.type === \"parameter\" ||\n        dataSource.id === selectedPage?.systemDataSourceId\n      ) {\n        values.set(dataSourceId, system);\n      }\n    }\n    return values;\n  }\n);\n\n/**\n * values of all variables without computing scope specifics like collections\n * simplified version of variable values by instance selector\n */\nconst $unscopedVariableValues = computed(\n  [\n    $dataSources,\n    $dataSourceVariables,\n    $resources,\n    $resourceVariableValues,\n    $resourcesCache,\n    $selectedPage,\n    $currentSystem,\n  ],\n  (\n    dataSources,\n    dataSourceVariables,\n    resources,\n    resourceVariableValues,\n    resourcesCache,\n    page,\n    system\n  ) => {\n    const values = new Map<string, unknown>();\n    // support global system\n    values.set(SYSTEM_VARIABLE_ID, system);\n    for (const [dataSourceId, dataSource] of dataSources) {\n      if (dataSource.type === \"variable\") {\n        values.set(\n          dataSourceId,\n          dataSourceVariables.get(dataSourceId) ?? dataSource.value.value\n        );\n      }\n      if (dataSource.type === \"parameter\") {\n        let value = dataSourceVariables.get(dataSourceId);\n        // support page system\n        if (dataSource.id === page?.systemDataSourceId) {\n          value = system;\n        }\n        values.set(dataSourceId, value);\n      }\n      if (dataSource.type === \"resource\") {\n        const resource = resources.get(dataSource.resourceId);\n        if (resource) {\n          const resourceRequest = computeResourceRequest(\n            resource,\n            resourceVariableValues\n          );\n          values.set(\n            dataSourceId,\n            resourcesCache.get(getResourceKey(resourceRequest))\n          );\n        }\n      }\n    }\n    return values;\n  }\n);\n\n/**\n * compute prop values within context of instance ancestors\n * like a dry-run of rendering and accessing react contexts deep in the tree\n * essential to support collections which provide different values in each item\n * for same variables\n */\nexport const $propValuesByInstanceSelector = computed(\n  [\n    $instances,\n    $props,\n    $selectedPage,\n    $unscopedVariableValues,\n    $pages,\n    $assets,\n    $uploadingFilesDataStore,\n  ],\n  (\n    instances,\n    props,\n    page,\n    unscopedVariableValues,\n    pages,\n    assets,\n    uploadingFilesDataStore\n  ) => {\n    // already includes global variables\n    const variableValues = new Map<string, unknown>(unscopedVariableValues);\n\n    let propsList = Array.from(props.values());\n\n    // ignore asset and page props when params is not provided\n    if (pages) {\n      const uploadingImageAssets = uploadingFilesDataStore\n        .map(uploadingFileDataToAsset)\n        .filter(<T>(value: T): value is NonNullable<T> => value !== undefined)\n        .filter((asset): asset is ImageAsset => asset.type === \"image\");\n\n      // use whole props list to let access hash props from other pages and instances\n      propsList = normalizeProps({\n        props: propsList,\n        assetBaseUrl,\n        assets,\n        uploadingImageAssets,\n        pages,\n        source: \"canvas\",\n      });\n    }\n    // collect props and group by instances\n    const propsByInstanceId = mapGroupBy(propsList, (prop) => prop.instanceId);\n\n    // traverse instances tree and compute props within each instance\n    const propValuesByInstanceSelector = new Map<\n      Instance[\"id\"],\n      Map<Prop[\"name\"], unknown>\n    >();\n    if (page === undefined) {\n      return propValuesByInstanceSelector;\n    }\n    const traverseInstances = (instanceSelector: InstanceSelector) => {\n      const [instanceId] = instanceSelector;\n      const instance = instances.get(instanceId);\n      if (instance === undefined) {\n        return;\n      }\n\n      const propValues = new Map<Prop[\"name\"], unknown>();\n      const props = propsByInstanceId.get(instanceId);\n      const parameters = new Map<Prop[\"name\"], DataSource[\"id\"]>();\n\n      if (props) {\n        for (const prop of props) {\n          // at this point asset and page either already converted to string\n          // or can be ignored\n          if (prop.type === \"asset\" || prop.type === \"page\") {\n            continue;\n          }\n          if (prop.type === \"expression\") {\n            const value = computeExpression(prop.value, variableValues);\n            if (value !== undefined) {\n              propValues.set(prop.name, value);\n            }\n            continue;\n          }\n          if (prop.type === \"action\") {\n            const action = getAction(prop, variableValues);\n            if (typeof action === \"function\") {\n              propValues.set(prop.name, action);\n            }\n            continue;\n          }\n          if (prop.type === \"parameter\") {\n            parameters.set(prop.name, prop.value);\n            continue;\n          }\n          propValues.set(prop.name, prop.value);\n        }\n      }\n\n      propValuesByInstanceSelector.set(\n        getInstanceKey(instanceSelector),\n        propValues\n      );\n\n      if (instance.component === collectionComponent) {\n        const originalData = propValues.get(\"data\");\n        const itemVariableId = parameters.get(\"item\");\n        const itemKeyVariableId = parameters.get(\"itemKey\");\n        if (itemVariableId !== undefined && originalData) {\n          for (const [key, value] of getCollectionEntries(originalData)) {\n            variableValues.set(itemVariableId, value);\n            if (itemKeyVariableId !== undefined) {\n              variableValues.set(itemKeyVariableId, key);\n            }\n            for (const child of instance.children) {\n              if (child.type === \"id\") {\n                const indexId = getIndexedInstanceId(instanceId, key);\n                traverseInstances([child.value, indexId, ...instanceSelector]);\n              }\n            }\n          }\n        }\n        return;\n      }\n      for (const child of instance.children) {\n        // plain text can be edited from props panel\n        if (child.type === \"text\" && instance.children.length === 1) {\n          propValues.set(textContentAttribute, child.value);\n        }\n        if (child.type === \"expression\") {\n          const value = computeExpression(child.value, variableValues);\n          if (value !== undefined) {\n            propValues.set(textContentAttribute, value);\n          }\n        }\n        if (child.type === \"id\") {\n          traverseInstances([child.value, ...instanceSelector]);\n        }\n      }\n    };\n\n    traverseInstances([page.rootInstanceId]);\n\n    return propValuesByInstanceSelector;\n  }\n);\n\nexport const $propValuesByInstanceSelectorWithMemoryProps = computed(\n  [$propValuesByInstanceSelector, $memoryProps, $isPreviewMode],\n  (propValuesByInstanceSelector, memoryProps, isPreviewMode) => {\n    if (false === isPreviewMode) {\n      const result = new Map(propValuesByInstanceSelector);\n\n      for (const [memoryKey, memoryValue] of memoryProps) {\n        const propsBySelector = new Map(result.get(memoryKey));\n\n        for (const [memoryProp, memoryPropValue] of memoryValue) {\n          propsBySelector.set(memoryProp, memoryPropValue.value);\n        }\n\n        result.set(memoryKey, propsBySelector);\n      }\n      return result;\n    }\n    return propValuesByInstanceSelector;\n  }\n);\n\nexport const $variableValuesByInstanceSelector = computed(\n  [\n    $instances,\n    $props,\n    $selectedPage,\n    $dataSources,\n    $dataSourceVariables,\n    $resources,\n    $resourceVariableValues,\n    $resourcesCache,\n    $currentSystem,\n  ],\n  (\n    instances,\n    props,\n    page,\n    dataSources,\n    dataSourceVariables,\n    resources,\n    resourceVariableValues,\n    resourcesCache,\n    system\n  ) => {\n    const propsByInstanceId = mapGroupBy(\n      props.values(),\n      (prop) => prop.instanceId\n    );\n\n    const variablesByInstanceId = mapGroupBy(\n      dataSources.values(),\n      (dataSource) => dataSource.scopeInstanceId\n    );\n\n    // traverse instances tree and compute props within each instance\n    const variableValuesByInstanceSelector = new Map<\n      Instance[\"id\"],\n      Map<Prop[\"name\"], unknown>\n    >();\n    if (page === undefined) {\n      return variableValuesByInstanceSelector;\n    }\n\n    const collectVariables = (\n      instanceSelector: InstanceSelector,\n      parentVariableValues = new Map<string, unknown>(),\n      parentVariableNames = new Map<DataSource[\"name\"], DataSource[\"id\"]>()\n    ) => {\n      const [instanceId] = instanceSelector;\n      const variableNames = new Map(parentVariableNames);\n      const variableValues = new Map<string, unknown>(parentVariableValues);\n      variableValuesByInstanceSelector.set(\n        getInstanceKey(instanceSelector),\n        variableValues\n      );\n      const variables = variablesByInstanceId.get(instanceId);\n      // set global system value\n      if (instanceId === ROOT_INSTANCE_ID) {\n        variableNames.set(\"system\", SYSTEM_VARIABLE_ID);\n        variableValues.set(SYSTEM_VARIABLE_ID, system);\n      }\n      if (variables) {\n        for (const variable of variables) {\n          // delete previous variable with the same name\n          // because it is masked and no longer available\n          variableValues.delete(variableNames.get(variable.name) ?? \"\");\n          variableNames.set(variable.name, variable.id);\n          if (variable.type === \"variable\") {\n            const value = dataSourceVariables.get(variable.id);\n            variableValues.set(variable.id, value ?? variable.value.value);\n          }\n          if (variable.type === \"parameter\") {\n            const value = dataSourceVariables.get(variable.id);\n            variableValues.set(variable.id, value);\n            // set page system value\n            if (variable.id === page.systemDataSourceId) {\n              variableValues.set(variable.id, system);\n            }\n          }\n          if (variable.type === \"resource\") {\n            const resource = resources.get(variable.resourceId);\n            if (resource) {\n              const resourceRequest = computeResourceRequest(\n                resource,\n                resourceVariableValues\n              );\n              variableValues.set(\n                variable.id,\n                resourcesCache.get(getResourceKey(resourceRequest))\n              );\n            }\n          }\n        }\n      }\n      return { variableValues, variableNames };\n    };\n\n    const traverseInstances = (\n      instanceSelector: InstanceSelector,\n      parentVariableValues = new Map<string, unknown>(),\n      parentVariableNames = new Map<DataSource[\"name\"], DataSource[\"id\"]>()\n    ) => {\n      let { variableValues, variableNames } = collectVariables(\n        instanceSelector,\n        parentVariableValues,\n        parentVariableNames\n      );\n\n      const [instanceId] = instanceSelector;\n      const propValues = new Map<Prop[\"name\"], unknown>();\n      const props = propsByInstanceId.get(instanceId);\n      const parameters = new Map<Prop[\"name\"], DataSource[\"id\"]>();\n      if (props) {\n        for (const prop of props) {\n          if (\n            prop.type === \"asset\" ||\n            prop.type === \"page\" ||\n            prop.type === \"action\"\n          ) {\n            continue;\n          }\n          if (prop.type === \"expression\") {\n            const value = computeExpression(prop.value, variableValues);\n            if (value !== undefined) {\n              propValues.set(prop.name, value);\n            }\n            continue;\n          }\n          if (prop.type === \"parameter\") {\n            parameters.set(prop.name, prop.value);\n            continue;\n          }\n          propValues.set(prop.name, prop.value);\n        }\n      }\n\n      const instance = instances.get(instanceId);\n      if (instance === undefined) {\n        return;\n      }\n\n      if (instance.component === collectionComponent) {\n        const originalData = propValues.get(\"data\");\n        const itemVariableId = parameters.get(\"item\");\n        const itemKeyVariableId = parameters.get(\"itemKey\");\n        if (itemVariableId === undefined) {\n          return;\n        }\n        // prevent accessing item from collection\n        variableValues.delete(itemVariableId);\n        if (itemKeyVariableId !== undefined) {\n          variableValues.delete(itemKeyVariableId);\n        }\n        if (originalData) {\n          for (const [key, value] of getCollectionEntries(originalData)) {\n            const itemVariableValues = new Map(variableValues);\n            itemVariableValues.set(itemVariableId, value);\n            if (itemKeyVariableId !== undefined) {\n              itemVariableValues.set(itemKeyVariableId, key);\n            }\n            for (const child of instance.children) {\n              if (child.type === \"id\") {\n                const indexId = getIndexedInstanceId(instanceId, key);\n                traverseInstances(\n                  [child.value, indexId, ...instanceSelector],\n                  itemVariableValues\n                );\n              }\n            }\n          }\n        }\n        return;\n      }\n      // reset values for slot children to let slots behave as isolated components\n      if (instance.component === portalComponent) {\n        // allow accessing global variables in slots\n        variableValues = globalVariableValues;\n        variableNames = globalVariableNames;\n      }\n      for (const child of instance.children) {\n        if (child.type === \"id\") {\n          traverseInstances(\n            [child.value, ...instanceSelector],\n            variableValues,\n            variableNames\n          );\n        }\n      }\n    };\n    const {\n      variableValues: globalVariableValues,\n      variableNames: globalVariableNames,\n    } = collectVariables([ROOT_INSTANCE_ID]);\n    traverseInstances(\n      [page.rootInstanceId, ROOT_INSTANCE_ID],\n      globalVariableValues,\n      globalVariableNames\n    );\n    return variableValuesByInstanceSelector;\n  }\n);\n\nconst $computedResourceRequests = computed(\n  [\n    $selectedPage,\n    $instances,\n    $dataSources,\n    $resources,\n    $resourceVariableValues,\n  ],\n  (page, instances, dataSources, resources, values) => {\n    const computedResourceRequests: ResourceRequest[] = [];\n    if (page === undefined) {\n      return computedResourceRequests;\n    }\n    const instanceIds = findTreeInstanceIds(instances, page.rootInstanceId);\n    instanceIds.add(ROOT_INSTANCE_ID);\n    // load only resources bound to variables on current page\n    // action resources should not be loaded automatically\n    for (const dataSource of dataSources.values()) {\n      if (\n        instanceIds.has(dataSource.scopeInstanceId ?? \"\") &&\n        dataSource.type === \"resource\"\n      ) {\n        const resource = resources.get(dataSource.resourceId);\n        if (resource) {\n          computedResourceRequests.push(\n            computeResourceRequest(resource, values)\n          );\n        }\n      }\n    }\n    return computedResourceRequests;\n  }\n);\n\n/**\n * subscribe to all resources changes\n * load them with currently available variable values\n * and store in cache\n */\nexport const subscribeResources = () => {\n  return $computedResourceRequests.subscribe((computedResourceRequests) => {\n    for (const resourceRequest of computedResourceRequests) {\n      preloadResource(resourceRequest);\n    }\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/nano-states/variables.ts",
    "content": "import { atom } from \"nanostores\";\nimport type { DataSource } from \"@webstudio-is/sdk\";\n\nexport const $dataSourceVariables = atom<Map<DataSource[\"id\"], unknown>>(\n  new Map()\n);\n"
  },
  {
    "path": "apps/builder/app/shared/page-utils.test.tsx",
    "content": "import { describe, expect, test } from \"vitest\";\nimport type { Project } from \"@webstudio-is/project\";\nimport {\n  ROOT_FOLDER_ID,\n  ROOT_INSTANCE_ID,\n  encodeDataVariableId,\n  type Instance,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport {\n  createDefaultPages,\n  createRootFolder,\n} from \"@webstudio-is/project-build\";\nimport { $project } from \"./nano-states\";\nimport { insertPageCopyMutable } from \"./page-utils\";\nimport {\n  $,\n  expression,\n  Parameter,\n  renderData,\n  Variable,\n  ws,\n} from \"@webstudio-is/template\";\nimport { nanoid } from \"nanoid\";\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map((item) => [item.id, item]));\n\nconst getWebstudioDataStub = (\n  data?: Partial<WebstudioData>\n): WebstudioData => ({\n  pages: createDefaultPages({ rootInstanceId: \"\" }),\n  assets: new Map(),\n  dataSources: new Map(),\n  resources: new Map(),\n  instances: new Map(),\n  props: new Map(),\n  breakpoints: new Map(),\n  styleSourceSelections: new Map(),\n  styleSources: new Map(),\n  styles: new Map(),\n  ...data,\n});\n\ndescribe(\"insert page copy\", () => {\n  $project.set({ id: \"projectId\" } as Project);\n\n  test(\"insert home page copy with new path and ids\", () => {\n    const data = getWebstudioDataStub({\n      instances: toMap<Instance>([\n        { type: \"instance\", id: \"bodyId\", component: \"Body\", children: [] },\n      ]),\n      pages: {\n        meta: {},\n        homePage: {\n          id: \"pageId\",\n          name: \"Name\",\n          path: \"\",\n          title: `\"Title\"`,\n          meta: {},\n          rootInstanceId: \"bodyId\",\n        },\n        pages: [],\n        folders: [createRootFolder([\"pageId\"])],\n      },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.pages.pages.length).toEqual(1);\n    const newPage = data.pages.pages[0];\n    expect(newPage).toEqual({\n      id: expect.not.stringMatching(\"pageId\"),\n      name: \"Name (1)\",\n      path: \"/copy-1\",\n      title: `\"Title\"`,\n      meta: {},\n      rootInstanceId: expect.not.stringMatching(\"bodyId\"),\n    });\n    expect(data.instances.size).toEqual(2);\n    expect(Array.from(data.instances.values())[1]).toEqual({\n      type: \"instance\",\n      id: newPage.rootInstanceId,\n      component: \"Body\",\n      children: [],\n    });\n  });\n\n  test(\"deduplicate path for non-home page copy\", () => {\n    const data = getWebstudioDataStub({\n      instances: toMap<Instance>([\n        { type: \"instance\", id: \"bodyId\", component: \"Body\", children: [] },\n      ]),\n      pages: {\n        meta: {},\n        homePage: {\n          id: \"homePageId\",\n          name: \"Home\",\n          path: \"\",\n          title: `\"Home\"`,\n          meta: {},\n          rootInstanceId: \"bodyId\",\n        },\n        pages: [\n          {\n            id: \"pageId\",\n            name: \"Name\",\n            path: \"/my-path\",\n            title: `\"Title\"`,\n            meta: {},\n            rootInstanceId: \"bodyId\",\n          },\n        ],\n        folders: [createRootFolder([\"homePageId\", \"pageId\"])],\n      },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.pages.pages.length).toEqual(2);\n    const newPage = data.pages.pages[1];\n    expect(newPage.path).toEqual(\"/copy-1/my-path\");\n  });\n\n  test(\"deduplicate wildcards in page copy\", () => {\n    const data = getWebstudioDataStub({\n      instances: toMap<Instance>([\n        { type: \"instance\", id: \"bodyId\", component: \"Body\", children: [] },\n      ]),\n      pages: {\n        meta: {},\n        homePage: {\n          id: \"homePageId\",\n          name: \"Home\",\n          path: \"\",\n          title: `\"Home\"`,\n          meta: {},\n          rootInstanceId: \"bodyId\",\n        },\n        pages: [\n          {\n            id: \"page1Id\",\n            name: \"My Name 1\",\n            // unnamed wildcard\n            path: \"/my-path/*\",\n            title: `\"My Title\"`,\n            meta: {},\n            rootInstanceId: \"bodyId\",\n          },\n          {\n            id: \"page2Id\",\n            name: \"My Name 2\",\n            // Named wildcard\n            path: \"/my-path/name*\",\n            title: `\"My Title\"`,\n            meta: {},\n            rootInstanceId: \"bodyId\",\n          },\n        ],\n        folders: [createRootFolder([\"homePageId\", \"page1Id\", \"page2Id\"])],\n      },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"page1Id\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"page2Id\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.pages.pages.length).toEqual(4);\n    const newPage1 = data.pages.pages[2];\n    const newPage2 = data.pages.pages[3];\n    expect(newPage1.path).toEqual(\"/copy-1/my-path/*\");\n    expect(newPage2.path).toEqual(\"/copy-1/my-path/name*\");\n  });\n\n  test(\"check full page path when duplicate inside a folder\", () => {\n    const data = getWebstudioDataStub({\n      instances: toMap<Instance>([\n        { type: \"instance\", id: \"bodyId\", component: \"Body\", children: [] },\n      ]),\n      pages: {\n        meta: {},\n        homePage: {\n          id: \"homePageId\",\n          name: \"Home\",\n          path: \"\",\n          title: `\"Home\"`,\n          meta: {},\n          rootInstanceId: \"bodyId\",\n        },\n        pages: [\n          {\n            id: \"pageId\",\n            name: \"My Name\",\n            path: \"/my-path\",\n            title: `\"My Title\"`,\n            meta: {},\n            rootInstanceId: \"bodyId\",\n          },\n        ],\n        folders: [\n          createRootFolder([\"folderId\"]),\n          {\n            id: \"folderId\",\n            name: \"Folder\",\n            slug: \"folder\",\n            children: [\"pageId\"],\n          },\n        ],\n      },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: \"folderId\" },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.pages.pages.length).toEqual(3);\n    const nestedPage = data.pages.pages[1];\n    const rootPage = data.pages.pages[2];\n    expect(nestedPage.path).toEqual(\"/copy-1/my-path\");\n    expect(rootPage.path).toEqual(\"/my-path\");\n  });\n\n  test(\"replace variables in page copy meta\", () => {\n    const bodyVariable = new Variable(\"bodyVariable\", \"\");\n    const dataWithoutPage = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${bodyVariable}`}></$.Body>\n    );\n    const [variableId] = dataWithoutPage.dataSources.keys();\n    const variableIdentifier = encodeDataVariableId(variableId);\n    const data = getWebstudioDataStub({\n      ...dataWithoutPage,\n      pages: {\n        meta: {},\n        homePage: {\n          id: \"pageId\",\n          rootInstanceId: \"bodyId\",\n          name: \"Name\",\n          path: \"\",\n          title: `\"Title: \" + ${variableIdentifier}`,\n          meta: {\n            description: `\"Description: \" + ${variableIdentifier}`,\n            excludePageFromSearch: `\"Exclude: \" + ${variableIdentifier}`,\n            socialImageUrl: `\"Image: \" + ${variableIdentifier}`,\n            language: `\"Language: \" + ${variableIdentifier}`,\n            status: `\"Status: \" + ${variableIdentifier}`,\n            redirect: `\"Redirect: \" + ${variableIdentifier}`,\n            custom: [\n              {\n                property: \"Property\",\n                content: `\"Value: \" + ${variableIdentifier}`,\n              },\n            ],\n          },\n        },\n        pages: [],\n        folders: [createRootFolder([\"pageId\"])],\n      },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.pages.pages.length).toEqual(1);\n    const newPage = data.pages.pages[0];\n    const [_oldVariableId, newVariableId] = data.dataSources.keys();\n    const newVariableIdentifier = encodeDataVariableId(newVariableId);\n    expect(newPage).toEqual({\n      id: expect.not.stringMatching(\"pageId\"),\n      name: \"Name (1)\",\n      path: \"/copy-1\",\n      title: `\"Title: \" + ${newVariableIdentifier}`,\n      meta: {\n        description: `\"Description: \" + ${newVariableIdentifier}`,\n        excludePageFromSearch: `\"Exclude: \" + ${newVariableIdentifier}`,\n        socialImageUrl: `\"Image: \" + ${newVariableIdentifier}`,\n        language: `\"Language: \" + ${newVariableIdentifier}`,\n        status: `\"Status: \" + ${newVariableIdentifier}`,\n        redirect: `\"Redirect: \" + ${newVariableIdentifier}`,\n        custom: [\n          {\n            property: \"Property\",\n            content: `\"Value: \" + ${newVariableIdentifier}`,\n          },\n        ],\n      },\n      rootInstanceId: expect.not.stringMatching(\"bodyId\"),\n    });\n  });\n\n  test(\"append number to name only when conflict is found\", () => {\n    const data = getWebstudioDataStub({\n      instances: toMap<Instance>([\n        { type: \"instance\", id: \"bodyId\", component: \"Body\", children: [] },\n      ]),\n      pages: {\n        meta: {},\n        homePage: {\n          id: \"pageId\",\n          name: \"Name\",\n          path: \"\",\n          title: `\"Title\"`,\n          meta: {},\n          rootInstanceId: \"bodyId\",\n        },\n        pages: [],\n        folders: [\n          createRootFolder([\"pageId\", \"folderId\"]),\n          {\n            id: \"folderId\",\n            name: \"Folder\",\n            slug: \"folder\",\n            children: [],\n          },\n        ],\n      },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: \"folderId\" },\n    });\n    insertPageCopyMutable({\n      source: { data, pageId: \"pageId\" },\n      target: { data, folderId: \"folderId\" },\n    });\n    expect(data.pages.pages.length).toEqual(4);\n    // inside folder with conflict\n    expect(data.pages.pages[0].name).toEqual(`Name (1)`);\n    expect(data.pages.pages[1].name).toEqual(`Name (2)`);\n    // inside folder without conflict\n    expect(data.pages.pages[2].name).toEqual(`Name`);\n    expect(data.pages.pages[3].name).toEqual(`Name (1)`);\n  });\n\n  test(\"preserve global variables when duplicate page\", () => {\n    const globalVariable = new Variable(\"globalVariable\", \"global value\");\n    const data = {\n      pages: createDefaultPages({\n        rootInstanceId: \"bodyId\",\n      }),\n      ...renderData(\n        <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">{expression`${globalVariable}`}</$.Box>\n          </$.Body>\n        </ws.root>\n      ),\n    };\n    data.instances.delete(ROOT_INSTANCE_ID);\n    insertPageCopyMutable({\n      source: { data, pageId: data.pages.homePage.id },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.dataSources.size).toEqual(1);\n    const [globalVariableId] = data.dataSources.keys();\n    expect(Array.from(data.instances.values())).toEqual([\n      expect.objectContaining({ component: \"Body\", id: \"bodyId\" }),\n      expect.objectContaining({ component: \"Box\", id: \"boxId\" }),\n      expect.objectContaining({ component: \"Body\" }),\n      expect.objectContaining({ component: \"Box\" }),\n    ]);\n    const newBox = Array.from(data.instances.values()).at(-1);\n    expect(newBox?.children).toEqual([\n      { type: \"expression\", value: encodeDataVariableId(globalVariableId) },\n    ]);\n  });\n\n  test(\"preserve existing global variables by name\", () => {\n    const globalVariable = new Variable(\"globalVariable\", \"global value\");\n    const sourceData = {\n      pages: createDefaultPages({\n        rootInstanceId: \"bodyId\",\n      }),\n      ...renderData(\n        <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">{expression`${globalVariable}`}</$.Box>\n          </$.Body>\n        </ws.root>,\n        // generate different ids in source and data projects\n        nanoid\n      ),\n    };\n    sourceData.instances.delete(ROOT_INSTANCE_ID);\n    const targetData = {\n      pages: createDefaultPages({\n        rootInstanceId: \"anotherBodyId\",\n      }),\n      ...renderData(\n        <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n          <$.Body ws:id=\"anotherBodyId\"></$.Body>\n        </ws.root>,\n        // generate different ids in source and data projects\n        nanoid\n      ),\n    };\n    targetData.instances.delete(ROOT_INSTANCE_ID);\n    insertPageCopyMutable({\n      source: { data: sourceData, pageId: sourceData.pages.homePage.id },\n      target: { data: targetData, folderId: ROOT_FOLDER_ID },\n    });\n    expect(targetData.dataSources.size).toEqual(1);\n    const [globalVariableId] = targetData.dataSources.keys();\n    expect(Array.from(targetData.instances.values())).toEqual([\n      expect.objectContaining({ component: \"Body\", id: \"anotherBodyId\" }),\n      expect.objectContaining({ component: \"Body\" }),\n      expect.objectContaining({ component: \"Box\" }),\n    ]);\n    const newBox = Array.from(targetData.instances.values()).at(-1);\n    expect(newBox?.children).toEqual([\n      { type: \"expression\", value: encodeDataVariableId(globalVariableId) },\n    ]);\n  });\n\n  test(\"restore newly added global variable by name\", () => {\n    const globalVariable = new Variable(\"globalVariable\", \"global value\");\n    const sourceData = {\n      pages: createDefaultPages({\n        rootInstanceId: \"bodyId\",\n      }),\n      ...renderData(\n        <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${globalVariable}`}>\n          <$.Body ws:id=\"bodyId\">\n            <$.Box ws:id=\"boxId\">{expression`${globalVariable}`}</$.Box>\n          </$.Body>\n        </ws.root>,\n        // generate different ids in source and data projects\n        nanoid\n      ),\n    };\n    sourceData.instances.delete(ROOT_INSTANCE_ID);\n    const targetData = {\n      pages: createDefaultPages({\n        rootInstanceId: \"anotherBodyId\",\n      }),\n      // generate different ids in source and data projects\n      ...renderData(<$.Body ws:id=\"anotherBodyId\"></$.Body>, nanoid),\n    };\n    insertPageCopyMutable({\n      source: { data: sourceData, pageId: sourceData.pages.homePage.id },\n      target: { data: targetData, folderId: ROOT_FOLDER_ID },\n    });\n    expect(targetData.dataSources.size).toEqual(1);\n    const [globalVariableId] = targetData.dataSources.keys();\n    expect(Array.from(targetData.instances.values())).toEqual([\n      expect.objectContaining({ component: \"Body\", id: \"anotherBodyId\" }),\n      expect.objectContaining({ component: \"Body\" }),\n      expect.objectContaining({ component: \"Box\" }),\n    ]);\n    const newBox = Array.from(targetData.instances.values()).at(-1);\n    expect(newBox?.children).toEqual([\n      { type: \"expression\", value: encodeDataVariableId(globalVariableId) },\n    ]);\n  });\n\n  test(\"delete page system in favor of global one\", () => {\n    const pageSystemVariable = new Parameter(\"system\");\n    const dataWithoutPages = renderData(\n      <$.Body ws:id=\"bodyId\" vars={expression`${pageSystemVariable}`}>\n        <$.Box ws:id=\"boxId\">{expression`${pageSystemVariable}`}</$.Box>\n      </$.Body>\n    );\n    const [pageSystemVariableId] = dataWithoutPages.dataSources.keys();\n    const data = {\n      pages: createDefaultPages({\n        rootInstanceId: \"bodyId\",\n        systemDataSourceId: pageSystemVariableId,\n      }),\n      ...dataWithoutPages,\n    };\n    data.pages.homePage.title = `${encodeDataVariableId(pageSystemVariableId)}`;\n    data.pages.homePage.meta.description = `${encodeDataVariableId(pageSystemVariableId)}`;\n    insertPageCopyMutable({\n      source: { data, pageId: data.pages.homePage.id },\n      target: { data, folderId: ROOT_FOLDER_ID },\n    });\n    expect(data.dataSources.size).toEqual(1);\n    expect(Array.from(data.instances.values())).toEqual([\n      expect.objectContaining({ component: \"Body\", id: \"bodyId\" }),\n      expect.objectContaining({ component: \"Box\", id: \"boxId\" }),\n      expect.objectContaining({ component: \"Body\" }),\n      expect.objectContaining({ component: \"Box\" }),\n    ]);\n    const newBox = Array.from(data.instances.values()).at(-1);\n    expect(newBox?.children).toEqual([\n      { type: \"expression\", value: \"$ws$system\" },\n    ]);\n    expect(data.pages.pages[0].title).toEqual(`$ws$system`);\n    expect(data.pages.pages[0].meta.description).toEqual(`$ws$system`);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/page-utils.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport {\n  decodeDataSourceVariable,\n  encodeDataSourceVariable,\n  findPageByIdOrPath,\n  getPagePath,\n  transpileExpression,\n  type Folder,\n  type DataSource,\n  type Page,\n  type Pages,\n  type WebstudioData,\n  ROOT_INSTANCE_ID,\n} from \"@webstudio-is/sdk\";\nimport {\n  extractWebstudioFragment,\n  insertWebstudioFragmentCopy,\n  unwrap,\n} from \"./instance-utils\";\nimport {\n  findAvailableVariables,\n  restoreExpressionVariables,\n  unsetExpressionVariables,\n} from \"./data-variables\";\nimport { $project } from \"./nano-states\";\nimport type { ConflictResolution } from \"./token-conflict-dialog\";\n\nconst deduplicateName = (\n  pages: Pages,\n  folderId: Folder[\"id\"],\n  pageName: Page[\"name\"]\n) => {\n  const { name = pageName, copyNumber } =\n    // extract a number from \"name (copyNumber)\"\n    pageName.match(/^(?<name>.+) \\((?<copyNumber>\\d+)\\)$/)?.groups ?? {};\n  const folder = pages.folders.find((folder) => folder.id === folderId);\n  const usedNames = new Set<string>();\n  for (const pageId of folder?.children ?? []) {\n    const page = findPageByIdOrPath(pageId, pages);\n    if (page) {\n      usedNames.add(page.name);\n    }\n  }\n  let newName = name;\n  let nameNumber = Number(copyNumber ?? \"0\");\n  while (usedNames.has(newName)) {\n    nameNumber += 1;\n    newName = `${name} (${nameNumber})`;\n  }\n  return newName;\n};\n\nconst deduplicatePath = (\n  pages: Pages,\n  folderId: Folder[\"id\"],\n  path: Page[\"path\"]\n) => {\n  const folderPath = getPagePath(folderId, pages);\n  if (path === \"/\") {\n    path = \"\";\n  }\n  let matchedPage = findPageByIdOrPath(`${folderPath}${path}`, pages);\n  if (matchedPage === undefined) {\n    return path;\n  }\n  let counter = 0;\n  while (matchedPage) {\n    counter += 1;\n    matchedPage = findPageByIdOrPath(\n      `${folderPath}/copy-${counter}${path}`,\n      pages\n    );\n  }\n  return `/copy-${counter}${path}`;\n};\n\nconst replaceDataSources = (\n  expression: string,\n  replacements: Map<DataSource[\"id\"], DataSource[\"id\"]>\n) => {\n  return transpileExpression({\n    expression,\n    replaceVariable: (identifier) => {\n      const dataSourceId = decodeDataSourceVariable(identifier);\n      if (dataSourceId === undefined) {\n        return;\n      }\n      return encodeDataSourceVariable(\n        replacements.get(dataSourceId) ?? dataSourceId\n      );\n    },\n  });\n};\n\nexport const insertPageCopyMutable = ({\n  source,\n  target,\n  conflictResolution,\n}: {\n  source: { data: WebstudioData; pageId: Page[\"id\"] };\n  target: { data: WebstudioData; folderId: Folder[\"id\"] };\n  conflictResolution?: ConflictResolution;\n}) => {\n  const project = $project.get();\n  const page = findPageByIdOrPath(source.pageId, source.data.pages);\n  if (project === undefined || page === undefined) {\n    return;\n  }\n  // copy paste project :root\n  insertWebstudioFragmentCopy({\n    data: target.data,\n    fragment: extractWebstudioFragment(source.data, ROOT_INSTANCE_ID),\n    availableVariables: findAvailableVariables({\n      ...target.data,\n      startingInstanceId: ROOT_INSTANCE_ID,\n    }),\n    projectId: project.id,\n    conflictResolution,\n  });\n  const unsetVariables = new Set<DataSource[\"id\"]>();\n  const unsetNameById = new Map<DataSource[\"id\"], DataSource[\"name\"]>();\n  // replace legacy page system with global variable\n  if (page.systemDataSourceId) {\n    unsetVariables.add(page.systemDataSourceId);\n    unsetNameById.set(page.systemDataSourceId, \"system\");\n  }\n  const availableVariables = findAvailableVariables({\n    ...target.data,\n    startingInstanceId: ROOT_INSTANCE_ID,\n  });\n  const maskedIdByName = new Map<DataSource[\"name\"], DataSource[\"id\"]>();\n  for (const dataSource of availableVariables) {\n    maskedIdByName.set(dataSource.name, dataSource.id);\n  }\n  // copy paste page body\n  const { newInstanceIds, newDataSourceIds } = insertWebstudioFragmentCopy({\n    data: target.data,\n    fragment: extractWebstudioFragment(source.data, page.rootInstanceId, {\n      unsetVariables,\n    }),\n    availableVariables,\n    projectId: project.id,\n    conflictResolution,\n  });\n  // unwrap page draft\n  const newPage = structuredClone(unwrap(page));\n  newPage.id = nanoid();\n  delete newPage.systemDataSourceId;\n  newPage.rootInstanceId =\n    newInstanceIds.get(page.rootInstanceId) ?? page.rootInstanceId;\n  newPage.name = deduplicateName(target.data.pages, target.folderId, page.name);\n  newPage.path = deduplicatePath(target.data.pages, target.folderId, page.path);\n  const transformExpression = (expression: string) => {\n    // rebind expressions with from page system variable to global one\n    expression = unsetExpressionVariables({ expression, unsetNameById });\n    expression = restoreExpressionVariables({ expression, maskedIdByName });\n    expression = replaceDataSources(expression, newDataSourceIds);\n    return expression;\n  };\n  newPage.title = transformExpression(newPage.title);\n  if (newPage.meta.description !== undefined) {\n    newPage.meta.description = transformExpression(newPage.meta.description);\n  }\n  if (newPage.meta.excludePageFromSearch !== undefined) {\n    newPage.meta.excludePageFromSearch = transformExpression(\n      newPage.meta.excludePageFromSearch\n    );\n  }\n  if (newPage.meta.socialImageUrl !== undefined) {\n    newPage.meta.socialImageUrl = transformExpression(\n      newPage.meta.socialImageUrl\n    );\n  }\n  if (newPage.meta.language !== undefined) {\n    newPage.meta.language = transformExpression(newPage.meta.language);\n  }\n  if (newPage.meta.status !== undefined) {\n    newPage.meta.status = transformExpression(newPage.meta.status);\n  }\n  if (newPage.meta.redirect !== undefined) {\n    newPage.meta.redirect = transformExpression(newPage.meta.redirect);\n  }\n  if (newPage.meta.custom) {\n    newPage.meta.custom = newPage.meta.custom.map(({ property, content }) => ({\n      property,\n      content: transformExpression(content),\n    }));\n  }\n  target.data.pages.pages.push(newPage);\n  for (const folder of target.data.pages.folders) {\n    if (folder.id === target.folderId) {\n      folder.children.push(newPage.id);\n    }\n  }\n  return newPage.id;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/pages/index.ts",
    "content": "export * from \"./use-switch-page\";\n"
  },
  {
    "path": "apps/builder/app/shared/pages/use-switch-page.ts",
    "content": "import { useEffect } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport { useNavigate } from \"@remix-run/react\";\nimport {\n  $authToken,\n  $pages,\n  $project,\n  $selectedPageHash,\n  $builderMode,\n  isBuilderMode,\n  setBuilderMode,\n} from \"~/shared/nano-states\";\nimport { builderPath } from \"~/shared/router-utils\";\nimport { $selectedPage, selectPage } from \"../awareness\";\nimport { findPageByIdOrPath } from \"@webstudio-is/sdk\";\n\nconst setPageStateFromUrl = () => {\n  const searchParams = new URLSearchParams(window.location.search);\n  const pages = $pages.get();\n  if (pages === undefined) {\n    return;\n  }\n\n  let mode = searchParams.get(\"mode\");\n\n  // Check in case of BuilderMode rename\n  if (!isBuilderMode(mode)) {\n    mode = null;\n  }\n\n  setBuilderMode(mode);\n\n  // check the page actually exists\n  // to avoid confusing the user with broken state\n  const pageId =\n    findPageByIdOrPath(searchParams.get(\"pageId\") ?? \"\", pages)?.id ??\n    pages.homePage.id;\n\n  $selectedPageHash.set({ hash: searchParams.get(\"pageHash\") ?? \"\" });\n  selectPage(pageId);\n};\n\n/**\n * Sync\n *  - searchParams to atoms\n *    - initial loading\n *    - popstate\n *\n *  - atoms to searchParams\n *    - on atom change\n */\nexport const useSyncPageUrl = () => {\n  const navigate = useNavigate();\n  const page = useStore($selectedPage);\n  const pageHash = useStore($selectedPageHash);\n  const builderMode = useStore($builderMode);\n\n  // Get pageId and pageHash from URL\n  // once pages are loaded\n  useEffect(() => {\n    const unsubscribe = $pages.subscribe((pages) => {\n      if (pages) {\n        unsubscribe();\n        setPageStateFromUrl();\n      }\n    });\n    return unsubscribe;\n  }, []);\n\n  useEffect(() => {\n    window.addEventListener(\"popstate\", setPageStateFromUrl);\n    return () => {\n      window.removeEventListener(\"popstate\", setPageStateFromUrl);\n    };\n  }, []);\n\n  useEffect(() => {\n    const project = $project.get();\n    const pages = $pages.get();\n\n    if (page === undefined || project === undefined || pages === undefined) {\n      return;\n    }\n\n    const searchParams = new URLSearchParams(window.location.search);\n\n    const searchParamsPageId = searchParams.get(\"pageId\") ?? pages.homePage.id;\n    const searchParamsPageHash = searchParams.get(\"pageHash\") ?? \"\";\n    const searParamsModeRaw = searchParams.get(\"mode\");\n    const searParamsMode = isBuilderMode(searParamsModeRaw)\n      ? searParamsModeRaw\n      : undefined;\n    const searchParamsSafemode = searchParams.get(\"safemode\");\n\n    // Do not navigate on popstate change or if params match\n    if (\n      searchParamsPageId === page.id &&\n      searchParamsPageHash === pageHash.hash &&\n      searParamsMode === builderMode\n    ) {\n      return;\n    }\n\n    navigate(\n      builderPath({\n        pageId: page.id === pages.homePage.id ? undefined : page.id,\n        authToken: $authToken.get(),\n        pageHash: pageHash.hash === \"\" ? undefined : pageHash.hash,\n        mode: builderMode === \"design\" ? undefined : builderMode,\n        safemode: searchParamsSafemode === \"true\",\n      })\n    );\n  }, [builderMode, navigate, page, pageHash]);\n\n  useEffect(() => {\n    return $selectedPage.subscribe((page) => {\n      // switch to home page when current one does not exist\n      // possible when undo creating page\n      if (page === undefined) {\n        const pages = $pages.get();\n        if (pages) {\n          selectPage(pages.homePage.id);\n        }\n      }\n    });\n  });\n};\n\n/**\n * Synchronize pageHash with scrolling position\n */\nexport const useHashLinkSync = () => {\n  const pageHash = useStore($selectedPageHash);\n\n  useEffect(() => {\n    if (pageHash.hash === \"\") {\n      // native browser behavior is to do nothing if hash is empty\n      // remix scroll to top, we emulate native\n      return;\n    }\n\n    let elementId = decodeURIComponent(pageHash.hash);\n    if (elementId.startsWith(\"#\")) {\n      elementId = elementId.slice(1);\n    }\n\n    // Try find element to scroll to\n    const element = document.getElementById(elementId);\n    if (element !== null) {\n      element.scrollIntoView();\n    }\n\n    // Remix scroll to top if element not found\n    // browser do nothing\n  }, [pageHash]);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/image-control.tsx",
    "content": "import type { ReactElement } from \"react\";\nimport { FloatingPanel } from \"@webstudio-is/design-system\";\nimport { AssetManager } from \"~/builder/shared/asset-manager\";\nimport { AssetUpload } from \"~/builder/shared/assets\";\n\n// @todo should be moved to shared as its being reused in another feature\nexport const ImageControl = (props: {\n  onAssetIdChange: (assetId: string) => void;\n  children: ReactElement;\n}) => {\n  return (\n    <FloatingPanel\n      title=\"Images\"\n      titleSuffix={<AssetUpload type=\"image\" accept=\"image/*\" />}\n      placement=\"bottom-within\"\n      content={\n        <AssetManager\n          accept=\"image/*\"\n          onChange={(assetId) => {\n            props.onAssetIdChange(assetId);\n          }}\n        />\n      }\n    >\n      {props.children}\n    </FloatingPanel>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/import-redirects-dialog.stories.tsx",
    "content": "import { StorySection } from \"@webstudio-is/design-system\";\nimport { ImportRedirectsDialog as ImportRedirectsDialogComponent } from \"./import-redirects-dialog\";\n\nexport default {\n  title: \"Builder/Project settings/Import Redirects Dialog\",\n  component: ImportRedirectsDialogComponent,\n};\n\nexport const ImportRedirectsDialog = () => (\n  <StorySection title=\"Import Redirects Dialog\">\n    <ImportRedirectsDialogComponent\n      isOpen={true}\n      onOpenChange={() => {}}\n      existingRedirects={[\n        { old: \"/old-page\", new: \"/new-page\", status: \"301\" },\n        { old: \"/blog/2023\", new: \"/blog/archive\", status: \"302\" },\n      ]}\n      onImport={() => {}}\n    />\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/import-redirects-dialog.tsx",
    "content": "import { useRef, useState, type DragEvent, type ChangeEvent } from \"react\";\nimport {\n  Box,\n  Button,\n  Dialog,\n  DialogActions,\n  DialogClose,\n  DialogContent,\n  DialogTitle,\n  Flex,\n  Grid,\n  Label,\n  Radio,\n  RadioAndLabel,\n  RadioGroup,\n  ScrollArea,\n  Text,\n  TextArea,\n  theme,\n  toast,\n} from \"@webstudio-is/design-system\";\nimport { UploadIcon } from \"@webstudio-is/icons\";\nimport type { PageRedirect } from \"@webstudio-is/sdk\";\nimport {\n  parseRedirects,\n  type ParsedRedirect,\n  type SkippedLine,\n} from \"~/shared/redirects/redirect-parsers\";\nimport { detectLoopsInBatch } from \"~/shared/redirects/redirect-loop-detection\";\n\ntype ImportStep = \"input\" | \"preview\";\ntype MergeMode = \"add\" | \"replace\";\n\ntype ImportRedirectsDialogProps = {\n  isOpen: boolean;\n  onOpenChange: (isOpen: boolean) => void;\n  existingRedirects: PageRedirect[];\n  onImport: (redirects: PageRedirect[], mode: MergeMode) => void;\n};\n\nconst ACCEPTED_EXTENSIONS = [\".csv\", \".json\", \".txt\", \".htaccess\"];\n\nconst formatSupportsText = `Supports: CSV, JSON, Netlify _redirects, Apache .htaccess`;\n\nexport const ImportRedirectsDialog = ({\n  isOpen,\n  onOpenChange,\n  existingRedirects,\n  onImport,\n}: ImportRedirectsDialogProps) => {\n  const [step, setStep] = useState<ImportStep>(\"input\");\n  const [textContent, setTextContent] = useState(\"\");\n  const [fileName, setFileName] = useState<string | null>(null);\n  const [isDragOver, setIsDragOver] = useState(false);\n  const [parsedRedirects, setParsedRedirects] = useState<ParsedRedirect[]>([]);\n  const [skippedLines, setSkippedLines] = useState<SkippedLine[]>([]);\n  const [mergeMode, setMergeMode] = useState<MergeMode>(\"add\");\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  const resetState = () => {\n    setStep(\"input\");\n    setTextContent(\"\");\n    setFileName(null);\n    setParsedRedirects([]);\n    setSkippedLines([]);\n    setMergeMode(\"add\");\n  };\n\n  const handleOpenChange = (open: boolean) => {\n    if (!open) {\n      resetState();\n    }\n    onOpenChange(open);\n  };\n\n  const handleFileRead = (content: string, name: string) => {\n    setTextContent(content);\n    setFileName(name);\n  };\n\n  const handleFileSelect = (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (file) {\n      const reader = new FileReader();\n      reader.onload = (e) => {\n        const content = e.target?.result as string;\n        handleFileRead(content, file.name);\n      };\n      reader.readAsText(file);\n    }\n    // Reset input so same file can be selected again\n    event.target.value = \"\";\n  };\n\n  const handleDragOver = (event: DragEvent<HTMLDivElement>) => {\n    event.preventDefault();\n    setIsDragOver(true);\n  };\n\n  const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {\n    event.preventDefault();\n    setIsDragOver(false);\n  };\n\n  const handleDrop = (event: DragEvent<HTMLDivElement>) => {\n    event.preventDefault();\n    setIsDragOver(false);\n\n    const file = event.dataTransfer.files[0];\n    if (file) {\n      const reader = new FileReader();\n      reader.onload = (e) => {\n        const content = e.target?.result as string;\n        handleFileRead(content, file.name);\n      };\n      reader.readAsText(file);\n    }\n  };\n\n  const handleParse = () => {\n    if (!textContent.trim()) {\n      toast.error(\"Please upload a file or paste redirect content\");\n      return;\n    }\n\n    const result = parseRedirects(textContent);\n    setParsedRedirects(result.redirects);\n    setSkippedLines(result.skipped);\n\n    if (result.redirects.length === 0 && result.skipped.length === 0) {\n      toast.error(\"No redirects found in the provided content\");\n      return;\n    }\n\n    if (result.redirects.length === 0) {\n      toast.error(\n        `No valid redirects found. ${result.skipped.length} line(s) were skipped.`\n      );\n      return;\n    }\n\n    setStep(\"preview\");\n  };\n\n  const handleImport = () => {\n    // Convert parsed redirects to PageRedirect format\n    const newRedirects: PageRedirect[] = parsedRedirects.map((r) => ({\n      old: r.old,\n      new: r.new,\n      status: r.status === 301 ? \"301\" : \"302\",\n    }));\n\n    // Find duplicates with existing redirects\n    const existingPaths = new Set(existingRedirects.map((r) => r.old));\n    const duplicates = newRedirects.filter((r) => existingPaths.has(r.old));\n    const uniqueNew = newRedirects.filter((r) => !existingPaths.has(r.old));\n\n    if (mergeMode === \"add\") {\n      // Detect loops with existing redirects\n      const { valid, looped } = detectLoopsInBatch(\n        uniqueNew,\n        existingRedirects\n      );\n      const loopCount = looped.length;\n\n      onImport(valid, \"add\");\n      const skippedParts = [\n        duplicates.length > 0 &&\n          `${duplicates.length} duplicate${duplicates.length !== 1 ? \"s\" : \"\"}`,\n        loopCount > 0 && `${loopCount} loop${loopCount !== 1 ? \"s\" : \"\"}`,\n      ].filter(Boolean);\n      const skippedMessage =\n        skippedParts.length > 0 ? ` (${skippedParts.join(\", \")} skipped)` : \"\";\n      toast.success(\n        `Imported ${valid.length} redirect${valid.length !== 1 ? \"s\" : \"\"}${skippedMessage}`\n      );\n    } else {\n      // Replace all - detect loops within the new set\n      const { valid, looped } = detectLoopsInBatch(newRedirects, []);\n      const loopCount = looped.length;\n\n      onImport(valid, \"replace\");\n      const skippedMessage =\n        loopCount > 0\n          ? ` (${loopCount} loop${loopCount !== 1 ? \"s\" : \"\"} skipped)`\n          : \"\";\n      toast.success(\n        `Replaced all redirects with ${valid.length} new redirect${valid.length !== 1 ? \"s\" : \"\"}${skippedMessage}`\n      );\n    }\n\n    handleOpenChange(false);\n  };\n\n  // Count duplicates for display\n  const existingPaths = new Set(existingRedirects.map((r) => r.old));\n  const duplicateRedirects = parsedRedirects.filter((r) =>\n    existingPaths.has(r.old)\n  );\n  const duplicateCount = duplicateRedirects.length;\n  const uniqueCount = parsedRedirects.length - duplicateCount;\n\n  return (\n    <Dialog open={isOpen} onOpenChange={handleOpenChange}>\n      <DialogContent css={{ width: 520, maxHeight: \"80vh\" }}>\n        <ScrollArea>\n          <Flex\n            direction=\"column\"\n            css={{\n              padding: theme.panel.padding,\n            }}\n            gap=\"3\"\n          >\n            {step === \"input\" && (\n              <InputStep\n                textContent={textContent}\n                fileName={fileName}\n                isDragOver={isDragOver}\n                fileInputRef={fileInputRef}\n                onTextChange={setTextContent}\n                onFileSelect={handleFileSelect}\n                onDragOver={handleDragOver}\n                onDragLeave={handleDragLeave}\n                onDrop={handleDrop}\n                onUploadClick={() => fileInputRef.current?.click()}\n              />\n            )}\n\n            {step === \"preview\" && (\n              <PreviewStep\n                parsedRedirects={parsedRedirects}\n                skippedLines={skippedLines}\n                duplicateRedirects={duplicateRedirects}\n                duplicateCount={duplicateCount}\n                uniqueCount={uniqueCount}\n                mergeMode={mergeMode}\n                existingRedirectsCount={existingRedirects.length}\n                onMergeModeChange={setMergeMode}\n              />\n            )}\n          </Flex>\n        </ScrollArea>\n\n        <DialogActions>\n          {step === \"input\" && (\n            <>\n              <Button onClick={handleParse} disabled={!textContent.trim()}>\n                Parse\n              </Button>\n              <DialogClose>\n                <Button color=\"ghost\">Cancel</Button>\n              </DialogClose>\n            </>\n          )}\n\n          {step === \"preview\" && (\n            <>\n              <Button\n                onClick={handleImport}\n                disabled={parsedRedirects.length === 0}\n              >\n                Import\n              </Button>\n              <DialogClose>\n                <Button color=\"ghost\">Cancel</Button>\n              </DialogClose>\n            </>\n          )}\n        </DialogActions>\n\n        <DialogTitle>Import redirects</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\n// Input Step Component\nconst InputStep = ({\n  textContent,\n  fileName,\n  isDragOver,\n  fileInputRef,\n  onTextChange,\n  onFileSelect,\n  onDragOver,\n  onDragLeave,\n  onDrop,\n  onUploadClick,\n}: {\n  textContent: string;\n  fileName: string | null;\n  isDragOver: boolean;\n  fileInputRef: React.RefObject<HTMLInputElement>;\n  onTextChange: (value: string) => void;\n  onFileSelect: (event: ChangeEvent<HTMLInputElement>) => void;\n  onDragOver: (event: DragEvent<HTMLDivElement>) => void;\n  onDragLeave: (event: DragEvent<HTMLDivElement>) => void;\n  onDrop: (event: DragEvent<HTMLDivElement>) => void;\n  onUploadClick: () => void;\n}) => {\n  return (\n    <>\n      <Text color=\"subtle\">{formatSupportsText}</Text>\n\n      {/* Hidden file input */}\n      <input\n        ref={fileInputRef}\n        type=\"file\"\n        accept={ACCEPTED_EXTENSIONS.join(\",\")}\n        onChange={onFileSelect}\n        style={{ display: \"none\" }}\n      />\n\n      {/* Drop zone */}\n      <Box\n        onDragOver={onDragOver}\n        onDragLeave={onDragLeave}\n        onDrop={onDrop}\n        css={{\n          border: `2px dashed ${isDragOver ? theme.colors.borderFocus : theme.colors.borderMain}`,\n          borderRadius: theme.borderRadius[6],\n          padding: theme.spacing[9],\n          textAlign: \"center\",\n          backgroundColor: isDragOver\n            ? theme.colors.backgroundHover\n            : \"transparent\",\n          transition: \"all 0.2s ease\",\n          cursor: \"pointer\",\n        }}\n        onClick={onUploadClick}\n      >\n        <Flex direction=\"column\" align=\"center\" gap=\"2\">\n          <UploadIcon size={24} />\n          <Text>{fileName ? fileName : \"Upload file or drag & drop\"}</Text>\n          <Text color=\"subtle\">{ACCEPTED_EXTENSIONS.join(\", \")}</Text>\n        </Flex>\n      </Box>\n\n      <Flex align=\"center\" gap=\"2\">\n        <Box\n          css={{ flex: 1, height: 1, backgroundColor: theme.colors.borderMain }}\n        />\n        <Text color=\"subtle\">OR</Text>\n        <Box\n          css={{ flex: 1, height: 1, backgroundColor: theme.colors.borderMain }}\n        />\n      </Flex>\n\n      <Grid gap=\"1\">\n        <Label>Paste content</Label>\n        <TextArea\n          rows={6}\n          maxRows={10}\n          autoGrow\n          value={textContent}\n          onChange={onTextChange}\n          placeholder={`/old-path,/new-path,301\\n/about-us,/about,301`}\n        />\n      </Grid>\n    </>\n  );\n};\n\n// Preview Step Component\nconst PreviewStep = ({\n  parsedRedirects,\n  skippedLines,\n  duplicateRedirects,\n  duplicateCount,\n  uniqueCount,\n  mergeMode,\n  existingRedirectsCount,\n  onMergeModeChange,\n}: {\n  parsedRedirects: ParsedRedirect[];\n  skippedLines: SkippedLine[];\n  duplicateRedirects: ParsedRedirect[];\n  duplicateCount: number;\n  uniqueCount: number;\n  mergeMode: MergeMode;\n  existingRedirectsCount: number;\n  onMergeModeChange: (mode: MergeMode) => void;\n}) => {\n  return (\n    <>\n      {/* Preview list */}\n      <Grid gap=\"1\">\n        <Label>Preview ({parsedRedirects.length})</Label>\n\n        <ScrollArea\n          css={{\n            border: `1px solid ${theme.colors.borderMain}`,\n            borderRadius: theme.borderRadius[4],\n            maxHeight: 200,\n            overflow: \"auto\",\n          }}\n        >\n          <Flex direction=\"column\" css={{ padding: theme.spacing[3] }}>\n            {parsedRedirects.map((redirect, index) => (\n              <Flex\n                key={index}\n                gap=\"2\"\n                align=\"center\"\n                css={{\n                  paddingBlock: theme.spacing[1],\n                }}\n              >\n                <Text\n                  truncate\n                  css={{\n                    flex: 1,\n                    minWidth: 0,\n                  }}\n                >\n                  {redirect.old}\n                </Text>\n                <Text color=\"subtle\" css={{ flexShrink: 0 }}>\n                  {redirect.status}\n                </Text>\n                <Text color=\"subtle\" css={{ flexShrink: 0 }}>\n                  →\n                </Text>\n                <Text\n                  truncate\n                  css={{\n                    flex: 1,\n                    minWidth: 0,\n                  }}\n                >\n                  {redirect.new}\n                </Text>\n              </Flex>\n            ))}\n          </Flex>\n        </ScrollArea>\n      </Grid>\n\n      {/* Unsupported lines */}\n      {skippedLines.length > 0 && (\n        <Grid gap=\"1\">\n          <Label>Unsupported ({skippedLines.length})</Label>\n          <ScrollArea\n            css={{\n              border: `1px solid ${theme.colors.borderMain}`,\n              borderRadius: theme.borderRadius[4],\n              maxHeight: 120,\n              overflow: \"auto\",\n            }}\n          >\n            <Flex direction=\"column\" css={{ padding: theme.spacing[3] }}>\n              {skippedLines.map((skipped, index) => (\n                <Flex\n                  key={index}\n                  direction=\"column\"\n                  css={{ paddingBlock: theme.spacing[1] }}\n                >\n                  <Text color=\"subtle\">\n                    Line {skipped.line}: {skipped.reason}\n                  </Text>\n                  <Text truncate>{skipped.content}</Text>\n                </Flex>\n              ))}\n            </Flex>\n          </ScrollArea>\n        </Grid>\n      )}\n\n      {/* Duplicates */}\n      {duplicateRedirects.length > 0 && (\n        <Grid gap=\"1\">\n          <Label>Duplicates ({duplicateRedirects.length})</Label>\n          <ScrollArea\n            css={{\n              border: `1px solid ${theme.colors.borderMain}`,\n              borderRadius: theme.borderRadius[4],\n              maxHeight: 120,\n              overflow: \"auto\",\n            }}\n          >\n            <Flex direction=\"column\" css={{ padding: theme.spacing[3] }}>\n              {duplicateRedirects.map((redirect, index) => (\n                <Flex\n                  key={index}\n                  gap=\"2\"\n                  align=\"center\"\n                  css={{\n                    paddingBlock: theme.spacing[1],\n                  }}\n                >\n                  <Text\n                    truncate\n                    css={{\n                      flex: 1,\n                      minWidth: 0,\n                    }}\n                  >\n                    {redirect.old}\n                  </Text>\n                  <Text color=\"subtle\" css={{ flexShrink: 0 }}>\n                    {redirect.status}\n                  </Text>\n                  <Text color=\"subtle\" css={{ flexShrink: 0 }}>\n                    →\n                  </Text>\n                  <Text\n                    truncate\n                    css={{\n                      flex: 1,\n                      minWidth: 0,\n                    }}\n                  >\n                    {redirect.new}\n                  </Text>\n                </Flex>\n              ))}\n            </Flex>\n          </ScrollArea>\n        </Grid>\n      )}\n\n      {/* Merge options */}\n      {existingRedirectsCount > 0 && (\n        <Grid gap=\"2\">\n          <Label>Import mode</Label>\n          <RadioGroup\n            value={mergeMode}\n            onValueChange={(value) => onMergeModeChange(value as MergeMode)}\n          >\n            <Flex direction=\"column\" gap=\"2\">\n              <RadioAndLabel>\n                <Radio value=\"add\" id=\"import-mode-add\" />\n                <Label htmlFor=\"import-mode-add\">\n                  Add to existing ({uniqueCount} new\n                  {duplicateCount > 0\n                    ? `, ${duplicateCount} duplicate${duplicateCount !== 1 ? \"s\" : \"\"} skipped`\n                    : \"\"}\n                  )\n                </Label>\n              </RadioAndLabel>\n              <RadioAndLabel>\n                <Radio value=\"replace\" id=\"import-mode-replace\" />\n                <Label htmlFor=\"import-mode-replace\">\n                  Replace all ({parsedRedirects.length} total, removes{\" \"}\n                  {existingRedirectsCount} existing)\n                </Label>\n              </RadioAndLabel>\n            </Flex>\n          </RadioGroup>\n        </Grid>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/index.ts",
    "content": "export * from \"./project-settings\";\nexport * from \"./image-control\";\nexport type { SectionName } from \"~/shared/nano-states/project-settings\";\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/project-settings.stories.tsx",
    "content": "import type { JSX } from \"react\";\nimport { createBrowserRouter, RouterProvider } from \"react-router-dom\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { ProjectSettingsDialog } from \"./project-settings\";\nimport { $pages, $project } from \"~/shared/sync/data-stores\";\nimport type { Project } from \"@webstudio-is/project\";\n\nexport default {\n  title: \"Project settings\",\n  component: ProjectSettingsDialog,\n};\n\nconst createRouter = (element: JSX.Element) =>\n  createBrowserRouter([\n    {\n      path: \"*\",\n      element,\n      loader: () => null,\n    },\n  ]);\n\n$project.set({ id: \"projectId\" } as Project);\n\nexport const General = () => {\n  const router = createRouter(\n    <ProjectSettingsDialog currentSection=\"general\" />\n  );\n  return (\n    <StorySection title=\"General\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n\nexport const Redirects = () => {\n  $pages.set({\n    homePage: {\n      id: \"pageId\",\n      name: \"My Name\",\n      path: \"\",\n      title: `\"My Title\"`,\n      meta: {},\n      rootInstanceId: \"body\",\n    },\n    pages: [],\n    folders: [],\n    redirects: [\n      { old: \"/old\", new: \"/new\" },\n      { old: \"/old\", new: \"https://google.com\" },\n      {\n        old: \"/oldddddddddddd/ddddddddddd/dddddddd/dddddd\",\n        new: \"https://gooooooooooooooooooooooooooooooooooooogle.com\",\n        status: \"302\",\n      },\n    ],\n  });\n\n  const router = createRouter(\n    <ProjectSettingsDialog currentSection=\"redirects\" />\n  );\n  return (\n    <StorySection title=\"Redirects\">\n      <RouterProvider router={router} />\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/project-settings.tsx",
    "content": "import type { FunctionComponent } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  Grid,\n  theme,\n  ScrollArea,\n  Flex,\n  List,\n  ListItem,\n  Text,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport { SpinnerIcon } from \"@webstudio-is/icons\";\nimport {\n  $openProjectSettings,\n  type SectionName,\n} from \"~/shared/nano-states/project-settings\";\nimport { $isDesignMode } from \"~/shared/nano-states\";\nimport { leftPanelWidth, rightPanelWidth } from \"./utils\";\nimport { SectionGeneral } from \"./section-general\";\nimport { SectionRedirects } from \"./section-redirects\";\nimport { SectionPublish } from \"./section-publish\";\nimport { SectionMarketplace } from \"./section-marketplace\";\nimport { SectionBackups } from \"./section-backups\";\nimport { titleCase } from \"title-case\";\n\nconst sections = new Map<\n  SectionName,\n  FunctionComponent<{ projectId?: string }>\n>([\n  [\"general\", SectionGeneral],\n  [\"redirects\", SectionRedirects],\n  [\"publish\", SectionPublish],\n  [\"marketplace\", SectionMarketplace],\n  [\"backups\", SectionBackups],\n] as const);\n\nexport const ProjectSettingsDialog = ({\n  currentSection,\n  onSectionChange,\n  onOpenChange,\n  projectId,\n  status = \"loaded\",\n}: {\n  currentSection?: SectionName;\n  onSectionChange?: (section: SectionName) => void;\n  onOpenChange?: (isOpen: boolean) => void;\n  projectId?: string;\n  status?: \"idle\" | \"loading\" | \"loaded\";\n}) => {\n  const isDesignMode = useStore($isDesignMode);\n  const SectionComponent = currentSection\n    ? sections.get(currentSection)\n    : undefined;\n\n  return (\n    <Dialog\n      draggable\n      open={sections.has(currentSection!)}\n      onOpenChange={onOpenChange}\n    >\n      <DialogContent\n        width={\n          Number.parseInt(leftPanelWidth, 10) +\n          Number.parseInt(rightPanelWidth, 10)\n        }\n        height={Number.parseInt(rawTheme.spacing[35], 10)}\n        data-floating-panel-container\n      >\n        <fieldset style={{ display: \"contents\" }} disabled={!isDesignMode}>\n          <Flex grow>\n            <List asChild>\n              <Flex\n                direction=\"column\"\n                shrink={false}\n                css={{\n                  width: leftPanelWidth,\n                  borderRight: `1px solid  ${theme.colors.borderMain}`,\n                }}\n              >\n                {Array.from(sections.keys()).map((name, index) => {\n                  return (\n                    <ListItem\n                      current={currentSection === name}\n                      asChild\n                      index={index}\n                      key={name}\n                      onSelect={() => {\n                        onSectionChange?.(name);\n                      }}\n                    >\n                      <Flex\n                        css={{\n                          position: \"relative\",\n                          height: theme.spacing[13],\n                          paddingInline: theme.panel.paddingInline,\n                          outline: \"none\",\n                          \"&:focus-visible, &:hover\": {\n                            background: theme.colors.backgroundHover,\n                          },\n                          \"&[aria-current=true]\": {\n                            background: theme.colors.backgroundItemCurrent,\n                            color: theme.colors.foregroundMain,\n                          },\n                        }}\n                        align=\"center\"\n                      >\n                        <Text variant=\"labels\" truncate>\n                          {titleCase(name)}\n                        </Text>\n                      </Flex>\n                    </ListItem>\n                  );\n                })}\n              </Flex>\n            </List>\n            <ScrollArea css={{ width: \"100%\" }}>\n              {status === \"loading\" ? (\n                <Flex justify=\"center\" align=\"center\" css={{ minHeight: 400 }}>\n                  <SpinnerIcon size={rawTheme.spacing[15]} />\n                </Flex>\n              ) : (\n                <Grid gap={2} css={{ paddingBlock: theme.spacing[5] }}>\n                  {SectionComponent && (\n                    <SectionComponent projectId={projectId} />\n                  )}\n                </Grid>\n              )}\n            </ScrollArea>\n          </Flex>\n          {/* Title is at the end intentionally,\n           * to make the close button last in the tab order\n           */}\n          <DialogTitle>Project settings</DialogTitle>\n        </fieldset>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nexport const ProjectSettings = () => {\n  const currentSection = useStore($openProjectSettings);\n\n  return (\n    <ProjectSettingsDialog\n      currentSection={currentSection}\n      onSectionChange={$openProjectSettings.set}\n      onOpenChange={(open) => {\n        $openProjectSettings.set(open ? \"general\" : undefined);\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/section-backups.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport { useEffect, useState } from \"react\";\nimport {\n  Grid,\n  Text,\n  Button,\n  Select,\n  Dialog,\n  DialogTitle,\n  DialogTrigger,\n  DialogContent,\n  DialogClose,\n  Flex,\n  theme,\n  toast,\n  PanelBanner,\n  Link,\n  rawTheme,\n} from \"@webstudio-is/design-system\";\nimport { UpgradeIcon } from \"@webstudio-is/icons\";\nimport { nativeClient, trpcClient } from \"~/shared/trpc/trpc-client\";\nimport { $project } from \"~/shared/sync/data-stores\";\nimport { $userPlanFeatures } from \"~/shared/nano-states\";\nimport { sectionSpacing } from \"./utils\";\nimport cmsUpgradeBanner from \"../cms-upgrade-banner.svg?url\";\n\nconst formatPublishDate = (date: string) => {\n  try {\n    const formatter = new Intl.DateTimeFormat(\"en\", {\n      dateStyle: \"long\",\n      timeStyle: \"short\",\n    });\n    return formatter.format(new Date(date));\n  } catch {\n    return date;\n  }\n};\n\nexport const SectionBackups = ({\n  projectId: projectIdProp,\n}: {\n  projectId?: string;\n}) => {\n  const userPlanFeatures = useStore($userPlanFeatures);\n  const hasPaidPlan = userPlanFeatures.purchases.length > 0;\n  const { data, load } = trpcClient.project.publishedBuilds.useQuery();\n  const project = useStore($project);\n  const projectId = projectIdProp ?? project?.id ?? \"\";\n\n  useEffect(() => {\n    load({ projectId });\n  }, [load, projectId]);\n  const options = data?.success ? data.data : [];\n  const [backupBuild = options.at(0), setBackupBuild] = useState<\n    undefined | (typeof options)[number]\n  >();\n  const restore = async () => {\n    if (!backupBuild?.buildId) {\n      return;\n    }\n    const result = await nativeClient.project.restoreDevelopmentBuild.mutate({\n      projectId,\n      fromBuildId: backupBuild.buildId,\n    });\n    if (result.success) {\n      location.reload();\n      return;\n    }\n    toast.error(result.error);\n  };\n\n  return (\n    <Grid gap={2} css={sectionSpacing}>\n      <Text variant=\"titles\">Backups</Text>\n      <Select\n        placeholder=\"No backups\"\n        options={options}\n        getValue={(option) => option.buildId ?? \"\"}\n        getLabel={(option) => {\n          if (!option.createdAt) {\n            return;\n          }\n          let label = formatPublishDate(option.createdAt);\n          if (option.domains) {\n            label += ` (${option.domains})`;\n          }\n          return label;\n        }}\n        value={backupBuild}\n        onChange={setBackupBuild}\n      />\n      <Dialog>\n        <DialogTrigger asChild>\n          <Button\n            css={{ justifySelf: \"start\" }}\n            disabled={!hasPaidPlan || options.length === 0}\n          >\n            Restore\n          </Button>\n        </DialogTrigger>\n        <DialogContent width={320}>\n          <DialogTitle>Restore published version</DialogTitle>\n          <Flex\n            direction=\"column\"\n            css={{ padding: theme.panel.padding }}\n            gap={2}\n          >\n            <Text>\n              Are you sure you want to restore the project to its published\n              version?\n            </Text>\n            {backupBuild?.createdAt && (\n              <Text color=\"destructive\">\n                All changes made after{\" \"}\n                {formatPublishDate(backupBuild.createdAt)} will be lost.\n              </Text>\n            )}\n            <Flex gap=\"2\" justify=\"end\">\n              <DialogClose>\n                <Button color=\"ghost\">Cancel</Button>\n              </DialogClose>\n              <DialogClose>\n                <Button color=\"destructive\" onClick={restore}>\n                  Restore\n                </Button>\n              </DialogClose>\n            </Flex>\n          </Flex>\n        </DialogContent>\n      </Dialog>\n      {!hasPaidPlan && (\n        <PanelBanner>\n          <img\n            src={cmsUpgradeBanner}\n            alt=\"Upgrade for backups\"\n            width={rawTheme.spacing[28]}\n            style={{ aspectRatio: \"4.1\" }}\n          />\n          <Text variant=\"regularBold\">Upgrade to restore from backups</Text>\n          <Flex align=\"center\" gap={1}>\n            <UpgradeIcon />\n            <Link\n              color=\"inherit\"\n              target=\"_blank\"\n              href=\"https://webstudio.is/pricing\"\n            >\n              Upgrade to Pro\n            </Link>\n          </Flex>\n        </PanelBanner>\n      )}\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/section-general.tsx",
    "content": "import { z } from \"zod\";\nimport { useId, useState, useEffect } from \"react\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Grid,\n  InputField,\n  Label,\n  theme,\n  Text,\n  Separator,\n  Button,\n  css,\n  Flex,\n  Tooltip,\n  InputErrorsTooltip,\n  ProBadge,\n  TextArea,\n  IconButton,\n} from \"@webstudio-is/design-system\";\nimport { CopyIcon, InfoCircleIcon } from \"@webstudio-is/icons\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport type { ProjectMeta } from \"@webstudio-is/sdk\";\nimport { ImageControl } from \"./image-control\";\nimport {\n  $assets,\n  $pages,\n  $project,\n  $userPlanFeatures,\n} from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { sectionSpacing } from \"./utils\";\nimport { CodeEditor } from \"~/shared/code-editor\";\nimport { CopyToClipboard } from \"~/shared/copy-to-clipboard\";\n\nconst imgStyle = css({\n  objectFit: \"contain\",\n  width: 72,\n  height: 72,\n  borderRadius: theme.borderRadius[4],\n  borderWidth: 1,\n  borderStyle: \"solid\",\n  borderColor: theme.colors.borderMain,\n});\n\nconst defaultMetaSettings: ProjectMeta = {\n  siteName: \"\",\n  contactEmail: \"\",\n  faviconAssetId: \"\",\n  code: \"\",\n};\n\nconst Email = z.string().email();\n\nconst validateContactEmail = (\n  contactEmail: string,\n  maxContactEmails: number\n) => {\n  contactEmail = contactEmail.trim();\n  if (contactEmail.length === 0) {\n    return;\n  }\n  const emails = contactEmail.split(/\\s*,\\s*/);\n  if (emails.length > maxContactEmails) {\n    if (maxContactEmails === 0) {\n      return `Upgrade to PRO to customize the contact email.`;\n    }\n    return `Only ${maxContactEmails} emails are allowed.`;\n  }\n  if (emails.every((email) => Email.safeParse(email).success) === false) {\n    return \"Contact email is invalid.\";\n  }\n};\n\nconst saveSetting = <Name extends keyof ProjectMeta>(\n  name: keyof ProjectMeta,\n  value: ProjectMeta[Name]\n) => {\n  serverSyncStore.createTransaction([$pages], (pages) => {\n    if (pages === undefined) {\n      return;\n    }\n    if (pages.meta === undefined) {\n      pages.meta = {};\n    }\n    pages.meta[name] = value;\n  });\n};\n\nexport const SectionGeneral = ({ projectId }: { projectId?: string }) => {\n  const { maxContactEmails } = useStore($userPlanFeatures);\n  const allowContactEmail = maxContactEmails > 0;\n  const pages = useStore($pages);\n  const project = useStore($project);\n  const assets = useStore($assets);\n  const [meta, setMeta] = useState(() => pages?.meta ?? defaultMetaSettings);\n  const siteNameId = useId();\n  const contactEmailId = useId();\n\n  // Update meta when pages data loads (important for dashboard mode)\n  useEffect(() => {\n    if (pages?.meta) {\n      setMeta(pages.meta);\n    }\n  }, [pages?.meta]);\n\n  const contactEmailError = validateContactEmail(\n    meta.contactEmail ?? \"\",\n    maxContactEmails\n  );\n  const asset = assets.get(meta.faviconAssetId ?? \"\");\n  const favIconUrl = asset ? `${asset.name}` : undefined;\n\n  // Use projectId prop if available (dashboard mode), otherwise use project from store (builder mode)\n  const effectiveProjectId = projectId ?? project?.id ?? \"\";\n\n  const handleSave = <Name extends keyof ProjectMeta>(\n    name: keyof ProjectMeta\n  ) => {\n    return (value: ProjectMeta[Name]) => {\n      setMeta({ ...meta, [name]: value });\n      saveSetting(name, value);\n    };\n  };\n\n  return (\n    <Grid gap={2}>\n      <Text variant=\"titles\" css={sectionSpacing}>\n        General\n      </Text>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Flex gap={1} align=\"center\">\n          <Text variant=\"labels\">Project ID:</Text>\n          <Text userSelect=\"text\">{effectiveProjectId}</Text>\n          <CopyToClipboard text={effectiveProjectId} copyText=\"Copy ID\">\n            <IconButton aria-label=\"Copy ID\">\n              <CopyIcon aria-hidden />\n            </IconButton>\n          </CopyToClipboard>\n        </Flex>\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Flex gap={1} align=\"center\">\n          <Label htmlFor={siteNameId}>Site name</Label>\n          <Tooltip\n            variant=\"wrapped\"\n            content=\"Used in search results and social previews.\"\n          >\n            <InfoCircleIcon tabIndex={0} />\n          </Tooltip>\n        </Flex>\n        <InputField\n          id={siteNameId}\n          placeholder=\"Current Site Name\"\n          autoFocus={true}\n          value={meta.siteName ?? \"\"}\n          onChange={(event) => {\n            handleSave(\"siteName\")(event.target.value);\n          }}\n        />\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Flex gap={1} align=\"center\">\n          <Label htmlFor={contactEmailId}>Contact email</Label>\n          <Tooltip\n            variant=\"wrapped\"\n            content=\"Used as the email recipient when submitting a webhook form without an action.\"\n          >\n            <InfoCircleIcon tabIndex={0} />\n          </Tooltip>\n          {allowContactEmail === false && <ProBadge>Pro</ProBadge>}\n        </Flex>\n        <InputErrorsTooltip\n          errors={contactEmailError ? [contactEmailError] : undefined}\n        >\n          <TextArea\n            id={contactEmailId}\n            color={contactEmailError ? \"error\" : undefined}\n            placeholder=\"john@company.com, jane@company.com\"\n            autoGrow={true}\n            rows={1}\n            value={meta.contactEmail ?? \"\"}\n            onChange={(value) => {\n              setMeta({ ...meta, contactEmail: value });\n              if (validateContactEmail(value, maxContactEmails) === undefined) {\n                saveSetting(\"contactEmail\", value);\n              }\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Separator />\n\n      <Grid gap={2} css={sectionSpacing} justify={\"start\"}>\n        <Label>Favicon</Label>\n        <Grid flow=\"column\" gap={3}>\n          <Image\n            width={72}\n            height={72}\n            className={imgStyle()}\n            src={favIconUrl}\n            loader={wsImageLoader}\n          />\n\n          <Grid gap={2}>\n            <Text color=\"subtle\">\n              Upload a square image to display in browser tabs.\n            </Text>\n            <ImageControl onAssetIdChange={handleSave(\"faviconAssetId\")}>\n              <Button css={{ justifySelf: \"start\" }}>Upload</Button>\n            </ImageControl>\n          </Grid>\n        </Grid>\n      </Grid>\n\n      <Separator />\n\n      <Grid gap={2} css={sectionSpacing}>\n        <Label>Custom code</Label>\n        <Text color=\"subtle\">\n          Custom code and scripts will be added at the end of the &lt;head&gt;\n          tag to every page across the published project.\n        </Text>\n        <CodeEditor\n          title=\"Custom code\"\n          lang=\"html\"\n          value={meta.code ?? \"\"}\n          onChange={handleSave(\"code\")}\n          onChangeComplete={handleSave(\"code\")}\n        />\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/section-marketplace.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  Grid,\n  InputField,\n  Label,\n  theme,\n  Text,\n  TextArea,\n  Button,\n  css,\n  Flex,\n  CheckboxAndLabel,\n  Checkbox,\n  InputErrorsTooltip,\n  PanelBanner,\n  Select,\n  Box,\n} from \"@webstudio-is/design-system\";\nimport { Image, wsImageLoader } from \"@webstudio-is/image\";\nimport { useState } from \"react\";\nimport {\n  MarketplaceProduct,\n  marketplaceCategories,\n} from \"@webstudio-is/project-build\";\nimport { ImageControl } from \"./image-control\";\nimport {\n  $assets,\n  $marketplaceProduct,\n  $project,\n} from \"~/shared/sync/data-stores\";\nimport { useIds } from \"~/shared/form-utils\";\nimport { MarketplaceApprovalStatus } from \"@webstudio-is/project\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { trpcClient } from \"~/shared/trpc/trpc-client\";\nimport { rightPanelWidth, sectionSpacing } from \"./utils\";\n\nconst thumbnailStyle = css({\n  borderRadius: theme.borderRadius[4],\n  outlineWidth: 1,\n  outlineStyle: \"solid\",\n  outlineColor: theme.colors.borderMain,\n  width: theme.spacing[28],\n  aspectRatio: \"1.91\",\n  background: \"#DFE3E6\",\n});\n\nconst thumbnailImageStyle = css({\n  display: \"block\",\n  width: \"100%\",\n  height: \"100%\",\n  variants: {\n    hasAsset: {\n      true: {\n        objectFit: \"cover\",\n      },\n    },\n  },\n});\n\nconst defaultMarketplaceProduct: Partial<MarketplaceProduct> = {\n  category: \"sectionTemplates\",\n};\n\nconst validate = (data: MarketplaceProduct) => {\n  const parsedResult = MarketplaceProduct.safeParse(data);\n  if (parsedResult.success === false) {\n    return parsedResult.error.formErrors.fieldErrors;\n  }\n};\n\nconst useMarketplaceApprovalStatus = () => {\n  const { send, data, state } =\n    trpcClient.project.setMarketplaceApprovalStatus.useMutation();\n  const project = useStore($project);\n\n  const status =\n    data?.marketplaceApprovalStatus ??\n    project?.marketplaceApprovalStatus ??\n    \"UNLISTED\";\n\n  const handleSuccess = ({\n    marketplaceApprovalStatus,\n  }: {\n    marketplaceApprovalStatus: MarketplaceApprovalStatus;\n  }) => {\n    const project = $project.get();\n    if (project) {\n      $project.set({\n        ...project,\n        marketplaceApprovalStatus,\n      });\n    }\n  };\n\n  return {\n    status,\n    state,\n    submit() {\n      if (project) {\n        send(\n          {\n            projectId: project.id,\n            marketplaceApprovalStatus: \"PENDING\",\n          },\n          handleSuccess\n        );\n      }\n    },\n    unlist() {\n      if (project) {\n        send(\n          {\n            projectId: project.id,\n            marketplaceApprovalStatus: \"UNLISTED\",\n          },\n          handleSuccess\n        );\n      }\n    },\n  };\n};\n\nexport const SectionMarketplace = () => {\n  const project = useStore($project);\n  const approval = useMarketplaceApprovalStatus();\n  const [data, setData] = useState(() => $marketplaceProduct.get());\n  const ids = useIds([\n    \"name\",\n    \"category\",\n    \"author\",\n    \"email\",\n    \"website\",\n    \"issues\",\n    \"description\",\n    \"isConfirmed\",\n  ]);\n  const assets = useStore($assets);\n  const [isConfirmed, setIsConfirmed] = useState<boolean>(false);\n  const [errors, setErrors] = useState<ReturnType<typeof validate>>();\n\n  if (data === undefined || project === undefined) {\n    return;\n  }\n  const asset = assets.get(data.thumbnailAssetId ?? \"\");\n\n  const handleSave = <Setting extends keyof MarketplaceProduct>(\n    setting: Setting\n  ) => {\n    return (value: MarketplaceProduct[Setting]) => {\n      const nextData = {\n        ...defaultMarketplaceProduct,\n        ...data,\n        [setting]: value,\n      };\n      const errors = validate(nextData);\n      setErrors(errors);\n      setData(nextData);\n\n      if (errors) {\n        return;\n      }\n      serverSyncStore.createTransaction(\n        [$marketplaceProduct],\n        (marketplaceProduct) => {\n          if (marketplaceProduct === undefined) {\n            return;\n          }\n          Object.assign(marketplaceProduct, nextData);\n        }\n      );\n    };\n  };\n\n  return (\n    <Grid gap={2}>\n      <Text variant=\"titles\" css={sectionSpacing}>\n        Marketplace\n      </Text>\n      <Grid gap={1} css={sectionSpacing}>\n        <Label htmlFor={ids.name}>Product name</Label>\n        <InputErrorsTooltip errors={errors?.name}>\n          <InputField\n            id={ids.name}\n            value={data.name ?? \"\"}\n            autoFocus\n            color={errors?.name && \"error\"}\n            onChange={(event) => {\n              handleSave(\"name\")(event.target.value);\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Label htmlFor={ids.category}>Category</Label>\n        <Select\n          options={Array.from(marketplaceCategories.keys())}\n          getLabel={(category: MarketplaceProduct[\"category\"]) =>\n            marketplaceCategories.get(category)?.label\n          }\n          getDescription={(category: MarketplaceProduct[\"category\"]) => (\n            <Box css={{ width: rightPanelWidth }}>\n              {marketplaceCategories.get(category)?.description}\n            </Box>\n          )}\n          onChange={handleSave(\"category\")}\n          value={data.category}\n          defaultValue={defaultMarketplaceProduct.category}\n        />\n      </Grid>\n\n      <Grid gap={2} css={sectionSpacing}>\n        <Label>Thumbnail</Label>\n        <InputErrorsTooltip errors={errors?.thumbnailAssetId}>\n          <Grid flow=\"column\" gap={3}>\n            <Box className={thumbnailStyle()}>\n              <Image\n                className={thumbnailImageStyle({\n                  hasAsset: asset !== undefined,\n                })}\n                src={asset ? `${asset.name}` : undefined}\n                loader={wsImageLoader}\n              />\n            </Box>\n\n            <Grid gap={2}>\n              <Text color=\"subtle\">\n                The optimal dimensions in marketplace are 600x315 px or larger\n                with a 1.91:1 aspect ratio.\n              </Text>\n              <ImageControl onAssetIdChange={handleSave(\"thumbnailAssetId\")}>\n                <Button css={{ justifySelf: \"start\" }}>Upload</Button>\n              </ImageControl>\n            </Grid>\n          </Grid>\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Label htmlFor={ids.author}>Author</Label>\n        <InputErrorsTooltip errors={errors?.author}>\n          <InputField\n            id={ids.author}\n            value={data.author ?? \"\"}\n            color={errors?.author && \"error\"}\n            onChange={(event) => {\n              handleSave(\"author\")(event.target.value);\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Label htmlFor={ids.email}>Email</Label>\n        <InputErrorsTooltip errors={errors?.email}>\n          <InputField\n            id={ids.email}\n            value={data.email ?? \"\"}\n            color={errors?.email && \"error\"}\n            onChange={(event) => {\n              handleSave(\"email\")(event.target.value);\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Label htmlFor={ids.website}>Website</Label>\n        <InputErrorsTooltip errors={errors?.website}>\n          <InputField\n            id={ids.website}\n            value={data.website ?? \"\"}\n            color={errors?.website && \"error\"}\n            onChange={(event) => {\n              handleSave(\"website\")(event.target.value);\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={1} css={sectionSpacing}>\n        <Label htmlFor={ids.issues}>Issues tracker</Label>\n        <InputErrorsTooltip errors={errors?.issues}>\n          <InputField\n            id={ids.issues}\n            value={data.issues ?? \"\"}\n            color={errors?.issues && \"error\"}\n            onChange={(event) => {\n              handleSave(\"issues\")(event.target.value);\n            }}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={2} css={sectionSpacing}>\n        <Label htmlFor={ids.description}>Description</Label>\n        <InputErrorsTooltip errors={errors?.description}>\n          <TextArea\n            id={ids.description}\n            rows={5}\n            autoGrow\n            maxRows={10}\n            value={data.description ?? \"\"}\n            color={errors?.description && \"error\"}\n            onChange={handleSave(\"description\")}\n          />\n        </InputErrorsTooltip>\n      </Grid>\n\n      <Grid gap={2} css={sectionSpacing}>\n        <PanelBanner>\n          <Text color=\"destructive\">\n            {`Don't forget to publish your project after every change to make your\n            changes available in the marketplace!`}\n          </Text>\n        </PanelBanner>\n      </Grid>\n\n      {approval.status === \"UNLISTED\" && (\n        <Grid gap={2} css={sectionSpacing}>\n          <CheckboxAndLabel>\n            <Checkbox\n              checked={isConfirmed}\n              id={ids.isConfirmed}\n              onCheckedChange={(value) => {\n                if (typeof value === \"boolean\") {\n                  setIsConfirmed(value);\n                }\n              }}\n            />\n            <Label htmlFor={ids.isConfirmed} css={{ flexShrink: 1 }}>\n              I understand that by submitting, this project will become\n              available in a public marketplace.\n            </Label>\n          </CheckboxAndLabel>\n        </Grid>\n      )}\n\n      <Flex align=\"center\" justify=\"between\" gap={2} css={sectionSpacing}>\n        <Text>Status: {approval.status.toLocaleLowerCase()}</Text>\n        {approval.status === \"UNLISTED\" ? (\n          <Button\n            color=\"primary\"\n            disabled={isConfirmed === false || errors !== undefined}\n            state={approval.state === \"idle\" ? undefined : \"pending\"}\n            onClick={approval.submit}\n          >\n            Start Review\n          </Button>\n        ) : (\n          <Button\n            state={approval.state === \"idle\" ? undefined : \"pending\"}\n            color=\"destructive\"\n            onClick={approval.unlist}\n          >\n            Unlist from Marketplace\n          </Button>\n        )}\n      </Flex>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/section-publish.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Grid,\n  Label,\n  CheckboxAndLabel,\n  Checkbox,\n  Text,\n} from \"@webstudio-is/design-system\";\nimport type { CompilerSettings } from \"@webstudio-is/sdk\";\nimport { $pages } from \"~/shared/sync/data-stores\";\nimport { useIds } from \"~/shared/form-utils\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { sectionSpacing } from \"./utils\";\n\nconst defaultPublishSettings: CompilerSettings = {\n  atomicStyles: true,\n};\n\nexport const SectionPublish = () => {\n  const ids = useIds([\"atomicStyles\"]);\n  const [settings, setSettings] = useState(\n    () => $pages.get()?.compiler ?? defaultPublishSettings\n  );\n\n  const handleSave = (settings: CompilerSettings) => {\n    serverSyncStore.createTransaction([$pages], (pages) => {\n      if (pages === undefined) {\n        return;\n      }\n      pages.compiler = settings;\n    });\n  };\n\n  return (\n    <Grid gap={2}>\n      <Text variant=\"titles\" css={sectionSpacing}>\n        Publishing\n      </Text>\n      <Grid gap={2} css={sectionSpacing}>\n        <CheckboxAndLabel>\n          <Checkbox\n            checked={settings.atomicStyles ?? true}\n            id={ids.atomicStyles}\n            onCheckedChange={(atomicStyles) => {\n              if (typeof atomicStyles === \"boolean\") {\n                const nextSettings = { ...settings, atomicStyles };\n                setSettings(nextSettings);\n                handleSave(nextSettings);\n              }\n            }}\n          />\n          <Label htmlFor={ids.atomicStyles}>\n            Generate atomic CSS when publishing\n          </Label>\n        </CheckboxAndLabel>\n      </Grid>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/section-redirects.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport type { PageRedirect } from \"@webstudio-is/sdk\";\nimport { __testing__ } from \"./section-redirects\";\n\nconst { validateFromPath, validateToPath } = __testing__;\n\ndescribe(\"validateFromPath\", () => {\n  describe(\"format validation\", () => {\n    test(\"returns error for empty path\", () => {\n      const result = validateFromPath(\"\", [], new Set());\n      expect(result.errors).toContain(\"Can't be empty\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for just /\", () => {\n      const result = validateFromPath(\"/\", [], new Set());\n      expect(result.errors).toContain(\"Can't be just a /\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for path not starting with /\", () => {\n      const result = validateFromPath(\"page\", [], new Set());\n      expect(result.errors).toContain(\n        \"Must start with a / or a full URL e.g. https://website.org\"\n      );\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for path ending with /\", () => {\n      const result = validateFromPath(\"/page/\", [], new Set());\n      expect(result.errors).toContain(\"Can't end with a /\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for path with double slashes\", () => {\n      const result = validateFromPath(\"/page//subpage\", [], new Set());\n      expect(result.errors).toContain(\"Can't contain repeating /\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for path with invalid characters\", () => {\n      const result = validateFromPath(\"/page name\", [], new Set());\n      expect(result.errors).toContain(\n        \"Path contains invalid characters (spaces or URL-unsafe characters are not allowed)\"\n      );\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for reserved /s prefix\", () => {\n      const result = validateFromPath(\"/s/something\", [], new Set());\n      expect(result.errors).toContain(\"/s prefix is reserved for the system\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for reserved /build prefix\", () => {\n      const result = validateFromPath(\"/build/something\", [], new Set());\n      expect(result.errors).toContain(\n        \"/build prefix is reserved for the system\"\n      );\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts valid path\", () => {\n      const result = validateFromPath(\"/valid-path\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with allowed special characters\", () => {\n      const result = validateFromPath(\n        \"/path-name_123:param?query\",\n        [],\n        new Set()\n      );\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with uppercase letters\", () => {\n      const result = validateFromPath(\"/Path/SubPage\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with wildcard\", () => {\n      const result = validateFromPath(\"/blog/*\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with named parameter\", () => {\n      const result = validateFromPath(\"/blog/:slug\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with optional parameter\", () => {\n      const result = validateFromPath(\"/page/:id?\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with multiple parameters\", () => {\n      const result = validateFromPath(\n        \"/blog/:year/:month/:slug\",\n        [],\n        new Set()\n      );\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts path with named splat\", () => {\n      const result = validateFromPath(\"/docs/:path*\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"accepts deeply nested path with wildcard\", () => {\n      const result = validateFromPath(\"/api/v1/users/*\", [], new Set());\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"skips duplicate/warning checks for paths not starting with /\", () => {\n      // External URLs or other formats that pass schema validation\n      // but don't start with / should skip the duplicate redirect\n      // and existing page checks (since those only apply to local paths)\n      const existingRedirects: Array<PageRedirect> = [\n        { old: \"https://old.com/page\", new: \"/new\", status: \"301\" },\n      ];\n      const existingPaths = new Set([\"https://old.com/page\"]);\n\n      // Even though this path \"exists\" in redirects and pages,\n      // no error/warning because it doesn't start with /\n      // Note: This tests the current behavior - external URLs bypass checks\n      const result = validateFromPath(\n        \"https://old.com/page\",\n        existingRedirects,\n        existingPaths\n      );\n      // The schema may reject this, but if it passes, no duplicate check\n      // If schema rejects, we get format errors instead\n      expect(result.warnings).toEqual([]);\n    });\n  });\n\n  describe(\"duplicate redirect validation\", () => {\n    const existingRedirects: Array<PageRedirect> = [\n      { old: \"/old-page\", new: \"/new-page\", status: \"301\" },\n      { old: \"/another-old\", new: \"/another-new\", status: \"302\" },\n      { old: \"/blog/*\", new: \"/articles\", status: \"301\" },\n      { old: \"/posts/:slug\", new: \"/blog\", status: \"301\" },\n    ];\n\n    test(\"returns error when redirect already exists for path\", () => {\n      const result = validateFromPath(\n        \"/old-page\",\n        existingRedirects,\n        new Set()\n      );\n      expect(result.errors).toContain(\"This path is already being redirected\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error when wildcard redirect already exists\", () => {\n      const result = validateFromPath(\"/blog/*\", existingRedirects, new Set());\n      expect(result.errors).toContain(\"This path is already being redirected\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error when parameterized redirect already exists\", () => {\n      const result = validateFromPath(\n        \"/posts/:slug\",\n        existingRedirects,\n        new Set()\n      );\n      expect(result.errors).toContain(\"This path is already being redirected\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"returns error for duplicate even if page exists (error takes precedence)\", () => {\n      const existingPaths = new Set([\"/old-page\"]);\n      const result = validateFromPath(\n        \"/old-page\",\n        existingRedirects,\n        existingPaths\n      );\n      expect(result.errors).toContain(\"This path is already being redirected\");\n      expect(result.warnings).toEqual([]);\n    });\n\n    test(\"allows path that is not already redirected\", () => {\n      const result = validateFromPath(\n        \"/different-path\",\n        existingRedirects,\n        new Set()\n      );\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n  });\n\n  describe(\"existing page warning\", () => {\n    const existingPaths = new Set([\"/about\", \"/contact\", \"/blog/:id\"]);\n\n    test(\"returns warning when redirecting from existing page path\", () => {\n      const result = validateFromPath(\"/about\", [], existingPaths);\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toContain(\n        \"This redirect will override an existing page\"\n      );\n    });\n\n    test(\"returns warning for dynamic route path\", () => {\n      const result = validateFromPath(\"/blog/:id\", [], existingPaths);\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toContain(\n        \"This redirect will override an existing page\"\n      );\n    });\n\n    test(\"no warning for path that doesn't exist as page\", () => {\n      const result = validateFromPath(\"/new-redirect\", [], existingPaths);\n      expect(result.errors).toEqual([]);\n      expect(result.warnings).toEqual([]);\n    });\n  });\n});\n\ndescribe(\"validateToPath\", () => {\n  // Note: ProjectNewRedirectPath uses `new URL(data, baseURL)` which is permissive\n  // for most strings. However, empty strings are explicitly rejected.\n\n  test(\"rejects empty path\", () => {\n    const errors = validateToPath(\"\");\n    expect(errors).toContain(\"Path is required\");\n  });\n\n  test(\"accepts relative path with spaces (URL-encoded)\", () => {\n    const errors = validateToPath(\"path with spaces\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts relative path starting with /\", () => {\n    const errors = validateToPath(\"/new-page\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts absolute URL\", () => {\n    const errors = validateToPath(\"https://example.com/page\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts home page path\", () => {\n    const errors = validateToPath(\"/\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts path with query parameters\", () => {\n    const errors = validateToPath(\"/page?param=value\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts path with hash fragment\", () => {\n    const errors = validateToPath(\"/page#section\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts external URL with path\", () => {\n    const errors = validateToPath(\"https://other-site.com/path/to/page\");\n    expect(errors).toEqual([]);\n  });\n\n  test(\"accepts protocol-relative URL\", () => {\n    const errors = validateToPath(\"//example.com/page\");\n    expect(errors).toEqual([]);\n  });\n});\n\ndescribe(\"redirect patterns (integration tests)\", () => {\n  // These tests document the supported redirect patterns\n  // and how they should be validated together\n\n  test(\"exact path redirect: /old → /new\", () => {\n    const fromResult = validateFromPath(\"/old-about\", [], new Set());\n    const toErrors = validateToPath(\"/about\");\n    expect(fromResult.errors).toEqual([]);\n    expect(toErrors).toEqual([]);\n  });\n\n  test(\"wildcard redirect: /old/* → /new (all paths go to single destination)\", () => {\n    const fromResult = validateFromPath(\"/old-blog/*\", [], new Set());\n    const toErrors = validateToPath(\"/blog\");\n    expect(fromResult.errors).toEqual([]);\n    expect(toErrors).toEqual([]);\n  });\n\n  test(\"parameterized from path: /old/:slug → /new (param captured but not substituted)\", () => {\n    // Note: The :slug is captured by the router but cannot be substituted\n    // into the \"to\" path - all matching URLs redirect to the same destination\n    const fromResult = validateFromPath(\"/old/:slug\", [], new Set());\n    const toErrors = validateToPath(\"/new\");\n    expect(fromResult.errors).toEqual([]);\n    expect(toErrors).toEqual([]);\n  });\n\n  test(\"complex pattern: /posts/:year/:month/* → /archive\", () => {\n    const fromResult = validateFromPath(\"/posts/:year/:month/*\", [], new Set());\n    const toErrors = validateToPath(\"/archive\");\n    expect(fromResult.errors).toEqual([]);\n    expect(toErrors).toEqual([]);\n  });\n\n  test(\"redirect to external URL\", () => {\n    const fromResult = validateFromPath(\"/legacy-page\", [], new Set());\n    const toErrors = validateToPath(\"https://new-domain.com/page\");\n    expect(fromResult.errors).toEqual([]);\n    expect(toErrors).toEqual([]);\n  });\n\n  test(\"redirect to home page\", () => {\n    const fromResult = validateFromPath(\"/old-home\", [], new Set());\n    const toErrors = validateToPath(\"/\");\n    expect(fromResult.errors).toEqual([]);\n    expect(toErrors).toEqual([]);\n  });\n});\n\ndescribe(\"shared schema validation\", () => {\n  // This test documents that validateToPath uses the same ProjectNewRedirectPath\n  // schema that is used in page-settings.tsx for page redirect destinations.\n  // This ensures consistent validation across the codebase.\n\n  test(\"validateToPath uses ProjectNewRedirectPath schema (shared with page settings)\", () => {\n    // Valid paths that should work in both places\n    expect(validateToPath(\"/page\")).toEqual([]);\n    expect(validateToPath(\"https://example.com\")).toEqual([]);\n    expect(validateToPath(\"/\")).toEqual([]);\n\n    // The schema is imported from @webstudio-is/sdk and shared\n    // between section-redirects.tsx and page-settings.tsx\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/section-redirects.tsx",
    "content": "import { useStore } from \"@nanostores/react\";\nimport {\n  Box,\n  Button,\n  Combobox,\n  Dialog,\n  DialogActions,\n  DialogClose,\n  DialogContent,\n  DialogTitle,\n  Flex,\n  Grid,\n  InputErrorsTooltip,\n  Link,\n  List,\n  ListItem,\n  rawTheme,\n  ScrollArea,\n  SearchField,\n  Select,\n  SmallIconButton,\n  Text,\n  theme,\n  Tooltip,\n} from \"@webstudio-is/design-system\";\nimport {\n  AlertIcon,\n  ArrowRightIcon,\n  InfoCircleIcon,\n  TrashIcon,\n  UploadIcon,\n} from \"@webstudio-is/icons\";\nimport { ImportRedirectsDialog } from \"./import-redirects-dialog\";\nimport { OldPagePath, ProjectNewRedirectPath } from \"@webstudio-is/sdk\";\nimport type { PageRedirect } from \"@webstudio-is/sdk\";\nimport { useRef, useState } from \"react\";\nimport { flushSync } from \"react-dom\";\nimport { $pages } from \"~/shared/sync/data-stores\";\nimport { $publishedOrigin } from \"~/shared/nano-states\";\nimport { serverSyncStore } from \"~/shared/sync/sync-stores\";\nimport { getExistingRoutePaths, sectionSpacing } from \"./utils\";\nimport {\n  LOOP_ERROR,\n  wouldCreateLoop,\n} from \"~/shared/redirects/redirect-loop-detection\";\n\nconst statusCodeOptions = [\"301\", \"302\"] as const;\n\nconst statusCodeDescriptions: Record<string, string> = {\n  \"301\": \"Moved permanently. SEO ranking transfers to new URL.\",\n  \"302\": \"Moved temporarily. SEO ranking stays with original URL.\",\n};\n\ntype ValidationResult = {\n  errors: string[];\n  warnings: string[];\n};\n\nconst validateFromPath = (\n  fromPath: string,\n  redirects: Array<PageRedirect>,\n  existingPaths: Set<string>\n): ValidationResult => {\n  const fromPathValidationResult = OldPagePath.safeParse(fromPath);\n\n  if (fromPathValidationResult.success) {\n    if (fromPath.startsWith(\"/\")) {\n      // Check for duplicate redirect first (error takes precedence)\n      if (redirects.some((redirect) => redirect.old === fromPath)) {\n        return {\n          errors: [\"This path is already being redirected\"],\n          warnings: [],\n        };\n      }\n\n      // Show warning if redirecting from an existing page path\n      // The redirect will take precedence over the page when published\n      if (existingPaths.has(fromPath)) {\n        return {\n          errors: [],\n          warnings: [\"This redirect will override an existing page\"],\n        };\n      }\n    }\n    return { errors: [], warnings: [] };\n  }\n\n  return {\n    errors: fromPathValidationResult.error.format()?._errors,\n    warnings: [],\n  };\n};\n\nconst validateToPath = (toPath: string): string[] => {\n  const toPathValidationResult = ProjectNewRedirectPath.safeParse(toPath);\n  if (toPathValidationResult.success) {\n    return [];\n  }\n  return toPathValidationResult.error.format()._errors;\n};\n\n// Exported for testing\nexport const __testing__ = {\n  validateFromPath,\n  validateToPath,\n};\n\nexport const SectionRedirects = () => {\n  const publishedOrigin = useStore($publishedOrigin);\n  const [redirects, setRedirects] = useState(\n    () => $pages.get()?.redirects ?? []\n  );\n\n  const [fromPath, setFromPath] = useState<string>(\"\");\n  const [toPath, setToPath] = useState<string>(\"\");\n  const [httpStatus, setHttpStatus] =\n    useState<PageRedirect[\"status\"]>(undefined);\n  const [fromPathErrors, setFromPathErrors] = useState<string[]>([]);\n  const [fromPathWarnings, setFromPathWarnings] = useState<string[]>([]);\n  const [toPathErrors, setToPathErrors] = useState<string[]>([]);\n  const [isImportDialogOpen, setIsImportDialogOpen] = useState(false);\n  const [isDeleteAllDialogOpen, setIsDeleteAllDialogOpen] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const pages = useStore($pages);\n  const existingPaths = getExistingRoutePaths(pages);\n  const fromPathRef = useRef<HTMLInputElement>(null);\n\n  const isValidRedirects =\n    fromPathErrors.length === 0 && toPathErrors.length === 0;\n\n  // Filter redirects based on search query\n  const filteredRedirects = searchQuery\n    ? redirects.filter(\n        (redirect) =>\n          redirect.old.toLowerCase().includes(searchQuery.toLowerCase()) ||\n          redirect.new.toLowerCase().includes(searchQuery.toLowerCase())\n      )\n    : redirects;\n\n  // Get all page paths for combobox suggestions\n  const pagePaths = Array.from(existingPaths).sort();\n\n  const handleFromPathChange = (value: string) => {\n    setFromPath(value);\n    const { errors, warnings } = validateFromPath(\n      value,\n      redirects,\n      existingPaths\n    );\n    setFromPathErrors(errors);\n    setFromPathWarnings(warnings);\n  };\n\n  const handleToPathChange = (value: string) => {\n    setToPath(value);\n    setToPathErrors(validateToPath(value));\n  };\n\n  const handleSave = (redirects: Array<PageRedirect>) => {\n    setRedirects(redirects);\n    serverSyncStore.createTransaction([$pages], (pages) => {\n      if (pages === undefined) {\n        return;\n      }\n      pages.redirects = redirects;\n    });\n  };\n\n  const handleAddRedirect = () => {\n    const { errors: fromPathValidationErrors, warnings } = validateFromPath(\n      fromPath,\n      redirects,\n      existingPaths\n    );\n    const toPathValidationErrors = validateToPath(toPath);\n    const hasLoop = wouldCreateLoop(fromPath, toPath, redirects);\n\n    // Update error state so user sees what went wrong\n    setFromPathErrors(fromPathValidationErrors);\n    setFromPathWarnings(warnings);\n    setToPathErrors(\n      hasLoop ? [...toPathValidationErrors, LOOP_ERROR] : toPathValidationErrors\n    );\n\n    if (\n      fromPathValidationErrors.length > 0 ||\n      toPathValidationErrors.length > 0 ||\n      hasLoop\n    ) {\n      return;\n    }\n\n    // Needs to apply state before setting focus.\n    flushSync(() => {\n      handleSave([\n        {\n          old: fromPath,\n          new: toPath,\n          status: httpStatus ?? \"301\",\n        },\n        ...redirects,\n      ]);\n      setFromPath(\"\");\n      setToPath(\"\");\n      setFromPathWarnings([]);\n    });\n    fromPathRef.current?.focus();\n  };\n\n  const handleDeleteRedirect = (index: number) => {\n    const newRedirects = [...redirects];\n    newRedirects.splice(index, 1);\n    handleSave(newRedirects);\n  };\n\n  const handleImportRedirects = (\n    importedRedirects: PageRedirect[],\n    mode: \"add\" | \"replace\"\n  ) => {\n    if (mode === \"replace\") {\n      handleSave(importedRedirects);\n    } else {\n      // Add mode - prepend new redirects\n      handleSave([...importedRedirects, ...redirects]);\n    }\n  };\n\n  const handleDeleteAll = () => {\n    handleSave([]);\n    setIsDeleteAllDialogOpen(false);\n  };\n\n  return (\n    <>\n      <ImportRedirectsDialog\n        isOpen={isImportDialogOpen}\n        onOpenChange={setIsImportDialogOpen}\n        existingRedirects={redirects}\n        onImport={handleImportRedirects}\n      />\n\n      <Grid gap={3} css={sectionSpacing}>\n        <Flex gap={1} align=\"center\">\n          <Text variant=\"titles\">Redirects</Text>\n          <Tooltip\n            variant=\"wrapped\"\n            content={\n              <Flex direction=\"column\" gap=\"2\">\n                <Text>\n                  Redirect old URLs to new ones so you don't lose traffic or\n                  search engine rankings.\n                </Text>\n                <Flex direction=\"column\" gap=\"1\">\n                  <Text>Supported patterns:</Text>\n                  <Text>/path → Exact match</Text>\n                  <Text>/blog/* → All paths under /blog/</Text>\n                  <Text>/:slug → Dynamic segment</Text>\n                  <Text>/:id? → Optional segment</Text>\n                </Flex>\n              </Flex>\n            }\n          >\n            <InfoCircleIcon\n              color={rawTheme.colors.foregroundSubtle}\n              tabIndex={0}\n            />\n          </Tooltip>\n        </Flex>\n\n        <Flex gap=\"2\" justify=\"between\">\n          <SearchField\n            placeholder=\"Search\"\n            value={searchQuery}\n            onChange={(event) => setSearchQuery(event.target.value)}\n            onAbort={() => setSearchQuery(\"\")}\n            disabled={redirects.length === 0}\n          />\n          <Flex gap=\"2\">\n            <Button\n              color=\"ghost\"\n              prefix={<TrashIcon />}\n              onClick={() => setIsDeleteAllDialogOpen(true)}\n              disabled={redirects.length === 0}\n            >\n              Delete all\n            </Button>\n            <Button\n              color=\"ghost\"\n              prefix={<UploadIcon />}\n              onClick={() => setIsImportDialogOpen(true)}\n            >\n              Import\n            </Button>\n          </Flex>\n        </Flex>\n\n        <Dialog\n          open={isDeleteAllDialogOpen}\n          onOpenChange={setIsDeleteAllDialogOpen}\n        >\n          <DialogContent>\n            <DialogTitle>Delete all redirects</DialogTitle>\n            <Flex css={{ padding: theme.panel.padding }}>\n              <Text>\n                Are you sure you want to delete all {redirects.length} redirect\n                {redirects.length !== 1 ? \"s\" : \"\"}? This action cannot be\n                undone.\n              </Text>\n            </Flex>\n            <DialogActions>\n              <Button color=\"destructive\" onClick={handleDeleteAll}>\n                Delete all\n              </Button>\n              <DialogClose>\n                <Button color=\"ghost\">Cancel</Button>\n              </DialogClose>\n            </DialogActions>\n          </DialogContent>\n        </Dialog>\n\n        <Flex gap=\"2\" align=\"center\">\n          <InputErrorsTooltip\n            errors={fromPathErrors.length > 0 ? fromPathErrors : undefined}\n            side=\"top\"\n          >\n            <Combobox<string>\n              inputRef={fromPathRef}\n              autoFocus\n              placeholder=\"/old-path or /old/*\"\n              value={fromPath}\n              color={fromPathErrors.length === 0 ? undefined : \"error\"}\n              getItems={() => pagePaths}\n              itemToString={(item) => item ?? \"\"}\n              onItemSelect={(value) => handleFromPathChange(value ?? \"\")}\n              onChange={(value) => {\n                // Don't reset value on blur (when value is undefined)\n                if (value !== undefined) {\n                  handleFromPathChange(value);\n                }\n              }}\n            />\n          </InputErrorsTooltip>\n\n          <Select\n            id=\"redirect-type\"\n            placeholder=\"301\"\n            options={[...statusCodeOptions]}\n            value={httpStatus ?? \"301\"}\n            css={{ width: theme.spacing[18] }}\n            getDescription={(option) => (\n              <Box css={{ width: theme.spacing[25] }}>\n                {statusCodeDescriptions[option]}\n              </Box>\n            )}\n            onChange={(value) => {\n              setHttpStatus(value as PageRedirect[\"status\"]);\n            }}\n          />\n\n          <InputErrorsTooltip\n            errors={toPathErrors.length > 0 ? toPathErrors : undefined}\n            side=\"top\"\n          >\n            <Combobox<string>\n              placeholder=\"/to-path or URL\"\n              value={toPath}\n              color={toPathErrors.length === 0 ? undefined : \"error\"}\n              getItems={() => pagePaths}\n              itemToString={(item) => item ?? \"\"}\n              onItemSelect={(value) => handleToPathChange(value ?? \"\")}\n              onChange={(value) => {\n                // Don't reset value on blur (when value is undefined)\n                if (value !== undefined) {\n                  handleToPathChange(value);\n                }\n              }}\n            />\n          </InputErrorsTooltip>\n\n          <Button\n            disabled={isValidRedirects === false || fromPath === toPath}\n            onClick={handleAddRedirect}\n            css={{ flexShrink: 0 }}\n          >\n            Add\n          </Button>\n        </Flex>\n        {fromPathWarnings.length > 0 && (\n          <Flex gap=\"1\" align=\"center\">\n            <AlertIcon\n              color={rawTheme.colors.backgroundAlertMain}\n              style={{ flexShrink: 0 }}\n            />\n            <Text color=\"subtle\">{fromPathWarnings.join(\". \")}</Text>\n          </Flex>\n        )}\n      </Grid>\n      {redirects.length > 0 ? (\n        <ScrollArea>\n          <Grid css={sectionSpacing}>\n            <List asChild>\n              <Flex direction=\"column\" gap=\"1\" align=\"stretch\">\n                {filteredRedirects.map((redirect, index) => {\n                  return (\n                    <ListItem asChild key={redirect.old}>\n                      <Grid\n                        align=\"center\"\n                        gap=\"2\"\n                        css={{\n                          p: theme.spacing[3],\n                          overflow: \"hidden\",\n                          gridTemplateColumns: `1fr auto auto 1fr`,\n                          position: \"relative\",\n                          \"& > button\": {\n                            opacity: 0,\n                            position: \"absolute\",\n                            right: 0,\n                            top: 0,\n                            bottom: 0,\n                            height: \"auto\",\n                            borderRadius: 0,\n                            background: theme.colors.backgroundPanel,\n                          },\n                          \"&:hover > button, &:focus-within > button\": {\n                            opacity: 1,\n                          },\n                        }}\n                      >\n                        <Tooltip content={redirect.old}>\n                          <Link\n                            underline=\"hover\"\n                            href={new URL(\n                              redirect.old,\n                              publishedOrigin\n                            ).toString()}\n                            css={{\n                              wordBreak: \"break-all\",\n                              overflow: \"hidden\",\n                              textOverflow: \"ellipsis\",\n                            }}\n                            target=\"_blank\"\n                          >\n                            {redirect.old}\n                          </Link>\n                        </Tooltip>\n                        <Text>{redirect.status ?? \"301\"}</Text>\n                        <ArrowRightIcon size={16} />\n                        <Tooltip content={redirect.new}>\n                          <Link\n                            underline=\"hover\"\n                            href={new URL(\n                              redirect.new,\n                              publishedOrigin\n                            ).toString()}\n                            css={{\n                              wordBreak: \"break-all\",\n                              overflow: \"hidden\",\n                              textOverflow: \"ellipsis\",\n                            }}\n                            target=\"_blank\"\n                          >\n                            {redirect.new}\n                          </Link>\n                        </Tooltip>\n                        <SmallIconButton\n                          variant=\"destructive\"\n                          icon={<TrashIcon />}\n                          aria-label={`Delete redirect from ${redirect.old}`}\n                          onClick={() => handleDeleteRedirect(index)}\n                        />\n                      </Grid>\n                    </ListItem>\n                  );\n                })}\n              </Flex>\n            </List>\n          </Grid>\n        </ScrollArea>\n      ) : null}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/utils.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport type { PageRedirect } from \"@webstudio-is/sdk\";\nimport { getExistingRoutePaths, findMatchingRedirect } from \"./utils\";\n\ndescribe(\"getExistingRoutePaths\", () => {\n  test(\"gets all the route paths that exists in the project\", () => {\n    const pages = createDefaultPages({\n      rootInstanceId: \"rootInstanceId\",\n      homePageId: \"homePageId\",\n    });\n\n    pages.pages.push({\n      id: \"pageId\",\n      meta: {},\n      name: \"Page\",\n      path: \"/page\",\n      rootInstanceId: \"rootInstanceId\",\n      title: `\"Page\"`,\n    });\n\n    pages.pages.push({\n      id: \"blogId\",\n      meta: {},\n      name: \"Blog\",\n      path: \"/blog/:id\",\n      rootInstanceId: \"rootInstanceId\",\n      title: `\"Blog\"`,\n    });\n\n    const result = getExistingRoutePaths(pages);\n    expect(Array.from(result)).toEqual([\"/page\", \"/blog/:id\"]);\n  });\n});\n\ndescribe(\"findMatchingRedirect\", () => {\n  const createRedirect = (\n    old: string,\n    newPath: string = \"/new\"\n  ): PageRedirect => ({\n    old,\n    new: newPath,\n    status: \"301\",\n  });\n\n  describe(\"exact matches\", () => {\n    test(\"finds redirect with exact path match\", () => {\n      const redirects = [createRedirect(\"/about\")];\n      const result = findMatchingRedirect(\"/about\", redirects);\n      expect(result?.old).toBe(\"/about\");\n    });\n\n    test(\"returns undefined when no match found\", () => {\n      const redirects = [createRedirect(\"/about\")];\n      const result = findMatchingRedirect(\"/contact\", redirects);\n      expect(result).toBeUndefined();\n    });\n\n    test(\"matches home page path /\", () => {\n      const redirects = [createRedirect(\"/old-home\")];\n      const result = findMatchingRedirect(\"/\", redirects);\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe(\"wildcard patterns\", () => {\n    test(\"matches wildcard pattern /*\", () => {\n      const redirects = [createRedirect(\"/blog/*\")];\n      const result = findMatchingRedirect(\"/blog/post-1\", redirects);\n      expect(result?.old).toBe(\"/blog/*\");\n    });\n\n    test(\"matches nested path with wildcard\", () => {\n      const redirects = [createRedirect(\"/docs/*\")];\n      const result = findMatchingRedirect(\"/docs/api/v1/users\", redirects);\n      expect(result?.old).toBe(\"/docs/*\");\n    });\n\n    test(\"does not match path outside wildcard scope\", () => {\n      const redirects = [createRedirect(\"/blog/*\")];\n      const result = findMatchingRedirect(\"/posts/article\", redirects);\n      expect(result).toBeUndefined();\n    });\n\n    test(\"matches wildcard at root level\", () => {\n      const redirects = [createRedirect(\"/*\")];\n      const result = findMatchingRedirect(\"/anything\", redirects);\n      expect(result?.old).toBe(\"/*\");\n    });\n  });\n\n  describe(\"dynamic segment patterns\", () => {\n    test(\"matches single dynamic segment\", () => {\n      const redirects = [createRedirect(\"/:slug\")];\n      const result = findMatchingRedirect(\"/about\", redirects);\n      expect(result?.old).toBe(\"/:slug\");\n    });\n\n    test(\"matches dynamic segment in middle of path\", () => {\n      const redirects = [createRedirect(\"/blog/:id/comments\")];\n      const result = findMatchingRedirect(\"/blog/123/comments\", redirects);\n      expect(result?.old).toBe(\"/blog/:id/comments\");\n    });\n\n    test(\"matches multiple dynamic segments\", () => {\n      const redirects = [createRedirect(\"/users/:userId/posts/:postId\")];\n      const result = findMatchingRedirect(\"/users/42/posts/99\", redirects);\n      expect(result?.old).toBe(\"/users/:userId/posts/:postId\");\n    });\n\n    test(\"does not match when segment count differs\", () => {\n      const redirects = [createRedirect(\"/blog/:id\")];\n      const result = findMatchingRedirect(\"/blog/123/extra\", redirects);\n      expect(result).toBeUndefined();\n    });\n  });\n\n  describe(\"optional segments\", () => {\n    test(\"matches optional segment when present\", () => {\n      const redirects = [createRedirect(\"/blog/:id?\")];\n      const result = findMatchingRedirect(\"/blog/123\", redirects);\n      expect(result?.old).toBe(\"/blog/:id?\");\n    });\n\n    test(\"matches optional segment when absent\", () => {\n      const redirects = [createRedirect(\"/blog/:id?\")];\n      const result = findMatchingRedirect(\"/blog\", redirects);\n      expect(result?.old).toBe(\"/blog/:id?\");\n    });\n  });\n\n  describe(\"priority and order\", () => {\n    test(\"returns first matching redirect\", () => {\n      const redirects = [\n        createRedirect(\"/about\", \"/new-about\"),\n        createRedirect(\"/:slug\", \"/catch-all\"),\n      ];\n      const result = findMatchingRedirect(\"/about\", redirects);\n      expect(result?.new).toBe(\"/new-about\");\n    });\n\n    test(\"falls through to wildcard if no exact match\", () => {\n      const redirects = [createRedirect(\"/specific\"), createRedirect(\"/*\")];\n      const result = findMatchingRedirect(\"/other\", redirects);\n      expect(result?.old).toBe(\"/*\");\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"handles empty redirects array\", () => {\n      const result = findMatchingRedirect(\"/about\", []);\n      expect(result).toBeUndefined();\n    });\n\n    test(\"handles path with special characters\", () => {\n      const redirects = [createRedirect(\"/path-with-dashes\")];\n      const result = findMatchingRedirect(\"/path-with-dashes\", redirects);\n      expect(result?.old).toBe(\"/path-with-dashes\");\n    });\n\n    test(\"handles path with underscores\", () => {\n      const redirects = [createRedirect(\"/path_with_underscores\")];\n      const result = findMatchingRedirect(\"/path_with_underscores\", redirects);\n      expect(result?.old).toBe(\"/path_with_underscores\");\n    });\n\n    test(\"handles deeply nested paths\", () => {\n      const redirects = [createRedirect(\"/a/b/c/d/e\")];\n      const result = findMatchingRedirect(\"/a/b/c/d/e\", redirects);\n      expect(result?.old).toBe(\"/a/b/c/d/e\");\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/project-settings/utils.ts",
    "content": "import { rawTheme, theme, type CSS } from \"@webstudio-is/design-system\";\nimport { getPagePath, type Pages, type PageRedirect } from \"@webstudio-is/sdk\";\nimport { matchPathnamePattern } from \"~/builder/shared/url-pattern\";\n\nexport const leftPanelWidth = rawTheme.spacing[26];\nexport const rightPanelWidth = rawTheme.spacing[35];\nexport const sectionSpacing: CSS = {\n  paddingInline: theme.panel.paddingInline,\n};\n\nexport const getExistingRoutePaths = (pages?: Pages): Set<string> => {\n  const paths: Set<string> = new Set();\n  if (pages === undefined) {\n    return paths;\n  }\n\n  for (const page of pages.pages) {\n    const pagePath = getPagePath(page.id, pages);\n    if (pagePath === undefined) {\n      continue;\n    }\n    paths.add(pagePath);\n  }\n  return paths;\n};\n\n/**\n * Find a redirect that would match the given page path.\n * Uses URLPattern for proper pattern matching (wildcards, dynamic segments).\n */\nexport const findMatchingRedirect = (\n  pagePath: string,\n  redirects: Array<PageRedirect>\n): PageRedirect | undefined => {\n  for (const redirect of redirects) {\n    // matchPathnamePattern returns matched groups if pattern matches, undefined otherwise\n    const match = matchPathnamePattern(redirect.old, pagePath);\n    if (match !== undefined) {\n      return redirect;\n    }\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/pubsub/create.test.ts",
    "content": "import { describe, test, expect, beforeEach, afterEach, vi } from \"vitest\";\nimport { createPubsub } from \"./create\";\n\ntype TestPublishMap = {\n  testAction: { value: string };\n  noPayloadAction: undefined;\n  numberAction: number;\n  storybook: string;\n  command: { source: string; name: string; [key: string]: unknown };\n  \"command:testCommand\": undefined;\n  \"command:commandWithPayload\": { data: string; count: number };\n  \"command:anotherCommand\": undefined;\n};\n\n// Helper to cast to Window type for test mocking\nconst asWindow = (obj: unknown) => obj as Window & typeof globalThis;\n\ndescribe(\"createPubsub\", () => {\n  let postMessageSpy: ReturnType<typeof vi.fn>;\n  let parentPostMessageSpy: ReturnType<typeof vi.fn>;\n  let addEventListenerSpy: ReturnType<typeof vi.fn>;\n\n  beforeEach(() => {\n    // Setup window and crypto\n    if (!global.window) {\n      (global as typeof globalThis).window = {} as Window & typeof globalThis;\n    }\n\n    // Mock crypto.getRandomValues\n    global.window.crypto = {\n      getRandomValues: vi.fn((arr: Uint8Array) => {\n        for (let i = 0; i < arr.length; i++) {\n          arr[i] = i;\n        }\n        return arr;\n      }),\n    } as unknown as Crypto;\n\n    // Mock requestAnimationFrame - execute callbacks immediately\n    global.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {\n      // Execute the callback immediately for testing\n      setTimeout(() => callback(0), 0);\n      return 0;\n    }) as typeof requestAnimationFrame;\n\n    postMessageSpy = vi.fn();\n    parentPostMessageSpy = vi.fn();\n    addEventListenerSpy = vi.fn();\n\n    global.window.postMessage = postMessageSpy;\n    global.window.addEventListener = addEventListenerSpy;\n    global.window.parent = {\n      postMessage: parentPostMessageSpy,\n    } as unknown as Window;\n\n    // Set NODE_ENV for consistent testing\n    process.env.NODE_ENV = \"test\";\n  });\n\n  afterEach(() => {\n    vi.clearAllMocks();\n    delete (global.window as Window & { __webstudio__$__api_token?: string })\n      .__webstudio__$__api_token;\n  });\n\n  describe(\"SSR environment\", () => {\n    test(\"should throw errors when used in SSR\", () => {\n      const originalWindow = global.window;\n      (global as typeof globalThis).window = undefined as unknown as Window &\n        typeof globalThis;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      expect(() =>\n        pubsub.publish({ type: \"testAction\", payload: { value: \"test\" } })\n      ).toThrow(\"publish is not available in this environment\");\n      expect(() => pubsub.usePublish()).toThrow(\n        \"usePublish is not available in this environment\"\n      );\n      expect(() => pubsub.useSubscribe(\"testAction\", () => {})).toThrow(\n        \"useSubscribe is not available in this environment\"\n      );\n      expect(() => pubsub.subscribe(\"testAction\", () => {})).toThrow(\n        \"subscribe is not available in this environment\"\n      );\n\n      global.window = originalWindow;\n    });\n  });\n\n  describe(\"token generation and validation\", () => {\n    test(\"should generate random token when window.self === window.top\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      createPubsub<TestPublishMap>();\n\n      expect(global.window.__webstudio__$__api_token).toBeDefined();\n    });\n\n    test(\"should use development token in non-production\", () => {\n      const originalNodeEnv = process.env.NODE_ENV;\n      process.env.NODE_ENV = \"development\";\n\n      global.window.self = asWindow({});\n      global.window.top = asWindow(global.window);\n\n      createPubsub<TestPublishMap>();\n\n      // In development, token should be \"development-token\"\n      expect(addEventListenerSpy).toHaveBeenCalledWith(\n        \"message\",\n        expect.any(Function),\n        false\n      );\n\n      process.env.NODE_ENV = originalNodeEnv;\n    });\n\n    test(\"should read token from top window when not top\", () => {\n      const mockToken = \"test-token\";\n      global.window.top = {\n        __webstudio__$__api_token: mockToken,\n      } as unknown as Window & typeof globalThis;\n      global.window.self = asWindow({});\n\n      createPubsub<TestPublishMap>();\n\n      // Token should be reset to undefined on canvas\n      expect(global.window.top.__webstudio__$__api_token).toBeUndefined();\n    });\n  });\n\n  describe(\"publish (Canvas -> Builder)\", () => {\n    test(\"should publish action to parent and self\", () => {\n      global.window.self = asWindow({});\n      global.window.top = asWindow(global.window);\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      // Clear the token since it gets reset in createPubsub for Canvas context\n      // Set it again after initialization\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      pubsub.publish({ type: \"testAction\", payload: { value: \"hello\" } });\n\n      expect(parentPostMessageSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          action: { type: \"testAction\", payload: { value: \"hello\" } },\n        }),\n        \"*\"\n      );\n      expect(postMessageSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          action: { type: \"testAction\", payload: { value: \"hello\" } },\n        }),\n        \"*\"\n      );\n    });\n\n    test(\"should throw error when publish is called from Builder (self === top)\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      expect(() =>\n        pubsub.publish({ type: \"testAction\", payload: { value: \"test\" } })\n      ).toThrow(\"publish is not available in the Builder environment\");\n    });\n\n    test(\"should handle action with no payload\", () => {\n      global.window.self = asWindow({});\n      global.window.top = asWindow(global.window);\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      // Set token again after initialization\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      pubsub.publish({ type: \"noPayloadAction\" });\n\n      expect(parentPostMessageSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          action: { type: \"noPayloadAction\" },\n        }),\n        \"*\"\n      );\n    });\n  });\n\n  describe(\"usePublish (Builder -> Canvas)\", () => {\n    test(\"should be available when self === top\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      expect(pubsub.usePublish).toBeDefined();\n      expect(typeof pubsub.usePublish).toBe(\"function\");\n    });\n\n    test(\"should be available for iframe context\", () => {\n      global.window.self = asWindow({});\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      // usePublish hook itself is available, but publish will throw when called from Canvas\n      expect(pubsub.usePublish).toBeDefined();\n    });\n  });\n\n  describe(\"subscribe\", () => {\n    test(\"should subscribe to messages and call handler\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"testAction\", handler);\n\n      // Get the message handler that was registered\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      // Simulate a message event\n      const mockEvent = {\n        data: {\n          action: { type: \"testAction\", payload: { value: \"test\" } },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      // Wait for requestAnimationFrame callback to execute\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler).toHaveBeenCalledWith({ value: \"test\" });\n    });\n\n    test(\"should return unsubscribe function\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      const unsubscribe = pubsub.subscribe(\"testAction\", handler);\n\n      expect(typeof unsubscribe).toBe(\"function\");\n\n      unsubscribe();\n\n      // Get the message handler\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      // Simulate a message event after unsubscribe\n      const mockEvent = {\n        data: {\n          action: { type: \"testAction\", payload: { value: \"test\" } },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      expect(handler).not.toHaveBeenCalled();\n    });\n\n    test(\"should handle multiple subscribers\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      // Create a fresh addEventListener spy for this test\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler1 = vi.fn();\n      const handler2 = vi.fn();\n\n      pubsub.subscribe(\"testAction\", handler1);\n      pubsub.subscribe(\"testAction\", handler2);\n\n      // Verify addEventListener was called\n      expect(localAddEventListenerSpy).toHaveBeenCalledTimes(1);\n\n      // Get the message handler from this pubsub instance\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      const mockEvent = {\n        data: {\n          action: { type: \"testAction\", payload: { value: \"test\" } },\n          token: \"development-token\",\n        },\n      };\n\n      // Call messageHandler and let it emit\n      messageHandler(mockEvent);\n\n      // Wait for requestAnimationFrame callback to execute\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler1).toHaveBeenCalledTimes(1);\n      expect(handler1).toHaveBeenCalledWith({ value: \"test\" });\n      expect(handler2).toHaveBeenCalledTimes(1);\n      expect(handler2).toHaveBeenCalledWith({ value: \"test\" });\n    });\n  });\n\n  describe(\"useSubscribe\", () => {\n    test(\"should be available as a hook function\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      expect(pubsub.useSubscribe).toBeDefined();\n      expect(typeof pubsub.useSubscribe).toBe(\"function\");\n    });\n  });\n\n  describe(\"message unwrapping and validation\", () => {\n    test(\"should reject invalid payload (not an object)\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"testAction\", handler);\n\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      // Mock console.error to avoid test output noise\n      const consoleErrorSpy = vi\n        .spyOn(console, \"error\")\n        .mockImplementation(() => {});\n\n      expect(() => {\n        messageHandler({ data: \"invalid\" });\n      }).toThrow(\"Invalid payload\");\n\n      expect(consoleErrorSpy).toHaveBeenCalledWith(\n        \"Invalid payload\",\n        \"invalid\"\n      );\n      consoleErrorSpy.mockRestore();\n    });\n\n    test(\"should reject payload without token\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"testAction\", handler);\n\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      expect(() => {\n        messageHandler({ data: { action: { type: \"testAction\" } } });\n      }).toThrow(\"Invalid payload, not wrapped\");\n    });\n\n    test(\"should reject payload with invalid token\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"testAction\", handler);\n\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      expect(() => {\n        messageHandler({\n          data: {\n            action: { type: \"testAction\" },\n            token: \"invalid-token\",\n          },\n        });\n      }).toThrow(\"Invalid token\");\n    });\n\n    test(\"should reject payload without action\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"testAction\", handler);\n\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      expect(() => {\n        messageHandler({\n          data: {\n            token: \"development-token\",\n          },\n        });\n      }).toThrow(\"Invalid payload, not wrapped\");\n    });\n\n    test(\"should hide token from subsequent subscribers\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"testAction\", handler);\n\n      const messageHandler = addEventListenerSpy.mock.calls[0][1];\n\n      const mockData = {\n        action: { type: \"testAction\", payload: { value: \"test\" } },\n        token: \"development-token\",\n      };\n\n      messageHandler({ data: mockData });\n\n      // Token should be set to undefined after unwrapping\n      expect(mockData.token).toBeUndefined();\n    });\n\n    test(\"should handle storybook environment\", async () => {\n      const originalIsStorybook = process.env.IS_STROYBOOK;\n      process.env.IS_STROYBOOK = \"true\";\n\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      // Create a fresh addEventListener spy for this test\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"storybook\", handler);\n\n      // Get the message handler from this pubsub instance\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      messageHandler({ data: \"storybook-data\" });\n\n      // Wait for requestAnimationFrame callback to execute\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler).toHaveBeenCalledWith(\"storybook-data\");\n\n      process.env.IS_STROYBOOK = originalIsStorybook;\n    });\n  });\n\n  describe(\"action types\", () => {\n    test(\"should handle action with undefined payload\", () => {\n      global.window.self = asWindow({});\n      global.window.top = asWindow(global.window);\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      // Set token again after initialization\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      pubsub.publish({ type: \"noPayloadAction\", payload: undefined });\n\n      expect(parentPostMessageSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          action: { type: \"noPayloadAction\", payload: undefined },\n        }),\n        \"*\"\n      );\n    });\n\n    test(\"should handle action with number payload\", () => {\n      global.window.self = asWindow({});\n      global.window.top = asWindow(global.window);\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      // Set token again after initialization\n      global.window.top.__webstudio__$__api_token = \"test-token\";\n\n      pubsub.publish({ type: \"numberAction\", payload: 42 });\n\n      expect(parentPostMessageSpy).toHaveBeenCalledWith(\n        expect.objectContaining({\n          action: { type: \"numberAction\", payload: 42 },\n        }),\n        \"*\"\n      );\n    });\n  });\n\n  describe(\"event listener registration\", () => {\n    test(\"should register message event listener on initialization\", () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      createPubsub<TestPublishMap>();\n\n      expect(addEventListenerSpy).toHaveBeenCalledWith(\n        \"message\",\n        expect.any(Function),\n        false\n      );\n    });\n  });\n\n  describe(\"command-specific subscriptions\", () => {\n    test(\"should subscribe to specific command and call handler\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"command:testCommand\", handler);\n\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      const mockEvent = {\n        data: {\n          action: {\n            type: \"command\",\n            payload: { source: \"builder\", name: \"testCommand\" },\n          },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      // Wait for requestAnimationFrame callback\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler).toHaveBeenCalledWith({\n        source: \"builder\",\n        name: \"testCommand\",\n      });\n    });\n\n    test(\"should only call handler for matching command\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler1 = vi.fn();\n      const handler2 = vi.fn();\n\n      pubsub.subscribe(\"command:testCommand\", handler1);\n      pubsub.subscribe(\"command:anotherCommand\", handler2);\n\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      // Send testCommand\n      const mockEvent = {\n        data: {\n          action: {\n            type: \"command\",\n            payload: { source: \"builder\", name: \"testCommand\" },\n          },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler1).toHaveBeenCalledTimes(1);\n      expect(handler2).not.toHaveBeenCalled();\n    });\n\n    test(\"should call both general command handler and specific command handler\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const generalHandler = vi.fn();\n      const specificHandler = vi.fn();\n\n      pubsub.subscribe(\"command\", generalHandler);\n      pubsub.subscribe(\"command:testCommand\", specificHandler);\n\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      const mockEvent = {\n        data: {\n          action: {\n            type: \"command\",\n            payload: { source: \"builder\", name: \"testCommand\" },\n          },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(generalHandler).toHaveBeenCalledWith({\n        source: \"builder\",\n        name: \"testCommand\",\n      });\n      expect(specificHandler).toHaveBeenCalledWith({\n        source: \"builder\",\n        name: \"testCommand\",\n      });\n    });\n\n    test(\"should unsubscribe from specific command\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      const unsubscribe = pubsub.subscribe(\"command:testCommand\", handler);\n\n      unsubscribe();\n\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      const mockEvent = {\n        data: {\n          action: {\n            type: \"command\",\n            payload: { source: \"builder\", name: \"testCommand\" },\n          },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler).not.toHaveBeenCalled();\n    });\n\n    test(\"should handle multiple subscribers to the same specific command\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler1 = vi.fn();\n      const handler2 = vi.fn();\n\n      pubsub.subscribe(\"command:testCommand\", handler1);\n      pubsub.subscribe(\"command:testCommand\", handler2);\n\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      const mockEvent = {\n        data: {\n          action: {\n            type: \"command\",\n            payload: { source: \"builder\", name: \"testCommand\" },\n          },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler1).toHaveBeenCalledWith({\n        source: \"builder\",\n        name: \"testCommand\",\n      });\n      expect(handler2).toHaveBeenCalledWith({\n        source: \"builder\",\n        name: \"testCommand\",\n      });\n    });\n\n    test(\"should pass payload to command-specific subscribers\", async () => {\n      global.window.self = asWindow(global.window);\n      global.window.top = asWindow(global.window);\n\n      const localAddEventListenerSpy = vi.fn();\n      global.window.addEventListener = localAddEventListenerSpy;\n\n      const pubsub = createPubsub<TestPublishMap>();\n\n      const handler = vi.fn();\n      pubsub.subscribe(\"command:commandWithPayload\", handler);\n\n      const messageHandler = localAddEventListenerSpy.mock.calls[0][1];\n\n      const mockEvent = {\n        data: {\n          action: {\n            type: \"command\",\n            payload: {\n              source: \"builder\",\n              name: \"commandWithPayload\",\n              data: \"test-data\",\n              count: 42,\n            },\n          },\n          token: \"development-token\",\n        },\n      };\n\n      messageHandler(mockEvent);\n\n      await new Promise((resolve) => setTimeout(resolve, 10));\n\n      expect(handler).toHaveBeenCalledWith({\n        source: \"builder\",\n        name: \"commandWithPayload\",\n        data: \"test-data\",\n        count: 42,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/pubsub/create.ts",
    "content": "import { createNanoEvents } from \"nanoevents\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport { batchUpdate } from \"./raf-queue\";\nimport { useEffectEvent } from \"../hook-utils/effect-event\";\nimport invariant from \"tiny-invariant\";\n\nconst apiTokenKey = \"__webstudio__$__api_token\";\n\ndeclare global {\n  interface Window {\n    [apiTokenKey]: string | undefined;\n  }\n}\n\nconst getRandomToken = () => {\n  const randomBytes = new Uint8Array(10);\n  window.crypto.getRandomValues(randomBytes);\n  return btoa(String.fromCharCode(...randomBytes));\n};\n\nexport const createPubsub = <PublishMap>() => {\n  type Action<Type extends keyof PublishMap> =\n    PublishMap[Type] extends undefined\n      ? { type: Type; payload?: undefined }\n      : { type: Type; payload: PublishMap[Type] };\n\n  if (typeof window === \"undefined\") {\n    return {\n      publish: () => {\n        throw new Error(\"publish is not available in this environment\");\n      },\n      usePublish: () => {\n        throw new Error(\"usePublish is not available in this environment\");\n      },\n      useSubscribe: () => {\n        throw new Error(\"useSubscribe is not available in this environment\");\n      },\n      subscribe: () => {\n        throw new Error(\"subscribe is not available in this environment\");\n      },\n    } as never; // Prevent type exposure\n  }\n\n  /**\n   * To avoid postMessage interception from the canvas, i.e., `globalThis.postMessage = () => console.log('INTERCEPTED');`,\n   */\n  const postMessageInternal = window.postMessage;\n  const parentPostMessageInternal = window.parent.postMessage;\n\n  /**\n   * Similar to a CSRF token, we use a token to ensure that the postMessage is coming from a trusted source.\n   */\n  let token =\n    window.self === window.top ? getRandomToken() : window.top?.[apiTokenKey];\n\n  if (window.top) {\n    // Initialize token at the Builder, reset it on the Canvas after reading\n    window.top[apiTokenKey] = window.self === window.top ? token : undefined;\n  }\n\n  // Use a fixed token in development to handle HMR updates consistently\n  if (process.env.NODE_ENV !== \"production\") {\n    token = \"development-token\";\n  }\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const emitter = createNanoEvents<Record<any, any>>();\n\n  const wrapAction = (action: unknown) => {\n    return { action, token };\n  };\n\n  const unwrapAction = (payload: unknown) => {\n    if (typeof payload !== \"object\" || payload === null) {\n      if (process.env.IS_STROYBOOK) {\n        return { type: \"storybook\", payload: payload } as Action<\n          keyof PublishMap\n        >;\n      }\n      console.error(\"Invalid payload\", payload);\n      throw new Error(\"Invalid payload\");\n    }\n\n    if (false === \"token\" in payload) {\n      throw new Error(\"Invalid payload, not wrapped\");\n    }\n\n    if (payload.token !== token) {\n      throw new Error(\"Invalid token\");\n    }\n\n    if (false === \"action\" in payload) {\n      throw new Error(\"Invalid payload, not wrapped\");\n    }\n\n    // Hide the token from the subsequent subscribers\n    payload.token = undefined;\n    return payload.action as Action<keyof PublishMap>;\n  };\n\n  const handleMessage = (event: MessageEvent) => {\n    const action = unwrapAction(event.data);\n    const type = action.type;\n    // Execute all updates within a single batch to improve performance\n    batchUpdate(() => {\n      emitter.emit(type, action.payload);\n\n      // Also emit command-specific events for type-safe subscriptions\n      if (\n        type === \"command\" &&\n        action.payload &&\n        typeof action.payload === \"object\" &&\n        \"name\" in action.payload\n      ) {\n        const commandName = (action.payload as { name: string }).name;\n        // Pass the full payload to command-specific subscribers\n        emitter.emit(\n          `command:${commandName}` as keyof PublishMap,\n          action.payload\n        );\n      }\n    });\n  };\n\n  window.addEventListener(\"message\", handleMessage, false);\n\n  return {\n    /**\n     * To publish a postMessage event on the current window and parent window from the iframe.\n     */\n    publish<Type extends keyof PublishMap>(action: Action<Type>) {\n      invariant(\n        window.self !== window.top,\n        \"publish is not available in the Builder environment\"\n      );\n\n      parentPostMessageInternal(wrapAction(action), \"*\");\n      postMessageInternal(wrapAction(action), \"*\");\n    },\n\n    /**\n     * To publish a postMessage event on the iframe and parent window from the parent window.\n     */\n    usePublish() {\n      const postMessageRef = useRef<undefined | typeof window.postMessage>(\n        undefined\n      );\n\n      const iframeRefCallback = useCallback(\n        (element: HTMLIFrameElement | null) => {\n          if (element == null) {\n            postMessageRef.current = undefined;\n            return;\n          }\n          /**\n           * To avoid postMessage interception from the canvas, i.e., `globalThis.postMessage = () => console.log('INTERCEPTED');`,\n           */\n          postMessageRef.current = element.contentWindow!.postMessage;\n        },\n        []\n      );\n\n      const publish = useCallback(\n        <Type extends keyof PublishMap>(action: Action<Type>) => {\n          invariant(\n            window.self === window.top,\n            \"publish is not available in the Canvas environment\"\n          );\n\n          if (postMessageRef.current === undefined) {\n            return;\n          }\n          // This ensures the method is called with the correct context.\n          const postMessageIframe = postMessageRef.current;\n\n          postMessageIframe(wrapAction(action), \"*\");\n          postMessageInternal(wrapAction(action), \"*\");\n        },\n        []\n      );\n      return [publish, iframeRefCallback] as const;\n    },\n\n    /**\n     * To subscribe a message event on the current window.\n     */\n    useSubscribe<Type extends keyof PublishMap>(\n      type: Type,\n      onAction: (payload: PublishMap[Type]) => void\n    ) {\n      const handleOnAction = useEffectEvent(onAction);\n\n      useEffect(() => {\n        return emitter.on(type, handleOnAction);\n      }, [type]);\n    },\n\n    subscribe<Type extends keyof PublishMap>(\n      type: Type,\n      onAction: (payload: PublishMap[Type]) => void\n    ) {\n      return emitter.on(type, onAction);\n    },\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/pubsub/index.ts",
    "content": "import { atom } from \"nanostores\";\nimport { createPubsub } from \"./create\";\n\n// Allow commands to declare their types\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface CommandRegistry {}\n\n// Generate namespaced command types from CommandRegistry\ntype NamespacedCommands = {\n  [K in keyof CommandRegistry as `command:${K & string}`]: CommandRegistry[K];\n};\n\nexport interface PubsubMap extends NamespacedCommands {\n  command: {\n    source: string;\n    name: string;\n    [key: string]: unknown;\n  };\n}\n\nexport const { publish, usePublish, useSubscribe, subscribe } =\n  createPubsub<PubsubMap>();\nexport type Publish = typeof publish;\nexport type UsePublish = typeof usePublish;\n\nexport const $publisher = atom<{ publish?: Publish }>({});\n"
  },
  {
    "path": "apps/builder/app/shared/pubsub/raf-queue.ts",
    "content": "type Task = () => void;\n\nlet handle: ReturnType<typeof requestAnimationFrame> | undefined;\nlet updateQueue: Task[] = [];\n\nconst processUpdates = (updates: Task[]) => {\n  for (const update of updates) {\n    update();\n  }\n};\n\nexport const batchUpdate = (update: () => void) => {\n  updateQueue.push(update);\n\n  if (handle !== undefined) {\n    return;\n  }\n\n  handle = requestAnimationFrame(() => {\n    const updates = updateQueue;\n    updateQueue = [];\n    handle = undefined;\n    processUpdates(updates);\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/README.md",
    "content": "# Redirect Parsers\n\nImport redirects from popular formats (CSV, JSON, Netlify `_redirects`, Apache `.htaccess`) into Webstudio.\n\n## Webstudio Redirect Format\n\nWebstudio stores redirects with this schema:\n\n```typescript\ntype PageRedirect = {\n  old: string; // Source path (must start with /, cannot end with /)\n  new: string; // Target path or URL\n  status?: \"301\" | \"302\"; // Optional, defaults to 301\n};\n```\n\n### Path Requirements\n\n| Requirement         | Valid              | Invalid                |\n| ------------------- | ------------------ | ---------------------- |\n| Must start with `/` | `/about`           | `about`                |\n| Cannot end with `/` | `/about`           | `/about/`              |\n| No double slashes   | `/a/b/c`           | `/a//b`                |\n| Allowed chars       | `a-zA-Z0-9-_/:?.*` | `<>{}`                 |\n| Reserved paths      | `/home`            | `/s/...`, `/build/...` |\n\n### Slash Normalization\n\nThe parser **strips trailing slashes** from source paths to match Webstudio's requirements:\n\n```\nInput:  /old-path/  →  Output: /old-path\nInput:  /old-path   →  Output: /old-path\n```\n\nTarget paths preserve their original form (Webstudio allows trailing slashes in targets).\n\n---\n\n## Supported Formats\n\n### 1. CSV\n\n**Platforms:** Shopify, HubSpot, WordPress (Redirection plugin), manual exports\n\n#### Column Name Variations\n\n| Source Column   | Target Column | Status Column |\n| --------------- | ------------- | ------------- |\n| `source`        | `target`      | `status`      |\n| `from`          | `to`          | `code`        |\n| `old`           | `new`         | `statuscode`  |\n| `redirect from` | `redirect to` |               |\n| `original url`  | `target url`  |               |\n| `original url`  | `destination` |               |\n\n#### Shopify Format\n\n```csv\nRedirect from,Redirect to\n/old-product,/new-product\n/old-collection,/new-collection\n```\n\n- No status column (all redirects are 301)\n\n#### HubSpot Format\n\n```csv\nOriginal URL,Target URL\n/old-landing,/new-landing\n/old-blog-post,/blog/new-post\n```\n\n- No status column (all redirects are 301)\n\n#### Generic Format\n\n```csv\nsource,target,status\n/page1,/newpage1,301\n/page2,/newpage2,302\n/page3,/newpage3,permanent\n/page4,/newpage4,temporary\n```\n\n---\n\n### 2. JSON\n\n**Platforms:** Vercel, Next.js, WordPress (Redirection plugin), custom exports\n\n#### Vercel / Next.js Format\n\n```json\n{\n  \"redirects\": [\n    {\n      \"source\": \"/old-page\",\n      \"destination\": \"/new-page\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/temp-redirect\",\n      \"destination\": \"/landing\",\n      \"permanent\": false\n    }\n  ]\n}\n```\n\n#### Next.js with statusCode\n\n```json\n{\n  \"redirects\": [\n    {\n      \"source\": \"/old-page\",\n      \"destination\": \"/new-page\",\n      \"statusCode\": 301\n    }\n  ]\n}\n```\n\n#### Generic Array Format\n\n```json\n[\n  { \"from\": \"/old\", \"to\": \"/new\", \"status\": 301 },\n  { \"source\": \"/a\", \"target\": \"/b\", \"code\": 302 },\n  { \"old\": \"/x\", \"new\": \"/y\" }\n]\n```\n\n#### Key Name Variations\n\n| Source Keys | Target Keys   | Status Keys        |\n| ----------- | ------------- | ------------------ |\n| `source`    | `destination` | `statusCode`       |\n| `from`      | `target`      | `status`           |\n| `old`       | `to`          | `code`             |\n|             | `new`         | `permanent` (bool) |\n\n---\n\n### 3. Netlify `_redirects`\n\n**Platform:** Netlify\n\n#### Format\n\n```\n# Comment\n/source /destination [status]\n```\n\n#### Examples\n\n```\n# Basic redirects\n/old /new 301\n/temp /landing 302\n\n# No status (defaults to 301)\n/about-us /about\n\n# External URLs\n/github https://github.com/example 301\n\n# Force redirect (! is ignored)\n/override /new 301!\n```\n\n---\n\n### 4. Apache `.htaccess`\n\n**Platforms:** Apache, cPanel, traditional hosting\n\n#### Supported Directives\n\n```apache\nRedirect 301 /old-page /new-page\nRedirect 302 /temp-page /landing\nRedirect permanent /legacy /modern\nRedirect temp /maintenance /coming-soon\nRedirect /simple /destination  # defaults to 302\n```\n\n#### Case Insensitive\n\n```apache\nredirect 301 /lowercase /target\nREDIRECT 301 /uppercase /target\n```\n\n---\n\n## Status Code Handling\n\n### Supported Codes\n\n| Code | Meaning                     | Webstudio               |\n| ---- | --------------------------- | ----------------------- |\n| 301  | Permanent redirect          | ✅ `\"301\"`              |\n| 302  | Temporary redirect          | ✅ `\"302\"`              |\n| 307  | Temporary (preserve method) | ⚠️ Converted to `\"302\"` |\n| 308  | Permanent (preserve method) | ⚠️ Converted to `\"301\"` |\n\n### Text Status Values\n\n| Text                      | Converted To |\n| ------------------------- | ------------ |\n| `permanent`               | `\"301\"`      |\n| `temporary`               | `\"302\"`      |\n| `true` (permanent field)  | `\"301\"`      |\n| `false` (permanent field) | `\"302\"`      |\n\n### Skipped Codes\n\n| Code  | Reason                             |\n| ----- | ---------------------------------- |\n| 200   | Rewrite (proxy without URL change) |\n| 404   | Not found                          |\n| Other | Invalid redirect status            |\n\n---\n\n## Unsupported Features\n\nThese patterns are **skipped with a warning** (not imported):\n\n### Dynamic Placeholders\n\n```\n/blog/:slug → /posts/:slug\n/news/:year/:month/:slug → /archive/:year-:month-:slug\n```\n\n### Splat Wildcards\n\n```\n/docs/* → /documentation/:splat\n/old/* → /new/*\n```\n\n### Conditions\n\n```\n# Netlify\n/ /en 302 Country=us\n/admin /login 302 Role=admin\n\n# Vercel/Next.js\n{\n  \"source\": \"/\",\n  \"has\": [{ \"type\": \"header\", \"key\": \"x-country\", \"value\": \"DE\" }],\n  \"destination\": \"/de\"\n}\n```\n\n### Query Parameter Matching\n\n```\n/store id=:id → /products/:id\n```\n\n### Apache Regex Rules\n\n```apache\nRedirectMatch ^/blog/(.*)$ /posts/$1\nRewriteRule ^old/(.*)$ /new/$1 [R=301,L]\n```\n\n---\n\n## User Documentation\n\n_This section contains user-friendly copy for the Import dialog and help content._\n\n### Supported File Formats\n\n**CSV** — Spreadsheet exports from Shopify, HubSpot, WordPress, or any custom CSV with columns for source path, target path, and optional status code.\n\n**JSON** — Configuration exports from Vercel, Next.js, or any JSON array of redirect objects.\n\n**Netlify \\_redirects** — The `_redirects` file format used by Netlify hosting.\n\n**Apache .htaccess** — Redirect directives from Apache server configuration files.\n\n### How It Works\n\n1. Upload or paste your redirect file\n2. We automatically detect the format\n3. Review the parsed redirects before importing\n4. Choose to add to or replace your existing redirects\n\n### Supported Routing Patterns\n\nWebstudio supports these path patterns in redirects:\n\n| Pattern    | Example   | Matches                               |\n| ---------- | --------- | ------------------------------------- |\n| Exact path | `/about`  | Only `/about`                         |\n| Wildcard   | `/blog/*` | `/blog/post`, `/blog/2024/post`, etc. |\n| Segment    | `/:slug`  | `/anything` (single segment)          |\n| Optional   | `/:id?`   | `/` or `/123`                         |\n\n**Important:** Captured values (like `:slug`) cannot be inserted into the destination path. The destination is always a fixed path or URL.\n\n### What Gets Imported\n\n✅ **Imported:**\n\n- Simple path-to-path redirects (`/old` → `/new`)\n- Redirects to external URLs (`/github` → `https://github.com/...`)\n- 301 (permanent) and 302 (temporary) status codes\n- 307 and 308 status codes (converted to 302 and 301)\n\n### What Gets Skipped\n\n⚠️ **Skipped (with explanation):**\n\n- Dynamic placeholders like `/blog/:slug` → `/posts/:slug`\n- Wildcard substitutions like `/old/*` → `/new/*`\n- Conditional redirects (country, header, cookie-based)\n- Query parameter matching\n- Regex-based rules (Apache RewriteRule, RedirectMatch)\n- Rewrites (status 200) and \"not found\" rules (status 404)\n\n### Limitations vs. Other Platforms\n\nWebstudio redirects are designed for simplicity and performance. Some advanced features from other platforms are not supported:\n\n| Feature                  | Netlify | Vercel | Apache | Webstudio |\n| ------------------------ | ------- | ------ | ------ | --------- |\n| Simple redirects         | ✅      | ✅     | ✅     | ✅        |\n| External URLs            | ✅      | ✅     | ✅     | ✅        |\n| Wildcard source          | ✅      | ✅     | ✅     | ✅        |\n| Wildcard substitution    | ✅      | ✅     | ✅     | ❌        |\n| Regex patterns           | ❌      | ❌     | ✅     | ❌        |\n| Header/cookie conditions | ✅      | ✅     | ✅     | ❌        |\n| Geo/country conditions   | ✅      | ✅     | ❌     | ❌        |\n| Query string matching    | ✅      | ✅     | ✅     | ❌        |\n| Rewrites (proxy)         | ✅      | ✅     | ✅     | ❌        |\n\n### Status Codes Explained\n\n| Code | Name      | When to Use                                          |\n| ---- | --------- | ---------------------------------------------------- |\n| 301  | Permanent | Page has moved forever. SEO transfers to new URL.    |\n| 302  | Temporary | Page is temporarily elsewhere. SEO stays at old URL. |\n\n---\n\n## Auto-Detection Priority\n\nThe parser detects formats in this order:\n\n1. **JSON** - Content starts with `[` or `{`\n2. **CSV** - First line contains known column headers\n3. **htaccess** - Any line starts with `Redirect` (case-insensitive)\n4. **Netlify** - Lines match pattern `/path /path [status]`\n\n---\n\n## UI Plan\n\n### Location\n\nAdd an \"Import\" button next to the \"Redirects\" title in `section-redirects.tsx`.\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  Redirects  ℹ️                              [Import]        │\n│  Redirect old URLs to new ones...                           │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Import Dialog\n\nWhen clicking \"Import\", open a dialog with:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  Import Redirects                                      [X]  │\n├─────────────────────────────────────────────────────────────┤\n│                                                             │\n│  Paste or drop a file containing redirects.                 │\n│  Supports: CSV, JSON, Netlify _redirects, Apache .htaccess  │\n│                                                             │\n│  ┌─────────────────────────────────────────────────────┐    │\n│  │                                                     │    │\n│  │     [Upload File]  or drag & drop here              │    │\n│  │                                                     │    │\n│  └─────────────────────────────────────────────────────┘    │\n│                                                             │\n│  ── OR ──                                                   │\n│                                                             │\n│  ┌─────────────────────────────────────────────────────┐    │\n│  │ Paste content here...                               │    │\n│  │                                                     │    │\n│  │                                                     │    │\n│  └─────────────────────────────────────────────────────┘    │\n│                                                             │\n│                                          [Cancel] [Parse]   │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Preview State\n\nAfter parsing, show a preview before importing:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  Import Redirects                                      [X]  │\n├─────────────────────────────────────────────────────────────┤\n│                                                             │\n│  ✓ Found 47 redirects                                       │\n│  ⚠ 3 lines skipped (see below)                              │\n│                                                             │\n│  Preview:                                                   │\n│  ┌─────────────────────────────────────────────────────┐    │\n│  │  /old-page        301  →  /new-page                 │    │\n│  │  /about-us        301  →  /about                    │    │\n│  │  /blog/post-1     302  →  /posts/1                  │    │\n│  │  ... (44 more)                                      │    │\n│  └─────────────────────────────────────────────────────┘    │\n│                                                             │\n│  Skipped:                                                   │\n│  ┌─────────────────────────────────────────────────────┐    │\n│  │  Line 5: /docs/* → wildcard patterns not supported  │    │\n│  │  Line 8: /:slug → placeholders not supported        │    │\n│  │  Line 12: missing target path                       │    │\n│  └─────────────────────────────────────────────────────┘    │\n│                                                             │\n│  ○ Add to existing redirects                                │\n│  ○ Replace all redirects                                    │\n│                                                             │\n│                                        [Back] [Import 47]   │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Conflict Handling\n\nIf importing redirects that already exist:\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  ⚠ 5 redirects have the same source path as existing ones  │\n│                                                             │\n│  ○ Skip duplicates (import 42 new)                          │\n│  ○ Overwrite existing (update 5, import 42 new)             │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Component Structure\n\n```\nsection-redirects.tsx\n├── ImportButton                    # Opens the dialog\n└── ImportRedirectsDialog           # The dialog component\n    ├── InputStep                   # File upload / paste textarea\n    │   ├── DropZone               # Drag & drop file area\n    │   ├── FileInput              # Hidden file input\n    │   └── TextArea               # Paste content\n    ├── PreviewStep                 # Shows parsed results\n    │   ├── SuccessSummary         # \"Found X redirects\"\n    │   ├── RedirectPreviewList    # Scrollable list of redirects\n    │   ├── SkippedList            # Collapsed/expandable skipped items\n    │   └── MergeOptions           # Radio: add vs replace\n    └── useImportRedirects          # Hook managing dialog state\n```\n\n### State Machine\n\n```\n                    ┌──────────┐\n                    │  CLOSED  │\n                    └────┬─────┘\n                         │ open\n                         ▼\n                    ┌──────────┐\n              ┌─────│  INPUT   │◄────────┐\n              │     └────┬─────┘         │\n              │          │ parse         │ back\n              │          ▼               │\n              │     ┌──────────┐         │\n              │     │ PARSING  │         │\n              │     └────┬─────┘         │\n              │          │               │\n              │     ┌────┴────┐          │\n              │     ▼         ▼          │\n              │ ┌───────┐ ┌─────────┐    │\n              │ │ ERROR │ │ PREVIEW │────┘\n              │ └───┬───┘ └────┬────┘\n              │     │          │ import\n              │     │          ▼\n              │     │     ┌──────────┐\n              │     │     │ IMPORTED │\n              │     │     └────┬─────┘\n              │     │          │\n              └─────┴──────────┘\n                         │ close\n                         ▼\n                    ┌──────────┐\n                    │  CLOSED  │\n                    └──────────┘\n```\n\n### Design System Components\n\nFrom `@webstudio-is/design-system`:\n\n- `Dialog`, `DialogContent`, `DialogTitle`, `DialogClose`\n- `Button`, `Text`, `Flex`, `Grid`, `Box`\n- `TextArea` (for paste input)\n- `RadioGroup` (for merge options)\n- `ScrollArea` (for preview list)\n- `PanelBanner` (for warnings/errors)\n\n### Acceptance Criteria\n\n1. **File Upload**: Accept `.csv`, `.json`, `.txt`, `.htaccess` files\n2. **Drag & Drop**: Support dropping files onto the dialog\n3. **Paste**: Support pasting content directly\n4. **Auto-detect**: Automatically detect format from content\n5. **Preview**: Show parsed redirects before importing\n6. **Skipped Feedback**: Show why lines were skipped with line numbers\n7. **Merge Options**: Let user choose add vs replace\n8. **Duplicate Handling**: Detect and handle conflicts with existing redirects\n9. **Validation**: Run Webstudio's `OldPagePath` validation on import\n10. **Toast Feedback**: Show success/error toast after import\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/apache.htaccess",
    "content": "# Apache .htaccess Redirect Configuration\n# =========================================\n# Comprehensive test fixture for Apache redirect directives\n# Docs: https://httpd.apache.org/docs/current/mod/mod_alias.html\n\n# -----------------------------------------------------------------\n# ENABLE REWRITE ENGINE (ignored by parser)\n# -----------------------------------------------------------------\nRewriteEngine On\nRewriteBase /\n\n# -----------------------------------------------------------------\n# BASIC REDIRECTS (supported)\n# -----------------------------------------------------------------\n\n# Simple redirects with status codes\nRedirect 301 /old-page /new-page\nRedirect 302 /temp-page /landing\nRedirect 301 /about-us /about\nRedirect 302 /contact-us /contact\n\n# Redirect with permanent/temp keywords\nRedirect permanent /legacy /modern\nRedirect temp /maintenance /coming-soon\n\n# Redirect without status (defaults to 302 in Apache)\nRedirect /simple /destination\n\n# External URL redirects\nRedirect 301 /github https://github.com/example/repo\nRedirect 301 /docs https://docs.example.com/\n\n# -----------------------------------------------------------------\n# CASE VARIATIONS (all should work)\n# -----------------------------------------------------------------\nredirect 301 /lowercase /target1\nREDIRECT 301 /uppercase /target2\n\n# -----------------------------------------------------------------\n# WHITESPACE VARIATIONS\n# -----------------------------------------------------------------\nRedirect    301    /extra-spaces    /target3\n\n# -----------------------------------------------------------------\n# STATUS CODE CONVERSION\n# -----------------------------------------------------------------\nRedirect 307 /temp-307 /temp-destination\nRedirect 308 /perm-308 /perm-destination\n\n# -----------------------------------------------------------------\n# REDIRECTMATCH - REGEX (skipped with warning)\n# -----------------------------------------------------------------\nRedirectMatch 301 ^/blog/([0-9]+)/([0-9]+)/(.*)$ /posts/$1-$2-$3\nRedirectMatch ^/category/(.*)$ /topics/$1\n\n# -----------------------------------------------------------------\n# REWRITERULE (skipped with warning)\n# -----------------------------------------------------------------\nRewriteRule ^old/(.*)$ /new/$1 [R=301,L]\nRewriteRule ^api/(.*)$ https://api.example.com/$1 [P]\nRewriteCond %{HTTP_HOST} ^www\\.example\\.com$ [NC]\nRewriteRule ^(.*)$ https://example.com/$1 [R=301,L]\n\n# -----------------------------------------------------------------\n# OTHER DIRECTIVES (ignored, not reported as skipped)\n# -----------------------------------------------------------------\nOptions +FollowSymLinks\nErrorDocument 404 /404.html\nHeader set X-Frame-Options \"SAMEORIGIN\"\n\n# -----------------------------------------------------------------\n# MORE REDIRECTS AFTER OTHER DIRECTIVES\n# -----------------------------------------------------------------\nRedirect 301 /final-redirect /final-destination\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/generic.csv",
    "content": "source,target,status\n# CSV Redirects - Comprehensive Test Fixture\n# Columns: source,target,status (generic naming)\n# Other supported column names shown in sections below\n\n# -----------------------------------------------------------------\n# BASIC REDIRECTS (supported)\n# -----------------------------------------------------------------\n/page1,/newpage1,301\n/page2,/newpage2,302\n/temp-page,/landing,307\n/perm-page,/destination,308\n\n# -----------------------------------------------------------------\n# TEXT STATUS VALUES\n# -----------------------------------------------------------------\n/text-permanent,/target1,permanent\n/text-temporary,/target2,temporary\n\n# -----------------------------------------------------------------\n# NO STATUS (defaults to 301)\n# -----------------------------------------------------------------\n/no-status,/default-301,\n\n# -----------------------------------------------------------------\n# EXTERNAL URLS\n# -----------------------------------------------------------------\n/external,https://example.com/external,301\n/github,https://github.com/example,301\n\n# -----------------------------------------------------------------\n# SPECIAL CHARACTERS\n# -----------------------------------------------------------------\n/path-with-query?ref=old,/path-with-query?ref=new,301\n/url%20encoded,/url-encoded,301\n\"/path,with,commas\",\"/new-path\",301\n\"/path \"\"with quotes\"\"\",\"/target\",301\n\n# -----------------------------------------------------------------\n# TRAILING SLASH (normalized)\n# -----------------------------------------------------------------\n/trailing/,/trailing-new,301\n\n# -----------------------------------------------------------------\n# ROOT PATH\n# -----------------------------------------------------------------\n/,/home,301\n\n# -----------------------------------------------------------------\n# INVALID ENTRIES (skipped)\n# -----------------------------------------------------------------\ninvalid-no-slash,/valid,301\n,/missing-source,301\n/missing-target,,301\n/invalid-status,/target,999\n/rewrite,/proxy,200\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/generic.json",
    "content": "[\n  {\n    \"$comment\": \"Generic JSON array - various key naming conventions\",\n    \"$sources\": \"WordPress Redirection plugin exports, custom tools, manual exports\"\n  },\n\n  {\n    \"$comment\": \"from/to naming (common generic format)\",\n    \"from\": \"/legacy/endpoint\",\n    \"to\": \"/modern/endpoint\",\n    \"status\": 301\n  },\n  {\n    \"from\": \"/old-api\",\n    \"to\": \"/api/v2\",\n    \"status\": 302\n  },\n\n  {\n    \"$comment\": \"source/destination naming (Vercel-style)\",\n    \"source\": \"/v1/users\",\n    \"destination\": \"/api/v2/users\",\n    \"code\": 301\n  },\n  {\n    \"source\": \"/v1/posts\",\n    \"destination\": \"/api/v2/posts\",\n    \"code\": 308\n  },\n\n  {\n    \"$comment\": \"source/target naming (alternative)\",\n    \"source\": \"/redirect-permanent\",\n    \"target\": \"/destination\",\n    \"permanent\": true\n  },\n  {\n    \"source\": \"/redirect-temporary\",\n    \"target\": \"/landing\",\n    \"permanent\": false\n  },\n\n  {\n    \"$comment\": \"old/new naming (Webstudio native format)\",\n    \"old\": \"/deprecated\",\n    \"new\": \"/current\",\n    \"status\": 302\n  },\n\n  {\n    \"$comment\": \"No status (defaults to 301)\",\n    \"source\": \"/no-status\",\n    \"target\": \"/should-default-301\"\n  },\n\n  {\n    \"$comment\": \"External URLs\",\n    \"source\": \"/external\",\n    \"target\": \"https://external.example.com/page\"\n  },\n\n  {\n    \"$comment\": \"INVALID ENTRIES (skipped with reason)\",\n    \"source\": \"invalid-no-slash\",\n    \"target\": \"/valid\",\n    \"status\": 301\n  },\n  {\n    \"$comment\": \"Missing source field\",\n    \"target\": \"/missing-source\",\n    \"status\": 301\n  },\n  {\n    \"$comment\": \"Missing target field\",\n    \"source\": \"/missing-target\",\n    \"status\": 301\n  },\n  {\n    \"$comment\": \"Invalid status code\",\n    \"source\": \"/invalid-status\",\n    \"target\": \"/destination\",\n    \"status\": 999\n  },\n  {\n    \"$comment\": \"Rewrite status (skipped)\",\n    \"source\": \"/rewrite-status\",\n    \"target\": \"/proxy\",\n    \"status\": 200\n  },\n\n  {\n    \"$comment\": \"Status code conversion\",\n    \"source\": \"/status-307\",\n    \"target\": \"/temp\",\n    \"status\": 307\n  },\n  {\n    \"source\": \"/status-308\",\n    \"target\": \"/perm\",\n    \"status\": 308\n  }\n]\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/hubspot.csv",
    "content": "Original URL,Target URL\n# HubSpot URL Mapping Export Format\n# Columns: \"Original URL\", \"Target URL\"\n# Note: HubSpot does not export status codes (all are 301)\n# Docs: https://knowledge.hubspot.com/website-pages/set-up-url-redirects\n\n/old-landing,/new-landing\n/blog/2023/post-title,/blog/post-title\n/resources/ebook-download,/resources/guides/ebook\n/pricing-old,/pricing\n/demo-request,/get-started\n/case-studies/company-a,/customers/company-a\n/webinar-archive,/resources/webinars\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/netlify._redirects",
    "content": "# Netlify _redirects Format - Comprehensive Test Fixture\n# ========================================================\n# Syntax: /from /to [status]\n# Official docs: https://docs.netlify.com/routing/redirects/\n\n# -----------------------------------------------------------------\n# BASIC REDIRECTS (supported)\n# -----------------------------------------------------------------\n\n# Simple permanent redirects (301)\n/home /\n/about-us /about 301\n/contact-us /contact 301\n\n# Temporary redirects (302)\n/sale /offers 302\n/promo /campaigns 302\n\n# No status code (defaults to 301)\n/default /target\n\n# External URL redirects\n/github https://github.com/example/repo 301\n/twitter https://twitter.com/example\n\n# -----------------------------------------------------------------\n# COMMENTS AND WHITESPACE\n# -----------------------------------------------------------------\n\n# This is a comment\n  # Indented comment\n\n   /indented-source /indented-target 301\n\n/lots-of-space    /destination   301\n\n# Empty lines are ignored\n\n\n# -----------------------------------------------------------------\n# STATUS CODES\n# -----------------------------------------------------------------\n\n/moved /new-location 301\n/found /temporary 302\n/temp-redirect /temp 307\n/perm-redirect /perm 308\n\n# -----------------------------------------------------------------\n# PATH PATTERNS (skipped - unsupported)\n# -----------------------------------------------------------------\n\n# Splat wildcards\n/blog/* /posts/:splat 301\n/old-docs/* /docs/:splat\n\n# Placeholders\n/news/:year/:month/:day/:slug /blog/:year-:month-:day-:slug 301\n/products/:category/:id /shop/:category/item/:id\n\n# Mixed patterns from real-world (Astro docs)\n/:lang/install/auto /:lang/install-and-setup/\n/:lang/guides/deploy/:service /:lang/deploy/:service\n/docs/* /:splat\n/:lang/docs/* /:lang/:splat\n\n# -----------------------------------------------------------------\n# CONDITIONS (skipped - unsupported)\n# -----------------------------------------------------------------\n\n# Country-based\n/ /en 302 Country=us,ca,gb\n/ /de 302 Country=de,at,ch\n\n# Language-based\n/products /en/products 301 Language=en\n\n# Role-based\n/admin/* /login 302 Role=admin\n\n# -----------------------------------------------------------------\n# REWRITES (skipped - status 200)\n# -----------------------------------------------------------------\n\n/api/* https://api.example.com/:splat 200\n\n# -----------------------------------------------------------------\n# FORCE REDIRECTS (the ! is ignored, redirect is processed)\n# -----------------------------------------------------------------\n\n/override /new-override 301!\n\n# -----------------------------------------------------------------\n# QUERY PARAMETER MATCHING (skipped - unsupported)\n# -----------------------------------------------------------------\n\n/store id=:id /products/:id 301\n/search q=:query /results?search=:query\n\n# -----------------------------------------------------------------\n# TRAILING SLASH HANDLING\n# -----------------------------------------------------------------\n\n# Will be normalized (trailing slash stripped from source)\n/old-path/ /new-path 301\n\n# -----------------------------------------------------------------\n# 404 CATCH-ALLS (skipped - status 404)\n# -----------------------------------------------------------------\n\n/:lang/* /:lang/404/ 404\n\n# -----------------------------------------------------------------\n# REAL-WORLD EXAMPLES (from Astro, React Router, Redux)\n# -----------------------------------------------------------------\n\n# Simple path moves (Astro)\n/en/getting-started/quick-start /en/install-and-setup/\n/reference/renderer-reference /en/reference/integrations-reference/\n/en/core-concepts/component-hydration /en/concepts/islands/\n\n# External API docs (React Router)\n/en/main/routers/create-browser-router https://api.reactrouter.com/v7/functions/react_router.createBrowserRouter.html\n/en/main/hooks/use-navigate https://api.reactrouter.com/v7/functions/react_router.useNavigate.html\n\n# Tutorial migrations (Redux)\n/basics/usagewithreact /tutorials/fundamentals/part-5-ui-react\n/advanced/asyncactions /tutorials/fundamentals/part-6-async-logic\n/recipes/computingderiveddata /usage/deriving-data-selectors\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/shopify.csv",
    "content": "Redirect from,Redirect to\n# Shopify URL Redirect Export Format\n# Columns: \"Redirect from\", \"Redirect to\"\n# Note: Shopify does not export status codes (all are 301)\n# Docs: https://help.shopify.com/en/manual/online-store/menus-and-links/url-redirect\n\n/products/old-product,/products/new-product\n/collections/summer-2024,/collections/summer-2025\n/pages/about-old,/pages/about\n/blogs/news/old-post,/blogs/news/new-post\n/products/discontinued-item,/collections/clearance\n/products/variant-123,/products/new-variant\n/collections/sale-2023,/collections/archive\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/fixtures/vercel-nextjs.json",
    "content": "{\n  \"$comment\": \"JSON Redirects - Comprehensive Test Fixture\",\n  \"$formats\": \"Vercel vercel.json, Next.js next.config.js, generic array format\",\n  \"$docs\": \"https://vercel.com/docs/edge-network/redirects, https://nextjs.org/docs/app/api-reference/next-config-js/redirects\",\n\n  \"redirects\": [\n    {\n      \"$comment\": \"BASIC REDIRECTS (supported)\",\n      \"source\": \"/old-page\",\n      \"destination\": \"/new-page\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/temp-redirect\",\n      \"destination\": \"/landing\",\n      \"permanent\": false\n    },\n    {\n      \"$comment\": \"Vercel/Next.js statusCode variant\",\n      \"source\": \"/with-status-code\",\n      \"destination\": \"/target\",\n      \"statusCode\": 301\n    },\n    {\n      \"source\": \"/temp-status-code\",\n      \"destination\": \"/other-target\",\n      \"statusCode\": 302\n    },\n    {\n      \"$comment\": \"External URLs\",\n      \"source\": \"/external\",\n      \"destination\": \"https://example.com\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/docs-external\",\n      \"destination\": \"https://docs.example.com/\",\n      \"permanent\": true\n    },\n\n    {\n      \"$comment\": \"DYNAMIC PATTERNS (skipped - unsupported)\",\n      \"source\": \"/blog/:slug\",\n      \"destination\": \"/posts/:slug\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/docs/:path*\",\n      \"destination\": \"https://docs.example.com/:path*\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/old-blog/:year/:month/:slug\",\n      \"destination\": \"/blog/:year-:month-:slug\",\n      \"permanent\": true\n    },\n\n    {\n      \"$comment\": \"CONDITIONS (skipped - unsupported)\",\n      \"source\": \"/with-query\",\n      \"has\": [\n        {\n          \"type\": \"query\",\n          \"key\": \"page\"\n        }\n      ],\n      \"destination\": \"/paginated?p=:page\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/geo-redirect\",\n      \"has\": [\n        {\n          \"type\": \"header\",\n          \"key\": \"x-vercel-ip-country\",\n          \"value\": \"DE\"\n        }\n      ],\n      \"destination\": \"/de\",\n      \"permanent\": false\n    },\n    {\n      \"source\": \"/missing-cookie\",\n      \"missing\": [\n        {\n          \"type\": \"cookie\",\n          \"key\": \"session\"\n        }\n      ],\n      \"destination\": \"/login\",\n      \"permanent\": false\n    },\n\n    {\n      \"$comment\": \"LOCALE HANDLING (skipped - unsupported)\",\n      \"source\": \"/with-locale/:path*\",\n      \"destination\": \"/:nextInternalLocale/:path*\",\n      \"locale\": false,\n      \"permanent\": true\n    },\n\n    {\n      \"$comment\": \"NO STATUS (defaults to 301)\",\n      \"source\": \"/no-status\",\n      \"destination\": \"/default-permanent\"\n    },\n\n    {\n      \"$comment\": \"STATUS CODE CONVERSION\",\n      \"source\": \"/redirect-307\",\n      \"destination\": \"/temp-307-target\",\n      \"statusCode\": 307\n    },\n    {\n      \"source\": \"/redirect-308\",\n      \"destination\": \"/perm-308-target\",\n      \"statusCode\": 308\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/redirect-loop-detection.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { wouldCreateLoop, detectLoopsInBatch } from \"./redirect-loop-detection\";\nimport type { PageRedirect } from \"@webstudio-is/sdk\";\n\ndescribe(\"wouldCreateLoop\", () => {\n  describe(\"self-redirect detection\", () => {\n    test(\"returns true for direct self-redirect\", () => {\n      expect(wouldCreateLoop(\"/about\", \"/about\", [])).toBe(true);\n    });\n\n    test(\"returns true for self-redirect with existing redirects\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/other\", new: \"/page\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/about\", \"/about\", existing)).toBe(true);\n    });\n  });\n\n  describe(\"direct loop detection (A → B → A)\", () => {\n    test(\"returns true when target redirects back to source\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/a\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(true);\n    });\n\n    test(\"returns false when no loop exists\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(false);\n    });\n  });\n\n  describe(\"chain loop detection (A → B → C → A)\", () => {\n    test(\"returns true for 3-node loop\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n        { old: \"/c\", new: \"/a\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(true);\n    });\n\n    test(\"returns true for longer chain loop\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n        { old: \"/c\", new: \"/d\", status: \"301\" },\n        { old: \"/d\", new: \"/e\", status: \"301\" },\n        { old: \"/e\", new: \"/a\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(true);\n    });\n\n    test(\"returns false for chain that doesn't loop back\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n        { old: \"/c\", new: \"/d\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(false);\n    });\n  });\n\n  describe(\"external URLs\", () => {\n    test(\"returns false for external http URL target\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/a\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"http://example.com/b\", existing)).toBe(\n        false\n      );\n    });\n\n    test(\"returns false for external https URL target\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/a\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"https://example.com/b\", existing)).toBe(\n        false\n      );\n    });\n\n    test(\"returns false for protocol-relative URL target\", () => {\n      expect(wouldCreateLoop(\"/a\", \"//example.com/b\", [])).toBe(false);\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"returns false when no existing redirects\", () => {\n      expect(wouldCreateLoop(\"/a\", \"/b\", [])).toBe(false);\n    });\n\n    test(\"returns false when target is not redirected anywhere\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/other\", new: \"/somewhere\", status: \"301\" },\n      ];\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(false);\n    });\n\n    test(\"handles redirect to home page\", () => {\n      const existing: PageRedirect[] = [{ old: \"/\", new: \"/a\", status: \"301\" }];\n      expect(wouldCreateLoop(\"/a\", \"/\", existing)).toBe(true);\n    });\n\n    test(\"prevents infinite loop in detection with circular existing redirects\", () => {\n      // Existing redirects already have a loop (shouldn't happen but be defensive)\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n        { old: \"/c\", new: \"/b\", status: \"301\" },\n      ];\n      // Should return true because following /b leads to a cycle\n      expect(wouldCreateLoop(\"/a\", \"/b\", existing)).toBe(true);\n    });\n  });\n});\n\ndescribe(\"detectLoopsInBatch\", () => {\n  describe(\"loops within imported set\", () => {\n    test(\"detects direct loop in imported redirects\", () => {\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"/b\", status: \"301\" },\n        { old: \"/b\", new: \"/a\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, []);\n\n      expect(result.valid).toHaveLength(1);\n      expect(result.valid[0]).toEqual({ old: \"/a\", new: \"/b\", status: \"301\" });\n      expect(result.looped).toHaveLength(1);\n      expect(result.looped[0].redirect).toEqual({\n        old: \"/b\",\n        new: \"/a\",\n        status: \"301\",\n      });\n      expect(result.looped[0].reason).toContain(\"loop\");\n    });\n\n    test(\"detects chain loop in imported redirects\", () => {\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"/b\", status: \"301\" },\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n        { old: \"/c\", new: \"/a\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, []);\n\n      expect(result.valid).toHaveLength(2);\n      expect(result.looped).toHaveLength(1);\n      expect(result.looped[0].redirect.old).toBe(\"/c\");\n    });\n\n    test(\"detects self-redirect in imported redirects\", () => {\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"/a\", status: \"301\" },\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, []);\n\n      expect(result.valid).toHaveLength(1);\n      expect(result.valid[0].old).toBe(\"/b\");\n      expect(result.looped).toHaveLength(1);\n      expect(result.looped[0].redirect.old).toBe(\"/a\");\n    });\n  });\n\n  describe(\"loops with existing redirects\", () => {\n    test(\"detects loop that would form with existing redirects\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/b\", new: \"/a\", status: \"301\" },\n      ];\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"/b\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, existing);\n\n      expect(result.valid).toHaveLength(0);\n      expect(result.looped).toHaveLength(1);\n    });\n\n    test(\"detects chain loop with existing redirects\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"/c\", new: \"/a\", status: \"301\" },\n      ];\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"/b\", status: \"301\" },\n        { old: \"/b\", new: \"/c\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, existing);\n\n      // First redirect is fine: /a → /b (no loop yet)\n      // Second redirect creates: /a → /b → /c → /a (loop!)\n      expect(result.valid).toHaveLength(1);\n      expect(result.looped).toHaveLength(1);\n      expect(result.looped[0].redirect.old).toBe(\"/b\");\n    });\n  });\n\n  describe(\"valid imports\", () => {\n    test(\"returns all redirects as valid when no loops\", () => {\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"/b\", status: \"301\" },\n        { old: \"/c\", new: \"/d\", status: \"301\" },\n        { old: \"/e\", new: \"/f\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, []);\n\n      expect(result.valid).toHaveLength(3);\n      expect(result.looped).toHaveLength(0);\n    });\n\n    test(\"handles empty input\", () => {\n      const result = detectLoopsInBatch([], []);\n\n      expect(result.valid).toHaveLength(0);\n      expect(result.looped).toHaveLength(0);\n    });\n\n    test(\"allows external URL targets\", () => {\n      const existing: PageRedirect[] = [\n        { old: \"https://example.com/a\", new: \"/a\", status: \"301\" },\n      ];\n      const newRedirects: PageRedirect[] = [\n        { old: \"/a\", new: \"https://example.com/a\", status: \"301\" },\n      ];\n      const result = detectLoopsInBatch(newRedirects, existing);\n\n      // External URL can't create a loop\n      expect(result.valid).toHaveLength(1);\n      expect(result.looped).toHaveLength(0);\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/redirect-loop-detection.ts",
    "content": "import type { PageRedirect } from \"@webstudio-is/sdk\";\n\nexport const LOOP_ERROR = \"This redirect would create a loop\";\n\n/**\n * Checks if adding a redirect from `fromPath` to `toPath` would create a loop\n * given the existing redirects.\n *\n * A loop occurs when following the redirect chain eventually leads back to fromPath.\n */\nexport const wouldCreateLoop = (\n  fromPath: string,\n  toPath: string,\n  existingRedirects: PageRedirect[]\n): boolean => {\n  // Self-redirect is always a loop\n  if (fromPath === toPath) {\n    return true;\n  }\n\n  // External URLs can't create loops (they leave the site)\n  if (\n    toPath.startsWith(\"http://\") ||\n    toPath.startsWith(\"https://\") ||\n    toPath.startsWith(\"//\")\n  ) {\n    return false;\n  }\n\n  // Build a map for O(1) lookup\n  const redirectMap = new Map<string, string>();\n  for (const redirect of existingRedirects) {\n    redirectMap.set(redirect.old, redirect.new);\n  }\n\n  // Follow the chain from toPath and check if we reach fromPath\n  const visited = new Set<string>([fromPath]);\n  let current = toPath;\n\n  while (current) {\n    // Found a loop back to the source\n    if (current === fromPath) {\n      return true;\n    }\n\n    // Cycle detected in existing redirects (defensive)\n    if (visited.has(current)) {\n      return true;\n    }\n\n    visited.add(current);\n\n    // Get next hop\n    const next = redirectMap.get(current);\n    if (!next) {\n      // Chain ends without looping back\n      return false;\n    }\n\n    // External URL ends the chain\n    if (\n      next.startsWith(\"http://\") ||\n      next.startsWith(\"https://\") ||\n      next.startsWith(\"//\")\n    ) {\n      return false;\n    }\n\n    current = next;\n  }\n\n  return false;\n};\n\nexport type LoopedRedirect = {\n  redirect: PageRedirect;\n  reason: string;\n};\n\nexport type BatchLoopResult = {\n  valid: PageRedirect[];\n  looped: LoopedRedirect[];\n};\n\n/**\n * Detects loops in a batch of new redirects, checking both:\n * 1. Loops within the new set itself\n * 2. Loops that would form with existing redirects\n *\n * Processes redirects in order, adding valid ones to the check map\n * so subsequent redirects are checked against the growing set.\n */\nexport const detectLoopsInBatch = (\n  newRedirects: PageRedirect[],\n  existingRedirects: PageRedirect[]\n): BatchLoopResult => {\n  const valid: PageRedirect[] = [];\n  const looped: LoopedRedirect[] = [];\n\n  // Start with existing redirects\n  const currentRedirects = [...existingRedirects];\n\n  for (const redirect of newRedirects) {\n    if (wouldCreateLoop(redirect.old, redirect.new, currentRedirects)) {\n      looped.push({\n        redirect,\n        reason: \"Creates redirect loop\",\n      });\n    } else {\n      valid.push(redirect);\n      // Add to current set so subsequent redirects check against it\n      currentRedirects.push(redirect);\n    }\n  }\n\n  return { valid, looped };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/redirect-parsers.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parseRedirects } from \"./redirect-parsers\";\n\ndescribe(\"parseRedirects\", () => {\n  describe(\"CSV format\", () => {\n    test(\"parses basic CSV with header\", () => {\n      const input = `source,target,status\n/old,/new,301\n/about,/about-us,302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"parses CSV without header (detects by first row being a redirect)\", () => {\n      const input = `/old,/new,301\n/about,/about-us,302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"parses CSV with different column names\", () => {\n      const input = `from,to,code\n/old,/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"parses CSV with alternative column names\", () => {\n      const input = `old,new,status\n/old,/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"parses semicolon-delimited CSV\", () => {\n      const input = `source;target;status\n/old;/new;301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"parses tab-delimited CSV\", () => {\n      const input = `source\\ttarget\\tstatus\n/old\\t/new\\t301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles CSV with only source and target (defaults status to 301)\", () => {\n      const input = `source,target\n/old,/new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"parses Shopify redirect export format\", () => {\n      const input = `Redirect from,Redirect to\n/old-page,/new-page\n/about,/about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old-page\", new: \"/new-page\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 301 },\n      ]);\n    });\n\n    test(\"parses HubSpot redirect export format\", () => {\n      const input = `Original URL,Target URL\n/old-page,/new-page\n/blog/old-post,/blog/new-post`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old-page\", new: \"/new-page\", status: 301 },\n        { old: \"/blog/old-post\", new: \"/blog/new-post\", status: 301 },\n      ]);\n    });\n\n    test(\"handles CSV with quoted values containing commas\", () => {\n      const input = `source,target,status\n\"/old,path\",/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old,path\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles CSV with external URL as target\", () => {\n      const input = `source,target,status\n/old,https://example.com/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"https://example.com/new\", status: 301 },\n      ]);\n    });\n\n    test(\"skips empty lines\", () => {\n      const input = `source,target,status\n/old,/new,301\n\n/about,/about-us,302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"skips invalid rows and reports them\", () => {\n      const input = `source,target,status\n/old,/new,301\ninvalid-no-slash,/new,301\n/about,/about-us,302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0]).toMatchObject({\n        line: 3,\n        reason: expect.stringContaining(\"source\"),\n      });\n    });\n\n    test(\"handles columns in different order\", () => {\n      const input = `target,status,source\n/new,301,/old\n/about-us,302,/about`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"ignores extra columns\", () => {\n      const input = `source,target,status,notes,author\n/old,/new,301,legacy redirect,john\n/about,/about-us,302,rebranding,jane`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"handles BOM at start of file\", () => {\n      const input = `\\uFEFFsource,target,status\n/old,/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n  });\n\n  describe(\"JSON format\", () => {\n    test(\"parses JSON array of redirects\", () => {\n      const input = `[\n        { \"source\": \"/old\", \"target\": \"/new\", \"status\": 301 },\n        { \"source\": \"/about\", \"target\": \"/about-us\", \"status\": 302 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"parses JSON with nested redirects array\", () => {\n      const input = `{\n        \"redirects\": [\n          { \"source\": \"/old\", \"target\": \"/new\", \"status\": 301 }\n        ]\n      }`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"parses JSON with alternative key names\", () => {\n      const input = `[\n        { \"from\": \"/old\", \"to\": \"/new\", \"code\": 301 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"parses Vercel-style JSON with permanent flag\", () => {\n      const input = `{\n        \"redirects\": [\n          { \"source\": \"/old\", \"destination\": \"/new\", \"permanent\": true },\n          { \"source\": \"/temp\", \"destination\": \"/new\", \"permanent\": false }\n        ]\n      }`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/temp\", new: \"/new\", status: 302 },\n      ]);\n    });\n\n    test(\"defaults status to 301 when not provided\", () => {\n      const input = `[\n        { \"source\": \"/old\", \"target\": \"/new\" }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"skips invalid entries in JSON array\", () => {\n      const input = `[\n        { \"source\": \"/old\", \"target\": \"/new\", \"status\": 301 },\n        { \"source\": \"no-slash\", \"target\": \"/new\", \"status\": 301 },\n        { \"target\": \"/new\", \"status\": 301 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(2);\n    });\n\n    test(\"reports error for invalid JSON\", () => {\n      const input = `{ invalid json }`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([]);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"Invalid JSON\");\n    });\n\n    test(\"handles empty JSON array\", () => {\n      const input = `[]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([]);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"ignores extra properties in JSON objects\", () => {\n      const input = `[\n        { \"source\": \"/old\", \"target\": \"/new\", \"status\": 301, \"comment\": \"legacy\", \"author\": \"john\" }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"skips JSON redirects with has/missing conditions (unsupported)\", () => {\n      const input = `{\n        \"redirects\": [\n          { \"source\": \"/simple\", \"destination\": \"/target\", \"permanent\": true },\n          { \"source\": \"/conditional\", \"destination\": \"/special\", \"permanent\": false, \"has\": [{\"type\": \"header\", \"key\": \"x-custom\"}] },\n          { \"source\": \"/another\", \"destination\": \"/dest\", \"permanent\": true }\n        ]\n      }`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"condition\");\n    });\n\n    test(\"skips JSON redirects with placeholders in source (unsupported)\", () => {\n      const input = `[\n        { \"source\": \"/simple\", \"target\": \"/target\", \"status\": 301 },\n        { \"source\": \"/blog/:slug\", \"target\": \"/posts/:slug\", \"status\": 301 },\n        { \"source\": \"/docs/:path*\", \"target\": \"/documentation/:path*\", \"status\": 301 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(2);\n      expect(result.skipped[0].reason).toContain(\"placeholder\");\n    });\n  });\n\n  describe(\"Netlify _redirects format\", () => {\n    test(\"parses basic Netlify redirects\", () => {\n      const input = `/old /new 301\n/about /about-us 302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"defaults to 301 when status not provided\", () => {\n      const input = `/old /new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"skips splat redirects (unsupported)\", () => {\n      const input = `/simple /target 301\n/blog/* /articles/:splat 301\n/other /other-target`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"wildcard\");\n    });\n\n    test(\"ignores comment lines\", () => {\n      const input = `# This is a comment\n/old /new 301\n# Another comment\n/about /about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"skips lines with conditions (unsupported)\", () => {\n      const input = `/old /new 301\n/admin /login 302 Role=admin\n/about /about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0]).toMatchObject({\n        line: 2,\n        reason: expect.stringContaining(\"condition\"),\n      });\n    });\n\n    test(\"handles external URLs as target\", () => {\n      const input = `/old https://example.com/new 301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"https://example.com/new\", status: 301 },\n      ]);\n    });\n\n    test(\"skips force redirects (200 status rewrites)\", () => {\n      const input = `/old /new 200\n/about /about-us 301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"rewrite\");\n    });\n\n    test(\"handles force flag with !\", () => {\n      const input = `/old /new 301!\n/about /about-us 302!`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"handles multiple spaces between parts\", () => {\n      const input = `/old    /new    301\n/about   /about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 301 },\n      ]);\n    });\n\n    test(\"handles tabs as separators\", () => {\n      const input = \"/old\\t/new\\t301\";\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"skips redirects with placeholders (unsupported)\", () => {\n      const input = `/simple /target 301\n/blog/:year/:slug /posts/:year-:slug 301\n/other /other-target`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"placeholder\");\n    });\n\n    test(\"skips redirects with query parameter matching (unsupported)\", () => {\n      const input = `/simple /target 301\n/store id=:id /products/:id 301\n/other /other-target`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"query\");\n    });\n\n    test(\"skips 303 status code\", () => {\n      const input = `/old /new 301\n/see-other /other 303\n/about /about-us 302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(1);\n    });\n  });\n\n  describe(\"htaccess format\", () => {\n    test(\"parses basic Redirect directive\", () => {\n      const input = `Redirect 301 /old /new\nRedirect 302 /about /about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"parses Redirect without status code (defaults to 302)\", () => {\n      const input = `Redirect /old /new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 302 },\n      ]);\n    });\n\n    test(\"parses Redirect with permanent/temp keywords\", () => {\n      const input = `Redirect permanent /old /new\nRedirect temp /about /about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"handles external URLs as target\", () => {\n      const input = `Redirect 301 /old https://example.com/new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"https://example.com/new\", status: 301 },\n      ]);\n    });\n\n    test(\"ignores comment lines\", () => {\n      const input = `# Redirect old pages\nRedirect 301 /old /new\n# End of redirects`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"ignores non-redirect directives\", () => {\n      const input = `RewriteEngine On\nRedirect 301 /old /new\nRewriteBase /`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"skips RewriteRule and reports as unsupported\", () => {\n      const input = `Redirect 301 /old /new\nRewriteRule ^/blog/(.*)$ /posts/$1 [R=301,L]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0]).toMatchObject({\n        line: 2,\n        reason: expect.stringContaining(\"RewriteRule\"),\n      });\n    });\n\n    test(\"skips RedirectMatch and reports as unsupported\", () => {\n      const input = `Redirect 301 /old /new\nRedirectMatch 301 ^/category/(.*)$ /new-category/$1`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0]).toMatchObject({\n        line: 2,\n        reason: expect.stringContaining(\"RedirectMatch\"),\n      });\n    });\n\n    test(\"handles case-insensitive Redirect directive\", () => {\n      const input = `redirect 301 /old /new\nREDIRECT 301 /about /about-us`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n    });\n\n    test(\"handles multiple spaces between parts\", () => {\n      const input = `Redirect    301    /old    /new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles tabs in Redirect directive\", () => {\n      const input = \"Redirect\\t301\\t/old\\t/new\";\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n  });\n\n  describe(\"auto-detection\", () => {\n    test(\"detects JSON format\", () => {\n      const input = `[{\"source\": \"/old\", \"target\": \"/new\"}]`;\n      const result = parseRedirects(input);\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"detects CSV format by header\", () => {\n      const input = `source,target,status\n/old,/new,301`;\n      const result = parseRedirects(input);\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"detects htaccess format by Redirect keyword\", () => {\n      const input = `Redirect 301 /old /new`;\n      const result = parseRedirects(input);\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"detects Netlify format by line structure\", () => {\n      const input = `/old /new 301`;\n      const result = parseRedirects(input);\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"returns empty result for empty input\", () => {\n      const result = parseRedirects(\"\");\n      expect(result.redirects).toEqual([]);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"returns empty result for whitespace-only input\", () => {\n      const result = parseRedirects(\"   \\n\\n   \");\n      expect(result.redirects).toEqual([]);\n      expect(result.skipped).toEqual([]);\n    });\n  });\n\n  describe(\"edge cases\", () => {\n    test(\"handles mixed valid and invalid entries\", () => {\n      const input = `source,target,status\n/old,/new,301\n,/missing-source,301\n/about,/about-us,invalid-status\n/valid,/valid-target,302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.skipped).toHaveLength(2);\n    });\n\n    test(\"trims whitespace from paths\", () => {\n      const input = `source,target,status\n  /old  ,  /new  ,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles Windows-style line endings\", () => {\n      const input =\n        \"source,target,status\\r\\n/old,/new,301\\r\\n/about,/about-us,302\";\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n    });\n\n    test(\"handles duplicate redirects (keeps all, validation happens later)\", () => {\n      const input = `source,target,status\n/old,/new,301\n/old,/different,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n    });\n\n    test(\"handles status codes as text\", () => {\n      const input = `source,target,status\n/old,/new,permanent\n/about,/about-us,temporary`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n      ]);\n    });\n\n    test(\"converts 307 to 302 and 308 to 301\", () => {\n      const input = `source,target,status\n/old,/new,301\n/about,/about-us,307\n/contact,/contact-us,308`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n        { old: \"/about\", new: \"/about-us\", status: 302 },\n        { old: \"/contact\", new: \"/contact-us\", status: 301 },\n      ]);\n      expect(result.skipped).toHaveLength(0);\n    });\n\n    test(\"skips unsupported status codes (200, 404, etc.)\", () => {\n      const input = `source,target,status\n/old,/new,301\n/rewrite,/target,200\n/error,/not-found,404`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(2);\n    });\n\n    test(\"handles Mac-style line endings (CR only)\", () => {\n      const input = \"source,target,status\\r/old,/new,301\\r/about,/about-us,302\";\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n    });\n\n    test(\"handles paths with query strings\", () => {\n      const input = `source,target,status\n/old?ref=123,/new?ref=456,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old?ref=123\", new: \"/new?ref=456\", status: 301 },\n      ]);\n    });\n\n    test(\"handles URL-encoded characters in paths\", () => {\n      const input = `source,target,status\n/hello%20world,/hello-world,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/hello%20world\", new: \"/hello-world\", status: 301 },\n      ]);\n    });\n\n    test(\"handles paths with hash fragments\", () => {\n      const input = `source,target,status\n/old#section,/new#section,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old#section\", new: \"/new#section\", status: 301 },\n      ]);\n    });\n\n    test(\"handles root path redirect\", () => {\n      const input = `source,target,status\n/,/home,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/\", new: \"/home\", status: 301 },\n      ]);\n    });\n\n    test(\"strips trailing slashes from source paths\", () => {\n      // Webstudio requires: paths cannot end with /\n      const input = `source,target,status\n/old/,/new/,301\n/about,/about-us/,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new/\", status: 301 },\n        { old: \"/about\", new: \"/about-us/\", status: 301 },\n      ]);\n    });\n  });\n\n  describe(\"fixture files\", () => {\n    test(\"parses Shopify CSV fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/shopify.csv\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      expect(result.redirects.length).toBeGreaterThan(0);\n      expect(result.redirects[0]).toMatchObject({\n        old: expect.stringMatching(/^\\//),\n        new: expect.stringMatching(/^\\//),\n        status: 301,\n      });\n      expect(result.skipped).toHaveLength(0);\n    });\n\n    test(\"parses HubSpot CSV fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/hubspot.csv\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      expect(result.redirects.length).toBeGreaterThan(0);\n      expect(result.skipped).toHaveLength(0);\n    });\n\n    test(\"parses Vercel/Next.js JSON fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/vercel-nextjs.json\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      // Should parse simple redirects and skip ones with placeholders/conditions\n      expect(result.redirects.length).toBeGreaterThan(0);\n      // Some should be skipped due to placeholders and conditions\n      expect(result.skipped.length).toBeGreaterThan(0);\n    });\n\n    test(\"parses Netlify _redirects fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/netlify._redirects\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      // Should parse simple redirects\n      expect(result.redirects.length).toBeGreaterThan(0);\n      // Many should be skipped: conditions, placeholders, rewrites\n      expect(result.skipped.length).toBeGreaterThan(0);\n    });\n\n    test(\"parses Apache htaccess fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/apache.htaccess\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      // Should parse Redirect directives\n      expect(result.redirects.length).toBeGreaterThan(0);\n      // RewriteRule and RedirectMatch should be skipped\n      expect(result.skipped.length).toBeGreaterThan(0);\n    });\n\n    test(\"parses generic CSV fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/generic.csv\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      // Should parse all valid redirects, converting 307/308\n      expect(result.redirects.length).toBeGreaterThanOrEqual(10);\n    });\n\n    test(\"parses generic JSON fixture\", () => {\n      const content = readFileSync(\n        join(__dirname, \"fixtures/generic.json\"),\n        \"utf-8\"\n      );\n\n      const result = parseRedirects(content);\n\n      // Should parse valid redirects, skip invalid ones\n      expect(result.redirects.length).toBeGreaterThan(0);\n      expect(result.skipped.length).toBeGreaterThan(0);\n    });\n  });\n\n  describe(\"additional edge cases\", () => {\n    // JSON edge cases\n    test(\"handles JSON with null values\", () => {\n      const input = `[\n        { \"source\": null, \"target\": \"/new\", \"status\": 301 },\n        { \"source\": \"/valid\", \"target\": \"/new\", \"status\": 301 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"missing source\");\n    });\n\n    test(\"handles JSON with mixed $comment and real data\", () => {\n      const input = `[\n        { \"$comment\": \"This is a comment\", \"source\": \"/old\", \"target\": \"/new\", \"status\": 301 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      // Should parse the redirect, ignoring the $comment field\n      expect(result.redirects).toHaveLength(1);\n      expect(result.redirects[0]).toEqual({\n        old: \"/old\",\n        new: \"/new\",\n        status: 301,\n      });\n    });\n\n    test(\"handles JSON with statusCode (Next.js camelCase)\", () => {\n      const input = `{\n        \"redirects\": [\n          { \"source\": \"/old\", \"destination\": \"/new\", \"statusCode\": 301 }\n        ]\n      }`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles JSON with both permanent and statusCode (permanent takes precedence)\", () => {\n      const input = `[\n        { \"source\": \"/old\", \"destination\": \"/new\", \"permanent\": true, \"statusCode\": 302 }\n      ]`;\n\n      const result = parseRedirects(input);\n\n      // statusCode should be found first since we check STATUS_KEYS before permanent\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"handles empty JSON object\", () => {\n      const input = `{}`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([]);\n      expect(result.skipped).toEqual([]);\n    });\n\n    test(\"handles JSON with non-array redirects property\", () => {\n      const input = `{ \"redirects\": \"not an array\" }`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([]);\n      expect(result.skipped).toEqual([]);\n    });\n\n    // CSV edge cases\n    test(\"handles CSV with mixed case column headers\", () => {\n      const input = `SOURCE,Target,STATUS\n/old,/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles CSV row with fewer columns than header\", () => {\n      const input = `source,target,status\n/old,/new\n/about,/about-us,302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      // First row should default to 301\n      expect(result.redirects[0].status).toBe(301);\n    });\n\n    test(\"handles CSV with quoted values containing quotes\", () => {\n      const input = `source,target,status\n\"/old\"\"path\",/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: '/old\"path', new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles CSV with empty target\", () => {\n      const input = `source,target,status\n/old,,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(0);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"missing target\");\n    });\n\n    // Netlify edge cases\n    test(\"handles Netlify with only two parts (no status)\", () => {\n      const input = `/old /new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/new\", status: 301 },\n      ]);\n    });\n\n    test(\"handles Netlify condition without status code\", () => {\n      const input = `/old /new Country=us`;\n\n      const result = parseRedirects(input);\n\n      // Should be skipped because Country= looks like a condition\n      expect(result.redirects).toHaveLength(0);\n      expect(result.skipped).toHaveLength(1);\n    });\n\n    test(\"handles Netlify with indented lines\", () => {\n      const input = `  /old /new 301\n    /about /about-us 302`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n    });\n\n    test(\"handles Netlify single line only\", () => {\n      const input = `/old /new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    // htaccess edge cases\n    test(\"handles htaccess with seeother keyword (303)\", () => {\n      const input = `Redirect seeother /old /new`;\n\n      const result = parseRedirects(input);\n\n      // seeother is not a recognized keyword, so it's treated as the path\n      // This results in: from=\"/old\", to=\"/new\", status defaulting\n      // Actually, \"seeother\" would be parsed as status, which is invalid\n      expect(result.skipped).toHaveLength(1);\n    });\n\n    test(\"handles htaccess with gone keyword\", () => {\n      const input = `Redirect gone /old`;\n\n      const result = parseRedirects(input);\n\n      // \"gone\" is a status keyword in Apache (410), but we only have from\n      // The implementation treats \"gone\" as path if no target follows\n      expect(result.redirects).toHaveLength(0);\n    });\n\n    test(\"handles htaccess RewriteCond (ignored silently)\", () => {\n      const input = `RewriteCond %{HTTP_HOST} ^www\\\\.example\\\\.com$\nRedirect 301 /old /new`;\n\n      const result = parseRedirects(input);\n\n      // RewriteCond should be ignored (not reported as skipped)\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(0);\n    });\n\n    test(\"handles htaccess Options directive (ignored silently)\", () => {\n      const input = `Options +FollowSymLinks\nRedirect 301 /old /new`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.skipped).toHaveLength(0);\n    });\n\n    test(\"handles htaccess with target containing spaces\", () => {\n      const input = `Redirect 301 /old /path/with spaces/target`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toEqual([\n        { old: \"/old\", new: \"/path/with spaces/target\", status: 301 },\n      ]);\n    });\n\n    // Path validation edge cases\n    test(\"skips paths with double slashes\", () => {\n      const input = `source,target,status\n//double-slash,/new,301`;\n\n      const result = parseRedirects(input);\n\n      // Double slash still starts with /, so it passes basic validation\n      // Webstudio's OldPagePath schema will reject it later\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"handles http URLs as source (invalid)\", () => {\n      const input = `source,target,status\nhttps://example.com/old,/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(0);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\n        \"source path must start with /\"\n      );\n    });\n\n    test(\"handles protocol-relative URLs as target\", () => {\n      const input = `source,target,status\n/old,//cdn.example.com/asset,301`;\n\n      const result = parseRedirects(input);\n\n      // Protocol-relative URLs start with // and are valid redirect targets\n      expect(result.redirects).toHaveLength(1);\n      expect(result.redirects[0].new).toBe(\"//cdn.example.com/asset\");\n    });\n\n    test(\"handles unicode paths\", () => {\n      const input = `source,target,status\n/über,/ueber,301\n/日本語,/japanese,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n      expect(result.redirects[0].old).toBe(\"/über\");\n      expect(result.redirects[1].old).toBe(\"/日本語\");\n    });\n\n    test(\"handles very long paths\", () => {\n      const longPath = \"/\" + \"a\".repeat(1000);\n      const input = `source,target,status\n${longPath},/new,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(1);\n      expect(result.redirects[0].old).toBe(longPath);\n    });\n\n    test(\"handles empty lines interspersed in Netlify format\", () => {\n      const input = `/old1 /new1 301\n\n/old2 /new2 301\n\n`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(2);\n    });\n\n    // Status code edge cases\n    test(\"handles numeric string status with leading zeros\", () => {\n      const input = `source,target,status\n/old,/new,0301`;\n\n      const result = parseRedirects(input);\n\n      // 0301 octal would be 193, but parseInt treats it as 301\n      expect(result.redirects).toHaveLength(1);\n      expect(result.redirects[0].status).toBe(301);\n    });\n\n    test(\"handles status code 303 (See Other)\", () => {\n      const input = `source,target,status\n/old,/new,303`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(0);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"unsupported status code 303\");\n    });\n\n    test(\"handles status code 410 (Gone)\", () => {\n      const input = `source,target,status\n/old,/new,410`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(0);\n      expect(result.skipped).toHaveLength(1);\n    });\n\n    // Wildcard detection edge cases\n    test(\"allows asterisk in query string (not a wildcard)\", () => {\n      const input = `source,target,status\n/search?q=*,/search-all,301`;\n\n      const result = parseRedirects(input);\n\n      // * in query string should NOT be treated as wildcard\n      expect(result.redirects).toHaveLength(1);\n    });\n\n    test(\"skips asterisk in path segment\", () => {\n      const input = `source,target,status\n/files/*.txt,/all-files,301`;\n\n      const result = parseRedirects(input);\n\n      expect(result.redirects).toHaveLength(0);\n      expect(result.skipped).toHaveLength(1);\n      expect(result.skipped[0].reason).toContain(\"wildcard\");\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/redirects/redirect-parsers.ts",
    "content": "import Papa from \"papaparse\";\n\nexport type ParsedRedirect = {\n  old: string;\n  new: string;\n  status: number;\n};\n\nexport type SkippedLine = {\n  line: number;\n  content: string;\n  reason: string;\n};\n\nexport type ParseResult = {\n  redirects: ParsedRedirect[];\n  skipped: SkippedLine[];\n};\n\n// Key mappings for normalizing different column/property names\nconst FROM_KEYS = [\n  \"from\",\n  \"source\",\n  \"old\",\n  \"redirect from\",\n  \"original url\",\n] as const;\nconst TO_KEYS = [\n  \"to\",\n  \"target\",\n  \"destination\",\n  \"new\",\n  \"redirect to\",\n  \"target url\",\n] as const;\nconst STATUS_KEYS = [\"status\", \"code\", \"statuscode\"] as const;\n\ntype RawRecord = Record<string, unknown>;\n\n/**\n * Detect format and parse accordingly\n */\nconst detectFormat = (\n  content: string\n): \"json\" | \"csv\" | \"htaccess\" | \"netlify\" | \"empty\" => {\n  const trimmed = content.trim();\n  if (trimmed === \"\") {\n    return \"empty\";\n  }\n\n  // JSON: starts with [ or {\n  if (trimmed.startsWith(\"[\") || trimmed.startsWith(\"{\")) {\n    return \"json\";\n  }\n\n  // Split into lines for further detection\n  const lines = trimmed.split(/\\r\\n|\\r|\\n/);\n  const nonEmptyLines = lines.filter(\n    (l) => l.trim() !== \"\" && !l.trim().startsWith(\"#\")\n  );\n\n  if (nonEmptyLines.length === 0) {\n    return \"empty\";\n  }\n\n  // CSV: Check first line for known column headers (must check BEFORE htaccess)\n  // This is important because Shopify uses \"Redirect from,Redirect to\" headers\n  const firstLine = nonEmptyLines[0].toLowerCase();\n  const csvHeaders = [\n    ...FROM_KEYS,\n    ...TO_KEYS,\n    ...STATUS_KEYS,\n    \"permanent\",\n  ].map((h) => h.toLowerCase());\n\n  // Check if first line looks like CSV headers\n  const possibleHeaders = firstLine.split(/[,;\\t]/);\n  const matchedHeaders = possibleHeaders.filter((h) =>\n    csvHeaders.includes(h.trim().toLowerCase())\n  );\n  if (matchedHeaders.length >= 2) {\n    return \"csv\";\n  }\n\n  // htaccess: any line starts with \"Redirect\" followed by space/tab (but not comma)\n  // Also check for RewriteRule or RedirectMatch which are htaccess-specific\n  for (const line of nonEmptyLines) {\n    const lower = line.trim().toLowerCase();\n    // Must be \"Redirect \" followed by status or path, not \"Redirect from,\" (CSV header)\n    if (\n      (lower.startsWith(\"redirect \") || lower.startsWith(\"redirect\\t\")) &&\n      !lower.includes(\",\") // Avoid matching CSV headers\n    ) {\n      return \"htaccess\";\n    }\n    if (\n      lower.startsWith(\"rewriterule \") ||\n      lower.startsWith(\"redirectmatch \")\n    ) {\n      return \"htaccess\";\n    }\n  }\n\n  // Check if lines have CSV-like structure (comma/semicolon/tab separated, multiple columns)\n  if (\n    nonEmptyLines.every((line) => {\n      const parts = line.split(/[,;\\t]/);\n      return parts.length >= 2;\n    })\n  ) {\n    // If first column looks like a path and has delimiters, it's CSV\n    const firstPart = nonEmptyLines[0].split(/[,;\\t]/)[0].trim();\n    if (firstPart.startsWith(\"/\") || firstPart.startsWith(\"http\")) {\n      return \"csv\";\n    }\n  }\n\n  // Netlify: lines match pattern \"/path /path [status]\"\n  // Check if most non-empty/non-comment lines match this pattern\n  const netlifyPattern = /^\\/\\S+\\s+\\S+/;\n  const matchingLines = nonEmptyLines.filter((line) =>\n    netlifyPattern.test(line.trim())\n  );\n  if (\n    matchingLines.length > 0 &&\n    matchingLines.length >= nonEmptyLines.length * 0.5\n  ) {\n    return \"netlify\";\n  }\n\n  // Default to netlify if lines start with /\n  if (nonEmptyLines[0].trim().startsWith(\"/\")) {\n    return \"netlify\";\n  }\n\n  return \"csv\"; // fallback\n};\n\n/**\n * Check if a path contains unsupported patterns\n */\nconst hasPlaceholder = (path: string): boolean => {\n  // :param placeholders like /blog/:slug\n  return /:[a-zA-Z_][a-zA-Z0-9_]*\\*?/.test(path);\n};\n\nconst hasWildcard = (path: string): boolean => {\n  // * wildcards but not in query string\n  const pathPart = path.split(\"?\")[0];\n  return pathPart.includes(\"*\");\n};\n\nconst hasConditions = (record: RawRecord): boolean => {\n  return \"has\" in record || \"missing\" in record;\n};\n\n/**\n * Normalize status code\n * Returns: 301, 302, or null (for invalid/unsupported codes)\n */\nconst normalizeStatus = (\n  value: unknown\n): { status: 301 | 302 } | { skip: string } => {\n  if (value === undefined || value === null || value === \"\") {\n    return { status: 301 }; // default\n  }\n\n  // Boolean (Vercel permanent flag)\n  if (typeof value === \"boolean\") {\n    return { status: value ? 301 : 302 };\n  }\n\n  // String\n  if (typeof value === \"string\") {\n    const lower = value.toLowerCase().trim();\n\n    if (lower === \"permanent\" || lower === \"true\") {\n      return { status: 301 };\n    }\n    if (lower === \"temporary\" || lower === \"temp\" || lower === \"false\") {\n      return { status: 302 };\n    }\n\n    // Numeric string (possibly with ! suffix for force)\n    const numMatch = lower.match(/^(\\d+)!?$/);\n    if (numMatch) {\n      const code = parseInt(numMatch[1], 10);\n      if (code === 301 || code === 308) {\n        return { status: 301 };\n      }\n      if (code === 302 || code === 307) {\n        return { status: 302 };\n      }\n      if (code === 200) {\n        return { skip: \"rewrite (status 200)\" };\n      }\n      return { skip: `unsupported status code ${code}` };\n    }\n  }\n\n  // Number\n  if (typeof value === \"number\") {\n    if (value === 301 || value === 308) {\n      return { status: 301 };\n    }\n    if (value === 302 || value === 307) {\n      return { status: 302 };\n    }\n    if (value === 200) {\n      return { skip: \"rewrite (status 200)\" };\n    }\n    return { skip: `unsupported status code ${value}` };\n  }\n\n  return { skip: `invalid status value` };\n};\n\n/**\n * Find a value in a record using multiple possible keys\n */\nconst findValue = (record: RawRecord, keys: readonly string[]): unknown => {\n  for (const key of keys) {\n    const lowerKey = key.toLowerCase();\n    for (const [k, v] of Object.entries(record)) {\n      if (k.toLowerCase() === lowerKey) {\n        return v;\n      }\n    }\n  }\n  return undefined;\n};\n\n/**\n * Normalize a path - strip trailing slashes from source paths\n */\nconst normalizePath = (path: string, isSource: boolean): string => {\n  let normalized = path.trim();\n  if (isSource && normalized.length > 1 && normalized.endsWith(\"/\")) {\n    normalized = normalized.slice(0, -1);\n  }\n  return normalized;\n};\n\n/**\n * Validate and normalize a single record into a redirect\n */\nconst normalizeRecord = (\n  record: RawRecord,\n  lineNumber: number,\n  originalContent: string\n): { redirect: ParsedRedirect } | { skipped: SkippedLine } => {\n  // Check for conditions first (Vercel/Next.js has/missing)\n  if (hasConditions(record)) {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"contains condition (has/missing) - not supported\",\n      },\n    };\n  }\n\n  // Find source/from\n  const fromValue = findValue(record, FROM_KEYS);\n  if (fromValue === undefined || fromValue === null || fromValue === \"\") {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"missing source path\",\n      },\n    };\n  }\n  const from = String(fromValue);\n\n  // Find target/to\n  const toValue = findValue(record, TO_KEYS);\n  if (toValue === undefined || toValue === null || toValue === \"\") {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"missing target path\",\n      },\n    };\n  }\n  const to = String(toValue);\n\n  // Check for wildcards in source\n  if (hasWildcard(from)) {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"contains wildcard (*) - not supported\",\n      },\n    };\n  }\n\n  // Check for placeholders in source or target\n  if (hasPlaceholder(from) || hasPlaceholder(to)) {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"contains placeholder (:param) - not supported\",\n      },\n    };\n  }\n\n  // Validate source path\n  const normalizedFrom = normalizePath(from, true);\n  if (!normalizedFrom.startsWith(\"/\")) {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"source path must start with /\",\n      },\n    };\n  }\n\n  // Validate target path\n  const normalizedTo = normalizePath(to, false);\n  if (!normalizedTo.startsWith(\"/\") && !normalizedTo.startsWith(\"http\")) {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: \"target path must start with / or http\",\n      },\n    };\n  }\n\n  // Find and normalize status\n  let statusValue = findValue(record, STATUS_KEYS);\n  // Check for Vercel-style permanent flag\n  if (statusValue === undefined && \"permanent\" in record) {\n    statusValue = record.permanent;\n    // Handle the boolean case\n    if (typeof statusValue === \"boolean\") {\n      // normalizeStatus will handle this\n    }\n  }\n\n  const statusResult = normalizeStatus(statusValue);\n  if (\"skip\" in statusResult) {\n    return {\n      skipped: {\n        line: lineNumber,\n        content: originalContent,\n        reason: statusResult.skip,\n      },\n    };\n  }\n\n  return {\n    redirect: {\n      old: normalizedFrom,\n      new: normalizedTo,\n      status: statusResult.status,\n    },\n  };\n};\n\n/**\n * Parse CSV content using papaparse\n */\nconst parseCSV = (content: string): ParseResult => {\n  const redirects: ParsedRedirect[] = [];\n  const skipped: SkippedLine[] = [];\n\n  // Remove BOM if present\n  const cleanContent = content.replace(/^\\uFEFF/, \"\");\n\n  // Use papaparse for robust CSV parsing\n  const parseResult = Papa.parse<Record<string, string>>(cleanContent, {\n    header: false, // We'll handle headers ourselves for flexibility\n    skipEmptyLines: true,\n    comments: \"#\", // Skip comment lines\n    delimiter: \"\", // Auto-detect delimiter\n    transformHeader: (header) => header.trim().toLowerCase(),\n  });\n\n  if (parseResult.errors.length > 0 && parseResult.data.length === 0) {\n    return {\n      redirects: [],\n      skipped: [\n        {\n          line: 1,\n          content: cleanContent.slice(0, 100),\n          reason: `CSV parse error: ${parseResult.errors[0].message}`,\n        },\n      ],\n    };\n  }\n\n  const rows = parseResult.data as unknown as string[][];\n  if (rows.length === 0) {\n    return { redirects, skipped };\n  }\n\n  // Parse header row - check if first row contains known column names\n  const firstRow = rows[0].map((cell) =>\n    typeof cell === \"string\" ? cell.trim().toLowerCase() : \"\"\n  );\n  const hasHeader = firstRow.some(\n    (h) =>\n      FROM_KEYS.map((k) => k.toLowerCase()).includes(h) ||\n      TO_KEYS.map((k) => k.toLowerCase()).includes(h)\n  );\n\n  const headers = hasHeader\n    ? firstRow\n    : [\"source\", \"target\", \"status\"].slice(0, firstRow.length);\n\n  const startIndex = hasHeader ? 1 : 0;\n\n  for (let i = startIndex; i < rows.length; i++) {\n    const row = rows[i];\n    const record: RawRecord = {};\n\n    for (let j = 0; j < headers.length && j < row.length; j++) {\n      const value = typeof row[j] === \"string\" ? row[j].trim() : \"\";\n      record[headers[j]] = value;\n    }\n\n    // Skip rows that are empty after trimming\n    const hasContent = Object.values(record).some((v) => v !== \"\");\n    if (!hasContent) {\n      continue;\n    }\n\n    // Calculate actual line number (accounting for header and empty lines)\n    // This is approximate since papaparse may skip lines\n    const lineNumber = i + 1;\n    const originalContent = row.join(\",\");\n    const result = normalizeRecord(record, lineNumber, originalContent);\n\n    if (\"redirect\" in result) {\n      redirects.push(result.redirect);\n    } else {\n      skipped.push(result.skipped);\n    }\n  }\n\n  return { redirects, skipped };\n};\n\n/**\n * Parse JSON content\n */\nconst parseJSON = (content: string): ParseResult => {\n  const redirects: ParsedRedirect[] = [];\n  const skipped: SkippedLine[] = [];\n\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(content);\n  } catch {\n    return {\n      redirects: [],\n      skipped: [\n        {\n          line: 1,\n          content: content.slice(0, 100) + (content.length > 100 ? \"...\" : \"\"),\n          reason: \"Invalid JSON\",\n        },\n      ],\n    };\n  }\n\n  // Extract redirects array\n  let records: unknown[];\n  if (Array.isArray(parsed)) {\n    records = parsed;\n  } else if (\n    typeof parsed === \"object\" &&\n    parsed !== null &&\n    \"redirects\" in parsed\n  ) {\n    const redirectsField = (parsed as { redirects: unknown }).redirects;\n    if (Array.isArray(redirectsField)) {\n      records = redirectsField;\n    } else {\n      return { redirects: [], skipped: [] };\n    }\n  } else {\n    return { redirects: [], skipped: [] };\n  }\n\n  for (let i = 0; i < records.length; i++) {\n    const record = records[i];\n\n    // Skip non-objects and comment objects\n    if (typeof record !== \"object\" || record === null) {\n      continue;\n    }\n\n    // Skip objects that are just comments\n    const keys = Object.keys(record);\n    if (keys.length === 1 && keys[0].startsWith(\"$\")) {\n      continue;\n    }\n\n    const lineNumber = i + 1;\n    const originalContent = JSON.stringify(record);\n    const result = normalizeRecord(\n      record as RawRecord,\n      lineNumber,\n      originalContent\n    );\n\n    if (\"redirect\" in result) {\n      redirects.push(result.redirect);\n    } else {\n      skipped.push(result.skipped);\n    }\n  }\n\n  return { redirects, skipped };\n};\n\n/**\n * Parse Netlify _redirects format\n */\nconst parseNetlify = (content: string): ParseResult => {\n  const redirects: ParsedRedirect[] = [];\n  const skipped: SkippedLine[] = [];\n\n  // Normalize line endings\n  const normalizedContent = content.replace(/\\r\\n|\\r/g, \"\\n\");\n  const lines = normalizedContent.split(\"\\n\");\n\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i];\n    const trimmedLine = line.trim();\n    const lineNumber = i + 1;\n\n    // Skip empty lines and comments\n    if (trimmedLine === \"\" || trimmedLine.startsWith(\"#\")) {\n      continue;\n    }\n\n    // Split by whitespace\n    const parts = trimmedLine.split(/\\s+/);\n\n    if (parts.length < 2) {\n      continue; // Not a valid redirect line\n    }\n\n    const from = parts[0];\n    const to = parts[1];\n    const statusPart = parts[2];\n    const rest = parts.slice(3).join(\" \");\n\n    // Check for conditions in remaining parts\n    if (rest && /[A-Za-z]+=/.test(rest)) {\n      skipped.push({\n        line: lineNumber,\n        content: trimmedLine,\n        reason: \"contains condition - not supported\",\n      });\n      continue;\n    }\n\n    // Check for query parameter matching (e.g., /store id=:id)\n    if (to.includes(\"=\") && !to.startsWith(\"http\")) {\n      // This might be query matching like \"/store id=:id /products/:id\"\n      // Re-parse: /source query=value /target\n      skipped.push({\n        line: lineNumber,\n        content: trimmedLine,\n        reason: \"contains query parameter matching - not supported\",\n      });\n      continue;\n    }\n\n    // Create record and normalize\n    const record: RawRecord = {\n      from,\n      to,\n      status: statusPart,\n    };\n\n    const result = normalizeRecord(record, lineNumber, trimmedLine);\n\n    if (\"redirect\" in result) {\n      redirects.push(result.redirect);\n    } else {\n      skipped.push(result.skipped);\n    }\n  }\n\n  return { redirects, skipped };\n};\n\n/**\n * Parse Apache htaccess format\n */\nconst parseHtaccess = (content: string): ParseResult => {\n  const redirects: ParsedRedirect[] = [];\n  const skipped: SkippedLine[] = [];\n\n  // Normalize line endings\n  const normalizedContent = content.replace(/\\r\\n|\\r/g, \"\\n\");\n  const lines = normalizedContent.split(\"\\n\");\n\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i];\n    const trimmedLine = line.trim();\n    const lineNumber = i + 1;\n\n    // Skip empty lines and comments\n    if (trimmedLine === \"\" || trimmedLine.startsWith(\"#\")) {\n      continue;\n    }\n\n    const lowerLine = trimmedLine.toLowerCase();\n\n    // Handle RewriteRule - skip with warning\n    if (\n      lowerLine.startsWith(\"rewriterule \") ||\n      lowerLine.startsWith(\"rewriterule\\t\")\n    ) {\n      skipped.push({\n        line: lineNumber,\n        content: trimmedLine,\n        reason: \"RewriteRule is not supported - use Redirect directive\",\n      });\n      continue;\n    }\n\n    // Handle RedirectMatch - skip with warning\n    if (\n      lowerLine.startsWith(\"redirectmatch \") ||\n      lowerLine.startsWith(\"redirectmatch\\t\")\n    ) {\n      skipped.push({\n        line: lineNumber,\n        content: trimmedLine,\n        reason:\n          \"RedirectMatch is not supported - use simple Redirect directive\",\n      });\n      continue;\n    }\n\n    // Only process Redirect directive\n    if (\n      !lowerLine.startsWith(\"redirect \") &&\n      !lowerLine.startsWith(\"redirect\\t\")\n    ) {\n      // Ignore other directives (RewriteEngine, RewriteBase, Options, etc.)\n      continue;\n    }\n\n    // Parse Redirect directive\n    // Format: Redirect [status] source target\n    // Status can be: 301, 302, 307, 308, permanent, temp, or omitted (defaults to 302)\n    const parts = trimmedLine.split(/\\s+/);\n\n    if (parts.length < 3) {\n      continue; // Invalid Redirect directive\n    }\n\n    // parts[0] is \"Redirect\"\n    let statusPart: string | undefined;\n    let from: string;\n    let to: string;\n\n    // Check if parts[1] is a status code or keyword\n    const part1Lower = parts[1].toLowerCase();\n    if (\n      /^\\d+$/.test(parts[1]) ||\n      part1Lower === \"permanent\" ||\n      part1Lower === \"temp\"\n    ) {\n      // Redirect status source target\n      statusPart = parts[1];\n      from = parts[2];\n      to = parts.slice(3).join(\" \").trim();\n    } else {\n      // Redirect source target (no status, defaults to 302 in Apache)\n      from = parts[1];\n      to = parts.slice(2).join(\" \").trim();\n      statusPart = \"302\";\n    }\n\n    if (!from || !to) {\n      continue;\n    }\n\n    const record: RawRecord = {\n      from,\n      to,\n      status: statusPart,\n    };\n\n    const result = normalizeRecord(record, lineNumber, trimmedLine);\n\n    if (\"redirect\" in result) {\n      redirects.push(result.redirect);\n    } else {\n      skipped.push(result.skipped);\n    }\n  }\n\n  return { redirects, skipped };\n};\n\n/**\n * Parse redirects from various formats (CSV, JSON, Netlify _redirects, htaccess)\n * Auto-detects format and returns parsed redirects with any skipped lines.\n */\nexport const parseRedirects = (content: string): ParseResult => {\n  const format = detectFormat(content);\n\n  switch (format) {\n    case \"empty\":\n      return { redirects: [], skipped: [] };\n    case \"json\":\n      return parseJSON(content);\n    case \"csv\":\n      return parseCSV(content);\n    case \"htaccess\":\n      return parseHtaccess(content);\n    case \"netlify\":\n      return parseNetlify(content);\n    default:\n      return { redirects: [], skipped: [] };\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/resource-utils.ts",
    "content": "import hash from \"@emotion/hash\";\nimport type { ResourceRequest } from \"@webstudio-is/sdk\";\n\nexport const getResourceKey = (resource: ResourceRequest) => {\n  try {\n    return hash(\n      JSON.stringify([\n        // explicitly list all fields to keep hash stable\n        resource.name,\n        resource.method,\n        resource.url,\n        resource.searchParams,\n        resource.headers,\n        resource.body,\n      ])\n    );\n  } catch {\n    // guard from invalid resources\n    return \"\";\n  }\n};\n"
  },
  {
    "path": "apps/builder/app/shared/resources.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { getResourceKey } from \"./resource-utils\";\nimport type { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndescribe(\"getResourceKey - pure function tests\", () => {\n  test(\"generates consistent hash for same resource\", () => {\n    const resource: ResourceRequest = {\n      name: \"test\",\n      method: \"get\",\n      url: \"/api/test\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const key1 = getResourceKey(resource);\n    const key2 = getResourceKey(resource);\n\n    expect(key1).toBe(key2);\n    expect(key1).toBeTruthy();\n    expect(typeof key1).toBe(\"string\");\n  });\n\n  test(\"generates different hashes for different resources\", () => {\n    const resource1: ResourceRequest = {\n      name: \"test1\",\n      method: \"get\",\n      url: \"/api/test1\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const resource2: ResourceRequest = {\n      name: \"test2\",\n      method: \"get\",\n      url: \"/api/test2\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const key1 = getResourceKey(resource1);\n    const key2 = getResourceKey(resource2);\n\n    expect(key1).not.toBe(key2);\n  });\n\n  test(\"different URLs produce different keys\", () => {\n    const base: ResourceRequest = {\n      name: \"api\",\n      method: \"get\",\n      url: \"/api/v1/users\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const modified: ResourceRequest = {\n      ...base,\n      url: \"/api/v2/users\",\n    };\n\n    expect(getResourceKey(base)).not.toBe(getResourceKey(modified));\n  });\n\n  test(\"different methods produce different keys\", () => {\n    const base: ResourceRequest = {\n      name: \"api\",\n      method: \"get\",\n      url: \"/api/users\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const modified: ResourceRequest = {\n      ...base,\n      method: \"post\",\n    };\n\n    expect(getResourceKey(base)).not.toBe(getResourceKey(modified));\n  });\n\n  test(\"search params affect the key\", () => {\n    const withoutParams: ResourceRequest = {\n      name: \"api\",\n      method: \"get\",\n      url: \"/api/users\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const withParams: ResourceRequest = {\n      ...withoutParams,\n      searchParams: [{ name: \"page\", value: \"1\" }],\n    };\n\n    expect(getResourceKey(withoutParams)).not.toBe(getResourceKey(withParams));\n  });\n\n  test(\"headers affect the key\", () => {\n    const withoutHeaders: ResourceRequest = {\n      name: \"api\",\n      method: \"get\",\n      url: \"/api/users\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const withHeaders: ResourceRequest = {\n      ...withoutHeaders,\n      headers: [{ name: \"Authorization\", value: \"Bearer token\" }],\n    };\n\n    expect(getResourceKey(withoutHeaders)).not.toBe(\n      getResourceKey(withHeaders)\n    );\n  });\n\n  test(\"body affects the key\", () => {\n    const withoutBody: ResourceRequest = {\n      name: \"api\",\n      method: \"post\",\n      url: \"/api/users\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const withBody: ResourceRequest = {\n      ...withoutBody,\n      body: JSON.stringify({ name: \"John\" }),\n    };\n\n    expect(getResourceKey(withoutBody)).not.toBe(getResourceKey(withBody));\n  });\n\n  test(\"handles resources with complex nested data\", () => {\n    const resource: ResourceRequest = {\n      name: \"complex\",\n      method: \"post\",\n      url: \"/api/test\",\n      searchParams: [\n        { name: \"filter\", value: \"active\" },\n        { name: \"sort\", value: \"desc\" },\n      ],\n      headers: [\n        { name: \"Content-Type\", value: \"application/json\" },\n        { name: \"Authorization\", value: \"Bearer xyz\" },\n      ],\n      body: JSON.stringify({ data: { nested: { value: true } } }),\n    };\n\n    const key = getResourceKey(resource);\n    expect(key).toBeTruthy();\n    expect(typeof key).toBe(\"string\");\n  });\n\n  test(\"order of search params matters\", () => {\n    const resource1: ResourceRequest = {\n      name: \"api\",\n      method: \"get\",\n      url: \"/api/test\",\n      searchParams: [\n        { name: \"a\", value: \"1\" },\n        { name: \"b\", value: \"2\" },\n      ],\n      headers: [],\n    };\n\n    const resource2: ResourceRequest = {\n      name: \"api\",\n      method: \"get\",\n      url: \"/api/test\",\n      searchParams: [\n        { name: \"b\", value: \"2\" },\n        { name: \"a\", value: \"1\" },\n      ],\n      headers: [],\n    };\n\n    // Order matters because we serialize the array as-is\n    expect(getResourceKey(resource1)).not.toBe(getResourceKey(resource2));\n  });\n\n  test(\"returns empty string for invalid resource with circular reference\", () => {\n    const invalidResource = {\n      name: \"test\",\n      method: \"get\" as const,\n      url: \"/api/test\",\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n      get searchParams(): any {\n        return [this];\n      },\n      headers: [],\n    };\n\n    const key = getResourceKey(invalidResource);\n    expect(key).toBe(\"\");\n  });\n\n  test(\"assets system resource produces consistent key\", () => {\n    const assetsResource: ResourceRequest = {\n      name: \"assets\",\n      method: \"get\",\n      url: \"/$resources/assets\",\n      searchParams: [],\n      headers: [],\n    };\n\n    const key1 = getResourceKey(assetsResource);\n    const key2 = getResourceKey(assetsResource);\n\n    expect(key1).toBe(key2);\n    expect(key1).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/resources.ts",
    "content": "import { atom, computed } from \"nanostores\";\nimport type { DataSource, Resource, ResourceRequest } from \"@webstudio-is/sdk\";\nimport { restResourcesLoader } from \"./router-utils\";\nimport { computeExpression } from \"./data-variables\";\nimport { fetch } from \"./fetch.client\";\nimport { getResourceKey } from \"./resource-utils\";\n\nconst MAX_PENDING_RESOURCES = 5;\n\nexport { getResourceKey };\n\nconst queue = new Map<string, ResourceRequest>();\nconst pending = new Map<string, ResourceRequest>();\nconst cache = new Map<string, unknown>();\n\nexport const $resourcesCache = atom(cache);\n\nconst updateCache = () => {\n  $resourcesCache.set(new Map(cache));\n};\n\nconst $pendingUpdater = atom({});\n\nconst updatePending = () => {\n  $pendingUpdater.set({});\n};\n\nexport const $hasPendingResources = computed(\n  $pendingUpdater,\n  () => queue.size > 0 || pending.size > 0\n);\n\nconst loadResources = async () => {\n  const list = Array.from(queue.values()).slice(0, MAX_PENDING_RESOURCES);\n  for (const resource of list) {\n    const key = getResourceKey(resource);\n    queue.delete(key);\n    pending.set(key, resource);\n  }\n  updatePending();\n  const response = await fetch(restResourcesLoader(), {\n    method: \"POST\",\n    body: JSON.stringify(Array.from(pending.values())),\n  });\n  if (response.ok) {\n    const results = new Map<string, unknown>(await response.json());\n    for (const [key, result] of results) {\n      cache.set(key, result);\n      pending.delete(key);\n    }\n  }\n  updateCache();\n  updatePending();\n  // restart loading until queue is empty\n  scheduleLoading();\n};\n\nlet timeoutId: undefined | number;\n\nconst scheduleLoading = () => {\n  // scheduling will be restarted after finishing pending one\n  // skip when there is nothing in queue\n  if (pending.size > 0 || queue.size === 0) {\n    return;\n  }\n  window.clearTimeout(timeoutId);\n  // start loading after one second of \"preload\" call\n  timeoutId = window.setTimeout(loadResources, 1000);\n};\n\nexport const preloadResource = (resource: ResourceRequest) => {\n  const key = getResourceKey(resource);\n  if (queue.has(key) || pending.has(key) || cache.has(key)) {\n    return;\n  }\n  // deduplicate resources in queue\n  queue.set(key, resource);\n  updatePending();\n  scheduleLoading();\n};\n\nexport const invalidateResource = (resource: ResourceRequest) => {\n  const key = getResourceKey(resource);\n  cache.delete(key);\n  preloadResource(resource);\n};\n\n/**\n * Invalidate the assets system resource.\n * Call this when assets are uploaded, deleted, or modified to refresh expressions using assets.\n */\nexport const invalidateAssets = () => {\n  const url = \"/$resources/assets\";\n  // System resources always use GET with no params/headers/body\n  const systemResourceRequest: ResourceRequest = {\n    name: \"assets\",\n    method: \"get\",\n    url,\n    searchParams: [],\n    headers: [],\n  };\n  invalidateResource(systemResourceRequest);\n};\n\nexport const computeResourceRequest = (\n  resource: Resource,\n  values: Map<DataSource[\"id\"], unknown>\n): ResourceRequest => {\n  const request: ResourceRequest = {\n    name: resource.name,\n    method: resource.method,\n    url: computeExpression(resource.url, values),\n    searchParams: (resource.searchParams ?? []).map(({ name, value }) => ({\n      name,\n      value: computeExpression(value, values),\n    })),\n    headers: resource.headers.map(({ name, value }) => ({\n      name,\n      value: computeExpression(value, values),\n    })),\n  };\n  if (resource.body !== undefined) {\n    request.body = computeExpression(resource.body, values);\n  }\n  return request;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/router-utils/index.ts",
    "content": "export * from \"./is-canvas\";\nexport * from \"./path-utils\";\n"
  },
  {
    "path": "apps/builder/app/shared/router-utils/is-canvas.ts",
    "content": "import { isBuilderUrl } from \"./origins\";\n\nexport const isBuilder = (request: Request): boolean => {\n  return isBuilderUrl(request.url) && false === isCanvas(request);\n};\n\nexport const isCanvas = (request: Request): boolean => {\n  const url = new URL(request.url);\n  if (isBuilderUrl(url.origin) && url.pathname === \"/canvas\") {\n    return true;\n  }\n\n  return false;\n};\n\nexport const isDashboard = (request: Request): boolean => {\n  if (isBuilder(request) || isCanvas(request)) {\n    return false;\n  }\n  return true;\n};\n\nexport const comparePathnames = (\n  pathnameOrUrlA: string,\n  pathnameOrUrlB: string\n) => {\n  const aPathname = new URL(pathnameOrUrlA, \"http://localhost\").pathname;\n  const bPathname = new URL(pathnameOrUrlB, \"http://localhost\").pathname;\n  return aPathname === bPathname;\n};\n\nexport const compareUrls = (urlA: string, urlB: string) => {\n  const aPathname = new URL(urlA, \"http://localhost\").href;\n  const bPathname = new URL(urlB, \"http://localhost\").href;\n  return aPathname === bPathname;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/router-utils/origins.ts",
    "content": "import { parseBuilderUrl } from \"@webstudio-is/http-client\";\n\nexport const getRequestOrigin = (urlStr: string) => {\n  const url = new URL(urlStr);\n\n  return url.origin;\n};\n\nexport const isCanvas = (urlStr: string): boolean => {\n  const url = new URL(urlStr);\n  const projectId = url.searchParams.get(\"projectId\");\n\n  return projectId !== null;\n};\n\nexport const isBuilderUrl = (urlStr: string): boolean => {\n  const { projectId } = parseBuilderUrl(urlStr);\n  return projectId !== undefined;\n};\n\nexport const getAuthorizationServerOrigin = (urlStr: string): string => {\n  const origin = getRequestOrigin(urlStr);\n  const { sourceOrigin } = parseBuilderUrl(origin);\n  return sourceOrigin;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/router-utils/path-utils.ts",
    "content": "import type { AUTH_PROVIDERS } from \"~/shared/session\";\nimport { publicStaticEnv } from \"~/env/env.static\";\nimport { getAuthorizationServerOrigin } from \"./origins\";\nimport type { BuilderMode } from \"../nano-states/misc\";\n\nconst searchParams = (params: Record<string, string | undefined | null>) => {\n  const searchParams = new URLSearchParams();\n  for (const [key, value] of Object.entries(params)) {\n    if (typeof value === \"string\") {\n      searchParams.set(key, value);\n    }\n  }\n  const asString = searchParams.toString();\n  return asString === \"\" ? \"\" : `?${asString}`;\n};\n\nexport const builderPath = ({\n  pageId,\n  authToken,\n  pageHash,\n  mode,\n  safemode = false,\n}: {\n  pageId?: string;\n  authToken?: string;\n  pageHash?: string;\n  mode?: \"preview\" | \"content\";\n  safemode?: boolean;\n} = {}) => {\n  return `/${searchParams({\n    pageId,\n    authToken,\n    pageHash,\n    mode,\n    safemode: safemode ? \"true\" : undefined,\n  })}`;\n};\n\nexport const builderUrl = (props: {\n  projectId: string;\n  pageId?: string;\n  origin: string;\n  authToken?: string;\n  mode?: BuilderMode;\n  safemode?: boolean;\n}) => {\n  const authServerOrigin = getAuthorizationServerOrigin(props.origin);\n\n  const url = new URL(\n    builderPath({\n      pageId: props.pageId,\n      authToken: props.authToken,\n      safemode: props.safemode,\n    }),\n    authServerOrigin\n  );\n\n  const fragments = url.host.split(\".\");\n  if (fragments.length <= 3) {\n    fragments.splice(0, 0, \"p-\" + props.projectId);\n  } else {\n    // staging | development branches\n    fragments[0] = \"p-\" + props.projectId + \"-dot-\" + fragments[0];\n  }\n\n  url.host = fragments.join(\".\");\n\n  if (props.mode !== undefined) {\n    url.searchParams.set(\"mode\", props.mode);\n  }\n\n  return url.href;\n};\n\nexport const dashboardPath = (\n  view: \"templates\" | \"search\" | \"projects\" = \"projects\"\n) => {\n  if (view === \"projects\") {\n    return `/dashboard`;\n  }\n  return `/dashboard/${view}`;\n};\n\nexport const dashboardUrl = (props: { origin: string }) => {\n  const authServerOrigin = getAuthorizationServerOrigin(props.origin);\n\n  return `${authServerOrigin}/dashboard`;\n};\n\nexport const cloneProjectUrl = (props: {\n  origin: string;\n  sourceAuthToken: string;\n}) => {\n  const authServerOrigin = getAuthorizationServerOrigin(props.origin);\n\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"projectToCloneAuthToken\", props.sourceAuthToken);\n\n  return `${authServerOrigin}/dashboard?${searchParams.toString()}`;\n};\n\nexport const loginPath = (params: {\n  error?: (typeof AUTH_PROVIDERS)[keyof typeof AUTH_PROVIDERS];\n  message?: string;\n  returnTo?: string;\n}) => `/login${searchParams(params)}`;\n\nexport const logoutPath = () => \"/logout\";\nexport const restLogoutPath = () => \"/dashboard-logout\";\n\nexport const userPlanSubscriptionPath = (subscriptionId?: string) => {\n  const urlSearchParams = new URLSearchParams();\n  urlSearchParams.set(\"return_url\", window.location.href);\n  if (subscriptionId) {\n    urlSearchParams.set(\"subscription\", subscriptionId);\n  }\n\n  return `/n8n/billing_portal/sessions?${urlSearchParams.toString()}`;\n};\n\nexport const authCallbackPath = ({\n  provider,\n}: {\n  provider: \"google\" | \"github\";\n}) => `/auth/${provider}/callback`;\n\nexport const authPath = ({\n  provider,\n}: {\n  provider: \"google\" | \"github\" | \"dev\";\n}) => `/auth/${provider}`;\n\nexport const restAssetsPath = () => {\n  return `/rest/assets`;\n};\n\nexport const restAssetsUploadPath = ({\n  name,\n  width,\n  height,\n}: {\n  name: string;\n  width?: number | undefined;\n  height?: number | undefined;\n}) => {\n  const urlSearchParams = new URLSearchParams();\n  if (width !== undefined) {\n    urlSearchParams.set(\"width\", String(width));\n  }\n  if (height !== undefined) {\n    urlSearchParams.set(\"height\", String(height));\n  }\n\n  if (urlSearchParams.size > 0) {\n    return `/rest/assets/${name}?${urlSearchParams.toString()}`;\n  }\n\n  return `/rest/assets/${name}`;\n};\n\nexport const restPatchPath = () => {\n  const urlSearchParams = new URLSearchParams();\n\n  urlSearchParams.set(\"client-version\", publicStaticEnv.VERSION);\n\n  const urlSearchParamsString = urlSearchParams.toString();\n\n  return `/rest/patch${\n    urlSearchParamsString ? `?${urlSearchParamsString}` : \"\"\n  }`;\n};\n\nexport const getCanvasUrl = () => {\n  return `/canvas`;\n};\n\nexport const restResourcesLoader = () => `/rest/resources-loader`;\n\nexport const marketplacePath = (method: string) =>\n  `/builder/marketplace/${method}`;\n"
  },
  {
    "path": "apps/builder/app/shared/session/index.ts",
    "content": "export * from \"./use-login-error-message\";\n"
  },
  {
    "path": "apps/builder/app/shared/session/use-login-error-message.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport { useSearchParams } from \"@remix-run/react\";\n\nexport const AUTH_PROVIDERS = {\n  LOGIN_DEV: \"login_dev\",\n  LOGIN_GITHUB: \"login_github\",\n  LOGIN_GOOGLE: \"login_google\",\n} as const;\n\nexport const LOGIN_ERROR_MESSAGES = {\n  [AUTH_PROVIDERS.LOGIN_DEV]: \"There has been an issue logging you in with dev\",\n  [AUTH_PROVIDERS.LOGIN_GITHUB]:\n    \"There has been an issue logging you in with Github\",\n  [AUTH_PROVIDERS.LOGIN_GOOGLE]:\n    \"There has been an issue logging you in with Google\",\n};\n\nexport const useLoginErrorMessage = (): string => {\n  const [searchParams, setSearchParams] = useSearchParams();\n  const [messageToReturn, setMessageToReturn] = useState(\"\");\n\n  useEffect(() => {\n    const error = searchParams.get(\"error\");\n    const message = searchParams.get(\"message\");\n\n    const hasMessageToShow =\n      error !== null && message != null && message !== \"\";\n\n    if (hasMessageToShow) {\n      console.error({ message });\n      setMessageToReturn(message);\n\n      setSearchParams((prevSearchParams) => {\n        const nextSearchParams = new URLSearchParams(prevSearchParams);\n        nextSearchParams.delete(\"error\");\n        nextSearchParams.delete(\"message\");\n        return nextSearchParams;\n      });\n      return;\n    }\n\n    switch (error) {\n      case AUTH_PROVIDERS.LOGIN_DEV:\n        setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_DEV]);\n        break;\n      case AUTH_PROVIDERS.LOGIN_GITHUB:\n        setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_GITHUB]);\n        break;\n      case AUTH_PROVIDERS.LOGIN_GOOGLE:\n        setMessageToReturn(LOGIN_ERROR_MESSAGES[AUTH_PROVIDERS.LOGIN_GOOGLE]);\n        break;\n\n      default:\n        break;\n    }\n  }, [searchParams, setSearchParams]);\n\n  return messageToReturn;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/share-project/index.ts",
    "content": "export { ShareProjectContainer } from \"./share-project-container\";\n"
  },
  {
    "path": "apps/builder/app/shared/share-project/share-project-container.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { builderUrl } from \"~/shared/router-utils\";\nimport { trpcClient } from \"../trpc/trpc-client\";\nimport { ShareProject, type LinkOptions } from \"./share-project\";\nimport { useStore } from \"@nanostores/react\";\nimport { $userPlanFeatures } from \"../nano-states\";\n\nconst useShareProjectContainer = (projectId: string) => {\n  const {\n    data,\n    load,\n    state: loadState,\n  } = trpcClient.authorizationToken.findMany.useQuery();\n  const { send: createToken, state: createState } =\n    trpcClient.authorizationToken.create.useMutation();\n  const { send: removeToken, state: removeState } =\n    trpcClient.authorizationToken.remove.useMutation();\n  const { send: updateToken, state: updateState } =\n    trpcClient.authorizationToken.update.useMutation();\n  const [links, setLinks] = useState(data ?? []);\n  const deletingLinks = useRef(new Set<string>());\n\n  useEffect(() => {\n    load({ projectId }, (data) => {\n      setLinks(data ?? []);\n    });\n  }, [load, projectId]);\n\n  const handleChangeDebounced = useDebouncedCallback((link: LinkOptions) => {\n    // Link is about to get deleted, no need to update.\n    if (deletingLinks.current.has(link.token)) {\n      return;\n    }\n    const updatedLink = {\n      projectId: projectId,\n      ...link,\n    };\n    const updatedLinks = links.map((currentLink) => {\n      if (currentLink.token === updatedLink.token) {\n        return { ...currentLink, ...updatedLink };\n      }\n      return currentLink;\n    });\n    // Optimistically set the links, otherwise checkbox will not move until we fetch an updated list.\n    setLinks(updatedLinks);\n    updateToken(updatedLink);\n  }, 100);\n\n  const handleDelete = (link: LinkOptions) => {\n    deletingLinks.current.add(link.token);\n\n    const updatedLinks = links.filter((currentLink) => {\n      return currentLink.token !== link.token;\n    });\n\n    setLinks(updatedLinks);\n\n    removeToken({ projectId: projectId, token: link.token });\n  };\n\n  const handleCreate = () => {\n    createToken(\n      {\n        projectId: projectId,\n        relation: \"viewers\",\n        name: \"Custom link\",\n      },\n      () => {\n        load({ projectId }, (data) => {\n          setLinks(data ?? []);\n        });\n      }\n    );\n  };\n\n  const isPending =\n    loadState !== \"idle\" ||\n    createState !== \"idle\" ||\n    removeState !== \"idle\" ||\n    updateState !== \"idle\";\n\n  return {\n    links,\n    handleChangeDebounced,\n    handleDelete,\n    handleCreate,\n    isPending,\n  };\n};\n\ntype ShareButtonProps = {\n  projectId: string;\n};\n\n/**\n * we place the logic inside Popover so that the fetcher does not exist outside of it.\n * Then remix will not call `trpc.findMany.useQuery` if Popover is closed\n */\nexport const ShareProjectContainer = ({ projectId }: ShareButtonProps) => {\n  const { allowAdditionalPermissions } = useStore($userPlanFeatures);\n  const {\n    links,\n    handleChangeDebounced,\n    handleDelete,\n    handleCreate,\n    isPending,\n  } = useShareProjectContainer(projectId);\n\n  return (\n    <ShareProject\n      allowAdditionalPermissions={allowAdditionalPermissions}\n      links={links}\n      onChange={handleChangeDebounced}\n      onDelete={handleDelete}\n      onCreate={handleCreate}\n      isPending={isPending}\n      builderUrl={({ authToken, mode }) =>\n        builderUrl({\n          projectId,\n          origin: window.location.origin,\n          authToken,\n          mode,\n        })\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/share-project/share-project.stories.tsx",
    "content": "import {\n  Button,\n  Flex,\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n  StorySection,\n} from \"@webstudio-is/design-system\";\nimport { useEffect, useState } from \"react\";\nimport { nanoid } from \"nanoid\";\nimport {\n  type LinkOptions,\n  ShareProject as ShareProjectComponent,\n} from \"./share-project\";\n\nexport default {\n  title: \"Share Project\",\n  component: ShareProjectComponent,\n};\n\nconst initialLinks: Array<LinkOptions> = [\n  {\n    token: nanoid(),\n    name: \"View Only\",\n    relation: \"viewers\",\n    canClone: false,\n    canCopy: false,\n    canPublish: false,\n  },\n  {\n    token: nanoid(),\n    name: \"View and Edit\",\n    relation: \"editors\",\n    canClone: false,\n    canCopy: false,\n    canPublish: false,\n  },\n  {\n    token: nanoid(),\n    name: \"Build\",\n    relation: \"builders\",\n    canClone: false,\n    canCopy: false,\n    canPublish: false,\n  },\n];\n\nconst INITIAL_LINKS: LinkOptions[] = [];\n\nconst useShareProject = (\n  links: Array<LinkOptions> = INITIAL_LINKS,\n  async = false\n) => {\n  const [currentLinks, setLinks] = useState(async ? [] : links);\n\n  const onChange = (updatedLink: LinkOptions) => {\n    setLinks(\n      currentLinks.map((link) =>\n        link.token === updatedLink.token ? updatedLink : link\n      )\n    );\n  };\n  const onDelete = (deletedLink: LinkOptions) => {\n    setLinks(currentLinks.filter((link) => link.token !== deletedLink.token));\n  };\n  const onCreate = () => {\n    setLinks([\n      ...currentLinks,\n      {\n        token: nanoid(),\n        name: \"Custom Link\",\n        relation: \"viewers\",\n        canClone: false,\n        canCopy: false,\n        canPublish: false,\n      },\n    ]);\n  };\n\n  useEffect(() => {\n    if (async) {\n      setTimeout(() => {\n        setLinks(links);\n      }, 1000);\n      return;\n    }\n\n    setLinks(links);\n  }, [async, links]);\n\n  return { links: currentLinks, onChange, onDelete, onCreate };\n};\n\nconst builderUrl = ({ authToken, mode }: { authToken: string; mode: string }) =>\n  `https://blabla.com/${authToken}/${mode}`;\n\nconst ShareProjectPopover = ({\n  label,\n  linkOptions,\n  async,\n}: {\n  label: string;\n  linkOptions?: Array<LinkOptions>;\n  async?: boolean;\n}) => {\n  const props = useShareProject(linkOptions, async);\n  return (\n    <Popover modal open>\n      <PopoverTrigger asChild>\n        <Button>{label}</Button>\n      </PopoverTrigger>\n      <PopoverContent>\n        <ShareProjectComponent\n          {...props}\n          allowAdditionalPermissions={false}\n          isPending={false}\n          builderUrl={builderUrl}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport const ShareProject = () => (\n  <StorySection title=\"Share Project\">\n    <Flex gap=\"9\" css={{ padding: 100 }}>\n      <ShareProjectPopover label=\"Empty\" />\n      <ShareProjectPopover label=\"With Links\" linkOptions={initialLinks} />\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "apps/builder/app/shared/share-project/share-project.tsx",
    "content": "import {\n  Fragment,\n  useId,\n  useState,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport {\n  Box,\n  Button,\n  css,\n  Flex,\n  Label,\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n  rawTheme,\n  Separator,\n  Switch,\n  theme,\n  Tooltip,\n  Collapsible,\n  keyframes,\n  Text,\n  InputField,\n  Link,\n  buttonStyle,\n  IconButton,\n  Checkbox,\n  Grid,\n} from \"@webstudio-is/design-system\";\nimport {\n  CopyIcon,\n  EllipsesIcon,\n  PlusIcon,\n  InfoCircleIcon,\n} from \"@webstudio-is/icons\";\nimport { CopyToClipboard } from \"~/shared/copy-to-clipboard\";\nimport { useIds } from \"../form-utils\";\nimport type { BuilderMode } from \"../nano-states\";\n\nconst Item = (props: ComponentProps<typeof Flex>) => (\n  <Flex\n    direction=\"column\"\n    css={{ padding: theme.panel.padding }}\n    gap=\"1\"\n    {...props}\n  />\n);\n\ntype PermissionProps = {\n  title: string;\n  info: ReactNode;\n  checked: boolean;\n  disabled?: boolean;\n  onCheckedChange: (checked: boolean) => void;\n};\nconst Permission = ({\n  title,\n  info,\n  checked,\n  disabled = false,\n  onCheckedChange,\n}: PermissionProps) => {\n  const id = useId();\n\n  const tooltipContent = (\n    <Flex direction=\"column\" gap=\"2\" css={{ maxWidth: theme.spacing[28] }}>\n      <Text variant=\"titles\">{title}</Text>\n      <Text>{info}</Text>\n    </Flex>\n  );\n\n  return (\n    <Flex align=\"center\" gap=\"1\">\n      <Switch\n        disabled={disabled}\n        checked={checked}\n        id={id}\n        onCheckedChange={onCheckedChange}\n      />\n      <Label disabled={disabled} htmlFor={id}>\n        {title}\n      </Label>\n      <Tooltip content={tooltipContent} variant=\"wrapped\">\n        <InfoCircleIcon color={rawTheme.colors.foregroundSubtle} tabIndex={0} />\n      </Tooltip>\n    </Flex>\n  );\n};\n\ntype MenuProps = {\n  name: string;\n  value: LinkOptions;\n  allowAdditionalPermissions: boolean;\n  onChange: (value: LinkOptions) => void;\n  onDelete: () => void;\n};\n\nconst Menu = ({\n  name,\n  allowAdditionalPermissions,\n  value,\n  onChange,\n  onDelete,\n}: MenuProps) => {\n  const ids = useIds([\"name\", \"canClone\", \"canCopy\", \"canPublish\"]);\n  const [isOpen, setIsOpen] = useState(false);\n  const [customLinkName, setCustomLinkName] = useState<string>(name);\n\n  const handleCheckedChange = (relation: Relation) => (checked: boolean) => {\n    if (checked) {\n      onChange({ ...value, relation });\n    }\n  };\n\n  const saveCustomLinkName = () => {\n    if (customLinkName.length === 0) {\n      return;\n    }\n\n    onChange({ ...value, name: customLinkName.trim() });\n  };\n\n  return (\n    <Popover open={isOpen} onOpenChange={setIsOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          prefix={<EllipsesIcon />}\n          color=\"ghost\"\n          aria-label=\"Menu Button for options\"\n        ></Button>\n      </PopoverTrigger>\n      <PopoverContent\n        css={{\n          //padding: 0,\n          width: theme.spacing[24],\n        }}\n        sideOffset={0}\n        onInteractOutside={saveCustomLinkName}\n      >\n        <Item>\n          <Label htmlFor={ids.name}>Name</Label>\n          <InputField\n            id={ids.name}\n            color={customLinkName.length === 0 ? \"error\" : undefined}\n            value={customLinkName}\n            onChange={(event) => setCustomLinkName(event.target.value)}\n            onKeyDown={(event) => {\n              if (event.key === \"Enter\") {\n                saveCustomLinkName();\n                setIsOpen(false);\n              }\n            }}\n            onBlur={saveCustomLinkName}\n            placeholder=\"Share Project\"\n            name=\"Name\"\n            autoFocus\n          />\n        </Item>\n        <Separator />\n        <Item>\n          <Label>Permissions</Label>\n          <Permission\n            checked={value.relation === \"viewers\"}\n            onCheckedChange={handleCheckedChange(\"viewers\")}\n            title=\"View\"\n            //info=\"Recipients can view, copy instances and clone the project\"\n            info={\n              <Flex direction=\"column\">\n                Recipients can view, copy instances and clone the project.\n                {!allowAdditionalPermissions && (\n                  <>\n                    <br />\n                    <br />\n                    Upgrade to a Pro account to set additional permissions.\n                    <br /> <br />\n                    <Link\n                      className={buttonStyle({ color: \"gradient\" })}\n                      color=\"contrast\"\n                      underline=\"none\"\n                      href=\"https://webstudio.is/pricing\"\n                      target=\"_blank\"\n                    >\n                      Upgrade\n                    </Link>\n                  </>\n                )}\n              </Flex>\n            }\n          />\n\n          <Grid\n            css={{\n              ml: theme.spacing[6],\n            }}\n          >\n            <Grid\n              gap={1}\n              flow={\"column\"}\n              css={{\n                alignItems: \"center\",\n                justifyContent: \"start\",\n              }}\n            >\n              <Checkbox\n                disabled={\n                  !allowAdditionalPermissions || value.relation !== \"viewers\"\n                }\n                checked={value.canClone}\n                onCheckedChange={(canClone) => {\n                  onChange({ ...value, canClone: Boolean(canClone) });\n                }}\n                id={ids.canClone}\n              />\n              <Label\n                htmlFor={ids.canClone}\n                disabled={\n                  !allowAdditionalPermissions || value.relation !== \"viewers\"\n                }\n              >\n                Can clone\n              </Label>\n            </Grid>\n            <Grid\n              gap={1}\n              flow={\"column\"}\n              css={{\n                alignItems: \"center\",\n                justifyContent: \"start\",\n              }}\n            >\n              <Checkbox\n                disabled={\n                  !allowAdditionalPermissions || value.relation !== \"viewers\"\n                }\n                checked={value.canCopy}\n                onCheckedChange={(canCopy) => {\n                  onChange({ ...value, canCopy: Boolean(canCopy) });\n                }}\n                id={ids.canCopy}\n              />\n              <Label\n                htmlFor={ids.canCopy}\n                disabled={\n                  !allowAdditionalPermissions || value.relation !== \"viewers\"\n                }\n              >\n                Can copy\n              </Label>\n            </Grid>\n          </Grid>\n\n          <Permission\n            disabled={!allowAdditionalPermissions}\n            onCheckedChange={handleCheckedChange(\"editors\")}\n            checked={value.relation === \"editors\"}\n            title=\"Content\"\n            info={\n              <Flex direction=\"column\">\n                Recipients can edit content only, such as text, images, and\n                predefined components.\n                {!allowAdditionalPermissions && (\n                  <>\n                    <br />\n                    <br />\n                    Upgrade to a Pro account to share with Content Edit\n                    permissions.\n                    <br /> <br />\n                    <Link\n                      className={buttonStyle({ color: \"gradient\" })}\n                      color=\"contrast\"\n                      underline=\"none\"\n                      href=\"https://webstudio.is/pricing\"\n                      target=\"_blank\"\n                    >\n                      Upgrade\n                    </Link>\n                  </>\n                )}\n              </Flex>\n            }\n          />\n          <Grid\n            css={{\n              ml: theme.spacing[6],\n            }}\n          >\n            <Grid\n              gap={1}\n              flow={\"column\"}\n              css={{\n                alignItems: \"center\",\n                justifyContent: \"start\",\n              }}\n            >\n              <Checkbox\n                disabled={\n                  !allowAdditionalPermissions || value.relation !== \"editors\"\n                }\n                checked={value.canPublish}\n                onCheckedChange={(canPublish) => {\n                  onChange({ ...value, canPublish: Boolean(canPublish) });\n                }}\n                id={ids.canPublish}\n              />\n              <Label\n                htmlFor={ids.canPublish}\n                disabled={\n                  !allowAdditionalPermissions || value.relation !== \"editors\"\n                }\n              >\n                Can publish\n              </Label>\n            </Grid>\n          </Grid>\n\n          <Permission\n            onCheckedChange={handleCheckedChange(\"builders\")}\n            checked={value.relation === \"builders\"}\n            title=\"Build\"\n            info=\"Recipients can make any changes but can not publish the project.\"\n          />\n\n          <Permission\n            disabled={!allowAdditionalPermissions}\n            onCheckedChange={handleCheckedChange(\"administrators\")}\n            checked={value.relation === \"administrators\"}\n            title=\"Admin\"\n            info={\n              <Flex direction=\"column\">\n                Recipients can make any changes and can also publish the\n                project.\n                {!allowAdditionalPermissions && (\n                  <>\n                    <br />\n                    <br />\n                    Upgrade to a Pro account to share with Admin permissions.\n                    <br /> <br />\n                    <Link\n                      className={buttonStyle({ color: \"gradient\" })}\n                      color=\"contrast\"\n                      underline=\"none\"\n                      href=\"https://webstudio.is/pricing\"\n                      target=\"_blank\"\n                    >\n                      Upgrade\n                    </Link>\n                  </>\n                )}\n              </Flex>\n            }\n          />\n        </Item>\n        <Separator />\n        <Item>\n          {/* @todo need a menu item that looks like one from dropdown but without DropdownMenu */}\n          <Button\n            color=\"neutral-destructive\"\n            onClick={() => {\n              onDelete();\n            }}\n          >\n            Delete Link\n          </Button>\n        </Item>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nconst itemStyle = css({\n  display: \"flex\",\n  alignItems: \"center\",\n  gap: theme.spacing[3],\n  padding: theme.panel.padding,\n  backgroundColor: theme.colors.backgroundPanel,\n});\n\ntype Relation = \"viewers\" | \"editors\" | \"builders\" | \"administrators\";\n\nexport type LinkOptions = {\n  token: string;\n  name: string;\n  relation: Relation;\n  canCopy: boolean;\n  canClone: boolean;\n  canPublish: boolean;\n};\n\ntype SharedLinkItemType = {\n  value: LinkOptions;\n  onChange: (value: LinkOptions) => void;\n  onDelete: () => void;\n  builderUrl: (props: { authToken: string; mode: BuilderMode }) => string;\n  allowAdditionalPermissions: boolean;\n};\n\nconst relationToMode: Record<Relation, BuilderMode> = {\n  viewers: \"preview\",\n  editors: \"content\",\n  builders: \"design\",\n  administrators: \"design\",\n};\n\nconst SharedLinkItem = ({\n  value,\n  onChange,\n  onDelete,\n  builderUrl,\n  allowAdditionalPermissions,\n}: SharedLinkItemType) => {\n  const [currentName, setCurrentName] = useState(value.name);\n\n  return (\n    <Box className={itemStyle()}>\n      <Label css={{ flexGrow: 1 }}>{currentName}</Label>\n      <CopyToClipboard\n        text={builderUrl({\n          authToken: value.token,\n          mode: relationToMode[value.relation],\n        })}\n        copyText=\"Copy link\"\n      >\n        <IconButton aria-label=\"Copy link\">\n          <CopyIcon aria-hidden />\n        </IconButton>\n      </CopyToClipboard>\n      <Menu\n        name={currentName}\n        value={value}\n        onChange={(value) => {\n          setCurrentName(value.name);\n          onChange(value);\n        }}\n        onDelete={onDelete}\n        allowAdditionalPermissions={allowAdditionalPermissions}\n      />\n    </Box>\n  );\n};\n\ntype ShareProjectProps = {\n  links?: Array<LinkOptions>;\n  onChange: (link: LinkOptions) => void;\n  onDelete: (link: LinkOptions) => void;\n  onCreate: () => void;\n  builderUrl: SharedLinkItemType[\"builderUrl\"];\n  isPending: boolean;\n  allowAdditionalPermissions: boolean;\n};\n\nconst animateCollapsibleHeight = keyframes({\n  \"0%\": {\n    height: 0,\n    overflow: \"hidden\",\n    opacity: 0,\n  },\n  \"100%\": {\n    height: \"var(--radix-collapsible-content-height)\",\n    overflow: \"hidden\",\n    opacity: 1,\n  },\n});\n\nconst collapsibleStyle = css({\n  animation: `${animateCollapsibleHeight} 200ms ${theme.easing.easeOut}`,\n});\n\nexport const ShareProject = ({\n  links = [],\n  onChange,\n  onDelete,\n  onCreate,\n  builderUrl,\n  isPending,\n  allowAdditionalPermissions,\n}: ShareProjectProps) => {\n  const items = links.map((link) => (\n    <Fragment key={link.token}>\n      <SharedLinkItem\n        onChange={(value) => {\n          onChange(value);\n        }}\n        onDelete={() => {\n          onDelete(link);\n        }}\n        builderUrl={builderUrl}\n        value={link}\n        allowAdditionalPermissions={allowAdditionalPermissions}\n      />\n      <Separator />\n    </Fragment>\n  ));\n\n  const create = (\n    <Box className={itemStyle({ css: { py: theme.spacing[\"7\"] } })}>\n      <Button\n        color=\"neutral\"\n        state={isPending ? \"pending\" : undefined}\n        prefix={\n          isPending ? <Flex css={{ width: theme.spacing[7] }} /> : <PlusIcon />\n        }\n        onClick={() => {\n          onCreate();\n        }}\n      >\n        {links.length === 0 ? \"Share a custom link\" : \"Add another link\"}\n      </Button>\n    </Box>\n  );\n\n  return (\n    <Flex direction=\"column\" css={{ width: theme.spacing[33] }}>\n      <Collapsible.Root open={items.length > 0}>\n        <Collapsible.Content className={collapsibleStyle()}>\n          {items}\n        </Collapsible.Content>\n      </Collapsible.Root>\n\n      {create}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/shim.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { mapGroupBy, setDifference, setIsSubsetOf, setUnion } from \"./shim\";\n\ntest(\"Set.prototype.difference\", () => {\n  // this set is bigger than other\n  expect(setDifference(new Set([1, 2, 3, 4]), new Set([3, 4, 5]))).toEqual(\n    new Set([1, 2])\n  );\n  // this set is smaller than other\n  expect(setDifference(new Set([1, 2, 3]), new Set([2, 3, 4, 5]))).toEqual(\n    new Set([1])\n  );\n});\n\ntest(\"Set.prototype.union\", () => {\n  expect(setUnion(new Set([2, 4, 6, 8]), new Set([1, 4, 9]))).toEqual(\n    new Set([2, 4, 6, 8, 1, 9])\n  );\n});\n\ntest(\"Set.prototype.isSubsetOf\", () => {\n  expect(setIsSubsetOf(new Set([1, 2, 3]), new Set([1, 2, 3]))).toBeTruthy();\n  expect(setIsSubsetOf(new Set([1, 2]), new Set([1, 2, 3]))).toBeTruthy();\n  expect(setIsSubsetOf(new Set(), new Set([1, 2, 3]))).toBeTruthy();\n  expect(setIsSubsetOf(new Set(), new Set())).toBeTruthy();\n  expect(setIsSubsetOf(new Set([1, 2, 3]), new Set([1, 2]))).toBeFalsy();\n  expect(setIsSubsetOf(new Set([1, 2, 3]), new Set())).toBeFalsy();\n});\n\ntest(\"Map.groupBy\", () => {\n  expect(mapGroupBy([1, 2, 3, 4, 5], (item) => item % 2)).toEqual(\n    new Map([\n      [0, [2, 4]],\n      [1, [1, 3, 5]],\n    ])\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/shared/shim.ts",
    "content": "export const setDifference = <Item>(current: Set<Item>, other: Set<Item>) => {\n  const result = new Set<Item>(current);\n  if (current.size <= other.size) {\n    for (const item of current) {\n      if (other.has(item)) {\n        result.delete(item);\n      }\n    }\n  } else {\n    for (const item of other) {\n      if (current.has(item)) {\n        result.delete(item);\n      }\n    }\n  }\n  return result;\n};\n\nexport const setUnion = <Item>(current: Set<Item>, other: Set<Item>) => {\n  const result = new Set<Item>(current);\n  for (const item of other) {\n    result.add(item);\n  }\n  return result;\n};\n\nexport const setIsSubsetOf = <Item>(current: Set<Item>, other: Set<Item>) => {\n  if (current.size > other.size) {\n    return false;\n  }\n  for (const item of current) {\n    if (!other.has(item)) {\n      return false;\n    }\n  }\n  return true;\n};\n\nexport const mapGroupBy = <Item, Key>(\n  array: Item[] | Iterable<Item>,\n  getKey: (item: Item) => Key\n) => {\n  const groups = new Map<Key, Item[]>();\n  for (const item of array) {\n    const key = getKey(item);\n    let group = groups.get(key);\n    if (group === undefined) {\n      group = [];\n      groups.set(key, group);\n    }\n    group.push(item);\n  }\n  return groups;\n};\n\nexport const objectGroupBy = <Item, Key>(\n  array: Item[] | Iterable<Item>,\n  getKey: (item: Item) => Key\n) => {\n  return Object.fromEntries(mapGroupBy(array, getKey));\n};\n\n// https://github.com/tc39/proposal-upsert\nexport const mapGetOrInsert = <Key, Value>(\n  map: Map<Key, Value>,\n  key: Key,\n  defaultValue: Value\n): Value => {\n  let value = map.get(key);\n  if (value === undefined) {\n    value = defaultValue;\n    map.set(key, value);\n  }\n  return value;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/store-utils.test.ts",
    "content": "import { test, expect } from \"vitest\";\nimport { type ReadableAtom, atom } from \"nanostores\";\nimport { shallowComputed } from \"./store-utils\";\n\ntype StoreValue<Store extends ReadableAtom> = Readonly<\n  ReturnType<Store[\"get\"]>\n>;\n\ntest(\"shallowComputed provides same reference when object/array values not changed\", () => {\n  const list = atom([\n    { type: \"a\", value: 0 },\n    { type: \"b\", value: 1 },\n    { type: \"a\", value: 2 },\n  ]);\n\n  const filtered = shallowComputed([list], (list) => {\n    return list.filter((item) => item.type === \"a\");\n  });\n\n  let prevList: StoreValue<typeof filtered> = filtered.get();\n  let computedList: StoreValue<typeof filtered> = filtered.get();\n  filtered.subscribe((list) => {\n    computedList = list;\n  });\n\n  expect(prevList).toBe(computedList);\n\n  // preserve the reference of array with same items\n  list.set([...list.get(), { type: \"b\", value: 3 }]);\n  expect(prevList).toBe(computedList);\n\n  // update reference when items added\n  list.set([...list.get(), { type: \"a\", value: 4 }]);\n  expect(prevList).not.toBe(computedList);\n  expect(computedList).toEqual([\n    { type: \"a\", value: 0 },\n    { type: \"a\", value: 2 },\n    { type: \"a\", value: 4 },\n  ]);\n  prevList = computedList;\n\n  list.set([...list.get(), { type: \"b\", value: 5 }]);\n  expect(prevList).toBe(computedList);\n\n  // update reference when items added\n  list.set(list.get().filter((item) => item.value !== 2));\n  expect(prevList).not.toBe(computedList);\n  expect(computedList).toEqual([\n    { type: \"a\", value: 0 },\n    { type: \"a\", value: 4 },\n  ]);\n  prevList = computedList;\n\n  list.off();\n  filtered.off();\n});\n"
  },
  {
    "path": "apps/builder/app/shared/store-utils.ts",
    "content": "import {\n  type AnyStore,\n  type ReadableAtom,\n  type StoreValue,\n  computed,\n} from \"nanostores\";\nimport { shallowEqual } from \"shallow-equal\";\n\n// https://github.com/nanostores/nanostores/blob/0d27b0d18d0f471ca0ac5439137d3dac71d01f08/computed/index.d.ts\n\ntype StoreValues<Stores extends AnyStore[]> = {\n  [Index in keyof Stores]: StoreValue<Stores[Index]>;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Comparable = Record<string, any> | any[] | null | undefined;\n\ninterface Computed {\n  <Value extends Comparable, OriginStores extends AnyStore[]>(\n    stores: [...OriginStores],\n    cb: (...values: StoreValues<OriginStores>) => Value\n  ): ReadableAtom<Value>;\n}\n\nexport const shallowComputed: Computed = (stores, cb) => {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  let prevComputed: any;\n  return (computed as Computed)(stores, (...values) => {\n    const nextComputed = cb(...values);\n    if (shallowEqual(prevComputed, nextComputed)) {\n      return prevComputed;\n    } else {\n      prevComputed = nextComputed;\n      return nextComputed;\n    }\n  });\n};\n"
  },
  {
    "path": "apps/builder/app/shared/string-utils.ts",
    "content": "import { noCase } from \"change-case\";\nimport { titleCase } from \"title-case\";\n\n// Initialize the cache with abbreviations that don't follow the title case rules.\nconst cache: Map<string, string> = new Map([\n  [\"id\", \"ID\"],\n  [\"url\", \"URL\"],\n]);\n\nexport const humanizeString = (string: string): string => {\n  let result = cache.get(string);\n  if (result === undefined) {\n    result = titleCase(noCase(string));\n    cache.set(string, result);\n  }\n  return result;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/style-object-model.test.tsx",
    "content": "import type { JSX } from \"react\";\nimport { describe, expect, test } from \"vitest\";\nimport type { HtmlTags } from \"html-tags\";\nimport {\n  type Breakpoint,\n  type Styles,\n  type StyleSourceSelections,\n  type Instance,\n  type StyleDecl,\n  getStyleDeclKey,\n} from \"@webstudio-is/sdk\";\nimport { $, renderData } from \"@webstudio-is/template\";\nimport { camelCaseProperty, parseCss } from \"@webstudio-is/css-data\";\nimport type {\n  CssProperty,\n  LayersValue,\n  ShadowValue,\n  StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  type StyleObjectModel,\n  getComputedStyleDecl,\n  getPresetStyleDeclKey,\n} from \"./style-object-model\";\n\n/**\n * Create model fixture with a few features\n *\n * presets\n * - each selector is a tag name\n *\n * css\n * - each selector is style source id\n * - media prelude is breakpoint id\n *\n * jsx\n * - ws:id - instance id\n * - ws:tag - instance tag\n * - class - the list of style source ids\n */\nconst createModel = ({\n  presets,\n  css,\n  jsx,\n  matchingBreakpoints,\n  matchingStates,\n}: {\n  presets?: Record<string, string>;\n  css: string;\n  jsx: JSX.Element;\n  matchingBreakpoints?: Breakpoint[\"id\"][];\n  matchingStates?: Set<string>;\n}): StyleObjectModel => {\n  const instanceTags = new Map<Instance[\"id\"], HtmlTags>();\n  const parsedStyles = parseCss(css);\n  const styles: Styles = new Map();\n  for (const { breakpoint, selector, state, property, value } of parsedStyles) {\n    const styleDecl: StyleDecl = {\n      styleSourceId: selector,\n      breakpointId: breakpoint ?? \"base\",\n      state,\n      property: camelCaseProperty(property),\n      value,\n    };\n    styles.set(getStyleDeclKey(styleDecl), styleDecl);\n  }\n  const { instances, props } = renderData(jsx);\n  const styleSourceSelections: StyleSourceSelections = new Map();\n  for (const prop of props.values()) {\n    if (prop.name === \"class\" && prop.type === \"string\") {\n      styleSourceSelections.set(prop.instanceId, {\n        instanceId: prop.instanceId,\n        values: prop.value.split(\" \"),\n      });\n    }\n  }\n  const instanceComponents = new Map<string, string>();\n  for (const instance of instances.values()) {\n    instanceComponents.set(instance.id, instance.component);\n    instanceTags.set(instance.id, (instance.tag ?? \"div\") as HtmlTags);\n  }\n  const presetStyles = new Map<string, StyleValue>();\n  for (const [componentName, css] of Object.entries(presets ?? {})) {\n    const parsedStyles = parseCss(css);\n    for (const styleDecl of parsedStyles) {\n      const key = getPresetStyleDeclKey({\n        component: componentName,\n        tag: styleDecl.selector,\n        state: styleDecl.state,\n        property: styleDecl.property,\n      });\n      presetStyles.set(key, styleDecl.value);\n    }\n  }\n  return {\n    styles,\n    styleSourceSelections,\n    presetStyles,\n    instanceTags,\n    instanceComponents,\n    matchingBreakpoints: matchingBreakpoints ?? [\"base\"],\n    matchingStates: matchingStates ?? new Set(),\n  };\n};\n\ntest(\"use cascaded style when specified and fallback to initial value\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        width: 10px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  // cascaded property\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 10 });\n  // initial for not inherited property\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"height\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n  // initial for inherited property\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"black\" });\n});\n\ntest(\"support initial keyword\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        width: initial;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n});\n\ntest(\"support inherit keyword\", () => {\n  const model = createModel({\n    css: `\n      level1Local {\n        height: 20px;\n      }\n      level2Local {\n        width: 10px;\n      }\n      level3Local {\n        width: inherit;\n        height: inherit;\n      }\n    `,\n    jsx: (\n      <$.Box ws:id=\"level1\" class=\"level1Local\">\n        <$.Box ws:id=\"level2\" class=\"level2Local\">\n          <$.Box ws:id=\"level3\" class=\"level3Local\"></$.Box>\n        </$.Box>\n      </$.Box>\n    ),\n  });\n  const instanceSelector = [\"level3\", \"level2\", \"level1\"];\n  // should inherit declared value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 10 });\n  // should inherit initial value as height is not inherited\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"height\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n});\n\ntest(\"support unset keyword\", () => {\n  const model = createModel({\n    css: `\n      level1Local {\n        color: blue;\n        width: 10px;\n      }\n      level2Local {\n        color: unset;\n        width: unset;\n      }\n    `,\n    jsx: (\n      <$.Box ws:id=\"level1\" class=\"level1Local\">\n        <$.Box ws:id=\"level2\" class=\"level2Local\"></$.Box>\n      </$.Box>\n    ),\n  });\n  const instanceSelector = [\"level2\", \"level1\"];\n  // when property is not inherited use initial value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n  // when property is inherited use inherited value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"blue\" });\n});\n\ntest(\"inherit style from ancestors\", () => {\n  const model = createModel({\n    css: `\n      level1Local {\n        color: blue;\n        width: 10px;\n      }\n    `,\n    jsx: (\n      <$.Box ws:id=\"level1\" class=\"level1Local\">\n        <$.Box ws:id=\"level2\" class=\"level2Local\">\n          <$.Box ws:id=\"level3\" class=\"level3Local\"></$.Box>\n        </$.Box>\n      </$.Box>\n    ),\n  });\n  const instanceSelector = [\"level3\", \"level2\", \"level1\"];\n  // inherited value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"blue\" });\n  // not inherited value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n});\n\ntest(\"support currentcolor keyword\", () => {\n  const model = createModel({\n    css: `\n      level1Local {\n        color: blue;\n      }\n      level2Local {\n        /* support lower case */\n        border-top-color: currentcolor;\n        /* support camel case */\n        background-color: currentColor;\n      }\n    `,\n    jsx: (\n      <$.Box ws:id=\"level1\" class=\"level1Local\">\n        <$.Box ws:id=\"level2\" class=\"level2Local\"></$.Box>\n      </$.Box>\n    ),\n  });\n  const instanceSelector = [\"level2\", \"level1\"];\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector,\n      property: \"border-top-color\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"blue\" });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector,\n      property: \"background-color\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"blue\" });\n});\n\ntest(\"in color property currentcolor is inherited\", () => {\n  const model = createModel({\n    css: `\n      level1Local {\n        color: blue;\n      }\n      level2Local {\n        color: currentcolor;\n      }\n      level3Local {\n        color: currentcolor;\n      }\n    `,\n    jsx: (\n      <$.Box ws:id=\"level1\" class=\"level1Local\">\n        <$.Box ws:id=\"level2\" class=\"level2Local\"></$.Box>\n      </$.Box>\n    ),\n  });\n  const instanceSelector = [\"level3\", \"level2\", \"level1\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"blue\" });\n});\n\ntest(\"in root color property currentcolor is initial\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        color: currentcolor;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"black\" });\n});\n\ntest(\"support custom properties\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --my-property: blue;\n        color: var(--my-property);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"--my-property\" })\n      .cascadedValue\n  ).toEqual({ type: \"unparsed\", value: \"blue\" });\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"unparsed\", value: \"blue\" });\n});\n\ntest(\"compute single custom property without layers\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --gradient: linear-gradient(#fff, #000), linear-gradient(#000, #fff);\n        background-image: var(--gradient);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"background-image\",\n    }).computedValue\n  ).toEqual({\n    type: \"unparsed\",\n    value: \"linear-gradient(#fff,#000),linear-gradient(#000,#fff)\",\n  });\n});\n\ntest(\"support custom properties in layers\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --gradient-1: linear-gradient(#fff, #000);\n        --gradient-2: linear-gradient(#000, #fff);\n        background-image: var(--gradient-1), var(--gradient-2);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"background-image\",\n    }).computedValue\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unparsed\", value: \"linear-gradient(#fff,#000)\" },\n      { type: \"unparsed\", value: \"linear-gradient(#000,#fff)\" },\n    ],\n  });\n});\n\ntest(\"parse single custom property without tuples\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --noise: contrast(300%) brightness(100%);\n        filter: var(--noise);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"filter\",\n    }).computedValue\n  ).toEqual({ type: \"unparsed\", value: \"contrast(300%) brightness(100%)\" });\n});\n\ntest(\"compute custom properties in shadows\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --blur: 30px;\n        --color: #ff0000;\n        box-shadow: 10px 20px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const layers = model.styles.get(\"bodyLocal:base:boxShadow:\")\n    ?.value as LayersValue;\n  const shadowValue = layers.value[0] as ShadowValue;\n  shadowValue.blur = { type: \"var\", value: \"blur\" };\n  shadowValue.color = { type: \"var\", value: \"color\" };\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"box-shadow\",\n    }).computedValue\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      {\n        type: \"shadow\",\n        position: \"outset\",\n        offsetX: { type: \"unit\", unit: \"px\", value: 10 },\n        offsetY: { type: \"unit\", unit: \"px\", value: 20 },\n        blur: { type: \"unit\", value: 30, unit: \"px\" },\n        color: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          components: [1, 0, 0],\n          alpha: 1,\n        },\n      },\n    ],\n  });\n});\n\ntest(\"support custom properties in tuples\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --noise-1: contrast(300%);\n        --noise-2: brightness(100%);\n        filter: var(--noise-1) var(--noise-2);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"filter\",\n    }).computedValue\n  ).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unparsed\", value: \"contrast(300%)\" },\n      { type: \"unparsed\", value: \"brightness(100%)\" },\n    ],\n  });\n});\n\ntest(\"support custom properties in unparsed values\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --size: 10px;\n        width: calc(var(--size) * 2);\n        height: calc(var(--height, 20px) * 2);\n        box-shadow: var(--size) red, var(--size) blue;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"width\",\n    }).computedValue\n  ).toEqual({\n    type: \"unparsed\",\n    value: \"calc(10px*2)\",\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"height\",\n    }).computedValue\n  ).toEqual({\n    type: \"unparsed\",\n    value: \"calc(20px*2)\",\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"box-shadow\",\n    }).computedValue\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unparsed\", value: \"10px red\" },\n      { type: \"unparsed\", value: \"10px blue\" },\n    ],\n  });\n});\n\ntest(\"support empty custom properties\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --inset: ;\n        box-shadow: var(--inset) red;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"--inset\",\n    }).computedValue\n  ).toEqual({ type: \"unparsed\", value: \"\" });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"box-shadow\",\n    }).computedValue\n  ).toEqual({\n    type: \"layers\",\n    value: [{ type: \"unparsed\", value: \"red\" }],\n  });\n});\n\ntest(\"use fallback value when custom property does not exist\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        color: var(--my-property, red);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"unparsed\", value: \"red\" });\n});\n\ntest(\"use initial value when custom property does not exist\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        color: var(--my-property);\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"black\" });\n});\n\ntest(\"use inherited value when custom property does not exist\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        color: red;\n        width: fit-content;\n      }\n      boxLocal {\n        color: var(--my-property);\n        width: var(--my-property);\n      }\n    `,\n    jsx: (\n      <$.Body ws:id=\"body\" class=\"bodyLocal\">\n        <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>\n      </$.Body>\n    ),\n  });\n  const instanceSelector = [\"box\", \"body\"];\n  // inherited property use inherited value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"red\" });\n  // not inherited property use initial value\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n});\n\ntest(\"inherit custom property\", () => {\n  const model = createModel({\n    css: `\n      level1Local {\n        --my-property: blue;\n      }\n      level3Local {\n        color: var(--my-property);\n      }\n    `,\n    jsx: (\n      <$.Box ws:id=\"level1\" class=\"level1Local\">\n        <$.Box ws:id=\"level2\" class=\"level2Local\">\n          <$.Box ws:id=\"level3\" class=\"level3Local\"></$.Box>\n        </$.Box>\n      </$.Box>\n    ),\n  });\n  const instanceSelector = [\"level3\", \"level2\", \"level1\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"--my-property\" })\n      .usedValue\n  ).toEqual({ type: \"unparsed\", value: \"blue\" });\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"unparsed\", value: \"blue\" });\n});\n\ntest(\"resolve dependency cycles in custom properties\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --color: red;\n      }\n      boxLocal {\n        --color: var(--another);\n        --another: var(--color);\n        color: var(--color);\n      }\n    `,\n    jsx: (\n      <$.Body ws:id=\"body\" class=\"bodyLocal\">\n        <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>\n      </$.Body>\n    ),\n  });\n  const instanceSelector = [\"box\", \"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"keyword\", value: \"black\" });\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"--color\" })\n      .usedValue\n  ).toEqual({ type: \"invalid\", value: \"\" });\n});\n\ntest(\"resolve non-cyclic references in custom properties\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --color: red;\n        --another: var(--color);\n      }\n      boxLocal {\n        --color: var(--another);\n        color: var(--color);\n      }\n    `,\n    jsx: (\n      <$.Body ws:id=\"body\" class=\"bodyLocal\">\n        <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>\n      </$.Body>\n    ),\n  });\n  const instanceSelector = [\"box\", \"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"color\" })\n      .usedValue\n  ).toEqual({ type: \"unparsed\", value: \"red\" });\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"--color\" })\n      .usedValue\n  ).toEqual({ type: \"unparsed\", value: \"red\" });\n});\n\ntest(\"allow multiple usages of the same custom property\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        --gradient: linear-gradient(#fff, #000);\n      }\n      boxLocal {\n        background-image: var(--gradient), var(--gradient);\n      }\n    `,\n    jsx: (\n      <$.Body ws:id=\"body\" class=\"bodyLocal\">\n        <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>\n      </$.Body>\n    ),\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"box\", \"body\"],\n      property: \"background-image\",\n    }).usedValue\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unparsed\", value: \"linear-gradient(#fff,#000)\" },\n      { type: \"unparsed\", value: \"linear-gradient(#fff,#000)\" },\n    ],\n  });\n});\n\ntest(\"cascade value from matching breakpoints\", () => {\n  const model = createModel({\n    css: `\n      @media small {\n        bodyLocal {\n          width: 20px;\n        }\n      }\n      @media large {\n        bodyLocal {\n          width: 30px;\n        }\n      }\n      bodyLocal {\n        width: 10px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    matchingBreakpoints: [\"base\", \"small\", \"large\"],\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 30 });\n});\n\ntest(\"ignore values from not matching breakpoints\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        width: 10px;\n      }\n      @media small {\n        bodyLocal {\n          width: 20px;\n        }\n      }\n      @media large {\n        bodyLocal {\n          width: 30px;\n        }\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    // large is not matching breakpoint\n    matchingBreakpoints: [\"base\", \"small\"],\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 20 });\n});\n\ntest(\"cascade value from matching style sources\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        width: 20px;\n      }\n      bodyToken {\n        width: 10px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyToken bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  // the latest token value wins\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 20 });\n});\n\ntest(\"cascade values with matching states\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal:hover {\n        width: 20px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    matchingStates: new Set([\":hover\"]),\n  });\n  const instanceSelector = [\"body\"];\n  // value with state wins\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 20 });\n});\n\ntest(\"prefer stateless values over matching states\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        width: 10px;\n      }\n      bodyLocal:hover {\n        width: 20px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    matchingStates: new Set([\":hover\"]),\n  });\n  const instanceSelector = [\"body\"];\n  // value with state wins\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 10 });\n});\n\ntest(\"ignore values from not matching states\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal:hover {\n        width: 20px;\n      }\n      bodyLocal:focus {\n        width: 30px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    matchingStates: new Set([\":hover\"]),\n  });\n  const instanceSelector = [\"body\"];\n  // ignore :focus state because it is not matching\n  expect(\n    getComputedStyleDecl({ model, instanceSelector, property: \"width\" })\n      .usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 20 });\n});\n\ntest(\"support html styles\", () => {\n  // @layer browser { body { display: block } }\n  const model = createModel({\n    css: \"\",\n    jsx: (\n      <$.Body ws:id=\"bodyId\" ws:tag=\"body\">\n        <$.Span ws:id=\"spanId\" ws:tag=\"span\"></$.Span>\n        <$.Heading ws:id=\"headingId\" ws:tag=\"h1\"></$.Heading>\n      </$.Body>\n    ),\n  });\n  // tag with browser styles\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"bodyId\"],\n      property: \"display\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"block\" });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"headingId\", \"bodyId\"],\n      property: \"margin-top\",\n    }).usedValue\n  ).toEqual({ type: \"unit\", value: 0.67, unit: \"em\" });\n  // tag without browser styles\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"spanId\"],\n      property: \"display\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"inline\" });\n});\n\ntest(\"support preset styles\", () => {\n  // @layer preset { body:hover { width: 100px } }\n  const model = createModel({\n    presets: {\n      Body: `\n        body {\n          width: 100px;\n        }\n      `,\n    },\n    css: ``,\n    jsx: <$.Body ws:id=\"body\" ws:tag=\"body\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"width\",\n    }).usedValue\n  ).toEqual({ type: \"unit\", value: 100, unit: \"px\" });\n});\n\ntest(\"ignore values from not matching states in preset styles\", () => {\n  // @layer preset {\n  //   body { color: red }\n  //   body:focus { color: blue }\n  // }\n  const model = createModel({\n    presets: {\n      Body: `\n        body {\n          color: red;\n        }\n        body:focus {\n          color: blue;\n        }\n      `,\n    },\n    css: ``,\n    jsx: <$.Body ws:id=\"body\" ws:tag=\"body\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"color\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"red\" });\n});\n\ntest(\"breakpoints are more specific than style sources\", () => {\n  const model = createModel({\n    css: `\n      @media small {\n        bodyToken {\n          width: 20px;\n        }\n      }\n      bodyLocal {\n        width: 10px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyToken bodyLocal\"></$.Body>,\n    matchingBreakpoints: [\"base\", \"small\"],\n  });\n  // bigger breakpoint on current style source should overide\n  // when the next style source has the same property on smaller breakpoint\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"width\",\n    }).usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 20 });\n});\n\ntest(\"breakpoints are more specific than matching states\", () => {\n  const model = createModel({\n    css: `\n      @media small {\n        bodyLocal {\n          width: 10px;\n        }\n      }\n      bodyLocal:hover {\n        width: 20px;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    matchingBreakpoints: [\"base\", \"small\"],\n    matchingStates: new Set([\":hover\"]),\n  });\n  // values with states override values without states on bigger breakpoint\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"width\",\n    }).usedValue\n  ).toEqual({ type: \"unit\", unit: \"px\", value: 10 });\n});\n\ntest(\"user styles are more specific than preset styles\", () => {\n  // @layer preset { body:hover { color: blue } }\n  const model = createModel({\n    presets: {\n      Body: `\n        /* with state */\n        body:hover {\n          color: blue;\n        }\n      `,\n    },\n    css: `\n      /* without state */\n      bodyLocal {\n        color: red;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n    matchingStates: new Set([\":hover\"]),\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"color\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"red\" });\n});\n\ntest(\"preset styles are more specific than browser styles\", () => {\n  // @layer browser { body { display: block } }\n  // @layer preset { body { display: flex } }\n  const model = createModel({\n    presets: {\n      Body: `\n        body {\n          display: flex;\n        }\n      `,\n    },\n    css: ``,\n    jsx: <$.Body ws:id=\"body\" ws:tag=\"body\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"display\",\n    }).usedValue\n  ).toEqual({ type: \"keyword\", value: \"flex\" });\n});\n\ntest(\"access cascaded value without resolving\", () => {\n  const model = createModel({\n    css: `\n      local {\n        color: initial;\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"local\"></$.Body>,\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"body\"],\n      property: \"color\",\n    }).cascadedValue\n  ).toEqual({ type: \"keyword\", value: \"initial\" });\n});\n\ntest(\"fallback cascaded value to initial value\", () => {\n  const model = createModel({\n    css: `\n      body {\n        width: 10px;\n      }\n    `,\n    jsx: (\n      <$.Body ws:id=\"body\" class=\"body\">\n        <$.Box ws:id=\"box\" class=\"box\"></$.Box>\n      </$.Body>\n    ),\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"box\", \"body\"],\n      property: \"width\",\n    }).cascadedValue\n  ).toEqual({ type: \"keyword\", value: \"auto\" });\n});\n\ntest(\"fallback cascaded value to inherited unresolved value\", () => {\n  const model = createModel({\n    css: `\n      body {\n        --color: red;\n        color: var(--color);\n      }\n    `,\n    jsx: (\n      <$.Body ws:id=\"body\" class=\"body\">\n        <$.Box ws:id=\"box\" class=\"box\"></$.Box>\n      </$.Body>\n    ),\n  });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector: [\"box\", \"body\"],\n      property: \"color\",\n    }).cascadedValue\n  ).toEqual({ type: \"var\", value: \"color\" });\n});\n\ntest(\"work with unknown or invalid properties\", () => {\n  const model = createModel({\n    css: `\n      bodyLocal {\n        unknown-property: [object Object];\n      }\n    `,\n    jsx: <$.Body ws:id=\"body\" class=\"bodyLocal\"></$.Body>,\n  });\n  const instanceSelector = [\"body\"];\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector,\n      property: \"unknownProperty\" as CssProperty,\n    }).usedValue\n  ).toEqual({ type: \"unparsed\", value: \"[object Object]\" });\n  expect(\n    getComputedStyleDecl({\n      model,\n      instanceSelector,\n      property: \"undefinedProperty\" as CssProperty,\n    }).usedValue\n  ).toEqual({ type: \"invalid\", value: \"\" });\n});\n\ndescribe(\"selected style\", () => {\n  test(\"access selected style source value within cascade\", () => {\n    const model = createModel({\n      css: `\n        token {\n          color: red;\n        }\n        local {\n          color: blue;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"token local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"token\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"fallback to previous style source\", () => {\n    const model = createModel({\n      css: `\n        token {\n          color: red;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"token local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"local\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"fallback to final style source\", () => {\n    const model = createModel({\n      css: `\n        first {\n          color: red;\n        }\n        local {\n          color: blue;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"first second local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"second\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"blue\" });\n  });\n\n  test(\"access selected state value\", () => {\n    const model = createModel({\n      css: `\n        local {\n          color: red;\n        }\n        local:hover {\n          color: green;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"green\" });\n  });\n\n  test(\"fallback to stateless value\", () => {\n    const model = createModel({\n      css: `\n        local {\n          color: red;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"prefer stateless when states are matched but not selected\", () => {\n    const model = createModel({\n      css: `\n        local {\n          color: red;\n        }\n        local:hover {\n          color: blue;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"local\"></$.Body>,\n      matchingStates: new Set([\":hover\"]),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"prefer selected state over matched one\", () => {\n    const model = createModel({\n      css: `\n        local:hover {\n          color: green;\n        }\n        local:focus {\n          color: blue;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"local\"></$.Body>,\n      matchingStates: new Set([\":hover\", \":focus\"]),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"green\" });\n  });\n\n  test(\"prefer selected state over breakpoint\", () => {\n    const model = createModel({\n      css: `\n        local:hover {\n          color: red;\n        }\n        @media small {\n          local {\n            color: blue;\n          }\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" class=\"local\"></$.Body>,\n      matchingBreakpoints: [\"base\", \"small\"],\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"prefer selected state from preset\", () => {\n    const model = createModel({\n      presets: {\n        Body: `\n          body {\n            color: red;\n          }\n          body:hover {\n            color: blue;\n          }\n        `,\n      },\n      css: \"\",\n      jsx: <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"blue\" });\n  });\n});\n\ndescribe(\"style value source\", () => {\n  test(\"default\", () => {\n    const model = createModel({\n      css: \"\",\n      jsx: <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({ name: \"default\" });\n  });\n\n  test(\"preset\", () => {\n    const model = createModel({\n      presets: {\n        Body: `\n          body {\n            color: red;\n          }\n        `,\n      },\n      css: \"\",\n      jsx: <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"local\"></$.Body>,\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({ name: \"preset\", instanceId: \"body\" });\n  });\n\n  test(\"remote style source\", () => {\n    const model = createModel({\n      // presets should be ignored\n      presets: {\n        Body: `\n          body {\n            color: yellow;\n          }\n        `,\n      },\n      css: `\n        second {\n          color: red;\n        }\n      `,\n      jsx: (\n        <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"first second third\"></$.Body>\n      ),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"first\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"remote\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"second\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"second\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"second\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"third\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"remote\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"second\",\n    });\n  });\n\n  test(\"overwritten style source\", () => {\n    const model = createModel({\n      // presets should be ignored\n      presets: {\n        Body: `\n          body {\n            color: yellow;\n          }\n        `,\n      },\n      css: `\n        first {\n          color: red;\n        }\n        second {\n          color: green;\n        }\n        third {\n          color: blue;\n        }\n      `,\n      jsx: (\n        <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"first second third\"></$.Body>\n      ),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"first\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"overwritten\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"third\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"second\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"overwritten\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"third\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        styleSourceId: \"third\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"third\",\n    });\n  });\n\n  test(\"remote matching state\", () => {\n    const model = createModel({\n      css: `\n        local:hover {\n          color: red;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"local\"></$.Body>,\n      matchingStates: new Set([\":hover\"]),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"remote\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"local\",\n      state: \":hover\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"local\",\n      state: \":hover\",\n    });\n  });\n\n  test(\"overwritten stateless\", () => {\n    const model = createModel({\n      css: `\n        local {\n          color: red;\n        }\n        local:hover {\n          color: blue;\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"local\"></$.Body>,\n      matchingStates: new Set([\":hover\"]),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"overwritten\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"local\",\n      state: \":hover\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        state: \":hover\",\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"local\",\n      state: \":hover\",\n    });\n  });\n\n  test(\"remote breakpoint\", () => {\n    const model = createModel({\n      css: `\n        local {\n          color: red;\n        }\n        @media small {\n          local {\n            border-top-color: blue;\n          }\n        }\n      `,\n      jsx: <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"local\"></$.Body>,\n      matchingBreakpoints: [\"base\", \"small\"],\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"remote\",\n      instanceId: \"body\",\n      breakpointId: \"base\",\n      styleSourceId: \"local\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"body\"],\n        property: \"border-top-color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"body\",\n      breakpointId: \"small\",\n      styleSourceId: \"local\",\n    });\n  });\n\n  test(\"remote inherited\", () => {\n    const model = createModel({\n      css: `\n        bodyLocal {\n          color: red;\n        }\n        boxLocal {\n          color: blue;\n          width: 100%;\n        }\n      `,\n      jsx: (\n        <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"bodyLocal\">\n          <$.Box ws:id=\"outer\" ws:tag=\"div\" class=\"outerLocal\">\n            <$.Box ws:id=\"box\" ws:tag=\"div\" class=\"boxLocal\">\n              <$.Box ws:id=\"inner\" ws:tag=\"div\" class=\"innerLocal\"></$.Box>\n            </$.Box>\n          </$.Box>\n        </$.Body>\n      ),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"inner\", \"box\", \"outer\", \"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"remote\",\n      instanceId: \"box\",\n      breakpointId: \"base\",\n      styleSourceId: \"boxLocal\",\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"inner\", \"box\", \"outer\", \"body\"],\n        property: \"width\",\n      }).source\n    ).toEqual({ name: \"default\" });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"box\", \"outer\", \"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"box\",\n      breakpointId: \"base\",\n      styleSourceId: \"boxLocal\",\n    });\n  });\n\n  test(\"local with inherit keyword\", () => {\n    const model = createModel({\n      css: `\n        bodyLocal {\n          color: red;\n        }\n        boxLocal {\n          color: inherit;\n        }\n      `,\n      jsx: (\n        <$.Body ws:id=\"body\" ws:tag=\"body\" class=\"bodyLocal\">\n          <$.Box ws:id=\"box\" ws:tag=\"div\" class=\"boxLocal\"></$.Box>\n        </$.Body>\n      ),\n    });\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector: [\"box\", \"body\"],\n        property: \"color\",\n      }).source\n    ).toEqual({\n      name: \"local\",\n      instanceId: \"box\",\n      breakpointId: \"base\",\n      styleSourceId: \"boxLocal\",\n    });\n  });\n});\n\ndescribe(\"pseudo-element inheritance\", () => {\n  test(\"pseudo-elements do not cascade non-inherited properties\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          width: 100px;\n          height: 50px;\n          display: flex;\n        }\n        boxLocal::before {\n          content: \"test\";\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Non-inherited properties should NOT cascade to pseudo-element\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"height\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"display\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"inline\" });\n\n    // Directly set property on pseudo-element should work\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"content\",\n      }).usedValue\n    ).toEqual({ type: \"unparsed\", value: '\"test\"' });\n  });\n\n  test(\"pseudo-elements inherit inherited properties\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          color: red;\n          font-size: 16px;\n          font-family: Arial;\n        }\n        boxLocal::before {\n          content: \"test\";\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Inherited properties should be inherited by pseudo-element\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"font-size\",\n      }).usedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 16 });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"font-family\",\n      }).usedValue\n    ).toEqual({ type: \"fontFamily\", value: [\"Arial\"] });\n  });\n\n  test(\"pseudo-elements can override inherited properties\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          color: red;\n          font-size: 16px;\n        }\n        boxLocal::before {\n          color: blue;\n          font-size: 20px;\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Pseudo-element can override inherited properties\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"blue\" });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"font-size\",\n      }).usedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 20 });\n  });\n\n  test(\"pseudo-elements support inherit keyword\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          width: 100px;\n          color: red;\n        }\n        boxLocal::before {\n          width: inherit;\n          color: inherit;\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Explicit inherit should work for non-inherited properties\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 100 });\n\n    // Explicit inherit should work for inherited properties\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n  });\n\n  test(\"pseudo-elements support initial keyword\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          color: red;\n        }\n        boxLocal::before {\n          color: initial;\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Initial keyword should reset to initial value\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"black\" });\n  });\n\n  test(\"pseudo-elements support unset keyword\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          color: red;\n          width: 100px;\n        }\n        boxLocal::before {\n          color: unset;\n          width: unset;\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Unset on inherited property should inherit\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"red\" });\n\n    // Unset on non-inherited property should use initial value\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n  });\n\n  test(\"multiple pseudo-elements on same instance\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          color: red;\n          width: 100px;\n        }\n        boxLocal::before {\n          content: \"before\";\n          color: blue;\n        }\n        boxLocal::after {\n          content: \"after\";\n          color: green;\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // ::before should have its own color\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"blue\" });\n\n    // ::before should not cascade width\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n\n    // ::after should have its own color\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::after\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"green\" });\n\n    // ::after should not cascade width\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::after\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n  });\n\n  test(\"pseudo-elements with custom properties\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          --my-color: red;\n          --my-size: 100px;\n          color: var(--my-color);\n        }\n        boxLocal::before {\n          color: var(--my-color);\n          width: var(--my-size);\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n    });\n    const instanceSelector = [\"box\"];\n\n    // Custom properties are always inherited\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"--my-color\",\n      }).computedValue\n    ).toEqual({ type: \"unparsed\", value: \"red\" });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"--my-size\",\n      }).computedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 100 });\n\n    // Pseudo-element should be able to use inherited custom properties\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"unparsed\", value: \"red\" });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::before\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 100 });\n  });\n\n  test(\"pseudo-classes still cascade properties (not pseudo-elements)\", () => {\n    const model = createModel({\n      css: `\n        boxLocal {\n          width: 100px;\n          color: red;\n        }\n        boxLocal:hover {\n          color: blue;\n        }\n      `,\n      jsx: <$.Box ws:id=\"box\" class=\"boxLocal\"></$.Box>,\n      matchingStates: new Set([\":hover\"]),\n    });\n    const instanceSelector = [\"box\"];\n\n    // Pseudo-classes should still cascade properties (original behavior)\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \":hover\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 100 });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \":hover\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"blue\" });\n  });\n\n  test(\"pseudo-element ::placeholder\", () => {\n    const model = createModel({\n      css: `\n        inputLocal {\n          color: black;\n          font-size: 16px;\n          width: 200px;\n        }\n        inputLocal::placeholder {\n          color: gray;\n        }\n      `,\n      jsx: <$.Input ws:id=\"input\" ws:tag=\"input\" class=\"inputLocal\"></$.Input>,\n    });\n    const instanceSelector = [\"input\"];\n\n    // ::placeholder should inherit font-size but not width\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::placeholder\",\n        property: \"font-size\",\n      }).usedValue\n    ).toEqual({ type: \"unit\", unit: \"px\", value: 16 });\n\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::placeholder\",\n        property: \"width\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"auto\" });\n\n    // ::placeholder should have its own color\n    expect(\n      getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: \"::placeholder\",\n        property: \"color\",\n      }).usedValue\n    ).toEqual({ type: \"keyword\", value: \"gray\" });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/style-object-model.ts",
    "content": "import { generate, walk, List, type CssNode } from \"css-tree\";\nimport type { HtmlTags } from \"html-tags\";\nimport {\n  camelCaseProperty,\n  cssTryParseValue,\n  html,\n  parseCssVar,\n  propertiesData,\n  isPseudoElement,\n} from \"@webstudio-is/css-data\";\nimport {\n  type StyleValue,\n  type VarFallback,\n  type VarValue,\n  type UnparsedValue,\n  type CssProperty,\n  toValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  type Instance,\n  type StyleDecl,\n  type StyleSourceSelections,\n  type Styles,\n  getStyleDeclKey,\n} from \"@webstudio-is/sdk\";\n\n/**\n *\n * use cases where computing is called\n *\n * 1. compiler\n *\n * put the whole tree into the model once\n * lazily compute each point in the tree reusing cached cascaded values\n *\n * 2. builder style panel\n *\n * property is updated within some point in the tree\n * invalidate only changed properties in model\n * lazily recompute changed properties\n *\n * custom property or color is updated within some point in the tree\n * invalidate dependent properties in model\n * lazily recompute changed and dependent properties\n *\n * any selection is changed\n * invalidate model\n * lazily recompute all properties\n *\n * 3. CSS preview\n *\n * all properties are collected from instance selector\n * property are iterated with own model and computed\n *\n */\n\ntype InstanceSelector = string[];\n\nexport type StyleValueSourceColor =\n  | \"default\"\n  | \"preset\"\n  | \"remote\"\n  | \"local\"\n  | \"overwritten\";\n\nexport type StyleValueSource = {\n  name: StyleValueSourceColor;\n  instanceId?: Instance[\"id\"];\n  styleSourceId?: StyleDecl[\"styleSourceId\"];\n  state?: StyleDecl[\"state\"];\n  breakpointId?: StyleDecl[\"breakpointId\"];\n};\n\n/**\n * model contains all data and cache of computed styles\n * and manages reactive subscriptions\n */\nexport type StyleObjectModel = {\n  styles: Styles;\n  styleSourceSelections: StyleSourceSelections;\n  // component:tag:state:property\n  presetStyles: Map<string, StyleValue>;\n  instanceTags: Map<Instance[\"id\"], HtmlTags>;\n  instanceComponents: Map<Instance[\"id\"], Instance[\"component\"]>;\n  /**:\n   * all currently matching and ordered breakpoints\n   */\n  matchingBreakpoints: StyleDecl[\"breakpointId\"][];\n  /**\n   * all currently matching and ordered breakpointsgg\n   */\n  matchingStates: Set<string>;\n};\n\nexport const getPresetStyleDeclKey = ({\n  component,\n  tag,\n  state,\n  property,\n}: {\n  component: string;\n  tag: string;\n  state?: string;\n  property: CssProperty;\n}) => `${component}:${tag}:${state ?? \"\"}:${property}`;\n\n/**\n *\n * Standard specificity is defined as ID-CLASS-TYPE.\n * Though webstudio does not rely on these and also\n * cannot rely on order of declarations.\n *\n * Instead webstudio define own specificity format\n * LAYER-STATE-BREAKPOINT-STYLESOURCE\n *\n * LAYER is similar to @layer and allows to group parts of styles like browser, preset, user styles\n * and preset states are wrapped with :where to avoid specificity increasing\n * in the future can be replaced with actual cascade laters\n * Declaration with selected STATE gets 2, with any other STATE gets 1\n * Declaration BREAKPOINT is its position in ordered list\n * Declaration STYLESOURCE is its position in predefined list\n * excluding everything after selected STYLESOURCE\n *\n */\nconst getCascadedValue = ({\n  model,\n  instanceId,\n  styleSourceId: selectedStyleSourceId,\n  state: selectedState,\n  property,\n  forPseudoElement = false,\n}: {\n  model: StyleObjectModel;\n  instanceId: Instance[\"id\"];\n  styleSourceId?: StyleDecl[\"styleSourceId\"];\n  state?: StyleDecl[\"state\"];\n  property: CssProperty;\n  forPseudoElement?: boolean;\n}) => {\n  const {\n    styles,\n    styleSourceSelections,\n    presetStyles,\n    instanceTags,\n    instanceComponents,\n    matchingBreakpoints,\n    matchingStates,\n  } = model;\n  const tag = instanceTags.get(instanceId);\n  const component = instanceComponents.get(instanceId);\n  let selectedIndex = -1;\n  // store the source of latest value\n  let source: StyleValueSource = { name: \"default\" };\n\n  // https://drafts.csswg.org/css-cascade-5/#declared\n  const declaredValues: StyleValue[] = [];\n\n  // browser styles - pseudo-elements don't inherit browser styles from parent\n  if (tag && !forPseudoElement) {\n    const key = `${tag}:${property}`;\n    const browserValue = html.get(key);\n    if (browserValue) {\n      declaredValues.push(browserValue);\n    }\n  }\n\n  const states = new Set<undefined | string>();\n\n  // When computing for a pseudo-element, only include the pseudo-element state itself\n  // This prevents cascading of properties from the parent element\n  if (forPseudoElement) {\n    if (selectedState) {\n      states.add(selectedState);\n    }\n  } else {\n    // Original behavior for non-pseudo-elements\n    // allow stateless to be overwritten\n    states.add(undefined);\n    for (const state of matchingStates) {\n      states.add(state);\n    }\n    // move selected state in the end if already present in matching states\n    if (selectedState) {\n      states.delete(selectedState);\n      states.add(selectedState);\n    }\n  }\n\n  // preset component styles\n  if (component && tag) {\n    for (const state of states) {\n      const key = getPresetStyleDeclKey({\n        component,\n        tag,\n        state,\n        property,\n      });\n      const styleValue = presetStyles.get(key);\n      if (styleValue) {\n        source = { name: \"preset\", state, instanceId };\n        declaredValues.push(styleValue);\n      }\n    }\n  }\n\n  // user styles\n  const styleSourceIds = styleSourceSelections.get(instanceId)?.values ?? [];\n  selectedStyleSourceId ??= styleSourceIds.at(-1);\n  for (const state of states) {\n    for (const breakpointId of matchingBreakpoints) {\n      for (const styleSourceId of styleSourceIds) {\n        const key = getStyleDeclKey({\n          styleSourceId,\n          breakpointId,\n          state,\n          property: camelCaseProperty(property),\n        });\n        const styleDecl = styles.get(key);\n        if (\n          styleSourceId === selectedStyleSourceId &&\n          state === selectedState\n        ) {\n          // reset selection from another state or breakpoint\n          selectedIndex = styleDecl ? declaredValues.length : -1;\n        }\n        if (styleDecl) {\n          source = {\n            name: \"remote\",\n            instanceId,\n            styleSourceId,\n            state,\n            breakpointId,\n          };\n          declaredValues.push(styleDecl.value);\n        }\n      }\n    }\n  }\n\n  // https://drafts.csswg.org/css-cascade-5/#cascaded\n  // when reset or unselected (-1) take last declared value\n  const cascadedValue = declaredValues.at(selectedIndex);\n  if (cascadedValue) {\n    if (selectedIndex > -1) {\n      // local when selected value is latest declared\n      if (selectedIndex === declaredValues.length - 1) {\n        source.name = \"local\";\n      } else {\n        source.name = \"overwritten\";\n      }\n    }\n    return { value: cascadedValue, source };\n  }\n};\n\nconst matchKeyword = (styleValue: undefined | StyleValue, keyword: string) =>\n  styleValue?.type === \"keyword\" && styleValue.value.toLowerCase() === keyword;\n\n/**\n * stable invalid values to support caching\n */\nconst guaranteedInvalidValue: StyleValue = { type: \"guaranteedInvalid\" };\nconst invalidValue: StyleValue = { type: \"invalid\", value: \"\" };\n\nconst customPropertyData = {\n  inherited: true,\n  initial: guaranteedInvalidValue,\n};\n\nconst invalidPropertyData = {\n  inherited: false,\n  initial: invalidValue,\n};\n\nconst substituteVars = (\n  styleValue: StyleValue,\n  mapper: (value: VarValue) => StyleValue\n): StyleValue => {\n  if (styleValue.type === \"var\") {\n    return mapper(styleValue);\n  }\n  if (styleValue.type === \"shadow\") {\n    const newShadowValue = { ...styleValue };\n    if (newShadowValue.offsetX.type === \"var\") {\n      newShadowValue.offsetX = mapper(newShadowValue.offsetX) as VarValue;\n    }\n    if (newShadowValue.offsetY.type === \"var\") {\n      newShadowValue.offsetY = mapper(newShadowValue.offsetY) as VarValue;\n    }\n    if (newShadowValue.blur?.type === \"var\") {\n      newShadowValue.blur = mapper(newShadowValue.blur) as VarValue;\n    }\n    if (newShadowValue.spread?.type === \"var\") {\n      newShadowValue.spread = mapper(newShadowValue.spread) as VarValue;\n    }\n    if (newShadowValue.color?.type === \"var\") {\n      newShadowValue.color = mapper(newShadowValue.color) as VarValue;\n    }\n    return newShadowValue;\n  }\n  // slightly optimize to not parse without variables\n  if (styleValue.type === \"unparsed\" && styleValue.value.includes(\"var(\")) {\n    // parse, replace variables and serialize back to unparsed value\n    const ast = cssTryParseValue(styleValue.value);\n    if (ast) {\n      walk(ast, {\n        enter(node, item, list) {\n          if (node.type === \"Function\" && node.name === \"var\") {\n            const varValue = parseCssVar(node);\n            if (varValue && item) {\n              const newValue = mapper(varValue);\n              list?.replace(\n                item,\n                List.createItem<CssNode>({\n                  type: \"Raw\",\n                  value: toValue(newValue),\n                  loc: null,\n                })\n              );\n            }\n          }\n        },\n      });\n      return {\n        type: \"unparsed\",\n        value: generate(ast),\n      };\n    }\n    return styleValue;\n  }\n  if (styleValue.type === \"layers\" || styleValue.type === \"tuple\") {\n    const newItems = styleValue.value.map((item) => {\n      return substituteVars(item, mapper) as UnparsedValue;\n    });\n    return { ...styleValue, value: newItems };\n  }\n  return styleValue;\n};\n\nexport type ComputedStyleDecl = {\n  property: CssProperty;\n  source: StyleValueSource;\n  cascadedValue: StyleValue;\n  computedValue: StyleValue;\n  usedValue: StyleValue;\n  // @todo We will delete it once we have added additional filters to advanced panel and\n  // don't need to differentiate this any more.\n  listed?: boolean;\n};\n\n/**\n * follow value processing specification\n * https://drafts.csswg.org/css-cascade-5/#value-stages\n */\nexport const getComputedStyleDecl = ({\n  model,\n  instanceSelector = [],\n  styleSourceId,\n  state,\n  property,\n  customPropertiesGraph = new Map(),\n}: {\n  model: StyleObjectModel;\n  instanceSelector?: InstanceSelector;\n  styleSourceId?: StyleDecl[\"styleSourceId\"];\n  state?: StyleDecl[\"state\"];\n  property: CssProperty;\n  /**\n   * for internal use only\n   */\n  customPropertiesGraph?: Map<Instance[\"id\"], Set<CssProperty>>;\n}): ComputedStyleDecl => {\n  const isCustomProperty = property.startsWith(\"--\");\n  const propertyData = isCustomProperty\n    ? customPropertyData\n    : (propertiesData[property] ?? invalidPropertyData);\n  const inherited = propertyData.inherited;\n  const initialValue: StyleValue = propertyData.initial;\n  let computedValue: StyleValue = initialValue;\n  let cascadedValue: undefined | StyleValue;\n  let source: StyleValueSource = { name: \"default\" };\n\n  // Check if we're computing for a pseudo-element\n  const computingForPseudoElement = state ? isPseudoElement(state) : false;\n\n  // If computing for a pseudo-element, treat it as a virtual child instance\n  // First compute the parent's value (without the pseudo-element state)\n  // Then compute the pseudo-element's value with inheritance from parent\n  if (computingForPseudoElement && instanceSelector.length > 0) {\n    // Step 1: Compute parent's value without the pseudo-element state\n    // Use a separate graph for parent lookup to avoid false cycle detection\n    // (parent using var(--x) doesn't mean pseudo-element can't also use var(--x))\n    const parentDecl = getComputedStyleDecl({\n      model,\n      instanceSelector,\n      styleSourceId,\n      state: undefined, // No state for parent\n      property,\n      customPropertiesGraph: new Map(),\n    });\n\n    // Step 2: Use parent's computed value as the inherited value for the pseudo-element\n    const inheritedValue: StyleValue = parentDecl.computedValue;\n    const inheritedSource: StyleValueSource =\n      parentDecl.source.name === \"local\"\n        ? { ...parentDecl.source, name: \"remote\" }\n        : parentDecl.source;\n\n    // Step 3: Get cascaded value for the pseudo-element itself\n    const targetInstanceId = instanceSelector[0];\n    const cascaded = getCascadedValue({\n      model,\n      instanceId: targetInstanceId,\n      styleSourceId,\n      state,\n      property,\n      forPseudoElement: true, // Flag to only collect pseudo-element styles\n    });\n\n    cascadedValue = cascaded?.value;\n    source = cascaded?.source ?? { name: \"default\" };\n\n    // Step 4: Resolve specified value with inheritance from parent\n    let specifiedValue: StyleValue = initialValue;\n\n    // explicit defaulting\n    // https://drafts.csswg.org/css-cascade-5/#defaulting-keywords\n    if (matchKeyword(cascadedValue, \"initial\")) {\n      specifiedValue = initialValue;\n    } else if (\n      matchKeyword(cascadedValue, \"inherit\") ||\n      // treat currentcolor as inherit when used on color property\n      // https://www.w3.org/TR/css-color-3/#currentColor-def\n      (property === \"color\" && matchKeyword(cascadedValue, \"currentcolor\"))\n    ) {\n      specifiedValue = inheritedValue;\n    } else if (matchKeyword(cascadedValue, \"unset\")) {\n      if (inherited) {\n        specifiedValue = inheritedValue;\n      } else {\n        specifiedValue = initialValue;\n      }\n    } else if (cascadedValue) {\n      specifiedValue = cascadedValue;\n    }\n    // defaulting https://drafts.csswg.org/css-cascade-5/#defaulting\n    else if (inherited) {\n      specifiedValue = inheritedValue;\n      cascadedValue = parentDecl.cascadedValue;\n      source = inheritedSource;\n    } else {\n      specifiedValue = initialValue;\n    }\n\n    // https://drafts.csswg.org/css-cascade-5/#computed\n    computedValue = specifiedValue;\n\n    // Handle custom properties and var() substitution\n    let usedCustomProperties = customPropertiesGraph.get(targetInstanceId);\n    if (usedCustomProperties === undefined) {\n      usedCustomProperties = new Set();\n      customPropertiesGraph.set(targetInstanceId, usedCustomProperties);\n    }\n\n    let invalid = false;\n    const parentUsedCustomProperties = usedCustomProperties;\n    usedCustomProperties = new Set<CssProperty>(usedCustomProperties);\n    customPropertiesGraph.set(targetInstanceId, usedCustomProperties);\n\n    computedValue = substituteVars(computedValue, (varValue) => {\n      const customProperty = `--${varValue.value}` as const;\n      // https://www.w3.org/TR/css-variables-1/#cycles\n      if (parentUsedCustomProperties.has(customProperty)) {\n        invalid = true;\n        return varValue;\n      }\n      usedCustomProperties.add(customProperty);\n\n      const fallback: undefined | VarFallback = varValue.fallback;\n      // Custom properties are always inherited, so look them up from parent (without state)\n      const customPropertyValue = getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state: undefined, // Don't pass pseudo-element state for custom property lookup\n        property: customProperty,\n        customPropertiesGraph,\n      });\n      let replacement = customPropertyValue.computedValue;\n      // https://www.w3.org/TR/css-variables-1/#invalid-variables\n      if (\n        replacement.type === \"guaranteedInvalid\" ||\n        (isCustomProperty === false && replacement.type === \"invalid\")\n      ) {\n        if (inherited) {\n          replacement = fallback ?? inheritedValue;\n        } else {\n          replacement = fallback ?? initialValue;\n        }\n      }\n      return replacement;\n    });\n\n    if (invalid) {\n      computedValue = invalidValue;\n    }\n\n    // https://drafts.csswg.org/css-cascade-5/#used\n    let usedValue: StyleValue = computedValue;\n    // https://drafts.csswg.org/css-color-4/#resolving-other-colors\n    if (matchKeyword(computedValue, \"currentcolor\")) {\n      const currentColor = getComputedStyleDecl({\n        model,\n        instanceSelector,\n        state, // Keep the pseudo-element state for currentcolor resolution\n        property: \"color\",\n      });\n      usedValue = currentColor.usedValue;\n    }\n\n    // fallback to initial value\n    cascadedValue ??= initialValue;\n\n    return { property, source, cascadedValue, computedValue, usedValue };\n  }\n\n  // Original behavior for non-pseudo-elements\n  // start computing from the root\n  for (let index = instanceSelector.length - 1; index >= 0; index -= 1) {\n    const instanceId = instanceSelector[index];\n    let usedCustomProperties = customPropertiesGraph.get(instanceId);\n    if (usedCustomProperties === undefined) {\n      usedCustomProperties = new Set();\n      customPropertiesGraph.set(instanceId, usedCustomProperties);\n    }\n\n    // https://drafts.csswg.org/css-cascade-5/#inheriting\n    const inheritedValue: StyleValue = computedValue;\n    const inheritedSource: StyleValueSource =\n      source.name === \"local\" ? { ...source, name: \"remote\" } : source;\n\n    // https://drafts.csswg.org/css-cascade-5/#cascaded\n    const cascaded = getCascadedValue({\n      model,\n      instanceId,\n      styleSourceId,\n      state,\n      property,\n    });\n    const inheritedCascadedValue = cascadedValue;\n    cascadedValue = cascaded?.value;\n    source = cascaded?.source ?? { name: \"default\" };\n\n    // resolve specified value\n    // https://drafts.csswg.org/css-cascade-5/#specified\n    let specifiedValue: StyleValue = initialValue;\n\n    // explicit defaulting\n    // https://drafts.csswg.org/css-cascade-5/#defaulting-keywords\n    if (matchKeyword(cascadedValue, \"initial\")) {\n      specifiedValue = initialValue;\n    } else if (\n      matchKeyword(cascadedValue, \"inherit\") ||\n      // treat currentcolor as inherit when used on color property\n      // https://www.w3.org/TR/css-color-3/#currentColor-def\n      (property === \"color\" && matchKeyword(cascadedValue, \"currentcolor\"))\n    ) {\n      specifiedValue = inheritedValue;\n    } else if (matchKeyword(cascadedValue, \"unset\")) {\n      if (inherited) {\n        specifiedValue = inheritedValue;\n      } else {\n        specifiedValue = initialValue;\n      }\n    } else if (cascadedValue) {\n      specifiedValue = cascadedValue;\n    }\n    // defaulting https://drafts.csswg.org/css-cascade-5/#defaulting\n    else if (inherited) {\n      specifiedValue = inheritedValue;\n      cascadedValue = inheritedCascadedValue;\n      source = inheritedSource;\n    } else {\n      specifiedValue = initialValue;\n    }\n\n    // https://drafts.csswg.org/css-cascade-5/#computed\n    computedValue = specifiedValue;\n\n    let invalid = false;\n    // check whether the property was used with parent node\n    // to support var(--var1), var(--var1) layers\n    const parentUsedCustomProperties = usedCustomProperties;\n    usedCustomProperties = new Set<CssProperty>(usedCustomProperties);\n    customPropertiesGraph.set(instanceId, usedCustomProperties);\n    computedValue = substituteVars(computedValue, (varValue) => {\n      const customProperty = `--${varValue.value}` as const;\n      // https://www.w3.org/TR/css-variables-1/#cycles\n      if (parentUsedCustomProperties.has(customProperty)) {\n        invalid = true;\n        return varValue;\n      }\n      usedCustomProperties.add(customProperty);\n\n      const fallback: undefined | VarFallback = varValue.fallback;\n      const customPropertyValue = getComputedStyleDecl({\n        model,\n        // resolve custom properties on instance they are defined\n        // instead of where they are accessed\n        instanceSelector: instanceSelector.slice(index),\n        property: customProperty,\n        customPropertiesGraph,\n      });\n      let replacement = customPropertyValue.computedValue;\n      // https://www.w3.org/TR/css-variables-1/#invalid-variables\n      if (\n        replacement.type === \"guaranteedInvalid\" ||\n        (isCustomProperty === false && replacement.type === \"invalid\")\n      ) {\n        if (inherited) {\n          replacement = fallback ?? inheritedValue;\n        } else {\n          replacement = fallback ?? initialValue;\n        }\n      }\n      return replacement;\n    });\n    if (invalid) {\n      computedValue = invalidValue;\n      break;\n    }\n  }\n\n  // https://drafts.csswg.org/css-cascade-5/#used\n  let usedValue: StyleValue = computedValue;\n  // https://drafts.csswg.org/css-color-4/#resolving-other-colors\n  if (matchKeyword(computedValue, \"currentcolor\")) {\n    const currentColor = getComputedStyleDecl({\n      model,\n      instanceSelector,\n      property: \"color\",\n    });\n    usedValue = currentColor.usedValue;\n  }\n\n  // fallback to initial value\n  cascadedValue ??= initialValue;\n\n  return { property, source, cascadedValue, computedValue, usedValue };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/style-source-utils.test.tsx",
    "content": "import { enableMapSet } from \"immer\";\nimport { describe, test, expect } from \"vitest\";\nimport type {\n  Breakpoint,\n  StyleDecl,\n  StyleSource,\n  StyleSources,\n  StyleSourceSelection,\n  StyleSourceSelections,\n  Styles,\n} from \"@webstudio-is/sdk\";\nimport type { StyleProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  getStyleSourceStylesSignature,\n  insertStyleSources,\n  insertPortalLocalStyleSources,\n  insertLocalStyleSourcesWithNewIds,\n  deleteStyleSourceMutable,\n  findUnusedTokens,\n  deleteStyleSourcesMutable,\n  validateAndRenameStyleSource,\n  renameStyleSourceMutable,\n  deleteLocalStyleSourcesMutable,\n  collectStyleSourcesFromInstances,\n  findDuplicateTokens,\n  findTokenWithMatchingStyles,\n  detectTokenConflicts,\n} from \"./style-source-utils\";\n\nenableMapSet();\n\nconst getIdValuePair = <T extends { id: string }>(item: T) =>\n  [item.id, item] as const;\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map(getIdValuePair));\n\nconst createStyleDecl = (\n  styleSourceId: string,\n  breakpointId: string,\n  property: StyleProperty,\n  value: StyleValue | string\n): StyleDecl => ({\n  styleSourceId,\n  breakpointId,\n  property,\n  value: typeof value === \"string\" ? { type: \"unparsed\", value } : value,\n});\n\ndescribe(\"getStyleSourceStylesSignature\", () => {\n  test(\"generates consistent signature for token styles\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styles: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDecl(\"token1\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ];\n\n    const signature = getStyleSourceStylesSignature(\n      \"token1\",\n      styles,\n      breakpoints,\n      new Map()\n    );\n\n    expect(signature).toBeTruthy();\n    expect(typeof signature).toBe(\"string\");\n  });\n\n  test(\"generates same signature for same styles in different order\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styles1: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDecl(\"token1\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ];\n    const styles2: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n      createStyleDecl(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const signature1 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles1,\n      breakpoints,\n      new Map()\n    );\n    const signature2 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles2,\n      breakpoints,\n      new Map()\n    );\n\n    expect(signature1).toBe(signature2);\n  });\n\n  test(\"generates different signature for different styles\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styles1: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n    const styles2: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n    ];\n\n    const signature1 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles1,\n      breakpoints,\n      new Map()\n    );\n    const signature2 = getStyleSourceStylesSignature(\n      \"token2\",\n      styles2,\n      breakpoints,\n      new Map()\n    );\n\n    expect(signature1).not.toBe(signature2);\n  });\n\n  test(\"handles different breakpoints correctly\", () => {\n    const breakpoints = toMap<Breakpoint>([\n      { id: \"base\", label: \"base\" },\n      { id: \"tablet\", label: \"tablet\", minWidth: 768 },\n    ]);\n    const styles1: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n    const styles2: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"tablet\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const signature1 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles1,\n      breakpoints,\n      new Map()\n    );\n    const signature2 = getStyleSourceStylesSignature(\n      \"token2\",\n      styles2,\n      breakpoints,\n      new Map()\n    );\n\n    expect(signature1).not.toBe(signature2);\n  });\n\n  test(\"handles pseudo states correctly\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styles1: StyleDecl[] = [\n      {\n        styleSourceId: \"token1\",\n        breakpointId: \"base\",\n        property: \"color\" as StyleProperty,\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n    const styles2: StyleDecl[] = [\n      {\n        styleSourceId: \"token2\",\n        breakpointId: \"base\",\n        property: \"color\" as StyleProperty,\n        state: \":hover\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const signature1 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles1,\n      breakpoints,\n      new Map()\n    );\n    const signature2 = getStyleSourceStylesSignature(\n      \"token2\",\n      styles2,\n      breakpoints,\n      new Map()\n    );\n\n    expect(signature1).not.toBe(signature2);\n  });\n\n  test(\"handles merged breakpoints correctly\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"existing\", label: \"base\" }]);\n    const mergedBreakpointIds = new Map([[\"fragment\", \"existing\"]]);\n    const styles1: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"existing\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n    const styles2: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"fragment\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const signature1 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles1,\n      breakpoints,\n      new Map()\n    );\n    const signature2 = getStyleSourceStylesSignature(\n      \"token2\",\n      styles2,\n      breakpoints,\n      mergedBreakpointIds\n    );\n\n    // Should be the same because fragment breakpoint maps to existing\n    expect(signature1).toBe(signature2);\n  });\n\n  test(\"handles complex multi-property multi-breakpoint styles\", () => {\n    const breakpoints = toMap<Breakpoint>([\n      { id: \"base\", label: \"base\" },\n      { id: \"tablet\", label: \"tablet\", minWidth: 768 },\n      { id: \"desktop\", label: \"desktop\", minWidth: 1200 },\n    ]);\n    const styles1: StyleDecl[] = [\n      createStyleDecl(\"token1\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDecl(\"token1\", \"tablet\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n      createStyleDecl(\"token1\", \"desktop\", \"fontSize\", {\n        type: \"unit\",\n        value: 20,\n        unit: \"px\",\n      }),\n    ];\n    // Same styles but in different order\n    const styles2: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"desktop\", \"fontSize\", {\n        type: \"unit\",\n        value: 20,\n        unit: \"px\",\n      }),\n      createStyleDecl(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDecl(\"token2\", \"tablet\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n    ];\n\n    const signature1 = getStyleSourceStylesSignature(\n      \"token1\",\n      styles1,\n      breakpoints,\n      new Map()\n    );\n    const signature2 = getStyleSourceStylesSignature(\n      \"token2\",\n      styles2,\n      breakpoints,\n      new Map()\n    );\n\n    expect(signature1).toBe(signature2);\n  });\n});\n\ndescribe(\"insertStyleSources\", () => {\n  // Case 2: Same styles AND same name -> reuse existing token\n  test(\"token with same styles and same name reuses existing token\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"existingToken:base:color:\",\n        createStyleDecl(\"existingToken\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"existingToken:base:fontSize:\",\n        createStyleDecl(\"existingToken\", \"base\", \"fontSize\", {\n          type: \"unit\",\n          value: 16,\n          unit: \"px\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"newToken\", type: \"token\", name: \"primaryColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"newToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDecl(\"newToken\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ];\n\n    const { styleSourceIdMap, updatedStyleSources } = insertStyleSources({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    // Should reuse existing token, not create a new one\n    expect(Array.from(updatedStyleSources.values())).toEqual([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    // Should map newToken -> existingToken\n    expect(styleSourceIdMap.get(\"newToken\")).toBe(\"existingToken\");\n  });\n\n  // Case 3: Same styles but different name -> insert new token with original name\n  test(\"token with same styles but different name inserts new token\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"token1:base:color:\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"token2\", type: \"token\", name: \"accentColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const { styleSourceIds, styleSourceIdMap, updatedStyleSources } =\n      insertStyleSources({\n        fragmentStyleSources,\n        fragmentStyles,\n        existingStyleSources,\n        existingStyles,\n        breakpoints,\n        mergedBreakpointIds: new Map(),\n      });\n\n    // Should insert new token with its original name\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"accentColor\" });\n    expect(tokens[1].id).not.toBe(\"token2\"); // Should have new ID\n    expect(styleSourceIds.has(\"token2\")).toBe(true);\n    expect(styleSourceIdMap.get(\"token2\")).toBe(tokens[1].id);\n  });\n\n  // Case 4: Different styles but same name -> add counter suffix\n  test(\"token with different styles but same name adds counter suffix\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"myToken\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"token1:base:color:\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"token2\", type: \"token\", name: \"myToken\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const { styleSourceIds, styleSourceIdMap, updatedStyleSources } =\n      insertStyleSources({\n        fragmentStyleSources,\n        fragmentStyles,\n        existingStyleSources,\n        existingStyles,\n        breakpoints,\n        mergedBreakpointIds: new Map(),\n      });\n\n    // Should add counter suffix to the new token\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({ id: \"token1\", type: \"token\", name: \"myToken\" });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"myToken-1\" });\n    expect(tokens[1].id).not.toBe(\"token2\"); // Should have new ID\n    expect(styleSourceIds.has(\"token2\")).toBe(true);\n    expect(styleSourceIdMap.get(\"token2\")).toBe(tokens[1].id);\n  });\n\n  // Case 4b: Multiple counter suffixes\n  test(\"token with name conflict increments counter correctly\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"myToken\" },\n      { id: \"token2\", type: \"token\", name: \"myToken-1\" },\n      { id: \"token3\", type: \"token\", name: \"myToken-2\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"token1:base:color:\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"token2:base:color:\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"green\",\n        }),\n      ],\n      [\n        \"token3:base:color:\",\n        createStyleDecl(\"token3\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"yellow\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"token4\", type: \"token\", name: \"myToken\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"token4\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const { updatedStyleSources } = insertStyleSources({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    // Should use counter 3\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(4);\n    expect(tokens[0]).toEqual({ id: \"token1\", type: \"token\", name: \"myToken\" });\n    expect(tokens[1]).toEqual({\n      id: \"token2\",\n      type: \"token\",\n      name: \"myToken-1\",\n    });\n    expect(tokens[2]).toEqual({\n      id: \"token3\",\n      type: \"token\",\n      name: \"myToken-2\",\n    });\n    expect(tokens[3]).toMatchObject({ type: \"token\", name: \"myToken-3\" });\n    expect(tokens[3].id).not.toBe(\"token4\"); // Should have new ID\n  });\n\n  // Case 6: Different styles and different name -> insert as-is\n  test(\"token with different styles and different name inserts normally\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"token1:base:color:\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"token2\", type: \"token\", name: \"secondaryColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"token2\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const { styleSourceIds, styleSourceIdMap, updatedStyleSources } =\n      insertStyleSources({\n        fragmentStyleSources,\n        fragmentStyles,\n        existingStyleSources,\n        existingStyles,\n        breakpoints,\n        mergedBreakpointIds: new Map(),\n      });\n\n    // Should insert new token normally\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"secondaryColor\" });\n    expect(tokens[1].id).not.toBe(\"token2\"); // Should have new ID\n    expect(styleSourceIds.has(\"token2\")).toBe(true);\n    expect(styleSourceIdMap.get(\"token2\")).toBe(tokens[1].id);\n  });\n\n  // Case 3 safeguard: Same styles but different name gets suffix when name conflicts\n  test(\"token with same styles but different name adds suffix when name already exists\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n      { id: \"token2\", type: \"token\", name: \"accentColor\" }, // This name is taken\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"token1:base:color:\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:fontSize:\",\n        createStyleDecl(\"token2\", \"base\", \"fontSize\", {\n          type: \"unit\",\n          value: 16,\n          unit: \"px\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"token3\", type: \"token\", name: \"accentColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"token3\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const { updatedStyleSources } = insertStyleSources({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    // Should add counter suffix to prevent duplicate name\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(3);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toEqual({\n      id: \"token2\",\n      type: \"token\",\n      name: \"accentColor\",\n    });\n    expect(tokens[2]).toMatchObject({ type: \"token\", name: \"accentColor-1\" });\n    expect(tokens[2].id).not.toBe(\"token3\"); // Should have new ID\n  });\n\n  // Case 6 safeguard: Different styles and different name gets suffix when name conflicts\n  test(\"token with different styles and name adds suffix when name already exists\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"token1\", type: \"token\", name: \"primaryColor\" },\n      { id: \"token2\", type: \"token\", name: \"secondaryColor\" }, // This name is taken\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"token1:base:color:\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"token2:base:color:\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"green\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"token3\", type: \"token\", name: \"secondaryColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"token3\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n    ];\n\n    const { updatedStyleSources } = insertStyleSources({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    // Should add counter suffix to prevent duplicate name\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(3);\n    expect(tokens[0]).toEqual({\n      id: \"token1\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toEqual({\n      id: \"token2\",\n      type: \"token\",\n      name: \"secondaryColor\",\n    });\n    expect(tokens[2]).toMatchObject({\n      type: \"token\",\n      name: \"secondaryColor-1\",\n    });\n    expect(tokens[2].id).not.toBe(\"token3\"); // Should have new ID\n  });\n\n  // Test that existing token with same styles but different name stays untouched\n  test(\"existing token with matching styles but different name stays untouched when inserting new token\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"existingToken:base:color:\",\n        createStyleDecl(\"existingToken\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"existingToken:base:fontSize:\",\n        createStyleDecl(\"existingToken\", \"base\", \"fontSize\", {\n          type: \"unit\",\n          value: 16,\n          unit: \"px\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"newToken\", type: \"token\", name: \"accentColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"newToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\",\n      }),\n      createStyleDecl(\"newToken\", \"base\", \"fontSize\", {\n        type: \"unit\",\n        value: 16,\n        unit: \"px\",\n      }),\n    ];\n\n    const { updatedStyleSources } = insertStyleSources({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    // Should insert new token with its own name, leaving existing one untouched\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"existingToken\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"accentColor\" });\n    expect(tokens[1].id).not.toBe(\"newToken\"); // Should have new ID\n  });\n\n  // Critical test: inserting base name when suffixed version exists\n  test(\"inserting token 'bbb' when 'bbb-1' with same styles exists inserts both tokens\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"bbb-1\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"existingToken:base:color:\",\n        createStyleDecl(\"existingToken\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"newToken\", type: \"token\", name: \"bbb\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"newToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"blue\",\n      }),\n    ];\n\n    const { styleSourceIdMap, updatedStyleSources } = insertStyleSources({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    // Both tokens should exist\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(2);\n    expect(tokens[0]).toEqual({\n      id: \"existingToken\",\n      type: \"token\",\n      name: \"bbb-1\",\n    });\n    expect(tokens[1]).toMatchObject({ type: \"token\", name: \"bbb\" }); // Different name, so inserted as-is\n    expect(tokens[1].id).not.toBe(\"newToken\"); // Should have new ID\n    expect(styleSourceIdMap.get(\"newToken\")).toBe(tokens[1].id);\n  });\n\n  // Test merge conflict resolution\n  test('token with different styles and same name merges when conflictResolution=\"merge\"', () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const existingStyleSources = toMap<StyleSource>([\n      { id: \"existingToken\", type: \"token\", name: \"primaryColor\" },\n    ]);\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"existingToken:base:color:\",\n        createStyleDecl(\"existingToken\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"existingToken:base:fontSize:\",\n        createStyleDecl(\"existingToken\", \"base\", \"fontSize\", {\n          type: \"unit\",\n          value: 16,\n          unit: \"px\",\n        }),\n      ],\n    ]);\n\n    const fragmentStyleSources: StyleSource[] = [\n      { id: \"newToken\", type: \"token\", name: \"primaryColor\" },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      createStyleDecl(\"newToken\", \"base\", \"color\", {\n        type: \"keyword\",\n        value: \"red\", // Different color - should override\n      }),\n      createStyleDecl(\"newToken\", \"base\", \"fontWeight\", {\n        type: \"keyword\",\n        value: \"bold\", // Additional property - should be added\n      }),\n    ];\n\n    const { styleSourceIds, styleSourceIdMap, updatedStyleSources } =\n      insertStyleSources({\n        fragmentStyleSources,\n        fragmentStyles,\n        existingStyleSources,\n        existingStyles,\n        breakpoints,\n        mergedBreakpointIds: new Map(),\n        conflictResolution: \"merge\",\n      });\n\n    // Should keep existing token (no new token created)\n    const tokens = Array.from(updatedStyleSources.values());\n    expect(tokens).toHaveLength(1);\n    expect(tokens[0]).toEqual({\n      id: \"existingToken\",\n      type: \"token\",\n      name: \"primaryColor\",\n    });\n\n    // Should map the fragment token to the existing token\n    expect(styleSourceIdMap.get(\"newToken\")).toBe(\"existingToken\");\n\n    // Should mark the fragment token for style insertion\n    // This tells insertWebstudioFragment to insert these styles, which will:\n    // - Override existing \"color\" from blue to red\n    // - Keep existing \"fontSize\" at 16px\n    // - Add new \"fontWeight\" bold\n    expect(styleSourceIds.has(\"newToken\")).toBe(true);\n  });\n});\n\n// Tests for insertPortalLocalStyleSources\n\ndescribe(\"insertPortalLocalStyleSources\", () => {\n  test(\"inserts local style sources and styles for portal content\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"local1\" },\n      { type: \"token\", id: \"token1\", name: \"myToken\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"instance1\", values: [\"local1\", \"token1\"] },\n      { instanceId: \"instance2\", values: [\"local1\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"local1\",\n        property: \"width\",\n        value: { type: \"unit\", value: 100, unit: \"px\" },\n      },\n      {\n        breakpointId: \"bp2\",\n        styleSourceId: \"local1\",\n        property: \"height\",\n        value: { type: \"unit\", value: 50, unit: \"px\" },\n      },\n    ];\n    const instanceIds = new Set([\"instance1\"]);\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map([[\"bp2\", \"bp2-merged\"]]);\n\n    insertPortalLocalStyleSources({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      instanceIds,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // Should only insert for instance1 (in instanceIds set)\n    expect(styleSourceSelections.has(\"instance1\")).toBe(true);\n    expect(styleSourceSelections.has(\"instance2\")).toBe(false);\n\n    // Should insert local style source\n    expect(styleSources.get(\"local1\")).toEqual({ type: \"local\", id: \"local1\" });\n\n    // Should insert styles with merged breakpoint IDs\n    expect(styles.size).toBe(2);\n    const stylesArray = Array.from(styles.values());\n    expect(stylesArray).toContainEqual({\n      breakpointId: \"bp1\",\n      styleSourceId: \"local1\",\n      property: \"width\",\n      value: { type: \"unit\", value: 100, unit: \"px\" },\n    });\n    expect(stylesArray).toContainEqual({\n      breakpointId: \"bp2-merged\",\n      styleSourceId: \"local1\",\n      property: \"height\",\n      value: { type: \"unit\", value: 50, unit: \"px\" },\n    });\n  });\n\n  test(\"preserves original IDs for portal content\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"portal-local-123\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"portal-instance\", values: [\"portal-local-123\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"portal-local-123\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n    const instanceIds = new Set([\"portal-instance\"]);\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map();\n\n    insertPortalLocalStyleSources({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      instanceIds,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // IDs should be preserved exactly\n    expect(styleSources.get(\"portal-local-123\")).toEqual({\n      type: \"local\",\n      id: \"portal-local-123\",\n    });\n    expect(styleSourceSelections.get(\"portal-instance\")).toEqual({\n      instanceId: \"portal-instance\",\n      values: [\"portal-local-123\"],\n    });\n  });\n\n  test(\"skips instances not in instanceIds set\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"local1\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"included\", values: [\"local1\"] },\n      { instanceId: \"excluded\", values: [\"local1\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"local1\",\n        property: \"width\",\n        value: { type: \"unit\", value: 100, unit: \"px\" },\n      },\n    ];\n    const instanceIds = new Set([\"included\"]);\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map();\n\n    insertPortalLocalStyleSources({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      instanceIds,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    expect(styleSourceSelections.has(\"included\")).toBe(true);\n    expect(styleSourceSelections.has(\"excluded\")).toBe(false);\n  });\n});\n\n// Tests for insertLocalStyleSourcesWithNewIds\n\ndescribe(\"insertLocalStyleSourcesWithNewIds\", () => {\n  test(\"generates new IDs for local style sources\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"old-local-1\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"old-instance\", values: [\"old-local-1\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"old-local-1\",\n        property: \"marginTop\" as StyleProperty,\n        value: { type: \"unit\", value: 10, unit: \"px\" },\n      },\n    ];\n    const fragmentInstanceIds = new Set([\"old-instance\"]);\n    const newInstanceIds = new Map([[\"old-instance\", \"new-instance\"]]);\n    const styleSourceIdMap = new Map();\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map();\n\n    insertLocalStyleSourcesWithNewIds({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      fragmentInstanceIds,\n      newInstanceIds,\n      styleSourceIdMap,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // Should create new local style source with different ID\n    expect(styleSources.size).toBe(1);\n    const newLocalStyleSource = Array.from(styleSources.values())[0];\n    expect(newLocalStyleSource.type).toBe(\"local\");\n    expect(newLocalStyleSource.id).not.toBe(\"old-local-1\");\n\n    // Should map to new instance ID\n    expect(styleSourceSelections.has(\"new-instance\")).toBe(true);\n    expect(styleSourceSelections.has(\"old-instance\")).toBe(false);\n\n    // Should create style with new style source ID\n    expect(styles.size).toBe(1);\n    const newStyle = Array.from(styles.values())[0];\n    expect(newStyle.styleSourceId).toBe(newLocalStyleSource.id);\n  });\n\n  test(\"merges local styles into existing ROOT_INSTANCE_ID local source\", async () => {\n    const { ROOT_INSTANCE_ID } = await import(\"@webstudio-is/sdk\");\n\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"fragment-local\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: ROOT_INSTANCE_ID, values: [\"fragment-local\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"fragment-local\",\n        property: \"fontFamily\",\n        value: { type: \"fontFamily\", value: [\"Arial\"] },\n      },\n    ];\n    const fragmentInstanceIds = new Set([ROOT_INSTANCE_ID]);\n    const newInstanceIds = new Map();\n    const styleSourceIdMap = new Map();\n\n    // Existing ROOT local style source\n    const existingRootLocal: StyleSource = {\n      type: \"local\",\n      id: \"existing-root-local\",\n    };\n    const styleSources: StyleSources = new Map([\n      [\"existing-root-local\", existingRootLocal],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\n        ROOT_INSTANCE_ID,\n        { instanceId: ROOT_INSTANCE_ID, values: [\"existing-root-local\"] },\n      ],\n    ]);\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map();\n\n    insertLocalStyleSourcesWithNewIds({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      fragmentInstanceIds,\n      newInstanceIds,\n      styleSourceIdMap,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // Should reuse existing ROOT local style source, not create new one\n    expect(styleSources.size).toBe(1);\n    expect(styleSources.get(\"existing-root-local\")).toBe(existingRootLocal);\n\n    // Style should use existing local style source ID\n    expect(styles.size).toBe(1);\n    const style = Array.from(styles.values())[0];\n    expect(style.styleSourceId).toBe(\"existing-root-local\");\n  });\n\n  test(\"creates new local source for non-ROOT instances even if local exists\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"fragment-local\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"regular-instance\", values: [\"fragment-local\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"fragment-local\",\n        property: \"paddingTop\" as StyleProperty,\n        value: { type: \"unit\", value: 20, unit: \"px\" },\n      },\n    ];\n    const fragmentInstanceIds = new Set([\"regular-instance\"]);\n    const newInstanceIds = new Map();\n    const styleSourceIdMap = new Map();\n\n    // Existing local style source for this instance\n    const existingLocal: StyleSource = { type: \"local\", id: \"existing-local\" };\n    const styleSources: StyleSources = new Map([\n      [\"existing-local\", existingLocal],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\n        \"regular-instance\",\n        { instanceId: \"regular-instance\", values: [\"existing-local\"] },\n      ],\n    ]);\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map();\n\n    insertLocalStyleSourcesWithNewIds({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      fragmentInstanceIds,\n      newInstanceIds,\n      styleSourceIdMap,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // Should create NEW local style source (not merge)\n    expect(styleSources.size).toBe(2);\n    const newLocalId = Array.from(styleSources.keys()).find(\n      (id) => id !== \"existing-local\"\n    );\n    expect(newLocalId).toBeDefined();\n\n    // Style should use new local style source ID\n    const style = Array.from(styles.values())[0];\n    expect(style.styleSourceId).toBe(newLocalId);\n  });\n\n  test(\"handles token remapping in styleSourceIdMap\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"local1\" },\n      { type: \"token\", id: \"old-token\", name: \"oldToken\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"instance1\", values: [\"local1\", \"old-token\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"bp1\",\n        styleSourceId: \"local1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"blue\" },\n      },\n    ];\n    const fragmentInstanceIds = new Set([\"instance1\"]);\n    const newInstanceIds = new Map();\n\n    // Token was already mapped to existing token\n    const styleSourceIdMap = new Map([[\"old-token\", \"existing-token\"]]);\n\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map();\n\n    insertLocalStyleSourcesWithNewIds({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      fragmentInstanceIds,\n      newInstanceIds,\n      styleSourceIdMap,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // styleSourceSelection should reference the mapped token ID\n    const selection = styleSourceSelections.get(\"instance1\");\n    expect(selection?.values).toContain(\"existing-token\");\n    expect(selection?.values).not.toContain(\"old-token\");\n  });\n\n  test(\"applies merged breakpoint IDs to styles\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"local1\" },\n    ];\n    const fragmentStyleSourceSelections: StyleSourceSelection[] = [\n      { instanceId: \"instance1\", values: [\"local1\"] },\n    ];\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"old-bp\",\n        styleSourceId: \"local1\",\n        property: \"display\",\n        value: { type: \"keyword\", value: \"flex\" },\n      },\n    ];\n    const fragmentInstanceIds = new Set([\"instance1\"]);\n    const newInstanceIds = new Map();\n    const styleSourceIdMap = new Map();\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n    const mergedBreakpointIds = new Map([[\"old-bp\", \"merged-bp\"]]);\n\n    insertLocalStyleSourcesWithNewIds({\n      fragmentStyleSources,\n      fragmentStyleSourceSelections,\n      fragmentStyles,\n      fragmentInstanceIds,\n      newInstanceIds,\n      styleSourceIdMap,\n      styleSources,\n      styleSourceSelections,\n      styles,\n      mergedBreakpointIds,\n    });\n\n    // Style should use merged breakpoint ID\n    const style = Array.from(styles.values())[0];\n    expect(style.breakpointId).toBe(\"merged-bp\");\n  });\n});\n\ndescribe(\"deleteStyleSourceMutable\", () => {\n  test(\"deletes style source from styleSources map\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Color\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Secondary Color\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n\n    deleteStyleSourceMutable({\n      styleSourceId: \"token1\",\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    expect(styleSources.has(\"token1\")).toBe(false);\n    expect(styleSources.has(\"token2\")).toBe(true);\n  });\n\n  test(\"removes style source from style source selections\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Color\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\"instance1\", { instanceId: \"instance1\", values: [\"token1\", \"local1\"] }],\n      [\"instance2\", { instanceId: \"instance2\", values: [\"token1\"] }],\n      [\"instance3\", { instanceId: \"instance3\", values: [\"local2\"] }],\n    ]);\n    const styles: Styles = new Map();\n\n    deleteStyleSourceMutable({\n      styleSourceId: \"token1\",\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    const selection1 = styleSourceSelections.get(\"instance1\");\n    expect(selection1?.values).toEqual([\"local1\"]);\n\n    const selection2 = styleSourceSelections.get(\"instance2\");\n    expect(selection2?.values).toEqual([]);\n\n    const selection3 = styleSourceSelections.get(\"instance3\");\n    expect(selection3?.values).toEqual([\"local2\"]);\n  });\n\n  test(\"deletes all styles associated with the style source\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Color\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"token1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"token1:base:fontSize\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"token1\",\n          property: \"fontSize\",\n          value: { type: \"unit\", value: 16, unit: \"px\" },\n        },\n      ],\n      [\n        \"local1:base:display\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local1\",\n          property: \"display\",\n          value: { type: \"keyword\", value: \"flex\" },\n        },\n      ],\n    ]);\n\n    deleteStyleSourceMutable({\n      styleSourceId: \"token1\",\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    expect(styles.has(\"token1:base:color\")).toBe(false);\n    expect(styles.has(\"token1:base:fontSize\")).toBe(false);\n    expect(styles.has(\"local1:base:display\")).toBe(true);\n    expect(styles.size).toBe(1);\n  });\n\n  test(\"handles deletion of non-existent style source gracefully\", () => {\n    const styleSources: StyleSources = new Map();\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n\n    expect(() => {\n      deleteStyleSourceMutable({\n        styleSourceId: \"non-existent\",\n        styleSources,\n        styleSourceSelections,\n        styles,\n      });\n    }).not.toThrow();\n  });\n});\n\ndescribe(\"findUnusedTokens\", () => {\n  test(\"identifies tokens with no usages\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Used Token\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Unused Token\" } as StyleSource,\n      ],\n      [\n        \"token3\",\n        { type: \"token\", id: \"token3\", name: \"Another Unused\" } as StyleSource,\n      ],\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n    ]);\n    const styleSourceUsages = new Map<string, Set<string>>([\n      [\"token1\", new Set([\"instance1\", \"instance2\"])],\n      [\"token2\", new Set()],\n      [\"local1\", new Set([\"instance3\"])],\n    ]);\n\n    const unusedTokens = findUnusedTokens({ styleSources, styleSourceUsages });\n\n    expect(unusedTokens).toContain(\"token2\");\n    expect(unusedTokens).toContain(\"token3\");\n    expect(unusedTokens).not.toContain(\"token1\");\n    expect(unusedTokens).not.toContain(\"local1\");\n    expect(unusedTokens.length).toBe(2);\n  });\n\n  test(\"returns empty array when all tokens are used\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Used Token 1\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Used Token 2\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceUsages = new Map([\n      [\"token1\", new Set([\"instance1\"])],\n      [\"token2\", new Set([\"instance2\"])],\n    ]);\n\n    const unusedTokens = findUnusedTokens({ styleSources, styleSourceUsages });\n\n    expect(unusedTokens).toEqual([]);\n  });\n\n  test(\"ignores local style sources\", () => {\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n      [\"local2\", { type: \"local\", id: \"local2\" } as StyleSource],\n    ]);\n    const styleSourceUsages = new Map();\n\n    const unusedTokens = findUnusedTokens({ styleSources, styleSourceUsages });\n\n    expect(unusedTokens).toEqual([]);\n  });\n\n  test(\"treats undefined usages as unused\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Unused Token\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceUsages = new Map();\n\n    const unusedTokens = findUnusedTokens({ styleSources, styleSourceUsages });\n\n    expect(unusedTokens).toEqual([\"token1\"]);\n  });\n});\n\ndescribe(\"deleteStyleSourcesMutable\", () => {\n  test(\"deletes multiple style sources\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Token 1\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Token 2\" } as StyleSource,\n      ],\n      [\n        \"token3\",\n        { type: \"token\", id: \"token3\", name: \"Token 3\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n\n    deleteStyleSourcesMutable({\n      styleSourceIds: [\"token1\", \"token3\"],\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    expect(styleSources.has(\"token1\")).toBe(false);\n    expect(styleSources.has(\"token2\")).toBe(true);\n    expect(styleSources.has(\"token3\")).toBe(false);\n  });\n\n  test(\"removes deleted style sources from all selections\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Token 1\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Token 2\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\n        \"instance1\",\n        { instanceId: \"instance1\", values: [\"token1\", \"token2\", \"local1\"] },\n      ],\n      [\"instance2\", { instanceId: \"instance2\", values: [\"token1\"] }],\n    ]);\n    const styles: Styles = new Map();\n\n    deleteStyleSourcesMutable({\n      styleSourceIds: [\"token1\", \"token2\"],\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    const selection1 = styleSourceSelections.get(\"instance1\");\n    expect(selection1?.values).toEqual([\"local1\"]);\n\n    const selection2 = styleSourceSelections.get(\"instance2\");\n    expect(selection2?.values).toEqual([]);\n  });\n\n  test(\"deletes all associated styles\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Token 1\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Token 2\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"token1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"token2:base:fontSize\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"token2\",\n          property: \"fontSize\",\n          value: { type: \"unit\", value: 16, unit: \"px\" },\n        },\n      ],\n      [\n        \"local1:base:display\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local1\",\n          property: \"display\",\n          value: { type: \"keyword\", value: \"flex\" },\n        },\n      ],\n    ]);\n\n    deleteStyleSourcesMutable({\n      styleSourceIds: [\"token1\", \"token2\"],\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    expect(styles.has(\"token1:base:color\")).toBe(false);\n    expect(styles.has(\"token2:base:fontSize\")).toBe(false);\n    expect(styles.has(\"local1:base:display\")).toBe(true);\n    expect(styles.size).toBe(1);\n  });\n\n  test(\"handles empty array of style source IDs\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Token 1\" } as StyleSource,\n      ],\n    ]);\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styles: Styles = new Map();\n\n    deleteStyleSourcesMutable({\n      styleSourceIds: [],\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n\n    expect(styleSources.has(\"token1\")).toBe(true);\n  });\n});\n\ndescribe(\"validateAndRenameStyleSource\", () => {\n  test(\"returns undefined for valid rename\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Old Name\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Other Token\" } as StyleSource,\n      ],\n    ]);\n\n    const error = validateAndRenameStyleSource({\n      id: \"token1\",\n      name: \"New Name\",\n      styleSources,\n    });\n\n    expect(error).toBeUndefined();\n  });\n\n  test(\"returns minlength error for empty name\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Old Name\" } as StyleSource,\n      ],\n    ]);\n\n    const error = validateAndRenameStyleSource({\n      id: \"token1\",\n      name: \"\",\n      styleSources,\n    });\n\n    expect(error).toEqual({ type: \"minlength\", id: \"token1\" });\n  });\n\n  test(\"returns minlength error for whitespace-only name\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Old Name\" } as StyleSource,\n      ],\n    ]);\n\n    const error = validateAndRenameStyleSource({\n      id: \"token1\",\n      name: \"   \",\n      styleSources,\n    });\n\n    expect(error).toEqual({ type: \"minlength\", id: \"token1\" });\n  });\n\n  test(\"returns duplicate error when name already exists\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Color\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        {\n          type: \"token\",\n          id: \"token2\",\n          name: \"Secondary Color\",\n        } as StyleSource,\n      ],\n    ]);\n\n    const error = validateAndRenameStyleSource({\n      id: \"token1\",\n      name: \"Secondary Color\",\n      styleSources,\n    });\n\n    expect(error).toEqual({ type: \"duplicate\", id: \"token1\" });\n  });\n\n  test(\"allows renaming to same name (no change)\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Color\" } as StyleSource,\n      ],\n    ]);\n\n    const error = validateAndRenameStyleSource({\n      id: \"token1\",\n      name: \"Primary Color\",\n      styleSources,\n    });\n\n    expect(error).toBeUndefined();\n  });\n\n  test(\"ignores local style sources when checking duplicates\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Color\" } as StyleSource,\n      ],\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n    ]);\n\n    const error = validateAndRenameStyleSource({\n      id: \"token1\",\n      name: \"Some Name\",\n      styleSources,\n    });\n\n    expect(error).toBeUndefined();\n  });\n});\n\ndescribe(\"renameStyleSourceMutable\", () => {\n  test(\"renames a token style source\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Old Name\" } as StyleSource,\n      ],\n    ]);\n\n    renameStyleSourceMutable({\n      id: \"token1\",\n      name: \"New Name\",\n      styleSources,\n    });\n\n    const styleSource = styleSources.get(\"token1\");\n    expect(styleSource?.type).toBe(\"token\");\n    if (styleSource?.type === \"token\") {\n      expect(styleSource.name).toBe(\"New Name\");\n    }\n  });\n\n  test(\"does not rename local style source\", () => {\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n    ]);\n\n    renameStyleSourceMutable({\n      id: \"local1\",\n      name: \"New Name\",\n      styleSources,\n    });\n\n    const styleSource = styleSources.get(\"local1\");\n    expect(styleSource?.type).toBe(\"local\");\n  });\n\n  test(\"handles non-existent style source gracefully\", () => {\n    const styleSources: StyleSources = new Map();\n\n    expect(() => {\n      renameStyleSourceMutable({\n        id: \"non-existent\",\n        name: \"New Name\",\n        styleSources,\n      });\n    }).not.toThrow();\n  });\n\n  test(\"preserves other properties of the style source\", () => {\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Old Name\" } as StyleSource,\n      ],\n    ]);\n\n    renameStyleSourceMutable({\n      id: \"token1\",\n      name: \"New Name\",\n      styleSources,\n    });\n\n    const styleSource = styleSources.get(\"token1\");\n    expect(styleSource?.id).toBe(\"token1\");\n    expect(styleSource?.type).toBe(\"token\");\n  });\n});\n\ndescribe(\"deleteLocalStyleSourcesMutable\", () => {\n  test(\"deletes local style sources from styleSources map\", () => {\n    const localStyleSourceIds = new Set([\"local1\", \"local2\"]);\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n      [\"local2\", { type: \"local\", id: \"local2\" } as StyleSource],\n      [\"local3\", { type: \"local\", id: \"local3\" } as StyleSource],\n      [\"token1\", { type: \"token\", id: \"token1\", name: \"Token\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map();\n\n    deleteLocalStyleSourcesMutable({\n      localStyleSourceIds,\n      styleSources,\n      styles,\n    });\n\n    expect(styleSources.has(\"local1\")).toBe(false);\n    expect(styleSources.has(\"local2\")).toBe(false);\n    expect(styleSources.has(\"local3\")).toBe(true);\n    expect(styleSources.has(\"token1\")).toBe(true);\n  });\n\n  test(\"deletes styles associated with local style sources\", () => {\n    const localStyleSourceIds = new Set([\"local1\", \"local2\"]);\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n      [\"local2\", { type: \"local\", id: \"local2\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"local1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"local2:base:fontSize\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local2\",\n          property: \"fontSize\",\n          value: { type: \"unit\", value: 16, unit: \"px\" },\n        },\n      ],\n      [\n        \"local3:base:display\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local3\",\n          property: \"display\",\n          value: { type: \"keyword\", value: \"flex\" },\n        },\n      ],\n    ]);\n\n    deleteLocalStyleSourcesMutable({\n      localStyleSourceIds,\n      styleSources,\n      styles,\n    });\n\n    expect(styles.has(\"local1:base:color\")).toBe(false);\n    expect(styles.has(\"local2:base:fontSize\")).toBe(false);\n    expect(styles.has(\"local3:base:display\")).toBe(true);\n    expect(styles.size).toBe(1);\n  });\n\n  test(\"handles empty set of local style source IDs\", () => {\n    const localStyleSourceIds = new Set<string>();\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map();\n\n    deleteLocalStyleSourcesMutable({\n      localStyleSourceIds,\n      styleSources,\n      styles,\n    });\n\n    expect(styleSources.has(\"local1\")).toBe(true);\n  });\n});\n\ndescribe(\"collectStyleSourcesFromInstances\", () => {\n  test(\"collects style sources and selections from instances\", () => {\n    const instanceIds = new Set([\"instance1\", \"instance2\"]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\"instance1\", { instanceId: \"instance1\", values: [\"local1\", \"token1\"] }],\n      [\"instance2\", { instanceId: \"instance2\", values: [\"local2\"] }],\n      [\"instance3\", { instanceId: \"instance3\", values: [\"local3\"] }],\n    ]);\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n      [\"local2\", { type: \"local\", id: \"local2\" } as StyleSource],\n      [\"local3\", { type: \"local\", id: \"local3\" } as StyleSource],\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Token 1\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"local1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n      [\n        \"local2:base:fontSize\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local2\",\n          property: \"fontSize\",\n          value: { type: \"unit\", value: 16, unit: \"px\" },\n        },\n      ],\n      [\n        \"local3:base:display\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local3\",\n          property: \"display\",\n          value: { type: \"keyword\", value: \"flex\" },\n        },\n      ],\n      [\n        \"token1:base:backgroundColor\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"token1\",\n          property: \"backgroundColor\",\n          value: { type: \"keyword\", value: \"blue\" },\n        },\n      ],\n    ]);\n\n    const result = collectStyleSourcesFromInstances({\n      instanceIds,\n      styleSourceSelections,\n      styleSources,\n      styles,\n    });\n\n    expect(result.styleSourceSelectionsArray).toHaveLength(2);\n    expect(result.styleSourceSelectionsArray[0].instanceId).toBe(\"instance1\");\n    expect(result.styleSourceSelectionsArray[1].instanceId).toBe(\"instance2\");\n\n    expect(result.styleSourcesMap.size).toBe(3);\n    expect(result.styleSourcesMap.has(\"local1\")).toBe(true);\n    expect(result.styleSourcesMap.has(\"local2\")).toBe(true);\n    expect(result.styleSourcesMap.has(\"token1\")).toBe(true);\n    expect(result.styleSourcesMap.has(\"local3\")).toBe(false);\n\n    expect(result.stylesArray).toHaveLength(3);\n    expect(\n      result.stylesArray.find((s) => s.styleSourceId === \"local1\")\n    ).toBeDefined();\n    expect(\n      result.stylesArray.find((s) => s.styleSourceId === \"local2\")\n    ).toBeDefined();\n    expect(\n      result.stylesArray.find((s) => s.styleSourceId === \"token1\")\n    ).toBeDefined();\n    expect(\n      result.stylesArray.find((s) => s.styleSourceId === \"local3\")\n    ).toBeUndefined();\n  });\n\n  test(\"returns empty arrays when instances have no style sources\", () => {\n    const instanceIds = new Set([\"instance1\", \"instance2\"]);\n    const styleSourceSelections: StyleSourceSelections = new Map();\n    const styleSources: StyleSources = new Map();\n    const styles: Styles = new Map();\n\n    const result = collectStyleSourcesFromInstances({\n      instanceIds,\n      styleSourceSelections,\n      styleSources,\n      styles,\n    });\n\n    expect(result.styleSourceSelectionsArray).toHaveLength(0);\n    expect(result.styleSourcesMap.size).toBe(0);\n    expect(result.stylesArray).toHaveLength(0);\n  });\n\n  test(\"handles missing style sources gracefully\", () => {\n    const instanceIds = new Set([\"instance1\"]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\n        \"instance1\",\n        { instanceId: \"instance1\", values: [\"local1\", \"missing-source\"] },\n      ],\n    ]);\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"local1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"local1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n    ]);\n\n    const result = collectStyleSourcesFromInstances({\n      instanceIds,\n      styleSourceSelections,\n      styleSources,\n      styles,\n    });\n\n    expect(result.styleSourceSelectionsArray).toHaveLength(1);\n    expect(result.styleSourcesMap.size).toBe(1);\n    expect(result.styleSourcesMap.has(\"local1\")).toBe(true);\n    expect(result.styleSourcesMap.has(\"missing-source\")).toBe(false);\n  });\n\n  test(\"deduplicates style sources from multiple instances\", () => {\n    const instanceIds = new Set([\"instance1\", \"instance2\"]);\n    const styleSourceSelections: StyleSourceSelections = new Map([\n      [\"instance1\", { instanceId: \"instance1\", values: [\"token1\"] }],\n      [\"instance2\", { instanceId: \"instance2\", values: [\"token1\"] }],\n    ]);\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Shared Token\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"token1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n    ]);\n\n    const result = collectStyleSourcesFromInstances({\n      instanceIds,\n      styleSourceSelections,\n      styleSources,\n      styles,\n    });\n\n    expect(result.styleSourceSelectionsArray).toHaveLength(2);\n    expect(result.styleSourcesMap.size).toBe(1);\n    expect(result.stylesArray).toHaveLength(1);\n  });\n});\n\ndescribe(\"findDuplicateTokens\", () => {\n  test(\"finds tokens with identical styles\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary Red\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Accent Red\" } as StyleSource,\n      ],\n      [\"token3\", { type: \"token\", id: \"token3\", name: \"Blue\" } as StyleSource],\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token3:base:color\",\n        createStyleDecl(\"token3\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(2);\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n    expect(duplicates.has(\"token3\")).toBe(false);\n    expect(duplicates.has(\"local1\")).toBe(false);\n  });\n\n  test(\"finds tokens with same name\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Primary\" } as StyleSource,\n      ],\n      [\n        \"token3\",\n        { type: \"token\", id: \"token3\", name: \"Secondary\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"token3:base:color\",\n        createStyleDecl(\"token3\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"green\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(2);\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n    expect(duplicates.has(\"token3\")).toBe(false);\n  });\n\n  test(\"finds tokens with same name AND same styles without duplicating\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Primary\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Primary\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(2);\n    // Should list each token only once, not twice (once for name, once for styles)\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n  });\n\n  test(\"finds mixed duplicates: some by style, some by name\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\"token1\", { type: \"token\", id: \"token1\", name: \"Red A\" } as StyleSource],\n      [\"token2\", { type: \"token\", id: \"token2\", name: \"Red B\" } as StyleSource],\n      [\n        \"token3\",\n        { type: \"token\", id: \"token3\", name: \"Duplicate Name\" } as StyleSource,\n      ],\n      [\n        \"token4\",\n        { type: \"token\", id: \"token4\", name: \"Duplicate Name\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token3:base:color\",\n        createStyleDecl(\"token3\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"token4:base:color\",\n        createStyleDecl(\"token4\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"green\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(4);\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n    expect(duplicates.get(\"token3\")).toEqual([\"token4\"]);\n    expect(duplicates.get(\"token4\")).toEqual([\"token3\"]);\n  });\n\n  test(\"finds multiple groups of duplicates\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\"token1\", { type: \"token\", id: \"token1\", name: \"Red 1\" } as StyleSource],\n      [\"token2\", { type: \"token\", id: \"token2\", name: \"Red 2\" } as StyleSource],\n      [\n        \"token3\",\n        { type: \"token\", id: \"token3\", name: \"Blue 1\" } as StyleSource,\n      ],\n      [\n        \"token4\",\n        { type: \"token\", id: \"token4\", name: \"Blue 2\" } as StyleSource,\n      ],\n      [\n        \"token5\",\n        { type: \"token\", id: \"token5\", name: \"Blue 3\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token3:base:color\",\n        createStyleDecl(\"token3\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"token4:base:color\",\n        createStyleDecl(\"token4\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n      [\n        \"token5:base:color\",\n        createStyleDecl(\"token5\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(5);\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n    expect(duplicates.get(\"token3\")).toEqual([\"token4\", \"token5\"]);\n    expect(duplicates.get(\"token4\")).toEqual([\"token3\", \"token5\"]);\n    expect(duplicates.get(\"token5\")).toEqual([\"token3\", \"token4\"]);\n  });\n\n  test(\"returns empty map when no duplicates exist\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\"token1\", { type: \"token\", id: \"token1\", name: \"Red\" } as StyleSource],\n      [\"token2\", { type: \"token\", id: \"token2\", name: \"Blue\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"blue\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(0);\n  });\n\n  test(\"compares tokens across breakpoints and states\", () => {\n    const breakpoints = toMap<Breakpoint>([\n      { id: \"base\", label: \"base\" },\n      { id: \"tablet\", label: \"tablet\", minWidth: 768 },\n    ]);\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Token 1\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Token 2\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token1:base:color\",\n        createStyleDecl(\"token1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token1:tablet:color\",\n        {\n          ...createStyleDecl(\"token1\", \"tablet\", \"color\", {\n            type: \"keyword\",\n            value: \"blue\",\n          }),\n          state: \":hover\",\n        },\n      ],\n      [\n        \"token2:base:color\",\n        createStyleDecl(\"token2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"token2:tablet:color\",\n        {\n          ...createStyleDecl(\"token2\", \"tablet\", \"color\", {\n            type: \"keyword\",\n            value: \"blue\",\n          }),\n          state: \":hover\",\n        },\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(2);\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n  });\n\n  test(\"ignores local style sources\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\"local1\", { type: \"local\", id: \"local1\" } as StyleSource],\n      [\"local2\", { type: \"local\", id: \"local2\" } as StyleSource],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"local1:base:color\",\n        createStyleDecl(\"local1\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n      [\n        \"local2:base:color\",\n        createStyleDecl(\"local2\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    expect(duplicates.size).toBe(0);\n  });\n\n  test(\"handles tokens with no styles\", () => {\n    const breakpoints = toMap<Breakpoint>([{ id: \"base\", label: \"base\" }]);\n    const styleSources: StyleSources = new Map([\n      [\n        \"token1\",\n        { type: \"token\", id: \"token1\", name: \"Empty 1\" } as StyleSource,\n      ],\n      [\n        \"token2\",\n        { type: \"token\", id: \"token2\", name: \"Empty 2\" } as StyleSource,\n      ],\n      [\n        \"token3\",\n        { type: \"token\", id: \"token3\", name: \"With Styles\" } as StyleSource,\n      ],\n    ]);\n    const styles: Styles = new Map([\n      [\n        \"token3:base:color\",\n        createStyleDecl(\"token3\", \"base\", \"color\", {\n          type: \"keyword\",\n          value: \"red\",\n        }),\n      ],\n    ]);\n\n    const duplicates = findDuplicateTokens({\n      styleSources,\n      styles,\n      breakpoints,\n    });\n\n    // Empty tokens should be considered duplicates of each other\n    expect(duplicates.size).toBe(2);\n    expect(duplicates.get(\"token1\")).toEqual([\"token2\"]);\n    expect(duplicates.get(\"token2\")).toEqual([\"token1\"]);\n    expect(duplicates.has(\"token3\")).toBe(false);\n  });\n});\n\ndescribe(\"findTokenWithMatchingStyles\", () => {\n  const breakpoints = new Map<Breakpoint[\"id\"], Breakpoint>([\n    [\"base\", { id: \"base\", label: \"Base\" }],\n  ]);\n\n  test(\"returns no conflict when token name doesn't exist\", () => {\n    const existingTokens: StyleSource[] = [\n      { type: \"token\", id: \"existing1\", name: \"PrimaryColor\" },\n    ];\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: \"SecondaryColor\",\n      tokenStyles: [],\n      existingTokens,\n      existingStyles: [],\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(result.hasConflict).toBe(false);\n    expect(result.matchingToken).toBeUndefined();\n  });\n\n  test(\"returns matching token when name and styles match\", () => {\n    const existingToken: Extract<StyleSource, { type: \"token\" }> = {\n      type: \"token\",\n      id: \"existing1\",\n      name: \"PrimaryColor\",\n    };\n\n    const existingStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const tokenStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: \"PrimaryColor\",\n      tokenStyles,\n      existingTokens: [existingToken],\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(result.hasConflict).toBe(false);\n    expect(result.matchingToken).toBeDefined();\n    expect(result.matchingToken?.id).toBe(\"existing1\");\n  });\n\n  test(\"returns conflict when name matches but styles differ\", () => {\n    const existingToken: Extract<StyleSource, { type: \"token\" }> = {\n      type: \"token\",\n      id: \"existing1\",\n      name: \"PrimaryColor\",\n    };\n\n    const existingStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const tokenStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"blue\" }, // Different color\n      },\n    ];\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: \"PrimaryColor\",\n      tokenStyles,\n      existingTokens: [existingToken],\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(result.hasConflict).toBe(true);\n    expect(result.matchingToken).toBeUndefined();\n  });\n\n  test(\"matches token with multiple style properties\", () => {\n    const existingToken: Extract<StyleSource, { type: \"token\" }> = {\n      type: \"token\",\n      id: \"existing1\",\n      name: \"ButtonStyle\",\n    };\n\n    const existingStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"white\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"backgroundColor\",\n        value: { type: \"keyword\", value: \"blue\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"paddingTop\",\n        value: { type: \"unit\", value: 10, unit: \"px\" },\n      },\n    ];\n\n    const tokenStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"paddingTop\",\n        value: { type: \"unit\", value: 10, unit: \"px\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"backgroundColor\",\n        value: { type: \"keyword\", value: \"blue\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"white\" },\n      },\n    ];\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: \"ButtonStyle\",\n      tokenStyles,\n      existingTokens: [existingToken],\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(result.hasConflict).toBe(false);\n    expect(result.matchingToken?.id).toBe(\"existing1\");\n  });\n\n  test(\"detects conflict when one style property differs\", () => {\n    const existingToken: Extract<StyleSource, { type: \"token\" }> = {\n      type: \"token\",\n      id: \"existing1\",\n      name: \"ButtonStyle\",\n    };\n\n    const existingStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"white\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"existing1\",\n        property: \"backgroundColor\",\n        value: { type: \"keyword\", value: \"blue\" },\n      },\n    ];\n\n    const tokenStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"white\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"fragment1\",\n        property: \"backgroundColor\",\n        value: { type: \"keyword\", value: \"red\" }, // Different!\n      },\n    ];\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: \"ButtonStyle\",\n      tokenStyles,\n      existingTokens: [existingToken],\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(result.hasConflict).toBe(true);\n    expect(result.matchingToken).toBeUndefined();\n  });\n\n  test(\"handles empty token styles\", () => {\n    const existingToken: Extract<StyleSource, { type: \"token\" }> = {\n      type: \"token\",\n      id: \"existing1\",\n      name: \"EmptyToken\",\n    };\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: \"EmptyToken\",\n      tokenStyles: [],\n      existingTokens: [existingToken],\n      existingStyles: [],\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(result.hasConflict).toBe(false);\n    expect(result.matchingToken?.id).toBe(\"existing1\");\n  });\n});\n\ndescribe(\"detectTokenConflicts\", () => {\n  const breakpoints = new Map<Breakpoint[\"id\"], Breakpoint>([\n    [\"base\", { id: \"base\", label: \"Base\" }],\n  ]);\n\n  test(\"returns empty array when no conflicts exist\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"token\", id: \"frag1\", name: \"NewToken\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const existingStyleSources = new Map<string, StyleSource>([\n      [\"exist1\", { type: \"token\", id: \"exist1\", name: \"ExistingToken\" }],\n    ]);\n\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"exist1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"exist1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"blue\" },\n        },\n      ],\n    ]);\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(conflicts).toHaveLength(0);\n  });\n\n  test(\"detects conflict when token name exists with different styles\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"token\", id: \"frag1\", name: \"PrimaryColor\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const existingStyleSources = new Map<string, StyleSource>([\n      [\"exist1\", { type: \"token\", id: \"exist1\", name: \"PrimaryColor\" }],\n    ]);\n\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"exist1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"exist1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"blue\" }, // Different color\n        },\n      ],\n    ]);\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(conflicts).toHaveLength(1);\n    expect(conflicts[0].tokenName).toBe(\"PrimaryColor\");\n    expect(conflicts[0].fragmentTokenId).toBe(\"frag1\");\n    expect(conflicts[0].existingToken.id).toBe(\"exist1\");\n  });\n\n  test(\"detects multiple conflicts\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"token\", id: \"frag1\", name: \"PrimaryColor\" },\n      { type: \"token\", id: \"frag2\", name: \"SecondaryColor\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag2\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"green\" },\n      },\n    ];\n\n    const existingStyleSources = new Map<string, StyleSource>([\n      [\"exist1\", { type: \"token\", id: \"exist1\", name: \"PrimaryColor\" }],\n      [\"exist2\", { type: \"token\", id: \"exist2\", name: \"SecondaryColor\" }],\n    ]);\n\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"exist1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"exist1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"blue\" },\n        },\n      ],\n      [\n        \"exist2:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"exist2\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"yellow\" },\n        },\n      ],\n    ]);\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(conflicts).toHaveLength(2);\n    expect(conflicts[0].tokenName).toBe(\"PrimaryColor\");\n    expect(conflicts[1].tokenName).toBe(\"SecondaryColor\");\n  });\n\n  test(\"ignores local style sources\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"local\", id: \"frag1\" },\n      { type: \"token\", id: \"frag2\", name: \"SomeToken\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag2\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"blue\" },\n      },\n    ];\n\n    const existingStyleSources = new Map<string, StyleSource>();\n    const existingStyles = new Map<string, StyleDecl>();\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(conflicts).toHaveLength(0);\n  });\n\n  test(\"no conflict when token name matches and styles match\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"token\", id: \"frag1\", name: \"SharedToken\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"base\",\n        styleSourceId: \"frag1\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ];\n\n    const existingStyleSources = new Map<string, StyleSource>([\n      [\"exist1\", { type: \"token\", id: \"exist1\", name: \"SharedToken\" }],\n    ]);\n\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"exist1:base:color\",\n        {\n          breakpointId: \"base\",\n          styleSourceId: \"exist1\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" }, // Same color\n        },\n      ],\n    ]);\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(conflicts).toHaveLength(0);\n  });\n\n  test(\"handles tokens with no styles\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"token\", id: \"frag1\", name: \"EmptyToken\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [];\n\n    const existingStyleSources = new Map<string, StyleSource>([\n      [\"exist1\", { type: \"token\", id: \"exist1\", name: \"EmptyToken\" }],\n    ]);\n\n    const existingStyles = new Map<string, StyleDecl>();\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds: new Map(),\n    });\n\n    expect(conflicts).toHaveLength(0);\n  });\n\n  test(\"uses merged breakpoint IDs when comparing\", () => {\n    const fragmentStyleSources: StyleSource[] = [\n      { type: \"token\", id: \"frag1\", name: \"ResponsiveToken\" },\n    ];\n\n    const fragmentStyles: StyleDecl[] = [\n      {\n        breakpointId: \"tablet-frag\",\n        styleSourceId: \"frag1\",\n        property: \"fontSize\",\n        value: { type: \"unit\", value: 18, unit: \"px\" },\n      },\n    ];\n\n    const breakpointsMap = new Map<Breakpoint[\"id\"], Breakpoint>([\n      [\"base\", { id: \"base\", label: \"Base\" }],\n      [\"tablet-exist\", { id: \"tablet-exist\", minWidth: 768, label: \"Tablet\" }],\n    ]);\n\n    const mergedBreakpointIds = new Map([[\"tablet-frag\", \"tablet-exist\"]]);\n\n    const existingStyleSources = new Map<string, StyleSource>([\n      [\"exist1\", { type: \"token\", id: \"exist1\", name: \"ResponsiveToken\" }],\n    ]);\n\n    const existingStyles = new Map<string, StyleDecl>([\n      [\n        \"exist1:tablet-exist:fontSize\",\n        {\n          breakpointId: \"tablet-exist\",\n          styleSourceId: \"exist1\",\n          property: \"fontSize\",\n          value: { type: \"unit\", value: 18, unit: \"px\" },\n        },\n      ],\n    ]);\n\n    const conflicts = detectTokenConflicts({\n      fragmentStyleSources,\n      fragmentStyles,\n      existingStyleSources,\n      existingStyles,\n      breakpoints: breakpointsMap,\n      mergedBreakpointIds,\n    });\n\n    // Should be no conflict since the breakpoints are merged and styles match\n    expect(conflicts).toHaveLength(0);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/style-source-utils.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport type {\n  Breakpoint,\n  Instance,\n  StyleDecl,\n  StyleSource,\n  StyleSourceSelection,\n  Styles,\n  StyleSources,\n  StyleSourceSelections,\n} from \"@webstudio-is/sdk\";\nimport { getStyleDeclKey, ROOT_INSTANCE_ID } from \"@webstudio-is/sdk\";\nimport { toValue } from \"@webstudio-is/css-engine\";\nimport type { ConflictResolution } from \"./token-conflict-dialog\";\nimport { removeByMutable } from \"./array-utils\";\n\n/**\n * Generates a normalized CSS string for comparing style source styles.\n * This creates a deterministic signature based on breakpoint, state, property, and value.\n * The signature is normalized by sorting to ensure order-independent comparison.\n */\nexport const getStyleSourceStylesSignature = (\n  styleSourceId: StyleSource[\"id\"],\n  styles: StyleDecl[],\n  breakpoints: Map<Breakpoint[\"id\"], Breakpoint>,\n  mergedBreakpointIds: Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>\n): string => {\n  const tokenStyles = styles\n    .filter((decl) => decl.styleSourceId === styleSourceId)\n    .map((decl) => {\n      // Get merged breakpoint id to ensure consistent comparison\n      const breakpointId =\n        mergedBreakpointIds.get(decl.breakpointId) ?? decl.breakpointId;\n      const breakpoint = breakpoints.get(breakpointId);\n      const breakpointKey = breakpoint\n        ? JSON.stringify({ minWidth: breakpoint.minWidth })\n        : \"base\";\n      const state = decl.state ?? \"\";\n      return `${breakpointKey}|${state}|${decl.property}:${toValue(decl.value)}`;\n    })\n    .sort()\n    .join(\";\");\n  return tokenStyles;\n};\n\n/**\n * Check if a token with the same name and matching styles already exists.\n * Returns the existing token if found, undefined otherwise.\n */\nexport const findTokenWithMatchingStyles = ({\n  tokenName,\n  tokenStyles,\n  existingTokens,\n  existingStyles,\n  breakpoints,\n  mergedBreakpointIds,\n}: {\n  tokenName: string;\n  tokenStyles: StyleDecl[];\n  existingTokens: StyleSource[];\n  existingStyles: StyleDecl[];\n  breakpoints: Map<Breakpoint[\"id\"], Breakpoint>;\n  mergedBreakpointIds: Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>;\n}):\n  | {\n      hasConflict: false;\n      matchingToken: Extract<StyleSource, { type: \"token\" }>;\n    }\n  | { hasConflict: true; matchingToken: undefined }\n  | { hasConflict: false; matchingToken: undefined } => {\n  // Find tokens with the same name\n  const tokensWithSameName = existingTokens.filter(\n    (token) => token.type === \"token\" && token.name === tokenName\n  );\n\n  if (tokensWithSameName.length === 0) {\n    return { hasConflict: false, matchingToken: undefined };\n  }\n\n  // Get the signature of the token we're checking\n  // Use a temporary ID since we're just comparing styles\n  const tempId = \"temp\";\n  const signature = getStyleSourceStylesSignature(\n    tempId,\n    tokenStyles.map((style) => ({ ...style, styleSourceId: tempId })),\n    breakpoints,\n    mergedBreakpointIds\n  );\n\n  // Check if any existing token with the same name has matching styles\n  for (const existing of tokensWithSameName) {\n    if (existing.type !== \"token\") {\n      continue;\n    }\n    const existingSignature = getStyleSourceStylesSignature(\n      existing.id,\n      existingStyles,\n      breakpoints,\n      mergedBreakpointIds\n    );\n    if (existingSignature === signature) {\n      return { hasConflict: false, matchingToken: existing };\n    }\n  }\n\n  // Same name but different styles = conflict\n  return { hasConflict: true, matchingToken: undefined };\n};\n\nexport type TokenConflict = {\n  tokenName: string;\n  fragmentTokenId: StyleSource[\"id\"];\n  fragmentToken: Extract<StyleSource, { type: \"token\" }>;\n  existingToken: Extract<StyleSource, { type: \"token\" }>;\n};\n\n/**\n * Detect all token conflicts before insertion.\n * Returns an array of conflicts where fragment tokens have the same name but different styles than existing tokens.\n */\nexport const detectTokenConflicts = ({\n  fragmentStyleSources,\n  fragmentStyles,\n  existingStyleSources,\n  existingStyles,\n  breakpoints,\n  mergedBreakpointIds,\n}: {\n  fragmentStyleSources: StyleSource[];\n  fragmentStyles: StyleDecl[];\n  existingStyleSources: StyleSources;\n  existingStyles: Styles;\n  breakpoints: Map<Breakpoint[\"id\"], Breakpoint>;\n  mergedBreakpointIds: Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>;\n}): TokenConflict[] => {\n  const conflicts: TokenConflict[] = [];\n  const existingTokens = Array.from(existingStyleSources.values());\n  const existingStylesArray = Array.from(existingStyles.values());\n\n  for (const styleSource of fragmentStyleSources) {\n    if (styleSource.type !== \"token\") {\n      continue;\n    }\n\n    const result = findTokenWithMatchingStyles({\n      tokenName: styleSource.name,\n      tokenStyles: fragmentStyles.filter(\n        (decl) => decl.styleSourceId === styleSource.id\n      ),\n      existingTokens,\n      existingStyles: existingStylesArray,\n      breakpoints,\n      mergedBreakpointIds,\n    });\n\n    if (result.hasConflict) {\n      // Find the first existing token with the same name for display purposes\n      const existingToken = existingTokens.find(\n        (token) => token.type === \"token\" && token.name === styleSource.name\n      );\n      if (existingToken && existingToken.type === \"token\") {\n        conflicts.push({\n          tokenName: styleSource.name,\n          fragmentTokenId: styleSource.id,\n          fragmentToken: styleSource,\n          existingToken,\n        });\n      }\n    }\n  }\n\n  return conflicts;\n};\n\n/**\n * Style Source Conflict Resolution Rules:\n *\n * When inserting a fragment with style sources (tokens), the following rules determine whether to reuse,\n * rename, or create new style sources:\n *\n * 1. Same name + Same styles → Reuse existing style source (no new style source created)\n * 2. Same name + Different styles → Add counter suffix (e.g., \"myToken-1\", \"myToken-2\") OR use existing based on onConflict\n * 3. Different name + Same styles → Insert as new style source with its own name\n * 4. Different name + Different styles → Insert as new style source normally\n * 5. Name collision safeguard → Always add counter suffix if name already exists\n * 6. Style comparison → Only compares CSS signatures when style source names match\n *\n * All new style sources receive a fresh UUID to prevent ID collisions. The styleSourceIdMap tracks\n * originalFragmentStyleSourceId → newStyleSourceId (or existingStyleSourceId if reused) to ensure all\n * references (styles, styleSourceSelections) are updated correctly.\n */\nexport const insertStyleSources = ({\n  fragmentStyleSources,\n  fragmentStyles,\n  existingStyleSources,\n  existingStyles,\n  breakpoints,\n  mergedBreakpointIds,\n  conflictResolution = \"theirs\",\n}: {\n  fragmentStyleSources: StyleSource[];\n  fragmentStyles: StyleDecl[];\n  existingStyleSources: StyleSources;\n  existingStyles: Styles;\n  breakpoints: Map<Breakpoint[\"id\"], Breakpoint>;\n  mergedBreakpointIds: Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>;\n  /** How to handle conflicts: \"theirs\" = add suffix (keep incoming), \"ours\" = use existing token, \"merge\" = merge styles (theirs overrides ours) */\n  conflictResolution?: ConflictResolution;\n}): {\n  styleSourceIds: Set<StyleSource[\"id\"]>;\n  styleSourceIdMap: Map<StyleSource[\"id\"], StyleSource[\"id\"]>;\n  updatedStyleSources: StyleSources;\n} => {\n  // Build a map of existing tokens by name\n  const existingTokensByName = new Map<string, StyleSource[]>();\n\n  for (const styleSource of existingStyleSources.values()) {\n    if (styleSource.type !== \"token\") {\n      continue;\n    }\n    const tokensWithName = existingTokensByName.get(styleSource.name) ?? [];\n    tokensWithName.push(styleSource);\n    existingTokensByName.set(styleSource.name, tokensWithName);\n  }\n\n  const styleSourceIds = new Set<StyleSource[\"id\"]>();\n  const styleSourceIdMap = new Map<StyleSource[\"id\"], StyleSource[\"id\"]>(); // old id -> new id\n  const updatedStyleSources = new Map(existingStyleSources);\n\n  for (const styleSource of fragmentStyleSources) {\n    if (styleSource.type === \"local\") {\n      continue;\n    }\n    styleSource.type satisfies \"token\";\n\n    const originalFragmentTokenId = styleSource.id;\n    const newTokenId = nanoid();\n\n    // Check if there's an existing token with the same name\n    const tokensWithSameName = existingTokensByName.get(styleSource.name);\n\n    if (tokensWithSameName && tokensWithSameName.length > 0) {\n      // Same name exists - compare styles to decide if we can reuse\n      const result = findTokenWithMatchingStyles({\n        tokenName: styleSource.name,\n        tokenStyles: fragmentStyles.filter(\n          (decl) => decl.styleSourceId === originalFragmentTokenId\n        ),\n        existingTokens: tokensWithSameName,\n        existingStyles: Array.from(existingStyles.values()),\n        breakpoints,\n        mergedBreakpointIds,\n      });\n\n      if (result.matchingToken) {\n        // Same name AND same styles -> reuse existing token\n        styleSourceIdMap.set(originalFragmentTokenId, result.matchingToken.id);\n        continue; // Don't insert, reuse existing\n      }\n\n      if (result.hasConflict) {\n        // Same name but different styles\n        if (conflictResolution === \"ours\") {\n          // Use the existing token instead of creating a new one\n          const existingToken = tokensWithSameName[0];\n          if (existingToken.type !== \"token\") {\n            continue;\n          }\n          styleSourceIdMap.set(originalFragmentTokenId, existingToken.id);\n          continue; // Don't insert, use existing\n        } else if (conflictResolution === \"merge\") {\n          // Merge: keep existing token name/id, but merge styles (theirs overrides ours)\n          const existingToken = tokensWithSameName[0];\n          if (existingToken.type !== \"token\") {\n            continue;\n          }\n\n          // Map the fragment token to the existing token\n          styleSourceIdMap.set(originalFragmentTokenId, existingToken.id);\n\n          // Mark the existing token for style insertion\n          // This will allow the fragment styles to be added/merged\n          styleSourceIds.add(originalFragmentTokenId);\n          continue;\n        } else {\n          // Default: add counter suffix\n          let maxCounter = 0;\n          const baseNamePattern = new RegExp(\n            `^${styleSource.name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}(?:-(\\\\d+))?$`\n          );\n          for (const existing of updatedStyleSources.values()) {\n            if (existing.type !== \"token\") {\n              continue;\n            }\n            const match = existing.name.match(baseNamePattern);\n            if (match) {\n              const counter = match[1] ? parseInt(match[1], 10) : 0;\n              maxCounter = Math.max(maxCounter, counter);\n            }\n          }\n          const newName = `${styleSource.name}-${maxCounter + 1}`;\n          const newStyleSource = {\n            ...styleSource,\n            id: newTokenId,\n            name: newName,\n          };\n          styleSourceIds.add(originalFragmentTokenId);\n          updatedStyleSources.set(newTokenId, newStyleSource);\n          styleSourceIdMap.set(originalFragmentTokenId, newTokenId);\n\n          // Add to tracking maps\n          const tokensWithNewName = existingTokensByName.get(newName) ?? [];\n          tokensWithNewName.push(newStyleSource);\n          existingTokensByName.set(newName, tokensWithNewName);\n          continue;\n        }\n      }\n    }\n\n    // Different name (or no existing tokens) -> insert with new ID\n    const newStyleSource = { ...styleSource, id: newTokenId };\n    styleSourceIds.add(originalFragmentTokenId);\n    updatedStyleSources.set(newTokenId, newStyleSource);\n    styleSourceIdMap.set(originalFragmentTokenId, newTokenId);\n\n    // Add to tracking maps\n    const tokensWithName = existingTokensByName.get(styleSource.name) ?? [];\n    tokensWithName.push(newStyleSource);\n    existingTokensByName.set(styleSource.name, tokensWithName);\n  }\n\n  return {\n    styleSourceIds,\n    styleSourceIdMap,\n    updatedStyleSources,\n  };\n};\n\n/**\n * Insert local style sources for portal content without changing IDs.\n * Portal content IDs are preserved to avoid data bloat.\n */\nexport const insertPortalLocalStyleSources = ({\n  fragmentStyleSources,\n  fragmentStyleSourceSelections,\n  fragmentStyles,\n  instanceIds,\n  styleSources,\n  styleSourceSelections,\n  styles,\n  mergedBreakpointIds,\n}: {\n  fragmentStyleSources: StyleSource[];\n  fragmentStyleSourceSelections: StyleSourceSelection[];\n  fragmentStyles: StyleDecl[];\n  instanceIds: Set<Instance[\"id\"]>;\n  styleSources: StyleSources;\n  styleSourceSelections: StyleSourceSelections;\n  styles: Styles;\n  mergedBreakpointIds: Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>;\n}): void => {\n  const instanceStyleSourceIds = new Set<StyleSource[\"id\"]>();\n  for (const styleSourceSelection of fragmentStyleSourceSelections) {\n    const { instanceId } = styleSourceSelection;\n    if (instanceIds.has(instanceId) === false) {\n      continue;\n    }\n    styleSourceSelections.set(instanceId, styleSourceSelection);\n    for (const styleSourceId of styleSourceSelection.values) {\n      instanceStyleSourceIds.add(styleSourceId);\n    }\n  }\n  const localStyleSourceIds = new Set<StyleSource[\"id\"]>();\n  for (const styleSource of fragmentStyleSources) {\n    if (\n      styleSource.type === \"local\" &&\n      instanceStyleSourceIds.has(styleSource.id)\n    ) {\n      localStyleSourceIds.add(styleSource.id);\n      styleSources.set(styleSource.id, styleSource);\n    }\n  }\n  for (const styleDecl of fragmentStyles) {\n    if (localStyleSourceIds.has(styleDecl.styleSourceId)) {\n      const { breakpointId } = styleDecl;\n      const newStyleDecl: StyleDecl = {\n        ...styleDecl,\n        breakpointId: mergedBreakpointIds.get(breakpointId) ?? breakpointId,\n      };\n      styles.set(getStyleDeclKey(newStyleDecl), newStyleDecl);\n    }\n  }\n};\n\n/**\n * Insert local style sources with new IDs for regular (non-portal) content.\n * Handles merging :root styles and token remapping.\n */\nexport const insertLocalStyleSourcesWithNewIds = ({\n  fragmentStyleSources,\n  fragmentStyleSourceSelections,\n  fragmentStyles,\n  fragmentInstanceIds,\n  newInstanceIds,\n  styleSourceIdMap,\n  styleSources,\n  styleSourceSelections,\n  styles,\n  mergedBreakpointIds,\n}: {\n  fragmentStyleSources: StyleSource[];\n  fragmentStyleSourceSelections: StyleSourceSelection[];\n  fragmentStyles: StyleDecl[];\n  fragmentInstanceIds: Set<Instance[\"id\"]>;\n  newInstanceIds: Map<Instance[\"id\"], Instance[\"id\"]>;\n  styleSourceIdMap: Map<StyleSource[\"id\"], StyleSource[\"id\"]>;\n  styleSources: StyleSources;\n  styleSourceSelections: StyleSourceSelections;\n  styles: Styles;\n  mergedBreakpointIds: Map<Breakpoint[\"id\"], Breakpoint[\"id\"]>;\n}): void => {\n  const newLocalStyleSources = new Map();\n  for (const styleSource of fragmentStyleSources) {\n    if (styleSource.type === \"local\") {\n      newLocalStyleSources.set(styleSource.id, styleSource);\n    }\n  }\n\n  const newLocalStyleSourceIds = new Map<\n    StyleSource[\"id\"],\n    StyleSource[\"id\"]\n  >();\n  for (const { instanceId, values } of fragmentStyleSourceSelections) {\n    if (fragmentInstanceIds.has(instanceId) === false) {\n      continue;\n    }\n\n    const existingStyleSourceIds =\n      styleSourceSelections.get(instanceId)?.values ?? [];\n    let existingLocalStyleSource;\n    for (const styleSourceId of existingStyleSourceIds) {\n      const styleSource = styleSources.get(styleSourceId);\n      if (styleSource?.type === \"local\") {\n        existingLocalStyleSource = styleSource;\n      }\n    }\n    const newStyleSourceIds = [];\n    for (let styleSourceId of values) {\n      const newLocalStyleSource = newLocalStyleSources.get(styleSourceId);\n      if (newLocalStyleSource) {\n        // merge only :root styles and duplicate others\n        if (instanceId === ROOT_INSTANCE_ID && existingLocalStyleSource) {\n          // write local styles into existing local style source\n          styleSourceId = existingLocalStyleSource.id;\n        } else {\n          // create new local styles\n          const newId = nanoid();\n          styleSources.set(newId, { ...newLocalStyleSource, id: newId });\n          styleSourceId = newId;\n        }\n        newLocalStyleSourceIds.set(newLocalStyleSource.id, styleSourceId);\n      } else {\n        // Check if this is a token that was mapped to an existing token\n        const mappedTokenId = styleSourceIdMap.get(styleSourceId);\n        if (mappedTokenId) {\n          styleSourceId = mappedTokenId;\n        }\n      }\n      newStyleSourceIds.push(styleSourceId);\n    }\n    const newInstanceId = newInstanceIds.get(instanceId) ?? instanceId;\n    styleSourceSelections.set(newInstanceId, {\n      instanceId: newInstanceId,\n      values: newStyleSourceIds,\n    });\n  }\n\n  for (const styleDecl of fragmentStyles) {\n    const { breakpointId, styleSourceId } = styleDecl;\n    if (newLocalStyleSourceIds.has(styleDecl.styleSourceId)) {\n      const newStyleDecl: StyleDecl = {\n        ...styleDecl,\n        styleSourceId:\n          newLocalStyleSourceIds.get(styleSourceId) ?? styleSourceId,\n        breakpointId: mergedBreakpointIds.get(breakpointId) ?? breakpointId,\n      };\n      styles.set(getStyleDeclKey(newStyleDecl), newStyleDecl);\n    }\n  }\n};\n\n/**\n * Delete a style source and all its associated styles and selections.\n * This is a mutable operation designed to be used within a transaction.\n */\nexport const deleteStyleSourceMutable = ({\n  styleSourceId,\n  styleSources,\n  styleSourceSelections,\n  styles,\n}: {\n  styleSourceId: StyleSource[\"id\"];\n  styleSources: StyleSources;\n  styleSourceSelections: StyleSourceSelections;\n  styles: Styles;\n}): void => {\n  styleSources.delete(styleSourceId);\n  for (const styleSourceSelection of styleSourceSelections.values()) {\n    if (styleSourceSelection.values.includes(styleSourceId)) {\n      removeByMutable(\n        styleSourceSelection.values,\n        (item) => item === styleSourceId\n      );\n    }\n  }\n  for (const [styleDeclKey, styleDecl] of styles) {\n    if (styleDecl.styleSourceId === styleSourceId) {\n      styles.delete(styleDeclKey);\n    }\n  }\n};\n\n/**\n * Find all unused tokens in the style sources.\n * A token is considered unused if it has no usages in any instance.\n */\nexport const findUnusedTokens = ({\n  styleSources,\n  styleSourceUsages,\n}: {\n  styleSources: StyleSources;\n  styleSourceUsages: Map<StyleSource[\"id\"], Set<Instance[\"id\"]>>;\n}): StyleSource[\"id\"][] => {\n  const unusedTokenIds: StyleSource[\"id\"][] = [];\n  for (const styleSource of styleSources.values()) {\n    if (styleSource.type === \"token\") {\n      const usages = styleSourceUsages.get(styleSource.id);\n      if (usages === undefined || usages.size === 0) {\n        unusedTokenIds.push(styleSource.id);\n      }\n    }\n  }\n  return unusedTokenIds;\n};\n\n/**\n * Delete multiple style sources (used for bulk unused token deletion).\n * This is a mutable operation designed to be used within a transaction.\n */\nexport const deleteStyleSourcesMutable = ({\n  styleSourceIds,\n  styleSources,\n  styleSourceSelections,\n  styles,\n}: {\n  styleSourceIds: StyleSource[\"id\"][];\n  styleSources: StyleSources;\n  styleSourceSelections: StyleSourceSelections;\n  styles: Styles;\n}): void => {\n  for (const styleSourceId of styleSourceIds) {\n    deleteStyleSourceMutable({\n      styleSourceId,\n      styleSources,\n      styleSourceSelections,\n      styles,\n    });\n  }\n};\n\nexport type RenameStyleSourceError =\n  | { type: \"minlength\"; id: StyleSource[\"id\"] }\n  | { type: \"duplicate\"; id: StyleSource[\"id\"] };\n\n/**\n * Validate and perform a style source rename.\n * Returns an error if validation fails, undefined on success.\n */\nexport const validateAndRenameStyleSource = ({\n  id,\n  name,\n  styleSources,\n}: {\n  id: StyleSource[\"id\"];\n  name: string;\n  styleSources: StyleSources;\n}): RenameStyleSourceError | undefined => {\n  if (name.trim().length === 0) {\n    return { type: \"minlength\", id };\n  }\n  for (const styleSource of styleSources.values()) {\n    if (\n      styleSource.type === \"token\" &&\n      styleSource.name === name &&\n      styleSource.id !== id\n    ) {\n      return { type: \"duplicate\", id };\n    }\n  }\n  return;\n};\n\n/**\n * Rename a style source (token).\n * This is a mutable operation designed to be used within a transaction.\n */\nexport const renameStyleSourceMutable = ({\n  id,\n  name,\n  styleSources,\n}: {\n  id: StyleSource[\"id\"];\n  name: string;\n  styleSources: StyleSources;\n}): void => {\n  const styleSource = styleSources.get(id);\n  if (styleSource?.type === \"token\") {\n    styleSource.name = name;\n  }\n};\n\n/**\n * Delete local style sources and their associated styles for a set of instances.\n * This is typically used when deleting instances to clean up their local styles.\n * This is a mutable operation designed to be used within a transaction.\n */\nexport const deleteLocalStyleSourcesMutable = ({\n  localStyleSourceIds,\n  styleSources,\n  styles,\n}: {\n  localStyleSourceIds: Set<StyleSource[\"id\"]>;\n  styleSources: StyleSources;\n  styles: Styles;\n}): void => {\n  for (const styleSourceId of localStyleSourceIds) {\n    styleSources.delete(styleSourceId);\n  }\n  for (const [styleDeclKey, styleDecl] of styles) {\n    if (localStyleSourceIds.has(styleDecl.styleSourceId)) {\n      styles.delete(styleDeclKey);\n    }\n  }\n};\n\n/**\n * Collect all style sources and their selections from a set of instances.\n * Returns the style sources, their selections, and the styles associated with them.\n * This is typically used when extracting a fragment of instances.\n */\nexport const collectStyleSourcesFromInstances = ({\n  instanceIds,\n  styleSourceSelections,\n  styleSources,\n  styles,\n}: {\n  instanceIds: Set<Instance[\"id\"]>;\n  styleSourceSelections: StyleSourceSelections;\n  styleSources: StyleSources;\n  styles: Styles;\n}): {\n  styleSourceSelectionsArray: StyleSourceSelection[];\n  styleSourcesMap: StyleSources;\n  stylesArray: StyleDecl[];\n} => {\n  const styleSourceSelectionsArray: StyleSourceSelection[] = [];\n  const styleSourcesMap: StyleSources = new Map();\n\n  // Collect all style sources bound to these instances\n  for (const instanceId of instanceIds) {\n    const styleSourceSelection = styleSourceSelections.get(instanceId);\n    if (styleSourceSelection) {\n      styleSourceSelectionsArray.push(styleSourceSelection);\n      for (const styleSourceId of styleSourceSelection.values) {\n        if (styleSourcesMap.has(styleSourceId)) {\n          continue;\n        }\n        const styleSource = styleSources.get(styleSourceId);\n        if (styleSource === undefined) {\n          continue;\n        }\n        styleSourcesMap.set(styleSourceId, styleSource);\n      }\n    }\n  }\n\n  // Collect styles bound to these style sources\n  const stylesArray: StyleDecl[] = [];\n  for (const styleDecl of styles.values()) {\n    if (styleSourcesMap.has(styleDecl.styleSourceId)) {\n      stylesArray.push(styleDecl);\n    }\n  }\n\n  return {\n    styleSourceSelectionsArray,\n    styleSourcesMap,\n    stylesArray,\n  };\n};\n\n/**\n * Find tokens that have duplicates based on:\n * 1. Identical styles (same CSS signature)\n * 2. Same token name\n *\n * Returns a map where keys are token IDs and values are arrays of duplicate token IDs.\n * Only includes tokens that have at least one duplicate.\n */\nexport const findDuplicateTokens = ({\n  styleSources,\n  styles,\n  breakpoints,\n}: {\n  styleSources: StyleSources;\n  styles: Styles;\n  breakpoints: Map<Breakpoint[\"id\"], Breakpoint>;\n}): Map<StyleSource[\"id\"], StyleSource[\"id\"][]> => {\n  const duplicatesMap = new Map<StyleSource[\"id\"], StyleSource[\"id\"][]>();\n  const stylesArray = Array.from(styles.values());\n  const tokens = Array.from(styleSources.values()).filter(\n    (source): source is Extract<StyleSource, { type: \"token\" }> =>\n      source.type === \"token\"\n  );\n\n  // Build a map of signature -> token IDs\n  const signatureToTokenIds = new Map<string, StyleSource[\"id\"][]>();\n  // Build a map of name -> token IDs\n  const nameToTokenIds = new Map<string, StyleSource[\"id\"][]>();\n\n  for (const token of tokens) {\n    // Group by signature (identical styles)\n    const signature = getStyleSourceStylesSignature(\n      token.id,\n      stylesArray,\n      breakpoints,\n      new Map()\n    );\n\n    if (!signatureToTokenIds.has(signature)) {\n      signatureToTokenIds.set(signature, []);\n    }\n    signatureToTokenIds.get(signature)!.push(token.id);\n\n    // Group by name (same token name)\n    if (!nameToTokenIds.has(token.name)) {\n      nameToTokenIds.set(token.name, []);\n    }\n    nameToTokenIds.get(token.name)!.push(token.id);\n  }\n\n  // Build the duplicates map - include tokens with signature OR name duplicates\n  const processedTokens = new Set<StyleSource[\"id\"]>();\n\n  for (const [_signature, tokenIds] of signatureToTokenIds) {\n    if (tokenIds.length > 1) {\n      for (const tokenId of tokenIds) {\n        if (!duplicatesMap.has(tokenId)) {\n          duplicatesMap.set(tokenId, []);\n        }\n        const duplicates = tokenIds.filter((id) => id !== tokenId);\n        duplicatesMap.get(tokenId)!.push(...duplicates);\n        processedTokens.add(tokenId);\n      }\n    }\n  }\n\n  for (const [_name, tokenIds] of nameToTokenIds) {\n    if (tokenIds.length > 1) {\n      for (const tokenId of tokenIds) {\n        if (!duplicatesMap.has(tokenId)) {\n          duplicatesMap.set(tokenId, []);\n        }\n        const duplicates = tokenIds.filter((id) => id !== tokenId);\n        // Avoid adding the same duplicate twice\n        for (const duplicate of duplicates) {\n          if (!duplicatesMap.get(tokenId)!.includes(duplicate)) {\n            duplicatesMap.get(tokenId)!.push(duplicate);\n          }\n        }\n      }\n    }\n  }\n\n  return duplicatesMap;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/sync/command-queue.ts",
    "content": "import type { Project } from \"@webstudio-is/project\";\nimport type { Build } from \"@webstudio-is/project-build\";\nimport type { Change } from \"immerhin\";\nimport type { Transaction } from \"~/shared/sync-client\";\n\ntype Command =\n  | {\n      type: \"transactions\";\n      projectId: Project[\"id\"];\n      transactions: Transaction<Change[]>[];\n    }\n  | {\n      type: \"setDetails\";\n      projectId: Project[\"id\"];\n      version: number;\n      buildId: Build[\"id\"];\n      authToken: string | undefined;\n    };\n\nconst projectCommandsQueue: Command[] = [];\n\nexport const enqueue = (command: Command) => {\n  if (command.type !== \"transactions\") {\n    projectCommandsQueue.push(command);\n    return;\n  }\n\n  // merge transactions in case of no commands in queue\n  for (const projectCommand of projectCommandsQueue.reverse()) {\n    if (projectCommand.type !== \"transactions\") {\n      break;\n    }\n\n    if (projectCommand.projectId === command.projectId) {\n      projectCommand.transactions.push(...command.transactions);\n      return;\n    }\n  }\n\n  projectCommandsQueue.push(command);\n};\n\nexport const dequeueAll = () => {\n  const commands = [...projectCommandsQueue];\n  projectCommandsQueue.length = 0;\n  return commands;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/sync/data-stores.ts",
    "content": "/**\n * All data stores used by the sync infrastructure.\n * This provides a single source of truth for stores used in sync-server and sync-stores.\n */\n\nimport { atom } from \"nanostores\";\nimport type {\n  Assets,\n  Breakpoints,\n  DataSources,\n  Instances,\n  Pages,\n  Props,\n  Resource,\n  Styles,\n  StyleSources,\n  StyleSourceSelections,\n} from \"@webstudio-is/sdk\";\nimport type { Project } from \"@webstudio-is/project\";\nimport type { MarketplaceProduct } from \"@webstudio-is/project-build\";\n\nexport const $project = atom<Project | undefined>();\n\nexport const $pages = atom<undefined | Pages>(undefined);\n\nexport const $assets = atom<Assets>(new Map());\n\nexport const $instances = atom<Instances>(new Map());\n\nexport const $props = atom<Props>(new Map());\n\nexport const $dataSources = atom<DataSources>(new Map());\n\nexport const $resources = atom(new Map<Resource[\"id\"], Resource>());\n\nexport const $breakpoints = atom<Breakpoints>(new Map());\n\n/**\n * styleSources defines where styles come from (local or token).\n *\n * $styles contains actual styling rules, tied to styleSourceIds.\n * $styles.styleSourceId -> $styleSources.id\n */\nexport const $styleSources = atom<StyleSources>(new Map());\n\n/**\n * This is a list of connections between instances (instanceIds) and styleSources.\n * $styleSourceSelections.values[] -> $styleSources.id[]\n */\nexport const $styleSourceSelections = atom<StyleSourceSelections>(new Map());\n\n/**\n * $styles contains actual styling rules\n * (breakpointId, styleSourceId, property, value, listed), tied to styleSourceIds\n * $styles.styleSourceId -> $styleSources.id\n */\nexport const $styles = atom<Styles>(new Map());\n\nexport const $marketplaceProduct = atom<undefined | MarketplaceProduct>();\n\nexport const $publisherHost = atom<string>(\"wstd.work\");\n\n/**\n * Get initial values for all data stores.\n * Used for resetting stores when switching between projects.\n */\nconst getInitialDataStoreValues = () => ({\n  project: undefined,\n  pages: undefined,\n  assets: new Map(),\n  instances: new Map(),\n  props: new Map(),\n  dataSources: new Map(),\n  resources: new Map(),\n  breakpoints: new Map(),\n  styleSources: new Map(),\n  styleSourceSelections: new Map(),\n  styles: new Map(),\n  marketplaceProduct: undefined,\n  publisherHost: \"wstd.work\",\n});\n\n/**\n * Reset all data stores to their initial values.\n * Call this when switching between projects to prevent data leakage.\n */\nexport const resetDataStores = () => {\n  const initial = getInitialDataStoreValues();\n  $project.set(initial.project);\n  $pages.set(initial.pages);\n  $assets.set(initial.assets);\n  $instances.set(initial.instances);\n  $props.set(initial.props);\n  $dataSources.set(initial.dataSources);\n  $resources.set(initial.resources);\n  $breakpoints.set(initial.breakpoints);\n  $styleSources.set(initial.styleSources);\n  $styleSourceSelections.set(initial.styleSourceSelections);\n  $styles.set(initial.styles);\n  $marketplaceProduct.set(initial.marketplaceProduct);\n  $publisherHost.set(initial.publisherHost);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/sync/project-queue.ts",
    "content": "import { useEffect } from \"react\";\nimport { atom } from \"nanostores\";\nimport type { Change } from \"immerhin\";\nimport type { Project } from \"@webstudio-is/project\";\nimport type { Build } from \"@webstudio-is/project-build\";\nimport type { AuthPermit } from \"@webstudio-is/trpc-interface/index.server\";\nimport * as commandQueue from \"./command-queue\";\nimport { restPatchPath } from \"~/shared/router-utils\";\nimport { toast } from \"@webstudio-is/design-system\";\nimport { fetch } from \"~/shared/fetch.client\";\nimport type { SyncStorage, Transaction } from \"~/shared/sync-client\";\nimport { loadBuilderData } from \"~/shared/builder-data\";\n\nexport { commandQueue };\n\n// Periodic check for new entries to group them into one job/call in sync queue.\nconst NEW_ENTRIES_INTERVAL = 1000;\n\n// First attempts we will simply retry without changing the state or notifying anyone.\nconst INTERVAL_RECOVERY = 2000;\n\n// After this amount of retries without success, we consider connection status error.\nconst MAX_RETRY_RECOVERY = 5;\n\n// We are assuming that error is fatal (unrecoverable) after this amount of attempts with API error.\nconst MAX_ALLOWED_API_ERRORS = 5;\n\n// When we reached max failed attempts we will slow down the attempts interval.\nconst INTERVAL_ERROR = 5000;\nconst MAX_INTERVAL_ERROR = 2 * 60000;\n\nconst pause = (timeout: number) => {\n  return new Promise((resolve) => setTimeout(resolve, timeout));\n};\n\nexport type QueueStatus =\n  | { status: \"running\" }\n  | { status: \"idle\" }\n  | { status: \"recovering\" }\n  | { status: \"failed\" }\n  | { status: \"fatal\"; error: string };\n\nexport const $queueStatus = atom<QueueStatus>({ status: \"idle\" });\n\n// Transaction completion tracking\ntype TransactionCompleteCallback = (success: boolean) => void;\nconst transactionCallbacks = new Map<string, TransactionCompleteCallback[]>();\n\n// Atom to communicate transaction IDs from sendTransaction to user code\nexport const $lastTransactionId = atom<string | undefined>(undefined);\n\n/**\n * Register a callback to be called when a specific transaction is confirmed by the server.\n * The callback receives true if the transaction was successfully persisted, false otherwise.\n */\nexport const onTransactionComplete = (\n  transactionId: string,\n  callback: TransactionCompleteCallback\n) => {\n  const callbacks = transactionCallbacks.get(transactionId) ?? [];\n  callbacks.push(callback);\n  transactionCallbacks.set(transactionId, callbacks);\n\n  // Cleanup after timeout to prevent memory leaks\n  setTimeout(() => {\n    transactionCallbacks.delete(transactionId);\n  }, 60000); // 1 minute timeout\n};\n\n/**\n * Register a callback to be called when the next transaction is confirmed by the server.\n * Use this right after calling serverSyncStore.createTransaction().\n */\nexport const onNextTransactionComplete = (callback: () => void) => {\n  let unsubscribe: (() => void) | undefined;\n  // eslint-disable-next-line prefer-const\n  unsubscribe = $lastTransactionId.subscribe((transactionId) => {\n    if (transactionId) {\n      onTransactionComplete(transactionId, (success) => {\n        if (success) {\n          callback();\n        }\n      });\n      unsubscribe?.();\n    }\n  });\n\n  // Cleanup after timeout to prevent memory leak if transaction never gets an ID\n  setTimeout(() => {\n    unsubscribe?.();\n  }, 60000);\n};\n\nconst getRandomBetween = (a: number, b: number) => {\n  return Math.random() * (b - a) + a;\n};\n// polling is important to queue new transactions independently\n// from async iterator and batch them into single job\nconst pollCommands = async function* () {\n  while (true) {\n    const commands = commandQueue.dequeueAll();\n    if (commands.length > 0) {\n      $queueStatus.set({ status: \"running\" });\n      yield* commands;\n      await pause(NEW_ENTRIES_INTERVAL);\n      // Do not switch on idle state until there is possibility that queue is not empty\n      continue;\n    }\n    $queueStatus.set({ status: \"idle\" });\n    await pause(NEW_ENTRIES_INTERVAL);\n  }\n};\n\nconst retry = async function* () {\n  let failedAttempts = 0;\n  let delay = INTERVAL_ERROR;\n\n  while (true) {\n    yield;\n    failedAttempts += 1;\n    if (failedAttempts < MAX_RETRY_RECOVERY) {\n      $queueStatus.set({ status: \"recovering\" });\n      await pause(INTERVAL_RECOVERY);\n    } else {\n      $queueStatus.set({ status: \"failed\" });\n\n      // Clamped exponential backoff with decorrelated jitter\n      // to prevent clients from sending simultaneous requests after server issues\n      delay = getRandomBetween(INTERVAL_ERROR, delay * 3);\n      delay = Math.min(delay, MAX_INTERVAL_ERROR);\n\n      toast.error(\n        `Builder is offline. Retry in ${Math.round(delay / 1000)} seconds.`\n      );\n\n      await pause(delay);\n    }\n  }\n};\n\nlet isPolling = false;\nlet pollingAbortController: AbortController | undefined;\n\n/**\n * Start the queue polling loop.\n * Called automatically by enqueueProjectDetails.\n */\nconst startPolling = () => {\n  if (pollingAbortController) {\n    return; // Already running\n  }\n  pollingAbortController = new AbortController();\n  pollQueue(pollingAbortController.signal);\n};\n\n/**\n * Stop the queue polling loop.\n * Call this to stop transaction synchronization (e.g., when closing a dialog).\n */\nexport const stopPolling = () => {\n  if (pollingAbortController) {\n    pollingAbortController.abort();\n    pollingAbortController = undefined;\n  }\n  isPolling = false;\n};\n\n/**\n * Enqueue project details for synchronization.\n * Called automatically by initializeClientSync after loading builder data.\n */\nexport const enqueueProjectDetails = ({\n  projectId,\n  buildId,\n  version,\n  authPermit,\n  authToken,\n}: {\n  buildId: Build[\"id\"];\n  projectId: Project[\"id\"];\n  authToken: string | undefined;\n  version: number;\n  authPermit: AuthPermit;\n}) => {\n  if (authPermit === \"view\") {\n    return;\n  }\n  commandQueue.enqueue({\n    type: \"setDetails\",\n    projectId,\n    buildId,\n    version,\n    authToken,\n  });\n  // Start polling after enqueuing to ensure command is in queue\n  startPolling();\n};\n\nconst pollQueue = async (signal: AbortSignal) => {\n  if (isPolling) {\n    return;\n  }\n\n  isPolling = true;\n\n  const detailsMap = new Map<\n    Project[\"id\"],\n    { version: number; buildId: Build[\"id\"]; authToken: string | undefined }\n  >();\n\n  polling: for await (const command of pollCommands()) {\n    // Check if aborted\n    if (signal.aborted) {\n      isPolling = false;\n      break polling;\n    }\n\n    if (command.type === \"setDetails\") {\n      // At this moment we can be sure that all transactions for the command.projectId is already synchronized\n      // There are 2 options\n      // - Project opened again before transactions synchronized,\n      //   in that case project is outdated and we need to ask user to reload it.\n      // - Project opened after sync, everything is ok.\n      if (command.version < (detailsMap.get(command.projectId)?.version ?? 0)) {\n        const error =\n          \"The project is outdated. Synchronization was incomplete when the project was opened. \" +\n          \"Please reload the page to get the latest version.\";\n        const shouldReload = confirm(error);\n\n        if (shouldReload) {\n          location.reload();\n        }\n\n        // stop synchronization and wait til user reload\n        $queueStatus.set({ status: \"fatal\", error });\n\n        if (shouldReload === false) {\n          toast.error(\n            \"Synchronization has been paused. Please reload to continue.\",\n            { id: \"outdated-error\", duration: Number.POSITIVE_INFINITY }\n          );\n        }\n\n        break polling;\n      }\n\n      detailsMap.set(command.projectId, {\n        version: command.version,\n        buildId: command.buildId,\n        authToken: command.authToken,\n      });\n      continue;\n    }\n\n    const { projectId, transactions } = command;\n    const details = detailsMap.get(projectId);\n\n    if (details === undefined) {\n      const error =\n        \"Project details not found. Synchronization has been paused. Please reload to continue.\";\n\n      toast.error(error, {\n        id: \"details-error\",\n        duration: Number.POSITIVE_INFINITY,\n      });\n\n      $queueStatus.set({ status: \"fatal\", error });\n\n      return;\n    }\n\n    // We don't know how to handle api errors. Like parsing errors, etc.\n    // Let\n    let apiErrorCount = 0;\n\n    for await (const _ of retry()) {\n      // in case of any error continue retrying\n      try {\n        const headers = new Headers();\n        if (details.authToken) {\n          headers.append(\"x-auth-token\", details.authToken);\n        }\n        // revise patches are not used on the server and reduce possible patch size\n        const optimizedTransactions = transactions.map((transaction) => ({\n          ...transaction,\n          payload: transaction.payload.map((change) => ({\n            namespace: change.namespace,\n            patches: change.patches,\n          })),\n        }));\n        const response = await fetch(restPatchPath(), {\n          method: \"post\",\n          body: JSON.stringify({\n            transactions: optimizedTransactions,\n            buildId: details.buildId,\n            projectId,\n            // provide latest stored version to server\n            version: details.version,\n            headers,\n          }),\n        });\n\n        if (response.ok) {\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          const result = (await response.json()) as any;\n          if (result.status === \"ok\") {\n            details.version += 1;\n\n            // Notify all transaction completion callbacks\n            for (const transaction of transactions) {\n              const callbacks = transactionCallbacks.get(transaction.id);\n              if (callbacks) {\n                for (const callback of callbacks) {\n                  callback(true);\n                }\n                transactionCallbacks.delete(transaction.id);\n              }\n            }\n\n            // stop retrying and wait next transactions\n            continue polling;\n          }\n          // when versions mismatched ask user to reload\n          // user may cancel to copy own state before reloading\n          if (\n            result.status === \"version_mismatched\" ||\n            result.status === \"authorization_error\"\n          ) {\n            const error =\n              result.errors ?? \"Unknown version mismatch. Please reload.\";\n\n            const shouldReload = confirm(error);\n            if (shouldReload) {\n              location.reload();\n            }\n\n            $queueStatus.set({ status: \"fatal\", error });\n\n            if (shouldReload === false) {\n              toast.error(\n                \"Synchronization has been paused. Please reload to continue.\",\n                { duration: Number.POSITIVE_INFINITY }\n              );\n            }\n\n            // stop synchronization and wait til user reload\n            break polling;\n          }\n\n          if (apiErrorCount >= MAX_ALLOWED_API_ERRORS) {\n            const error = `Fatal error. ${\n              result.errors ?? JSON.stringify(result.errors)\n            } Synchronization has been paused.`;\n            // Api error we don't know how to handle, as retries will not help probably\n            // We should show error and break synchronization\n            $queueStatus.set({ status: \"fatal\", error });\n\n            toast.error(error, {\n              id: \"fatal-error\",\n              duration: Number.POSITIVE_INFINITY,\n            });\n\n            break polling;\n          }\n\n          apiErrorCount += 1;\n        } else {\n          // Various 500 responses, from proxies etc\n          // It's usually ok to be here, probably restorable with retries\n          const text = await response.text();\n          // To investigate some strange errors we have seen\n\n          console.info(`Non ok response: ${text}`);\n        }\n      } catch (error) {\n        if (navigator.onLine) {\n          // ERR_CONNECTION_REFUSED or like, probably restorable with retries\n          // anyway lets's log it\n\n          console.info(\n            error instanceof Error ? error.message : JSON.stringify(error)\n          );\n        }\n      }\n    }\n  }\n};\n\nexport class ServerSyncStorage implements SyncStorage {\n  name = \"server\";\n  private projectId: string;\n\n  constructor(projectId: string) {\n    this.projectId = projectId;\n  }\n\n  sendTransaction(transaction: Transaction<Change[]>) {\n    if (transaction.object === \"server\") {\n      $lastTransactionId.set(transaction.id);\n      commandQueue.enqueue({\n        type: \"transactions\",\n        transactions: [transaction],\n        projectId: this.projectId,\n      });\n    }\n  }\n  subscribe(setState: (state: unknown) => void, signal: AbortSignal) {\n    const projectId = this.projectId;\n    loadBuilderData({ projectId, signal })\n      .then((data) => {\n        const serverData = new Map(Object.entries(data));\n        setState(new Map([[\"server\", serverData]]));\n      })\n      .catch((err) => {\n        if (err instanceof Error) {\n          console.error(err);\n          return;\n        }\n\n        // Abort error do nothing\n      });\n  }\n}\n\n/**\n * Promisify idle state of the queue for a one-off notification when everything is saved.\n */\nexport const isSyncIdle = () => {\n  return new Promise<QueueStatus>((resolve, reject) => {\n    const handle = (status: QueueStatus) => {\n      if (status.status === \"idle\") {\n        resolve(status);\n        return true;\n      }\n      if (status.status === \"fatal\") {\n        reject(\n          new Error(\n            \"Synchronization is in fatal state. Please reload the page or check your internet connection.\"\n          )\n        );\n        return true;\n      }\n      return false;\n    };\n    const status = $queueStatus.get();\n\n    if (handle(status) === false) {\n      const unsubscribe = $queueStatus.subscribe((status) => {\n        if (handle(status)) {\n          unsubscribe();\n        }\n      });\n    }\n  });\n};\n\nexport const usePreventUnload = () => {\n  useEffect(() => {\n    const handler = (event: BeforeUnloadEvent) => {\n      const { status } = $queueStatus.get();\n      if (status === \"idle\" || status === \"fatal\") {\n        return;\n      }\n      event.preventDefault();\n    };\n    window.addEventListener(\"beforeunload\", handler);\n    return () => window.removeEventListener(\"beforeunload\", handler);\n  }, []);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/sync/sync-client.ts",
    "content": "import type { Project } from \"@webstudio-is/project\";\nimport type { AuthPermit } from \"@webstudio-is/trpc-interface/index.server\";\nimport { SyncClient } from \"~/shared/sync-client\";\nimport { registerContainers, createObjectPool } from \"./sync-stores\";\nimport {\n  ServerSyncStorage,\n  enqueueProjectDetails,\n  stopPolling,\n} from \"./project-queue\";\nimport { loadBuilderData } from \"~/shared/builder-data\";\nimport {\n  $project,\n  $pages,\n  $assets,\n  $instances,\n  $props,\n  $dataSources,\n  $resources,\n  $breakpoints,\n  $styleSources,\n  $styleSourceSelections,\n  $styles,\n  $marketplaceProduct,\n  $publisherHost,\n  resetDataStores,\n} from \"./data-stores\";\n\nlet client: SyncClient | undefined;\nlet currentProjectId: string | undefined;\n\n/**\n * Initialize the sync infrastructure and load project data.\n * Can be used from both the builder and dashboard contexts.\n */\nexport const initializeClientSync = ({\n  projectId,\n  authPermit,\n  authToken,\n  signal,\n  onReady,\n}: {\n  projectId: Project[\"id\"];\n  authPermit: AuthPermit;\n  authToken?: string;\n  signal: AbortSignal;\n  onReady?: () => void;\n}) => {\n  // Note: \"view\" permit will skip transaction synchronization\n\n  // Reset sync client if projectId changed\n  if (client && currentProjectId !== projectId) {\n    destroyClientSync();\n    client = undefined;\n  }\n\n  // Only register containers once and create sync client\n  if (!client) {\n    registerContainers();\n    client = new SyncClient({\n      role: \"leader\",\n      object: createObjectPool(),\n      storages: [new ServerSyncStorage(projectId)],\n    });\n    currentProjectId = projectId;\n  }\n\n  client.connect({\n    signal,\n    onReady() {\n      // Load builder data if we don't have it yet OR if projectId changed\n      const currentProjectInStore = $project.get()?.id;\n      const needsDataLoad =\n        !$pages.get() || currentProjectInStore !== projectId;\n\n      loadBuilderData({ projectId, signal })\n        .then((data) => {\n          if (needsDataLoad) {\n            // Set publisherHost from loaded data (needed for $publishedOrigin computed store)\n            $publisherHost.set(data.publisherHost);\n\n            // Set all the stores with loaded data\n            $project.set(data.project);\n            $pages.set(data.pages);\n            $assets.set(data.assets);\n            $instances.set(data.instances);\n            $props.set(data.props);\n            $dataSources.set(data.dataSources);\n            $resources.set(data.resources);\n            $breakpoints.set(data.breakpoints);\n            $styleSources.set(data.styleSources);\n            $styleSourceSelections.set(data.styleSourceSelections);\n            $styles.set(data.styles);\n            $marketplaceProduct.set(data.marketplaceProduct);\n          }\n\n          // Start project sync with build info from loaded data\n          if (authPermit !== \"view\") {\n            enqueueProjectDetails({\n              projectId,\n              buildId: data.id,\n              version: data.version,\n              authPermit,\n              authToken,\n            });\n          }\n\n          onReady?.();\n        })\n        .catch((error) => {\n          if (error.name !== \"AbortError\") {\n            console.error(\"Failed to load project data:\", error);\n          }\n        });\n    },\n  });\n};\n\n/**\n * Destroy sync client and reset all data stores.\n * Call this when closing the builder or switching between projects.\n */\nexport const destroyClientSync = () => {\n  resetDataStores();\n  stopPolling();\n};\n\nexport const getSyncClient = () => client;\n"
  },
  {
    "path": "apps/builder/app/shared/sync/sync-stores.ts",
    "content": "import { Store } from \"immerhin\";\nimport { enableMapSet, setAutoFreeze } from \"immer\";\nimport { useEffect } from \"react\";\nimport {\n  $project,\n  $pages,\n  $instances,\n  $props,\n  $dataSources,\n  $breakpoints,\n  $styles,\n  $styleSources,\n  $styleSourceSelections,\n  $assets,\n  $resources,\n  $marketplaceProduct,\n} from \"./data-stores\";\nimport {\n  $selectedPageHash,\n  $selectedInstanceSizes,\n  $selectedInstanceRenderState,\n  $hoveredInstanceSelector,\n  $authTokenPermissions,\n  $toastErrors,\n  $selectedStyleSources,\n  $selectedStyleState,\n  $dataSourceVariables,\n  $dragAndDropState,\n  $selectedInstanceStates,\n  $canvasIframeState,\n  $uploadingFilesDataStore,\n  $memoryProps,\n  $detectedFontsWeights,\n  $builderMode,\n  $selectedBreakpointId,\n  $textEditingInstanceSelector,\n  $textEditorContextMenu,\n  $textEditorContextMenuCommand,\n  $isResizingCanvas,\n  $collaborativeInstanceRect,\n  $collaborativeInstanceSelector,\n  $hoveredInstanceOutline,\n  $selectedInstanceOutline,\n  $blockChildOutline,\n  $textToolbar,\n  $gridCellData,\n  $registeredComponentMetas,\n  $registeredTemplates,\n  $modifierKeys,\n  $instanceContextMenu,\n} from \"~/shared/nano-states\";\nimport { $ephemeralStyles } from \"~/canvas/stores\";\nimport {\n  ImmerhinSyncObject,\n  NanostoresSyncObject,\n  SyncClient,\n  SyncObjectPool,\n  type SyncEmitter,\n} from \"../sync-client\";\nimport { $canvasScrollbarSize } from \"~/builder/shared/nano-states\";\nimport { $awareness, $temporaryInstances } from \"../awareness\";\nimport { $systemDataByPage } from \"../system\";\nimport { $resourcesCache } from \"../resources\";\n\nenableMapSet();\n// safari structuredClone fix\nsetAutoFreeze(false);\n\nexport const clientSyncStore = new Store();\nexport const serverSyncStore = new Store();\n\nexport const registerContainers = () => {\n  // synchronize patches\n  serverSyncStore.register(\"pages\", $pages);\n  serverSyncStore.register(\"breakpoints\", $breakpoints);\n  serverSyncStore.register(\"instances\", $instances);\n  serverSyncStore.register(\"styles\", $styles);\n  serverSyncStore.register(\"styleSources\", $styleSources);\n  serverSyncStore.register(\"styleSourceSelections\", $styleSourceSelections);\n  serverSyncStore.register(\"props\", $props);\n  serverSyncStore.register(\"dataSources\", $dataSources);\n  serverSyncStore.register(\"resources\", $resources);\n  serverSyncStore.register(\"assets\", $assets);\n  serverSyncStore.register(\"marketplaceProduct\", $marketplaceProduct);\n};\n\nexport const createObjectPool = () => {\n  return new SyncObjectPool([\n    new ImmerhinSyncObject(\"server\", serverSyncStore),\n    new ImmerhinSyncObject(\"client\", clientSyncStore),\n    new NanostoresSyncObject(\"awareness\", $awareness),\n    new NanostoresSyncObject(\"temporaryInstances\", $temporaryInstances),\n\n    new NanostoresSyncObject(\"project\", $project),\n    new NanostoresSyncObject(\"dataSourceVariables\", $dataSourceVariables),\n    new NanostoresSyncObject(\"resourcesCache\", $resourcesCache),\n    new NanostoresSyncObject(\"selectedPageHash\", $selectedPageHash),\n    new NanostoresSyncObject(\"selectedInstanceSizes\", $selectedInstanceSizes),\n    new NanostoresSyncObject(\n      \"selectedInstanceRenderState\",\n      $selectedInstanceRenderState\n    ),\n    new NanostoresSyncObject(\n      \"hoveredInstanceSelector\",\n      $hoveredInstanceSelector\n    ),\n    new NanostoresSyncObject(\"builderMode\", $builderMode),\n    new NanostoresSyncObject(\"authTokenPermissions\", $authTokenPermissions),\n    new NanostoresSyncObject(\"toastErrors\", $toastErrors),\n    new NanostoresSyncObject(\"selectedStyleSources\", $selectedStyleSources),\n    new NanostoresSyncObject(\"selectedStyleState\", $selectedStyleState),\n    new NanostoresSyncObject(\"dragAndDropState\", $dragAndDropState),\n    new NanostoresSyncObject(\"ephemeralStyles\", $ephemeralStyles),\n    new NanostoresSyncObject(\"selectedInstanceStates\", $selectedInstanceStates),\n    new NanostoresSyncObject(\"canvasIframeState\", $canvasIframeState),\n    new NanostoresSyncObject(\n      \"uploadingFilesDataStore\",\n      $uploadingFilesDataStore\n    ),\n    new NanostoresSyncObject(\"memoryProps\", $memoryProps),\n    new NanostoresSyncObject(\"detectedFontsWeights\", $detectedFontsWeights),\n    new NanostoresSyncObject(\"selectedBreakpointId\", $selectedBreakpointId),\n    new NanostoresSyncObject(\n      \"textEditingInstanceSelector\",\n      $textEditingInstanceSelector\n    ),\n    new NanostoresSyncObject(\"textEditorContextMenu\", $textEditorContextMenu),\n    new NanostoresSyncObject(\n      \"textEditorContextMenuCommand\",\n      $textEditorContextMenuCommand\n    ),\n    new NanostoresSyncObject(\"isResizingCanvas\", $isResizingCanvas),\n    new NanostoresSyncObject(\"textToolbar\", $textToolbar),\n    new NanostoresSyncObject(\n      \"selectedInstanceOutline\",\n      $selectedInstanceOutline\n    ),\n    new NanostoresSyncObject(\"hoveredInstanceOutline\", $hoveredInstanceOutline),\n    new NanostoresSyncObject(\"blockChildOutline\", $blockChildOutline),\n    new NanostoresSyncObject(\"instanceContextMenu\", $instanceContextMenu),\n    new NanostoresSyncObject(\"modifierKeys\", $modifierKeys),\n    new NanostoresSyncObject(\"gridCellData\", $gridCellData),\n    new NanostoresSyncObject(\n      \"collaborativeInstanceSelector\",\n      $collaborativeInstanceSelector\n    ),\n    new NanostoresSyncObject(\n      \"collaborativeInstanceRect\",\n      $collaborativeInstanceRect\n    ),\n    new NanostoresSyncObject(\n      \"registeredComponentMetas\",\n      $registeredComponentMetas\n    ),\n    new NanostoresSyncObject(\"registeredTemplates\", $registeredTemplates),\n    new NanostoresSyncObject(\"canvasScrollbarWidth\", $canvasScrollbarSize),\n    new NanostoresSyncObject(\"systemDataByPage\", $systemDataByPage),\n  ]);\n};\n\ndeclare global {\n  interface Window {\n    __webstudioSharedSyncEmitter__: SyncEmitter | undefined;\n  }\n}\n\n/**\n * prevent syncEmitter interception from embedded scripts on canvas\n * i.e., `globalThis.syncEmitter = () => console.log('INTERCEPTED');`,\n */\nconst sharedSyncEmitter =\n  typeof window === \"undefined\"\n    ? undefined\n    : window.__webstudioSharedSyncEmitter__;\nif (typeof window !== \"undefined\") {\n  delete window.__webstudioSharedSyncEmitter__;\n}\n\nexport const useCanvasStore = () => {\n  useEffect(() => {\n    const canvasClient = new SyncClient({\n      role: \"follower\",\n      object: createObjectPool(),\n      emitter: sharedSyncEmitter,\n    });\n\n    const controller = new AbortController();\n    canvasClient.connect({ signal: controller.signal });\n    return () => {\n      controller.abort();\n    };\n  }, []);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/sync-client.test.ts",
    "content": "import { describe, expect, test, vi } from \"vitest\";\nimport { createNanoEvents } from \"nanoevents\";\nimport { atom } from \"nanostores\";\nimport { Store } from \"immerhin\";\nimport { enableMapSet } from \"immer\";\nimport {\n  ImmerhinSyncObject,\n  NanostoresSyncObject,\n  SyncClient,\n  SyncObjectPool,\n  type SyncStorage,\n} from \"./sync-client\";\n\nenableMapSet();\n\nconst createFollowerStore = () => {\n  const followerStore = new Store();\n  followerStore.register(\"users\", atom(new Map()));\n  return followerStore;\n};\n\nconst createLeaderStore = () => {\n  const leaderStore = new Store();\n  leaderStore.register(\n    \"users\",\n    atom(\n      new Map([\n        [\"john\", \"John Johnson\"],\n        [\"frank\", \"Frank Frankson\"],\n      ])\n    )\n  );\n  return leaderStore;\n};\n\ntest(\"synchronize initial state when follower is connected\", () => {\n  const emitter = createNanoEvents();\n  const leaderStore = createLeaderStore();\n  const followerStore = createFollowerStore();\n  const leader = new SyncClient({\n    role: \"leader\",\n    object: new ImmerhinSyncObject(\"my-data\", leaderStore),\n    emitter,\n  });\n  const follower = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", followerStore),\n    emitter,\n  });\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(0);\n  expect(leaderStore.containers.get(\"users\")?.get()).not.toEqual(\n    followerStore.containers.get(\"users\")?.get()\n  );\n  // first leader and follow is connected later\n  leader.connect({ signal: new AbortController().signal });\n  follower.connect({ signal: new AbortController().signal });\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(leaderStore.containers.get(\"users\")?.get()).toEqual(\n    followerStore.containers.get(\"users\")?.get()\n  );\n});\n\ntest(\"synchronize initial state when leader is connected\", () => {\n  const emitter = createNanoEvents();\n  const leaderStore = createLeaderStore();\n  const followerStore = createFollowerStore();\n  const leader = new SyncClient({\n    role: \"leader\",\n    object: new ImmerhinSyncObject(\"my-data\", leaderStore),\n    emitter,\n  });\n  const follower = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", followerStore),\n    emitter,\n  });\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(0);\n  expect(leaderStore.containers.get(\"users\")?.get()).not.toEqual(\n    followerStore.containers.get(\"users\")?.get()\n  );\n  // first follower and leader is connected later\n  follower.connect({ signal: new AbortController().signal });\n  leader.connect({ signal: new AbortController().signal });\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(leaderStore.containers.get(\"users\")?.get()).toEqual(\n    followerStore.containers.get(\"users\")?.get()\n  );\n});\n\ntest(\"synchronize initial state when one of followers become leader\", () => {\n  const emitter = createNanoEvents();\n  const leaderStore = createLeaderStore();\n  const followerStore = createFollowerStore();\n  const leader = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", leaderStore),\n    emitter,\n  });\n  const follower = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", followerStore),\n    emitter,\n  });\n  leader.connect({ signal: new AbortController().signal });\n  follower.connect({ signal: new AbortController().signal });\n  expect(leader.role).toEqual(\"follower\");\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(0);\n  leader.lead();\n  expect(leader.role).toEqual(\"leader\");\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(leaderStore.containers.get(\"users\")?.get()).toEqual(\n    followerStore.containers.get(\"users\")?.get()\n  );\n});\n\ntest(\"exchange transactions between leader and follower\", async () => {\n  const emitter = createNanoEvents();\n  const leaderStore = createLeaderStore();\n  const followerStore = createFollowerStore();\n  const leader = new SyncClient({\n    role: \"leader\",\n    object: new ImmerhinSyncObject(\"my-data\", leaderStore),\n    emitter,\n  });\n  const follower = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", followerStore),\n    emitter,\n  });\n  leader.connect({ signal: new AbortController().signal });\n  follower.connect({ signal: new AbortController().signal });\n  leaderStore.createTransaction(\n    [leaderStore.containers.get(\"users\")!],\n    (users) => {\n      users.set(\"james\", \"James Jameson\");\n    }\n  );\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(3);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(3);\n  followerStore.createTransaction(\n    [followerStore.containers.get(\"users\")!],\n    (users) => {\n      users.set(\"mark\", \"Mark Grayson\");\n    }\n  );\n  expect(leaderStore.containers.get(\"users\")?.get().size).toEqual(4);\n  expect(followerStore.containers.get(\"users\")?.get().size).toEqual(4);\n  expect(leaderStore.containers.get(\"users\")?.get()).toEqual(\n    followerStore.containers.get(\"users\")?.get()\n  );\n});\n\ntest(\"exchange transactions between followers\", async () => {\n  const emitter = createNanoEvents();\n  const follower1Store = createFollowerStore();\n  const follower2Store = createFollowerStore();\n  const follower1 = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", follower1Store),\n    emitter,\n  });\n  const follower2 = new SyncClient({\n    role: \"follower\",\n    object: new ImmerhinSyncObject(\"my-data\", follower2Store),\n    emitter,\n  });\n  follower1.connect({ signal: new AbortController().signal });\n  follower2.connect({ signal: new AbortController().signal });\n  follower1Store.createTransaction(\n    [follower1Store.containers.get(\"users\")!],\n    (users) => {\n      users.set(\"james\", \"James Jameson\");\n    }\n  );\n  expect(follower1Store.containers.get(\"users\")?.get().size).toEqual(1);\n  expect(follower2Store.containers.get(\"users\")?.get().size).toEqual(1);\n  follower2Store.createTransaction(\n    [follower2Store.containers.get(\"users\")!],\n    (users) => {\n      users.set(\"mark\", \"Mark Grayson\");\n    }\n  );\n  expect(follower1Store.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(follower2Store.containers.get(\"users\")?.get().size).toEqual(2);\n  expect(follower1Store.containers.get(\"users\")?.get()).toEqual(\n    follower2Store.containers.get(\"users\")?.get()\n  );\n});\n\ntest(\"support pool of objects\", () => {\n  const store1 = createFollowerStore();\n  const store2 = createFollowerStore();\n  const leader = new SyncClient({\n    role: \"leader\",\n    object: new SyncObjectPool([\n      new ImmerhinSyncObject(\"store1\", store1),\n      new ImmerhinSyncObject(\"store2\", store2),\n    ]),\n  });\n  leader.connect({ signal: new AbortController().signal });\n  store1.createTransaction([store1.containers.get(\"users\")!], (users) => {\n    users.set(\"james\", \"James Jameson\");\n  });\n  store2.createTransaction([store2.containers.get(\"users\")!], (users) => {\n    users.set(\"mark\", \"Mark Grayson\");\n  });\n  expect(store1.containers.get(\"users\")?.get()).toEqual(\n    new Map([[\"james\", \"James Jameson\"]])\n  );\n  expect(store2.containers.get(\"users\")?.get()).toEqual(\n    new Map([[\"mark\", \"Mark Grayson\"]])\n  );\n});\n\ntest(\"merge state in pool object to partially restore from storage\", () => {\n  const store1 = atom(0);\n  const store2 = atom(1);\n  const objectPool = new SyncObjectPool([\n    new NanostoresSyncObject(\"store1\", store1),\n    new NanostoresSyncObject(\"store2\", store2),\n  ]);\n  objectPool.setState(new Map([[\"store1\", 2]]));\n  expect(store1.get()).toEqual(2);\n  expect(store2.get()).toEqual(1);\n});\n\ndescribe(\"nanostores sync object\", () => {\n  test(\"sync initial state and exchange transactions\", () => {\n    const emitter = createNanoEvents();\n    const $leader = atom(1);\n    const $follower = atom(2);\n    new SyncClient({\n      role: \"leader\",\n      object: new NanostoresSyncObject(\"my-data\", $leader),\n      emitter,\n    }).connect({ signal: new AbortController().signal });\n    new SyncClient({\n      role: \"follower\",\n      object: new NanostoresSyncObject(\"my-data\", $follower),\n      emitter,\n    }).connect({ signal: new AbortController().signal });\n    expect($leader.get()).toEqual(1);\n    expect($follower.get()).toEqual(1);\n    $leader.set(3);\n    expect($leader.get()).toEqual(3);\n    expect($follower.get()).toEqual(3);\n    $follower.set(5);\n    expect($leader.get()).toEqual(5);\n    expect($follower.get()).toEqual(5);\n  });\n\n  test(\"set received state and access it\", () => {\n    const $store = atom(1);\n    const object = new NanostoresSyncObject(\"nanostores\", $store);\n    expect($store.get()).toEqual(1);\n    expect(object.getState()).toEqual(1);\n    object.setState(2);\n    expect($store.get()).toEqual(2);\n    expect(object.getState()).toEqual(2);\n  });\n\n  test(\"send new store value as transaction\", () => {\n    const $store = atom(1);\n    const object = new NanostoresSyncObject(\"nanostores\", $store);\n    const sendTransaction = vi.fn();\n    object.subscribe(sendTransaction, new AbortController().signal);\n    $store.set(2);\n    expect(sendTransaction).toBeCalledTimes(1);\n    expect(sendTransaction).toBeCalledWith({\n      id: expect.any(String),\n      object: \"nanostores\",\n      payload: 2,\n    });\n  });\n\n  test(\"prevent sending back received state\", () => {\n    const $store = atom(1);\n    const object = new NanostoresSyncObject(\"nanostores\", $store);\n    const sendTransaction = vi.fn();\n    object.subscribe(sendTransaction, new AbortController().signal);\n    expect(sendTransaction).toBeCalledTimes(0);\n    object.setState(2);\n    expect(sendTransaction).toBeCalledTimes(0);\n  });\n\n  test(\"prevent sending back applied transactions\", () => {\n    const $store = atom(1);\n    const object = new NanostoresSyncObject(\"nanostores\", $store);\n    const sendTransaction = vi.fn();\n    object.subscribe(sendTransaction, new AbortController().signal);\n    expect(sendTransaction).toBeCalledTimes(0);\n    object.applyTransaction({\n      id: \"my-transaction\",\n      object: \"nanostores\",\n      payload: 2,\n    });\n    expect(sendTransaction).toBeCalledTimes(0);\n  });\n});\n\ndescribe(\"storages\", () => {\n  class TestStorage implements SyncStorage {\n    name = \"TestStorage\";\n    value: undefined | number;\n    constructor(value: undefined | number) {\n      this.value = value;\n    }\n    sendTransaction = vi.fn();\n    subscribe(setState: (state: unknown) => void) {\n      setState(this.value);\n    }\n  }\n\n  test(\"get initial state from storage\", () => {\n    const $store = atom(0);\n    const client = new SyncClient({\n      role: \"leader\",\n      object: new NanostoresSyncObject(\"nanostores\", $store),\n      storages: [new TestStorage(1)],\n    });\n    client.connect({ signal: new AbortController().signal });\n    expect($store.get()).toEqual(1);\n  });\n\n  test(\"fallback to current state when storage is empty\", () => {\n    const $store = atom(0);\n    const client = new SyncClient({\n      role: \"leader\",\n      object: new NanostoresSyncObject(\"nanostores\", $store),\n      storages: [new TestStorage(undefined)],\n    });\n    client.connect({ signal: new AbortController().signal });\n    expect($store.get()).toEqual(0);\n  });\n\n  test(\"get state from the first non-empty storage\", () => {\n    const $store = atom(0);\n    const client = new SyncClient({\n      role: \"leader\",\n      object: new NanostoresSyncObject(\"nanostores\", $store),\n      storages: [\n        new TestStorage(undefined),\n        new TestStorage(1),\n        new TestStorage(2),\n      ],\n    });\n    client.connect({ signal: new AbortController().signal });\n    expect($store.get()).toEqual(1);\n  });\n\n  test(\"send transactions to all provided storages\", () => {\n    const $store = atom(0);\n    const storage1 = new TestStorage(undefined);\n    const storage2 = new TestStorage(undefined);\n    const client = new SyncClient({\n      role: \"leader\",\n      object: new NanostoresSyncObject(\"nanostores\", $store),\n      storages: [storage1, storage2],\n    });\n    client.connect({ signal: new AbortController().signal });\n    $store.set(1);\n    expect(storage1.sendTransaction).toBeCalledTimes(1);\n    expect(storage1.sendTransaction).toHaveBeenCalledWith({\n      id: expect.any(String),\n      object: \"nanostores\",\n      payload: 1,\n    });\n    expect(storage2.sendTransaction).toBeCalledTimes(1);\n    expect(storage2.sendTransaction).toHaveBeenCalledWith({\n      id: expect.any(String),\n      object: \"nanostores\",\n      payload: 1,\n    });\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/sync-client.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { createNanoEvents, type Emitter } from \"nanoevents\";\nimport type { Change, Store } from \"immerhin\";\nimport type { WritableAtom } from \"nanostores\";\n\nexport type Transaction<Payload = unknown> = {\n  id: string;\n  object: string;\n  payload: Payload;\n};\n\nexport type RevertedTransaction = {\n  id: string;\n  object: string;\n};\n\nexport interface SyncStorage {\n  name: string;\n  sendTransaction(transaction: Transaction): void;\n  subscribe(setState: (state: unknown) => void, signal: AbortSignal): void;\n}\n\nconst subscribeStorages = (\n  storages: SyncStorage[],\n  setState: (state: unknown) => void,\n  signal: AbortSignal\n) => {\n  if (storages.length === 0) {\n    setState(undefined);\n    return;\n  }\n  storages[0].subscribe((value) => {\n    if (value === undefined) {\n      // invoke the next storage\n      subscribeStorages(storages.slice(1), setState, signal);\n      return;\n    }\n    setState(value);\n  }, signal);\n};\n\ninterface SyncObject {\n  name: string;\n  getState(): unknown;\n  setState(state: unknown): void;\n  applyTransaction(transaction: Transaction): void;\n  revertTransaction(transaction: RevertedTransaction): void;\n  subscribe(\n    sendTransaction: (transaction: Transaction) => void,\n    signal: AbortSignal\n  ): void;\n}\n\nexport class ImmerhinSyncObject implements SyncObject {\n  name: string;\n  store: Store;\n  constructor(name: string, store: Store) {\n    this.name = name;\n    this.store = store;\n  }\n  getState() {\n    const state = new Map<string, unknown>();\n    for (const [namespace, $store] of this.store.containers) {\n      state.set(namespace, $store.get());\n    }\n    return state;\n  }\n  setState(state: Map<string, unknown>) {\n    for (const [namespace, $store] of this.store.containers) {\n      // catch errors triggered by CSP configuration when user put iframe onto canvas\n      try {\n        // Immer cannot handle Map instances from another realm.\n        // Use `clone` to recreate the data with the current realm's classes.\n        // This works because the structured clone algorithm skips prototype chains; classes must be defined in both realms.\n        $store.set(structuredClone(state.get(namespace)));\n      } catch (error) {\n        console.error(error);\n      }\n    }\n  }\n  applyTransaction(transaction: Transaction<Change[]>) {\n    this.store.addTransaction(transaction.id, transaction.payload, \"remote\");\n  }\n  revertTransaction(transaction: RevertedTransaction) {\n    this.store.revertTransaction(transaction.id);\n  }\n  subscribe(\n    sendTransaction: (transaction: Transaction<Change[]>) => void,\n    signal: AbortSignal\n  ) {\n    const unsubscribe = this.store.subscribe((id, payload, source) => {\n      if (source === \"remote\") {\n        return;\n      }\n      sendTransaction({ id, object: this.name, payload });\n    });\n    signal.addEventListener(\"abort\", unsubscribe);\n  }\n}\n\nexport class NanostoresSyncObject implements SyncObject {\n  name: string;\n  store: WritableAtom<unknown>;\n  // track the source of \"store.set\" to avoid cyclic updates\n  operation: \"local\" | \"state\" | \"add\" = \"local\";\n  constructor(name: string, store: WritableAtom<unknown>) {\n    this.name = name;\n    this.store = store;\n  }\n  getState() {\n    return this.store.get();\n  }\n  setState(state: unknown) {\n    this.operation = \"state\";\n    this.store.set(state);\n    this.operation = \"local\";\n  }\n  applyTransaction(transaction: Transaction) {\n    this.operation = \"add\";\n    // catch errors triggered by CSP configuration when user put iframe onto canvas\n    try {\n      // `instanceof` checks do not work with instances like Map, File, etc., from another realm.\n      // Use `clone` to recreate the data with the current realm's classes.\n      // This works because the structured clone algorithm skips prototype chains; classes must be defined in both realms.\n      this.store.set(structuredClone(transaction.payload));\n    } catch (error) {\n      console.error(error);\n    }\n    this.operation = \"local\";\n  }\n  revertTransaction(_transaction: RevertedTransaction) {\n    // @todo store the list of transactions\n  }\n  subscribe(\n    sendTransaction: (transaction: Transaction) => void,\n    signal: AbortSignal\n  ) {\n    const unsubscribe = this.store.listen((payload) => {\n      if (this.operation !== \"local\") {\n        return;\n      }\n      const transaction = { id: nanoid(), object: this.name, payload };\n      sendTransaction(transaction);\n    });\n    signal.addEventListener(\"abort\", unsubscribe);\n  }\n}\n\nexport class SyncObjectPool implements SyncObject {\n  name = \"SyncObjectPool\";\n  objects = new Map<string, SyncObject>();\n  constructor(objects: SyncObject[]) {\n    for (const object of objects) {\n      this.objects.set(object.name, object);\n    }\n  }\n  getState() {\n    const state = new Map<string, unknown>();\n    for (const [name, object] of this.objects) {\n      state.set(name, object.getState());\n    }\n    return state;\n  }\n  setState(state: Map<string, unknown>) {\n    for (const [name, object] of this.objects) {\n      // merge into existing state to partially restore from storage\n      if (state.has(name)) {\n        object.setState(state.get(name) as never);\n      }\n    }\n  }\n  applyTransaction(transaction: Transaction) {\n    this.objects.get(transaction.object)?.applyTransaction(transaction);\n  }\n  revertTransaction(transaction: RevertedTransaction) {\n    this.objects.get(transaction.object)?.revertTransaction(transaction);\n  }\n  subscribe(\n    sendTransaction: (transaction: Transaction) => void,\n    signal: AbortSignal\n  ) {\n    for (const object of this.objects.values()) {\n      object.subscribe(sendTransaction, signal);\n    }\n  }\n}\n\ntype SyncMessage =\n  | { type: \"connect\"; clientId: string }\n  | { type: \"state\"; clientId: string; state: unknown }\n  | { type: \"apply\"; clientId: string; transaction: Transaction }\n  | { type: \"revert\"; clientId: string; transaction: RevertedTransaction };\n\nexport type SyncEmitter = Emitter<{\n  message: (message: SyncMessage) => void;\n}>;\n\ntype SyncClientOptions = {\n  role: \"leader\" | \"follower\";\n  object: SyncObject;\n  emitter?: SyncEmitter;\n  storages?: SyncStorage[];\n};\n\nexport class SyncClient {\n  clientId = nanoid();\n  role: SyncClientOptions[\"role\"];\n  object: SyncObject;\n  storages: SyncStorage[];\n  emitter: SyncEmitter;\n  connection: \"disconnected\" | \"connecting\" | \"connected\" = \"disconnected\";\n\n  constructor(options: SyncClientOptions) {\n    this.role = options.role;\n    this.object = options.object;\n    this.storages = options.storages ?? [];\n    this.emitter = options.emitter ?? createNanoEvents();\n  }\n\n  lead() {\n    this.role = \"leader\";\n    this.emitter.emit(\"message\", {\n      clientId: this.clientId,\n      type: \"state\",\n      state: this.object.getState(),\n    });\n  }\n\n  connect({ signal, onReady }: { signal: AbortSignal; onReady?: () => void }) {\n    const off = this.emitter.on(\"message\", (message) => {\n      // ignore own messages\n      if (this.clientId === message.clientId) {\n        return;\n      }\n      if (message.type === \"connect\") {\n        if (this.role !== \"leader\") {\n          return;\n        }\n        this.emitter.emit(\"message\", {\n          clientId: this.clientId,\n          type: \"state\",\n          state: this.object.getState(),\n        });\n      }\n      if (message.type === \"state\") {\n        if (this.connection === \"connected\") {\n          return;\n        }\n        this.connection = \"connected\";\n        this.object.setState(message.state);\n      }\n      if (message.type === \"apply\") {\n        /*\n        // leader interacts with server\n        // and should validate transactions from various actors\n        if (this.role === \"leader\" && invalid(event.transaction)) {\n          this.emitter.emit('revert', { clientId, transactions: [transaction.id] })\n          return;\n        }\n        */\n        this.object.applyTransaction(message.transaction);\n        if (this.role === \"leader\") {\n          for (const storage of this.storages) {\n            storage.sendTransaction(message.transaction);\n          }\n        }\n      }\n      if (message.type === \"revert\") {\n        this.object.revertTransaction(message.transaction);\n      }\n    });\n    signal.addEventListener(\"abort\", off);\n\n    this.object.subscribe((transaction) => {\n      this.emitter.emit(\"message\", {\n        clientId: this.clientId,\n        type: \"apply\",\n        transaction,\n      });\n      if (this.role === \"leader\") {\n        for (const storage of this.storages) {\n          storage.sendTransaction(transaction);\n        }\n      }\n    }, signal);\n\n    this.emitter.emit(\"message\", {\n      clientId: this.clientId,\n      type: \"connect\",\n    });\n    if (this.connection === \"disconnected\") {\n      this.connection = \"connecting\";\n    }\n    if (this.role === \"leader\") {\n      subscribeStorages(\n        this.storages,\n        (state) => {\n          // fallback to default object state\n          state ??= this.object.getState();\n          this.object.setState(state);\n          this.emitter.emit(\"message\", {\n            clientId: this.clientId,\n            type: \"state\",\n            state,\n          });\n          onReady?.();\n        },\n        signal\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "apps/builder/app/shared/system.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport type { Page, Pages } from \"@webstudio-is/sdk\";\nimport { $pages } from \"~/shared/sync/data-stores\";\nimport { registerContainers } from \"~/shared/sync/sync-stores\";\nimport { updateCurrentSystem } from \"./system\";\nimport { selectPage } from \"./awareness\";\n\nregisterContainers();\n\nconst getInitialPages = (page: Page): Pages => ({\n  folders: [\n    {\n      id: \"rootId\",\n      name: \"\",\n      slug: \"\",\n      children: [\"homeId\", \"dynamicId\"],\n    },\n  ],\n  homePage: {\n    id: \"homeId\",\n    path: \"\",\n    name: \"\",\n    title: \"\",\n    meta: {},\n    rootInstanceId: \"\",\n  },\n  pages: [page],\n});\n\ndescribe(\"history\", () => {\n  test(\"add new path at the start\", () => {\n    $pages.set(\n      getInitialPages({\n        id: \"dynamicId\",\n        path: \"/blog/:date/post/:slug\",\n        name: \"\",\n        title: \"\",\n        meta: {},\n        rootInstanceId: \"\",\n      })\n    );\n    selectPage(\"dynamicId\");\n    updateCurrentSystem({\n      params: { date: \"my-date\", slug: \"my-slug\" },\n    });\n    expect($pages.get()?.pages[0].history).toEqual([\n      \"/blog/my-date/post/my-slug\",\n    ]);\n    updateCurrentSystem({\n      params: { date: \"another-date\", slug: \"another-slug\" },\n    });\n    expect($pages.get()?.pages[0].history).toEqual([\n      \"/blog/another-date/post/another-slug\",\n      \"/blog/my-date/post/my-slug\",\n    ]);\n  });\n\n  test(\"move existing path to the start\", () => {\n    $pages.set(\n      getInitialPages({\n        id: \"dynamicId\",\n        path: \"/blog/:date/post/:slug\",\n        name: \"\",\n        title: \"\",\n        meta: {},\n        rootInstanceId: \"\",\n        history: [\n          \"/blog/another-date/post/another-slug\",\n          \"/blog/my-date/post/my-slug\",\n        ],\n      })\n    );\n    selectPage(\"dynamicId\");\n    updateCurrentSystem({\n      params: { date: \"my-date\", slug: \"my-slug\" },\n    });\n    expect($pages.get()?.pages[0].history).toEqual([\n      \"/blog/my-date/post/my-slug\",\n      \"/blog/another-date/post/another-slug\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "apps/builder/app/shared/system.ts",
    "content": "import { atom, computed } from \"nanostores\";\nimport { findPageByIdOrPath, type Page, type System } from \"@webstudio-is/sdk\";\nimport {\n  compilePathnamePattern,\n  matchPathnamePattern,\n  tokenizePathnamePattern,\n} from \"~/builder/shared/url-pattern\";\nimport { $selectedPage } from \"./awareness\";\nimport { $pages, $publishedOrigin } from \"./nano-states\";\nimport { serverSyncStore } from \"./sync/sync-stores\";\n\nexport const $systemDataByPage = atom(\n  new Map<Page[\"id\"], Pick<System, \"search\" | \"params\">>()\n);\n\nconst extractParams = (pattern: string, path?: string) => {\n  const params: System[\"params\"] = {};\n  const tokens = tokenizePathnamePattern(pattern);\n  // try to match the first item in history to let user\n  // see the page without manually entering params\n  // or selecting them in address bar\n  const matchedParams = path ? matchPathnamePattern(pattern, path) : undefined;\n  for (const token of tokens) {\n    if (token.type === \"param\") {\n      params[token.name] = matchedParams?.[token.name] ?? undefined;\n    }\n  }\n  return params;\n};\n\nexport const $currentSystem = computed(\n  [$publishedOrigin, $selectedPage, $systemDataByPage],\n  (origin, page, systemByPage) => {\n    const system: System = {\n      search: {},\n      params: {},\n      pathname: \"/\",\n      origin,\n    };\n    if (page === undefined) {\n      return system;\n    }\n    const systemData = systemByPage.get(page.id);\n    const extractedParams = extractParams(page.path, page.history?.[0]);\n    const params = { ...extractedParams, ...systemData?.params };\n    const pathname = compilePath(page.path, params) || \"/\";\n    return {\n      search: { ...system.search, ...systemData?.search },\n      params,\n      pathname,\n      origin,\n    };\n  }\n);\n\nconst compilePath = (pattern: string, params: System[\"params\"]) => {\n  const tokens = tokenizePathnamePattern(pattern);\n  return compilePathnamePattern(tokens, params);\n};\n\n/**\n * put new path into the beginning of history\n * and drop paths in the end when exceeded 20\n */\nconst savePathInHistory = (pageId: string, path: string) => {\n  serverSyncStore.createTransaction([$pages], (pages) => {\n    if (pages === undefined) {\n      return;\n    }\n    const page = findPageByIdOrPath(pageId, pages);\n    if (page === undefined) {\n      return;\n    }\n    const history = Array.from(page.history ?? []);\n    history.unshift(path);\n    page.history = Array.from(new Set(history)).slice(0, 20);\n  });\n};\n\nexport const updateCurrentSystem = (\n  update: Partial<Pick<System, \"search\" | \"params\">>\n) => {\n  const page = $selectedPage.get();\n  if (page === undefined) {\n    return;\n  }\n  const systemDataByPage = new Map($systemDataByPage.get());\n  const systemData = systemDataByPage.get(page.id);\n  const search = update.search ?? systemData?.search ?? {};\n  const params = update.params ?? systemData?.params ?? {};\n  systemDataByPage.set(page.id, { search, params });\n  $systemDataByPage.set(systemDataByPage);\n  savePathInHistory(page.id, compilePath(page.path, params));\n};\n"
  },
  {
    "path": "apps/builder/app/shared/tailwind/__generated__/preflight.ts",
    "content": "import type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\n\nexport const preflight: Record<\n  string,\n  undefined | { property: CssProperty; value: StyleValue }[]\n> = {\n  html: [\n    {\n      property: \"line-height\",\n      value: { type: \"unit\", unit: \"number\", value: 1.5 },\n    },\n    {\n      property: \"text-size-adjust\",\n      value: { type: \"unit\", unit: \"%\", value: 100 },\n    },\n    { property: \"tab-size\", value: { type: \"unit\", unit: \"number\", value: 4 } },\n    {\n      property: \"font-family\",\n      value: {\n        type: \"fontFamily\",\n        value: [\n          \"ui-sans-serif\",\n          \"system-ui\",\n          \"sans-serif\",\n          \"Apple Color Emoji\",\n          \"Segoe UI Emoji\",\n          \"Segoe UI Symbol\",\n          \"Noto Color Emoji\",\n        ],\n      },\n    },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    {\n      property: \"-webkit-tap-highlight-color\",\n      value: { type: \"keyword\", value: \"transparent\" },\n    },\n  ],\n  body: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    { property: \"line-height\", value: { type: \"keyword\", value: \"inherit\" } },\n  ],\n  hr: [\n    { property: \"height\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"border-top-width\",\n      value: { type: \"unit\", unit: \"px\", value: 1 },\n    },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  h1: [\n    { property: \"font-size\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  h2: [\n    { property: \"font-size\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  h3: [\n    { property: \"font-size\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  h4: [\n    { property: \"font-size\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  h5: [\n    { property: \"font-size\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  h6: [\n    { property: \"font-size\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  a: [\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"text-decoration-line\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"text-decoration-style\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"text-decoration-color\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n  ],\n  b: [{ property: \"font-weight\", value: { type: \"keyword\", value: \"bolder\" } }],\n  strong: [\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"bolder\" } },\n  ],\n  code: [\n    {\n      property: \"font-family\",\n      value: {\n        type: \"fontFamily\",\n        value: [\n          \"ui-monospace\",\n          \"SFMono-Regular\",\n          \"Menlo\",\n          \"Monaco\",\n          \"Consolas\",\n          \"Liberation Mono\",\n          \"Courier New\",\n          \"monospace\",\n        ],\n      },\n    },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"em\", value: 1 } },\n  ],\n  kbd: [\n    {\n      property: \"font-family\",\n      value: {\n        type: \"fontFamily\",\n        value: [\n          \"ui-monospace\",\n          \"SFMono-Regular\",\n          \"Menlo\",\n          \"Monaco\",\n          \"Consolas\",\n          \"Liberation Mono\",\n          \"Courier New\",\n          \"monospace\",\n        ],\n      },\n    },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"em\", value: 1 } },\n  ],\n  samp: [\n    {\n      property: \"font-family\",\n      value: {\n        type: \"fontFamily\",\n        value: [\n          \"ui-monospace\",\n          \"SFMono-Regular\",\n          \"Menlo\",\n          \"Monaco\",\n          \"Consolas\",\n          \"Liberation Mono\",\n          \"Courier New\",\n          \"monospace\",\n        ],\n      },\n    },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"em\", value: 1 } },\n  ],\n  pre: [\n    {\n      property: \"font-family\",\n      value: {\n        type: \"fontFamily\",\n        value: [\n          \"ui-monospace\",\n          \"SFMono-Regular\",\n          \"Menlo\",\n          \"Monaco\",\n          \"Consolas\",\n          \"Liberation Mono\",\n          \"Courier New\",\n          \"monospace\",\n        ],\n      },\n    },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"normal\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"em\", value: 1 } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  small: [\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 80 } },\n  ],\n  sub: [\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 75 } },\n    {\n      property: \"line-height\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    { property: \"position\", value: { type: \"keyword\", value: \"relative\" } },\n    {\n      property: \"vertical-align\",\n      value: { type: \"keyword\", value: \"baseline\" },\n    },\n    { property: \"bottom\", value: { type: \"unit\", unit: \"em\", value: -0.25 } },\n  ],\n  sup: [\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 75 } },\n    {\n      property: \"line-height\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    { property: \"position\", value: { type: \"keyword\", value: \"relative\" } },\n    {\n      property: \"vertical-align\",\n      value: { type: \"keyword\", value: \"baseline\" },\n    },\n    { property: \"top\", value: { type: \"unit\", unit: \"em\", value: -0.5 } },\n  ],\n  table: [\n    {\n      property: \"text-indent\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"border-top-color\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"border-right-color\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"border-bottom-color\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"border-left-color\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"border-collapse\",\n      value: { type: \"keyword\", value: \"collapse\" },\n    },\n  ],\n  button: [\n    { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"line-height\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    { property: \"text-transform\", value: { type: \"keyword\", value: \"none\" } },\n    { property: \"appearance\", value: { type: \"keyword\", value: \"button\" } },\n    {\n      property: \"background-color\",\n      value: { type: \"keyword\", value: \"transparent\" },\n    },\n    {\n      property: \"background-image\",\n      value: { type: \"layers\", value: [{ type: \"keyword\", value: \"none\" }] },\n    },\n    { property: \"cursor\", value: { type: \"keyword\", value: \"pointer\" } },\n  ],\n  input: [\n    { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"line-height\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  optgroup: [\n    { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"line-height\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  select: [\n    { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"line-height\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    { property: \"text-transform\", value: { type: \"keyword\", value: \"none\" } },\n  ],\n  textarea: [\n    { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"font-feature-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    {\n      property: \"font-variation-settings\",\n      value: { type: \"keyword\", value: \"inherit\" },\n    },\n    { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"font-weight\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"line-height\", value: { type: \"keyword\", value: \"inherit\" } },\n    { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    { property: \"resize\", value: { type: \"keyword\", value: \"vertical\" } },\n  ],\n  progress: [\n    {\n      property: \"vertical-align\",\n      value: { type: \"keyword\", value: \"baseline\" },\n    },\n  ],\n  summary: [\n    { property: \"display\", value: { type: \"keyword\", value: \"list-item\" } },\n  ],\n  blockquote: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  dl: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  dd: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  figure: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  p: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  fieldset: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  legend: [\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  ol: [\n    {\n      property: \"list-style-position\",\n      value: { type: \"keyword\", value: \"outside\" },\n    },\n    { property: \"list-style-image\", value: { type: \"keyword\", value: \"none\" } },\n    { property: \"list-style-type\", value: { type: \"keyword\", value: \"none\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  ul: [\n    {\n      property: \"list-style-position\",\n      value: { type: \"keyword\", value: \"outside\" },\n    },\n    { property: \"list-style-image\", value: { type: \"keyword\", value: \"none\" } },\n    { property: \"list-style-type\", value: { type: \"keyword\", value: \"none\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  menu: [\n    {\n      property: \"list-style-position\",\n      value: { type: \"keyword\", value: \"outside\" },\n    },\n    { property: \"list-style-image\", value: { type: \"keyword\", value: \"none\" } },\n    { property: \"list-style-type\", value: { type: \"keyword\", value: \"none\" } },\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  dialog: [\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", unit: \"number\", value: 0 },\n    },\n  ],\n  img: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n    { property: \"max-width\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"height\", value: { type: \"keyword\", value: \"auto\" } },\n  ],\n  video: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n    { property: \"max-width\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n    { property: \"height\", value: { type: \"keyword\", value: \"auto\" } },\n  ],\n  canvas: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n  ],\n  audio: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n  ],\n  iframe: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n  ],\n  embed: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n  ],\n  object: [\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n    { property: \"vertical-align\", value: { type: \"keyword\", value: \"middle\" } },\n  ],\n};\n"
  },
  {
    "path": "apps/builder/app/shared/tailwind/preflight-bin.ts",
    "content": "import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { parseCss } from \"@webstudio-is/css-data\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport { tags } from \"@webstudio-is/sdk\";\n\nconst cssFile = new URL(\"./preflight.css\", import.meta.url);\nconst css = await readFile(cssFile, \"utf8\");\nconst parsed = parseCss(css);\nconst result: Record<string, { property: CssProperty; value: StyleValue }[]> =\n  {};\nfor (const { selector, breakpoint, ...styleDecl } of parsed) {\n  if (tags.includes(selector) && styleDecl.state === undefined) {\n    result[selector] ??= [];\n    result[selector].push(styleDecl);\n  }\n}\nlet code = \"\";\ncode += `import type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\\n\\n`;\nconst type =\n  \"Record<string, undefined | { property: CssProperty, value: StyleValue }[]>\";\ncode += `export const preflight: ${type} = ${JSON.stringify(result)}`;\n\nconst generatedFile = new URL(\"./__generated__/preflight.ts\", import.meta.url);\nawait mkdir(new URL(\".\", generatedFile), { recursive: true });\nawait writeFile(generatedFile, code);\n"
  },
  {
    "path": "apps/builder/app/shared/tailwind/preflight.css",
    "content": "/* https://github.com/unocss/unocss/blob/81b4946d3ab24dff5c5940963d46d6082c52688f/packages-presets/reset/tailwind-compat.css */\n/* https://github.com/tailwindlabs/tailwindcss/blob/449dfcf00d547dc6609466ef47840aa23bb0f11f/packages/tailwindcss/preflight.css */\n\n/*\nPlease read: https://github.com/unocss/unocss/blob/main/packages-presets/reset/tailwind-compat.md\n*/\n\n/*\n1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)\n2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n2. [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`\n*/\n\n*,\n::before,\n::after {\n  box-sizing: border-box; /* 1 */\n  border-width: 0; /* 2 */\n  border-style: solid; /* 2 */\n  border-color: var(--un-default-border-color, #e5e7eb); /* 2 */\n}\n\n/*\n1. Use a consistent sensible line-height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n3. Use a more readable tab size.\n4. Use the user's configured `sans` font-family by default.\n5. Use the user's configured `sans` font-feature-settings by default.\n6. Use the user's configured `sans` font-variation-settings by default.\n7. Disable tap highlights on iOS.\n*/\n\nhtml,\n:host {\n  line-height: 1.5; /* 1 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n  -moz-tab-size: 4; /* 3 */\n  tab-size: 4; /* 3 */\n  font-family:\n    ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\",\n    \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 4 */\n  font-feature-settings: normal; /* 5 */\n  font-variation-settings: normal; /* 6 */\n  -webkit-tap-highlight-color: transparent; /* 7 */\n}\n\n/*\n1. Remove the margin in all browsers.\n2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.\n*/\n\nbody {\n  margin: 0; /* 1 */\n  line-height: inherit; /* 2 */\n}\n\n/*\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n3. Ensure horizontal rules are visible by default.\n*/\n\nhr {\n  height: 0; /* 1 */\n  color: inherit; /* 2 */\n  border-top-width: 1px; /* 3 */\n}\n\n/*\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr:where([title]) {\n  text-decoration: underline dotted;\n}\n\n/*\nRemove the default font size and weight for headings.\n*/\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  font-size: inherit;\n  font-weight: inherit;\n}\n\n/*\nReset links to optimize for opt-in styling instead of opt-out.\n*/\n\na {\n  color: inherit;\n  text-decoration: inherit;\n}\n\n/*\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/*\n1. Use the user's configured `mono` font-family by default.\n2. Use the user's configured `mono` font-feature-settings by default.\n3. Use the user's configured `mono` font-variation-settings by default.\n4. Correct the odd `em` font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n  font-family:\n    ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\",\n    \"Courier New\", monospace; /* 1 */\n  font-feature-settings: normal; /* 2 */\n  font-variation-settings: normal; /* 3 */\n  font-size: 1em; /* 4 */\n}\n\n/*\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n  font-size: 80%;\n}\n\n/*\nPrevent `sub` and `sup` elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/*\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n3. Remove gaps between table borders by default.\n*/\n\ntable {\n  text-indent: 0; /* 1 */\n  border-color: inherit; /* 2 */\n  border-collapse: collapse; /* 3 */\n}\n\n/*\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n3. Remove default padding in all browsers.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: inherit; /* 1 */\n  font-feature-settings: inherit; /* 1 */\n  font-variation-settings: inherit; /* 1 */\n  font-size: 100%; /* 1 */\n  font-weight: inherit; /* 1 */\n  line-height: inherit; /* 1 */\n  color: inherit; /* 1 */\n  margin: 0; /* 2 */\n  padding: 0; /* 3 */\n}\n\n/*\nRemove the inheritance of text transform in Edge and Firefox.\n*/\n\nbutton,\nselect {\n  text-transform: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Remove default button styles.\n*/\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button; /* 1 */\n  background-color: transparent; /* 2 */\n  background-image: none; /* 2 */\n}\n\n/*\nUse the modern Firefox focus style for all focusable elements.\n*/\n\n:-moz-focusring {\n  outline: auto;\n}\n\n/*\nRemove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)\n*/\n\n:-moz-ui-invalid {\n  box-shadow: none;\n}\n\n/*\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n  vertical-align: baseline;\n}\n\n/*\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/*\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/*\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/*\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to `inherit` in Safari.\n*/\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n  display: list-item;\n}\n\n/*\nRemoves the default spacing for appropriate elements.\n*/\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n  margin: 0;\n}\n\nfieldset {\n  margin: 0;\n  padding: 0;\n}\n\nlegend {\n  padding: 0;\n}\n\nol,\nul,\nmenu {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\ndialog {\n  padding: 0;\n}\n\n/*\nPrevent resizing textareas horizontally by default.\n*/\n\ntextarea {\n  resize: vertical;\n}\n\n/*\n1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)\n2. Set the default placeholder color to the user's configured gray 400 color.\n*/\n\ninput::placeholder,\ntextarea::placeholder {\n  opacity: 1; /* 1 */\n  color: #9ca3af; /* 2 */\n}\n\n/*\nSet the default cursor for buttons.\n*/\n\nbutton,\n[role=\"button\"] {\n  cursor: pointer;\n}\n\n/*\nMake sure disabled buttons don't get the pointer cursor.\n*/\n\n:disabled {\n  cursor: default;\n}\n\n/*\n1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)\n2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)\n   This can trigger a poorly considered lint error in some tools but is included by design.\n*/\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n  display: block; /* 1 */\n  vertical-align: middle; /* 2 */\n}\n\n/*\nConstrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)\n*/\n\nimg,\nvideo {\n  max-width: 100%;\n  height: auto;\n}\n\n/*\nMake elements with the HTML hidden attribute stay hidden by default.\n*/\n\n[hidden]:where(:not([hidden=\"until-found\"])) {\n  display: none;\n}\n"
  },
  {
    "path": "apps/builder/app/shared/tailwind/tailwind.test.tsx",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { css, renderTemplate, ws } from \"@webstudio-is/template\";\nimport { generateFragmentFromTailwind } from \"./tailwind\";\n\ntest(\"extract local styles from tailwind classes\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"m-2\">\n          <ws.element ws:tag=\"span\" class=\"text-sm\"></ws.element>\n        </ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          margin: 0.5rem;\n        `}\n      >\n        <ws.element\n          ws:tag=\"span\"\n          ws:style={css`\n            font-size: 0.875rem;\n            line-height: 1.25rem;\n          `}\n        ></ws.element>\n      </ws.element>\n    )\n  );\n});\n\ntest(\"ignore dark mode\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"bg-white dark:bg-gray-800\"></ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          --tw-bg-opacity: 1;\n          background-color: rgb(255 255 255 / var(--tw-bg-opacity));\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"ignore empty class\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(<ws.element ws:tag=\"div\" class=\"\"></ws.element>)\n    )\n  ).toEqual(renderTemplate(<ws.element ws:tag=\"div\"></ws.element>));\n});\n\ntest(\"preserve custom class\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"custom-class m-2\"></ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          margin: 0.5rem;\n        `}\n        class=\"custom-class\"\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"generate border\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(<ws.element ws:tag=\"div\" class=\"border\"></ws.element>)\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          border-style: solid;\n          border-color: var(--tw-default-border-color, #e5e7eb);\n          border-width: 1px;\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"override border opacity\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          class=\"border border-gray-200 border-opacity-60\"\n        ></ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          border-style: solid;\n          border-color: rgb(229 231 235 / var(--tw-border-opacity));\n          border-width: 1px;\n          --tw-border-opacity: 0.6;\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"generate shadow\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(<ws.element ws:tag=\"div\" class=\"shadow\"></ws.element>)\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          --tw-ring-offset-shadow: 0 0 rgb(0 0 0 / 0);\n          --tw-ring-shadow: 0 0 rgb(0 0 0 / 0);\n          --tw-shadow-inset: ;\n          --tw-shadow:\n            var(--tw-shadow-inset) 0 1px 3px 0\n              var(--tw-shadow-color, rgb(0 0 0 / 0.1)),\n            var(--tw-shadow-inset) 0 1px 2px -1px\n              var(--tw-shadow-color, rgb(0 0 0 / 0.1));\n          box-shadow:\n            var(--tw-ring-offset-shadow), var(--tw-ring-shadow),\n            var(--tw-shadow);\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"preserve or override existing local styles\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          class=\"m-2\"\n          ws:style={css`\n            margin-top: 1px;\n            color: red;\n          `}\n        ></ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          color: red;\n          margin: 0.5rem;\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"add preflight matching tags\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element ws:tag=\"p\" class=\"text-pretty border\"></ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"p\"\n        ws:style={css`\n          /* this one comes from tag preflight */\n          margin: 0;\n          border-style: solid;\n          border-color: var(--tw-default-border-color, #e5e7eb);\n          border-width: 1px;\n          text-wrap: pretty;\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"add preflight matching tags when no classes are used\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(<ws.element ws:tag=\"a\"></ws.element>)\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"a\"\n        ws:style={css`\n          color: inherit;\n          text-decoration: inherit;\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ntest(\"extract states from tailwind classes\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          class=\"bg-indigo-600 hover:bg-indigo-500\"\n        ></ws.element>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <ws.element\n        ws:tag=\"div\"\n        ws:style={css`\n          --tw-bg-opacity: 1;\n          background-color: rgb(79 70 229 / var(--tw-bg-opacity));\n          &:hover {\n            --tw-bg-opacity: 1;\n            background-color: rgb(99 102 241 / var(--tw-bg-opacity));\n          }\n        `}\n      ></ws.element>\n    )\n  );\n});\n\ndescribe(\"extract breakpoints\", () => {\n  test(\"extract new breakpoints from tailwind classes\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"opacity-10 sm:opacity-20 md:opacity-30 lg:opacity-40 xl:opacity-50 2xl:opacity-60\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            /* base -> max-width: 479px */\n            @media (max-width: 479px) {\n              opacity: 0.1;\n            }\n            /* min-width: 640px -> max-width: 767px */\n            @media (max-width: 767px) {\n              opacity: 0.2;\n            }\n            /* min-width: 768px -> max-width: 991px */\n            @media (max-width: 991px) {\n              opacity: 0.3;\n            }\n            /* min-width: 1024px -> base */\n            opacity: 0.4;\n            /* unchanged */\n            @media (min-width: 1280px) {\n              opacity: 0.5;\n            }\n            /* min-width: 1536px -> min-width: 1440px */\n            @media (min-width: 1440px) {\n              opacity: 0.6;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"base is first breakpoint\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"opacity-10 sm:opacity-20\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              opacity: 0.1;\n            }\n            opacity: 0.2;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"base is last breakpoint\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"opacity-10 xl:opacity-20\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            opacity: 0.1;\n            @media (min-width: 1280px) {\n              opacity: 0.2;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"base is middle breakpoint\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"opacity-10 sm:opacity-20 xl:opacity-30\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              opacity: 0.1;\n            }\n            opacity: 0.2;\n            @media (min-width: 1280px) {\n              opacity: 0.3;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"container class without user min-width breakpoints\", async () => {\n    // container class should only create max-width breakpoints, not min-width\n    // this prevents unwanted 1280/1440 breakpoints from being created\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(<ws.element ws:tag=\"div\" class=\"container\"></ws.element>)\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              max-width: none;\n            }\n            @media (max-width: 767px) {\n              max-width: 640px;\n            }\n            @media (max-width: 991px) {\n              max-width: 768px;\n            }\n            max-width: 1024px;\n            width: 100%;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"container class with user min-width breakpoints\", async () => {\n    // when user already has min-width breakpoints, container should use them\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            ws:style={css`\n              @media (min-width: 1280px) {\n                color: red;\n              }\n            `}\n            class=\"container\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (min-width: 1280px) {\n              color: red;\n            }\n            @media (max-width: 479px) {\n              max-width: none;\n            }\n            @media (max-width: 767px) {\n              max-width: 640px;\n            }\n            @media (max-width: 991px) {\n              max-width: 768px;\n            }\n            max-width: 1024px;\n            @media (min-width: 1280px) {\n              max-width: 1280px;\n            }\n            @media (min-width: 1440px) {\n              max-width: 1536px;\n            }\n            width: 100%;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"explicit xl class creates min-width breakpoint\", async () => {\n    // explicit xl: classes should create min-width breakpoints\n    // unlike container which is special-cased\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element ws:tag=\"div\" class=\"text-sm xl:text-lg\"></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            font-size: 0.875rem;\n            @media (min-width: 1280px) {\n              font-size: 1.125rem;\n            }\n            line-height: 1.25rem;\n            @media (min-width: 1280px) {\n              line-height: 1.75rem;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"container combined with explicit xl class\", async () => {\n    // container shouldn't create min-width breakpoints\n    // but explicit xl: class should\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element ws:tag=\"div\" class=\"container xl:text-lg\"></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              max-width: none;\n            }\n            @media (max-width: 767px) {\n              max-width: 640px;\n            }\n            @media (max-width: 991px) {\n              max-width: 768px;\n            }\n            max-width: 1024px;\n            width: 100%;\n            font-size: unset;\n            @media (min-width: 1280px) {\n              font-size: 1.125rem;\n            }\n            line-height: unset;\n            @media (min-width: 1280px) {\n              line-height: 1.75rem;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"merge tailwind breakpoints with already defined ones\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            ws:style={css`\n              @media (max-width: 479px) {\n                color: red;\n              }\n              color: green;\n            `}\n            class=\"opacity-10 sm:opacity-20 md:opacity-30\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              color: red;\n            }\n            color: green;\n            @media (max-width: 479px) {\n              opacity: 0.1;\n            }\n            @media (max-width: 767px) {\n              opacity: 0.2;\n            }\n            opacity: 0.3;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"state\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"opacity-10 sm:hover:opacity-20\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            opacity: 0.1;\n            @media (max-width: 479px) {\n              &:hover {\n                opacity: unset;\n              }\n            }\n            &:hover {\n              opacity: 0.2;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"adapt max-* breakpoints\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"max-sm:opacity-10 max-md:opacity-20 max-lg:opacity-30 max-xl:opacity-40 max-2xl:opacity-50 opacity-60\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              opacity: 0.1;\n            }\n            @media (max-width: 767px) {\n              opacity: 0.2;\n            }\n            @media (max-width: 991px) {\n              opacity: 0.3;\n            }\n            opacity: 0.4;\n            @media (min-width: 1280px) {\n              opacity: 0.5;\n            }\n            @media (min-width: 1440px) {\n              opacity: 0.6;\n            }\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"ignore composite breakpoints\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"opacity-10 md:max-xl:flex\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            opacity: 0.1;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"use unset for missing base breakpoint 1\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element ws:tag=\"div\" class=\"sm:opacity-10\"></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              opacity: unset;\n            }\n            opacity: 0.1;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"use unset for missing base breakpoint 2\", async () => {\n    expect(\n      await generateFragmentFromTailwind(\n        renderTemplate(\n          <ws.element\n            ws:tag=\"div\"\n            class=\"max-sm:opacity-10 md:opacity-20\"\n          ></ws.element>\n        )\n      )\n    ).toEqual(\n      renderTemplate(\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 479px) {\n              opacity: 0.1;\n            }\n            @media (max-width: 767px) {\n              opacity: unset;\n            }\n            opacity: 0.2;\n          `}\n        ></ws.element>\n      )\n    );\n  });\n\n  test(\"non-responsive classes create only base breakpoint\", async () => {\n    const fragment = await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"m-2 p-4 text-red-500\"></ws.element>\n      )\n    );\n    expect(fragment.breakpoints).toEqual([{ id: \"base\", label: \"\" }]);\n  });\n\n  test(\"container class creates only needed breakpoints\", async () => {\n    const fragment = await generateFragmentFromTailwind(\n      renderTemplate(<ws.element ws:tag=\"div\" class=\"container\"></ws.element>)\n    );\n    // container should only create max-width breakpoints, not 1280/1440/1920 min-width ones\n    expect(fragment.breakpoints).toEqual([\n      { id: \"0\", label: \"479\", maxWidth: 479 },\n      { id: \"1\", label: \"767\", maxWidth: 767 },\n      { id: \"2\", label: \"991\", maxWidth: 991 },\n      { id: \"base\", label: \"\" },\n    ]);\n  });\n\n  test(\"sm:class creates only needed breakpoints\", async () => {\n    const fragment = await generateFragmentFromTailwind(\n      renderTemplate(\n        <ws.element ws:tag=\"div\" class=\"opacity-50 sm:opacity-100\"></ws.element>\n      )\n    );\n    // sm: should only create 479 max-width and base, not all breakpoints\n    expect(fragment.breakpoints).toEqual([\n      { id: \"0\", label: \"479\", maxWidth: 479 },\n      { id: \"base\", label: \"\" },\n    ]);\n  });\n});\n\ntest(\"generate space without display property\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <>\n          <ws.element ws:tag=\"div\" class=\"space-x-4 md:space-x-6\"></ws.element>\n          <ws.element ws:tag=\"div\" class=\"space-y-4 md:space-y-6\"></ws.element>\n        </>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <>\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            display: flex;\n            @media (max-width: 767px) {\n              column-gap: 1rem;\n            }\n            column-gap: 1.5rem;\n          `}\n        ></ws.element>\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            display: flex;\n            flex-direction: column;\n            align-items: start;\n            @media (max-width: 767px) {\n              row-gap: 1rem;\n            }\n            row-gap: 1.5rem;\n          `}\n        ></ws.element>\n      </>\n    )\n  );\n});\n\ntest(\"generate space with display property\", async () => {\n  expect(\n    await generateFragmentFromTailwind(\n      renderTemplate(\n        <>\n          <ws.element ws:tag=\"div\" class=\"flex space-x-4\"></ws.element>\n          <ws.element\n            ws:tag=\"div\"\n            class=\"hidden md:flex space-y-4\"\n          ></ws.element>\n        </>\n      )\n    )\n  ).toEqual(\n    renderTemplate(\n      <>\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            display: flex;\n            column-gap: 1rem;\n          `}\n        ></ws.element>\n        <ws.element\n          ws:tag=\"div\"\n          ws:style={css`\n            @media (max-width: 767px) {\n              display: none;\n            }\n            display: flex;\n            row-gap: 1rem;\n          `}\n        ></ws.element>\n      </>\n    )\n  );\n});\n"
  },
  {
    "path": "apps/builder/app/shared/tailwind/tailwind.ts",
    "content": "import { createGenerator } from \"@unocss/core\";\nimport { presetLegacyCompat } from \"@unocss/preset-legacy-compat\";\nimport { presetWind3 } from \"@unocss/preset-wind3\";\nimport {\n  camelCaseProperty,\n  parseCss,\n  parseMediaQuery,\n  type ParsedStyleDecl,\n} from \"@webstudio-is/css-data\";\nimport {\n  getStyleDeclKey,\n  type Breakpoint,\n  type Instance,\n  type Prop,\n  type WebstudioFragment,\n} from \"@webstudio-is/sdk\";\nimport { isBaseBreakpoint } from \"../nano-states\";\nimport { preflight } from \"./__generated__/preflight\";\n\n// breakpoints used to map tailwind classes to webstudio breakpoints\n// includes both min-width (desktop-first) and max-width (mobile-first) breakpoints\nconst tailwindBreakpoints: Breakpoint[] = [\n  { id: \"1920\", label: \"1920\", minWidth: 1920 },\n  { id: \"1440\", label: \"1440\", minWidth: 1440 },\n  { id: \"1280\", label: \"1280\", minWidth: 1280 },\n  { id: \"base\", label: \"\" },\n  { id: \"991\", label: \"991\", maxWidth: 991 },\n  { id: \"767\", label: \"767\", maxWidth: 767 },\n  { id: \"479\", label: \"479\", maxWidth: 479 },\n];\n\nconst tailwindToWebstudioMappings: Record<number, undefined | number> = {\n  639.9: 479,\n  640: 480,\n  767.9: 767,\n  1023.9: 991,\n  1024: 992,\n  1279.9: 1279,\n  1535.9: 1439,\n  1536: 1440,\n};\n\ntype StyleDecl = Omit<ParsedStyleDecl, \"selector\">;\n\ntype StyleBreakpoint = {\n  styleDecl: StyleDecl;\n  minWidth?: number;\n  maxWidth?: number;\n};\n\ntype Range = {\n  styleDecl: StyleDecl;\n  start: number;\n  end: number;\n};\n\nconst serializeStyleBreakpoint = (breakpoint: StyleBreakpoint) => {\n  if (breakpoint?.minWidth !== undefined) {\n    return `(min-width: ${breakpoint.minWidth}px)`;\n  }\n  if (breakpoint?.maxWidth !== undefined) {\n    return `(max-width: ${breakpoint.maxWidth}px)`;\n  }\n};\n\nconst UPPER_BOUND = Number.MAX_SAFE_INTEGER;\n\nconst breakpointsToRanges = (breakpoints: StyleBreakpoint[]) => {\n  // collect lower bounds and ids\n  const values = new Set<number>([0]);\n  const styles = new Map<undefined | number, StyleDecl>();\n  for (const breakpoint of breakpoints) {\n    if (breakpoint.minWidth !== undefined) {\n      values.add(breakpoint.minWidth);\n      styles.set(breakpoint.minWidth, breakpoint.styleDecl);\n    } else if (breakpoint.maxWidth !== undefined) {\n      values.add(breakpoint.maxWidth + 1);\n      styles.set(breakpoint.maxWidth, breakpoint.styleDecl);\n    } else {\n      // base breakpoint\n      styles.set(undefined, breakpoint.styleDecl);\n    }\n  }\n  const sortedValues = Array.from(values).sort((left, right) => left - right);\n  const ranges: Range[] = [];\n  for (let index = 0; index < sortedValues.length; index += 1) {\n    const start = sortedValues[index];\n    let end;\n    if (index === sortedValues.length - 1) {\n      end = UPPER_BOUND;\n    } else {\n      end = sortedValues[index + 1] - 1;\n    }\n    const styleDecl =\n      styles.get(start) ?? styles.get(end) ?? styles.get(undefined);\n    if (styleDecl) {\n      ranges.push({ styleDecl, start, end });\n      continue;\n    }\n    // when declaration is missing add new one with unset value\n    // to fill the hole in breakpoints\n    // for example\n    // \"sm:opacity-20\" has a hole at the start\n    // \"max-sm:opacity-10 md:opacity-20\" has a whole in the middle\n    const example = Array.from(styles.values())[0];\n    if (example) {\n      const newStyleDecl: StyleDecl = {\n        ...example,\n        value: { type: \"keyword\", value: \"unset\" },\n      };\n      ranges.push({ styleDecl: newStyleDecl, start, end });\n    }\n  }\n  return ranges;\n};\n\nconst rangesToBreakpoints = (\n  ranges: Range[],\n  userBreakpoints: Breakpoint[]\n) => {\n  const breakpoints: StyleBreakpoint[] = [];\n  for (const { styleDecl, start, end } of ranges) {\n    let matchedBreakpoint;\n    for (const breakpoint of userBreakpoints) {\n      if (breakpoint.minWidth === start) {\n        matchedBreakpoint = { styleDecl, minWidth: start };\n      }\n      if (breakpoint.maxWidth === end) {\n        matchedBreakpoint = { styleDecl, maxWidth: end };\n      }\n      if (\n        breakpoint.minWidth === undefined &&\n        breakpoint.maxWidth === undefined\n      ) {\n        matchedBreakpoint ??= { styleDecl };\n      }\n    }\n    if (matchedBreakpoint) {\n      styleDecl.breakpoint = serializeStyleBreakpoint(matchedBreakpoint);\n      breakpoints.push(matchedBreakpoint);\n    }\n  }\n  return breakpoints;\n};\n\nconst adaptBreakpoints = (\n  parsedStyles: StyleDecl[],\n  userBreakpoints: Breakpoint[]\n) => {\n  const breakpointGroups = new Map<string, StyleBreakpoint[]>();\n  for (const styleDecl of parsedStyles) {\n    const mediaQuery = styleDecl.breakpoint\n      ? parseMediaQuery(styleDecl.breakpoint)\n      : undefined;\n    // Skip condition-only breakpoints (e.g. dark mode prefers-color-scheme)\n    // and combined min+max width breakpoints (e.g. md:max-xl:)\n    // @todo support composite breakpoints by splitting into range-based breakpoints\n    if (mediaQuery?.condition !== undefined) {\n      continue;\n    }\n    if (\n      mediaQuery?.minWidth !== undefined &&\n      mediaQuery?.maxWidth !== undefined\n    ) {\n      continue;\n    }\n    if (mediaQuery?.minWidth) {\n      mediaQuery.minWidth =\n        tailwindToWebstudioMappings[mediaQuery.minWidth] ?? mediaQuery.minWidth;\n    }\n    if (mediaQuery?.maxWidth) {\n      mediaQuery.maxWidth =\n        tailwindToWebstudioMappings[mediaQuery.maxWidth] ?? mediaQuery.maxWidth;\n    }\n    const groupKey = `${styleDecl.property}:${styleDecl.state ?? \"\"}`;\n    let group = breakpointGroups.get(groupKey);\n    if (group === undefined) {\n      group = [];\n      breakpointGroups.set(groupKey, group);\n    }\n    group.push({ styleDecl, ...mediaQuery });\n  }\n  const newStyles: typeof parsedStyles = [];\n  for (const group of breakpointGroups.values()) {\n    const ranges = breakpointsToRanges(group);\n    const newGroup = rangesToBreakpoints(ranges, userBreakpoints);\n    for (const { styleDecl } of newGroup) {\n      newStyles.push(styleDecl);\n    }\n  }\n  return newStyles;\n};\n\nconst createUnoGenerator = async () => {\n  return await createGenerator({\n    presets: [\n      presetWind3({\n        // css variables are defined on the same element as styleDecl\n        preflight: \"on-demand\",\n        // dark mode will be ignored by parser\n        dark: \"media\",\n      }),\n      // until we support oklch natively\n      presetLegacyCompat({ legacyColorSpace: true }),\n    ],\n  });\n};\n\nconst parseTailwindClasses = async (\n  classes: string,\n  userBreakpoints: Breakpoint[],\n  fragmentBreakpoints: Breakpoint[]\n) => {\n  // avoid caching uno generator instance\n  // to prevent bloating css with preflights from previous calls\n  const generator = await createUnoGenerator();\n  let hasColumnGaps = false;\n  let hasRowGaps = false;\n  let hasFlexOrGrid = false;\n  let hasContainer = false;\n  classes = classes\n    .split(\" \")\n    .map((item) => {\n      // styles data cannot express space-x and space-y selectors\n      // with lobotomized owl so replace with gaps\n      if (item.includes(\"space-x-\")) {\n        hasColumnGaps = true;\n        return item.replace(\"space-x-\", \"gap-x-\");\n      }\n      if (item.includes(\"space-y-\")) {\n        hasRowGaps = true;\n        return item.replace(\"space-y-\", \"gap-y-\");\n      }\n      hasFlexOrGrid ||= item.endsWith(\"flex\") || item.endsWith(\"grid\");\n      hasContainer ||= item === \"container\";\n      return item;\n    })\n    .join(\" \");\n  const generated = await generator.generate(classes);\n  // use tailwind prefix instead of unocss one\n  const css = generated.css.replaceAll(\"--un-\", \"--tw-\");\n  let parsedStyles: StyleDecl[] = [];\n  // @todo probably builtin in v4\n  if (css.includes(\"border\")) {\n    // Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)\n    // [UnoCSS]: allow to override the default border color with css var `--un-default-border-color`\n    const reset = `.styles {\n      border-style: solid;\n      border-color: var(--tw-default-border-color, #e5e7eb);\n      border-width: 0;\n    }`;\n    parsedStyles.push(...parseCss(reset));\n  }\n  parsedStyles.push(...parseCss(css));\n  // skip preflights with ::before, ::after and ::backdrop\n  parsedStyles = parsedStyles.filter(\n    (styleDecl) => !styleDecl.state?.startsWith(\"::\")\n  );\n  // setup base breakpoint for container class to avoid hole in ranges\n  if (hasContainer) {\n    parsedStyles.unshift({\n      property: \"max-width\",\n      value: { type: \"keyword\", value: \"none\" },\n    });\n  }\n  // gaps work only with flex and grid\n  // so try to use one or another for different axes\n  if (hasColumnGaps && !hasFlexOrGrid) {\n    parsedStyles.unshift({\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    });\n  }\n  if (hasRowGaps && !hasFlexOrGrid) {\n    parsedStyles.unshift(\n      {\n        property: \"display\",\n        value: { type: \"keyword\", value: \"flex\" },\n      },\n      {\n        property: \"flex-direction\",\n        value: { type: \"keyword\", value: \"column\" },\n      },\n      {\n        property: \"align-items\",\n        value: { type: \"keyword\", value: \"start\" },\n      }\n    );\n  }\n  parsedStyles = adaptBreakpoints(parsedStyles, userBreakpoints);\n  // container class generates max-width styles for all Tailwind breakpoints\n  // filter out min-width ones only if user doesn't have min-width breakpoints\n  const hasUserMinWidthBreakpoints = fragmentBreakpoints.some(\n    (bp) => bp.minWidth !== undefined\n  );\n  if (hasContainer && !hasUserMinWidthBreakpoints) {\n    parsedStyles = parsedStyles.filter(\n      (styleDecl) =>\n        styleDecl.property !== \"max-width\" ||\n        !styleDecl.breakpoint?.includes(\"min-width\")\n    );\n  }\n  const newClasses = classes\n    .split(\" \")\n    .filter((item) => !generated.matched.has(item))\n    .join(\" \");\n  return { newClasses, parsedStyles };\n};\n\nconst getUniqueIdForList = <List extends Array<{ id: string }>>(list: List) => {\n  const existingIds = list.map((item) => item.id);\n  let index = 0;\n  while (existingIds.includes(index.toString())) {\n    index += 1;\n  }\n  return index.toString();\n};\n\nexport const generateFragmentFromTailwind = async (\n  fragment: WebstudioFragment\n): Promise<WebstudioFragment> => {\n  // lazily create breakpoint\n  let breakpoints = fragment.breakpoints;\n  const getBreakpointId = (mediaQuery: undefined | string) => {\n    if (mediaQuery === undefined) {\n      let baseBreakpoint = breakpoints.find(isBaseBreakpoint);\n      if (baseBreakpoint === undefined) {\n        baseBreakpoint = { id: \"base\", label: \"\" };\n        breakpoints = [...breakpoints];\n        breakpoints.push(baseBreakpoint);\n      }\n      return baseBreakpoint.id;\n    }\n    const parsedMediaQuery = parseMediaQuery(mediaQuery);\n    // unknown breakpoint\n    if (parsedMediaQuery === undefined) {\n      return;\n    }\n    let breakpoint = breakpoints.find(\n      (item) =>\n        item.minWidth === parsedMediaQuery.minWidth &&\n        item.maxWidth === parsedMediaQuery.maxWidth\n    );\n    if (breakpoint === undefined) {\n      const label = `${parsedMediaQuery.minWidth ?? parsedMediaQuery.maxWidth}`;\n      breakpoint = {\n        // make sure new breakpoint id is not conflicted with already defined by fragment\n        id: getUniqueIdForList(breakpoints),\n        label,\n        ...parsedMediaQuery,\n      };\n      breakpoints = [...breakpoints];\n      breakpoints.push(breakpoint);\n    }\n    return breakpoint.id;\n  };\n\n  const styleSourceSelections = new Map(\n    fragment.styleSourceSelections.map((item) => [item.instanceId, item])\n  );\n  const styleSources = new Map(\n    fragment.styleSources.map((item) => [item.id, item])\n  );\n  const styles = new Map(\n    fragment.styles.map((item) => [getStyleDeclKey(item), item])\n  );\n  const getLocalStyleSource = (instanceId: Instance[\"id\"]) => {\n    const styleSourceSelection = styleSourceSelections.get(instanceId);\n    const lastStyleSourceId = styleSourceSelection?.values.at(-1);\n    const lastStyleSource = styleSources.get(lastStyleSourceId ?? \"\");\n    return lastStyleSource?.type === \"local\" ? lastStyleSource : undefined;\n  };\n  const createLocalStyleSource = (instanceId: Instance[\"id\"]) => {\n    const localStyleSource = {\n      type: \"local\" as const,\n      id: `${instanceId}:ws:style`,\n    };\n    let styleSourceSelection = structuredClone(\n      styleSourceSelections.get(instanceId)\n    );\n    if (styleSourceSelection === undefined) {\n      styleSourceSelection = { instanceId, values: [] };\n      styleSourceSelections.set(instanceId, styleSourceSelection);\n    }\n    styleSources.set(localStyleSource.id, localStyleSource);\n    styleSourceSelection.values.push(localStyleSource.id);\n    return localStyleSource;\n  };\n  const createOrMergeLocalStyles = (\n    instanceId: Instance[\"id\"],\n    newStyles: StyleDecl[]\n  ) => {\n    const localStyleSource =\n      getLocalStyleSource(instanceId) ?? createLocalStyleSource(instanceId);\n    for (const parsedStyleDecl of newStyles) {\n      const breakpointId = getBreakpointId(parsedStyleDecl.breakpoint);\n      // ignore unknown breakpoints\n      if (breakpointId === undefined) {\n        continue;\n      }\n      const styleDecl = {\n        breakpointId,\n        styleSourceId: localStyleSource.id,\n        state: parsedStyleDecl.state,\n        property: camelCaseProperty(parsedStyleDecl.property),\n        value: parsedStyleDecl.value,\n      };\n      const styleDeclKey = getStyleDeclKey(styleDecl);\n      styles.delete(styleDeclKey);\n      styles.set(styleDeclKey, styleDecl);\n    }\n  };\n\n  for (const instance of fragment.instances) {\n    const tag = instance.tag;\n    if (tag && preflight[tag]) {\n      createOrMergeLocalStyles(instance.id, preflight[tag]);\n    }\n  }\n\n  const props: Prop[] = [];\n  await Promise.all(\n    fragment.props.map(async (prop) => {\n      if (prop.name === \"class\" && prop.type === \"string\") {\n        // always use tailwindBreakpoints for parsing to support all Tailwind classes\n        // new breakpoints will be created as needed via getBreakpointId\n        const { newClasses, parsedStyles } = await parseTailwindClasses(\n          prop.value,\n          tailwindBreakpoints,\n          fragment.breakpoints\n        );\n        if (parsedStyles.length > 0) {\n          createOrMergeLocalStyles(prop.instanceId, parsedStyles);\n          if (newClasses.length > 0) {\n            props.push({ ...prop, value: newClasses });\n          }\n        }\n        return;\n      }\n      props.push(prop);\n    })\n  );\n\n  return {\n    ...fragment,\n    props,\n    breakpoints,\n    styleSources: Array.from(styleSources.values()),\n    styleSourceSelections: Array.from(styleSourceSelections.values()),\n    styles: Array.from(styles.values()),\n  };\n};\n"
  },
  {
    "path": "apps/builder/app/shared/token-conflict-dialog.tsx",
    "content": "import { useState } from \"react\";\nimport { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogTitle,\n  DialogDescription,\n  DialogActions,\n  Button,\n  Flex,\n  Text,\n  theme,\n  RadioGroup,\n  Radio,\n  Label,\n} from \"@webstudio-is/design-system\";\nimport type { TokenConflict } from \"./style-source-utils\";\n\nexport type ConflictResolution = \"ours\" | \"theirs\" | \"merge\";\n\ntype DialogState =\n  | {\n      conflicts: TokenConflict[];\n      resolve: (resolution: ConflictResolution) => void;\n    }\n  | undefined;\n\nconst $tokenConflictDialogState = atom<DialogState>(undefined);\n\nexport const showTokenConflictDialog = (\n  conflicts: Array<{\n    tokenName: string;\n    fragmentTokenId: string;\n  }>\n): Promise<ConflictResolution> => {\n  return new Promise((resolve) => {\n    const fullConflicts: TokenConflict[] = conflicts.map((c) => ({\n      tokenName: c.tokenName,\n      fragmentTokenId: c.fragmentTokenId,\n      fragmentToken: {\n        type: \"token\" as const,\n        id: c.fragmentTokenId,\n        name: c.tokenName,\n      },\n      existingToken: {\n        type: \"token\" as const,\n        id: \"existing\",\n        name: c.tokenName,\n      },\n    }));\n\n    $tokenConflictDialogState.set({\n      conflicts: fullConflicts,\n      resolve,\n    });\n  });\n};\n\nexport const TokenConflictDialog = () => {\n  const dialogState = useStore($tokenConflictDialogState);\n  const [resolution, setResolution] = useState<ConflictResolution | undefined>(\n    \"theirs\"\n  );\n\n  if (!dialogState) {\n    return;\n  }\n\n  const { conflicts, resolve } = dialogState;\n\n  const handleClose = () => {\n    $tokenConflictDialogState.set(undefined);\n    setResolution(\"theirs\");\n  };\n\n  const handleResolve = () => {\n    if (resolution) {\n      resolve(resolution);\n      handleClose();\n    }\n  };\n\n  const handleCancel = () => {\n    handleClose();\n  };\n\n  if (conflicts.length === 0) {\n    return null;\n  }\n\n  const conflictCount = conflicts.length;\n  const firstConflict = conflicts[0];\n\n  return (\n    <Dialog\n      open={true}\n      onOpenChange={(open) => {\n        if (!open) {\n          handleCancel();\n        }\n      }}\n    >\n      <DialogContent css={{ minWidth: \"40ch\" }}>\n        <DialogTitle>Token conflict detected</DialogTitle>\n        <Flex\n          direction=\"column\"\n          gap=\"2\"\n          css={{\n            padding: theme.panel.padding,\n          }}\n        >\n          <DialogDescription asChild>\n            <Text as=\"p\">\n              {conflictCount === 1\n                ? `The token \"${firstConflict.tokenName}\" already exists with different styles.`\n                : `${conflictCount} tokens already exist with the same names but different styles.`}\n            </Text>\n          </DialogDescription>\n\n          <RadioGroup\n            value={resolution}\n            onValueChange={(value) =>\n              setResolution(value as ConflictResolution)\n            }\n          >\n            <Flex direction=\"column\" gap=\"1\">\n              <Label>\n                <Flex\n                  gap=\"2\"\n                  css={{\n                    padding: theme.spacing[3],\n                    cursor: \"pointer\",\n                    borderRadius: theme.borderRadius[4],\n                    \"&:hover\": {\n                      backgroundColor: theme.colors.backgroundHover,\n                    },\n                  }}\n                >\n                  <Radio value=\"theirs\" />\n                  <Flex direction=\"column\" gap=\"1\">\n                    <Text variant=\"labels\">Theirs</Text>\n                    <Text color=\"subtle\">\n                      Keep incoming tokens with a suffix added to their names\n                      (e.g., \"primary-color-1\")\n                    </Text>\n                  </Flex>\n                </Flex>\n              </Label>\n\n              <Label>\n                <Flex\n                  gap=\"2\"\n                  css={{\n                    padding: theme.spacing[3],\n                    cursor: \"pointer\",\n                    borderRadius: theme.borderRadius[4],\n                    \"&:hover\": {\n                      backgroundColor: theme.colors.backgroundHover,\n                    },\n                  }}\n                >\n                  <Radio value=\"ours\" />\n                  <Flex direction=\"column\" gap=\"1\">\n                    <Text variant=\"labels\">Ours</Text>\n                    <Text color=\"subtle\">\n                      Discard incoming tokens and use your existing project\n                      tokens instead\n                    </Text>\n                  </Flex>\n                </Flex>\n              </Label>\n\n              <Label>\n                <Flex\n                  gap=\"2\"\n                  css={{\n                    padding: theme.spacing[3],\n                    cursor: \"pointer\",\n                    borderRadius: theme.borderRadius[4],\n                    \"&:hover\": {\n                      backgroundColor: theme.colors.backgroundHover,\n                    },\n                  }}\n                >\n                  <Radio value=\"merge\" />\n                  <Flex direction=\"column\" gap=\"1\">\n                    <Text variant=\"labels\">Merge</Text>\n                    <Text color=\"subtle\">\n                      Combine both into your existing token (incoming styles\n                      override existing ones)\n                    </Text>\n                  </Flex>\n                </Flex>\n              </Label>\n            </Flex>\n          </RadioGroup>\n\n          <Flex as=\"details\" direction=\"column\" gap=\"1\">\n            <Text as=\"summary\">Show conflicting tokens</Text>\n            <Text\n              color=\"subtle\"\n              css={{\n                maxHeight: 150,\n                overflow: \"auto\",\n              }}\n            >\n              {conflicts.map((conflict) => conflict.tokenName).join(\", \")}\n            </Text>\n          </Flex>\n        </Flex>\n        <DialogActions>\n          <Button color=\"positive\" onClick={handleResolve}>\n            Continue\n          </Button>\n          <Button color=\"ghost\" onClick={handleCancel}>\n            Cancel\n          </Button>\n        </DialogActions>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "apps/builder/app/shared/tree-utils.test.ts",
    "content": "import { test, expect } from \"vitest\";\nimport type {\n  Instance,\n  StyleDecl,\n  Styles,\n  StyleSource,\n  StyleSourceSelection,\n} from \"@webstudio-is/sdk\";\nimport { getStyleDeclKey } from \"@webstudio-is/sdk\";\nimport {\n  cloneStyles,\n  findLocalStyleSourcesWithinInstances,\n  isDescendantOrSelf,\n} from \"./tree-utils\";\n\nconst createStyleSource = (\n  type: StyleSource[\"type\"],\n  id: StyleSource[\"id\"]\n): StyleSource => {\n  if (type === \"token\") {\n    return {\n      type,\n      id,\n      name: id,\n    };\n  }\n  return {\n    type,\n    id,\n  };\n};\n\nconst createStyleSourceSelection = (\n  instanceId: Instance[\"id\"],\n  values: StyleSource[\"id\"][]\n): StyleSourceSelection => {\n  return {\n    instanceId,\n    values,\n  };\n};\n\nconst createStyleDecl = (\n  styleSourceId: string,\n  breakpointId: string,\n  value?: string\n): StyleDecl => {\n  return {\n    styleSourceId,\n    breakpointId,\n    property: \"width\",\n    value: {\n      type: \"keyword\",\n      value: value ?? \"value\",\n    },\n  };\n};\n\nconst createStyleDeclPair = (\n  styleSourceId: string,\n  breakpointId: string,\n  state?: string,\n  value?: string\n) => {\n  return [\n    getStyleDeclKey({\n      styleSourceId,\n      breakpointId,\n      state,\n      property: \"width\",\n    }),\n    createStyleDecl(styleSourceId, breakpointId, value),\n  ] as const;\n};\n\ntest(\"clone styles with appled new style source ids\", () => {\n  const styles: Styles = new Map([\n    createStyleDeclPair(\"styleSource1\", \"bp1\"),\n    createStyleDeclPair(\"styleSource2\", \"bp2\"),\n    createStyleDeclPair(\"styleSource1\", \"bp3\"),\n    createStyleDeclPair(\"styleSource3\", \"bp4\"),\n    createStyleDeclPair(\"styleSource1\", \"bp5\"),\n    createStyleDeclPair(\"styleSource3\", \"bp6\"),\n  ]);\n  const clonedStyleSourceIds = new Map<StyleSource[\"id\"], StyleSource[\"id\"]>();\n  clonedStyleSourceIds.set(\"styleSource2\", \"newStyleSource2\");\n  clonedStyleSourceIds.set(\"styleSource3\", \"newStyleSource3\");\n  expect(cloneStyles(styles, clonedStyleSourceIds)).toEqual([\n    createStyleDecl(\"newStyleSource2\", \"bp2\"),\n    createStyleDecl(\"newStyleSource3\", \"bp4\"),\n    createStyleDecl(\"newStyleSource3\", \"bp6\"),\n  ]);\n});\n\ntest(\"find local style sources within instances\", () => {\n  const instanceIds = new Set([\"instance2\", \"instance4\"]);\n  const styleSources = [\n    createStyleSource(\"local\", \"local1\"),\n    createStyleSource(\"local\", \"local2\"),\n    createStyleSource(\"token\", \"token3\"),\n    createStyleSource(\"local\", \"local4\"),\n    createStyleSource(\"token\", \"token5\"),\n    createStyleSource(\"local\", \"local6\"),\n  ];\n  const styleSourceSelections = [\n    createStyleSourceSelection(\"instance1\", [\"local1\"]),\n    createStyleSourceSelection(\"instance2\", [\"local2\"]),\n    createStyleSourceSelection(\"instance3\", [\"token3\"]),\n    createStyleSourceSelection(\"instance4\", [\"local4\", \"token5\"]),\n    createStyleSourceSelection(\"instance5\", [\"local6\"]),\n  ];\n  expect(\n    findLocalStyleSourcesWithinInstances(\n      styleSources,\n      styleSourceSelections,\n      instanceIds\n    )\n  ).toEqual(new Set([\"local2\", \"local4\"]));\n});\n\ntest(\"is descendant or self\", () => {\n  expect(isDescendantOrSelf([\"1\", \"2\", \"3\"], [\"1\", \"2\", \"3\"])).toBe(true);\n  expect(isDescendantOrSelf([\"0\", \"1\", \"2\", \"3\"], [\"1\", \"2\", \"3\"])).toBe(true);\n  expect(isDescendantOrSelf([\"1\", \"2\", \"3\"], [\"0\", \"1\", \"2\", \"3\"])).toBe(false);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/tree-utils.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { shallowEqual } from \"shallow-equal\";\nimport type {\n  Instance,\n  Instances,\n  Props,\n  StyleDecl,\n  Styles,\n  StyleSource,\n  StyleSourceSelection,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport { collectionComponent, elementComponent } from \"@webstudio-is/sdk\";\nimport { isRichTextTree } from \"./content-model\";\n\n// slots can have multiple parents so instance should be addressed\n// with full rendered path to avoid double selections with slots\n// and support deletion of slot child from specific parent\n// selector starts with target instance and ends with root\nexport type InstanceSelector = Instance[\"id\"][];\n\nexport const areInstanceSelectorsEqual = (\n  left?: InstanceSelector,\n  right?: InstanceSelector\n) => {\n  if (left === undefined || right === undefined) {\n    return false;\n  }\n  return left.join(\",\") === right.join(\",\");\n};\n\nexport const isDescendantOrSelf = (\n  descendant: InstanceSelector,\n  self: InstanceSelector\n) => {\n  if (self.length === 0) {\n    return true;\n  }\n\n  if (descendant.length < self.length) {\n    return false;\n  }\n\n  const endSlice = descendant.slice(-self.length);\n\n  return shallowEqual(endSlice, self);\n};\n\nexport type DroppableTarget = {\n  parentSelector: InstanceSelector;\n  position: number | \"end\";\n};\n\nconst getCollectionDropTarget = (\n  instances: Instances,\n  dropTarget: DroppableTarget\n) => {\n  const [parentId, grandparentId] = dropTarget.parentSelector;\n  const parent = instances.get(parentId);\n  const grandparent = instances.get(grandparentId);\n  if (parent === undefined && grandparent?.component === collectionComponent) {\n    return {\n      parentSelector: dropTarget.parentSelector.slice(1),\n      position: dropTarget.position,\n    };\n  }\n};\n\nexport const getInstanceOrCreateFragmentIfNecessary = (\n  instances: Instances,\n  dropTarget: DroppableTarget\n) => {\n  const [parentId] = dropTarget.parentSelector;\n  const instance = instances.get(parentId);\n  if (instance === undefined) {\n    return;\n  }\n  // slot should accept only single child\n  // otherwise multiple slots will have to maintain own children\n  // here all slot children are wrapped with fragment instance\n  if (instance.component === \"Slot\") {\n    if (instance.children.length === 0) {\n      const id = nanoid();\n      const fragment: Instance = {\n        type: \"instance\",\n        id,\n        component: \"Fragment\",\n        children: [],\n      };\n      instances.set(id, fragment);\n      instance.children.push({ type: \"id\", value: id });\n      return {\n        parentSelector: [fragment.id, ...dropTarget.parentSelector],\n        position: dropTarget.position,\n      };\n    }\n    // first slot child is always fragment\n    if (instance.children[0].type === \"id\") {\n      const fragmentId = instance.children[0].value;\n      return {\n        parentSelector: [fragmentId, ...dropTarget.parentSelector],\n        position: dropTarget.position,\n      };\n    }\n  }\n  return;\n};\n\n/**\n * Navigator tree and canvas dnd do not have text representation\n * and position does not consider it and include only instances.\n * This function adjust the position to consider text children\n */\nconst adjustChildrenPosition = (\n  children: Instance[\"children\"],\n  position: number\n) => {\n  let newPosition = 0;\n  let idPosition = 0;\n  for (let index = 0; index < children.length; index += 1) {\n    newPosition = index;\n    if (idPosition === position) {\n      return newPosition;\n    }\n    const child = children[index];\n    if (child.type === \"id\") {\n      idPosition += 1;\n    }\n  }\n  // the index after last item\n  return newPosition + 1;\n};\n\n/**\n * Wrap children before and after drop target with spans\n * to preserve lexical specific components while allowing\n * to insert into editable components\n */\nexport const wrapEditableChildrenAroundDropTargetMutable = (\n  instances: Instances,\n  props: Props,\n  metas: Map<string, WsComponentMeta>,\n  dropTarget: DroppableTarget\n) => {\n  const [parentId] = dropTarget.parentSelector;\n  const parentInstance = instances.get(parentId);\n  if (parentInstance === undefined || parentInstance.children.length === 0) {\n    return;\n  }\n  // wrap only containers with text and rich text childre\n  const isParentRichText = isRichTextTree({\n    instances,\n    props,\n    metas,\n    instanceId: parentId,\n  });\n  if (!isParentRichText) {\n    return;\n  }\n  const position =\n    dropTarget.position === \"end\"\n      ? parentInstance.children.length\n      : adjustChildrenPosition(parentInstance.children, dropTarget.position);\n\n  const newChildren: Instance[\"children\"] = [];\n  let newPosition = 0;\n  // create left span when not at the beginning\n  if (position !== 0) {\n    const leftSpan: Instance = {\n      id: nanoid(),\n      type: \"instance\",\n      component: elementComponent,\n      tag: \"span\",\n      children: parentInstance.children.slice(0, position),\n    };\n    newChildren.push({ type: \"id\", value: leftSpan.id });\n    instances.set(leftSpan.id, leftSpan);\n    newPosition = 1;\n  }\n  // create right span when not in the end\n  if (position < parentInstance.children.length) {\n    const rightSpan: Instance = {\n      id: nanoid(),\n      type: \"instance\",\n      component: elementComponent,\n      tag: \"span\",\n      children: parentInstance.children.slice(position),\n    };\n    newChildren.push({ type: \"id\", value: rightSpan.id });\n    instances.set(rightSpan.id, rightSpan);\n  }\n  parentInstance.children = newChildren;\n  return {\n    parentSelector: dropTarget.parentSelector,\n    position: newPosition,\n  };\n};\n\nexport const getReparentDropTargetMutable = (\n  instances: Instances,\n  props: Props,\n  metas: Map<string, WsComponentMeta>,\n  dropTarget: DroppableTarget\n): undefined | DroppableTarget => {\n  dropTarget = getCollectionDropTarget(instances, dropTarget) ?? dropTarget;\n  dropTarget =\n    getInstanceOrCreateFragmentIfNecessary(instances, dropTarget) ?? dropTarget;\n  dropTarget =\n    wrapEditableChildrenAroundDropTargetMutable(\n      instances,\n      props,\n      metas,\n      dropTarget\n    ) ?? dropTarget;\n  return dropTarget;\n};\n\nexport const cloneStyles = (\n  styles: Styles,\n  clonedStyleSourceIds: Map<Instance[\"id\"], Instance[\"id\"]>\n) => {\n  const clonedStyles: StyleDecl[] = [];\n  for (const styleDecl of styles.values()) {\n    const styleSourceId = clonedStyleSourceIds.get(styleDecl.styleSourceId);\n    if (styleSourceId === undefined) {\n      continue;\n    }\n    clonedStyles.push({\n      ...styleDecl,\n      styleSourceId,\n    });\n  }\n  return clonedStyles;\n};\n\nexport const findLocalStyleSourcesWithinInstances = (\n  styleSources: IterableIterator<StyleSource> | StyleSource[],\n  styleSourceSelections:\n    | IterableIterator<StyleSourceSelection>\n    | StyleSourceSelection[],\n  instanceIds: Set<Instance[\"id\"]>\n) => {\n  const localStyleSourceIds = new Set<StyleSource[\"id\"]>();\n  for (const styleSource of styleSources) {\n    if (styleSource.type === \"local\") {\n      localStyleSourceIds.add(styleSource.id);\n    }\n  }\n\n  const subtreeLocalStyleSourceIds = new Set<StyleSource[\"id\"]>();\n  for (const { instanceId, values } of styleSourceSelections) {\n    // skip selections outside of subtree\n    if (instanceIds.has(instanceId) === false) {\n      continue;\n    }\n    // find only local style sources on selections\n    for (const styleSourceId of values) {\n      if (localStyleSourceIds.has(styleSourceId)) {\n        subtreeLocalStyleSourceIds.add(styleSourceId);\n      }\n    }\n  }\n\n  return subtreeLocalStyleSourceIds;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/trpc/trpc-client.ts",
    "content": "import type { AppRouter } from \"~/services/trcp-router.server\";\nimport { createTRPCProxyClient, httpBatchLink } from \"@trpc/client\";\n\nimport type {\n  AnyMutationProcedure,\n  AnyRouter,\n  inferRouterInputs,\n  inferRouterOutputs,\n} from \"@trpc/server\";\nimport { createRecursiveProxy } from \"@trpc/server/shared\";\nimport { useMemo, useState } from \"react\";\nimport { fetch } from \"~/shared/fetch.client\";\n\nexport const nativeClient = createTRPCProxyClient<AppRouter>({\n  links: [\n    httpBatchLink({\n      fetch,\n      url: \"/trpc\",\n    }),\n  ],\n});\n\ntype Procedures<T> = T extends AnyRouter\n  ? {\n      [Procedure in keyof T[\"_def\"][\"record\"]]: T[\"_def\"][\"record\"][Procedure] extends AnyMutationProcedure\n        ? {\n            useMutation: () => {\n              send: (\n                input: inferRouterInputs<T>[Procedure],\n                onSuccess?: (data: inferRouterOutputs<T>[Procedure]) => void\n              ) => void;\n              data?: inferRouterOutputs<T>[Procedure];\n              state: \"idle\" | \"submitting\";\n              error?: string | undefined;\n            };\n          }\n        : {\n            useQuery: () => {\n              load: (\n                input: inferRouterInputs<T>[Procedure],\n                onSuccess?: (data: inferRouterOutputs<T>[Procedure]) => void\n              ) => void;\n              data?: inferRouterOutputs<T>[Procedure];\n              state: \"idle\" | \"submitting\";\n              error?: string | undefined;\n            };\n          };\n    }\n  : never;\n\ntype Client = Record<\n  string,\n  Record<\n    string,\n    { query: (args: unknown) => unknown; mutate: (args: unknown) => unknown }\n  >\n>;\n\nexport const trpcClient: {\n  [SubRoute in keyof AppRouter[\"_def\"][\"record\"]]: Procedures<\n    AppRouter[\"_def\"][\"record\"][SubRoute]\n  >;\n} = createRecursiveProxy((options) => {\n  const path = [...options.path];\n  const hook = path.pop();\n  const [data, setData] = useState<unknown>(undefined);\n  const [state, setState] = useState<\"idle\" | \"submitting\">(\"idle\");\n  const [error, setError] = useState<string | undefined>(undefined);\n\n  if (hook !== \"useMutation\" && hook !== \"useQuery\") {\n    throw new Error(`Invalid hook ${hook}`);\n  }\n\n  // const method = path.join(\".\");\n  if (path.length !== 2) {\n    throw new Error(`Invalid path ${path}`);\n  }\n\n  const [namespace, method] = path;\n\n  const submit = useMemo(() => {\n    let requestId = 0;\n\n    return async (input: never, onSuccess?: (data: unknown) => void) => {\n      requestId += 1;\n      const currentRequestId = requestId;\n      try {\n        setState(\"submitting\");\n\n        // if (process.env.NODE_ENV !== \"production\") {\n        // await new Promise((resolve) => setTimeout(resolve, 1000));\n        // }\n\n        const result = await (nativeClient as unknown as Client)[namespace][\n          method\n        ][hook === \"useMutation\" ? \"mutate\" : \"query\"](input);\n\n        // Newer request has been made, ignore this one\n        // In the future to support optimistic updates we can call onComplete with the requestId order\n        if (currentRequestId !== requestId) {\n          return;\n        }\n\n        setData(result);\n        setError(undefined);\n        onSuccess?.(result);\n      } catch (error) {\n        if (currentRequestId !== requestId) {\n          return;\n        }\n\n        console.error(\"TRPC ERROR\", error);\n\n        if (error instanceof Error) {\n          setError(error.message);\n          return;\n        }\n        setError(\"Unknown error\");\n      } finally {\n        if (currentRequestId === requestId) {\n          setState(\"idle\");\n        }\n      }\n    };\n  }, [hook, namespace, method]);\n\n  return {\n    [hook === \"useMutation\" ? \"send\" : \"load\"]: submit,\n    data: data,\n    state,\n    error,\n  };\n}) as never;\n"
  },
  {
    "path": "apps/builder/app/shared/use-set-features.ts",
    "content": "import { useSearchParams } from \"@remix-run/react\";\nimport { parse, readLocal, setLocal } from \"@webstudio-is/feature-flags\";\nimport { useEffect } from \"react\";\n\n// Allows to set feature flags via URL parameter features=name1,name2,name3\nexport const useSetFeatures = () => {\n  const [searchParams] = useSearchParams();\n  useEffect(() => {\n    const features = searchParams.get(\"features\");\n    if (features) {\n      const currentFlags = readLocal();\n      for (const flag of parse(features)) {\n        if (currentFlags.includes(flag) === false) {\n          currentFlags.push(flag);\n        }\n      }\n      setLocal(currentFlags.join(\",\"));\n    }\n  }, [searchParams]);\n};\n"
  },
  {
    "path": "apps/builder/app/shared/visually-hidden.ts",
    "content": "/**\n * Radix's VisuallyHiddenPrimitive.Root https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/src/VisuallyHidden.tsx\n * component makes content from hidden elements accessible to screen readers.\n * react-aria VisuallyHidden https://github.com/adobe/react-spectrum/blob/e4bc3269fa41aa096700445c6bfa9c8620545e6a/packages/%40react-aria/visually-hidden/src/VisuallyHidden.tsx#L32-L43\n * The problem we're addressing is that the Radix and similar frameworks reuse the same Content children\n * for VisuallyHiddenPrimitive.Root within the Tooltip and similar components.\n * Using the same Content children, however, leads to duplicated React elements, breaking our 'isSelected' logic.\n * To prevent this, we check if an instance is a descendant of VisuallyHiddenPrimitive.Root, and if so,\n * we avoid rendering it.\n */\nexport const getIsVisuallyHidden = (currentElement: HTMLElement) => {\n  for (\n    let element: HTMLElement | null = currentElement;\n    element !== null;\n    element = element.parentElement\n  ) {\n    if (\n      element.style.overflow === \"hidden\" &&\n      element.style.clip === \"rect(0px, 0px, 0px, 0px)\" &&\n      element.style.position === \"absolute\" &&\n      element.style.width === \"1px\" &&\n      element.style.height === \"1px\"\n    ) {\n      return true;\n    }\n  }\n  return false;\n};\n"
  },
  {
    "path": "apps/builder/app/shared/webstudio-data-migrator.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport type { WebstudioData } from \"@webstudio-is/sdk\";\nimport { createDefaultPages } from \"@webstudio-is/project-build\";\nimport type { StyleProperty } from \"@webstudio-is/css-engine\";\nimport { migrateWebstudioDataMutable } from \"./webstudio-data-migrator\";\n\nconst emptyData: WebstudioData = {\n  pages: createDefaultPages({\n    rootInstanceId: \"rootInstanceId\",\n  }),\n  assets: new Map(),\n  dataSources: new Map(),\n  resources: new Map(),\n  instances: new Map(),\n  props: new Map(),\n  breakpoints: new Map(),\n  styleSources: new Map(),\n  styleSourceSelections: new Map(),\n  styles: new Map(),\n};\n\ntest(\"expand overflow shorthand\", () => {\n  const data = structuredClone(emptyData);\n  data.styles.set(\"base:local:overflow::hover\", {\n    breakpointId: \"base\",\n    styleSourceId: \"local\",\n    state: \":hover\",\n    property: \"overflow\" as StyleProperty,\n    value: {\n      type: \"tuple\",\n      value: [\n        { type: \"keyword\", value: \"auto\" },\n        { type: \"keyword\", value: \"hidden\" },\n      ],\n    },\n  });\n  migrateWebstudioDataMutable(data);\n  expect(Array.from(data.styles.values())).toEqual([\n    {\n      breakpointId: \"base\",\n      property: \"overflowX\",\n      state: \":hover\",\n      styleSourceId: \"local\",\n      value: { type: \"keyword\", value: \"auto\" },\n    },\n    {\n      breakpointId: \"base\",\n      property: \"overflowY\",\n      state: \":hover\",\n      styleSourceId: \"local\",\n      value: { type: \"keyword\", value: \"hidden\" },\n    },\n  ]);\n});\n"
  },
  {
    "path": "apps/builder/app/shared/webstudio-data-migrator.ts",
    "content": "import {\n  getStyleDeclKey,\n  type StyleDecl,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport { hyphenateProperty, toValue } from \"@webstudio-is/css-engine\";\nimport {\n  camelCaseProperty,\n  expandShorthands,\n  parseCssValue,\n} from \"@webstudio-is/css-data\";\n\n/**\n *\n * Transform loaded data and sync into server\n * before passing into application\n * should be idempotent because can be executed multiple times\n *\n * Very basic version of client migrations\n * Should be versioned eventually to avoid running every time\n *\n * For now patch is prevented by excluding empty transactions from sync queue\n *\n */\nexport const migrateWebstudioDataMutable = (data: WebstudioData) => {\n  for (const [styleDeclKey, styleDecl] of data.styles) {\n    const property = hyphenateProperty(styleDecl.property) as string;\n\n    // expands overflow shorthand into overflow-x and overflow-y longhands\n    // expands transition shorthand into transition-property, transition-duration, transition-timing-function, transition-delay longhands\n    // expands white-space into white-space-collapse and text-wrap-mode\n    if (\n      property === \"overflow\" ||\n      property === \"transition\" ||\n      property === \"white-space\" ||\n      property === \"background-position\"\n    ) {\n      data.styles.delete(styleDeclKey);\n      const longhands = expandShorthands([\n        [property, toValue(styleDecl.value)],\n      ]);\n      for (const [hyphenedProperty, value] of longhands) {\n        const longhandStyleDecl: StyleDecl = {\n          ...styleDecl,\n          property: camelCaseProperty(hyphenedProperty),\n          value: parseCssValue(hyphenedProperty, value),\n        };\n        data.styles.set(getStyleDeclKey(longhandStyleDecl), longhandStyleDecl);\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "apps/builder/docker-compose.yaml",
    "content": "version: \"3.1\"\n\nvolumes:\n  db:\n\nservices:\n  db:\n    image: postgres\n    # Uncomment to log all queries\n    command: [\"postgres\", \"-c\", \"log_statement=all\"]\n    restart: always\n    environment:\n      # postgresql://user:pass@localhost:5432/webstudio\n      POSTGRES_PASSWORD: pass\n      POSTGRES_DB: webstudio\n      POSTGRES_USER: user\n    volumes:\n      - db:/var/lib/postgresql/data\n    ports:\n      - ${PGPORT:-5432}:5432\n"
  },
  {
    "path": "apps/builder/docs/test-cases.md",
    "content": "1. Instance states\n\n   - Hover over an instance, check if its outline is correct\n   - Select an instance, check if its outline is correct\n\n1. Create instances by drag&drop\n\n   - Drag a component over an instance - check if its outline is correct\n   - Drag a Box over a Box, drop it - check if it was inserted in that Box\n   - Drag a Box and place it before a Box - check if it was inserted before\n   - Drag a Box and place it after a Box - check if it was inserted after\n   - Drag a Box exactly between 2 Boxes - check if it was inserted between\n   - Drag a Box to the bottom edge - check if parent Box gets outlined and insertion happened after that dragged over Box inside that outlined parent\n   - Drag a Box to the top edge - check if parent Box gets outlined and insertion happened before that dragged over Box inside that outlined parent\n   - Create a Box with child Boxes, where children are aligned vertically - repeat the tests above with trying to insert inside the new Box\n   - Try to drop into a Paragraph - check that it's not allowed, and it drops next to the Paragraph instead\n\n1. Moving instances on canvas by drag&drop\n\n   - Do the tests described in \"Create instances by drag&drop\",\n     but instead of moving a component from the panel to the canvas,\n     move a component that's already on the canvas\n   - Create a Paragraph, and make a word in it Bold\n     - drag & drop the Bold component - check that entire Paragraph have been moved\n   - Try to drop a Box inside itself - check that it's not allowed, and it drops next to it instead\n   - Create a Box that contains another Box, give the parent Box a padding\n     - drag the parent Box, and try to put it inside the child Box - check that it's not allowed, and it drops next to the parent Box instead\n\n1. Create instance by clicking on a component\n\n   - Make sure no instance is selected (no outline in the canvas)\n   - Click on a component in the panel - check that it's inserted at the end of the root instance\n   - Select an istance that can accept children (e.g. Box), by clicking on it on the canvas\n   - Click on a component in the panel - check that it's inserted at the end of the selected instance\n   - Select an instance that cannot accept children (e.g. Heading), but choose one that is not the last child of its parent\n   - Click on a component in the panel - check that it's inserted in the parent of the selected instance, and that it's positioned right after the selected instance\n\n1. Styles apply\n\n   - Select an instance\n   - Change any style\n   - See if it is applied\n\n1. Content editable components\n\n   - Add any contenteditable component (heading, paragraph, text ...)\n   - Check if you can drag&drop it on the canvas\n   - Double click or press enter to enter editing mode\n   - Press escape or click away to exit editing mode\n   - Selecting with the mouse any char or word shows a menu abov\n     e it\n   - Selecting with keybord does the same\n   - Changing selection using mouse or keyboard moves the menu\n   - Menu shows correctly on very left, right, top corners\n   - Typing renders typed characters as expected\n   - After editing and unfocusing the component content stays the way it was changed\n   - After each menu action unfocus component and see that content stays the way it was changed\n   - Run every action on first, middle and last word, on first line on second line and on the last line\n   - Enter multiline text, unfocus\n   - Check every change is also saved when after reloading\n   - Enter text and unfocus quickly - changes shouldn't disappear\n   - Keep typing text for some time and see in network tab that we send only a few requests, not one for each char\n\n1. Breakpoints\n\n   - Clicking on different breakpoints resizes the canvas\n   - Click on Edit breakpoints shows you breakpoints editor\n     - lets you change the breakpoint min-width value and it gets applied immediately on the canvas and on the width slider constraint\n     - adding a new breakpoint adds it, lets you specify the value, works like every other breakpoint\n     - Trash icon button shows confirmation dialog and upon confirmation\n       - Delete button deletes the corresponding styles of that breakpoint entirely\n       - Abort button doesn't delete and returns you to breakpoints editor\n     - Done button returns you to breakpoints selector\n   - Click anywhere outside of breakpoints closes breakpoints selector\n   - Click on the canvas closes breakpoints selector\n   - Styles added on each breakpoint are only applied to each breakpoint, verify that by resizing the canvas in preview mode\n\n1. Auth\n\n   - Try to access the dashboard when not logged in to make sure you will be redirected to the login page\n   - Open the login page when logged in to make sure you are redirected to the dashboard\n   - Ensure you can view a built project in an incognito tab\n   - Ensure you can view a forked project in an incognito tab\n   - Click on dev login, add the first four characters in AUTH_SECRET value in the `.env` value and make sure user is authenticated and redirected to the dashboard\n   - Click on dev login and write a wrong value to make sure you get an error message and stay in the login page.\n   - Click on login with GitHub and make sure that you are redirected to the dashboard\n   - Click on login with Google and make sure that you are redirected to the dashboard\n\n1. Default properties\n\n   - Add a Box component to the canvas\n   - Open Props panel\n   - See the default tag is provided and can be modified\n\n1. Asset manager\n\n   - Open assets panel\n   - Upload an image\n   - Check it loads and shows a progress bar\n   - Delete an asset by clicking on the `x` icon and then the delete button in the tooltip\n\n1. Navigator view settings\n\n   - Initially navigator always shown\n   - Check that `Menu > View > Undock navigator` is checked\n   - Click on \"Show navigator\"\n   - Navigator is now not shown\n   - New tab was added that lets you open navigator\n\n1. Navigator keyboard interactions\n\n   - Move focus to the navigator by clicking on a tree item\n   - Check that pressing `arrow up` and `arrow down` changes the selected item\n   - Check that you can expand and collapse the selected item using `arrow right`, `arrow left`, and `spacebar`\n   - Check that pressing `delete` or `backspace` deletes the selected item\n\n1. Navigator drag&drop\n\n   - Drag a tree item and put the cursor between some tree items\n     - check that a line appeared between the tree items\n     - check that after you drop, the dragged item moves between the tree items\n   - Drag a tree item over an empty `Box` item,\n     - check that an outline appears around the `Box` item,\n     - check that when you drop, the dragged item moves inside the `Box` item\n   - Drag a tree item over a non-empty but collapsed `Box` item\n     - check that the `Box` item expands automatically when you hold over it\n     - check that if you drop before it expands, the dragged item moves inside the `Box`, and becomes the last child\n   - Drag a tree item over a `Heading` item, check that it doesn't allow you to drop inside a `Heading`\n   - Make some text inside a `Heading` \"bold\" so that the `Heading` item in the tree gets a child item `Bold Text`\n     - check that you cannot drag the `Bold Text` item\n   - Start dragging a tree item but do not move the cursor vertically\n     - check that by moving horizontally you're able to change how deeply the item is nested inside the tree\n   - Drag an item between some tree items\n     - check that moving the cursor horizontally allows you to change the depth of the placement indicator line\n     - check that after you drop, the dragged item moves to the correct depth\n   - Check that after a drag&drop the dragged item is the selected item\n\n1. Flex panel\n\n   - Select an instance\n   - Select \"flexbox\" as the selected display option.\n   - Check if clicking a flex property icon opens a dropdown.\n   - Check if all flexbox states are computable through the icons seletables.\n   - Check if all flexbox states are computable through the grid.\n   - Check if gap inputs work.\n   - Check if tooltips are present on all icons(including gap input icons).\n   - Check that tooltips have a first time interaction delay.\n\n1. Creating a new page\n\n   - Open the pages panel\n   - Click on the \"New Page\" button in the panel header\n   - Enter a page name\n   - Enter a page path\n   - Click on the \"Create\" button\n   - Check than the page is created and selected as the current page\n   - Repeat the proccess and make sure it doens't allow you to create a page with an empty name or a path that's used for another page\n\n1. Deleting a page\n\n   - Open the pages panel\n   - Make sure you have pages other than the Home page, create one if you don't\n   - Click on the menu icon of a page other than the Home page\n   - Page settings should open\n   - Click on the delete icon in the page settings header\n   - Page settings should close\n   - Page should disappear from the list\n   - If the page was selected, the Home page should become selected instead\n   - In the Home page settings, the delete icon should be absent\n\n1. Editing a page\n\n   - Open the pages panel\n   - Click on the menu icon of a page\n   - Page settings should open\n   - Change the page name\n   - The name should change in the pages list as well (with a small delay)\n   - Change the page path\n   - Reload browser tab and open the page settings again and make sure your changes are persisted\n"
  },
  {
    "path": "apps/builder/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/builder\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"description\": \"Webstudio Builder UI as a service\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"prebuild\": \"which remix\",\n    \"build\": \"remix vite:build\",\n    \"css-to-ws\": \"NODE_OPTIONS=--conditions=webstudio css-to-ws\",\n    \"build:webflow-presets\": \"pnpm css-to-ws ./app/shared/copy-paste/plugin-webflow/style-presets.css ./app/shared/copy-paste/plugin-webflow/__generated__/style-presets.ts\",\n    \"build:tailwind-preflight\": \"tsx --conditions=webstudio ./app/shared/tailwind/preflight-bin.ts && prettier --write ./app/shared/tailwind/__generated__/preflight.ts\",\n    \"start\": \"remix-serve build/server/index.js\",\n    \"dev\": \"remix vite:dev\",\n    \"dev:auth\": \"DEBUG=OAuth2Strategy,ws:* remix vite:dev\",\n    \"typecheck\": \"tsgo --noEmit --emitDeclarationOnly false\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@atlaskit/pragmatic-drag-and-drop\": \"^1.7.4\",\n    \"@bomb.sh/args\": \"^0.3.1\",\n    \"@codemirror/autocomplete\": \"^6.18.6\",\n    \"@codemirror/commands\": \"^6.8.0\",\n    \"@codemirror/lang-css\": \"^6.3.1\",\n    \"@codemirror/lang-html\": \"^6.4.9\",\n    \"@codemirror/lang-javascript\": \"^6.2.3\",\n    \"@codemirror/lang-markdown\": \"^6.3.2\",\n    \"@codemirror/language\": \"^6.11.0\",\n    \"@codemirror/lint\": \"^6.8.5\",\n    \"@codemirror/state\": \"^6.5.2\",\n    \"@codemirror/view\": \"^6.36.4\",\n    \"@emotion/hash\": \"^0.9.2\",\n    \"@floating-ui/dom\": \"^1.6.13\",\n    \"@fontsource-variable/inter\": \"^5.0.20\",\n    \"@fontsource-variable/manrope\": \"^5.0.20\",\n    \"@fontsource/roboto-mono\": \"^5.0.18\",\n    \"@lexical/headless\": \"^0.21.0\",\n    \"@lexical/link\": \"^0.21.0\",\n    \"@lexical/react\": \"^0.21.0\",\n    \"@lexical/selection\": \"^0.21.0\",\n    \"@lexical/utils\": \"^0.21.0\",\n    \"@lezer/common\": \"^1.2.3\",\n    \"@lezer/css\": \"^1.1.11\",\n    \"@lezer/highlight\": \"^1.2.1\",\n    \"@nanostores/react\": \"^0.8.0\",\n    \"@nkzw/use-relative-time\": \"^1.1.0\",\n    \"@radix-ui/react-select\": \"^2.2.2\",\n    \"@radix-ui/react-tooltip\": \"^1.2.4\",\n    \"@react-aria/interactions\": \"^3.23.0\",\n    \"@react-aria/utils\": \"^3.27.0\",\n    \"@remix-run/node\": \"^2.16.5\",\n    \"@remix-run/react\": \"^2.16.5\",\n    \"@remix-run/serve\": \"^2.16.5\",\n    \"@remix-run/server-runtime\": \"^2.16.5\",\n    \"@trpc/client\": \"^10.45.2\",\n    \"@trpc/server\": \"^10.45.2\",\n    \"@tsndr/cloudflare-worker-jwt\": \"^2.5.3\",\n    \"@unocss/core\": \"66.1.2\",\n    \"@unocss/preset-legacy-compat\": \"66.1.2\",\n    \"@unocss/preset-wind3\": \"66.1.2\",\n    \"@vercel/remix\": \"2.15.3\",\n    \"@webstudio-is/asset-uploader\": \"workspace:*\",\n    \"@webstudio-is/authorization-token\": \"workspace:*\",\n    \"@webstudio-is/css-data\": \"workspace:*\",\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/dashboard\": \"workspace:*\",\n    \"@webstudio-is/design-system\": \"workspace:*\",\n    \"@webstudio-is/domain\": \"workspace:*\",\n    \"@webstudio-is/feature-flags\": \"workspace:*\",\n    \"@webstudio-is/fonts\": \"workspace:*\",\n    \"@webstudio-is/html-data\": \"workspace:*\",\n    \"@webstudio-is/http-client\": \"workspace:*\",\n    \"@webstudio-is/icons\": \"workspace:*\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/postgrest\": \"workspace:*\",\n    \"@webstudio-is/project\": \"workspace:*\",\n    \"@webstudio-is/project-build\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"args-tokenizer\": \"^0.3.0\",\n    \"bcp-47\": \"^2.1.0\",\n    \"change-case\": \"^5.4.4\",\n    \"colorjs.io\": \"^0.6.1\",\n    \"cookie\": \"^1.0.1\",\n    \"css-tree\": \"^3.1.0\",\n    \"debug\": \"^4.3.7\",\n    \"downshift\": \"^6.1.7\",\n    \"entrijs\": \"^0.0.11\",\n    \"fast-deep-equal\": \"^3.1.3\",\n    \"immer\": \"^10.1.1\",\n    \"immerhin\": \"^0.10.0\",\n    \"isbot\": \"^5.1.25\",\n    \"lexical\": \"^0.21.0\",\n    \"match-sorter\": \"^8.0.0\",\n    \"micromark\": \"^4.0.2\",\n    \"micromark-extension-gfm\": \"^3.0.0\",\n    \"nanoevents\": \"^9.1.0\",\n    \"nanoid\": \"^5.1.5\",\n    \"nanostores\": \"^0.11.3\",\n    \"papaparse\": \"^5.5.3\",\n    \"parse5\": \"7.3.0\",\n    \"picocolors\": \"^1.1.1\",\n    \"pretty-bytes\": \"^6.1.1\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-error-boundary\": \"^5.0.0\",\n    \"remix-auth\": \"^3.7.0\",\n    \"remix-auth-form\": \"^1.5.0\",\n    \"remix-auth-github\": \"^1.7.0\",\n    \"remix-auth-google\": \"^2.0.0\",\n    \"remix-auth-oauth2\": \"^2.3.0\",\n    \"shallow-equal\": \"^3.1.0\",\n    \"slugify\": \"^1.6.6\",\n    \"strip-indent\": \"^4.0.0\",\n    \"tiny-invariant\": \"^1.3.3\",\n    \"title-case\": \"^4.3.2\",\n    \"urlpattern-polyfill\": \"^10.1.0\",\n    \"use-debounce\": \"^10.0.4\",\n    \"valid-filename\": \"^4.0.0\",\n    \"warn-once\": \"^0.1.1\",\n    \"zod\": \"^3.24.2\",\n    \"zod-validation-error\": \"^3.4.0\"\n  },\n  \"devDependencies\": {\n    \"@remix-run/dev\": \"^2.16.5\",\n    \"@types/debug\": \"^4.1.12\",\n    \"@types/dom-navigation\": \"^1.0.5\",\n    \"@types/papaparse\": \"^5.5.2\",\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@vitest/coverage-v8\": \"3.1.2\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"css.escape\": \"^1.5.1\",\n    \"fast-glob\": \"^3.3.2\",\n    \"html-tags\": \"^4.0.0\",\n    \"react-router-dom\": \"^6.30.0\",\n    \"react-test-renderer\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"type-fest\": \"^4.37.0\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\",\n    \"vitest\": \"^3.1.2\"\n  },\n  \"sideEffects\": false,\n  \"license\": \"AGPL-3.0-or-later\"\n}\n"
  },
  {
    "path": "apps/builder/public/robots.txt",
    "content": "User-agent: *\nAllow: /login\nDisallow: /\n"
  },
  {
    "path": "apps/builder/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"app\", \"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"],\n\n  \"compilerOptions\": {\n    \"target\": \"ES2023\",\n    \"types\": [\n      \"@remix-run/node\",\n      \"vite/client\",\n      \"react/canary\",\n      \"react-dom/canary\",\n      \"@types/dom-navigation\"\n    ],\n\n    \"paths\": {\n      \"~/*\": [\"./app/*\"]\n    },\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": null,\n    \"noEmit\": true,\n    \"allowJs\": true,\n    \"checkJs\": true\n  }\n}\n"
  },
  {
    "path": "apps/builder/vite.config.ts",
    "content": "import path, { resolve } from \"node:path\";\nimport { defineConfig, type CorsOptions } from \"vite\";\nimport { vitePlugin as remix } from \"@remix-run/dev\";\nimport { vercelPreset } from \"@vercel/remix/vite\";\nimport type { IncomingMessage } from \"node:http\";\nimport pc from \"picocolors\";\n\nimport {\n  getAuthorizationServerOrigin,\n  isBuilderUrl,\n} from \"./app/shared/router-utils/origins\";\nimport { readFileSync, existsSync } from \"node:fs\";\nimport fg from \"fast-glob\";\n\nconst rootDir = [\"..\", \"../..\", \"../../..\"]\n  .map((dir) => path.join(__dirname, dir))\n  .find((dir) => existsSync(path.join(dir, \".git\")));\n\nconst hasPrivateFolders =\n  fg.sync([path.join(rootDir ?? \"\", \"packages/*/private-src/*\")], {\n    ignore: [\"**/node_modules/**\"],\n  }).length > 0;\n\nconst conditions = hasPrivateFolders\n  ? [\"webstudio-private\", \"webstudio\"]\n  : [\"webstudio\"];\n\nexport default defineConfig(({ mode }) => {\n  if (mode === \"development\") {\n    // Enable self-signed certificates for development service 2 service fetch calls.\n    // This is particularly important for secure communication with the oauth.ws.token endpoint.\n    process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n  }\n\n  return {\n    plugins: [\n      remix({\n        presets: [vercelPreset()],\n        future: {\n          v3_lazyRouteDiscovery: false,\n          v3_relativeSplatPath: false,\n          v3_singleFetch: false,\n          v3_fetcherPersist: false,\n          v3_throwAbortReason: false,\n        },\n      }),\n      {\n        name: \"request-timing-logger\",\n        configureServer(server) {\n          server.middlewares.use((req, res, next) => {\n            const start = Date.now();\n            res.on(\"finish\", () => {\n              const duration = Date.now() - start;\n              if (\n                !(\n                  req.url?.startsWith(\"/@\") ||\n                  req.url?.startsWith(\"/app\") ||\n                  req.url?.includes(\"/node_modules\")\n                )\n              ) {\n                console.info(\n                  `[${req.method}] ${req.url} - ${duration}ms : ${pc.dim(req.headers.host)}`\n                );\n              }\n            });\n            next();\n          });\n        },\n      },\n    ],\n    resolve: {\n      conditions: [...conditions, \"browser\", \"development|production\"],\n      alias: [\n        {\n          find: \"~\",\n          replacement: resolve(\"app\"),\n        },\n\n        // before 2,899.74 kB, after 2,145.98 kB\n        {\n          find: \"@supabase/node-fetch\",\n          replacement: resolve(\"./app/shared/empty.ts\"),\n        },\n      ],\n    },\n    ssr: {\n      resolve: {\n        conditions: [...conditions, \"node\", \"development|production\"],\n      },\n    },\n    define: {\n      \"process.env.NODE_ENV\": JSON.stringify(mode),\n    },\n    server: {\n      // Service-to-service OAuth token call requires a specified host for the wstd.dev domain\n      host: \"wstd.dev\",\n      // Needed for SSL\n      proxy: {},\n\n      https: {\n        key: readFileSync(\"../../https/privkey.pem\"),\n        cert: readFileSync(\"../../https/fullchain.pem\"),\n      },\n      cors: ((\n        req: IncomingMessage,\n        callback: (error: Error | null, options: CorsOptions | null) => void\n      ) => {\n        // Handle CORS preflight requests in development to mimic Remix production behavior\n        if (req.method === \"OPTIONS\" || req.method === \"POST\") {\n          if (req.headers.origin != null && req.url != null) {\n            const url = new URL(req.url, `https://${req.headers.host}`);\n\n            // Allow CORS for /builder-logout path when requested from the authorization server\n            if (url.pathname === \"/builder-logout\" && isBuilderUrl(url.href)) {\n              return callback(null, {\n                origin: getAuthorizationServerOrigin(url.href),\n                preflightContinue: false,\n                credentials: true,\n              });\n            }\n          }\n\n          if (req.method === \"OPTIONS\") {\n            // Respond with method not allowed for other preflight requests\n            return callback(null, {\n              preflightContinue: false,\n              optionsSuccessStatus: 405,\n            });\n          }\n        }\n\n        // Disable CORS for all other requests\n        return callback(null, {\n          origin: false,\n        });\n      }) as never,\n    },\n    envPrefix: \"GITHUB_\",\n  };\n});\n"
  },
  {
    "path": "apps/builder/vitest.config.ts",
    "content": "import { resolve } from \"node:path\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  resolve: {\n    alias: [\n      {\n        find: \"~\",\n        replacement: resolve(\"app\"),\n      },\n    ],\n    conditions: [\"webstudio\", \"browser\", \"development|production\"],\n    // conditions: [\"webstudio\", ...defaultClientConditions],\n  },\n  // resolve webstudio condition in tests\n  ssr: {\n    resolve: {\n      conditions: [\"webstudio\", \"node\", \"development|production\"],\n      // conditions: [\"webstudio\", ...defaultServerConditions],\n    },\n  },\n});\n"
  },
  {
    "path": "codemod/migrate-css-variables.ts",
    "content": "/**\n * To run it go to the root:\n *    $ pnpm tsx ./codemod/migrate-css-theme.ts\n *    $ pnpm run format\n *    $ pnpm run checks\n */\n/* eslint-disable no-console, @typescript-eslint/no-explicit-any  */\n/// <reference lib=\"es2021\" />\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\n\nconst getRecursiveFileReads = async (dir: string) => {\n  const results: any = [];\n  const files = await fs.readdir(dir);\n  for (const file of files) {\n    const filePath = path.join(dir, file);\n    const stat = await fs.stat(filePath);\n    if (stat.isDirectory() && file !== \"node_modules\") {\n      results.push(...(await getRecursiveFileReads(filePath)));\n      continue;\n    }\n\n    if (path.extname(file) !== \".tsx\" && path.extname(file) !== \".ts\") {\n      continue;\n    }\n\n    const buffer = await fs.readFile(filePath);\n\n    results.push({\n      filePath,\n      buffer,\n    });\n  }\n  return results;\n};\n\nconst readDirectory = async (dir: string) => {\n  const results = await getRecursiveFileReads(dir);\n  return results;\n};\n\nconst printProperty = (name: string) => {\n  if (/^[0-9a-z]+$/i.test(name) && /^[0-9]/.test(name) === false) {\n    return `.${name}`;\n  }\n  return /^[0-9]+$/.test(name) ? `[${name}]` : `[\"${name}\"]`;\n};\n\nconst printVariable = (variableName: string, groupName: string) =>\n  `theme${printProperty(groupName)}${printProperty(variableName)}`;\n\nconst guessGroupName = (variableName: string) => {\n  if (\n    /^(slate|yellow|orange|blue|sky|green|gray|red|mint|amber|lime|gold|cyan|indigo|grass|crimson|pink|purple|teal|violet|bronze|brown|tomato|plum)A?[0-9]+$/.test(\n      variableName\n    ) ||\n    [\n      \"panel\",\n      \"loContrast\",\n      \"hiContrast\",\n      \"hint\",\n      \"muted\",\n      \"highContrast\",\n      \"transparentExtreme\",\n      \"background\",\n    ].includes(variableName)\n  ) {\n    return \"colors\";\n  }\n  if (variableName === \"sans\" || variableName === \"mono\") {\n    return \"fonts\";\n  }\n  return;\n};\n\nconst migrateVariables = (originalCode: string) => {\n  let code = originalCode;\n\n  // \"$bar$1\" -> theme.bar[1]\n  code = code.replaceAll(\n    /\"\\$([a-z0-9]+)(?:\\$([a-z0-9]+))?\"/gi,\n    (orig, match1, match2) => {\n      if (match2) {\n        return printVariable(match2, match1);\n      }\n      const groupName = guessGroupName(match1);\n      if (groupName) {\n        return printVariable(match1, groupName);\n      }\n      return orig;\n    }\n  );\n\n  const replaceStringContent = (\n    originalString: string,\n    originalStringContent: string\n  ) => {\n    const stringContent = originalStringContent.replaceAll(\n      /\\$([a-z0-9]+)(?:\\$([a-z0-9]+))?/gi,\n      (orig, match1, match2) => {\n        if (match2) {\n          return `\\${${printVariable(match2, match1)}}`;\n        }\n        const groupName = guessGroupName(match1);\n        if (groupName) {\n          return `\\${${printVariable(match1, groupName)}}`;\n        }\n        return orig;\n      }\n    );\n\n    if (stringContent === originalStringContent) {\n      return originalString;\n    }\n    return `\\`${stringContent}\\``;\n  };\n\n  // \"something $bar$1\" -> `something ${theme.bar[1]}`\n  code = code.replaceAll(/\"([^\"]*)\"/g, replaceStringContent);\n\n  // `something $bar$1` -> `something ${theme.bar[1]}`\n  code = code.replaceAll(/`([^`]*)`/g, replaceStringContent);\n\n  return code;\n};\n\nconst addImport = (code: string, filePath: string) => {\n  let importCode = `import { theme } from \"@webstudio-is/design-system\";`;\n\n  const pathParts = filePath.split(path.sep);\n  const designSystemIndex = pathParts.indexOf(\"design-system\");\n\n  if (designSystemIndex !== -1) {\n    importCode = `import { theme } from \"${\"../\".repeat(\n      pathParts.length - designSystemIndex - 3\n    )}stitches.config\";`;\n  }\n\n  const lines = code.split(\"\\n\");\n\n  // while looking from the bottom up,\n  // insert our import code before the first import we find\n  const nextLinesReverse: string[] = [];\n  let inserted = false;\n  for (let i = lines.length - 1; i >= 0; i--) {\n    const line = lines[i];\n    if (\n      (line.startsWith(\"import\") || line.startsWith(\"} from\")) &&\n      inserted === false\n    ) {\n      nextLinesReverse.push(importCode);\n      inserted = true;\n    }\n    nextLinesReverse.push(line);\n  }\n\n  if (inserted === false) {\n    nextLinesReverse.push(importCode);\n  }\n\n  return nextLinesReverse.reverse().join(\"\\n\");\n};\n\nconst update = async ({ filePath, buffer }) => {\n  if (filePath.endsWith(\"stitches.config.ts\") || filePath.endsWith(\".d.ts\")) {\n    return;\n  }\n\n  const originalCode = buffer.toString(\"utf-8\");\n\n  let code = migrateVariables(originalCode);\n\n  if (code === originalCode) {\n    return;\n  }\n\n  code = addImport(code, filePath);\n\n  await fs.writeFile(filePath, code);\n};\n\nconst main = async () => {\n  const files = [\n    ...(await readDirectory(path.resolve(__dirname, \"..\", \"packages\"))),\n    ...(await readDirectory(path.resolve(__dirname, \"..\", \"apps\"))),\n  ];\n  files.forEach(update);\n};\n\nmain();\n"
  },
  {
    "path": "codemod/migrate-tokens.ts",
    "content": "/**\n * To run it go to the root: $ pnpm tsx ./codemod/migrate-spacing.ts\n */\nimport fs from \"fs/promises\";\nimport path from \"path\";\n\nconst getRecursiveFileReads = async (dir: string) => {\n  const results: any = [];\n  const files = await fs.readdir(dir);\n  for (const file of files) {\n    const filePath = path.join(dir, file);\n    const stat = await fs.stat(filePath);\n    if (stat.isDirectory() && file !== \"node_modules\") {\n      results.push(...(await getRecursiveFileReads(filePath)));\n      continue;\n    }\n\n    if (path.extname(file) !== \".tsx\" && path.extname(file) !== \".ts\") {\n      continue;\n    }\n\n    const buffer = await fs.readFile(filePath);\n\n    results.push({\n      filePath,\n      buffer,\n    });\n  }\n  return results;\n};\n\nconst readDirectory = async (dir: string) => {\n  const results = await getRecursiveFileReads(dir);\n  console.info(results.length);\n  return results;\n};\n\nconst spaceProps = [\n  \"size\",\n  \"sizes\",\n  \"blockSize\",\n  \"minBlockSize\",\n  \"maxBlockSize\",\n  \"inlineSize\",\n  \"minInlineSize\",\n  \"maxInlineSize\",\n  \"width\",\n  \"minWidth\",\n  \"maxWidth\",\n  \"height\",\n  \"minHeight\",\n  \"maxHeight\",\n  \"flexBasis\",\n  \"gridTemplateColumns\",\n  \"gridTemplateRows\",\n\n  \"gap\",\n  \"gridGap\",\n  \"columnGap\",\n  \"gridColumnGap\",\n  \"rowGap\",\n  \"gridRowGap\",\n  \"inset\",\n  \"insetBlock\",\n  \"insetBlockEnd\",\n  \"insetBlockStart\",\n  \"insetInline\",\n  \"insetInlineEnd\",\n  \"insetInlineStart\",\n  \"margin\",\n  \"marginTop\",\n  \"marginRight\",\n  \"marginBottom\",\n  \"marginLeft\",\n  \"marginBlock\",\n  \"marginBlockEnd\",\n  \"marginBlockStart\",\n  \"marginInline\",\n  \"marginInlineEnd\",\n  \"marginInlineStart\",\n  \"padding\",\n  \"paddingTop\",\n  \"paddingRight\",\n  \"paddingBottom\",\n  \"paddingLeft\",\n  \"paddingBlock\",\n  \"paddingBlockEnd\",\n  \"paddingBlockStart\",\n  \"paddingInline\",\n  \"paddingInlineEnd\",\n  \"paddingInlineStart\",\n  \"top\",\n  \"right\",\n  \"bottom\",\n  \"left\",\n  \"scrollMargin\",\n  \"scrollMarginTop\",\n  \"scrollMarginRight\",\n  \"scrollMarginBottom\",\n  \"scrollMarginLeft\",\n  \"scrollMarginX\",\n  \"scrollMarginY\",\n  \"scrollMarginBlock\",\n  \"scrollMarginBlockEnd\",\n  \"scrollMarginBlockStart\",\n  \"scrollMarginInline\",\n  \"scrollMarginInlineEnd\",\n  \"scrollMarginInlineStart\",\n  \"scrollPadding\",\n  \"scrollPaddingTop\",\n  \"scrollPaddingRight\",\n  \"scrollPaddingBottom\",\n  \"scrollPaddingLeft\",\n  \"scrollPaddingX\",\n  \"scrollPaddingY\",\n  \"scrollPaddingBlock\",\n  \"scrollPaddingBlockEnd\",\n  \"scrollPaddingBlockStart\",\n  \"scrollPaddingInline\",\n  \"scrollPaddingInlineEnd\",\n  \"scrollPaddingInlineStart\",\n\n  \"mx\",\n  \"my\",\n  \"ml\",\n  \"mb\",\n  \"mr\",\n  \"mt\",\n  \"m\",\n  \"px\",\n  \"py\",\n  \"pl\",\n  \"pb\",\n  \"pr\",\n  \"pt\",\n  \"p\",\n];\n\nconst spaceValues = new Map([\n  [\"$sizes$1\", \"3\"],\n  [\"$sizes$2\", \"5\"],\n  [\"$sizes$3\", \"9\"],\n  [\"$sizes$4\", \"10\"],\n  [\"$sizes$5\", \"11\"],\n  [\"$sizes$6\", \"13\"],\n  [\"$sizes$7\", \"17\"],\n  [\"$sizes$8\", \"19\"],\n  [\"$sizes$9\", \"20\"],\n  [\"$space$1\", \"3\"],\n  [\"$space$2\", \"5\"],\n  [\"$space$3\", \"9\"],\n  [\"$space$4\", \"10\"],\n  [\"$space$5\", \"11\"],\n  [\"$space$6\", \"13\"],\n  [\"$space$7\", \"17\"],\n  [\"$space$8\", \"19\"],\n  [\"$space$9\", \"20\"],\n  [\"$styleSection\", \"5\"],\n  [\"$sidebarLeft\", \"16\"],\n  [\"$sidebarRight\", \"30\"],\n\n  [\"$1\", \"3\"],\n  [\"$2\", \"5\"],\n  [\"$3\", \"9\"],\n  [\"$4\", \"10\"],\n  [\"$5\", \"11\"],\n  [\"$6\", \"13\"],\n  [\"$7\", \"17\"],\n  [\"$8\", \"19\"],\n  [\"$9\", \"20\"],\n\n  [\"1px\", \"1\"],\n  [\"2px\", \"2\"],\n  [\"3px\", \"2\"],\n  [\"6px\", \"4\"],\n  [\"7px\", \"4\"],\n  [\"8px\", \"5\"],\n]);\n\nconst fontSizeProps = [\"fontSize\"];\nconst fontSizeValues = new Map([\n  [\"$1\", \"3\"],\n  [\"$2\", \"3\"],\n  [\"$3\", \"4\"],\n  [\"$4\", \"4\"],\n  [\"$5\", \"5\"],\n  [\"$6\", \"6\"],\n  [\"$7\", \"7\"],\n  [\"$8\", \"8\"],\n  [\"$9\", \"9\"],\n  [\"14px\", \"4\"],\n]);\n\nconst lineHeightProps = [\"lineHeight\"];\nconst lineHeightValues = new Map([\n  [\"15px\", \"3\"],\n  [\"16px\", \"3\"],\n  [\"20px\", \"4\"],\n]);\n\nconst borderRadiusProps = [\"borderRadius\"];\nconst borderRadiusValues = new Map([\n  [\"$1\", \"4\"],\n  [\"$2\", \"6\"],\n  [\"$3\", \"7\"],\n  [\"$round\", \"round\"],\n  [\"$pill\", \"pill\"],\n]);\n\nconst updateProperty = (\n  line: string,\n  props: Array<string>,\n  values: Map<string, string>,\n  group: string\n) => {\n  for (const prop of props) {\n    const regex = new RegExp(`${prop}: \"(.+)\"`, \"g\");\n    const match = regex.exec(line);\n    if (match && match[1]) {\n      let value = match[1];\n      for (const [key, nextValue] of values) {\n        if (value.includes(key) === false) continue;\n        // This is to allow the script to run multiple times\n        value = value.replaceAll(`$${group}$`, `__${group}__`);\n\n        value = value.replaceAll(key, `__${group}__${nextValue}`);\n      }\n\n      const next = `${prop}: \"${value}\"`;\n      line = line.replace(regex, next);\n    }\n  }\n  line = line.replaceAll(`__${group}__`, `$${group}$`);\n  return line;\n};\n\nconst update = async ({ filePath, buffer }) => {\n  let code = buffer.toString(\"utf-8\");\n  const lines = code.split(\"\\n\");\n  const nextLines: Array<string> = [];\n  let line: string = \"\";\n  for (line of lines) {\n    line = updateProperty(line, spaceProps, spaceValues, \"spacing\");\n    line = updateProperty(line, fontSizeProps, fontSizeValues, \"fontSize\");\n    line = updateProperty(\n      line,\n      lineHeightProps,\n      lineHeightValues,\n      \"lineHeight\"\n    );\n    line = updateProperty(\n      line,\n      borderRadiusProps,\n      borderRadiusValues,\n      \"borderRadius\"\n    );\n    nextLines.push(line);\n  }\n  await fs.writeFile(filePath, nextLines.join(\"\\n\"));\n};\n\nconst main = async () => {\n  const src = path.resolve(__dirname, \"..\");\n  const files = await readDirectory(src);\n  files.forEach(update);\n};\n\nmain();\n"
  },
  {
    "path": "fixtures/README.md",
    "content": "# Webstudio Fixtures\n\nFixtures are test projects that ensure the CLI and build process work correctly across different deployment targets.\n\n### Updating Fixtures After Changes\n\n```bash\n# From repository root - updates all fixtures\npnpm fixtures\n```\n\nThis command will:\n\n1. Link all fixtures to their respective projects\n2. Sync the latest data from the development environment\n3. Build all fixtures with the latest code\n\n### Working with Individual Fixtures\n\n```bash\n# Go to fixture folder\ncd fixtures/webstudio-features\n\n# Link to project\npnpm fixtures:link\n\n# Sync latest data\npnpm fixtures:sync\n\n# Build fixture\npnpm fixtures:build\n\n# Start it\npnpm run dev\n```\n\n## Adding Assets to Fixtures\n\nTo add a new asset (image, video, audio, etc.) to a fixture:\n\n1. **Publish to staging**\n\n   - Make your changes in the builder\n   - Publish to the staging environment\n\n2. **Get the build ID from CI**\n\n   - After publishing, check the CI logs\n   - Find the build ID that was created\n\n3. **Update the fixture's build ID**\n\n   - Edit `fixtures/[fixture-name]/package.json`\n   - Update the `--buildId` parameter in the `fixtures:sync` script\n\n   ```json\n   \"fixtures:sync\": \"pnpm cli sync --buildId NEW_BUILD_ID_HERE && pnpm prettier --write ./.webstudio/\"\n   ```\n\n4. **Sync with the correct environment**\n\n   ```bash\n   # Go to fixture folder\n   cd fixtures/webstudio-features\n\n   # Sync from staging environment (replace with your branch)\n   BUILDER_HOST=fix-assets.staging.webstudio.is pnpm run fixtures\n   ```\n\n5. **Fix the origin URL**\n\n   - After syncing, edit `.webstudio/data.json`\n   - Change the origin back to:\n\n   ```json\n   \"origin\": \"https://main.development.webstudio.is\"\n   ```\n\n   - This is necessary because sync changes it to the current branch\n\n6. **Commit the changes**\n   ```bash\n   git add fixtures/\n   git commit -m \"chore: update fixtures with new assets\"\n   git push\n   ```\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# cloudflare\nworker-configuration.d.ts\n.wrangler\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.template/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.template/package.json",
    "content": "{\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"webstudio\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\"\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"f565d527-32e7-4731-bc71-aca9e9574587\",\n    \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n    \"version\": 43,\n    \"createdAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"updatedAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"\",\n        \"faviconAssetId\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n        \"code\": \"\"\n      },\n      \"homePage\": {\n        \"id\": \"9di_L14CzctvSruIoKVvE\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n        \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n          \"name\": \"Another page\",\n          \"title\": \"\\\"Another page\\\"\",\n          \"history\": [\"/another-page\"],\n          \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/another-page\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"9di_L14CzctvSruIoKVvE\", \"WPPAbLFyJD_02vhjRd8P4\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"rKj-wYctg3-GnqL3WHN9I\",\n        {\n          \"id\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"yH9RXhqCyeaVkrOt8MzLc\",\n        {\n          \"id\": \"yH9RXhqCyeaVkrOt8MzLc\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"8nSCZbeS002IVwkTdoIes\",\n        {\n          \"id\": \"8nSCZbeS002IVwkTdoIes\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"7gBD25KrrbBdJYNDlhPz7\",\n        {\n          \"id\": \"7gBD25KrrbBdJYNDlhPz7\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"flexDirection\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          }\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"0KA68BwP9gdTzE1ESO2Zp\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"mf2C07UBmGT7y_G4Du3yg\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 400\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7_QL45cpvP-zG8Hkgf4cr\"\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp\",\n        {\n          \"type\": \"local\",\n          \"id\": \"0KA68BwP9gdTzE1ESO2Zp\"\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mf2C07UBmGT7y_G4Du3yg\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"instanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"values\": [\"7_QL45cpvP-zG8Hkgf4cr\"]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"instanceId\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"values\": [\"0KA68BwP9gdTzE1ESO2Zp\"]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"values\": [\"mf2C07UBmGT7y_G4Du3yg\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"1p34InvRgqoKVqeNZ1uBb\",\n        {\n          \"id\": \"1p34InvRgqoKVqeNZ1uBb\",\n          \"instanceId\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"WPPAbLFyJD_02vhjRd8P4\"\n        }\n      ],\n      [\n        \"su3ag3OxH9WTBjJg5eIyg\",\n        {\n          \"id\": \"su3ag3OxH9WTBjJg5eIyg\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"vGCYpBBB1QUPIPPIdyexn\",\n        {\n          \"id\": \"vGCYpBBB1QUPIPPIdyexn\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"width\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"JKAGY7DWpciEl0UdnWuKL\",\n        {\n          \"id\": \"JKAGY7DWpciEl0UdnWuKL\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"height\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"CAkmmL8-JAgokmeopoFXh\",\n        {\n          \"id\": \"CAkmmL8-JAgokmeopoFXh\",\n          \"instanceId\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"name\": \"src\",\n          \"type\": \"string\",\n          \"value\": \"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"2KT4-bRzToj9cAGAN_woK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2KT4-bRzToj9cAGAN_woK\",\n          \"scopeInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"tdXe9gFf83hSo9BLWU6xl\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"scopeInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"name\": \"system\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MMimeobf_zi4ZkRGXapju\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"MYDt0guk1-vzc7yzqyN6A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"BMJfjOzunWs8XkQgvvx1e\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"pjkZo5EiBqaeUXBcyHf_O\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"uHB3Fjb7-NELG-bnH7bXB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"2sIE8GxbKRBaav_zdhaZ1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MYDt0guk1-vzc7yzqyN6A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MYDt0guk1-vzc7yzqyN6A\",\n          \"component\": \"Heading\",\n          \"label\": \"xD\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Simple Project to test CLI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Please don't change directly in the fixture\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pjkZo5EiBqaeUXBcyHf_O\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test another page link\"\n            }\n          ]\n        }\n      ],\n      [\n        \"n_VBMr7klpx25buS0NV7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"n_VBMr7klpx25buS0NV7R\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"wthNByqb3RPmheb-56VYI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wthNByqb3RPmheb-56VYI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wthNByqb3RPmheb-56VYI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Another page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"2sIE8GxbKRBaav_zdhaZ1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"destination\": \"saas\",\n      \"domains\": [\"cli-basic-test-d0osr\"],\n      \"assetsDomain\": \"cli-basic-test-d0osr\",\n      \"excludeWstdDomainFromSearch\": false\n    }\n  },\n  \"page\": {\n    \"id\": \"9di_L14CzctvSruIoKVvE\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n    \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"9di_L14CzctvSruIoKVvE\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n      \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n      \"name\": \"Another page\",\n      \"title\": \"\\\"Another page\\\"\",\n      \"history\": [\"/another-page\"],\n      \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n      \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/another-page\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\",\n      \"name\": \"iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 999,\n      \"type\": \"image\",\n      \"format\": \"svg\",\n      \"createdAt\": \"2024-07-26T13:39:48.678+00:00\",\n      \"meta\": {\n        \"width\": 14,\n        \"height\": 16\n      }\n    },\n    {\n      \"id\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n      \"name\": \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 64701,\n      \"type\": \"image\",\n      \"format\": \"jpg\",\n      \"createdAt\": \"2024-12-06T14:36:07.046+00:00\",\n      \"meta\": {\n        \"width\": 820,\n        \"height\": 985\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"cli-basic-test-d0osr\",\n  \"projectTitle\": \"cli-basic-test\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\": {\n    url: \"/cgi/image/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg?format=raw\",\n    width: 14,\n    height: 16,\n  },\n  d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd: {\n    url: \"/cgi/image/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg?format=raw\",\n    width: 820,\n    height: 985,\n  },\n};\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2025-01-04\",\n  },\n];\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/[another-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Another page\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/[another-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Another page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Heading as Heading,\n  Text as Text,\n  Image as Image,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jaw2zx cbipm55 ctniqj4 ctgx88l`}>\n      <Heading className={`w-heading`}>{\"Simple Project to test CLI\"}</Heading>\n      <Text className={`w-text cn3rfux`}>\n        {\"Please don't change directly in the fixture\"}\n      </Text>\n      <Link href={\"/another-page\"} className={`w-link`}>\n        {\"Test another page link\"}\n      </Link>\n      <Image\n        src={\"/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\"}\n        width={14}\n        height={16}\n        className={`w-image c161qeci`}\n      />\n      <Image\n        src={\"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"}\n        className={`w-image`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n}\n@media all {\n  .c1jaw2zx {\n    display: flex;\n  }\n  .cbipm55 {\n    align-items: center;\n  }\n  .ctniqj4 {\n    justify-content: center;\n  }\n  .ctgx88l {\n    flex-direction: column;\n  }\n  .cn3rfux {\n    margin-bottom: 1em;\n  }\n  .c161qeci {\n    width: 400px;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (import.meta.env.DEV) {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // @todo https://developers.cloudflare.com/images/transform-images/transform-via-url/\n  return props.src;\n};\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/entry.server.tsx",
    "content": "import type { AppLoadContext, EntryContext } from \"react-router\";\nimport { ServerRouter } from \"react-router\";\nimport { isbot } from \"isbot\";\nimport { renderToReadableStream } from \"react-dom/server\";\n\nexport default async function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n  _loadContext: AppLoadContext\n) {\n  let shellRendered = false;\n  const userAgent = request.headers.get(\"user-agent\");\n\n  const body = await renderToReadableStream(\n    <ServerRouter context={routerContext} url={request.url} />,\n    {\n      onError(error: unknown) {\n        responseStatusCode = 500;\n        // Log streaming rendering errors from inside the shell.  Don't log\n        // errors encountered during initial shell rendering since they'll\n        // reject and get logged in handleDocumentRequest.\n        if (shellRendered) {\n          console.error(error);\n        }\n      },\n    }\n  );\n  shellRendered = true;\n\n  // Ensure requests from bots and SPA Mode renders wait for all content to load before responding\n  // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation\n  if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {\n    await body.allReady;\n  }\n\n  responseHeaders.set(\"Content-Type\", \"text/html\");\n  return new Response(body, {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/extension.ts",
    "content": "import { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"react-router\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/routes/[another-page]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[another-page]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[another-page]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/routes/[sitemap.xml]._index.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/routes/_index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/_index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/_index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/package.json",
    "content": "{\n  \"name\": \"webstudio-react-router-cloudflare\",\n  \"scripts\": {\n    \"typecheck\": \"wrangler types && tsgo\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-d845c167-ea07-4875-b08d-83e97c09dcce-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId f565d527-32e7-4731-bc71-aca9e9574587 && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"pnpm cli build --template cloudflare-new --template .template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json\",\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"typegen\": \"wrangler types\",\n    \"preview\": \"react-router build && vite preview\",\n    \"deploy\": \"react-router build && wrangler deploy\"\n  },\n  \"dependencies\": {\n    \"@cloudflare/vite-plugin\": \"^1.1.0\",\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\",\n    \"wrangler\": \"^4.14.1\"\n  },\n  \"type\": \"module\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"typescript\": \"5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    unstable_viteEnvironmentApi: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"./worker-configuration.d.ts\",\n      \"vite/client\",\n      \"@webstudio-is/react-sdk/placeholder\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { cloudflare } from \"@cloudflare/vite-plugin\";\nimport { reactRouter } from \"@react-router/dev/vite\";\n\nexport default defineConfig({\n  plugins: [cloudflare({ viteEnvironment: { name: \"ssr\" } }), reactRouter()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/workers/app.ts",
    "content": "import { createRequestHandler } from \"react-router\";\n\ndeclare module \"react-router\" {\n  export interface AppLoadContext {\n    cloudflare: {\n      env: Env;\n      ctx: ExecutionContext;\n    };\n  }\n}\n\nconst requestHandler = createRequestHandler(\n  // @ts-ignore\n  () => import(\"virtual:react-router/server-build\"),\n  import.meta.env.MODE\n);\n\nexport default {\n  async fetch(request, env, ctx) {\n    return requestHandler(request, {\n      EXCLUDE_FROM_SEARCH: false,\n      getDefaultActionResource: undefined,\n      cloudflare: { env, ctx },\n    });\n  },\n} satisfies ExportedHandler<Env>;\n"
  },
  {
    "path": "fixtures/react-router-cloudflare/wrangler.jsonc",
    "content": "{\n  \"$schema\": \"node_modules/wrangler/config-schema.json\",\n  \"name\": \"webstudio-app\",\n  \"compatibility_date\": \"2025-04-28\",\n  \"main\": \"./workers/app.ts\"\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/.dockerignore",
    "content": ".react-router\nbuild\nnode_modules\nREADME.md\n"
  },
  {
    "path": "fixtures/react-router-docker/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "fixtures/react-router-docker/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-docker/.template/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-docker/.template/package.json",
    "content": "{\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"webstudio\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/.template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\"\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"f565d527-32e7-4731-bc71-aca9e9574587\",\n    \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n    \"version\": 43,\n    \"createdAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"updatedAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"\",\n        \"faviconAssetId\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n        \"code\": \"\"\n      },\n      \"homePage\": {\n        \"id\": \"9di_L14CzctvSruIoKVvE\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n        \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n          \"name\": \"Another page\",\n          \"title\": \"\\\"Another page\\\"\",\n          \"history\": [\"/another-page\"],\n          \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/another-page\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"9di_L14CzctvSruIoKVvE\", \"WPPAbLFyJD_02vhjRd8P4\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"rKj-wYctg3-GnqL3WHN9I\",\n        {\n          \"id\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"yH9RXhqCyeaVkrOt8MzLc\",\n        {\n          \"id\": \"yH9RXhqCyeaVkrOt8MzLc\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"8nSCZbeS002IVwkTdoIes\",\n        {\n          \"id\": \"8nSCZbeS002IVwkTdoIes\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"7gBD25KrrbBdJYNDlhPz7\",\n        {\n          \"id\": \"7gBD25KrrbBdJYNDlhPz7\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"flexDirection\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          }\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"0KA68BwP9gdTzE1ESO2Zp\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"mf2C07UBmGT7y_G4Du3yg\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 400\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7_QL45cpvP-zG8Hkgf4cr\"\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp\",\n        {\n          \"type\": \"local\",\n          \"id\": \"0KA68BwP9gdTzE1ESO2Zp\"\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mf2C07UBmGT7y_G4Du3yg\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"instanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"values\": [\"7_QL45cpvP-zG8Hkgf4cr\"]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"instanceId\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"values\": [\"0KA68BwP9gdTzE1ESO2Zp\"]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"values\": [\"mf2C07UBmGT7y_G4Du3yg\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"1p34InvRgqoKVqeNZ1uBb\",\n        {\n          \"id\": \"1p34InvRgqoKVqeNZ1uBb\",\n          \"instanceId\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"WPPAbLFyJD_02vhjRd8P4\"\n        }\n      ],\n      [\n        \"su3ag3OxH9WTBjJg5eIyg\",\n        {\n          \"id\": \"su3ag3OxH9WTBjJg5eIyg\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"vGCYpBBB1QUPIPPIdyexn\",\n        {\n          \"id\": \"vGCYpBBB1QUPIPPIdyexn\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"width\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"JKAGY7DWpciEl0UdnWuKL\",\n        {\n          \"id\": \"JKAGY7DWpciEl0UdnWuKL\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"height\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"CAkmmL8-JAgokmeopoFXh\",\n        {\n          \"id\": \"CAkmmL8-JAgokmeopoFXh\",\n          \"instanceId\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"name\": \"src\",\n          \"type\": \"string\",\n          \"value\": \"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"2KT4-bRzToj9cAGAN_woK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2KT4-bRzToj9cAGAN_woK\",\n          \"scopeInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"tdXe9gFf83hSo9BLWU6xl\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"scopeInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"name\": \"system\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MMimeobf_zi4ZkRGXapju\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"MYDt0guk1-vzc7yzqyN6A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"BMJfjOzunWs8XkQgvvx1e\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"pjkZo5EiBqaeUXBcyHf_O\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"uHB3Fjb7-NELG-bnH7bXB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"2sIE8GxbKRBaav_zdhaZ1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MYDt0guk1-vzc7yzqyN6A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MYDt0guk1-vzc7yzqyN6A\",\n          \"component\": \"Heading\",\n          \"label\": \"xD\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Simple Project to test CLI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Please don't change directly in the fixture\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pjkZo5EiBqaeUXBcyHf_O\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test another page link\"\n            }\n          ]\n        }\n      ],\n      [\n        \"n_VBMr7klpx25buS0NV7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"n_VBMr7klpx25buS0NV7R\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"wthNByqb3RPmheb-56VYI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wthNByqb3RPmheb-56VYI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wthNByqb3RPmheb-56VYI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Another page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"2sIE8GxbKRBaav_zdhaZ1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"destination\": \"saas\",\n      \"domains\": [\"cli-basic-test-d0osr\"],\n      \"assetsDomain\": \"cli-basic-test-d0osr\",\n      \"excludeWstdDomainFromSearch\": false\n    }\n  },\n  \"page\": {\n    \"id\": \"9di_L14CzctvSruIoKVvE\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n    \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"9di_L14CzctvSruIoKVvE\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n      \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n      \"name\": \"Another page\",\n      \"title\": \"\\\"Another page\\\"\",\n      \"history\": [\"/another-page\"],\n      \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n      \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/another-page\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\",\n      \"name\": \"iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 999,\n      \"type\": \"image\",\n      \"format\": \"svg\",\n      \"createdAt\": \"2024-07-26T13:39:48.678+00:00\",\n      \"meta\": {\n        \"width\": 14,\n        \"height\": 16\n      }\n    },\n    {\n      \"id\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n      \"name\": \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 64701,\n      \"type\": \"image\",\n      \"format\": \"jpg\",\n      \"createdAt\": \"2024-12-06T14:36:07.046+00:00\",\n      \"meta\": {\n        \"width\": 820,\n        \"height\": 985\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"cli-basic-test-d0osr\",\n  \"projectTitle\": \"cli-basic-test\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/Dockerfile",
    "content": "FROM node:22-alpine AS dependencies-env\nCOPY .npmrc package.json /app/\nWORKDIR /app\nRUN npm install --omit=dev\n\nFROM dependencies-env AS build-env\nCOPY . /app/\nWORKDIR /app\nRUN npm install\nRUN npm run build\n\nFROM node:22-alpine\nCOPY .npmrc package.json /app/\nCOPY --from=dependencies-env /app/node_modules /app/node_modules\nCOPY --from=build-env /app/build /app/build\nCOPY --from=build-env /app/public /app/public\nWORKDIR /app\n# there is a DOMAINS env with comma separated allowed domains for image processing\nCMD [\"npm\", \"run\", \"start\"]\n"
  },
  {
    "path": "fixtures/react-router-docker/README.md",
    "content": "# React Router Docker Fixture\n\nSee the [main fixtures README](../README.md) for complete documentation on how to use and update fixtures.\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\": {\n    url: \"/cgi/image/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg?format=raw\",\n    width: 14,\n    height: 16,\n  },\n  d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd: {\n    url: \"/cgi/image/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg?format=raw\",\n    width: 820,\n    height: 985,\n  },\n};\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2025-01-04\",\n  },\n];\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/[another-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Another page\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/[another-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Another page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Heading as Heading,\n  Text as Text,\n  Image as Image,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jaw2zx cbipm55 ctniqj4 ctgx88l`}>\n      <Heading className={`w-heading`}>{\"Simple Project to test CLI\"}</Heading>\n      <Text className={`w-text cn3rfux`}>\n        {\"Please don't change directly in the fixture\"}\n      </Text>\n      <Link href={\"/another-page\"} className={`w-link`}>\n        {\"Test another page link\"}\n      </Link>\n      <Image\n        src={\"/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\"}\n        width={14}\n        height={16}\n        className={`w-image c161qeci`}\n      />\n      <Image\n        src={\"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"}\n        className={`w-image`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-docker/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n}\n@media all {\n  .c1jaw2zx {\n    display: flex;\n  }\n  .cbipm55 {\n    align-items: center;\n  }\n  .ctniqj4 {\n    justify-content: center;\n  }\n  .ctgx88l {\n    flex-direction: column;\n  }\n  .cn3rfux {\n    margin-bottom: 1em;\n  }\n  .c161qeci {\n    width: 400px;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * URL.canParse(props.src)\n * @type {(url: string) => boolean}\n */\nconst UrlCanParse = (url) => {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n  // IPX (sharp) does not support ico\n  if (props.src.endsWith(\".ico\")) {\n    return props.src;\n  }\n  // handle absolute urls\n  const path = UrlCanParse(props.src) ? `/${props.src}` : props.src;\n  // https://github.com/unjs/ipx?tab=readme-ov-file#modifiers\n  return `/_image/w_${props.width},q_${props.quality}${path}`;\n};\n"
  },
  {
    "path": "fixtures/react-router-docker/app/extension.ts",
    "content": "import { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"react-router\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "fixtures/react-router-docker/app/routes/[_image].$.ts",
    "content": "import { env } from \"node:process\";\nimport type { LoaderFunctionArgs } from \"react-router\";\nimport {\n  createIPX,\n  createIPXH3Handler,\n  ipxFSStorage,\n  ipxHttpStorage,\n} from \"ipx\";\nimport { createApp, toWebHandler } from \"h3\";\n\nconst domains = env.DOMAINS?.split(/\\s*,\\s*/) ?? [];\n\nconst ipx = createIPX({\n  storage: ipxFSStorage({ dir: \"./public\" }),\n  httpStorage: ipxHttpStorage({ domains }),\n});\n\nconst handleRequest = toWebHandler(\n  createApp().use(\"/_image\", createIPXH3Handler(ipx))\n);\n\nexport const loader = async (args: LoaderFunctionArgs) => {\n  return handleRequest(args.request);\n};\n"
  },
  {
    "path": "fixtures/react-router-docker/app/routes/[another-page]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[another-page]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[another-page]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-docker/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-docker/app/routes/[sitemap.xml]._index.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-docker/app/routes/_index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/_index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/_index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-docker/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "fixtures/react-router-docker/package.json",
    "content": "{\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"react-router-serve ./build/server/index.js\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-d845c167-ea07-4875-b08d-83e97c09dcce-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId f565d527-32e7-4731-bc71-aca9e9574587 && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"pnpm cli build --template docker --template .template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json\"\n  },\n  \"dependencies\": {\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@react-router/node\": \"^7.5.3\",\n    \"@react-router/serve\": \"^7.5.3\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"h3\": \"^1.15.1\",\n    \"ipx\": \"^3.0.3\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"private\": true,\n  \"sideEffects\": false,\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"typescript\": \"5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"name\": \"webstudio-react-router-docker\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"type\": \"module\"\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\", \"@webstudio-is/react-sdk/placeholder\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-docker/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { reactRouter } from \"@react-router/dev/vite\";\n\nexport default defineConfig({\n  plugins: [reactRouter()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "fixtures/react-router-netlify/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "fixtures/react-router-netlify/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-netlify/.template/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-netlify/.template/package.json",
    "content": "{\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"webstudio\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/.template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\"\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"f565d527-32e7-4731-bc71-aca9e9574587\",\n    \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n    \"version\": 43,\n    \"createdAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"updatedAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"\",\n        \"faviconAssetId\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n        \"code\": \"\"\n      },\n      \"homePage\": {\n        \"id\": \"9di_L14CzctvSruIoKVvE\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n        \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n          \"name\": \"Another page\",\n          \"title\": \"\\\"Another page\\\"\",\n          \"history\": [\"/another-page\"],\n          \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/another-page\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"9di_L14CzctvSruIoKVvE\", \"WPPAbLFyJD_02vhjRd8P4\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"rKj-wYctg3-GnqL3WHN9I\",\n        {\n          \"id\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"yH9RXhqCyeaVkrOt8MzLc\",\n        {\n          \"id\": \"yH9RXhqCyeaVkrOt8MzLc\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"8nSCZbeS002IVwkTdoIes\",\n        {\n          \"id\": \"8nSCZbeS002IVwkTdoIes\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"7gBD25KrrbBdJYNDlhPz7\",\n        {\n          \"id\": \"7gBD25KrrbBdJYNDlhPz7\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"flexDirection\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          }\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"0KA68BwP9gdTzE1ESO2Zp\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"mf2C07UBmGT7y_G4Du3yg\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 400\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7_QL45cpvP-zG8Hkgf4cr\"\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp\",\n        {\n          \"type\": \"local\",\n          \"id\": \"0KA68BwP9gdTzE1ESO2Zp\"\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mf2C07UBmGT7y_G4Du3yg\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"instanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"values\": [\"7_QL45cpvP-zG8Hkgf4cr\"]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"instanceId\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"values\": [\"0KA68BwP9gdTzE1ESO2Zp\"]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"values\": [\"mf2C07UBmGT7y_G4Du3yg\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"1p34InvRgqoKVqeNZ1uBb\",\n        {\n          \"id\": \"1p34InvRgqoKVqeNZ1uBb\",\n          \"instanceId\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"WPPAbLFyJD_02vhjRd8P4\"\n        }\n      ],\n      [\n        \"su3ag3OxH9WTBjJg5eIyg\",\n        {\n          \"id\": \"su3ag3OxH9WTBjJg5eIyg\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"vGCYpBBB1QUPIPPIdyexn\",\n        {\n          \"id\": \"vGCYpBBB1QUPIPPIdyexn\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"width\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"JKAGY7DWpciEl0UdnWuKL\",\n        {\n          \"id\": \"JKAGY7DWpciEl0UdnWuKL\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"height\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"CAkmmL8-JAgokmeopoFXh\",\n        {\n          \"id\": \"CAkmmL8-JAgokmeopoFXh\",\n          \"instanceId\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"name\": \"src\",\n          \"type\": \"string\",\n          \"value\": \"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"2KT4-bRzToj9cAGAN_woK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2KT4-bRzToj9cAGAN_woK\",\n          \"scopeInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"tdXe9gFf83hSo9BLWU6xl\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"scopeInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"name\": \"system\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MMimeobf_zi4ZkRGXapju\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"MYDt0guk1-vzc7yzqyN6A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"BMJfjOzunWs8XkQgvvx1e\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"pjkZo5EiBqaeUXBcyHf_O\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"uHB3Fjb7-NELG-bnH7bXB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"2sIE8GxbKRBaav_zdhaZ1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MYDt0guk1-vzc7yzqyN6A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MYDt0guk1-vzc7yzqyN6A\",\n          \"component\": \"Heading\",\n          \"label\": \"xD\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Simple Project to test CLI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Please don't change directly in the fixture\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pjkZo5EiBqaeUXBcyHf_O\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test another page link\"\n            }\n          ]\n        }\n      ],\n      [\n        \"n_VBMr7klpx25buS0NV7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"n_VBMr7klpx25buS0NV7R\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"wthNByqb3RPmheb-56VYI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wthNByqb3RPmheb-56VYI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wthNByqb3RPmheb-56VYI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Another page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"2sIE8GxbKRBaav_zdhaZ1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"destination\": \"saas\",\n      \"domains\": [\"cli-basic-test-d0osr\"],\n      \"assetsDomain\": \"cli-basic-test-d0osr\",\n      \"excludeWstdDomainFromSearch\": false\n    }\n  },\n  \"page\": {\n    \"id\": \"9di_L14CzctvSruIoKVvE\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n    \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"9di_L14CzctvSruIoKVvE\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n      \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n      \"name\": \"Another page\",\n      \"title\": \"\\\"Another page\\\"\",\n      \"history\": [\"/another-page\"],\n      \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n      \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/another-page\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\",\n      \"name\": \"iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 999,\n      \"type\": \"image\",\n      \"format\": \"svg\",\n      \"createdAt\": \"2024-07-26T13:39:48.678+00:00\",\n      \"meta\": {\n        \"width\": 14,\n        \"height\": 16\n      }\n    },\n    {\n      \"id\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n      \"name\": \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 64701,\n      \"type\": \"image\",\n      \"format\": \"jpg\",\n      \"createdAt\": \"2024-12-06T14:36:07.046+00:00\",\n      \"meta\": {\n        \"width\": 820,\n        \"height\": 985\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"cli-basic-test-d0osr\",\n  \"projectTitle\": \"cli-basic-test\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\": {\n    url: \"/cgi/image/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg?format=raw\",\n    width: 14,\n    height: 16,\n  },\n  d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd: {\n    url: \"/cgi/image/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg?format=raw\",\n    width: 820,\n    height: 985,\n  },\n};\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2025-01-04\",\n  },\n];\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/[another-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Another page\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/[another-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Another page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Heading as Heading,\n  Text as Text,\n  Image as Image,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jaw2zx cbipm55 ctniqj4 ctgx88l`}>\n      <Heading className={`w-heading`}>{\"Simple Project to test CLI\"}</Heading>\n      <Text className={`w-text cn3rfux`}>\n        {\"Please don't change directly in the fixture\"}\n      </Text>\n      <Link href={\"/another-page\"} className={`w-link`}>\n        {\"Test another page link\"}\n      </Link>\n      <Image\n        src={\"/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\"}\n        width={14}\n        height={16}\n        className={`w-image c161qeci`}\n      />\n      <Image\n        src={\"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"}\n        className={`w-image`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n}\n@media all {\n  .c1jaw2zx {\n    display: flex;\n  }\n  .cbipm55 {\n    align-items: center;\n  }\n  .ctniqj4 {\n    justify-content: center;\n  }\n  .ctgx88l {\n    flex-direction: column;\n  }\n  .cn3rfux {\n    margin-bottom: 1em;\n  }\n  .c161qeci {\n    width: 400px;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (import.meta.env.DEV) {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://docs.netlify.com/image-cdn/overview/\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  if (props.height) {\n    searchParams.set(\"h\", props.height.toString());\n  }\n  searchParams.set(\"q\", props.quality.toString());\n  // fit=contain by default\n  return `/.netlify/images?${searchParams}`;\n};\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/extension.ts",
    "content": "import { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"react-router\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/routes/[another-page]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[another-page]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[another-page]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/routes/[sitemap.xml]._index.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/routes/_index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/_index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/_index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-netlify/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "fixtures/react-router-netlify/netlify.toml",
    "content": "[build]\ncommand = \"react-router build\"\npublish = \"build/client\"\n\n[dev]\ncommand = \"react-router dev\"\n"
  },
  {
    "path": "fixtures/react-router-netlify/package.json",
    "content": "{\n  \"name\": \"webstudio-react-router-netlify\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-d845c167-ea07-4875-b08d-83e97c09dcce-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId f565d527-32e7-4731-bc71-aca9e9574587 && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"pnpm cli build --template netlify --template .template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json\",\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"start\": \"npx netlify-cli serve\",\n    \"deploy\": \"npx netlify-cli deploy --build --prod\"\n  },\n  \"dependencies\": {\n    \"@netlify/vite-plugin-react-router\": \"^1.0.1\",\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@react-router/node\": \"^7.5.3\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"type\": \"module\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"typescript\": \"5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\", \"@webstudio-is/react-sdk/placeholder\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-netlify/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport netlifyPlugin from \"@netlify/vite-plugin-react-router\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), netlifyPlugin()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "fixtures/react-router-vercel/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "fixtures/react-router-vercel/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-vercel/.template/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/react-router-vercel/.template/package.json",
    "content": "{\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"webstudio\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/.template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\"\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"f565d527-32e7-4731-bc71-aca9e9574587\",\n    \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n    \"version\": 43,\n    \"createdAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"updatedAt\": \"2025-01-04T11:01:50.091+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"\",\n        \"faviconAssetId\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n        \"code\": \"\"\n      },\n      \"homePage\": {\n        \"id\": \"9di_L14CzctvSruIoKVvE\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n        \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n          \"name\": \"Another page\",\n          \"title\": \"\\\"Another page\\\"\",\n          \"history\": [\"/another-page\"],\n          \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/another-page\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"9di_L14CzctvSruIoKVvE\", \"WPPAbLFyJD_02vhjRd8P4\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"rKj-wYctg3-GnqL3WHN9I\",\n        {\n          \"id\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"yH9RXhqCyeaVkrOt8MzLc\",\n        {\n          \"id\": \"yH9RXhqCyeaVkrOt8MzLc\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"8nSCZbeS002IVwkTdoIes\",\n        {\n          \"id\": \"8nSCZbeS002IVwkTdoIes\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"7gBD25KrrbBdJYNDlhPz7\",\n        {\n          \"id\": \"7gBD25KrrbBdJYNDlhPz7\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"flexDirection\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          }\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"0KA68BwP9gdTzE1ESO2Zp\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"mf2C07UBmGT7y_G4Du3yg\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 400\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7_QL45cpvP-zG8Hkgf4cr\"\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp\",\n        {\n          \"type\": \"local\",\n          \"id\": \"0KA68BwP9gdTzE1ESO2Zp\"\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mf2C07UBmGT7y_G4Du3yg\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"instanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"values\": [\"7_QL45cpvP-zG8Hkgf4cr\"]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"instanceId\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"values\": [\"0KA68BwP9gdTzE1ESO2Zp\"]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"values\": [\"mf2C07UBmGT7y_G4Du3yg\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"1p34InvRgqoKVqeNZ1uBb\",\n        {\n          \"id\": \"1p34InvRgqoKVqeNZ1uBb\",\n          \"instanceId\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"WPPAbLFyJD_02vhjRd8P4\"\n        }\n      ],\n      [\n        \"su3ag3OxH9WTBjJg5eIyg\",\n        {\n          \"id\": \"su3ag3OxH9WTBjJg5eIyg\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"vGCYpBBB1QUPIPPIdyexn\",\n        {\n          \"id\": \"vGCYpBBB1QUPIPPIdyexn\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"width\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"JKAGY7DWpciEl0UdnWuKL\",\n        {\n          \"id\": \"JKAGY7DWpciEl0UdnWuKL\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"height\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"CAkmmL8-JAgokmeopoFXh\",\n        {\n          \"id\": \"CAkmmL8-JAgokmeopoFXh\",\n          \"instanceId\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"name\": \"src\",\n          \"type\": \"string\",\n          \"value\": \"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"2KT4-bRzToj9cAGAN_woK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2KT4-bRzToj9cAGAN_woK\",\n          \"scopeInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"tdXe9gFf83hSo9BLWU6xl\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"scopeInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"name\": \"system\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MMimeobf_zi4ZkRGXapju\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"MYDt0guk1-vzc7yzqyN6A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"BMJfjOzunWs8XkQgvvx1e\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"pjkZo5EiBqaeUXBcyHf_O\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"uHB3Fjb7-NELG-bnH7bXB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"2sIE8GxbKRBaav_zdhaZ1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MYDt0guk1-vzc7yzqyN6A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MYDt0guk1-vzc7yzqyN6A\",\n          \"component\": \"Heading\",\n          \"label\": \"xD\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Simple Project to test CLI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Please don't change directly in the fixture\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pjkZo5EiBqaeUXBcyHf_O\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test another page link\"\n            }\n          ]\n        }\n      ],\n      [\n        \"n_VBMr7klpx25buS0NV7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"n_VBMr7klpx25buS0NV7R\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"wthNByqb3RPmheb-56VYI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wthNByqb3RPmheb-56VYI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wthNByqb3RPmheb-56VYI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Another page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"2sIE8GxbKRBaav_zdhaZ1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"destination\": \"saas\",\n      \"domains\": [\"cli-basic-test-d0osr\"],\n      \"assetsDomain\": \"cli-basic-test-d0osr\",\n      \"excludeWstdDomainFromSearch\": false\n    }\n  },\n  \"page\": {\n    \"id\": \"9di_L14CzctvSruIoKVvE\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n    \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"9di_L14CzctvSruIoKVvE\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n      \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n      \"name\": \"Another page\",\n      \"title\": \"\\\"Another page\\\"\",\n      \"history\": [\"/another-page\"],\n      \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n      \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/another-page\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\",\n      \"name\": \"iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 999,\n      \"type\": \"image\",\n      \"format\": \"svg\",\n      \"createdAt\": \"2024-07-26T13:39:48.678+00:00\",\n      \"meta\": {\n        \"width\": 14,\n        \"height\": 16\n      }\n    },\n    {\n      \"id\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n      \"name\": \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 64701,\n      \"type\": \"image\",\n      \"format\": \"jpg\",\n      \"createdAt\": \"2024-12-06T14:36:07.046+00:00\",\n      \"meta\": {\n        \"width\": 820,\n        \"height\": 985\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"cli-basic-test-d0osr\",\n  \"projectTitle\": \"cli-basic-test\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\": {\n    url: \"/cgi/image/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg?format=raw\",\n    width: 14,\n    height: 16,\n  },\n  d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd: {\n    url: \"/cgi/image/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg?format=raw\",\n    width: 820,\n    height: 985,\n  },\n};\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2025-01-04\",\n  },\n];\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/[another-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Another page\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/[another-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Another page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Heading as Heading,\n  Text as Text,\n  Image as Image,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2025-01-04T11:01:50.091Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jaw2zx cbipm55 ctniqj4 ctgx88l`}>\n      <Heading className={`w-heading`}>{\"Simple Project to test CLI\"}</Heading>\n      <Text className={`w-text cn3rfux`}>\n        {\"Please don't change directly in the fixture\"}\n      </Text>\n      <Link href={\"/another-page\"} className={`w-link`}>\n        {\"Test another page link\"}\n      </Link>\n      <Image\n        src={\"/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\"}\n        width={14}\n        height={16}\n        className={`w-image c161qeci`}\n      />\n      <Image\n        src={\"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"}\n        className={`w-image`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n}\n@media all {\n  .c1jaw2zx {\n    display: flex;\n  }\n  .cbipm55 {\n    align-items: center;\n  }\n  .ctniqj4 {\n    justify-content: center;\n  }\n  .ctgx88l {\n    flex-direction: column;\n  }\n  .cn3rfux {\n    margin-bottom: 1em;\n  }\n  .c161qeci {\n    width: 400px;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (import.meta.env.DEV) {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://vercel.com/blog/build-your-own-web-framework#automatic-image-optimization\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  searchParams.set(\"q\", props.quality.toString());\n  return `/_vercel/image?${searchParams}`;\n};\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/extension.ts",
    "content": "import { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"react-router\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/routes/[another-page]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[another-page]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[another-page]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/routes/[sitemap.xml]._index.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/routes/_index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/_index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/_index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/react-router-vercel/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "fixtures/react-router-vercel/package.json",
    "content": "{\n  \"name\": \"webstudio-react-router-vercel\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-d845c167-ea07-4875-b08d-83e97c09dcce-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId f565d527-32e7-4731-bc71-aca9e9574587 && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"pnpm cli build --template vercel --template .template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json\",\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"deploy\": \"npx vercel deploy\"\n  },\n  \"dependencies\": {\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@react-router/node\": \"^7.5.3\",\n    \"@vercel/react-router\": \"^1.1.0\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"private\": true,\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"typescript\": \"5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\nimport { vercelPreset } from \"@vercel/react-router/vite\";\n\nexport default {\n  presets: [vercelPreset()],\n} satisfies Config;\n"
  },
  {
    "path": "fixtures/react-router-vercel/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\", \"@webstudio-is/react-sdk/placeholder\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/vercel.json",
    "content": "{\n  \"framework\": \"react-router\",\n  \"images\": {\n    \"domains\": [],\n    \"sizes\": [\n      16, 32, 48, 64, 96, 128, 144, 256, 384, 640, 750, 828, 1080, 1200, 1920,\n      2048, 3840\n    ],\n    \"minimumCacheTTL\": 60,\n    \"formats\": [\"image/webp\", \"image/avif\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/react-router-vercel/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { reactRouter } from \"@react-router/dev/vite\";\n\nexport default defineConfig({\n  plugins: [reactRouter()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "fixtures/ssg/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/ssg/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\"\n}\n"
  },
  {
    "path": "fixtures/ssg/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"a2e8de30-03d5-4514-a3a6-406b3266a3af\",\n    \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n    \"version\": 38,\n    \"createdAt\": \"2024-07-29T12:50:07.515+00:00\",\n    \"updatedAt\": \"2024-07-29T12:50:07.515+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"\",\n        \"faviconAssetId\": \"\",\n        \"code\": \"\"\n      },\n      \"homePage\": {\n        \"id\": \"9di_L14CzctvSruIoKVvE\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n        \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n          \"name\": \"Another page\",\n          \"title\": \"\\\"Another page\\\"\",\n          \"history\": [\"/another-page\"],\n          \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/another-page\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"9di_L14CzctvSruIoKVvE\", \"WPPAbLFyJD_02vhjRd8P4\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"rKj-wYctg3-GnqL3WHN9I\",\n        {\n          \"id\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"yH9RXhqCyeaVkrOt8MzLc\",\n        {\n          \"id\": \"yH9RXhqCyeaVkrOt8MzLc\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"8nSCZbeS002IVwkTdoIes\",\n        {\n          \"id\": \"8nSCZbeS002IVwkTdoIes\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"7gBD25KrrbBdJYNDlhPz7\",\n        {\n          \"id\": \"7gBD25KrrbBdJYNDlhPz7\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"flexDirection\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          }\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"0KA68BwP9gdTzE1ESO2Zp\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"mf2C07UBmGT7y_G4Du3yg\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 400\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7_QL45cpvP-zG8Hkgf4cr\"\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp\",\n        {\n          \"type\": \"local\",\n          \"id\": \"0KA68BwP9gdTzE1ESO2Zp\"\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mf2C07UBmGT7y_G4Du3yg\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"instanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"values\": [\"7_QL45cpvP-zG8Hkgf4cr\"]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"instanceId\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"values\": [\"0KA68BwP9gdTzE1ESO2Zp\"]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"values\": [\"mf2C07UBmGT7y_G4Du3yg\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"1p34InvRgqoKVqeNZ1uBb\",\n        {\n          \"id\": \"1p34InvRgqoKVqeNZ1uBb\",\n          \"instanceId\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"WPPAbLFyJD_02vhjRd8P4\"\n        }\n      ],\n      [\n        \"su3ag3OxH9WTBjJg5eIyg\",\n        {\n          \"id\": \"su3ag3OxH9WTBjJg5eIyg\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"vGCYpBBB1QUPIPPIdyexn\",\n        {\n          \"id\": \"vGCYpBBB1QUPIPPIdyexn\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"width\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"JKAGY7DWpciEl0UdnWuKL\",\n        {\n          \"id\": \"JKAGY7DWpciEl0UdnWuKL\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"height\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"CAkmmL8-JAgokmeopoFXh\",\n        {\n          \"id\": \"CAkmmL8-JAgokmeopoFXh\",\n          \"instanceId\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"name\": \"src\",\n          \"type\": \"string\",\n          \"value\": \"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"2KT4-bRzToj9cAGAN_woK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2KT4-bRzToj9cAGAN_woK\",\n          \"scopeInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"tdXe9gFf83hSo9BLWU6xl\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"scopeInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"name\": \"system\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MMimeobf_zi4ZkRGXapju\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"MYDt0guk1-vzc7yzqyN6A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"BMJfjOzunWs8XkQgvvx1e\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"pjkZo5EiBqaeUXBcyHf_O\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"uHB3Fjb7-NELG-bnH7bXB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"2sIE8GxbKRBaav_zdhaZ1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MYDt0guk1-vzc7yzqyN6A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MYDt0guk1-vzc7yzqyN6A\",\n          \"component\": \"Heading\",\n          \"label\": \"xD\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Simple Project to test CLI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Please don't change directly in the fixture\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pjkZo5EiBqaeUXBcyHf_O\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test another page link\"\n            }\n          ]\n        }\n      ],\n      [\n        \"n_VBMr7klpx25buS0NV7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"n_VBMr7klpx25buS0NV7R\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"wthNByqb3RPmheb-56VYI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wthNByqb3RPmheb-56VYI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wthNByqb3RPmheb-56VYI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Another page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"2sIE8GxbKRBaav_zdhaZ1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"domains\": [],\n      \"projectDomain\": \"cli-basic-test-d0osr\"\n    }\n  },\n  \"page\": {\n    \"id\": \"9di_L14CzctvSruIoKVvE\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n    \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"9di_L14CzctvSruIoKVvE\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n      \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n      \"name\": \"Another page\",\n      \"title\": \"\\\"Another page\\\"\",\n      \"history\": [\"/another-page\"],\n      \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n      \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/another-page\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\",\n      \"name\": \"iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 999,\n      \"type\": \"image\",\n      \"format\": \"svg\",\n      \"createdAt\": \"2024-07-26T13:39:48.678+00:00\",\n      \"meta\": {\n        \"width\": 14,\n        \"height\": 16\n      }\n    },\n    {\n      \"id\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n      \"name\": \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 64701,\n      \"type\": \"image\",\n      \"format\": \"jpg\",\n      \"createdAt\": \"2024-12-06T14:36:07.046+00:00\",\n      \"meta\": {\n        \"width\": 820,\n        \"height\": 985\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"cli-basic-test-d0osr\",\n  \"projectTitle\": \"cli-basic-test\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\": {\n    url: \"/cgi/image/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg?format=raw\",\n    width: 14,\n    height: 16,\n  },\n  d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd: {\n    url: \"/cgi/image/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg?format=raw\",\n    width: 820,\n    height: 985,\n  },\n};\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2024-07-29\",\n  },\n];\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/[another-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Another page\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/[another-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Heading as Heading,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2024-07-29T12:50:07.515Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined = undefined;\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Another page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Heading as Heading,\n  Text as Text,\n  Link as Link,\n  Image as Image,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2024-07-29T12:50:07.515Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined = undefined;\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jaw2zx cbipm55 ctniqj4 ctgx88l`}>\n      <Heading className={`w-heading`}>{\"Simple Project to test CLI\"}</Heading>\n      <Text className={`w-text cn3rfux`}>\n        {\"Please don't change directly in the fixture\"}\n      </Text>\n      <Link href={\"/another-page\"} className={`w-link`}>\n        {\"Test another page link\"}\n      </Link>\n      <Image\n        src={\"/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\"}\n        width={14}\n        height={16}\n        className={`w-image c161qeci`}\n      />\n      <Image\n        src={\"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"}\n        className={`w-image`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/ssg/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n}\n@media all {\n  .c1jaw2zx {\n    display: flex;\n  }\n  .cbipm55 {\n    align-items: center;\n  }\n  .ctniqj4 {\n    justify-content: center;\n  }\n  .ctgx88l {\n    flex-direction: column;\n  }\n  .cn3rfux {\n    margin-bottom: 1em;\n  }\n  .c161qeci {\n    width: 400px;\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\n\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = ({ src }) => {\n  return src;\n};\n"
  },
  {
    "path": "fixtures/ssg/package.json",
    "content": "{\n  \"name\": \"ssg\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"postbuild\": \"prettier --write \\\"dist/**/*.html\\\" || true\",\n    \"dev\": \"vite dev\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link ${BUILDER_URL_DEPRECATED:-https://main.development.webstudio.is}'/builder/d845c167-ea07-4875-b08d-83e97c09dcce?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId a2e8de30-03d5-4514-a3a6-406b3266a3af && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"rm -rf pages && pnpm cli build --template ssg --template internal && prettier --write .\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"type\": \"module\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"prettier\": \"3.5.3\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-remix\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vike\": \"^0.4.229\"\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg/pages/+config.ts",
    "content": "import type { Config } from \"vike/types\";\n\nexport default {\n  meta: {\n    Head: {\n      env: { server: true, client: true },\n    },\n    lang: {\n      env: { server: true, client: true },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "fixtures/ssg/pages/another-page/+Head.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport {\n  favIconAsset,\n  pageBackgroundImageAssets,\n  pageFontAssets,\n  siteName,\n} from \"../../app/__generated__/[another-page]._index\";\nimport \"../../app/__generated__/index.css\";\n\nexport const Head = ({}: { data: PageContext[\"data\"] }) => {\n  const ldJson = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebSite\",\n    name: siteName,\n  };\n  return (\n    <>\n      {siteName && (\n        <script\n          type=\"application/ld+json\"\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify(ldJson, null, 2),\n          }}\n        ></script>\n      )}\n      {favIconAsset && (\n        <link\n          rel=\"icon\"\n          href={imageLoader({\n            src: `${assetBaseUrl}${favIconAsset}`,\n            // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n            width: 144,\n            height: 144,\n            fit: \"pad\",\n            quality: 100,\n            format: \"auto\",\n          })}\n        />\n      )}\n      {pageFontAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"font\"\n          crossOrigin=\"anonymous\"\n        />\n      ))}\n      {pageBackgroundImageAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"image\"\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "fixtures/ssg/pages/another-page/+Page.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport {\n  PageSettingsMeta,\n  PageSettingsTitle,\n  ReactSdkContext,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport {\n  Page,\n  breakpoints,\n  siteName,\n} from \"../../app/__generated__/[another-page]._index\";\n\nconst PageComponent = ({ data }: { data: PageContext[\"data\"] }) => {\n  const { system, resources, url, pageMeta } = data;\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        siteName={siteName}\n        imageLoader={imageLoader}\n        assetBaseUrl={assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\nexport default PageComponent;\n"
  },
  {
    "path": "fixtures/ssg/pages/another-page/+data.ts",
    "content": "import type { PageContextServer } from \"vike/types\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  getPageMeta,\n  getResources,\n} from \"../../app/__generated__/[another-page]._index.server\";\nimport { assets } from \"../../app/__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const data = async (pageContext: PageContextServer) => {\n  const url = new URL(pageContext.urlOriginal, \"http://url\");\n  const headers = new Headers(pageContext.headers ?? {});\n  const host = headers.get(\"x-forwarded-host\") || headers.get(\"host\") || \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = pageContext.routeParams;\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  return {\n    url: url.href,\n    system,\n    resources,\n    pageMeta,\n  } satisfies PageContextServer[\"data\"];\n};\n"
  },
  {
    "path": "fixtures/ssg/pages/index/+Head.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport {\n  favIconAsset,\n  pageBackgroundImageAssets,\n  pageFontAssets,\n  siteName,\n} from \"../../app/__generated__/_index\";\nimport \"../../app/__generated__/index.css\";\n\nexport const Head = ({}: { data: PageContext[\"data\"] }) => {\n  const ldJson = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebSite\",\n    name: siteName,\n  };\n  return (\n    <>\n      {siteName && (\n        <script\n          type=\"application/ld+json\"\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify(ldJson, null, 2),\n          }}\n        ></script>\n      )}\n      {favIconAsset && (\n        <link\n          rel=\"icon\"\n          href={imageLoader({\n            src: `${assetBaseUrl}${favIconAsset}`,\n            // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n            width: 144,\n            height: 144,\n            fit: \"pad\",\n            quality: 100,\n            format: \"auto\",\n          })}\n        />\n      )}\n      {pageFontAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"font\"\n          crossOrigin=\"anonymous\"\n        />\n      ))}\n      {pageBackgroundImageAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"image\"\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "fixtures/ssg/pages/index/+Page.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport {\n  PageSettingsMeta,\n  PageSettingsTitle,\n  ReactSdkContext,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport { Page, breakpoints, siteName } from \"../../app/__generated__/_index\";\n\nconst PageComponent = ({ data }: { data: PageContext[\"data\"] }) => {\n  const { system, resources, url, pageMeta } = data;\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        siteName={siteName}\n        imageLoader={imageLoader}\n        assetBaseUrl={assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\nexport default PageComponent;\n"
  },
  {
    "path": "fixtures/ssg/pages/index/+data.ts",
    "content": "import type { PageContextServer } from \"vike/types\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  getPageMeta,\n  getResources,\n} from \"../../app/__generated__/_index.server\";\nimport { assets } from \"../../app/__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const data = async (pageContext: PageContextServer) => {\n  const url = new URL(pageContext.urlOriginal, \"http://url\");\n  const headers = new Headers(pageContext.headers ?? {});\n  const host = headers.get(\"x-forwarded-host\") || headers.get(\"host\") || \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = pageContext.routeParams;\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  return {\n    url: url.href,\n    system,\n    resources,\n    pageMeta,\n  } satisfies PageContextServer[\"data\"];\n};\n"
  },
  {
    "path": "fixtures/ssg/renderer/+onRenderClient.tsx",
    "content": "import { type Root, createRoot } from \"react-dom/client\";\nimport type { OnRenderClientSync } from \"vike/types\";\n\nlet root: Root;\n\nexport const onRenderClient: OnRenderClientSync = (pageContext) => {\n  const lang = pageContext.data.pageMeta.language || \"en\";\n  const Head = pageContext.config.Head ?? (() => <></>);\n  const Page = pageContext.Page ?? (() => <></>);\n  const htmlContent = (\n    <>\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Head data={pageContext.data} />\n        {/* avoid hydrating custom code on client, it will duplicate all scripts */}\n      </head>\n      <Page data={pageContext.data} />\n    </>\n  );\n  if (root === undefined) {\n    root = createRoot(document.documentElement);\n  }\n  document.documentElement.lang = lang;\n  root.render(htmlContent);\n};\n"
  },
  {
    "path": "fixtures/ssg/renderer/+onRenderHtml.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { dangerouslySkipEscape, escapeInject } from \"vike/server\";\nimport type { OnRenderHtmlSync } from \"vike/types\";\nimport {\n  CustomCode,\n  projectId,\n  lastPublished,\n  // @todo think about how to make __generated__ typeable\n  /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */\n  // @ts-ignore\n} from \"../app/__generated__/_index\";\n\nexport const onRenderHtml: OnRenderHtmlSync = (pageContext) => {\n  const lang = pageContext.data.pageMeta.language || \"en\";\n  const Head = pageContext.config.Head ?? (() => <></>);\n  const Page = pageContext.Page ?? (() => <></>);\n  const html = dangerouslySkipEscape(\n    renderToString(\n      <html\n        lang={lang}\n        data-ws-project={projectId}\n        data-ws-last-published={lastPublished}\n      >\n        <head>\n          <meta charSet=\"UTF-8\" />\n          <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n          <Head data={pageContext.data} />\n          <CustomCode />\n        </head>\n        <Page data={pageContext.data} />\n      </html>\n    )\n  );\n  return escapeInject`<!DOCTYPE html>\n${html}\n`;\n};\n"
  },
  {
    "path": "fixtures/ssg/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"vite/client\",\n      \"@webstudio-is/react-sdk/placeholder\",\n      \"./vike.d.ts\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg/vike.d.ts",
    "content": "import type { ImageLoader } from \"@webstudio-is/image\";\nimport type { PageMeta, System } from \"@webstudio-is/sdk\";\n\ndeclare global {\n  namespace Vike {\n    interface Config {\n      lang?: (props: { data: PageData }) => string;\n      Head?: (props: { data: PageData }) => React.ReactNode;\n    }\n\n    interface PageContext {\n      constants: {\n        assetBaseUrl: string;\n        imageLoader: ImageLoader;\n      };\n      data: {\n        url: string;\n        system: System;\n        resources: Record<string, unknown>;\n        pageMeta: PageMeta;\n      };\n      Page?: (props: { data: PageData }) => React.ReactNode;\n    }\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport vike from \"vike/plugin\";\n\nexport default defineConfig({\n  plugins: [react(), vike({ prerender: true })],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"8a7358b1-7de3-459d-b7b1-56dddfb6ce1e\"\n}\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"8f0fdff2-e76b-4f86-9203-ea7df5f1143c\",\n    \"projectId\": \"8a7358b1-7de3-459d-b7b1-56dddfb6ce1e\",\n    \"version\": 7,\n    \"createdAt\": \"2026-01-04T10:46:48.415+00:00\",\n    \"updatedAt\": \"2026-01-04T10:46:48.415+00:00\",\n    \"pages\": {\n      \"meta\": {},\n      \"homePage\": {\n        \"id\": \"Y60VJnVZMU_ifI0GnAR1N\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"iavYtlaxaMTf7HB5DBf8e\",\n        \"systemDataSourceId\": \"XE93KQdVwfiA3TCf8hQAQ\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"fOBamlMdmM461hLaITmV7\",\n          \"name\": \"Untitled\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"6dkXcbi85BtasJdJdkV9L\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"302\",\n            \"redirect\": \"\\\"/\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/redirect\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"Y60VJnVZMU_ifI0GnAR1N\", \"fOBamlMdmM461hLaITmV7\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"pTyOZEX5PG52onfyt3-Vg\",\n        {\n          \"id\": \"pTyOZEX5PG52onfyt3-Vg\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"GzBytedsc258Pp7rh1hiD\",\n        {\n          \"id\": \"GzBytedsc258Pp7rh1hiD\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"ugRM71O_Lm78kMKjkrtyz\",\n        {\n          \"id\": \"ugRM71O_Lm78kMKjkrtyz\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"NKdYAYw-oxmW8laNbpiwK\",\n        {\n          \"id\": \"NKdYAYw-oxmW8laNbpiwK\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [],\n    \"styleSources\": [],\n    \"styleSourceSelections\": [],\n    \"props\": [],\n    \"dataSources\": [\n      [\n        \"XE93KQdVwfiA3TCf8hQAQ\",\n        {\n          \"id\": \"XE93KQdVwfiA3TCf8hQAQ\",\n          \"scopeInstanceId\": \"iavYtlaxaMTf7HB5DBf8e\",\n          \"name\": \"system\",\n          \"type\": \"parameter\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"iavYtlaxaMTf7HB5DBf8e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"iavYtlaxaMTf7HB5DBf8e\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"cenhRks51W1S2iJMoGmqx\"\n            }\n          ]\n        }\n      ],\n      [\n        \"cenhRks51W1S2iJMoGmqx\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"cenhRks51W1S2iJMoGmqx\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"FIXTURE-CLIENT-DO-NOT-TOUCH\"\n            }\n          ]\n        }\n      ],\n      [\n        \"6dkXcbi85BtasJdJdkV9L\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"6dkXcbi85BtasJdJdkV9L\",\n          \"component\": \"ws:element\",\n          \"tag\": \"body\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"destination\": \"saas\",\n      \"domains\": [\"fixture-client-do-not-touch-wsmel\"],\n      \"assetsDomain\": \"fixture-client-do-not-touch-wsmel\",\n      \"excludeWstdDomainFromSearch\": false\n    }\n  },\n  \"page\": {\n    \"id\": \"Y60VJnVZMU_ifI0GnAR1N\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"iavYtlaxaMTf7HB5DBf8e\",\n    \"systemDataSourceId\": \"XE93KQdVwfiA3TCf8hQAQ\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"Y60VJnVZMU_ifI0GnAR1N\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"iavYtlaxaMTf7HB5DBf8e\",\n      \"systemDataSourceId\": \"XE93KQdVwfiA3TCf8hQAQ\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"fOBamlMdmM461hLaITmV7\",\n      \"name\": \"Untitled\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"6dkXcbi85BtasJdJdkV9L\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"302\",\n        \"redirect\": \"\\\"/\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/redirect\"\n    }\n  ],\n  \"assets\": [],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"fixture-client-do-not-touch-wsmel\",\n  \"projectTitle\": \"FIXTURE-CLIENT-DO-NOT-TOUCH\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2026-01-04\",\n  },\n];\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/[redirect]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 302,\n    redirect: \"/\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/[redirect]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const projectId = \"8a7358b1-7de3-459d-b7b1-56dddfb6ce1e\";\n\nexport const lastPublished = \"2026-01-04T10:46:48.415Z\";\n\nexport const siteName = undefined;\n\nexport const breakpoints = [\n  { id: \"pTyOZEX5PG52onfyt3-Vg\" },\n  { id: \"GzBytedsc258Pp7rh1hiD\", maxWidth: 991 },\n  { id: \"ugRM71O_Lm78kMKjkrtyz\", maxWidth: 767 },\n  { id: \"NKdYAYw-oxmW8laNbpiwK\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined = undefined;\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return <body className={`w-element`} />;\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Heading as Heading,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"8a7358b1-7de3-459d-b7b1-56dddfb6ce1e\";\n\nexport const lastPublished = \"2026-01-04T10:46:48.415Z\";\n\nexport const siteName = undefined;\n\nexport const breakpoints = [\n  { id: \"pTyOZEX5PG52onfyt3-Vg\" },\n  { id: \"GzBytedsc258Pp7rh1hiD\", maxWidth: 991 },\n  { id: \"ugRM71O_Lm78kMKjkrtyz\", maxWidth: 767 },\n  { id: \"NKdYAYw-oxmW8laNbpiwK\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined = undefined;\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"FIXTURE-CLIENT-DO-NOT-TOUCH\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-element {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\n\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (process.env.NODE_ENV !== \"production\") {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://docs.netlify.com/image-cdn/overview/\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  if (props.height) {\n    searchParams.set(\"h\", props.height.toString());\n  }\n  searchParams.set(\"q\", props.quality.toString());\n  // fit=contain by default\n  return `/.netlify/images?${searchParams}`;\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/package.json",
    "content": "{\n  \"name\": \"ssg-netlify-by-project-id\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"postbuild\": \"prettier --write \\\"dist/**/*.html\\\" || true\",\n    \"dev\": \"vite dev\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-8a7358b1-7de3-459d-b7b1-56dddfb6ce1e-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=f55154e6-36b9-4920-bc81-3095cc88f8ff'\",\n    \"fixtures:sync\": \"pnpm cli sync && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"rm -rf pages && pnpm cli build --template ssg-netlify --template internal && prettier --write .\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"type\": \"module\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"prettier\": \"3.5.3\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-remix\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vike\": \"^0.4.229\"\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/+config.ts",
    "content": "import type { Config } from \"vike/types\";\n\nexport default {\n  meta: {\n    Head: {\n      env: { server: true, client: true },\n    },\n    lang: {\n      env: { server: true, client: true },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/index/+Head.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport {\n  favIconAsset,\n  pageBackgroundImageAssets,\n  pageFontAssets,\n  siteName,\n} from \"../../app/__generated__/_index\";\nimport \"../../app/__generated__/index.css\";\n\nexport const Head = ({}: { data: PageContext[\"data\"] }) => {\n  const ldJson = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebSite\",\n    name: siteName,\n  };\n  return (\n    <>\n      {siteName && (\n        <script\n          type=\"application/ld+json\"\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify(ldJson, null, 2),\n          }}\n        ></script>\n      )}\n      {favIconAsset && (\n        <link\n          rel=\"icon\"\n          href={imageLoader({\n            src: `${assetBaseUrl}${favIconAsset}`,\n            // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n            width: 144,\n            height: 144,\n            fit: \"pad\",\n            quality: 100,\n            format: \"auto\",\n          })}\n        />\n      )}\n      {pageFontAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"font\"\n          crossOrigin=\"anonymous\"\n        />\n      ))}\n      {pageBackgroundImageAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"image\"\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/index/+Page.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport {\n  PageSettingsMeta,\n  PageSettingsTitle,\n  ReactSdkContext,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport { Page, breakpoints, siteName } from \"../../app/__generated__/_index\";\n\nconst PageComponent = ({ data }: { data: PageContext[\"data\"] }) => {\n  const { system, resources, url, pageMeta } = data;\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        siteName={siteName}\n        imageLoader={imageLoader}\n        assetBaseUrl={assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\nexport default PageComponent;\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/index/+data.ts",
    "content": "import type { PageContextServer } from \"vike/types\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  getPageMeta,\n  getResources,\n} from \"../../app/__generated__/_index.server\";\nimport { assets } from \"../../app/__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const data = async (pageContext: PageContextServer) => {\n  const url = new URL(pageContext.urlOriginal, \"http://url\");\n  const headers = new Headers(pageContext.headers ?? {});\n  const host = headers.get(\"x-forwarded-host\") || headers.get(\"host\") || \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = pageContext.routeParams;\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  return {\n    url: url.href,\n    system,\n    resources,\n    pageMeta,\n  } satisfies PageContextServer[\"data\"];\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/redirect/+Head.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport {\n  favIconAsset,\n  pageBackgroundImageAssets,\n  pageFontAssets,\n  siteName,\n} from \"../../app/__generated__/[redirect]._index\";\nimport \"../../app/__generated__/index.css\";\n\nexport const Head = ({}: { data: PageContext[\"data\"] }) => {\n  const ldJson = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebSite\",\n    name: siteName,\n  };\n  return (\n    <>\n      {siteName && (\n        <script\n          type=\"application/ld+json\"\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify(ldJson, null, 2),\n          }}\n        ></script>\n      )}\n      {favIconAsset && (\n        <link\n          rel=\"icon\"\n          href={imageLoader({\n            src: `${assetBaseUrl}${favIconAsset}`,\n            // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n            width: 144,\n            height: 144,\n            fit: \"pad\",\n            quality: 100,\n            format: \"auto\",\n          })}\n        />\n      )}\n      {pageFontAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"font\"\n          crossOrigin=\"anonymous\"\n        />\n      ))}\n      {pageBackgroundImageAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"image\"\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/redirect/+Page.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport {\n  PageSettingsMeta,\n  PageSettingsTitle,\n  ReactSdkContext,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { assetBaseUrl, imageLoader } from \"../../app/constants.mjs\";\nimport {\n  Page,\n  breakpoints,\n  siteName,\n} from \"../../app/__generated__/[redirect]._index\";\n\nconst PageComponent = ({ data }: { data: PageContext[\"data\"] }) => {\n  const { system, resources, url, pageMeta } = data;\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        siteName={siteName}\n        imageLoader={imageLoader}\n        assetBaseUrl={assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\nexport default PageComponent;\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/pages/redirect/+data.ts",
    "content": "import type { PageContextServer } from \"vike/types\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  getPageMeta,\n  getResources,\n} from \"../../app/__generated__/[redirect]._index.server\";\nimport { assets } from \"../../app/__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const data = async (pageContext: PageContextServer) => {\n  const url = new URL(pageContext.urlOriginal, \"http://url\");\n  const headers = new Headers(pageContext.headers ?? {});\n  const host = headers.get(\"x-forwarded-host\") || headers.get(\"host\") || \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = pageContext.routeParams;\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  return {\n    url: url.href,\n    system,\n    resources,\n    pageMeta,\n  } satisfies PageContextServer[\"data\"];\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/renderer/+onRenderClient.tsx",
    "content": "import { type Root, createRoot } from \"react-dom/client\";\nimport type { OnRenderClientSync } from \"vike/types\";\n\nlet root: Root;\n\nexport const onRenderClient: OnRenderClientSync = (pageContext) => {\n  const lang = pageContext.data.pageMeta.language || \"en\";\n  const Head = pageContext.config.Head ?? (() => <></>);\n  const Page = pageContext.Page ?? (() => <></>);\n  const htmlContent = (\n    <>\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Head data={pageContext.data} />\n        {/* avoid hydrating custom code on client, it will duplicate all scripts */}\n      </head>\n      <Page data={pageContext.data} />\n    </>\n  );\n  if (root === undefined) {\n    root = createRoot(document.documentElement);\n  }\n  document.documentElement.lang = lang;\n  root.render(htmlContent);\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/renderer/+onRenderHtml.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { dangerouslySkipEscape, escapeInject } from \"vike/server\";\nimport type { OnRenderHtmlSync } from \"vike/types\";\nimport {\n  CustomCode,\n  projectId,\n  lastPublished,\n  // @todo think about how to make __generated__ typeable\n  /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */\n  // @ts-ignore\n} from \"../app/__generated__/_index\";\n\nexport const onRenderHtml: OnRenderHtmlSync = (pageContext) => {\n  const lang = pageContext.data.pageMeta.language || \"en\";\n  const Head = pageContext.config.Head ?? (() => <></>);\n  const Page = pageContext.Page ?? (() => <></>);\n  const html = dangerouslySkipEscape(\n    renderToString(\n      <html\n        lang={lang}\n        data-ws-project={projectId}\n        data-ws-last-published={lastPublished}\n      >\n        <head>\n          <meta charSet=\"UTF-8\" />\n          <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n          <Head data={pageContext.data} />\n          <CustomCode />\n        </head>\n        <Page data={pageContext.data} />\n      </html>\n    )\n  );\n  return escapeInject`<!DOCTYPE html>\n${html}\n`;\n};\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"vite/client\",\n      \"@webstudio-is/react-sdk/placeholder\",\n      \"./vike.d.ts\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/vike.d.ts",
    "content": "import type { ImageLoader } from \"@webstudio-is/image\";\nimport type { PageMeta, System } from \"@webstudio-is/sdk\";\n\ndeclare global {\n  namespace Vike {\n    interface Config {\n      lang?: (props: { data: PageData }) => string;\n      Head?: (props: { data: PageData }) => React.ReactNode;\n    }\n\n    interface PageContext {\n      constants: {\n        assetBaseUrl: string;\n        imageLoader: ImageLoader;\n      };\n      data: {\n        url: string;\n        system: System;\n        resources: Record<string, unknown>;\n        pageMeta: PageMeta;\n      };\n      Page?: (props: { data: PageData }) => React.ReactNode;\n    }\n  }\n}\n"
  },
  {
    "path": "fixtures/ssg-netlify-by-project-id/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport vike from \"vike/plugin\";\n\nexport default defineConfig({\n  plugins: [react(), vike({ prerender: true })],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\"\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"a2e8de30-03d5-4514-a3a6-406b3266a3af\",\n    \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n    \"version\": 38,\n    \"createdAt\": \"2024-07-29T12:50:07.515+00:00\",\n    \"updatedAt\": \"2024-07-29T12:50:07.515+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"\",\n        \"faviconAssetId\": \"\",\n        \"code\": \"\"\n      },\n      \"homePage\": {\n        \"id\": \"9di_L14CzctvSruIoKVvE\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"Home\\\"\",\n        \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n        \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n        \"meta\": {},\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n          \"name\": \"Another page\",\n          \"title\": \"\\\"Another page\\\"\",\n          \"history\": [\"/another-page\"],\n          \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/another-page\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\"9di_L14CzctvSruIoKVvE\", \"WPPAbLFyJD_02vhjRd8P4\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"rKj-wYctg3-GnqL3WHN9I\",\n        {\n          \"id\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"yH9RXhqCyeaVkrOt8MzLc\",\n        {\n          \"id\": \"yH9RXhqCyeaVkrOt8MzLc\",\n          \"label\": \"Tablet\",\n          \"maxWidth\": 991\n        }\n      ],\n      [\n        \"8nSCZbeS002IVwkTdoIes\",\n        {\n          \"id\": \"8nSCZbeS002IVwkTdoIes\",\n          \"label\": \"Mobile landscape\",\n          \"maxWidth\": 767\n        }\n      ],\n      [\n        \"7gBD25KrrbBdJYNDlhPz7\",\n        {\n          \"id\": \"7gBD25KrrbBdJYNDlhPz7\",\n          \"label\": \"Mobile portrait\",\n          \"maxWidth\": 479\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:display:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:alignItems:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:justifyContent:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr:rKj-wYctg3-GnqL3WHN9I:flexDirection:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"7_QL45cpvP-zG8Hkgf4cr\",\n          \"property\": \"flexDirection\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          }\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp:rKj-wYctg3-GnqL3WHN9I:marginBottom:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"0KA68BwP9gdTzE1ESO2Zp\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg:rKj-wYctg3-GnqL3WHN9I:width:\",\n        {\n          \"breakpointId\": \"rKj-wYctg3-GnqL3WHN9I\",\n          \"styleSourceId\": \"mf2C07UBmGT7y_G4Du3yg\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 400\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"7_QL45cpvP-zG8Hkgf4cr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7_QL45cpvP-zG8Hkgf4cr\"\n        }\n      ],\n      [\n        \"0KA68BwP9gdTzE1ESO2Zp\",\n        {\n          \"type\": \"local\",\n          \"id\": \"0KA68BwP9gdTzE1ESO2Zp\"\n        }\n      ],\n      [\n        \"mf2C07UBmGT7y_G4Du3yg\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mf2C07UBmGT7y_G4Du3yg\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"instanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"values\": [\"7_QL45cpvP-zG8Hkgf4cr\"]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"instanceId\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"values\": [\"0KA68BwP9gdTzE1ESO2Zp\"]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"values\": [\"mf2C07UBmGT7y_G4Du3yg\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"1p34InvRgqoKVqeNZ1uBb\",\n        {\n          \"id\": \"1p34InvRgqoKVqeNZ1uBb\",\n          \"instanceId\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"WPPAbLFyJD_02vhjRd8P4\"\n        }\n      ],\n      [\n        \"su3ag3OxH9WTBjJg5eIyg\",\n        {\n          \"id\": \"su3ag3OxH9WTBjJg5eIyg\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"vGCYpBBB1QUPIPPIdyexn\",\n        {\n          \"id\": \"vGCYpBBB1QUPIPPIdyexn\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"width\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"JKAGY7DWpciEl0UdnWuKL\",\n        {\n          \"id\": \"JKAGY7DWpciEl0UdnWuKL\",\n          \"instanceId\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"name\": \"height\",\n          \"type\": \"asset\",\n          \"value\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\"\n        }\n      ],\n      [\n        \"CAkmmL8-JAgokmeopoFXh\",\n        {\n          \"id\": \"CAkmmL8-JAgokmeopoFXh\",\n          \"instanceId\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"name\": \"src\",\n          \"type\": \"string\",\n          \"value\": \"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"2KT4-bRzToj9cAGAN_woK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2KT4-bRzToj9cAGAN_woK\",\n          \"scopeInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"tdXe9gFf83hSo9BLWU6xl\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"tdXe9gFf83hSo9BLWU6xl\",\n          \"scopeInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n          \"name\": \"system\"\n        }\n      ]\n    ],\n    \"resources\": [],\n    \"instances\": [\n      [\n        \"MMimeobf_zi4ZkRGXapju\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MMimeobf_zi4ZkRGXapju\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"MYDt0guk1-vzc7yzqyN6A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"BMJfjOzunWs8XkQgvvx1e\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"pjkZo5EiBqaeUXBcyHf_O\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"uHB3Fjb7-NELG-bnH7bXB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"2sIE8GxbKRBaav_zdhaZ1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MYDt0guk1-vzc7yzqyN6A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MYDt0guk1-vzc7yzqyN6A\",\n          \"component\": \"Heading\",\n          \"label\": \"xD\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Simple Project to test CLI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"BMJfjOzunWs8XkQgvvx1e\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"BMJfjOzunWs8XkQgvvx1e\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Please don't change directly in the fixture\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pjkZo5EiBqaeUXBcyHf_O\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pjkZo5EiBqaeUXBcyHf_O\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test another page link\"\n            }\n          ]\n        }\n      ],\n      [\n        \"n_VBMr7klpx25buS0NV7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"n_VBMr7klpx25buS0NV7R\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"wthNByqb3RPmheb-56VYI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wthNByqb3RPmheb-56VYI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wthNByqb3RPmheb-56VYI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Another page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uHB3Fjb7-NELG-bnH7bXB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uHB3Fjb7-NELG-bnH7bXB\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"2sIE8GxbKRBaav_zdhaZ1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"2sIE8GxbKRBaav_zdhaZ1\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"domains\": [],\n      \"projectDomain\": \"cli-basic-test-d0osr\"\n    }\n  },\n  \"page\": {\n    \"id\": \"9di_L14CzctvSruIoKVvE\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"Home\\\"\",\n    \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n    \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n    \"meta\": {},\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"9di_L14CzctvSruIoKVvE\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"Home\\\"\",\n      \"rootInstanceId\": \"MMimeobf_zi4ZkRGXapju\",\n      \"systemDataSourceId\": \"2KT4-bRzToj9cAGAN_woK\",\n      \"meta\": {},\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"WPPAbLFyJD_02vhjRd8P4\",\n      \"name\": \"Another page\",\n      \"title\": \"\\\"Another page\\\"\",\n      \"history\": [\"/another-page\"],\n      \"rootInstanceId\": \"n_VBMr7klpx25buS0NV7R\",\n      \"systemDataSourceId\": \"tdXe9gFf83hSo9BLWU6xl\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/another-page\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\",\n      \"name\": \"iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 999,\n      \"type\": \"image\",\n      \"format\": \"svg\",\n      \"createdAt\": \"2024-07-26T13:39:48.678+00:00\",\n      \"meta\": {\n        \"width\": 14,\n        \"height\": 16\n      }\n    },\n    {\n      \"id\": \"d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd\",\n      \"name\": \"147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg\",\n      \"projectId\": \"d845c167-ea07-4875-b08d-83e97c09dcce\",\n      \"size\": 64701,\n      \"type\": \"image\",\n      \"format\": \"jpg\",\n      \"createdAt\": \"2024-12-06T14:36:07.046+00:00\",\n      \"meta\": {\n        \"width\": 820,\n        \"height\": 985\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"cli-basic-test-d0osr\",\n  \"projectTitle\": \"cli-basic-test\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/WS_CF_README.md",
    "content": "# Welcome to Remix + Vite!\n\n📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features.\n\n## Typegen\n\nGenerate types for your Cloudflare bindings in `wrangler.toml`:\n\n```sh\nnpm run typegen\n```\n\nYou will need to rerun typegen whenever you make changes to `wrangler.toml`.\n\n## Development\n\nRun the Vite dev server:\n\n```sh\nnpm run dev\n```\n\nTo run Wrangler:\n\n```sh\nnpm run build\nnpm run start\n```\n\n## Deployment\n\n> [!WARNING]  \n> Cloudflare does _not_ use `wrangler.toml` to configure deployment bindings.\n> You **MUST** [configure deployment bindings manually in the Cloudflare dashboard][bindings].\n\nFirst, build your app for production:\n\n```sh\nnpm run build\n```\n\nThen, deploy your app to Cloudflare Pages:\n\n```sh\nnpm run deploy\n```\n\n[bindings]: https://developers.cloudflare.com/pages/functions/bindings/\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"1d8bf4398f643f5333d415091507d778aaed62f28883642636cbed0be156a0ee\": {\n    url: \"/cgi/image/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg?format=raw\",\n    width: 14,\n    height: 16,\n  },\n  d0974db9300c1a3b0fb8b291dd9fabd45ad136478908394280af2f7087e3aecd: {\n    url: \"/cgi/image/147-1478573_cat-icon-png-black-cat-png-icon.png_ZJ6-qJjk1RlFzuYwyCXdp.jpeg?format=raw\",\n    width: 820,\n    height: 985,\n  },\n};\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2024-07-29\",\n  },\n];\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/[another-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Another page\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/[another-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-remix\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2024-07-29T12:50:07.515Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined = undefined;\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Another page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Home\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n} from \"@webstudio-is/sdk-components-react-remix\";\nimport {\n  Heading as Heading,\n  Text as Text,\n  Image as Image,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"d845c167-ea07-4875-b08d-83e97c09dcce\";\n\nexport const lastPublished = \"2024-07-29T12:50:07.515Z\";\n\nexport const siteName = \"\";\n\nexport const breakpoints = [\n  { id: \"rKj-wYctg3-GnqL3WHN9I\" },\n  { id: \"yH9RXhqCyeaVkrOt8MzLc\", maxWidth: 991 },\n  { id: \"8nSCZbeS002IVwkTdoIes\", maxWidth: 767 },\n  { id: \"7gBD25KrrbBdJYNDlhPz7\", maxWidth: 479 },\n];\n\nexport const favIconAsset: string | undefined = undefined;\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nexport const CustomCode = () => {\n  return <></>;\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jaw2zx cbipm55 ctniqj4 ctgx88l`}>\n      <Heading className={`w-heading`}>{\"Simple Project to test CLI\"}</Heading>\n      <Text className={`w-text cn3rfux`}>\n        {\"Please don't change directly in the fixture\"}\n      </Text>\n      <Link href={\"/another-page\"} className={`w-link`}>\n        {\"Test another page link\"}\n      </Link>\n      <Image\n        src={\"/assets/iconly_svg_converted-converted_zMaMiAAutUl8XrITgz7d1.svg\"}\n        width={14}\n        height={16}\n        className={`w-image c161qeci`}\n      />\n      <Image\n        src={\"https://picsum.photos/id/237/100/100.jpg?blur=4&grayscale\"}\n        className={`w-image`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n}\n@media all {\n  .c1jaw2zx {\n    display: flex;\n  }\n  .cbipm55 {\n    align-items: center;\n  }\n  .ctniqj4 {\n    justify-content: center;\n  }\n  .ctgx88l {\n    flex-direction: column;\n  }\n  .cn3rfux {\n    margin-bottom: 1em;\n  }\n  .c161qeci {\n    width: 400px;\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = ({ src }) => {\n  return src;\n};\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/extension.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nimport { AppLoadContext } from \"@remix-run/server-runtime\";\nimport { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"@remix-run/server-runtime\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"@remix-run/react\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/routes/[another-page]._index.tsx",
    "content": "import {\n  type ServerRuntimeMetaFunction as MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  json,\n  redirect,\n} from \"@remix-run/server-runtime\";\nimport { useLoaderData } from \"@remix-run/react\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  cachedFetch,\n  formIdFieldName,\n  formBotFieldName,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n  PageSettingsCanonicalLink,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[another-page]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[another-page]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    return redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return json(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n      <PageSettingsCanonicalLink href={url} />\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/routes/[sitemap.xml]._index.tsx",
    "content": "import type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/app/routes/_index.tsx",
    "content": "import {\n  type ServerRuntimeMetaFunction as MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  json,\n  redirect,\n} from \"@remix-run/server-runtime\";\nimport { useLoaderData } from \"@remix-run/react\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  cachedFetch,\n  formIdFieldName,\n  formBotFieldName,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n  PageSettingsCanonicalLink,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/_index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/_index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    return redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return json(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n      <PageSettingsCanonicalLink href={url} />\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/functions/[[path]].ts",
    "content": "import { createPagesFunctionHandler } from \"@remix-run/cloudflare-pages\";\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - the server build file is generated by `remix vite:build`\nimport * as build from \"../build/server\";\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - the server build file is generated by `remix vite:build`\nexport const onRequest = createPagesFunctionHandler({ build });\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/load-context.ts",
    "content": "import { type PlatformProxy } from \"wrangler\";\n\ntype Cloudflare = Omit<PlatformProxy<Env>, \"dispose\">;\n\ndeclare module \"@remix-run/cloudflare\" {\n  interface AppLoadContext {\n    cloudflare: Cloudflare;\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-d845c167-ea07-4875-b08d-83e97c09dcce-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=e9d1343f-9298-4fd3-a66e-f89a5af2dd93'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId a2e8de30-03d5-4514-a3a6-406b3266a3af && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"pnpm cli build --template cloudflare --template saas-helpers --template internal && pnpm prettier --write ./app/ ./package.json ./tsconfig.json\",\n    \"build\": \"remix vite:build\",\n    \"dev\": \"remix vite:dev\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"deploy\": \"npm run build && wrangler pages deploy ./build/client\",\n    \"start\": \"wrangler pages dev ./build/client\",\n    \"typegen\": \"wrangler types\",\n    \"preview\": \"npm run build && wrangler pages dev ./build/client\",\n    \"build-cf-types\": \"wrangler types\"\n  },\n  \"private\": true,\n  \"sideEffects\": false,\n  \"name\": \"webstudio-cloudflare-template\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"dependencies\": {\n    \"@remix-run/cloudflare\": \"2.16.5\",\n    \"@remix-run/cloudflare-pages\": \"2.16.5\",\n    \"@remix-run/node\": \"2.16.5\",\n    \"@remix-run/react\": \"2.16.5\",\n    \"@remix-run/server-runtime\": \"2.16.5\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-remix\": \"workspace:*\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"webstudio\": \"workspace:*\",\n    \"worktop\": \"0.8.0-next.18\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20240620.0\",\n    \"@remix-run/dev\": \"2.16.5\",\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"fast-glob\": \"^3.3.2\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\",\n    \"wrangler\": \"^3.63.2\"\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\"\n  ],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"@remix-run/cloudflare\",\n      \"vite/client\",\n      \"@cloudflare/workers-types/2023-07-01\",\n      \"@webstudio-is/react-sdk/placeholder\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/vite.config.ts",
    "content": "import {\n  vitePlugin as remix,\n  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,\n} from \"@remix-run/dev\";\nimport { defineConfig } from \"vite\";\n\nimport { existsSync } from \"node:fs\";\n// @ts-ignore\nimport path from \"node:path\";\n// @ts-ignore\nimport fg from \"fast-glob\";\n\nconst rootDir = [\"..\", \"../..\", \"../../..\"]\n  .map((dir) => path.join(__dirname, dir))\n  .find((dir) => existsSync(path.join(dir, \".git\")));\n\nconst hasPrivateFolders =\n  fg.sync([path.join(rootDir ?? \"\", \"packages/*/private-src/*\")], {\n    ignore: [\"**/node_modules/**\"],\n  }).length > 0;\n\nconst conditions = hasPrivateFolders\n  ? [\"webstudio-private\", \"webstudio\"]\n  : [\"webstudio\"];\n\nexport default defineConfig(({ mode }) => ({\n  resolve: {\n    conditions: [...conditions, \"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [...conditions, \"node\", \"development|production\"],\n    },\n  },\n  plugins: [\n    // without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)\n    mode === \"production\" ? undefined : remixCloudflareDevProxy(),\n    remix({\n      future: {\n        v3_lazyRouteDiscovery: false,\n        v3_relativeSplatPath: false,\n        v3_singleFetch: false,\n        v3_fetcherPersist: false,\n        v3_throwAbortReason: false,\n      },\n    }),\n  ].filter(Boolean),\n}));\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/worker-configuration.d.ts",
    "content": "// Generated by Wrangler\n// After adding bindings to `wrangler.toml`, regenerate this interface via `npm build-cf-types`\ninterface Env {}\n"
  },
  {
    "path": "fixtures/webstudio-cloudflare-template/wrangler.toml",
    "content": "#:schema node_modules/wrangler/config-schema.json\n# https://developers.cloudflare.com/pages/functions/wrangler-configuration/\nname = \"webstudio-remix-app\"\ncompatibility_date = \"2024-04-05\"\npages_build_output_dir=\"./build\"\n\n# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)\n# Note: Use secrets to store sensitive data.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#environment-variables\n# [vars]\n# MY_VARIABLE = \"production_value\"\n\n# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai\n# [ai]\n# binding = \"AI\"\n\n# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases\n# [[d1_databases]]\n# binding = \"MY_DB\"\n# database_name = \"my-database\"\n# database_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.\n# Durable Objects can live for as long as needed. Use these when you need a long-running \"server\", such as in realtime apps.\n# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects\n# [[durable_objects.bindings]]\n# name = \"MY_DURABLE_OBJECT\"\n# class_name = \"MyDurableObject\"\n# script_name = 'my-durable-object'\n\n# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces\n# [[kv_namespaces]]\n# binding = \"MY_KV_NAMESPACE\"\n# id = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers\n# [[queues.producers]]\n# binding = \"MY_QUEUE\"\n# queue = \"my-queue\"\n\n# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets\n# [[r2_buckets]]\n# binding = \"MY_BUCKET\"\n# bucket_name = \"my-bucket\"\n\n# Bind another Worker service. Use this binding to call another Worker without network overhead.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings\n# [[services]]\n# binding = \"MY_SERVICE\"\n# service = \"my-service\""
  },
  {
    "path": "fixtures/webstudio-features/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "fixtures/webstudio-features/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/webstudio-features/.template/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "fixtures/webstudio-features/.template/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = ({ src }) => {\n  return src;\n};\n"
  },
  {
    "path": "fixtures/webstudio-features/.template/package.json",
    "content": "{\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"fast-glob\": \"^3.3.2\"\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/.template/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/.template/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\n// @ts-ignore\nimport { reactRouter } from \"@react-router/dev/vite\";\n// @ts-ignore\nimport { dedupeMeta } from \"./proxy-emulator/dedupe-meta\";\nimport { existsSync } from \"fs\";\n// @ts-ignore\nimport path from \"path\";\n// @ts-ignore\nimport fg from \"fast-glob\";\n\nconst rootDir = [\"..\", \"../..\", \"../../..\"]\n  .map((dir) => path.join(__dirname, dir))\n  .find((dir) => existsSync(path.join(dir, \".git\")));\n\nconst hasPrivateFolders =\n  fg.sync([path.join(rootDir ?? \"\", \"packages/*/private-src/*\")], {\n    ignore: [\"**/node_modules/**\"],\n  }).length > 0;\n\nconst conditions = hasPrivateFolders\n  ? [\"webstudio-private\", \"webstudio\"]\n  : [\"webstudio\"];\n\nexport default defineConfig({\n  resolve: {\n    conditions: [...conditions, \"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [...conditions, \"node\", \"development|production\"],\n    },\n  },\n  plugins: [reactRouter(), dedupeMeta],\n});\n"
  },
  {
    "path": "fixtures/webstudio-features/.webstudio/config.json",
    "content": "{\n  \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\"\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/.webstudio/data.json",
    "content": "{\n  \"build\": {\n    \"id\": \"1b66ee06-8ea5-4420-9a0e-e0d3a67aca32\",\n    \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n    \"version\": 634,\n    \"createdAt\": \"2026-01-15T16:19:55.574+00:00\",\n    \"updatedAt\": \"2026-01-15T16:19:55.574+00:00\",\n    \"pages\": {\n      \"meta\": {\n        \"siteName\": \"KittyGuardedZone\",\n        \"faviconAssetId\": \"7cf5892080fa66b5e6175ffd2d27c304ee6b09ce1f21847a95000225ad1afa59\",\n        \"code\": \"<script>console.log('KittyGuardedZone')</script>\\n\"\n      },\n      \"homePage\": {\n        \"id\": \"7Db64ZXgYiRqKSQNR-qTQ\",\n        \"name\": \"Home\",\n        \"title\": \"\\\"The Ultimate Cat Protection Zone\\\"\",\n        \"rootInstanceId\": \"On9cvWCxr5rdZtY9O1Bv0\",\n        \"systemDataSourceId\": \"u9wtUkt2-E0RDaljl_I6E\",\n        \"meta\": {\n          \"description\": \"\\\"Dive into the world of felines and discover why some whiskers are best left untouched. From intriguing cat behaviors to protective measures, \\\\nKittyGuardedZone is your go-to hub for all things 'hands-off' in the cat realm.\\\"\",\n          \"socialImageAssetId\": \"cd939c56-bcdd-4e64-bd9c-567a9bccd3da\",\n          \"custom\": [\n            {\n              \"property\": \"fb:app_id\",\n              \"content\": \"\\\"app_id_app_id_app_id\\\"\"\n            }\n          ]\n        },\n        \"path\": \"\"\n      },\n      \"pages\": [\n        {\n          \"id\": \"xfvB4UThQXmQ_OubPYrkg\",\n          \"name\": \"radix excluded from the search\",\n          \"title\": \"\\\"Radix Revelations: Unraveling the Feline Mystique\\\"\",\n          \"rootInstanceId\": \"uKWGyE9JY3cPwY-xI9vk6\",\n          \"systemDataSourceId\": \"GqDH_PSDF_8QXqPW2A0un\",\n          \"meta\": {\n            \"description\": \"\\\"Delve deep into the radix roots of feline behaviors. At KittyNoTouchy, we dissect the core essence, or 'radix', of what makes cats the enigmatic creatures they are. Join us as we explore the radix of their instincts, habits, and quirks.\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"socialImageAssetId\": \"88d5e2ff-b8f2-4899-aaf8-dde4ade6da10\"\n          },\n          \"path\": \"/radix\"\n        },\n        {\n          \"id\": \"szYLvBduHPmbtqQKCDY0b\",\n          \"name\": \"RouteWithSymbols\",\n          \"title\": \"\\\"RouteWithSymbols\\\"\",\n          \"history\": [\"/_route_with_symbols_\"],\n          \"rootInstanceId\": \"EDEfpMPRqDejthtwkH7ws\",\n          \"systemDataSourceId\": \"UiZtSC5BK8Ub-Ow7-XOdW\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\"\n          },\n          \"path\": \"/_route_with_symbols_\"\n        },\n        {\n          \"id\": \"U1tRJl2ERr8_OFe0g9cN_\",\n          \"name\": \"form\",\n          \"title\": \"\\\"form\\\"\",\n          \"rootInstanceId\": \"a-4nDFkaWy4px1fn38XWJ\",\n          \"systemDataSourceId\": \"-CX9SNsPSue42RkfxjL8_\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\"\n          },\n          \"path\": \"/form\"\n        },\n        {\n          \"id\": \"-J9I4Oo6mONfQlf_3-OqG\",\n          \"name\": \"heading-with-id\",\n          \"title\": \"\\\"heading-with-id\\\"\",\n          \"rootInstanceId\": \"O-ljaGZQ0iRNTlEshMkgE\",\n          \"systemDataSourceId\": \"Q6XrK4-ZZUdXsocdiWqVk\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\"\n          },\n          \"path\": \"/heading-with-id\"\n        },\n        {\n          \"id\": \"TS39WeBd0Qr3QgbSlzdf2\",\n          \"name\": \"resources\",\n          \"title\": \"\\\"resources\\\"\",\n          \"rootInstanceId\": \"AWY2qZfpbykoiWELeJhse\",\n          \"systemDataSourceId\": \"keP0A66msOpEL6HbrC9OJ\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"false\",\n            \"socialImageAssetId\": \"\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"path\": \"/resources\"\n        },\n        {\n          \"id\": \"Pnz-BxUm6XmvFygk_XdEy\",\n          \"name\": \"Nested Page\",\n          \"title\": \"\\\"Nested Page\\\"\",\n          \"rootInstanceId\": \"L0ZXd5F9xk9Rsl9ORzIkJ\",\n          \"systemDataSourceId\": \"8qqAbq6TEA3ccoCagjlZL\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"false\",\n            \"socialImageAssetId\": \"\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"path\": \"/nested-page\"\n        },\n        {\n          \"id\": \"9xlwLSHxuk8HmS3-EEGcf\",\n          \"name\": \"expressions\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"MTUwXTcUJcWmr9iGvdcoH\",\n          \"systemDataSourceId\": \"8tTIagkgzmdy4T4qzKXqd\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": []\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/expressions\"\n        },\n        {\n          \"id\": \"lTS5DKrDEC_mXSAc5ZDDA\",\n          \"name\": \"class-names\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"mdyCS8QnKx3fL1MLXifmy\",\n          \"systemDataSourceId\": \"5o_tom3ekCqphQfInC6NH\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": []\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/class-names\"\n        },\n        {\n          \"id\": \"FsnS9ui6btzM4W3YELE3Q\",\n          \"name\": \"sitemap.xml\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"z4Gp8noJpB4NS3JmdXh1Y\",\n          \"systemDataSourceId\": \"NB_R8mrrGxFXCCsAtL8QK\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"xml\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/sitemap.xml\"\n        },\n        {\n          \"id\": \"Q1D-6G1cl0SfXyM9Xj4_O\",\n          \"name\": \"content-block\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"-BW4QOi3PJTZ1sDCY8LW6\",\n          \"systemDataSourceId\": \"F3zbkztYW_mJNC5EOkopM\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": []\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/content-block\"\n        },\n        {\n          \"id\": \"8fF_9MQOwOqsLM9BhULUG\",\n          \"name\": \"head-tag\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"1NipO5NmaHukA_dxzfVRF\",\n          \"systemDataSourceId\": \"XYfrcSJVN66CpmamKON1m\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": []\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/head-tag\"\n        },\n        {\n          \"id\": \"oc1Ra5zelesj-wiJwiw-P\",\n          \"name\": \"animations\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"Qrpsu4yKLKARA-82TnhLI\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"status\": \"200\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/animations\"\n        },\n        {\n          \"id\": \"GgJUcrFoHE6EGMccjLa4t\",\n          \"name\": \"duration\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"Lmn0VIRtr_Yn9AvDyjgZG\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/duration\"\n        },\n        {\n          \"id\": \"uuYXNj4-XxzMtJpR8O5IH\",\n          \"name\": \"Text Duration\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"LTaU1Znm6P4yt3S8kAZi-\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": []\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/text-duration\"\n        },\n        {\n          \"id\": \"yaneLQ-EP83HtTFnZtbbj\",\n          \"name\": \"Assets\",\n          \"title\": \"\\\"Untitled\\\"\",\n          \"rootInstanceId\": \"jVzCxkeTMBCb0S3JUhDsQ\",\n          \"meta\": {\n            \"description\": \"\\\"\\\"\",\n            \"excludePageFromSearch\": \"true\",\n            \"language\": \"\\\"\\\"\",\n            \"socialImageUrl\": \"\\\"\\\"\",\n            \"redirect\": \"\\\"\\\"\",\n            \"documentType\": \"html\",\n            \"custom\": [\n              {\n                \"property\": \"\",\n                \"content\": \"\\\"\\\"\"\n              }\n            ]\n          },\n          \"marketplace\": {\n            \"include\": false\n          },\n          \"path\": \"/assets1\"\n        }\n      ],\n      \"folders\": [\n        {\n          \"id\": \"root\",\n          \"name\": \"Root\",\n          \"slug\": \"\",\n          \"children\": [\n            \"7Db64ZXgYiRqKSQNR-qTQ\",\n            \"xfvB4UThQXmQ_OubPYrkg\",\n            \"szYLvBduHPmbtqQKCDY0b\",\n            \"U1tRJl2ERr8_OFe0g9cN_\",\n            \"-J9I4Oo6mONfQlf_3-OqG\",\n            \"TS39WeBd0Qr3QgbSlzdf2\",\n            \"42cWhASQ3tTtKDnsvzhUF\",\n            \"9xlwLSHxuk8HmS3-EEGcf\",\n            \"lTS5DKrDEC_mXSAc5ZDDA\",\n            \"FsnS9ui6btzM4W3YELE3Q\",\n            \"Q1D-6G1cl0SfXyM9Xj4_O\",\n            \"8fF_9MQOwOqsLM9BhULUG\",\n            \"oc1Ra5zelesj-wiJwiw-P\",\n            \"GgJUcrFoHE6EGMccjLa4t\",\n            \"uuYXNj4-XxzMtJpR8O5IH\",\n            \"yaneLQ-EP83HtTFnZtbbj\"\n          ]\n        },\n        {\n          \"id\": \"42cWhASQ3tTtKDnsvzhUF\",\n          \"name\": \"Nested\",\n          \"slug\": \"nested\",\n          \"children\": [\"Pnz-BxUm6XmvFygk_XdEy\"]\n        }\n      ]\n    },\n    \"breakpoints\": [\n      [\n        \"UoTkWyaFuTYJihS3MFYK5\",\n        {\n          \"id\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"label\": \"Base\"\n        }\n      ],\n      [\n        \"ZMaWCtWpH-ao0e_kgIHqR\",\n        {\n          \"id\": \"ZMaWCtWpH-ao0e_kgIHqR\",\n          \"label\": \"mobile\",\n          \"minWidth\": 372\n        }\n      ],\n      [\n        \"Z8WjyXWkCrr35PXgjHdpY\",\n        {\n          \"id\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"label\": \"Tablet\",\n          \"minWidth\": 472\n        }\n      ]\n    ],\n    \"styles\": [\n      [\n        \"y3sBN2gB0KCBXkheM6GMm:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"y3sBN2gB0KCBXkheM6GMm\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 4\n          }\n        }\n      ],\n      [\n        \"NeVTA8c7vqOjhHotkd0Dv:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"NeVTA8c7vqOjhHotkd0Dv\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"NeVTA8c7vqOjhHotkd0Dv:UoTkWyaFuTYJihS3MFYK5:justifyContent:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"NeVTA8c7vqOjhHotkd0Dv\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"ureLSWiLGOYeWpki2JUeu:UoTkWyaFuTYJihS3MFYK5:minWidth:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ureLSWiLGOYeWpki2JUeu\",\n          \"property\": \"minWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"7HeiD-PCsn4VW6_fz5Skr:UoTkWyaFuTYJihS3MFYK5:minWidth:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"7HeiD-PCsn4VW6_fz5Skr\",\n          \"property\": \"minWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"ureLSWiLGOYeWpki2JUeu:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ureLSWiLGOYeWpki2JUeu\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"ureLSWiLGOYeWpki2JUeu:UoTkWyaFuTYJihS3MFYK5:flexShrink:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ureLSWiLGOYeWpki2JUeu\",\n          \"property\": \"flexShrink\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"7HeiD-PCsn4VW6_fz5Skr:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"7HeiD-PCsn4VW6_fz5Skr\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"7HeiD-PCsn4VW6_fz5Skr:UoTkWyaFuTYJihS3MFYK5:flexShrink:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"7HeiD-PCsn4VW6_fz5Skr\",\n          \"property\": \"flexShrink\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"ureLSWiLGOYeWpki2JUeu:UoTkWyaFuTYJihS3MFYK5:flexBasis:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ureLSWiLGOYeWpki2JUeu\",\n          \"property\": \"flexBasis\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"7HeiD-PCsn4VW6_fz5Skr:UoTkWyaFuTYJihS3MFYK5:flexBasis:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"7HeiD-PCsn4VW6_fz5Skr\",\n          \"property\": \"flexBasis\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"NeVTA8c7vqOjhHotkd0Dv:UoTkWyaFuTYJihS3MFYK5:alignItems:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"NeVTA8c7vqOjhHotkd0Dv\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"start\"\n          }\n        }\n      ],\n      [\n        \"8VVhDFG7dmCfwcwD4DH8D:UoTkWyaFuTYJihS3MFYK5:aspectRatio:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8VVhDFG7dmCfwcwD4DH8D\",\n          \"property\": \"aspectRatio\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"number\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"_t7PW0R2Naz9Zy7J1f8eQ:UoTkWyaFuTYJihS3MFYK5:paddingTop:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"_t7PW0R2Naz9Zy7J1f8eQ\",\n          \"property\": \"paddingTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 16\n          }\n        }\n      ],\n      [\n        \"_t7PW0R2Naz9Zy7J1f8eQ:UoTkWyaFuTYJihS3MFYK5:paddingRight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"_t7PW0R2Naz9Zy7J1f8eQ\",\n          \"property\": \"paddingRight\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 16\n          }\n        }\n      ],\n      [\n        \"_t7PW0R2Naz9Zy7J1f8eQ:UoTkWyaFuTYJihS3MFYK5:paddingLeft:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"_t7PW0R2Naz9Zy7J1f8eQ\",\n          \"property\": \"paddingLeft\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 16\n          }\n        }\n      ],\n      [\n        \"_t7PW0R2Naz9Zy7J1f8eQ:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"_t7PW0R2Naz9Zy7J1f8eQ\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 16\n          }\n        }\n      ],\n      [\n        \"dPMbxwoxmuFjloniiOQb-:UoTkWyaFuTYJihS3MFYK5:borderBottomWidth:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"dPMbxwoxmuFjloniiOQb-\",\n          \"property\": \"borderBottomWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"dPMbxwoxmuFjloniiOQb-:UoTkWyaFuTYJihS3MFYK5:borderBottomStyle:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"dPMbxwoxmuFjloniiOQb-\",\n          \"property\": \"borderBottomStyle\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"solid\"\n          }\n        }\n      ],\n      [\n        \"dPMbxwoxmuFjloniiOQb-:UoTkWyaFuTYJihS3MFYK5:borderBottomColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"dPMbxwoxmuFjloniiOQb-\",\n          \"property\": \"borderBottomColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"alpha\": 1,\n            \"r\": 226,\n            \"g\": 232,\n            \"b\": 240\n          }\n        }\n      ],\n      [\n        \"wVw5XxXIHCKE0ye-4HNeK:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"wVw5XxXIHCKE0ye-4HNeK\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:flexShrink:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"flexShrink\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:flexBasis:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"flexBasis\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"%\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:alignItems:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:justifyContent:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"space-between\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:paddingTop:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"paddingTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:fontWeight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"fontWeight\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"500\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:textDecorationLine::hover\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"state\": \":hover\",\n          \"property\": \"textDecorationLine\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"underline\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:--accordion-trigger-icon-transform:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"property\": \"--accordion-trigger-icon-transform\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"0deg\"\n          }\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e:UoTkWyaFuTYJihS3MFYK5:--accordion-trigger-icon-transform:[data-state=open]\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"h9BV0WW02L74rm4w_yY7e\",\n          \"state\": \"[data-state=open]\",\n          \"property\": \"--accordion-trigger-icon-transform\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"180deg\"\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:rotate:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"rotate\",\n          \"value\": {\n            \"type\": \"var\",\n            \"value\": \"accordion-trigger-icon-transform\",\n            \"fallbacks\": []\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:height:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"height\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:width:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:transitionProperty:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"transitionProperty\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"all\"\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:transitionTimingFunction:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"transitionTimingFunction\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"cubic-bezier(0.4, 0, 0.2, 1)\"\n          }\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz:UoTkWyaFuTYJihS3MFYK5:transitionDuration:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"8Mg2sVpRQaHDWI8DG7Qjz\",\n          \"property\": \"transitionDuration\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 200,\n            \"unit\": \"ms\"\n          }\n        }\n      ],\n      [\n        \"ZLyayr_Puk3HM35c44F2J:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ZLyayr_Puk3HM35c44F2J\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 0.875\n          }\n        }\n      ],\n      [\n        \"ZLyayr_Puk3HM35c44F2J:UoTkWyaFuTYJihS3MFYK5:lineHeight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ZLyayr_Puk3HM35c44F2J\",\n          \"property\": \"lineHeight\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1.25\n          }\n        }\n      ],\n      [\n        \"ZLyayr_Puk3HM35c44F2J:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ZLyayr_Puk3HM35c44F2J\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"n6w3ORlRsss2eBGMq3bW1:UoTkWyaFuTYJihS3MFYK5:borderBottomWidth:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"n6w3ORlRsss2eBGMq3bW1\",\n          \"property\": \"borderBottomWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"n6w3ORlRsss2eBGMq3bW1:UoTkWyaFuTYJihS3MFYK5:borderBottomStyle:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"n6w3ORlRsss2eBGMq3bW1\",\n          \"property\": \"borderBottomStyle\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"solid\"\n          }\n        }\n      ],\n      [\n        \"n6w3ORlRsss2eBGMq3bW1:UoTkWyaFuTYJihS3MFYK5:borderBottomColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"n6w3ORlRsss2eBGMq3bW1\",\n          \"property\": \"borderBottomColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"alpha\": 1,\n            \"r\": 226,\n            \"g\": 232,\n            \"b\": 240\n          }\n        }\n      ],\n      [\n        \"ANkU37d5ZGfcSzPhzwnGD:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ANkU37d5ZGfcSzPhzwnGD\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:flexShrink:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"flexShrink\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:flexBasis:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"flexBasis\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"%\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:alignItems:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:justifyContent:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"space-between\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:paddingTop:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"paddingTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:fontWeight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"fontWeight\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"500\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:textDecorationLine::hover\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"state\": \":hover\",\n          \"property\": \"textDecorationLine\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"underline\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:--accordion-trigger-icon-transform:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"property\": \"--accordion-trigger-icon-transform\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"0deg\"\n          }\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI:UoTkWyaFuTYJihS3MFYK5:--accordion-trigger-icon-transform:[data-state=open]\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"s6cuBxkGT20NUG_kWpsQI\",\n          \"state\": \"[data-state=open]\",\n          \"property\": \"--accordion-trigger-icon-transform\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"180deg\"\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:rotate:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"rotate\",\n          \"value\": {\n            \"type\": \"var\",\n            \"value\": \"accordion-trigger-icon-transform\",\n            \"fallbacks\": []\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:height:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"height\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:width:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:transitionProperty:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"transitionProperty\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"all\"\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:transitionTimingFunction:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"transitionTimingFunction\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"cubic-bezier(0.4, 0, 0.2, 1)\"\n          }\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK:UoTkWyaFuTYJihS3MFYK5:transitionDuration:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"eIzOGCjVa5lQceZUhtIVK\",\n          \"property\": \"transitionDuration\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 200,\n            \"unit\": \"ms\"\n          }\n        }\n      ],\n      [\n        \"4pgxDxhhX5AvMmL4olZw0:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"4pgxDxhhX5AvMmL4olZw0\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 0.875\n          }\n        }\n      ],\n      [\n        \"4pgxDxhhX5AvMmL4olZw0:UoTkWyaFuTYJihS3MFYK5:lineHeight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"4pgxDxhhX5AvMmL4olZw0\",\n          \"property\": \"lineHeight\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1.25\n          }\n        }\n      ],\n      [\n        \"4pgxDxhhX5AvMmL4olZw0:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"4pgxDxhhX5AvMmL4olZw0\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"dWnaFMuWYYTjdOgCbt0LQ:UoTkWyaFuTYJihS3MFYK5:borderBottomWidth:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"dWnaFMuWYYTjdOgCbt0LQ\",\n          \"property\": \"borderBottomWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"dWnaFMuWYYTjdOgCbt0LQ:UoTkWyaFuTYJihS3MFYK5:borderBottomStyle:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"dWnaFMuWYYTjdOgCbt0LQ\",\n          \"property\": \"borderBottomStyle\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"solid\"\n          }\n        }\n      ],\n      [\n        \"dWnaFMuWYYTjdOgCbt0LQ:UoTkWyaFuTYJihS3MFYK5:borderBottomColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"dWnaFMuWYYTjdOgCbt0LQ\",\n          \"property\": \"borderBottomColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"alpha\": 1,\n            \"r\": 226,\n            \"g\": 232,\n            \"b\": 240\n          }\n        }\n      ],\n      [\n        \"rpyoufLHt2eYUBYVUvsvU:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"rpyoufLHt2eYUBYVUvsvU\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:flexShrink:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"flexShrink\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 1,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:flexBasis:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"flexBasis\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"%\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:alignItems:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:justifyContent:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"space-between\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:paddingTop:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"paddingTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:fontWeight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"fontWeight\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"500\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:textDecorationLine::hover\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"state\": \":hover\",\n          \"property\": \"textDecorationLine\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"underline\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:--accordion-trigger-icon-transform:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"property\": \"--accordion-trigger-icon-transform\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"0deg\"\n          }\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6:UoTkWyaFuTYJihS3MFYK5:--accordion-trigger-icon-transform:[data-state=open]\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"T633SDU2F_Tk_BEOYeis6\",\n          \"state\": \"[data-state=open]\",\n          \"property\": \"--accordion-trigger-icon-transform\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"180deg\"\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:rotate:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"rotate\",\n          \"value\": {\n            \"type\": \"var\",\n            \"value\": \"accordion-trigger-icon-transform\",\n            \"fallbacks\": []\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:height:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"height\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:width:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:flexGrow:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"flexGrow\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 0,\n            \"unit\": \"number\"\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:transitionProperty:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"transitionProperty\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"all\"\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:transitionTimingFunction:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"transitionTimingFunction\",\n          \"value\": {\n            \"type\": \"unparsed\",\n            \"value\": \"cubic-bezier(0.4, 0, 0.2, 1)\"\n          }\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR:UoTkWyaFuTYJihS3MFYK5:transitionDuration:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"-tj0IHwf7a17jeBULU-OR\",\n          \"property\": \"transitionDuration\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 200,\n            \"unit\": \"ms\"\n          }\n        }\n      ],\n      [\n        \"EG1HUMyU4Rw4xD2pnGi28:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"EG1HUMyU4Rw4xD2pnGi28\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 0.875\n          }\n        }\n      ],\n      [\n        \"EG1HUMyU4Rw4xD2pnGi28:UoTkWyaFuTYJihS3MFYK5:lineHeight:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"EG1HUMyU4Rw4xD2pnGi28\",\n          \"property\": \"lineHeight\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1.25\n          }\n        }\n      ],\n      [\n        \"EG1HUMyU4Rw4xD2pnGi28:UoTkWyaFuTYJihS3MFYK5:paddingBottom:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"EG1HUMyU4Rw4xD2pnGi28\",\n          \"property\": \"paddingBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"rem\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"49DYl_wcHv6juFsJK5Qv3:UoTkWyaFuTYJihS3MFYK5:aspectRatio:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"49DYl_wcHv6juFsJK5Qv3\",\n          \"property\": \"aspectRatio\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"number\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"USUcfFPW4YwsvbvNORkpm:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"USUcfFPW4YwsvbvNORkpm\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"block\"\n          }\n        }\n      ],\n      [\n        \"y8DFYnVCeQ_smp3u6CSTn:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"y8DFYnVCeQ_smp3u6CSTn\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"block\"\n          }\n        }\n      ],\n      [\n        \"ZLyayr_Puk3HM35c44F2J:UoTkWyaFuTYJihS3MFYK5:overflowX:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ZLyayr_Puk3HM35c44F2J\",\n          \"property\": \"overflowX\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"hidden\"\n          }\n        }\n      ],\n      [\n        \"ZLyayr_Puk3HM35c44F2J:UoTkWyaFuTYJihS3MFYK5:overflowY:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"ZLyayr_Puk3HM35c44F2J\",\n          \"property\": \"overflowY\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"hidden\"\n          }\n        }\n      ],\n      [\n        \"4pgxDxhhX5AvMmL4olZw0:UoTkWyaFuTYJihS3MFYK5:overflowX:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"4pgxDxhhX5AvMmL4olZw0\",\n          \"property\": \"overflowX\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"hidden\"\n          }\n        }\n      ],\n      [\n        \"4pgxDxhhX5AvMmL4olZw0:UoTkWyaFuTYJihS3MFYK5:overflowY:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"4pgxDxhhX5AvMmL4olZw0\",\n          \"property\": \"overflowY\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"hidden\"\n          }\n        }\n      ],\n      [\n        \"EG1HUMyU4Rw4xD2pnGi28:UoTkWyaFuTYJihS3MFYK5:overflowX:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"EG1HUMyU4Rw4xD2pnGi28\",\n          \"property\": \"overflowX\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"hidden\"\n          }\n        }\n      ],\n      [\n        \"EG1HUMyU4Rw4xD2pnGi28:UoTkWyaFuTYJihS3MFYK5:overflowY:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"EG1HUMyU4Rw4xD2pnGi28\",\n          \"property\": \"overflowY\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"hidden\"\n          }\n        }\n      ],\n      [\n        \"o9VRyqmvIarF6HrGMccU8:UoTkWyaFuTYJihS3MFYK5:width:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"o9VRyqmvIarF6HrGMccU8\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 300\n          }\n        }\n      ],\n      [\n        \"g6Z7dAxi6LYB-XtI6SaRR:UoTkWyaFuTYJihS3MFYK5:marginTop:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"g6Z7dAxi6LYB-XtI6SaRR\",\n          \"property\": \"marginTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"value\": 12,\n            \"unit\": \"px\"\n          }\n        }\n      ],\n      [\n        \"LnOCG1kkSViZaAVoz4GXw:UoTkWyaFuTYJihS3MFYK5:marginTop:\",\n        {\n          \"styleSourceId\": \"LnOCG1kkSViZaAVoz4GXw\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"marginTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"LnOCG1kkSViZaAVoz4GXw:UoTkWyaFuTYJihS3MFYK5:marginBottom:\",\n        {\n          \"styleSourceId\": \"LnOCG1kkSViZaAVoz4GXw\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"MX5X3_9QBk2KNCj9_oKWA:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"MX5X3_9QBk2KNCj9_oKWA\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"9manQUfGxXPqfUvJR7nx_:UoTkWyaFuTYJihS3MFYK5:backgroundColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"9manQUfGxXPqfUvJR7nx_\",\n          \"property\": \"backgroundColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"r\": 251,\n            \"g\": 247,\n            \"b\": 3,\n            \"alpha\": 1\n          }\n        }\n      ],\n      [\n        \"-S9zvxalqR_PGnY3STOlK:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"styleSourceId\": \"-S9zvxalqR_PGnY3STOlK\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"FdQDnQuBGdJjsbJJkurgl:UoTkWyaFuTYJihS3MFYK5:backgroundColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"FdQDnQuBGdJjsbJJkurgl\",\n          \"property\": \"backgroundColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"r\": 89,\n            \"g\": 250,\n            \"b\": 2,\n            \"alpha\": 1\n          }\n        }\n      ],\n      [\n        \"w3UQay-q-ZxidA7tiKjlT:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"styleSourceId\": \"w3UQay-q-ZxidA7tiKjlT\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"2fMK05sbKQLCp_N7ES5cH:UoTkWyaFuTYJihS3MFYK5:backgroundColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"2fMK05sbKQLCp_N7ES5cH\",\n          \"property\": \"backgroundColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"r\": 2,\n            \"g\": 250,\n            \"b\": 168,\n            \"alpha\": 1\n          }\n        }\n      ],\n      [\n        \"z9mqrlxshNacx8dvtUKFI:UoTkWyaFuTYJihS3MFYK5:fontSize:\",\n        {\n          \"styleSourceId\": \"z9mqrlxshNacx8dvtUKFI\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"fontSize\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 1\n          }\n        }\n      ],\n      [\n        \"nlUHVVdPJqX4ucX3QvPHQ:UoTkWyaFuTYJihS3MFYK5:backgroundColor:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"nlUHVVdPJqX4ucX3QvPHQ\",\n          \"property\": \"backgroundColor\",\n          \"value\": {\n            \"type\": \"rgb\",\n            \"r\": 2,\n            \"g\": 139,\n            \"b\": 250,\n            \"alpha\": 1\n          }\n        }\n      ],\n      [\n        \"Eboa64A1ivGBt9vVhHdkx:UoTkWyaFuTYJihS3MFYK5:height:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"Eboa64A1ivGBt9vVhHdkx\",\n          \"property\": \"height\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"dvh\",\n            \"value\": 100\n          }\n        }\n      ],\n      [\n        \"Eboa64A1ivGBt9vVhHdkx:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"Eboa64A1ivGBt9vVhHdkx\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"Eboa64A1ivGBt9vVhHdkx:UoTkWyaFuTYJihS3MFYK5:alignItems:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"Eboa64A1ivGBt9vVhHdkx\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"Eboa64A1ivGBt9vVhHdkx:UoTkWyaFuTYJihS3MFYK5:justifyContent:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"Eboa64A1ivGBt9vVhHdkx\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"C-HNDjw4PJgh2x3Xt-zaT:UoTkWyaFuTYJihS3MFYK5:height:\",\n        {\n          \"styleSourceId\": \"C-HNDjw4PJgh2x3Xt-zaT\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"height\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"dvh\",\n            \"value\": 100\n          }\n        }\n      ],\n      [\n        \"C-HNDjw4PJgh2x3Xt-zaT:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"styleSourceId\": \"C-HNDjw4PJgh2x3Xt-zaT\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"flex\"\n          }\n        }\n      ],\n      [\n        \"C-HNDjw4PJgh2x3Xt-zaT:UoTkWyaFuTYJihS3MFYK5:alignItems:\",\n        {\n          \"styleSourceId\": \"C-HNDjw4PJgh2x3Xt-zaT\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"C-HNDjw4PJgh2x3Xt-zaT:UoTkWyaFuTYJihS3MFYK5:justifyContent:\",\n        {\n          \"styleSourceId\": \"C-HNDjw4PJgh2x3Xt-zaT\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"justifyContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"iseTqeMVub0Y6Q_3eOQ0S:Z8WjyXWkCrr35PXgjHdpY:maxWidth:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"iseTqeMVub0Y6Q_3eOQ0S\",\n          \"property\": \"maxWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 900\n          }\n        }\n      ],\n      [\n        \"iseTqeMVub0Y6Q_3eOQ0S:Z8WjyXWkCrr35PXgjHdpY:width:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"iseTqeMVub0Y6Q_3eOQ0S\",\n          \"property\": \"width\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"%\",\n            \"value\": 100\n          }\n        }\n      ],\n      [\n        \"iseTqeMVub0Y6Q_3eOQ0S:Z8WjyXWkCrr35PXgjHdpY:minWidth:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"iseTqeMVub0Y6Q_3eOQ0S\",\n          \"property\": \"minWidth\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"px\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"iseTqeMVub0Y6Q_3eOQ0S:Z8WjyXWkCrr35PXgjHdpY:justifySelf:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"iseTqeMVub0Y6Q_3eOQ0S\",\n          \"property\": \"justifySelf\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"Uhs5fZ65aKJ2wz5eB2fCJ:Z8WjyXWkCrr35PXgjHdpY:display:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"Uhs5fZ65aKJ2wz5eB2fCJ\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"grid\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"Uhs5fZ65aKJ2wz5eB2fCJ:Z8WjyXWkCrr35PXgjHdpY:gridAutoFlow:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"Uhs5fZ65aKJ2wz5eB2fCJ\",\n          \"property\": \"gridAutoFlow\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"Uhs5fZ65aKJ2wz5eB2fCJ:Z8WjyXWkCrr35PXgjHdpY:gridAutoColumns:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"Uhs5fZ65aKJ2wz5eB2fCJ\",\n          \"property\": \"gridAutoColumns\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"fr\",\n            \"value\": 1\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"9GVBj1HWWhCsENaCEZID-:Z8WjyXWkCrr35PXgjHdpY:display:\",\n        {\n          \"styleSourceId\": \"9GVBj1HWWhCsENaCEZID-\",\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"grid\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"9GVBj1HWWhCsENaCEZID-:Z8WjyXWkCrr35PXgjHdpY:gridAutoFlow:\",\n        {\n          \"styleSourceId\": \"9GVBj1HWWhCsENaCEZID-\",\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"property\": \"gridAutoFlow\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"column\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"9GVBj1HWWhCsENaCEZID-:Z8WjyXWkCrr35PXgjHdpY:gridAutoColumns:\",\n        {\n          \"styleSourceId\": \"9GVBj1HWWhCsENaCEZID-\",\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"property\": \"gridAutoColumns\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"fr\",\n            \"value\": 1\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"mcplLi9TYqk__JBKYzuss:Z8WjyXWkCrr35PXgjHdpY:height:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"mcplLi9TYqk__JBKYzuss\",\n          \"property\": \"height\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"dvh\",\n            \"value\": 40\n          }\n        }\n      ],\n      [\n        \"Uhs5fZ65aKJ2wz5eB2fCJ:Z8WjyXWkCrr35PXgjHdpY:alignItems:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"Uhs5fZ65aKJ2wz5eB2fCJ\",\n          \"property\": \"alignItems\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"start\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"Uhs5fZ65aKJ2wz5eB2fCJ:Z8WjyXWkCrr35PXgjHdpY:alignContent:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"Uhs5fZ65aKJ2wz5eB2fCJ\",\n          \"property\": \"alignContent\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"start\"\n          },\n          \"listed\": true\n        }\n      ],\n      [\n        \"8WyGOidmbyXhMWoXUdEkK:Z8WjyXWkCrr35PXgjHdpY:marginTop:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"8WyGOidmbyXhMWoXUdEkK\",\n          \"property\": \"marginTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"8WyGOidmbyXhMWoXUdEkK:Z8WjyXWkCrr35PXgjHdpY:marginBottom:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"8WyGOidmbyXhMWoXUdEkK\",\n          \"property\": \"marginBottom\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"hSMd0d2O_5ogkTfvV8g8B:Z8WjyXWkCrr35PXgjHdpY:marginTop:\",\n        {\n          \"breakpointId\": \"Z8WjyXWkCrr35PXgjHdpY\",\n          \"styleSourceId\": \"hSMd0d2O_5ogkTfvV8g8B\",\n          \"property\": \"marginTop\",\n          \"value\": {\n            \"type\": \"unit\",\n            \"unit\": \"em\",\n            \"value\": 0\n          }\n        }\n      ],\n      [\n        \"SdJcDQJhrZ0zxtPqj546x:UoTkWyaFuTYJihS3MFYK5:textAlign:\",\n        {\n          \"styleSourceId\": \"SdJcDQJhrZ0zxtPqj546x\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"textAlign\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"hEVSzkOEBn_3dNyo65PAM:UoTkWyaFuTYJihS3MFYK5:textAlign:\",\n        {\n          \"styleSourceId\": \"hEVSzkOEBn_3dNyo65PAM\",\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"property\": \"textAlign\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"center\"\n          }\n        }\n      ],\n      [\n        \"U6kV-2LTnN9kR4jWfLb1c:UoTkWyaFuTYJihS3MFYK5:display:\",\n        {\n          \"breakpointId\": \"UoTkWyaFuTYJihS3MFYK5\",\n          \"styleSourceId\": \"U6kV-2LTnN9kR4jWfLb1c\",\n          \"property\": \"display\",\n          \"value\": {\n            \"type\": \"keyword\",\n            \"value\": \"block\"\n          }\n        }\n      ]\n    ],\n    \"styleSources\": [\n      [\n        \"y3sBN2gB0KCBXkheM6GMm\",\n        {\n          \"type\": \"local\",\n          \"id\": \"y3sBN2gB0KCBXkheM6GMm\"\n        }\n      ],\n      [\n        \"NeVTA8c7vqOjhHotkd0Dv\",\n        {\n          \"type\": \"local\",\n          \"id\": \"NeVTA8c7vqOjhHotkd0Dv\"\n        }\n      ],\n      [\n        \"ureLSWiLGOYeWpki2JUeu\",\n        {\n          \"type\": \"local\",\n          \"id\": \"ureLSWiLGOYeWpki2JUeu\"\n        }\n      ],\n      [\n        \"7HeiD-PCsn4VW6_fz5Skr\",\n        {\n          \"type\": \"local\",\n          \"id\": \"7HeiD-PCsn4VW6_fz5Skr\"\n        }\n      ],\n      [\n        \"8VVhDFG7dmCfwcwD4DH8D\",\n        {\n          \"type\": \"local\",\n          \"id\": \"8VVhDFG7dmCfwcwD4DH8D\"\n        }\n      ],\n      [\n        \"_t7PW0R2Naz9Zy7J1f8eQ\",\n        {\n          \"type\": \"local\",\n          \"id\": \"_t7PW0R2Naz9Zy7J1f8eQ\"\n        }\n      ],\n      [\n        \"dPMbxwoxmuFjloniiOQb-\",\n        {\n          \"type\": \"local\",\n          \"id\": \"dPMbxwoxmuFjloniiOQb-\"\n        }\n      ],\n      [\n        \"wVw5XxXIHCKE0ye-4HNeK\",\n        {\n          \"type\": \"local\",\n          \"id\": \"wVw5XxXIHCKE0ye-4HNeK\"\n        }\n      ],\n      [\n        \"h9BV0WW02L74rm4w_yY7e\",\n        {\n          \"type\": \"local\",\n          \"id\": \"h9BV0WW02L74rm4w_yY7e\"\n        }\n      ],\n      [\n        \"8Mg2sVpRQaHDWI8DG7Qjz\",\n        {\n          \"type\": \"local\",\n          \"id\": \"8Mg2sVpRQaHDWI8DG7Qjz\"\n        }\n      ],\n      [\n        \"ZLyayr_Puk3HM35c44F2J\",\n        {\n          \"type\": \"local\",\n          \"id\": \"ZLyayr_Puk3HM35c44F2J\"\n        }\n      ],\n      [\n        \"n6w3ORlRsss2eBGMq3bW1\",\n        {\n          \"type\": \"local\",\n          \"id\": \"n6w3ORlRsss2eBGMq3bW1\"\n        }\n      ],\n      [\n        \"ANkU37d5ZGfcSzPhzwnGD\",\n        {\n          \"type\": \"local\",\n          \"id\": \"ANkU37d5ZGfcSzPhzwnGD\"\n        }\n      ],\n      [\n        \"s6cuBxkGT20NUG_kWpsQI\",\n        {\n          \"type\": \"local\",\n          \"id\": \"s6cuBxkGT20NUG_kWpsQI\"\n        }\n      ],\n      [\n        \"eIzOGCjVa5lQceZUhtIVK\",\n        {\n          \"type\": \"local\",\n          \"id\": \"eIzOGCjVa5lQceZUhtIVK\"\n        }\n      ],\n      [\n        \"4pgxDxhhX5AvMmL4olZw0\",\n        {\n          \"type\": \"local\",\n          \"id\": \"4pgxDxhhX5AvMmL4olZw0\"\n        }\n      ],\n      [\n        \"dWnaFMuWYYTjdOgCbt0LQ\",\n        {\n          \"type\": \"local\",\n          \"id\": \"dWnaFMuWYYTjdOgCbt0LQ\"\n        }\n      ],\n      [\n        \"rpyoufLHt2eYUBYVUvsvU\",\n        {\n          \"type\": \"local\",\n          \"id\": \"rpyoufLHt2eYUBYVUvsvU\"\n        }\n      ],\n      [\n        \"T633SDU2F_Tk_BEOYeis6\",\n        {\n          \"type\": \"local\",\n          \"id\": \"T633SDU2F_Tk_BEOYeis6\"\n        }\n      ],\n      [\n        \"-tj0IHwf7a17jeBULU-OR\",\n        {\n          \"type\": \"local\",\n          \"id\": \"-tj0IHwf7a17jeBULU-OR\"\n        }\n      ],\n      [\n        \"EG1HUMyU4Rw4xD2pnGi28\",\n        {\n          \"type\": \"local\",\n          \"id\": \"EG1HUMyU4Rw4xD2pnGi28\"\n        }\n      ],\n      [\n        \"49DYl_wcHv6juFsJK5Qv3\",\n        {\n          \"type\": \"local\",\n          \"id\": \"49DYl_wcHv6juFsJK5Qv3\"\n        }\n      ],\n      [\n        \"USUcfFPW4YwsvbvNORkpm\",\n        {\n          \"type\": \"local\",\n          \"id\": \"USUcfFPW4YwsvbvNORkpm\"\n        }\n      ],\n      [\n        \"y8DFYnVCeQ_smp3u6CSTn\",\n        {\n          \"type\": \"local\",\n          \"id\": \"y8DFYnVCeQ_smp3u6CSTn\"\n        }\n      ],\n      [\n        \"o9VRyqmvIarF6HrGMccU8\",\n        {\n          \"type\": \"local\",\n          \"id\": \"o9VRyqmvIarF6HrGMccU8\"\n        }\n      ],\n      [\n        \"g6Z7dAxi6LYB-XtI6SaRR\",\n        {\n          \"type\": \"local\",\n          \"id\": \"g6Z7dAxi6LYB-XtI6SaRR\"\n        }\n      ],\n      [\n        \"LnOCG1kkSViZaAVoz4GXw\",\n        {\n          \"type\": \"local\",\n          \"id\": \"LnOCG1kkSViZaAVoz4GXw\"\n        }\n      ],\n      [\n        \"MX5X3_9QBk2KNCj9_oKWA\",\n        {\n          \"type\": \"local\",\n          \"id\": \"MX5X3_9QBk2KNCj9_oKWA\"\n        }\n      ],\n      [\n        \"9manQUfGxXPqfUvJR7nx_\",\n        {\n          \"type\": \"local\",\n          \"id\": \"9manQUfGxXPqfUvJR7nx_\"\n        }\n      ],\n      [\n        \"FdQDnQuBGdJjsbJJkurgl\",\n        {\n          \"type\": \"local\",\n          \"id\": \"FdQDnQuBGdJjsbJJkurgl\"\n        }\n      ],\n      [\n        \"-S9zvxalqR_PGnY3STOlK\",\n        {\n          \"type\": \"local\",\n          \"id\": \"-S9zvxalqR_PGnY3STOlK\"\n        }\n      ],\n      [\n        \"2fMK05sbKQLCp_N7ES5cH\",\n        {\n          \"type\": \"local\",\n          \"id\": \"2fMK05sbKQLCp_N7ES5cH\"\n        }\n      ],\n      [\n        \"w3UQay-q-ZxidA7tiKjlT\",\n        {\n          \"type\": \"local\",\n          \"id\": \"w3UQay-q-ZxidA7tiKjlT\"\n        }\n      ],\n      [\n        \"nlUHVVdPJqX4ucX3QvPHQ\",\n        {\n          \"type\": \"local\",\n          \"id\": \"nlUHVVdPJqX4ucX3QvPHQ\"\n        }\n      ],\n      [\n        \"z9mqrlxshNacx8dvtUKFI\",\n        {\n          \"type\": \"local\",\n          \"id\": \"z9mqrlxshNacx8dvtUKFI\"\n        }\n      ],\n      [\n        \"Eboa64A1ivGBt9vVhHdkx\",\n        {\n          \"type\": \"local\",\n          \"id\": \"Eboa64A1ivGBt9vVhHdkx\"\n        }\n      ],\n      [\n        \"C-HNDjw4PJgh2x3Xt-zaT\",\n        {\n          \"type\": \"local\",\n          \"id\": \"C-HNDjw4PJgh2x3Xt-zaT\"\n        }\n      ],\n      [\n        \"iseTqeMVub0Y6Q_3eOQ0S\",\n        {\n          \"type\": \"local\",\n          \"id\": \"iseTqeMVub0Y6Q_3eOQ0S\"\n        }\n      ],\n      [\n        \"Uhs5fZ65aKJ2wz5eB2fCJ\",\n        {\n          \"type\": \"local\",\n          \"id\": \"Uhs5fZ65aKJ2wz5eB2fCJ\"\n        }\n      ],\n      [\n        \"9GVBj1HWWhCsENaCEZID-\",\n        {\n          \"type\": \"local\",\n          \"id\": \"9GVBj1HWWhCsENaCEZID-\"\n        }\n      ],\n      [\n        \"mcplLi9TYqk__JBKYzuss\",\n        {\n          \"type\": \"local\",\n          \"id\": \"mcplLi9TYqk__JBKYzuss\"\n        }\n      ],\n      [\n        \"8WyGOidmbyXhMWoXUdEkK\",\n        {\n          \"type\": \"local\",\n          \"id\": \"8WyGOidmbyXhMWoXUdEkK\"\n        }\n      ],\n      [\n        \"hSMd0d2O_5ogkTfvV8g8B\",\n        {\n          \"type\": \"local\",\n          \"id\": \"hSMd0d2O_5ogkTfvV8g8B\"\n        }\n      ],\n      [\n        \"SdJcDQJhrZ0zxtPqj546x\",\n        {\n          \"type\": \"local\",\n          \"id\": \"SdJcDQJhrZ0zxtPqj546x\"\n        }\n      ],\n      [\n        \"hEVSzkOEBn_3dNyo65PAM\",\n        {\n          \"type\": \"local\",\n          \"id\": \"hEVSzkOEBn_3dNyo65PAM\"\n        }\n      ],\n      [\n        \"U6kV-2LTnN9kR4jWfLb1c\",\n        {\n          \"type\": \"local\",\n          \"id\": \"U6kV-2LTnN9kR4jWfLb1c\"\n        }\n      ]\n    ],\n    \"styleSourceSelections\": [\n      [\n        \"nVMWvMsaLCcb0o1wuNQgg\",\n        {\n          \"instanceId\": \"nVMWvMsaLCcb0o1wuNQgg\",\n          \"values\": [\"y3sBN2gB0KCBXkheM6GMm\"]\n        }\n      ],\n      [\n        \"f0kF-WmL7DQg7MSyRvqY1\",\n        {\n          \"instanceId\": \"f0kF-WmL7DQg7MSyRvqY1\",\n          \"values\": [\"NeVTA8c7vqOjhHotkd0Dv\"]\n        }\n      ],\n      [\n        \"5XDbqPrZDeCwq4YJ3CHsc\",\n        {\n          \"instanceId\": \"5XDbqPrZDeCwq4YJ3CHsc\",\n          \"values\": [\"ureLSWiLGOYeWpki2JUeu\"]\n        }\n      ],\n      [\n        \"qPnkiFGDj8dITWb1kmpGl\",\n        {\n          \"instanceId\": \"qPnkiFGDj8dITWb1kmpGl\",\n          \"values\": [\"7HeiD-PCsn4VW6_fz5Skr\"]\n        }\n      ],\n      [\n        \"pX1ovPI7NdC0HRjkw6Kpw\",\n        {\n          \"instanceId\": \"pX1ovPI7NdC0HRjkw6Kpw\",\n          \"values\": [\"8VVhDFG7dmCfwcwD4DH8D\"]\n        }\n      ],\n      [\n        \"On9cvWCxr5rdZtY9O1Bv0\",\n        {\n          \"instanceId\": \"On9cvWCxr5rdZtY9O1Bv0\",\n          \"values\": [\"_t7PW0R2Naz9Zy7J1f8eQ\"]\n        }\n      ],\n      [\n        \"zJ927zk9txwUbYycKB7QA\",\n        {\n          \"instanceId\": \"zJ927zk9txwUbYycKB7QA\",\n          \"values\": [\"dPMbxwoxmuFjloniiOQb-\"]\n        }\n      ],\n      [\n        \"sMxg7xT1hwYt05hbOvoPL\",\n        {\n          \"instanceId\": \"sMxg7xT1hwYt05hbOvoPL\",\n          \"values\": [\"wVw5XxXIHCKE0ye-4HNeK\"]\n        }\n      ],\n      [\n        \"qQSA4NoyKC88O68mBiQe2\",\n        {\n          \"instanceId\": \"qQSA4NoyKC88O68mBiQe2\",\n          \"values\": [\"h9BV0WW02L74rm4w_yY7e\"]\n        }\n      ],\n      [\n        \"RSk81lLj2IGXgchTuXF7V\",\n        {\n          \"instanceId\": \"RSk81lLj2IGXgchTuXF7V\",\n          \"values\": [\"8Mg2sVpRQaHDWI8DG7Qjz\"]\n        }\n      ],\n      [\n        \"IUftdfjK-ilSzfOTdIx1u\",\n        {\n          \"instanceId\": \"IUftdfjK-ilSzfOTdIx1u\",\n          \"values\": [\"ZLyayr_Puk3HM35c44F2J\"]\n        }\n      ],\n      [\n        \"C838wkvIcA1BQu30Xu2G8\",\n        {\n          \"instanceId\": \"C838wkvIcA1BQu30Xu2G8\",\n          \"values\": [\"n6w3ORlRsss2eBGMq3bW1\"]\n        }\n      ],\n      [\n        \"fYUOB_brm6s0Ky68lzMfU\",\n        {\n          \"instanceId\": \"fYUOB_brm6s0Ky68lzMfU\",\n          \"values\": [\"ANkU37d5ZGfcSzPhzwnGD\"]\n        }\n      ],\n      [\n        \"dfd4gonev_AX6BpuCsxjb\",\n        {\n          \"instanceId\": \"dfd4gonev_AX6BpuCsxjb\",\n          \"values\": [\"s6cuBxkGT20NUG_kWpsQI\"]\n        }\n      ],\n      [\n        \"wRw75kuvFzl5NWD8IGJoI\",\n        {\n          \"instanceId\": \"wRw75kuvFzl5NWD8IGJoI\",\n          \"values\": [\"eIzOGCjVa5lQceZUhtIVK\"]\n        }\n      ],\n      [\n        \"wNRVuu0L5E8TVufKdswp1\",\n        {\n          \"instanceId\": \"wNRVuu0L5E8TVufKdswp1\",\n          \"values\": [\"4pgxDxhhX5AvMmL4olZw0\"]\n        }\n      ],\n      [\n        \"65djoTmSBGemZ2L5izQ5M\",\n        {\n          \"instanceId\": \"65djoTmSBGemZ2L5izQ5M\",\n          \"values\": [\"dWnaFMuWYYTjdOgCbt0LQ\"]\n        }\n      ],\n      [\n        \"UJYfe6kH7HqhH0YYeJwe7\",\n        {\n          \"instanceId\": \"UJYfe6kH7HqhH0YYeJwe7\",\n          \"values\": [\"rpyoufLHt2eYUBYVUvsvU\"]\n        }\n      ],\n      [\n        \"600nGddaNxGGdsuGgpxJR\",\n        {\n          \"instanceId\": \"600nGddaNxGGdsuGgpxJR\",\n          \"values\": [\"T633SDU2F_Tk_BEOYeis6\"]\n        }\n      ],\n      [\n        \"Ta70VqUb_fGJXBT_zsnxQ\",\n        {\n          \"instanceId\": \"Ta70VqUb_fGJXBT_zsnxQ\",\n          \"values\": [\"-tj0IHwf7a17jeBULU-OR\"]\n        }\n      ],\n      [\n        \"mOVPnIrlt6IwVAzI_i2Fc\",\n        {\n          \"instanceId\": \"mOVPnIrlt6IwVAzI_i2Fc\",\n          \"values\": [\"EG1HUMyU4Rw4xD2pnGi28\"]\n        }\n      ],\n      [\n        \"AdXSAYCx4QDo5QN6nLoGs\",\n        {\n          \"instanceId\": \"AdXSAYCx4QDo5QN6nLoGs\",\n          \"values\": [\"49DYl_wcHv6juFsJK5Qv3\"]\n        }\n      ],\n      [\n        \"9I4GRU1sev48hREkQcKQ-\",\n        {\n          \"instanceId\": \"9I4GRU1sev48hREkQcKQ-\",\n          \"values\": [\"USUcfFPW4YwsvbvNORkpm\"]\n        }\n      ],\n      [\n        \"81ejLVXyFEV1SxiJrWhyw\",\n        {\n          \"instanceId\": \"81ejLVXyFEV1SxiJrWhyw\",\n          \"values\": [\"y8DFYnVCeQ_smp3u6CSTn\"]\n        }\n      ],\n      [\n        \"drCx7m8q_gnNQLrhPDA-g\",\n        {\n          \"instanceId\": \"drCx7m8q_gnNQLrhPDA-g\",\n          \"values\": [\"o9VRyqmvIarF6HrGMccU8\"]\n        }\n      ],\n      [\n        \"T-rqx2b12pRrWcafRRoIG\",\n        {\n          \"instanceId\": \"T-rqx2b12pRrWcafRRoIG\",\n          \"values\": [\"g6Z7dAxi6LYB-XtI6SaRR\"]\n        }\n      ],\n      [\n        \"LYBzzBvHLrfSOmPLs52dP\",\n        {\n          \"instanceId\": \"LYBzzBvHLrfSOmPLs52dP\",\n          \"values\": [\"LnOCG1kkSViZaAVoz4GXw\"]\n        }\n      ],\n      [\n        \"3rtiNx74sdhz1rNTQBmRA\",\n        {\n          \"instanceId\": \"3rtiNx74sdhz1rNTQBmRA\",\n          \"values\": [\"MX5X3_9QBk2KNCj9_oKWA\"]\n        }\n      ],\n      [\n        \"Dwv02N8aJoxXacL58cmf-\",\n        {\n          \"instanceId\": \"Dwv02N8aJoxXacL58cmf-\",\n          \"values\": [\"9manQUfGxXPqfUvJR7nx_\"]\n        }\n      ],\n      [\n        \"TNyJ2G7r6h267foLukveQ\",\n        {\n          \"instanceId\": \"TNyJ2G7r6h267foLukveQ\",\n          \"values\": [\"FdQDnQuBGdJjsbJJkurgl\"]\n        }\n      ],\n      [\n        \"YCTo59XfqTGNPH7ow1rvU\",\n        {\n          \"instanceId\": \"YCTo59XfqTGNPH7ow1rvU\",\n          \"values\": [\"-S9zvxalqR_PGnY3STOlK\"]\n        }\n      ],\n      [\n        \"VJAhRG8CDslSQStNf0C_A\",\n        {\n          \"instanceId\": \"VJAhRG8CDslSQStNf0C_A\",\n          \"values\": [\"2fMK05sbKQLCp_N7ES5cH\"]\n        }\n      ],\n      [\n        \"67Te7ahXi4mbqb0CMC6Xh\",\n        {\n          \"instanceId\": \"67Te7ahXi4mbqb0CMC6Xh\",\n          \"values\": [\"w3UQay-q-ZxidA7tiKjlT\"]\n        }\n      ],\n      [\n        \"RHGykFKK2R4PyjNtIuSEk\",\n        {\n          \"instanceId\": \"RHGykFKK2R4PyjNtIuSEk\",\n          \"values\": [\"nlUHVVdPJqX4ucX3QvPHQ\"]\n        }\n      ],\n      [\n        \"ccyTkEM40ar6Z2UTXhFvY\",\n        {\n          \"instanceId\": \"ccyTkEM40ar6Z2UTXhFvY\",\n          \"values\": [\"z9mqrlxshNacx8dvtUKFI\"]\n        }\n      ],\n      [\n        \"1e-3lR1XsDg2IsPu4mSNr\",\n        {\n          \"instanceId\": \"1e-3lR1XsDg2IsPu4mSNr\",\n          \"values\": [\"Eboa64A1ivGBt9vVhHdkx\"]\n        }\n      ],\n      [\n        \"YrVDoWK0I_9hpQs5yJzLt\",\n        {\n          \"instanceId\": \"YrVDoWK0I_9hpQs5yJzLt\",\n          \"values\": [\"C-HNDjw4PJgh2x3Xt-zaT\"]\n        }\n      ],\n      [\n        \"Qrpsu4yKLKARA-82TnhLI\",\n        {\n          \"instanceId\": \"Qrpsu4yKLKARA-82TnhLI\",\n          \"values\": [\"iseTqeMVub0Y6Q_3eOQ0S\"]\n        }\n      ],\n      [\n        \"3hPo1LW5P_4VAyXUmVtpY\",\n        {\n          \"instanceId\": \"3hPo1LW5P_4VAyXUmVtpY\",\n          \"values\": [\"Uhs5fZ65aKJ2wz5eB2fCJ\"]\n        }\n      ],\n      [\n        \"KCX8plaeWZXn98-4qmHBW\",\n        {\n          \"instanceId\": \"KCX8plaeWZXn98-4qmHBW\",\n          \"values\": [\"9GVBj1HWWhCsENaCEZID-\"]\n        }\n      ],\n      [\n        \"81TW-o_0QLHlfmBAKFyCQ\",\n        {\n          \"instanceId\": \"81TW-o_0QLHlfmBAKFyCQ\",\n          \"values\": [\"mcplLi9TYqk__JBKYzuss\"]\n        }\n      ],\n      [\n        \"cvCjIfx9TJ3XeeSpCnW1w\",\n        {\n          \"instanceId\": \"cvCjIfx9TJ3XeeSpCnW1w\",\n          \"values\": [\"8WyGOidmbyXhMWoXUdEkK\"]\n        }\n      ],\n      [\n        \"hi1l-ro0pCVQpHFIwIhFO\",\n        {\n          \"instanceId\": \"hi1l-ro0pCVQpHFIwIhFO\",\n          \"values\": [\"hSMd0d2O_5ogkTfvV8g8B\"]\n        }\n      ],\n      [\n        \"tOdjbQbLEf52thwmubL7R\",\n        {\n          \"instanceId\": \"tOdjbQbLEf52thwmubL7R\",\n          \"values\": [\"SdJcDQJhrZ0zxtPqj546x\"]\n        }\n      ],\n      [\n        \"g__o13UKOkD0KImnp-sWp\",\n        {\n          \"instanceId\": \"g__o13UKOkD0KImnp-sWp\",\n          \"values\": [\"hEVSzkOEBn_3dNyo65PAM\"]\n        }\n      ],\n      [\n        \"Ol5bklKKxyJaS7Q3jcCfD\",\n        {\n          \"instanceId\": \"Ol5bklKKxyJaS7Q3jcCfD\",\n          \"values\": [\"U6kV-2LTnN9kR4jWfLb1c\"]\n        }\n      ]\n    ],\n    \"props\": [\n      [\n        \"rTRZFZEd03RBH4gUWj9LW\",\n        {\n          \"id\": \"rTRZFZEd03RBH4gUWj9LW\",\n          \"instanceId\": \"pX1ovPI7NdC0HRjkw6Kpw\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"cd939c56-bcdd-4e64-bd9c-567a9bccd3da\"\n        }\n      ],\n      [\n        \"_x-dxwbTQ-XBLRuYQE9Pm\",\n        {\n          \"id\": \"_x-dxwbTQ-XBLRuYQE9Pm\",\n          \"instanceId\": \"AM9fD6dv2Ftc3Xjcsd7Uc\",\n          \"name\": \"collapsible\",\n          \"type\": \"boolean\",\n          \"value\": true\n        }\n      ],\n      [\n        \"XDeoQFsXw3NhVjns6I5HM\",\n        {\n          \"id\": \"XDeoQFsXw3NhVjns6I5HM\",\n          \"instanceId\": \"AM9fD6dv2Ftc3Xjcsd7Uc\",\n          \"name\": \"value\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$RR_FthRebEUcAKUJIXl0j\"\n        }\n      ],\n      [\n        \"UZg-1PcrNODgUo94IvnGC\",\n        {\n          \"id\": \"UZg-1PcrNODgUo94IvnGC\",\n          \"instanceId\": \"AM9fD6dv2Ftc3Xjcsd7Uc\",\n          \"name\": \"onValueChange\",\n          \"type\": \"action\",\n          \"value\": [\n            {\n              \"type\": \"execute\",\n              \"args\": [\"value\"],\n              \"code\": \"$ws$dataSource$RR_FthRebEUcAKUJIXl0j = value\"\n            }\n          ]\n        }\n      ],\n      [\n        \"XxpufvbtZ9iuhkPDDigde\",\n        {\n          \"id\": \"XxpufvbtZ9iuhkPDDigde\",\n          \"instanceId\": \"d0sd_G-kHirxgjq6s6Uq1\",\n          \"name\": \"code\",\n          \"type\": \"string\",\n          \"value\": \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 16 16\\\" fill=\\\"currentColor\\\" width=\\\"100%\\\" height=\\\"100%\\\" style=\\\"display: block;\\\"><path d=\\\"M4.04 6.284a.65.65 0 0 1 .92.001L8 9.335l3.04-3.05a.65.65 0 1 1 .921.918l-3.5 3.512a.65.65 0 0 1-.921 0L4.039 7.203a.65.65 0 0 1 .001-.92Z\\\"/></svg>\"\n        }\n      ],\n      [\n        \"d26zsuAR2XZt1RRN6oJXk\",\n        {\n          \"id\": \"d26zsuAR2XZt1RRN6oJXk\",\n          \"instanceId\": \"StPslEr81nfBISqBE2R-Y\",\n          \"name\": \"code\",\n          \"type\": \"string\",\n          \"value\": \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 16 16\\\" fill=\\\"currentColor\\\" width=\\\"100%\\\" height=\\\"100%\\\" style=\\\"display: block;\\\"><path d=\\\"M4.04 6.284a.65.65 0 0 1 .92.001L8 9.335l3.04-3.05a.65.65 0 1 1 .921.918l-3.5 3.512a.65.65 0 0 1-.921 0L4.039 7.203a.65.65 0 0 1 .001-.92Z\\\"/></svg>\"\n        }\n      ],\n      [\n        \"8Ar0H_oY5LOeyp2mMksps\",\n        {\n          \"id\": \"8Ar0H_oY5LOeyp2mMksps\",\n          \"instanceId\": \"sO80m5u4f87jVGG91t6u8\",\n          \"name\": \"code\",\n          \"type\": \"string\",\n          \"value\": \"<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" viewBox=\\\"0 0 16 16\\\" fill=\\\"currentColor\\\" width=\\\"100%\\\" height=\\\"100%\\\" style=\\\"display: block;\\\"><path d=\\\"M4.04 6.284a.65.65 0 0 1 .92.001L8 9.335l3.04-3.05a.65.65 0 1 1 .921.918l-3.5 3.512a.65.65 0 0 1-.921 0L4.039 7.203a.65.65 0 0 1 .001-.92Z\\\"/></svg>\"\n        }\n      ],\n      [\n        \"HNaXZUvlg14jFvxc29n9T\",\n        {\n          \"id\": \"HNaXZUvlg14jFvxc29n9T\",\n          \"instanceId\": \"AdXSAYCx4QDo5QN6nLoGs\",\n          \"name\": \"src\",\n          \"type\": \"asset\",\n          \"value\": \"9a8bc926-7804-4d3f-af81-69196b1d2ed8\"\n        }\n      ],\n      [\n        \"tUn-hTQ0dKjsaRZ4q3m21\",\n        {\n          \"id\": \"tUn-hTQ0dKjsaRZ4q3m21\",\n          \"instanceId\": \"9I4GRU1sev48hREkQcKQ-\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"szYLvBduHPmbtqQKCDY0b\"\n        }\n      ],\n      [\n        \"OER1GvKVEE4CdEX1yrNe3\",\n        {\n          \"id\": \"OER1GvKVEE4CdEX1yrNe3\",\n          \"instanceId\": \"isNSM3wXcnHFikwNPlEOL\",\n          \"name\": \"state\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$KvfuNNCNslj7nAGsD69Fl\"\n        }\n      ],\n      [\n        \"RHHw5ACTgdDO751J8CgWB\",\n        {\n          \"id\": \"RHHw5ACTgdDO751J8CgWB\",\n          \"instanceId\": \"isNSM3wXcnHFikwNPlEOL\",\n          \"name\": \"onStateChange\",\n          \"type\": \"action\",\n          \"value\": [\n            {\n              \"type\": \"execute\",\n              \"args\": [\"state\"],\n              \"code\": \"$ws$dataSource$KvfuNNCNslj7nAGsD69Fl = state\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Nyxr4ohgm_MNVtGpsGUn3\",\n        {\n          \"id\": \"Nyxr4ohgm_MNVtGpsGUn3\",\n          \"instanceId\": \"a5YPRc19IJyhTrjjasA_R\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$KvfuNNCNslj7nAGsD69Fl === 'initial' || $ws$dataSource$KvfuNNCNslj7nAGsD69Fl === 'error'\"\n        }\n      ],\n      [\n        \"3tFb2mZpmJXG5txK78S9g\",\n        {\n          \"id\": \"3tFb2mZpmJXG5txK78S9g\",\n          \"instanceId\": \"ydR5B_9uMS4PXFS76TmBh\",\n          \"name\": \"name\",\n          \"type\": \"string\",\n          \"value\": \"name\"\n        }\n      ],\n      [\n        \"7L7d7raf6WNI4_velVRy3\",\n        {\n          \"id\": \"7L7d7raf6WNI4_velVRy3\",\n          \"instanceId\": \"TsqGP49hjgEW41ReCwrpZ\",\n          \"name\": \"name\",\n          \"type\": \"string\",\n          \"value\": \"email\"\n        }\n      ],\n      [\n        \"mtOiOi1u0WbNI09rVIS6T\",\n        {\n          \"id\": \"mtOiOi1u0WbNI09rVIS6T\",\n          \"instanceId\": \"Gw-ta0R4FNFAGBTVRWKep\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$KvfuNNCNslj7nAGsD69Fl === 'success'\"\n        }\n      ],\n      [\n        \"FTd65V4oEibesTqvAjKp0\",\n        {\n          \"id\": \"FTd65V4oEibesTqvAjKp0\",\n          \"instanceId\": \"ewk_WKpu4syHLPABMmvUz\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$KvfuNNCNslj7nAGsD69Fl === 'error'\"\n        }\n      ],\n      [\n        \"vtsKrdjJH3YT89prj2K5T\",\n        {\n          \"id\": \"vtsKrdjJH3YT89prj2K5T\",\n          \"instanceId\": \"-1RvizaBcVpHsjvnYxn1c\",\n          \"name\": \"state\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$Ip0FRY7L24QrIMdtuMN5j\"\n        }\n      ],\n      [\n        \"E2n44qWixMBKO6m8kg1wp\",\n        {\n          \"id\": \"E2n44qWixMBKO6m8kg1wp\",\n          \"instanceId\": \"-1RvizaBcVpHsjvnYxn1c\",\n          \"name\": \"onStateChange\",\n          \"type\": \"action\",\n          \"value\": [\n            {\n              \"type\": \"execute\",\n              \"args\": [\"state\"],\n              \"code\": \"$ws$dataSource$Ip0FRY7L24QrIMdtuMN5j = state\"\n            }\n          ]\n        }\n      ],\n      [\n        \"K6DEgf4WkIDqdiuiwAS5E\",\n        {\n          \"id\": \"K6DEgf4WkIDqdiuiwAS5E\",\n          \"instanceId\": \"qhnVrmYGlyrMZi3UzqSQA\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$Ip0FRY7L24QrIMdtuMN5j === 'initial' || $ws$dataSource$Ip0FRY7L24QrIMdtuMN5j === 'error'\"\n        }\n      ],\n      [\n        \"kWwLbf7GOo7-n4xOi8nZi\",\n        {\n          \"id\": \"kWwLbf7GOo7-n4xOi8nZi\",\n          \"instanceId\": \"e035xi9fcwYtrn9La49Eh\",\n          \"name\": \"name\",\n          \"type\": \"string\",\n          \"value\": \"name\"\n        }\n      ],\n      [\n        \"dnX31oCvPdAPBQ1JqbnXr\",\n        {\n          \"id\": \"dnX31oCvPdAPBQ1JqbnXr\",\n          \"instanceId\": \"dcHjdeW_HXPkyQlx3ZiL7\",\n          \"name\": \"name\",\n          \"type\": \"string\",\n          \"value\": \"email\"\n        }\n      ],\n      [\n        \"JbdIh72OZ8RnHXvYTsLRd\",\n        {\n          \"id\": \"JbdIh72OZ8RnHXvYTsLRd\",\n          \"instanceId\": \"966cjxuqP_T99N27-mqWE\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$Ip0FRY7L24QrIMdtuMN5j === 'success'\"\n        }\n      ],\n      [\n        \"6UMH0WLK_fORElv3ffHCg\",\n        {\n          \"id\": \"6UMH0WLK_fORElv3ffHCg\",\n          \"instanceId\": \"SYG5hhOz31xFJUN_v9zq6\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$Ip0FRY7L24QrIMdtuMN5j === 'error'\"\n        }\n      ],\n      [\n        \"cpfLtqW20MR6y68u70Ta2\",\n        {\n          \"id\": \"cpfLtqW20MR6y68u70Ta2\",\n          \"instanceId\": \"isNSM3wXcnHFikwNPlEOL\",\n          \"name\": \"method\",\n          \"type\": \"string\",\n          \"value\": \"get\"\n        }\n      ],\n      [\n        \"d5fWTvwp-dtCYQ0rleaQ0\",\n        {\n          \"id\": \"d5fWTvwp-dtCYQ0rleaQ0\",\n          \"instanceId\": \"isNSM3wXcnHFikwNPlEOL\",\n          \"name\": \"action\",\n          \"type\": \"string\",\n          \"value\": \"/custom\"\n        }\n      ],\n      [\n        \"Oe1u15XWPgU6oBGnrmT5E\",\n        {\n          \"id\": \"Oe1u15XWPgU6oBGnrmT5E\",\n          \"instanceId\": \"y4pceTmziuBRIDgUBQNLD\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"h3\"\n        }\n      ],\n      [\n        \"-QA9iF6dEVIibtNCO1EQp\",\n        {\n          \"id\": \"-QA9iF6dEVIibtNCO1EQp\",\n          \"instanceId\": \"YdHHf4u3jrdbRIWpB_VfH\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"h3\"\n        }\n      ],\n      [\n        \"qk_m-CJJD4kY_F07wADHU\",\n        {\n          \"id\": \"qk_m-CJJD4kY_F07wADHU\",\n          \"instanceId\": \"D8wLZzLWQfxH9uaKsn-0L\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"span\"\n        }\n      ],\n      [\n        \"GmWaLJTu_cVzrs8-chvQ4\",\n        {\n          \"id\": \"GmWaLJTu_cVzrs8-chvQ4\",\n          \"instanceId\": \"8AXawjUE3fJoOH_1qOAoq\",\n          \"name\": \"href\",\n          \"type\": \"asset\",\n          \"value\": \"9a8bc926-7804-4d3f-af81-69196b1d2ed8\"\n        }\n      ],\n      [\n        \"bCTT4ssGpfxQfmKk3mONc\",\n        {\n          \"id\": \"bCTT4ssGpfxQfmKk3mONc\",\n          \"instanceId\": \"qmxnOlSxUGpuhuonVArWJ\",\n          \"name\": \"id\",\n          \"type\": \"string\",\n          \"value\": \"my-heading\"\n        }\n      ],\n      [\n        \"z8GMy7HFi_j_felIKxGP5\",\n        {\n          \"id\": \"z8GMy7HFi_j_felIKxGP5\",\n          \"instanceId\": \"81ejLVXyFEV1SxiJrWhyw\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": {\n            \"pageId\": \"-J9I4Oo6mONfQlf_3-OqG\",\n            \"instanceId\": \"qmxnOlSxUGpuhuonVArWJ\"\n          }\n        }\n      ],\n      [\n        \"QlPyQWR-xy_0Lw2-akDvI\",\n        {\n          \"id\": \"QlPyQWR-xy_0Lw2-akDvI\",\n          \"instanceId\": \"WGSZGPAhyIuWhM1DJ7HbZ\",\n          \"name\": \"data\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$PgXFOzhK1YDdnv2BuvlwR.data\"\n        }\n      ],\n      [\n        \"HRyG-UjWjdupsAVbyRc-H\",\n        {\n          \"id\": \"HRyG-UjWjdupsAVbyRc-H\",\n          \"instanceId\": \"WGSZGPAhyIuWhM1DJ7HbZ\",\n          \"name\": \"item\",\n          \"type\": \"parameter\",\n          \"value\": \"MShox7Ngja3P8_t6XGMBw\"\n        }\n      ],\n      [\n        \"CHRYq1SSaV_uPOgkM6Cv1\",\n        {\n          \"id\": \"CHRYq1SSaV_uPOgkM6Cv1\",\n          \"instanceId\": \"05oK4Ks0ocFv3w8MJOcNR\",\n          \"name\": \"code\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$MShox7Ngja3P8_t6XGMBw.name\"\n        }\n      ],\n      [\n        \"te2oQYASF3qsGMGekIcsF\",\n        {\n          \"id\": \"te2oQYASF3qsGMGekIcsF\",\n          \"instanceId\": \"sHn4Mh7DJ5X0RiETB-zVc\",\n          \"name\": \"code\",\n          \"type\": \"expression\",\n          \"value\": \"`<script>\\nconst a = ${$ws$dataSource$LYqm_7uq8W4T7BKky9zcj.data.args}\\n\\nconst b = ${$ws$dataSource$KqMdUAQ4YmObkN1ofVzeO}\\n\\nconsole.log(a, b);\\n</script>`\"\n        }\n      ],\n      [\n        \"8GVa_kUsGQz9Ppne_hLj-\",\n        {\n          \"id\": \"8GVa_kUsGQz9Ppne_hLj-\",\n          \"instanceId\": \"drCx7m8q_gnNQLrhPDA-g\",\n          \"name\": \"className\",\n          \"type\": \"string\",\n          \"value\": \"custom-class \\\"broken 'with `symbols\"\n        }\n      ],\n      [\n        \"UVdfQwl7RfYB2wI__NFXr\",\n        {\n          \"id\": \"UVdfQwl7RfYB2wI__NFXr\",\n          \"instanceId\": \"drCx7m8q_gnNQLrhPDA-g\",\n          \"name\": \"id\",\n          \"type\": \"string\",\n          \"value\": \"\\\"broken'with`symbols\"\n        }\n      ],\n      [\n        \"I4cM2PzBX49KWxsKdGMd1\",\n        {\n          \"id\": \"I4cM2PzBX49KWxsKdGMd1\",\n          \"instanceId\": \"T-rqx2b12pRrWcafRRoIG\",\n          \"name\": \"className\",\n          \"type\": \"expression\",\n          \"value\": \"`${$ws$dataSource$FG6camSaR89vSBREAV9PT} class_3`\"\n        }\n      ],\n      [\n        \"flD2WGZT36p4CEaZOVFl6\",\n        {\n          \"id\": \"flD2WGZT36p4CEaZOVFl6\",\n          \"instanceId\": \"zJkrskDJhaEk-nOSRWrGg\",\n          \"name\": \"data\",\n          \"type\": \"expression\",\n          \"value\": \"[\\n  {\\n    \\\"path\\\": \\\"/\\\",\\n    \\\"lastModified\\\": \\\"2024-05-07\\\"\\n  },\\n  {\\n    \\\"path\\\": \\\"/olegs-test\\\",\\n    \\\"lastModified\\\": \\\"2024-05-07\\\"\\n  }\\n]\"\n        }\n      ],\n      [\n        \"4TLznPGh--A7PjsBOBxka\",\n        {\n          \"id\": \"4TLznPGh--A7PjsBOBxka\",\n          \"instanceId\": \"zJkrskDJhaEk-nOSRWrGg\",\n          \"name\": \"item\",\n          \"type\": \"parameter\",\n          \"value\": \"2pjBK-2keQ15QSbnjLOk1\"\n        }\n      ],\n      [\n        \"NHtEMEG0kS-0V_RVLYG-S\",\n        {\n          \"id\": \"NHtEMEG0kS-0V_RVLYG-S\",\n          \"instanceId\": \"USgd2tJcA7pt-9xq4H4XP\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"url\"\n        }\n      ],\n      [\n        \"xnnMp6L60pLHmdUgyanrg\",\n        {\n          \"id\": \"xnnMp6L60pLHmdUgyanrg\",\n          \"instanceId\": \"vw4jVDxlyJhw77jdG3lpd\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"loc\"\n        }\n      ],\n      [\n        \"KnGNfGmPtiKrV2FNVkpaW\",\n        {\n          \"id\": \"KnGNfGmPtiKrV2FNVkpaW\",\n          \"instanceId\": \"zgwCRmVObvwI6a7zmD9OD\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"lastmod\"\n        }\n      ],\n      [\n        \"2ec8PdWkJoKEehFI6knpl\",\n        {\n          \"id\": \"2ec8PdWkJoKEehFI6knpl\",\n          \"instanceId\": \"SVI6fI342JAxCvwsg4Oc6\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"urlset\"\n        }\n      ],\n      [\n        \"z9yQBVKOIKgPCiMWHFNkx\",\n        {\n          \"id\": \"z9yQBVKOIKgPCiMWHFNkx\",\n          \"instanceId\": \"SVI6fI342JAxCvwsg4Oc6\",\n          \"name\": \"xmlns\",\n          \"type\": \"string\",\n          \"value\": \"http://www.sitemaps.org/schemas/sitemap/0.9\"\n        }\n      ],\n      [\n        \"StcDsvZEgDu_30Lyl2F3G\",\n        {\n          \"id\": \"StcDsvZEgDu_30Lyl2F3G\",\n          \"instanceId\": \"8rjJjZTvM-WXjU8dVktBZ\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"url\"\n        }\n      ],\n      [\n        \"PlgU3AVcOnqXiNtqNAqky\",\n        {\n          \"id\": \"PlgU3AVcOnqXiNtqNAqky\",\n          \"instanceId\": \"sDGB6PDQ9YABCdk9DKBmz\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"loc\"\n        }\n      ],\n      [\n        \"r-V5MpdY0q8n-aSYi8RtT\",\n        {\n          \"id\": \"r-V5MpdY0q8n-aSYi8RtT\",\n          \"instanceId\": \"7Xcdw3QXZnp_Ehel6A6bI\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"lastmod\"\n        }\n      ],\n      [\n        \"A8Wu7uH9OlQedZwv5-P7X\",\n        {\n          \"id\": \"A8Wu7uH9OlQedZwv5-P7X\",\n          \"instanceId\": \"LYBzzBvHLrfSOmPLs52dP\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"h3\"\n        }\n      ],\n      [\n        \"K8Q0cGdTrOHzJ0WXutMae\",\n        {\n          \"id\": \"K8Q0cGdTrOHzJ0WXutMae\",\n          \"instanceId\": \"bBAE2vpcCLzlcin29wQYA\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"xhtml:link\"\n        }\n      ],\n      [\n        \"HOGRU7ZXYeL11n6TtpKJo\",\n        {\n          \"id\": \"HOGRU7ZXYeL11n6TtpKJo\",\n          \"instanceId\": \"bBAE2vpcCLzlcin29wQYA\",\n          \"name\": \"rel\",\n          \"type\": \"string\",\n          \"value\": \"alternate\"\n        }\n      ],\n      [\n        \"G-QTw79T_Q1w2oGjRWO0c\",\n        {\n          \"id\": \"G-QTw79T_Q1w2oGjRWO0c\",\n          \"instanceId\": \"bBAE2vpcCLzlcin29wQYA\",\n          \"name\": \"hreflang\",\n          \"type\": \"string\",\n          \"value\": \"en\"\n        }\n      ],\n      [\n        \"Fpb7wkI2VjRdc03wTTNfo\",\n        {\n          \"id\": \"Fpb7wkI2VjRdc03wTTNfo\",\n          \"instanceId\": \"bBAE2vpcCLzlcin29wQYA\",\n          \"name\": \"href\",\n          \"type\": \"string\",\n          \"value\": \"custom-en-location\"\n        }\n      ],\n      [\n        \"0AvNbmtwI9MjjfowIxYYh\",\n        {\n          \"id\": \"0AvNbmtwI9MjjfowIxYYh\",\n          \"instanceId\": \"yDQOMG0BUdvM5g5hDV7JP\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"xhtml:link\"\n        }\n      ],\n      [\n        \"bIQ9oPI1NqjcPRnNELKdB\",\n        {\n          \"id\": \"bIQ9oPI1NqjcPRnNELKdB\",\n          \"instanceId\": \"yDQOMG0BUdvM5g5hDV7JP\",\n          \"name\": \"rel\",\n          \"type\": \"string\",\n          \"value\": \"alternate\"\n        }\n      ],\n      [\n        \"sdbARDMZjIiaN1socbZbD\",\n        {\n          \"id\": \"sdbARDMZjIiaN1socbZbD\",\n          \"instanceId\": \"yDQOMG0BUdvM5g5hDV7JP\",\n          \"name\": \"hreflang\",\n          \"type\": \"string\",\n          \"value\": \"en\"\n        }\n      ],\n      [\n        \"erBMakiSxs-TaKCWuCoiD\",\n        {\n          \"id\": \"erBMakiSxs-TaKCWuCoiD\",\n          \"instanceId\": \"yDQOMG0BUdvM5g5hDV7JP\",\n          \"name\": \"href\",\n          \"type\": \"expression\",\n          \"value\": \"`${$ws$dataSource$NB_R8mrrGxFXCCsAtL8QK.origin ?? '${ORIGIN}'}${$ws$dataSource$2pjBK__DASH__2keQ15QSbnjLOk1.path}en`\"\n        }\n      ],\n      [\n        \"Pf6idCc5mNEGBLrdL_1yN\",\n        {\n          \"id\": \"Pf6idCc5mNEGBLrdL_1yN\",\n          \"instanceId\": \"SVI6fI342JAxCvwsg4Oc6\",\n          \"name\": \"xmlns:xhtml\",\n          \"type\": \"string\",\n          \"value\": \"http://www.w3.org/TR/xhtml11/xhtml11_schema.html\"\n        }\n      ],\n      [\n        \"kQb_yRWDNAsug8p2lhSxN\",\n        {\n          \"id\": \"kQb_yRWDNAsug8p2lhSxN\",\n          \"instanceId\": \"wLYkMGH-OJP4SnFB4F-bs\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"title\"\n        }\n      ],\n      [\n        \"q2FTulKKH-VEhZpaVqYoj\",\n        {\n          \"id\": \"q2FTulKKH-VEhZpaVqYoj\",\n          \"instanceId\": \"NBRETFGlP8H6t_1ZLbJ5J\",\n          \"name\": \"datetime\",\n          \"type\": \"string\",\n          \"value\": \"1733402818245\"\n        }\n      ],\n      [\n        \"wb8XS5grnnYTgGAotvCfZ\",\n        {\n          \"id\": \"wb8XS5grnnYTgGAotvCfZ\",\n          \"instanceId\": \"WCz4oAOyt2V-prz3LIAes\",\n          \"name\": \"tag\",\n          \"type\": \"string\",\n          \"value\": \"link\"\n        }\n      ],\n      [\n        \"mrHOFAJDwzKZZA5oGONt1\",\n        {\n          \"id\": \"mrHOFAJDwzKZZA5oGONt1\",\n          \"instanceId\": \"GFN6suio87WybEb7dc8IM\",\n          \"name\": \"rel\",\n          \"type\": \"string\",\n          \"value\": \"help\"\n        }\n      ],\n      [\n        \"TdOgnu7UkI5hG6eGZhAdL\",\n        {\n          \"id\": \"TdOgnu7UkI5hG6eGZhAdL\",\n          \"instanceId\": \"GFN6suio87WybEb7dc8IM\",\n          \"name\": \"href\",\n          \"type\": \"string\",\n          \"value\": \"/help-head-slot\"\n        }\n      ],\n      [\n        \"jyAJft6LnEDjCPoS38vLN\",\n        {\n          \"id\": \"jyAJft6LnEDjCPoS38vLN\",\n          \"instanceId\": \"Py53B3dAV0BD8QAHaWV-K\",\n          \"name\": \"name\",\n          \"type\": \"string\",\n          \"value\": \"keywords\"\n        }\n      ],\n      [\n        \"dxwgbOjQGh_vDHcuSdJf8\",\n        {\n          \"id\": \"dxwgbOjQGh_vDHcuSdJf8\",\n          \"instanceId\": \"Py53B3dAV0BD8QAHaWV-K\",\n          \"name\": \"content\",\n          \"type\": \"string\",\n          \"value\": \"Head Slot Content\"\n        }\n      ],\n      [\n        \"IIwy20wFuPHblBiV5yOa8\",\n        {\n          \"id\": \"IIwy20wFuPHblBiV5yOa8\",\n          \"instanceId\": \"69Q0-F165KT33psqFcq6M\",\n          \"name\": \"href\",\n          \"type\": \"page\",\n          \"value\": \"7Db64ZXgYiRqKSQNR-qTQ\"\n        }\n      ],\n      [\n        \"9axbHjZtX8BX6vXEai1xq\",\n        {\n          \"id\": \"9axbHjZtX8BX6vXEai1xq\",\n          \"instanceId\": \"EveN4Skg9xb8Lj1QJcW52\",\n          \"name\": \"content\",\n          \"type\": \"string\",\n          \"value\": \"Head Slot Content\"\n        }\n      ],\n      [\n        \"B3AKQ4tazCkXwEKfNr2E7\",\n        {\n          \"id\": \"B3AKQ4tazCkXwEKfNr2E7\",\n          \"instanceId\": \"EveN4Skg9xb8Lj1QJcW52\",\n          \"name\": \"property\",\n          \"type\": \"string\",\n          \"value\": \"og:title\"\n        }\n      ],\n      [\n        \"Eo5zG6ibHI2ueliryZsqN\",\n        {\n          \"id\": \"Eo5zG6ibHI2ueliryZsqN\",\n          \"instanceId\": \"f0kF-WmL7DQg7MSyRvqY1\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"boolean\",\n          \"value\": true\n        }\n      ],\n      [\n        \"DohJcBH-kmzk2lvN_F2hS\",\n        {\n          \"id\": \"DohJcBH-kmzk2lvN_F2hS\",\n          \"instanceId\": \"56x98yMSVYpp7eVo8wuzK\",\n          \"name\": \"rel\",\n          \"type\": \"string\",\n          \"value\": \"canonical\"\n        }\n      ],\n      [\n        \"_wNeTsN1d6jG8ZM5ud05d\",\n        {\n          \"id\": \"_wNeTsN1d6jG8ZM5ud05d\",\n          \"instanceId\": \"56x98yMSVYpp7eVo8wuzK\",\n          \"name\": \"href\",\n          \"type\": \"string\",\n          \"value\": \"https://overwritten.slot/head-slot-tag\"\n        }\n      ],\n      [\n        \"gPNdj8LXDT6H6DLLtqzUi\",\n        {\n          \"id\": \"gPNdj8LXDT6H6DLLtqzUi\",\n          \"instanceId\": \"D6OU4dJ2tzc_Uq5LqMVWo\",\n          \"name\": \"action\",\n          \"type\": \"animationAction\",\n          \"value\": {\n            \"type\": \"view\",\n            \"animations\": [\n              {\n                \"name\": \"Fade In\",\n                \"description\": \"Fade in the element as it scrolls into view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"opacity\": {\n                        \"type\": \"unit\",\n                        \"unit\": \"number\",\n                        \"value\": 0\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"rangeStart\": [\n                    \"entry\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"entry\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 100,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Fade Out\",\n                \"description\": \"Fade out the element as it scrolls out of view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 1,\n                    \"styles\": {\n                      \"opacity\": {\n                        \"type\": \"unit\",\n                        \"unit\": \"number\",\n                        \"value\": 0\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"forwards\",\n                  \"rangeStart\": [\n                    \"exit\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"exit\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 100,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Parallax In\",\n                \"description\": \"Parallax the element as it scrolls out of view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"translate\": {\n                        \"type\": \"tuple\",\n                        \"value\": [\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"number\",\n                            \"value\": 0\n                          },\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"px\",\n                            \"value\": 100\n                          }\n                        ]\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Parallax Out\",\n                \"description\": \"Parallax the element as it scrolls out of view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 1,\n                    \"styles\": {\n                      \"translate\": {\n                        \"type\": \"tuple\",\n                        \"value\": [\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"number\",\n                            \"value\": 0\n                          },\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"px\",\n                            \"value\": -100\n                          }\n                        ]\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"forwards\",\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 100,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              }\n            ],\n            \"isPinned\": false\n          }\n        }\n      ],\n      [\n        \"JQasStAdmMJWCzknMxi6D\",\n        {\n          \"id\": \"JQasStAdmMJWCzknMxi6D\",\n          \"instanceId\": \"ed6Q3Y2M1MXZf7E8rMwwW\",\n          \"name\": \"action\",\n          \"type\": \"animationAction\",\n          \"value\": {\n            \"type\": \"view\",\n            \"animations\": [\n              {\n                \"name\": \"Fade In\",\n                \"description\": \"Fade in the element as it scrolls into view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"opacity\": {\n                        \"type\": \"unit\",\n                        \"unit\": \"number\",\n                        \"value\": 0\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"rangeStart\": [\n                    \"entry\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"entry\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 100,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Fade Out\",\n                \"description\": \"Fade out the element as it scrolls out of view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 1,\n                    \"styles\": {\n                      \"opacity\": {\n                        \"type\": \"unit\",\n                        \"unit\": \"number\",\n                        \"value\": 0\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"forwards\",\n                  \"rangeStart\": [\n                    \"exit\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"exit\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 100,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Parallax In\",\n                \"description\": \"Parallax the element as it scrolls out of view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"translate\": {\n                        \"type\": \"tuple\",\n                        \"value\": [\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"number\",\n                            \"value\": 0\n                          },\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"px\",\n                            \"value\": 100\n                          }\n                        ]\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Parallax Out\",\n                \"description\": \"Parallax the element as it scrolls out of view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 1,\n                    \"styles\": {\n                      \"translate\": {\n                        \"type\": \"tuple\",\n                        \"value\": [\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"number\",\n                            \"value\": 0\n                          },\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"px\",\n                            \"value\": -100\n                          }\n                        ]\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"forwards\",\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 100,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              }\n            ],\n            \"isPinned\": false\n          }\n        }\n      ],\n      [\n        \"PkOUxrUpSNIkRV8MuLSQh\",\n        {\n          \"id\": \"PkOUxrUpSNIkRV8MuLSQh\",\n          \"instanceId\": \"GRj9UMUzk-HAWRctEcfFH\",\n          \"name\": \"slidingWindow\",\n          \"type\": \"number\",\n          \"value\": 20\n        }\n      ],\n      [\n        \"CKL9YyMMdbB8Bg1o2M0S-\",\n        {\n          \"id\": \"CKL9YyMMdbB8Bg1o2M0S-\",\n          \"instanceId\": \"Iz65PDmd9YjqrIy5fGtWt\",\n          \"name\": \"action\",\n          \"type\": \"animationAction\",\n          \"value\": {\n            \"type\": \"view\",\n            \"animations\": [\n              {\n                \"name\": \"Parent\",\n                \"description\": \"Parallax the element as it scrolls into the view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"translate\": {\n                        \"type\": \"tuple\",\n                        \"value\": [\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"number\",\n                            \"value\": 0\n                          },\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"px\",\n                            \"value\": 100\n                          }\n                        ]\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"duration\": {\n                    \"type\": \"unit\",\n                    \"value\": 302,\n                    \"unit\": \"ms\"\n                  },\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              }\n            ],\n            \"insetStart\": {\n              \"type\": \"keyword\",\n              \"value\": \"auto\"\n            },\n            \"insetEnd\": {\n              \"type\": \"keyword\",\n              \"value\": \"auto\"\n            },\n            \"isPinned\": true,\n            \"debug\": false\n          }\n        }\n      ],\n      [\n        \"UMpi59UWI6MbY9ExyAHPM\",\n        {\n          \"id\": \"UMpi59UWI6MbY9ExyAHPM\",\n          \"instanceId\": \"Z4ogkQT54jFVlCIFkXKmb\",\n          \"name\": \"action\",\n          \"type\": \"animationAction\",\n          \"value\": {\n            \"type\": \"view\",\n            \"animations\": [\n              {\n                \"name\": \"Parent\",\n                \"description\": \"Parallax the element as it scrolls into the view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"translate\": {\n                        \"type\": \"tuple\",\n                        \"value\": [\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"number\",\n                            \"value\": 0\n                          },\n                          {\n                            \"type\": \"unit\",\n                            \"unit\": \"px\",\n                            \"value\": 200\n                          }\n                        ]\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"duration\": {\n                    \"type\": \"unit\",\n                    \"value\": 600,\n                    \"unit\": \"ms\"\n                  },\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              },\n              {\n                \"name\": \"Parallax In\",\n                \"description\": \"Parallax the element as it scrolls into the view.\",\n                \"keyframes\": [\n                  {\n                    \"offset\": 0,\n                    \"styles\": {\n                      \"opacity\": {\n                        \"type\": \"unit\",\n                        \"unit\": \"%\",\n                        \"value\": 0\n                      }\n                    }\n                  }\n                ],\n                \"timing\": {\n                  \"easing\": \"linear\",\n                  \"fill\": \"backwards\",\n                  \"duration\": {\n                    \"type\": \"unit\",\n                    \"value\": 600,\n                    \"unit\": \"ms\"\n                  },\n                  \"rangeStart\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 0,\n                      \"unit\": \"%\"\n                    }\n                  ],\n                  \"rangeEnd\": [\n                    \"cover\",\n                    {\n                      \"type\": \"unit\",\n                      \"value\": 50,\n                      \"unit\": \"%\"\n                    }\n                  ]\n                }\n              }\n            ],\n            \"insetStart\": {\n              \"type\": \"keyword\",\n              \"value\": \"auto\"\n            },\n            \"insetEnd\": {\n              \"type\": \"keyword\",\n              \"value\": \"auto\"\n            },\n            \"isPinned\": true,\n            \"debug\": false\n          }\n        }\n      ],\n      [\n        \"pCJa0iDVD_LkD7VvQcoka\",\n        {\n          \"id\": \"pCJa0iDVD_LkD7VvQcoka\",\n          \"instanceId\": \"Z4ogkQT54jFVlCIFkXKmb\",\n          \"name\": \"data-ws-show\",\n          \"type\": \"boolean\",\n          \"value\": true\n        }\n      ],\n      [\n        \"qG9NyGQuq0KVR0zvf76Er\",\n        {\n          \"id\": \"qG9NyGQuq0KVR0zvf76Er\",\n          \"instanceId\": \"Ol5bklKKxyJaS7Q3jcCfD\",\n          \"name\": \"href\",\n          \"type\": \"string\",\n          \"value\": \"https://github.com/\"\n        }\n      ],\n      [\n        \"ZxrMS3X4PXwZxwVcIs8qJ\",\n        {\n          \"id\": \"ZxrMS3X4PXwZxwVcIs8qJ\",\n          \"instanceId\": \"a94S1sqeP8lT7aeVPBv6c\",\n          \"name\": \"controls\",\n          \"type\": \"boolean\",\n          \"value\": true\n        }\n      ],\n      [\n        \"uAf7YS4QAgaZwOJceKp70\",\n        {\n          \"id\": \"uAf7YS4QAgaZwOJceKp70\",\n          \"instanceId\": \"a94S1sqeP8lT7aeVPBv6c\",\n          \"name\": \"src\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$BV__DASH__6Do0Ww17cqZ0zP7Io3.data['2b151fc7b4b0324e6ab78c40f72c7f59273f81fb1be3d08b3f9976428601b95e'].url\"\n        }\n      ],\n      [\n        \"cGY1ZGxYEUmexYtGPrqCr\",\n        {\n          \"id\": \"cGY1ZGxYEUmexYtGPrqCr\",\n          \"instanceId\": \"aAVR8Rg52_1qq8NV5rKKe\",\n          \"name\": \"controls\",\n          \"type\": \"boolean\",\n          \"value\": true\n        }\n      ],\n      [\n        \"YFS85MWqKhrhYouWYJ3Ui\",\n        {\n          \"id\": \"YFS85MWqKhrhYouWYJ3Ui\",\n          \"instanceId\": \"aAVR8Rg52_1qq8NV5rKKe\",\n          \"name\": \"src\",\n          \"type\": \"expression\",\n          \"value\": \"$ws$dataSource$BV__DASH__6Do0Ww17cqZ0zP7Io3.data['4afe692f78ec0530e355a551a3860302c4478037db7b25a3bdae82c32c78634d'].url\"\n        }\n      ]\n    ],\n    \"dataSources\": [\n      [\n        \"RR_FthRebEUcAKUJIXl0j\",\n        {\n          \"type\": \"variable\",\n          \"id\": \"RR_FthRebEUcAKUJIXl0j\",\n          \"scopeInstanceId\": \"AM9fD6dv2Ftc3Xjcsd7Uc\",\n          \"name\": \"accordionValue\",\n          \"value\": {\n            \"type\": \"string\",\n            \"value\": \"0\"\n          }\n        }\n      ],\n      [\n        \"KvfuNNCNslj7nAGsD69Fl\",\n        {\n          \"type\": \"variable\",\n          \"id\": \"KvfuNNCNslj7nAGsD69Fl\",\n          \"scopeInstanceId\": \"isNSM3wXcnHFikwNPlEOL\",\n          \"name\": \"formState\",\n          \"value\": {\n            \"type\": \"string\",\n            \"value\": \"initial\"\n          }\n        }\n      ],\n      [\n        \"Ip0FRY7L24QrIMdtuMN5j\",\n        {\n          \"type\": \"variable\",\n          \"id\": \"Ip0FRY7L24QrIMdtuMN5j\",\n          \"scopeInstanceId\": \"-1RvizaBcVpHsjvnYxn1c\",\n          \"name\": \"formState\",\n          \"value\": {\n            \"type\": \"string\",\n            \"value\": \"initial\"\n          }\n        }\n      ],\n      [\n        \"PgXFOzhK1YDdnv2BuvlwR\",\n        {\n          \"type\": \"resource\",\n          \"id\": \"PgXFOzhK1YDdnv2BuvlwR\",\n          \"scopeInstanceId\": \"AWY2qZfpbykoiWELeJhse\",\n          \"name\": \"list\",\n          \"resourceId\": \"1vX6SQdaCjJN6MvJlG_cQ\"\n        }\n      ],\n      [\n        \"MShox7Ngja3P8_t6XGMBw\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"MShox7Ngja3P8_t6XGMBw\",\n          \"scopeInstanceId\": \"WGSZGPAhyIuWhM1DJ7HbZ\",\n          \"name\": \"collectionItem\"\n        }\n      ],\n      [\n        \"u9wtUkt2-E0RDaljl_I6E\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"u9wtUkt2-E0RDaljl_I6E\",\n          \"scopeInstanceId\": \"On9cvWCxr5rdZtY9O1Bv0\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"GqDH_PSDF_8QXqPW2A0un\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"GqDH_PSDF_8QXqPW2A0un\",\n          \"scopeInstanceId\": \"uKWGyE9JY3cPwY-xI9vk6\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"UiZtSC5BK8Ub-Ow7-XOdW\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"UiZtSC5BK8Ub-Ow7-XOdW\",\n          \"scopeInstanceId\": \"EDEfpMPRqDejthtwkH7ws\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"-CX9SNsPSue42RkfxjL8_\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"-CX9SNsPSue42RkfxjL8_\",\n          \"scopeInstanceId\": \"a-4nDFkaWy4px1fn38XWJ\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"Q6XrK4-ZZUdXsocdiWqVk\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"Q6XrK4-ZZUdXsocdiWqVk\",\n          \"scopeInstanceId\": \"O-ljaGZQ0iRNTlEshMkgE\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"keP0A66msOpEL6HbrC9OJ\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"keP0A66msOpEL6HbrC9OJ\",\n          \"scopeInstanceId\": \"AWY2qZfpbykoiWELeJhse\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"8qqAbq6TEA3ccoCagjlZL\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"8qqAbq6TEA3ccoCagjlZL\",\n          \"scopeInstanceId\": \"L0ZXd5F9xk9Rsl9ORzIkJ\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"8tTIagkgzmdy4T4qzKXqd\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"8tTIagkgzmdy4T4qzKXqd\",\n          \"scopeInstanceId\": \"MTUwXTcUJcWmr9iGvdcoH\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"LYqm_7uq8W4T7BKky9zcj\",\n        {\n          \"type\": \"resource\",\n          \"id\": \"LYqm_7uq8W4T7BKky9zcj\",\n          \"scopeInstanceId\": \"MTUwXTcUJcWmr9iGvdcoH\",\n          \"name\": \"jsonResourceVariable\",\n          \"resourceId\": \"fjMzCru8O2U31xY2P1Ovr\"\n        }\n      ],\n      [\n        \"KqMdUAQ4YmObkN1ofVzeO\",\n        {\n          \"type\": \"variable\",\n          \"id\": \"KqMdUAQ4YmObkN1ofVzeO\",\n          \"scopeInstanceId\": \"MTUwXTcUJcWmr9iGvdcoH\",\n          \"name\": \"jsonVar\",\n          \"value\": {\n            \"type\": \"json\",\n            \"value\": {\n              \"hello\": \"world\"\n            }\n          }\n        }\n      ],\n      [\n        \"5o_tom3ekCqphQfInC6NH\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"5o_tom3ekCqphQfInC6NH\",\n          \"scopeInstanceId\": \"mdyCS8QnKx3fL1MLXifmy\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"FG6camSaR89vSBREAV9PT\",\n        {\n          \"type\": \"variable\",\n          \"id\": \"FG6camSaR89vSBREAV9PT\",\n          \"scopeInstanceId\": \"mdyCS8QnKx3fL1MLXifmy\",\n          \"name\": \"classVar\",\n          \"value\": {\n            \"type\": \"string\",\n            \"value\": \"varClass\"\n          }\n        }\n      ],\n      [\n        \"NB_R8mrrGxFXCCsAtL8QK\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"NB_R8mrrGxFXCCsAtL8QK\",\n          \"scopeInstanceId\": \"z4Gp8noJpB4NS3JmdXh1Y\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"2pjBK-2keQ15QSbnjLOk1\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"2pjBK-2keQ15QSbnjLOk1\",\n          \"scopeInstanceId\": \"zJkrskDJhaEk-nOSRWrGg\",\n          \"name\": \"url\"\n        }\n      ],\n      [\n        \"F3zbkztYW_mJNC5EOkopM\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"F3zbkztYW_mJNC5EOkopM\",\n          \"scopeInstanceId\": \"-BW4QOi3PJTZ1sDCY8LW6\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"XYfrcSJVN66CpmamKON1m\",\n        {\n          \"type\": \"parameter\",\n          \"id\": \"XYfrcSJVN66CpmamKON1m\",\n          \"scopeInstanceId\": \"1NipO5NmaHukA_dxzfVRF\",\n          \"name\": \"system\"\n        }\n      ],\n      [\n        \"rKNaaGjEYnq4XPiTrSlfe\",\n        {\n          \"type\": \"variable\",\n          \"id\": \"rKNaaGjEYnq4XPiTrSlfe\",\n          \"scopeInstanceId\": \":root\",\n          \"name\": \"globalVariable\",\n          \"value\": {\n            \"type\": \"string\",\n            \"value\": \"globalValue\"\n          }\n        }\n      ],\n      [\n        \"BV-6Do0Ww17cqZ0zP7Io3\",\n        {\n          \"type\": \"resource\",\n          \"id\": \"BV-6Do0Ww17cqZ0zP7Io3\",\n          \"scopeInstanceId\": \"jVzCxkeTMBCb0S3JUhDsQ\",\n          \"name\": \"assets\",\n          \"resourceId\": \"oIYuHoIwG7GM5J9cCSsai\"\n        }\n      ]\n    ],\n    \"resources\": [\n      [\n        \"1vX6SQdaCjJN6MvJlG_cQ\",\n        {\n          \"id\": \"1vX6SQdaCjJN6MvJlG_cQ\",\n          \"name\": \"list\",\n          \"method\": \"get\",\n          \"url\": \"\\\"https://gist.githubusercontent.com/TrySound/56507c301ec85669db5f1541406a9259/raw/a49548730ab592c86b9e7781f5b29beec4765494/collection.json\\\"\",\n          \"headers\": [],\n          \"body\": \"\"\n        }\n      ],\n      [\n        \"fjMzCru8O2U31xY2P1Ovr\",\n        {\n          \"id\": \"fjMzCru8O2U31xY2P1Ovr\",\n          \"name\": \"jsonResourceVariable\",\n          \"method\": \"get\",\n          \"url\": \"\\\"https://httpbin.org/get?hello=world\\\"\",\n          \"headers\": []\n        }\n      ],\n      [\n        \"oIYuHoIwG7GM5J9cCSsai\",\n        {\n          \"id\": \"oIYuHoIwG7GM5J9cCSsai\",\n          \"name\": \"assets\",\n          \"control\": \"system\",\n          \"method\": \"get\",\n          \"url\": \"\\\"/$resources/assets\\\"\",\n          \"searchParams\": [],\n          \"headers\": []\n        }\n      ]\n    ],\n    \"instances\": [\n      [\n        \"On9cvWCxr5rdZtY9O1Bv0\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"On9cvWCxr5rdZtY9O1Bv0\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"nVMWvMsaLCcb0o1wuNQgg\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"f0kF-WmL7DQg7MSyRvqY1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"nVMWvMsaLCcb0o1wuNQgg\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"nVMWvMsaLCcb0o1wuNQgg\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"DO NOT TOUCH THIS PROJECT, IT'S USED FOR FIXTURES\"\n            }\n          ]\n        }\n      ],\n      [\n        \"f0kF-WmL7DQg7MSyRvqY1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"f0kF-WmL7DQg7MSyRvqY1\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"5XDbqPrZDeCwq4YJ3CHsc\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"qPnkiFGDj8dITWb1kmpGl\"\n            }\n          ]\n        }\n      ],\n      [\n        \"5XDbqPrZDeCwq4YJ3CHsc\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"5XDbqPrZDeCwq4YJ3CHsc\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"oLXYe1UQiVMhVnZGvJSMr\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"p34JHWcU6UNrd9FVnY80Q\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"Ol5bklKKxyJaS7Q3jcCfD\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"D8wLZzLWQfxH9uaKsn-0L\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"8AXawjUE3fJoOH_1qOAoq\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"82HYqzxZeahPxSDFNWem5\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"9I4GRU1sev48hREkQcKQ-\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"81ejLVXyFEV1SxiJrWhyw\"\n            }\n          ]\n        }\n      ],\n      [\n        \"qPnkiFGDj8dITWb1kmpGl\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"qPnkiFGDj8dITWb1kmpGl\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"pX1ovPI7NdC0HRjkw6Kpw\"\n            }\n          ]\n        }\n      ],\n      [\n        \"oLXYe1UQiVMhVnZGvJSMr\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"oLXYe1UQiVMhVnZGvJSMr\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Heading\"\n            }\n          ]\n        }\n      ],\n      [\n        \"p34JHWcU6UNrd9FVnY80Q\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"p34JHWcU6UNrd9FVnY80Q\",\n          \"component\": \"Paragraph\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"a little kitten painted in black and white gouache with a thick brush\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pX1ovPI7NdC0HRjkw6Kpw\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pX1ovPI7NdC0HRjkw6Kpw\",\n          \"component\": \"Image\",\n          \"children\": []\n        }\n      ],\n      [\n        \"uKWGyE9JY3cPwY-xI9vk6\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uKWGyE9JY3cPwY-xI9vk6\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"AM9fD6dv2Ftc3Xjcsd7Uc\"\n            }\n          ]\n        }\n      ],\n      [\n        \"AM9fD6dv2Ftc3Xjcsd7Uc\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"AM9fD6dv2Ftc3Xjcsd7Uc\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:Accordion\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"zJ927zk9txwUbYycKB7QA\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"C838wkvIcA1BQu30Xu2G8\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"65djoTmSBGemZ2L5izQ5M\"\n            }\n          ]\n        }\n      ],\n      [\n        \"zJ927zk9txwUbYycKB7QA\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"zJ927zk9txwUbYycKB7QA\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionItem\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"sMxg7xT1hwYt05hbOvoPL\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"IUftdfjK-ilSzfOTdIx1u\"\n            }\n          ]\n        }\n      ],\n      [\n        \"sMxg7xT1hwYt05hbOvoPL\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"sMxg7xT1hwYt05hbOvoPL\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionHeader\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"qQSA4NoyKC88O68mBiQe2\"\n            }\n          ]\n        }\n      ],\n      [\n        \"qQSA4NoyKC88O68mBiQe2\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"qQSA4NoyKC88O68mBiQe2\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionTrigger\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"q-DVI4YTNrQ1LizmEyJHI\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"RSk81lLj2IGXgchTuXF7V\"\n            }\n          ]\n        }\n      ],\n      [\n        \"q-DVI4YTNrQ1LizmEyJHI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"q-DVI4YTNrQ1LizmEyJHI\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Is it accessible?\"\n            }\n          ]\n        }\n      ],\n      [\n        \"RSk81lLj2IGXgchTuXF7V\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"RSk81lLj2IGXgchTuXF7V\",\n          \"component\": \"Box\",\n          \"label\": \"Icon Container\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"d0sd_G-kHirxgjq6s6Uq1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"d0sd_G-kHirxgjq6s6Uq1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"d0sd_G-kHirxgjq6s6Uq1\",\n          \"component\": \"HtmlEmbed\",\n          \"label\": \"Chevron Icon\",\n          \"children\": []\n        }\n      ],\n      [\n        \"IUftdfjK-ilSzfOTdIx1u\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"IUftdfjK-ilSzfOTdIx1u\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionContent\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Yes. It adheres to the WAI-ARIA design pattern.\"\n            }\n          ]\n        }\n      ],\n      [\n        \"C838wkvIcA1BQu30Xu2G8\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"C838wkvIcA1BQu30Xu2G8\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionItem\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"fYUOB_brm6s0Ky68lzMfU\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"wNRVuu0L5E8TVufKdswp1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"fYUOB_brm6s0Ky68lzMfU\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"fYUOB_brm6s0Ky68lzMfU\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionHeader\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"dfd4gonev_AX6BpuCsxjb\"\n            }\n          ]\n        }\n      ],\n      [\n        \"dfd4gonev_AX6BpuCsxjb\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"dfd4gonev_AX6BpuCsxjb\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionTrigger\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"lZ7sI6Kw_0VZkURriKscB\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"wRw75kuvFzl5NWD8IGJoI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"lZ7sI6Kw_0VZkURriKscB\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"lZ7sI6Kw_0VZkURriKscB\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Is it styled?\"\n            }\n          ]\n        }\n      ],\n      [\n        \"wRw75kuvFzl5NWD8IGJoI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wRw75kuvFzl5NWD8IGJoI\",\n          \"component\": \"Box\",\n          \"label\": \"Icon Container\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"StPslEr81nfBISqBE2R-Y\"\n            }\n          ]\n        }\n      ],\n      [\n        \"StPslEr81nfBISqBE2R-Y\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"StPslEr81nfBISqBE2R-Y\",\n          \"component\": \"HtmlEmbed\",\n          \"label\": \"Chevron Icon\",\n          \"children\": []\n        }\n      ],\n      [\n        \"wNRVuu0L5E8TVufKdswp1\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wNRVuu0L5E8TVufKdswp1\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionContent\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Yes. It comes with default styles that matches the other components' aesthetic.\"\n            }\n          ]\n        }\n      ],\n      [\n        \"65djoTmSBGemZ2L5izQ5M\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"65djoTmSBGemZ2L5izQ5M\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionItem\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"UJYfe6kH7HqhH0YYeJwe7\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"mOVPnIrlt6IwVAzI_i2Fc\"\n            }\n          ]\n        }\n      ],\n      [\n        \"UJYfe6kH7HqhH0YYeJwe7\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"UJYfe6kH7HqhH0YYeJwe7\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionHeader\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"600nGddaNxGGdsuGgpxJR\"\n            }\n          ]\n        }\n      ],\n      [\n        \"600nGddaNxGGdsuGgpxJR\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"600nGddaNxGGdsuGgpxJR\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionTrigger\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"1iNKIMG91n83PzJnEdxq9\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"Ta70VqUb_fGJXBT_zsnxQ\"\n            }\n          ]\n        }\n      ],\n      [\n        \"1iNKIMG91n83PzJnEdxq9\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"1iNKIMG91n83PzJnEdxq9\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Is it animated?\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Ta70VqUb_fGJXBT_zsnxQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Ta70VqUb_fGJXBT_zsnxQ\",\n          \"component\": \"Box\",\n          \"label\": \"Icon Container\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"sO80m5u4f87jVGG91t6u8\"\n            }\n          ]\n        }\n      ],\n      [\n        \"sO80m5u4f87jVGG91t6u8\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"sO80m5u4f87jVGG91t6u8\",\n          \"component\": \"HtmlEmbed\",\n          \"label\": \"Chevron Icon\",\n          \"children\": []\n        }\n      ],\n      [\n        \"mOVPnIrlt6IwVAzI_i2Fc\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"mOVPnIrlt6IwVAzI_i2Fc\",\n          \"component\": \"@webstudio-is/sdk-components-react-radix:AccordionContent\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Yes. It's animated by default, but you can disable it if you prefer.\"\n            }\n          ]\n        }\n      ],\n      [\n        \"EDEfpMPRqDejthtwkH7ws\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"EDEfpMPRqDejthtwkH7ws\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"AdXSAYCx4QDo5QN6nLoGs\"\n            }\n          ]\n        }\n      ],\n      [\n        \"AdXSAYCx4QDo5QN6nLoGs\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"AdXSAYCx4QDo5QN6nLoGs\",\n          \"component\": \"Image\",\n          \"label\": \"webp image, used to test raw image uploads\",\n          \"children\": []\n        }\n      ],\n      [\n        \"9I4GRU1sev48hREkQcKQ-\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"9I4GRU1sev48hREkQcKQ-\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Symbols in path\"\n            }\n          ]\n        }\n      ],\n      [\n        \"82HYqzxZeahPxSDFNWem5\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"82HYqzxZeahPxSDFNWem5\",\n          \"component\": \"Box\",\n          \"children\": []\n        }\n      ],\n      [\n        \"a-4nDFkaWy4px1fn38XWJ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"a-4nDFkaWy4px1fn38XWJ\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"-1RvizaBcVpHsjvnYxn1c\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"isNSM3wXcnHFikwNPlEOL\"\n            }\n          ]\n        }\n      ],\n      [\n        \"isNSM3wXcnHFikwNPlEOL\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"isNSM3wXcnHFikwNPlEOL\",\n          \"component\": \"Form\",\n          \"label\": \"Form with custom props\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"a5YPRc19IJyhTrjjasA_R\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"Gw-ta0R4FNFAGBTVRWKep\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"ewk_WKpu4syHLPABMmvUz\"\n            }\n          ]\n        }\n      ],\n      [\n        \"a5YPRc19IJyhTrjjasA_R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"a5YPRc19IJyhTrjjasA_R\",\n          \"component\": \"Box\",\n          \"label\": \"Form Content\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"y4pceTmziuBRIDgUBQNLD\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"_gLjS0enBOV8KW9Ykz_es\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"ydR5B_9uMS4PXFS76TmBh\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"8RU1FyL2QRyqhNUKELGrb\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"TsqGP49hjgEW41ReCwrpZ\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"5GWjwVdapuGdn443GIKDW\"\n            }\n          ]\n        }\n      ],\n      [\n        \"_gLjS0enBOV8KW9Ykz_es\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"_gLjS0enBOV8KW9Ykz_es\",\n          \"component\": \"Label\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Name\"\n            }\n          ]\n        }\n      ],\n      [\n        \"ydR5B_9uMS4PXFS76TmBh\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ydR5B_9uMS4PXFS76TmBh\",\n          \"component\": \"Input\",\n          \"children\": []\n        }\n      ],\n      [\n        \"8RU1FyL2QRyqhNUKELGrb\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"8RU1FyL2QRyqhNUKELGrb\",\n          \"component\": \"Label\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Email\"\n            }\n          ]\n        }\n      ],\n      [\n        \"TsqGP49hjgEW41ReCwrpZ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"TsqGP49hjgEW41ReCwrpZ\",\n          \"component\": \"Input\",\n          \"children\": []\n        }\n      ],\n      [\n        \"5GWjwVdapuGdn443GIKDW\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"5GWjwVdapuGdn443GIKDW\",\n          \"component\": \"Button\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Submit\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Gw-ta0R4FNFAGBTVRWKep\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Gw-ta0R4FNFAGBTVRWKep\",\n          \"component\": \"Box\",\n          \"label\": \"Success Message\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Thank you for getting in touch!\"\n            }\n          ]\n        }\n      ],\n      [\n        \"ewk_WKpu4syHLPABMmvUz\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ewk_WKpu4syHLPABMmvUz\",\n          \"component\": \"Box\",\n          \"label\": \"Error Message\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Sorry, something went wrong.\"\n            }\n          ]\n        }\n      ],\n      [\n        \"-1RvizaBcVpHsjvnYxn1c\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"-1RvizaBcVpHsjvnYxn1c\",\n          \"component\": \"Form\",\n          \"label\": \"Default form\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"qhnVrmYGlyrMZi3UzqSQA\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"966cjxuqP_T99N27-mqWE\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"SYG5hhOz31xFJUN_v9zq6\"\n            }\n          ]\n        }\n      ],\n      [\n        \"qhnVrmYGlyrMZi3UzqSQA\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"qhnVrmYGlyrMZi3UzqSQA\",\n          \"component\": \"Box\",\n          \"label\": \"Form Content\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"YdHHf4u3jrdbRIWpB_VfH\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"A0RNI1WVwOGGDbwYnoZia\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"e035xi9fcwYtrn9La49Eh\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"LImtuVzw5R9yQsG4faiGV\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"dcHjdeW_HXPkyQlx3ZiL7\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"ZAtG6JgK4sbTnOAZlp2rU\"\n            }\n          ]\n        }\n      ],\n      [\n        \"A0RNI1WVwOGGDbwYnoZia\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"A0RNI1WVwOGGDbwYnoZia\",\n          \"component\": \"Label\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Name\"\n            }\n          ]\n        }\n      ],\n      [\n        \"e035xi9fcwYtrn9La49Eh\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"e035xi9fcwYtrn9La49Eh\",\n          \"component\": \"Input\",\n          \"children\": []\n        }\n      ],\n      [\n        \"LImtuVzw5R9yQsG4faiGV\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"LImtuVzw5R9yQsG4faiGV\",\n          \"component\": \"Label\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Email\"\n            }\n          ]\n        }\n      ],\n      [\n        \"dcHjdeW_HXPkyQlx3ZiL7\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"dcHjdeW_HXPkyQlx3ZiL7\",\n          \"component\": \"Input\",\n          \"children\": []\n        }\n      ],\n      [\n        \"ZAtG6JgK4sbTnOAZlp2rU\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ZAtG6JgK4sbTnOAZlp2rU\",\n          \"component\": \"Button\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Submit\"\n            }\n          ]\n        }\n      ],\n      [\n        \"966cjxuqP_T99N27-mqWE\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"966cjxuqP_T99N27-mqWE\",\n          \"component\": \"Box\",\n          \"label\": \"Success Message\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Thank you for getting in touch!\"\n            }\n          ]\n        }\n      ],\n      [\n        \"SYG5hhOz31xFJUN_v9zq6\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"SYG5hhOz31xFJUN_v9zq6\",\n          \"component\": \"Box\",\n          \"label\": \"Error Message\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Sorry, something went wrong.\"\n            }\n          ]\n        }\n      ],\n      [\n        \"y4pceTmziuBRIDgUBQNLD\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"y4pceTmziuBRIDgUBQNLD\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Form with custom action and method\"\n            }\n          ]\n        }\n      ],\n      [\n        \"YdHHf4u3jrdbRIWpB_VfH\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"YdHHf4u3jrdbRIWpB_VfH\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Default form\"\n            }\n          ]\n        }\n      ],\n      [\n        \"8AXawjUE3fJoOH_1qOAoq\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"8AXawjUE3fJoOH_1qOAoq\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"go download this little kitten\"\n            }\n          ]\n        }\n      ],\n      [\n        \"D8wLZzLWQfxH9uaKsn-0L\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"D8wLZzLWQfxH9uaKsn-0L\",\n          \"component\": \"Text\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \" or \"\n            }\n          ]\n        }\n      ],\n      [\n        \"O-ljaGZQ0iRNTlEshMkgE\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"O-ljaGZQ0iRNTlEshMkgE\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"qmxnOlSxUGpuhuonVArWJ\"\n            }\n          ]\n        }\n      ],\n      [\n        \"qmxnOlSxUGpuhuonVArWJ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"qmxnOlSxUGpuhuonVArWJ\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Heading you can edit\"\n            }\n          ]\n        }\n      ],\n      [\n        \"81ejLVXyFEV1SxiJrWhyw\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"81ejLVXyFEV1SxiJrWhyw\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Link to instance\"\n            }\n          ]\n        }\n      ],\n      [\n        \"AWY2qZfpbykoiWELeJhse\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"AWY2qZfpbykoiWELeJhse\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"WGSZGPAhyIuWhM1DJ7HbZ\"\n            }\n          ]\n        }\n      ],\n      [\n        \"WGSZGPAhyIuWhM1DJ7HbZ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"WGSZGPAhyIuWhM1DJ7HbZ\",\n          \"component\": \"ws:collection\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"-F-b3eIEZ8WKW_F-Aw8nN\"\n            }\n          ]\n        }\n      ],\n      [\n        \"-F-b3eIEZ8WKW_F-Aw8nN\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"-F-b3eIEZ8WKW_F-Aw8nN\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"05oK4Ks0ocFv3w8MJOcNR\"\n            }\n          ]\n        }\n      ],\n      [\n        \"05oK4Ks0ocFv3w8MJOcNR\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"05oK4Ks0ocFv3w8MJOcNR\",\n          \"component\": \"HtmlEmbed\",\n          \"children\": []\n        }\n      ],\n      [\n        \"L0ZXd5F9xk9Rsl9ORzIkJ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"L0ZXd5F9xk9Rsl9ORzIkJ\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"VFPjLwt6Caq4l9PPJSiyI\"\n            }\n          ]\n        }\n      ],\n      [\n        \"VFPjLwt6Caq4l9PPJSiyI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"VFPjLwt6Caq4l9PPJSiyI\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Nested page\"\n            }\n          ]\n        }\n      ],\n      [\n        \"MTUwXTcUJcWmr9iGvdcoH\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"MTUwXTcUJcWmr9iGvdcoH\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"DiiJ6c5jxLGydyTZTCdg0\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"sHn4Mh7DJ5X0RiETB-zVc\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"ABEvi07-c3_3dU6v4w-MS\"\n            }\n          ]\n        }\n      ],\n      [\n        \"DiiJ6c5jxLGydyTZTCdg0\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"DiiJ6c5jxLGydyTZTCdg0\",\n          \"component\": \"Heading\",\n          \"label\": \"worl\",\n          \"children\": [\n            {\n              \"type\": \"expression\",\n              \"value\": \"`${$ws$dataSource$LYqm_7uq8W4T7BKky9zcj.data.args}`\"\n            }\n          ]\n        }\n      ],\n      [\n        \"sHn4Mh7DJ5X0RiETB-zVc\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"sHn4Mh7DJ5X0RiETB-zVc\",\n          \"component\": \"HtmlEmbed\",\n          \"children\": []\n        }\n      ],\n      [\n        \"mdyCS8QnKx3fL1MLXifmy\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"mdyCS8QnKx3fL1MLXifmy\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"drCx7m8q_gnNQLrhPDA-g\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"T-rqx2b12pRrWcafRRoIG\"\n            }\n          ]\n        }\n      ],\n      [\n        \"drCx7m8q_gnNQLrhPDA-g\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"drCx7m8q_gnNQLrhPDA-g\",\n          \"component\": \"Box\",\n          \"children\": []\n        }\n      ],\n      [\n        \"T-rqx2b12pRrWcafRRoIG\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"T-rqx2b12pRrWcafRRoIG\",\n          \"component\": \"Box\",\n          \"children\": []\n        }\n      ],\n      [\n        \"z4Gp8noJpB4NS3JmdXh1Y\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"z4Gp8noJpB4NS3JmdXh1Y\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"SVI6fI342JAxCvwsg4Oc6\"\n            }\n          ]\n        }\n      ],\n      [\n        \"SVI6fI342JAxCvwsg4Oc6\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"SVI6fI342JAxCvwsg4Oc6\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"zJkrskDJhaEk-nOSRWrGg\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"LYBzzBvHLrfSOmPLs52dP\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"8rjJjZTvM-WXjU8dVktBZ\"\n            }\n          ]\n        }\n      ],\n      [\n        \"zJkrskDJhaEk-nOSRWrGg\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"zJkrskDJhaEk-nOSRWrGg\",\n          \"component\": \"ws:collection\",\n          \"label\": \"urls\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"USgd2tJcA7pt-9xq4H4XP\"\n            }\n          ]\n        }\n      ],\n      [\n        \"USgd2tJcA7pt-9xq4H4XP\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"USgd2tJcA7pt-9xq4H4XP\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"vw4jVDxlyJhw77jdG3lpd\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"zgwCRmVObvwI6a7zmD9OD\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"yDQOMG0BUdvM5g5hDV7JP\"\n            }\n          ]\n        }\n      ],\n      [\n        \"vw4jVDxlyJhw77jdG3lpd\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"vw4jVDxlyJhw77jdG3lpd\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"expression\",\n              \"value\": \"`${$ws$dataSource$NB_R8mrrGxFXCCsAtL8QK.origin ?? '${ORIGIN}'}${$ws$dataSource$2pjBK__DASH__2keQ15QSbnjLOk1.path}`\"\n            }\n          ]\n        }\n      ],\n      [\n        \"zgwCRmVObvwI6a7zmD9OD\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"zgwCRmVObvwI6a7zmD9OD\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"expression\",\n              \"value\": \"$ws$dataSource$2pjBK__DASH__2keQ15QSbnjLOk1.lastModified\"\n            }\n          ]\n        }\n      ],\n      [\n        \"LYBzzBvHLrfSOmPLs52dP\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"LYBzzBvHLrfSOmPLs52dP\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Below is custom section\"\n            }\n          ]\n        }\n      ],\n      [\n        \"8rjJjZTvM-WXjU8dVktBZ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"8rjJjZTvM-WXjU8dVktBZ\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"sDGB6PDQ9YABCdk9DKBmz\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"7Xcdw3QXZnp_Ehel6A6bI\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"bBAE2vpcCLzlcin29wQYA\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"wLYkMGH-OJP4SnFB4F-bs\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"WCz4oAOyt2V-prz3LIAes\"\n            }\n          ]\n        }\n      ],\n      [\n        \"sDGB6PDQ9YABCdk9DKBmz\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"sDGB6PDQ9YABCdk9DKBmz\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"custom-hand-made-location\"\n            }\n          ]\n        }\n      ],\n      [\n        \"7Xcdw3QXZnp_Ehel6A6bI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"7Xcdw3QXZnp_Ehel6A6bI\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"NBRETFGlP8H6t_1ZLbJ5J\"\n            }\n          ]\n        }\n      ],\n      [\n        \"bBAE2vpcCLzlcin29wQYA\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"bBAE2vpcCLzlcin29wQYA\",\n          \"component\": \"XmlNode\",\n          \"children\": []\n        }\n      ],\n      [\n        \"yDQOMG0BUdvM5g5hDV7JP\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"yDQOMG0BUdvM5g5hDV7JP\",\n          \"component\": \"XmlNode\",\n          \"children\": []\n        }\n      ],\n      [\n        \"wLYkMGH-OJP4SnFB4F-bs\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"wLYkMGH-OJP4SnFB4F-bs\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Hello\"\n            }\n          ]\n        }\n      ],\n      [\n        \"-BW4QOi3PJTZ1sDCY8LW6\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"-BW4QOi3PJTZ1sDCY8LW6\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"Dwv02N8aJoxXacL58cmf-\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"TNyJ2G7r6h267foLukveQ\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"VJAhRG8CDslSQStNf0C_A\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"RHGykFKK2R4PyjNtIuSEk\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Dwv02N8aJoxXacL58cmf-\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Dwv02N8aJoxXacL58cmf-\",\n          \"component\": \"Box\",\n          \"label\": \"With Templates And Content\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"3rtiNx74sdhz1rNTQBmRA\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"mNIGsDuFN_jhG8g1socRY\"\n            }\n          ]\n        }\n      ],\n      [\n        \"3rtiNx74sdhz1rNTQBmRA\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"3rtiNx74sdhz1rNTQBmRA\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Content Block With Templates And Content\"\n            }\n          ]\n        }\n      ],\n      [\n        \"mNIGsDuFN_jhG8g1socRY\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"mNIGsDuFN_jhG8g1socRY\",\n          \"component\": \"ws:block\",\n          \"label\": \"With Templates And Content\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"B1Gn2WHnTDrPgYCvarFx4\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"GAKL3iFwdWc85Yp9vsGBI\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"KIJoY0O_HgjVtc8cskt5U\"\n            }\n          ]\n        }\n      ],\n      [\n        \"B1Gn2WHnTDrPgYCvarFx4\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"B1Gn2WHnTDrPgYCvarFx4\",\n          \"component\": \"ws:block-template\",\n          \"label\": \"Templates\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"4KmEPyxWtLOVyK7EqhJyT\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"nCJ6DV5oGv4DOdzQvlav8\"\n            }\n          ]\n        }\n      ],\n      [\n        \"4KmEPyxWtLOVyK7EqhJyT\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"4KmEPyxWtLOVyK7EqhJyT\",\n          \"component\": \"Heading\",\n          \"label\": \"T-Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Heading text you can edit\",\n              \"placeholder\": true\n            }\n          ]\n        }\n      ],\n      [\n        \"nCJ6DV5oGv4DOdzQvlav8\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"nCJ6DV5oGv4DOdzQvlav8\",\n          \"component\": \"Paragraph\",\n          \"label\": \"T-Paragraph\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Paragraph text you can edit\",\n              \"placeholder\": true\n            }\n          ]\n        }\n      ],\n      [\n        \"GAKL3iFwdWc85Yp9vsGBI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"GAKL3iFwdWc85Yp9vsGBI\",\n          \"component\": \"Heading\",\n          \"label\": \"T-Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"H1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"KIJoY0O_HgjVtc8cskt5U\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"KIJoY0O_HgjVtc8cskt5U\",\n          \"component\": \"Paragraph\",\n          \"label\": \"T-Paragraph\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Paragraph\"\n            }\n          ]\n        }\n      ],\n      [\n        \"TNyJ2G7r6h267foLukveQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"TNyJ2G7r6h267foLukveQ\",\n          \"component\": \"Box\",\n          \"label\": \"With Templates Only\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"YCTo59XfqTGNPH7ow1rvU\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"_HZlreqcBl-3vSqpdwIZj\"\n            }\n          ]\n        }\n      ],\n      [\n        \"YCTo59XfqTGNPH7ow1rvU\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"YCTo59XfqTGNPH7ow1rvU\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Content Block With Templates Only\"\n            }\n          ]\n        }\n      ],\n      [\n        \"_HZlreqcBl-3vSqpdwIZj\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"_HZlreqcBl-3vSqpdwIZj\",\n          \"component\": \"ws:block\",\n          \"label\": \"With Templates Only\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"uWkdb-95WncG3WVqWe1gl\"\n            }\n          ]\n        }\n      ],\n      [\n        \"uWkdb-95WncG3WVqWe1gl\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"uWkdb-95WncG3WVqWe1gl\",\n          \"component\": \"ws:block-template\",\n          \"label\": \"Templates\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"XnNmz3xvg5uC5ZHfSMbKQ\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"u4sJ-bYw1KAAtN2XybZFD\"\n            }\n          ]\n        }\n      ],\n      [\n        \"XnNmz3xvg5uC5ZHfSMbKQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"XnNmz3xvg5uC5ZHfSMbKQ\",\n          \"component\": \"Heading\",\n          \"label\": \"T-Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Heading text you can edit\",\n              \"placeholder\": true\n            }\n          ]\n        }\n      ],\n      [\n        \"u4sJ-bYw1KAAtN2XybZFD\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"u4sJ-bYw1KAAtN2XybZFD\",\n          \"component\": \"Paragraph\",\n          \"label\": \"T-Paragraph\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Paragraph text you can edit\",\n              \"placeholder\": true\n            }\n          ]\n        }\n      ],\n      [\n        \"VJAhRG8CDslSQStNf0C_A\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"VJAhRG8CDslSQStNf0C_A\",\n          \"component\": \"Box\",\n          \"label\": \"With Content Only\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"67Te7ahXi4mbqb0CMC6Xh\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"Wtt1RQs-cGQVxwGoCBTrQ\"\n            }\n          ]\n        }\n      ],\n      [\n        \"67Te7ahXi4mbqb0CMC6Xh\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"67Te7ahXi4mbqb0CMC6Xh\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"With Content Only\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Wtt1RQs-cGQVxwGoCBTrQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Wtt1RQs-cGQVxwGoCBTrQ\",\n          \"component\": \"ws:block\",\n          \"label\": \"With Content Only\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"pKWGbMqgT95dnExR0JI93\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"5dI7VvESR58C2ZXcszIRr\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"lI28xjfL4rGE5Wn1kcatW\"\n            }\n          ]\n        }\n      ],\n      [\n        \"pKWGbMqgT95dnExR0JI93\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"pKWGbMqgT95dnExR0JI93\",\n          \"component\": \"ws:block-template\",\n          \"label\": \"Templates\",\n          \"children\": []\n        }\n      ],\n      [\n        \"5dI7VvESR58C2ZXcszIRr\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"5dI7VvESR58C2ZXcszIRr\",\n          \"component\": \"Heading\",\n          \"label\": \"T-Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"H1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"lI28xjfL4rGE5Wn1kcatW\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"lI28xjfL4rGE5Wn1kcatW\",\n          \"component\": \"Paragraph\",\n          \"label\": \"T-Paragraph\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Paragraph\"\n            }\n          ]\n        }\n      ],\n      [\n        \"RHGykFKK2R4PyjNtIuSEk\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"RHGykFKK2R4PyjNtIuSEk\",\n          \"component\": \"Box\",\n          \"label\": \"Empty\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"ccyTkEM40ar6Z2UTXhFvY\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"qwlUNFg1FL8rTLvu614Fe\"\n            }\n          ]\n        }\n      ],\n      [\n        \"ccyTkEM40ar6Z2UTXhFvY\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ccyTkEM40ar6Z2UTXhFvY\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Empty\"\n            }\n          ]\n        }\n      ],\n      [\n        \"qwlUNFg1FL8rTLvu614Fe\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"qwlUNFg1FL8rTLvu614Fe\",\n          \"component\": \"ws:block\",\n          \"label\": \"With Content Only\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"ciTnbTkbURP61HSjy8TX3\"\n            }\n          ]\n        }\n      ],\n      [\n        \"ciTnbTkbURP61HSjy8TX3\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ciTnbTkbURP61HSjy8TX3\",\n          \"component\": \"ws:block-template\",\n          \"label\": \"Templates\",\n          \"children\": []\n        }\n      ],\n      [\n        \"NBRETFGlP8H6t_1ZLbJ5J\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"NBRETFGlP8H6t_1ZLbJ5J\",\n          \"component\": \"XmlTime\",\n          \"children\": []\n        }\n      ],\n      [\n        \"WCz4oAOyt2V-prz3LIAes\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"WCz4oAOyt2V-prz3LIAes\",\n          \"component\": \"XmlNode\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"https://webstudio.is\"\n            }\n          ]\n        }\n      ],\n      [\n        \"1NipO5NmaHukA_dxzfVRF\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"1NipO5NmaHukA_dxzfVRF\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"Jdxu-0HI2f6Lq-LJwIOyY\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"McxYFTe8LB_iEqEL6DHFc\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"69Q0-F165KT33psqFcq6M\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Jdxu-0HI2f6Lq-LJwIOyY\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Jdxu-0HI2f6Lq-LJwIOyY\",\n          \"component\": \"HeadSlot\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"4DTeDG3l8nX_C_oNtaHZQ\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"GFN6suio87WybEb7dc8IM\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"Py53B3dAV0BD8QAHaWV-K\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"EveN4Skg9xb8Lj1QJcW52\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"56x98yMSVYpp7eVo8wuzK\"\n            }\n          ]\n        }\n      ],\n      [\n        \"GFN6suio87WybEb7dc8IM\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"GFN6suio87WybEb7dc8IM\",\n          \"component\": \"HeadLink\",\n          \"label\": \"Link\",\n          \"children\": []\n        }\n      ],\n      [\n        \"Py53B3dAV0BD8QAHaWV-K\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Py53B3dAV0BD8QAHaWV-K\",\n          \"component\": \"HeadMeta\",\n          \"label\": \"Meta\",\n          \"children\": []\n        }\n      ],\n      [\n        \"McxYFTe8LB_iEqEL6DHFc\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"McxYFTe8LB_iEqEL6DHFc\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Test Head Slot\"\n            }\n          ]\n        }\n      ],\n      [\n        \"69Q0-F165KT33psqFcq6M\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"69Q0-F165KT33psqFcq6M\",\n          \"component\": \"Link\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Go Home\"\n            }\n          ]\n        }\n      ],\n      [\n        \"EveN4Skg9xb8Lj1QJcW52\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"EveN4Skg9xb8Lj1QJcW52\",\n          \"component\": \"HeadMeta\",\n          \"label\": \"Meta\",\n          \"children\": []\n        }\n      ],\n      [\n        \"4DTeDG3l8nX_C_oNtaHZQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"4DTeDG3l8nX_C_oNtaHZQ\",\n          \"component\": \"HeadTitle\",\n          \"label\": \"Title\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Head Slot Title\"\n            }\n          ]\n        }\n      ],\n      [\n        \"56x98yMSVYpp7eVo8wuzK\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"56x98yMSVYpp7eVo8wuzK\",\n          \"component\": \"HeadLink\",\n          \"label\": \"Link\",\n          \"children\": []\n        }\n      ],\n      [\n        \"ABEvi07-c3_3dU6v4w-MS\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ABEvi07-c3_3dU6v4w-MS\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"expression\",\n              \"value\": \"$ws$dataSource$rKNaaGjEYnq4XPiTrSlfe\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Qrpsu4yKLKARA-82TnhLI\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Qrpsu4yKLKARA-82TnhLI\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"1e-3lR1XsDg2IsPu4mSNr\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"3hPo1LW5P_4VAyXUmVtpY\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"81TW-o_0QLHlfmBAKFyCQ\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"KCX8plaeWZXn98-4qmHBW\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"YrVDoWK0I_9hpQs5yJzLt\"\n            }\n          ]\n        }\n      ],\n      [\n        \"1e-3lR1XsDg2IsPu4mSNr\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"1e-3lR1XsDg2IsPu4mSNr\",\n          \"component\": \"Box\",\n          \"label\": \"Intro\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"4wwrTenFWHa_8V-EfwjgO\"\n            }\n          ]\n        }\n      ],\n      [\n        \"4wwrTenFWHa_8V-EfwjgO\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"4wwrTenFWHa_8V-EfwjgO\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"ANIMATIONS\"\n            }\n          ]\n        }\n      ],\n      [\n        \"YrVDoWK0I_9hpQs5yJzLt\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"YrVDoWK0I_9hpQs5yJzLt\",\n          \"component\": \"Box\",\n          \"label\": \"Footer\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"5_baZPv9KywpYUey_eAcH\"\n            }\n          ]\n        }\n      ],\n      [\n        \"5_baZPv9KywpYUey_eAcH\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"5_baZPv9KywpYUey_eAcH\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"THE END\"\n            }\n          ]\n        }\n      ],\n      [\n        \"3hPo1LW5P_4VAyXUmVtpY\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"3hPo1LW5P_4VAyXUmVtpY\",\n          \"component\": \"Box\",\n          \"label\": \"Grid\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"nhg6K_WDmDwhfMi9qkUzE\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"Vylc3nJKmfZAYwc_rYx6j\"\n            }\n          ]\n        }\n      ],\n      [\n        \"nhg6K_WDmDwhfMi9qkUzE\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"nhg6K_WDmDwhfMi9qkUzE\",\n          \"component\": \"Box\",\n          \"label\": \"AnimCol\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"D6OU4dJ2tzc_Uq5LqMVWo\"\n            }\n          ]\n        }\n      ],\n      [\n        \"D6OU4dJ2tzc_Uq5LqMVWo\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"D6OU4dJ2tzc_Uq5LqMVWo\",\n          \"component\": \"@webstudio-is/sdk-components-animation:AnimateChildren\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"cvCjIfx9TJ3XeeSpCnW1w\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"815GnawXGdYDbF2lvJ4p-\"\n            }\n          ]\n        }\n      ],\n      [\n        \"cvCjIfx9TJ3XeeSpCnW1w\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"cvCjIfx9TJ3XeeSpCnW1w\",\n          \"component\": \"Heading\",\n          \"label\": \"Animated H0\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"ANIMATED CHILD 0\"\n            }\n          ]\n        }\n      ],\n      [\n        \"815GnawXGdYDbF2lvJ4p-\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"815GnawXGdYDbF2lvJ4p-\",\n          \"component\": \"Heading\",\n          \"label\": \"Animated H0\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"ANIMATED CHILD 1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Vylc3nJKmfZAYwc_rYx6j\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Vylc3nJKmfZAYwc_rYx6j\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. \"\n            }\n          ]\n        }\n      ],\n      [\n        \"KCX8plaeWZXn98-4qmHBW\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"KCX8plaeWZXn98-4qmHBW\",\n          \"component\": \"Box\",\n          \"label\": \"Grid\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"tYYYhZkINGVL853ry_940\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"EHq0nc3KxVSs35jB5udcn\"\n            }\n          ]\n        }\n      ],\n      [\n        \"tYYYhZkINGVL853ry_940\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"tYYYhZkINGVL853ry_940\",\n          \"component\": \"Box\",\n          \"label\": \"AnimCol\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"ed6Q3Y2M1MXZf7E8rMwwW\"\n            }\n          ]\n        }\n      ],\n      [\n        \"ed6Q3Y2M1MXZf7E8rMwwW\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"ed6Q3Y2M1MXZf7E8rMwwW\",\n          \"component\": \"@webstudio-is/sdk-components-animation:AnimateChildren\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"GRj9UMUzk-HAWRctEcfFH\"\n            }\n          ]\n        }\n      ],\n      [\n        \"EHq0nc3KxVSs35jB5udcn\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"EHq0nc3KxVSs35jB5udcn\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. \"\n            }\n          ]\n        }\n      ],\n      [\n        \"GRj9UMUzk-HAWRctEcfFH\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"GRj9UMUzk-HAWRctEcfFH\",\n          \"component\": \"@webstudio-is/sdk-components-animation:AnimateText\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"hi1l-ro0pCVQpHFIwIhFO\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"oRr8wiz8x-p2kEhV0zBnt\"\n            }\n          ]\n        }\n      ],\n      [\n        \"hi1l-ro0pCVQpHFIwIhFO\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"hi1l-ro0pCVQpHFIwIhFO\",\n          \"component\": \"Heading\",\n          \"label\": \"Animated H0\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"ANIMATED CHILD 1\"\n            }\n          ]\n        }\n      ],\n      [\n        \"oRr8wiz8x-p2kEhV0zBnt\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"oRr8wiz8x-p2kEhV0zBnt\",\n          \"component\": \"Box\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. \"\n            }\n          ]\n        }\n      ],\n      [\n        \"81TW-o_0QLHlfmBAKFyCQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"81TW-o_0QLHlfmBAKFyCQ\",\n          \"component\": \"Box\",\n          \"label\": \"Spacer\",\n          \"children\": []\n        }\n      ],\n      [\n        \"Lmn0VIRtr_Yn9AvDyjgZG\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Lmn0VIRtr_Yn9AvDyjgZG\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"Iz65PDmd9YjqrIy5fGtWt\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Iz65PDmd9YjqrIy5fGtWt\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Iz65PDmd9YjqrIy5fGtWt\",\n          \"component\": \"@webstudio-is/sdk-components-animation:AnimateChildren\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"tOdjbQbLEf52thwmubL7R\"\n            }\n          ]\n        }\n      ],\n      [\n        \"tOdjbQbLEf52thwmubL7R\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"tOdjbQbLEf52thwmubL7R\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"HELLO WORLD\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"\\n\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"GOOD\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"\\n\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"BAD\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"\\n\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"UGLY\"\n            }\n          ]\n        }\n      ],\n      [\n        \"LTaU1Znm6P4yt3S8kAZi-\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"LTaU1Znm6P4yt3S8kAZi-\",\n          \"component\": \"Body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"Z4ogkQT54jFVlCIFkXKmb\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Z4ogkQT54jFVlCIFkXKmb\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Z4ogkQT54jFVlCIFkXKmb\",\n          \"component\": \"@webstudio-is/sdk-components-animation:AnimateChildren\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"aqJaUhJ3xDmbIDws8d1TS\"\n            }\n          ]\n        }\n      ],\n      [\n        \"aqJaUhJ3xDmbIDws8d1TS\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"aqJaUhJ3xDmbIDws8d1TS\",\n          \"component\": \"@webstudio-is/sdk-components-animation:AnimateText\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"g__o13UKOkD0KImnp-sWp\"\n            }\n          ]\n        }\n      ],\n      [\n        \"g__o13UKOkD0KImnp-sWp\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"g__o13UKOkD0KImnp-sWp\",\n          \"component\": \"Heading\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"HELLO WORLD\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"\\n\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"GOOD\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"\\n\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"BAD\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"\\n\"\n            },\n            {\n              \"type\": \"text\",\n              \"value\": \"UGLY\"\n            }\n          ]\n        }\n      ],\n      [\n        \"Ol5bklKKxyJaS7Q3jcCfD\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"Ol5bklKKxyJaS7Q3jcCfD\",\n          \"component\": \"ws:element\",\n          \"tag\": \"a\",\n          \"children\": [\n            {\n              \"type\": \"text\",\n              \"value\": \"Click here to adore more kittens\"\n            }\n          ]\n        }\n      ],\n      [\n        \"jVzCxkeTMBCb0S3JUhDsQ\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"jVzCxkeTMBCb0S3JUhDsQ\",\n          \"component\": \"ws:element\",\n          \"tag\": \"body\",\n          \"children\": [\n            {\n              \"type\": \"id\",\n              \"value\": \"a94S1sqeP8lT7aeVPBv6c\"\n            },\n            {\n              \"type\": \"id\",\n              \"value\": \"aAVR8Rg52_1qq8NV5rKKe\"\n            }\n          ]\n        }\n      ],\n      [\n        \"a94S1sqeP8lT7aeVPBv6c\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"a94S1sqeP8lT7aeVPBv6c\",\n          \"component\": \"ws:element\",\n          \"tag\": \"audio\",\n          \"children\": []\n        }\n      ],\n      [\n        \"aAVR8Rg52_1qq8NV5rKKe\",\n        {\n          \"type\": \"instance\",\n          \"id\": \"aAVR8Rg52_1qq8NV5rKKe\",\n          \"component\": \"ws:element\",\n          \"tag\": \"video\",\n          \"children\": []\n        }\n      ]\n    ],\n    \"deployment\": {\n      \"destination\": \"saas\",\n      \"domains\": [\"webstudio-fixture-project-a-0su3o\"],\n      \"assetsDomain\": \"webstudio-fixture-project-a-0su3o\",\n      \"excludeWstdDomainFromSearch\": false\n    }\n  },\n  \"page\": {\n    \"id\": \"7Db64ZXgYiRqKSQNR-qTQ\",\n    \"name\": \"Home\",\n    \"title\": \"\\\"The Ultimate Cat Protection Zone\\\"\",\n    \"rootInstanceId\": \"On9cvWCxr5rdZtY9O1Bv0\",\n    \"systemDataSourceId\": \"u9wtUkt2-E0RDaljl_I6E\",\n    \"meta\": {\n      \"description\": \"\\\"Dive into the world of felines and discover why some whiskers are best left untouched. From intriguing cat behaviors to protective measures, \\\\nKittyGuardedZone is your go-to hub for all things 'hands-off' in the cat realm.\\\"\",\n      \"socialImageAssetId\": \"cd939c56-bcdd-4e64-bd9c-567a9bccd3da\",\n      \"custom\": [\n        {\n          \"property\": \"fb:app_id\",\n          \"content\": \"\\\"app_id_app_id_app_id\\\"\"\n        }\n      ]\n    },\n    \"path\": \"\"\n  },\n  \"pages\": [\n    {\n      \"id\": \"7Db64ZXgYiRqKSQNR-qTQ\",\n      \"name\": \"Home\",\n      \"title\": \"\\\"The Ultimate Cat Protection Zone\\\"\",\n      \"rootInstanceId\": \"On9cvWCxr5rdZtY9O1Bv0\",\n      \"systemDataSourceId\": \"u9wtUkt2-E0RDaljl_I6E\",\n      \"meta\": {\n        \"description\": \"\\\"Dive into the world of felines and discover why some whiskers are best left untouched. From intriguing cat behaviors to protective measures, \\\\nKittyGuardedZone is your go-to hub for all things 'hands-off' in the cat realm.\\\"\",\n        \"socialImageAssetId\": \"cd939c56-bcdd-4e64-bd9c-567a9bccd3da\",\n        \"custom\": [\n          {\n            \"property\": \"fb:app_id\",\n            \"content\": \"\\\"app_id_app_id_app_id\\\"\"\n          }\n        ]\n      },\n      \"path\": \"\"\n    },\n    {\n      \"id\": \"xfvB4UThQXmQ_OubPYrkg\",\n      \"name\": \"radix excluded from the search\",\n      \"title\": \"\\\"Radix Revelations: Unraveling the Feline Mystique\\\"\",\n      \"rootInstanceId\": \"uKWGyE9JY3cPwY-xI9vk6\",\n      \"systemDataSourceId\": \"GqDH_PSDF_8QXqPW2A0un\",\n      \"meta\": {\n        \"description\": \"\\\"Delve deep into the radix roots of feline behaviors. At KittyNoTouchy, we dissect the core essence, or 'radix', of what makes cats the enigmatic creatures they are. Join us as we explore the radix of their instincts, habits, and quirks.\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"socialImageAssetId\": \"88d5e2ff-b8f2-4899-aaf8-dde4ade6da10\"\n      },\n      \"path\": \"/radix\"\n    },\n    {\n      \"id\": \"szYLvBduHPmbtqQKCDY0b\",\n      \"name\": \"RouteWithSymbols\",\n      \"title\": \"\\\"RouteWithSymbols\\\"\",\n      \"history\": [\"/_route_with_symbols_\"],\n      \"rootInstanceId\": \"EDEfpMPRqDejthtwkH7ws\",\n      \"systemDataSourceId\": \"UiZtSC5BK8Ub-Ow7-XOdW\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\"\n      },\n      \"path\": \"/_route_with_symbols_\"\n    },\n    {\n      \"id\": \"U1tRJl2ERr8_OFe0g9cN_\",\n      \"name\": \"form\",\n      \"title\": \"\\\"form\\\"\",\n      \"rootInstanceId\": \"a-4nDFkaWy4px1fn38XWJ\",\n      \"systemDataSourceId\": \"-CX9SNsPSue42RkfxjL8_\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\"\n      },\n      \"path\": \"/form\"\n    },\n    {\n      \"id\": \"-J9I4Oo6mONfQlf_3-OqG\",\n      \"name\": \"heading-with-id\",\n      \"title\": \"\\\"heading-with-id\\\"\",\n      \"rootInstanceId\": \"O-ljaGZQ0iRNTlEshMkgE\",\n      \"systemDataSourceId\": \"Q6XrK4-ZZUdXsocdiWqVk\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\"\n      },\n      \"path\": \"/heading-with-id\"\n    },\n    {\n      \"id\": \"TS39WeBd0Qr3QgbSlzdf2\",\n      \"name\": \"resources\",\n      \"title\": \"\\\"resources\\\"\",\n      \"rootInstanceId\": \"AWY2qZfpbykoiWELeJhse\",\n      \"systemDataSourceId\": \"keP0A66msOpEL6HbrC9OJ\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"false\",\n        \"socialImageAssetId\": \"\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"path\": \"/resources\"\n    },\n    {\n      \"id\": \"Pnz-BxUm6XmvFygk_XdEy\",\n      \"name\": \"Nested Page\",\n      \"title\": \"\\\"Nested Page\\\"\",\n      \"rootInstanceId\": \"L0ZXd5F9xk9Rsl9ORzIkJ\",\n      \"systemDataSourceId\": \"8qqAbq6TEA3ccoCagjlZL\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"false\",\n        \"socialImageAssetId\": \"\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"path\": \"/nested-page\"\n    },\n    {\n      \"id\": \"9xlwLSHxuk8HmS3-EEGcf\",\n      \"name\": \"expressions\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"MTUwXTcUJcWmr9iGvdcoH\",\n      \"systemDataSourceId\": \"8tTIagkgzmdy4T4qzKXqd\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": []\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/expressions\"\n    },\n    {\n      \"id\": \"lTS5DKrDEC_mXSAc5ZDDA\",\n      \"name\": \"class-names\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"mdyCS8QnKx3fL1MLXifmy\",\n      \"systemDataSourceId\": \"5o_tom3ekCqphQfInC6NH\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": []\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/class-names\"\n    },\n    {\n      \"id\": \"FsnS9ui6btzM4W3YELE3Q\",\n      \"name\": \"sitemap.xml\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"z4Gp8noJpB4NS3JmdXh1Y\",\n      \"systemDataSourceId\": \"NB_R8mrrGxFXCCsAtL8QK\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"xml\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/sitemap.xml\"\n    },\n    {\n      \"id\": \"Q1D-6G1cl0SfXyM9Xj4_O\",\n      \"name\": \"content-block\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"-BW4QOi3PJTZ1sDCY8LW6\",\n      \"systemDataSourceId\": \"F3zbkztYW_mJNC5EOkopM\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": []\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/content-block\"\n    },\n    {\n      \"id\": \"8fF_9MQOwOqsLM9BhULUG\",\n      \"name\": \"head-tag\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"1NipO5NmaHukA_dxzfVRF\",\n      \"systemDataSourceId\": \"XYfrcSJVN66CpmamKON1m\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": []\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/head-tag\"\n    },\n    {\n      \"id\": \"oc1Ra5zelesj-wiJwiw-P\",\n      \"name\": \"animations\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"Qrpsu4yKLKARA-82TnhLI\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"status\": \"200\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/animations\"\n    },\n    {\n      \"id\": \"GgJUcrFoHE6EGMccjLa4t\",\n      \"name\": \"duration\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"Lmn0VIRtr_Yn9AvDyjgZG\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/duration\"\n    },\n    {\n      \"id\": \"uuYXNj4-XxzMtJpR8O5IH\",\n      \"name\": \"Text Duration\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"LTaU1Znm6P4yt3S8kAZi-\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": []\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/text-duration\"\n    },\n    {\n      \"id\": \"yaneLQ-EP83HtTFnZtbbj\",\n      \"name\": \"Assets\",\n      \"title\": \"\\\"Untitled\\\"\",\n      \"rootInstanceId\": \"jVzCxkeTMBCb0S3JUhDsQ\",\n      \"meta\": {\n        \"description\": \"\\\"\\\"\",\n        \"excludePageFromSearch\": \"true\",\n        \"language\": \"\\\"\\\"\",\n        \"socialImageUrl\": \"\\\"\\\"\",\n        \"redirect\": \"\\\"\\\"\",\n        \"documentType\": \"html\",\n        \"custom\": [\n          {\n            \"property\": \"\",\n            \"content\": \"\\\"\\\"\"\n          }\n        ]\n      },\n      \"marketplace\": {\n        \"include\": false\n      },\n      \"path\": \"/assets1\"\n    }\n  ],\n  \"assets\": [\n    {\n      \"id\": \"2b151fc7b4b0324e6ab78c40f72c7f59273f81fb1be3d08b3f9976428601b95e\",\n      \"name\": \"e-mail-39993_vrxyjxQv3j67Krs62Vz7Y.mp3\",\n      \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n      \"size\": 19680,\n      \"type\": \"file\",\n      \"format\": \"mp3\",\n      \"createdAt\": \"2026-01-14T01:59:48.958+00:00\",\n      \"meta\": {}\n    },\n    {\n      \"id\": \"4afe692f78ec0530e355a551a3860302c4478037db7b25a3bdae82c32c78634d\",\n      \"name\": \"webm-example_2r_6VmRBjhAy3ldaqz0gk.webm\",\n      \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n      \"size\": 901185,\n      \"type\": \"file\",\n      \"format\": \"webm\",\n      \"createdAt\": \"2026-01-14T02:59:19.806+00:00\",\n      \"meta\": {}\n    },\n    {\n      \"id\": \"7cf5892080fa66b5e6175ffd2d27c304ee6b09ce1f21847a95000225ad1afa59\",\n      \"name\": \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\",\n      \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n      \"size\": 268326,\n      \"type\": \"image\",\n      \"format\": \"png\",\n      \"createdAt\": \"2025-06-20T17:59:02.94+00:00\",\n      \"meta\": {\n        \"width\": 790,\n        \"height\": 786\n      }\n    },\n    {\n      \"id\": \"9a8bc926-7804-4d3f-af81-69196b1d2ed8\",\n      \"name\": \"small-avif-kitty_FnabJsioMWpBtXZSGf4DR.webp\",\n      \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n      \"size\": 2906,\n      \"type\": \"image\",\n      \"format\": \"webp\",\n      \"createdAt\": \"2023-09-12T09:44:22.12+00:00\",\n      \"meta\": {\n        \"width\": 100,\n        \"height\": 100\n      }\n    },\n    {\n      \"id\": \"cbf6b1b052e52b256cef54a032a546bf43bf3f5441be4d1c5eeaabce26903d78\",\n      \"name\": \"video_QamtUWsD-ShifhzZLoNIv_ald_1xtyEb3uHhFb7nCQa.mp4\",\n      \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n      \"size\": 170479,\n      \"type\": \"file\",\n      \"format\": \"mp4\",\n      \"createdAt\": \"2026-01-14T02:59:28.168+00:00\",\n      \"meta\": {}\n    },\n    {\n      \"id\": \"cd939c56-bcdd-4e64-bd9c-567a9bccd3da\",\n      \"name\": \"_937084ed-a798-49fe-8664-df93a2af605e_uiBk3o6UWdqolyakMvQJ9.jpeg\",\n      \"projectId\": \"cddc1d44-af37-4cb6-a430-d300cf6f932d\",\n      \"size\": 210614,\n      \"type\": \"image\",\n      \"format\": \"jpeg\",\n      \"createdAt\": \"2023-09-06T11:28:43.031+00:00\",\n      \"meta\": {\n        \"width\": 1024,\n        \"height\": 1024\n      }\n    }\n  ],\n  \"user\": {\n    \"email\": \"hello@webstudio.is\"\n  },\n  \"projectDomain\": \"webstudio-fixture-project-a-0su3o\",\n  \"projectTitle\": \"webstudio-fixture-project-a\",\n  \"origin\": \"https://main.development.webstudio.is\"\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/README.md",
    "content": "# Webstudio Features Fixture\n\nSee the [main fixtures README](../README.md) for complete documentation on how to use and update fixtures.\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/$resources.assets.ts",
    "content": "export const assets = {\n  \"2b151fc7b4b0324e6ab78c40f72c7f59273f81fb1be3d08b3f9976428601b95e\": {\n    url: \"/cgi/asset/e-mail-39993_vrxyjxQv3j67Krs62Vz7Y.mp3?format=raw\",\n  },\n  \"4afe692f78ec0530e355a551a3860302c4478037db7b25a3bdae82c32c78634d\": {\n    url: \"/cgi/asset/webm-example_2r_6VmRBjhAy3ldaqz0gk.webm?format=raw\",\n  },\n  \"7cf5892080fa66b5e6175ffd2d27c304ee6b09ce1f21847a95000225ad1afa59\": {\n    url: \"/cgi/image/cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png?format=raw\",\n    width: 790,\n    height: 786,\n  },\n  \"9a8bc926-7804-4d3f-af81-69196b1d2ed8\": {\n    url: \"/cgi/image/small-avif-kitty_FnabJsioMWpBtXZSGf4DR.webp?format=raw\",\n    width: 100,\n    height: 100,\n  },\n  cbf6b1b052e52b256cef54a032a546bf43bf3f5441be4d1c5eeaabce26903d78: {\n    url: \"/cgi/asset/video_QamtUWsD-ShifhzZLoNIv_ald_1xtyEb3uHhFb7nCQa.mp4?format=raw\",\n  },\n  \"cd939c56-bcdd-4e64-bd9c-567a9bccd3da\": {\n    url: \"/cgi/image/_937084ed-a798-49fe-8664-df93a2af605e_uiBk3o6UWdqolyakMvQJ9.jpeg?format=raw\",\n    width: 1024,\n    height: 1024,\n  },\n};\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/$resources.sitemap.xml.ts",
    "content": "export const sitemap = [\n  {\n    path: \"/\",\n    lastModified: \"2026-01-15\",\n  },\n  {\n    path: \"/_route_with_symbols_\",\n    lastModified: \"2026-01-15\",\n  },\n  {\n    path: \"/form\",\n    lastModified: \"2026-01-15\",\n  },\n  {\n    path: \"/heading-with-id\",\n    lastModified: \"2026-01-15\",\n  },\n  {\n    path: \"/resources\",\n    lastModified: \"2026-01-15\",\n  },\n  {\n    path: \"/nested/nested-page\",\n    lastModified: \"2026-01-15\",\n  },\n];\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[_route_with_symbols_]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"RouteWithSymbols\",\n    description: \"\",\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[_route_with_symbols_]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Image as Image } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Image\n        src={\"/assets/small-avif-kitty_FnabJsioMWpBtXZSGf4DR.webp\"}\n        className={`w-image c1czoo99`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[animations]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[animations]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Box as Box,\n  Heading as Heading,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  AnimateChildren as AnimateChildren,\n  AnimateText as AnimateText,\n} from \"@webstudio-is/sdk-components-animation\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body c1jgcte3 cncutr5 c1ywt30e cqvurle`}>\n      <Box className={`w-box c1m04i8w c13v7j50 cpjvam0 cqkqnmd`}>\n        <Heading className={`w-heading`}>{\"ANIMATIONS\"}</Heading>\n      </Box>\n      <Box className={`w-box c1y8ynw5 c2gdfrb c1yk3skc c16asro7 c1pt69cw`}>\n        <Box className={`w-box`}>\n          <AnimateChildren\n            action={{\n              type: \"view\",\n              animations: [\n                {\n                  name: \"Fade In\",\n                  description: \"Fade in the element as it scrolls into view.\",\n                  keyframes: [\n                    {\n                      offset: 0,\n                      styles: {\n                        opacity: { type: \"unit\", unit: \"number\", value: 0 },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"backwards\",\n                    rangeStart: [\n                      \"entry\",\n                      { type: \"unit\", value: 0, unit: \"%\" },\n                    ],\n                    rangeEnd: [\n                      \"entry\",\n                      { type: \"unit\", value: 100, unit: \"%\" },\n                    ],\n                  },\n                },\n                {\n                  name: \"Fade Out\",\n                  description:\n                    \"Fade out the element as it scrolls out of view.\",\n                  keyframes: [\n                    {\n                      offset: 1,\n                      styles: {\n                        opacity: { type: \"unit\", unit: \"number\", value: 0 },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"forwards\",\n                    rangeStart: [\"exit\", { type: \"unit\", value: 0, unit: \"%\" }],\n                    rangeEnd: [\"exit\", { type: \"unit\", value: 100, unit: \"%\" }],\n                  },\n                },\n                {\n                  name: \"Parallax In\",\n                  description:\n                    \"Parallax the element as it scrolls out of view.\",\n                  keyframes: [\n                    {\n                      offset: 0,\n                      styles: {\n                        translate: {\n                          type: \"tuple\",\n                          value: [\n                            { type: \"unit\", unit: \"number\", value: 0 },\n                            { type: \"unit\", unit: \"px\", value: 100 },\n                          ],\n                        },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"backwards\",\n                    rangeStart: [\n                      \"cover\",\n                      { type: \"unit\", value: 0, unit: \"%\" },\n                    ],\n                    rangeEnd: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n                  },\n                },\n                {\n                  name: \"Parallax Out\",\n                  description:\n                    \"Parallax the element as it scrolls out of view.\",\n                  keyframes: [\n                    {\n                      offset: 1,\n                      styles: {\n                        translate: {\n                          type: \"tuple\",\n                          value: [\n                            { type: \"unit\", unit: \"number\", value: 0 },\n                            { type: \"unit\", unit: \"px\", value: -100 },\n                          ],\n                        },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"forwards\",\n                    rangeStart: [\n                      \"cover\",\n                      { type: \"unit\", value: 50, unit: \"%\" },\n                    ],\n                    rangeEnd: [\n                      \"cover\",\n                      { type: \"unit\", value: 100, unit: \"%\" },\n                    ],\n                  },\n                },\n              ],\n              isPinned: false,\n            }}\n          >\n            <Heading className={`w-heading c1pdroxx cudat22`}>\n              {\"ANIMATED CHILD 0\"}\n            </Heading>\n            <Heading className={`w-heading`}>{\"ANIMATED CHILD 1\"}</Heading>\n          </AnimateChildren>\n        </Box>\n        <Box className={`w-box`}>\n          {\n            \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. \"\n          }\n        </Box>\n      </Box>\n      <Box className={`w-box cq3mp4w`} />\n      <Box className={`w-box c1y8ynw5 c2gdfrb c1yk3skc`}>\n        <Box className={`w-box`}>\n          <AnimateChildren\n            action={{\n              type: \"view\",\n              animations: [\n                {\n                  name: \"Fade In\",\n                  description: \"Fade in the element as it scrolls into view.\",\n                  keyframes: [\n                    {\n                      offset: 0,\n                      styles: {\n                        opacity: { type: \"unit\", unit: \"number\", value: 0 },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"backwards\",\n                    rangeStart: [\n                      \"entry\",\n                      { type: \"unit\", value: 0, unit: \"%\" },\n                    ],\n                    rangeEnd: [\n                      \"entry\",\n                      { type: \"unit\", value: 100, unit: \"%\" },\n                    ],\n                  },\n                },\n                {\n                  name: \"Fade Out\",\n                  description:\n                    \"Fade out the element as it scrolls out of view.\",\n                  keyframes: [\n                    {\n                      offset: 1,\n                      styles: {\n                        opacity: { type: \"unit\", unit: \"number\", value: 0 },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"forwards\",\n                    rangeStart: [\"exit\", { type: \"unit\", value: 0, unit: \"%\" }],\n                    rangeEnd: [\"exit\", { type: \"unit\", value: 100, unit: \"%\" }],\n                  },\n                },\n                {\n                  name: \"Parallax In\",\n                  description:\n                    \"Parallax the element as it scrolls out of view.\",\n                  keyframes: [\n                    {\n                      offset: 0,\n                      styles: {\n                        translate: {\n                          type: \"tuple\",\n                          value: [\n                            { type: \"unit\", unit: \"number\", value: 0 },\n                            { type: \"unit\", unit: \"px\", value: 100 },\n                          ],\n                        },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"backwards\",\n                    rangeStart: [\n                      \"cover\",\n                      { type: \"unit\", value: 0, unit: \"%\" },\n                    ],\n                    rangeEnd: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n                  },\n                },\n                {\n                  name: \"Parallax Out\",\n                  description:\n                    \"Parallax the element as it scrolls out of view.\",\n                  keyframes: [\n                    {\n                      offset: 1,\n                      styles: {\n                        translate: {\n                          type: \"tuple\",\n                          value: [\n                            { type: \"unit\", unit: \"number\", value: 0 },\n                            { type: \"unit\", unit: \"px\", value: -100 },\n                          ],\n                        },\n                      },\n                    },\n                  ],\n                  timing: {\n                    easing: \"linear\",\n                    fill: \"forwards\",\n                    rangeStart: [\n                      \"cover\",\n                      { type: \"unit\", value: 50, unit: \"%\" },\n                    ],\n                    rangeEnd: [\n                      \"cover\",\n                      { type: \"unit\", value: 100, unit: \"%\" },\n                    ],\n                  },\n                },\n              ],\n              isPinned: false,\n            }}\n          >\n            <AnimateText slidingWindow={20} className={`w-text-animation`}>\n              <Heading className={`w-heading c1pdroxx`}>\n                {\"ANIMATED CHILD 1\"}\n              </Heading>\n              <Box className={`w-box`}>\n                {\n                  \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. \"\n                }\n              </Box>\n            </AnimateText>\n          </AnimateChildren>\n        </Box>\n        <Box className={`w-box`}>\n          {\n            \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sed tellus at nisi feugiat accumsan. Aliquam tristique vitae augue eget lacinia. \"\n          }\n        </Box>\n      </Box>\n      <Box className={`w-box c1m04i8w c13v7j50 cpjvam0 cqkqnmd`}>\n        <Heading className={`w-heading`}>{\"THE END\"}</Heading>\n      </Box>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[assets1]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const assets_1: ResourceRequest = {\n    name: \"assets\",\n    url: \"/$resources/assets\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  };\n  const _data = new Map<string, ResourceRequest>([[\"assets_1\", assets_1]]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: undefined,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[assets1]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  let assets = useResource(\"assets_1\");\n  return (\n    <Body className={`w-element`}>\n      <audio\n        controls={true}\n        src={\n          assets?.data?.[\n            \"2b151fc7b4b0324e6ab78c40f72c7f59273f81fb1be3d08b3f9976428601b95e\"\n          ]?.url\n        }\n        className={`w-element`}\n      />\n      <video\n        controls={true}\n        src={\n          assets?.data?.[\n            \"4afe692f78ec0530e355a551a3860302c4478037db7b25a3bdae82c32c78634d\"\n          ]?.url\n        }\n        className={`w-element`}\n      />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[class-names]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[class-names]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Box as Box } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  let [classVar, set$classVar] = useVariableState<any>(\"varClass\");\n  return (\n    <Body className={`w-body`}>\n      <Box\n        id={\"\\\"broken'with`symbols\"}\n        className={`w-box cm16yxw ${\"custom-class \\\"broken 'with `symbols\"}`}\n      />\n      <Box className={`w-box ctm310 ${`${classVar} class_3`}`} />\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[content-block]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[content-block]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Box as Box,\n  Heading as Heading,\n  Paragraph as Paragraph,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Box className={`w-box c620822`}>\n        <Heading className={`w-heading cc5h0no`}>\n          {\"Content Block With Templates And Content\"}\n        </Heading>\n        <Heading className={`w-heading`}>{\"H1\"}</Heading>\n        <Paragraph className={`w-paragraph`}>{\"Paragraph\"}</Paragraph>\n      </Box>\n      <Box className={`w-box ch21dzk`}>\n        <Heading className={`w-heading cc5h0no`}>\n          {\"Content Block With Templates Only\"}\n        </Heading>\n      </Box>\n      <Box className={`w-box c44hi06`}>\n        <Heading className={`w-heading cc5h0no`}>{\"With Content Only\"}</Heading>\n        <Heading className={`w-heading`}>{\"H1\"}</Heading>\n        <Paragraph className={`w-paragraph`}>{\"Paragraph\"}</Paragraph>\n      </Box>\n      <Box className={`w-box cno1tjk`}>\n        <Heading className={`w-heading cc5h0no`}>{\"Empty\"}</Heading>\n      </Box>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[duration]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: undefined,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[duration]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { AnimateChildren as AnimateChildren } from \"@webstudio-is/sdk-components-animation\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <AnimateChildren\n        action={{\n          type: \"view\",\n          animations: [\n            {\n              name: \"Parent\",\n              description: \"Parallax the element as it scrolls into the view.\",\n              keyframes: [\n                {\n                  offset: 0,\n                  styles: {\n                    translate: {\n                      type: \"tuple\",\n                      value: [\n                        { type: \"unit\", unit: \"number\", value: 0 },\n                        { type: \"unit\", unit: \"px\", value: 100 },\n                      ],\n                    },\n                  },\n                },\n              ],\n              timing: {\n                easing: \"linear\",\n                fill: \"backwards\",\n                duration: { type: \"unit\", value: 302, unit: \"ms\" },\n                rangeStart: [\"cover\", { type: \"unit\", value: 0, unit: \"%\" }],\n                rangeEnd: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n              },\n            },\n          ],\n          insetStart: { type: \"keyword\", value: \"auto\" },\n          insetEnd: { type: \"keyword\", value: \"auto\" },\n          isPinned: true,\n          debug: false,\n        }}\n      >\n        <Heading className={`w-heading cjib6ds`}>\n          {\"HELLO WORLD\"}\n          {\"\"}\n          <br />\n          {\"\"}\n          {\"GOOD\"}\n          {\"\"}\n          <br />\n          {\"\"}\n          {\"BAD\"}\n          {\"\"}\n          <br />\n          {\"\"}\n          {\"UGLY\"}\n        </Heading>\n      </AnimateChildren>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[expressions]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const jsonResourceVariable_1: ResourceRequest = {\n    name: \"jsonResourceVariable\",\n    url: \"https://httpbin.org/get?hello=world\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  };\n  const _data = new Map<string, ResourceRequest>([\n    [\"jsonResourceVariable_1\", jsonResourceVariable_1],\n  ]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[expressions]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Heading as Heading,\n  HtmlEmbed as HtmlEmbed,\n  Box as Box,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  let jsonResourceVariable = useResource(\"jsonResourceVariable_1\");\n  let [jsonVar, set$jsonVar] = useVariableState<any>({ hello: \"world\" });\n  let [globalVariable, set$globalVariable] =\n    useVariableState<any>(\"globalValue\");\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>\n        {`${jsonResourceVariable?.data?.args}`}\n      </Heading>\n      <HtmlEmbed\n        code={`<script>\nconst a = ${jsonResourceVariable?.data?.args}\n\nconst b = ${jsonVar}\n\nconsole.log(a, b);\n</script>`}\n        className={`w-html-embed`}\n      />\n      <Box className={`w-box`}>{globalVariable}</Box>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[form]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const action: ResourceRequest = {\n    name: \"action\",\n    url: \"/custom\",\n    searchParams: [],\n    method: \"get\",\n    headers: [{ name: \"Content-Type\", value: \"application/json\" }],\n  };\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([[\"action\", action]]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"form\",\n    description: \"\",\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[form]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Form as Form,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Box as Box,\n  Label as Label,\n  Input as Input,\n  Button as Button,\n  Heading as Heading,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  let [formState, set$formState] = useVariableState<any>(\"initial\");\n  let [formState_1, set$formState_1] = useVariableState<any>(\"initial\");\n  return (\n    <Body className={`w-body`}>\n      <Form\n        state={formState}\n        onStateChange={(state: any) => {\n          formState = state;\n          set$formState(formState);\n        }}\n        className={`w-webhook-form`}\n      >\n        {(formState === \"initial\" || formState === \"error\") && (\n          <Box className={`w-box`}>\n            <Heading tag={\"h3\"} className={`w-heading`}>\n              {\"Default form\"}\n            </Heading>\n            <Label className={`w-input-label`}>{\"Name\"}</Label>\n            <Input name={\"name\"} className={`w-text-input`} />\n            <Label className={`w-input-label`}>{\"Email\"}</Label>\n            <Input name={\"email\"} className={`w-text-input`} />\n            <Button className={`w-button`}>{\"Submit\"}</Button>\n          </Box>\n        )}\n        {formState === \"success\" && (\n          <Box className={`w-box`}>{\"Thank you for getting in touch!\"}</Box>\n        )}\n        {formState === \"error\" && (\n          <Box className={`w-box`}>{\"Sorry, something went wrong.\"}</Box>\n        )}\n      </Form>\n      <Form\n        state={formState_1}\n        onStateChange={(state: any) => {\n          formState_1 = state;\n          set$formState_1(formState_1);\n        }}\n        action={\"action\"}\n        className={`w-webhook-form`}\n      >\n        {(formState_1 === \"initial\" || formState_1 === \"error\") && (\n          <Box className={`w-box`}>\n            <Heading tag={\"h3\"} className={`w-heading`}>\n              {\"Form with custom action and method\"}\n            </Heading>\n            <Label className={`w-input-label`}>{\"Name\"}</Label>\n            <Input name={\"name\"} className={`w-text-input`} />\n            <Label className={`w-input-label`}>{\"Email\"}</Label>\n            <Input name={\"email\"} className={`w-text-input`} />\n            <Button className={`w-button`}>{\"Submit\"}</Button>\n          </Box>\n        )}\n        {formState_1 === \"success\" && (\n          <Box className={`w-box`}>{\"Thank you for getting in touch!\"}</Box>\n        )}\n        {formState_1 === \"error\" && (\n          <Box className={`w-box`}>{\"Sorry, something went wrong.\"}</Box>\n        )}\n      </Form>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[head-tag]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[head-tag]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  HeadSlot as HeadSlot,\n  HeadLink as HeadLink,\n  HeadMeta as HeadMeta,\n  Heading as Heading,\n  HeadTitle as HeadTitle,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <HeadSlot>\n        <HeadTitle>{\"Head Slot Title\"}</HeadTitle>\n        <HeadLink rel={\"help\"} href={\"/help-head-slot\"} />\n        <HeadMeta name={\"keywords\"} content={\"Head Slot Content\"} />\n        <HeadMeta content={\"Head Slot Content\"} property={\"og:title\"} />\n        <HeadLink\n          rel={\"canonical\"}\n          href={\"https://overwritten.slot/head-slot-tag\"}\n        />\n      </HeadSlot>\n      <Heading className={`w-heading`}>{\"Test Head Slot\"}</Heading>\n      <Link href={\"/\"} className={`w-link`}>\n        {\"Go Home\"}\n      </Link>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[heading-with-id]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"heading-with-id\",\n    description: \"\",\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[heading-with-id]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading id={\"my-heading\"} className={`w-heading`}>\n        {\"Heading you can edit\"}\n      </Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[nested].[nested-page]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Nested Page\",\n    description: \"\",\n    excludePageFromSearch: false,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[nested].[nested-page]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <Heading className={`w-heading`}>{\"Nested page\"}</Heading>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[radix]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Radix Revelations: Unraveling the Feline Mystique\",\n    description:\n      \"Delve deep into the radix roots of feline behaviors. At KittyNoTouchy, we dissect the core essence, or 'radix', of what makes cats the enigmatic creatures they are. Join us as we explore the radix of their instincts, habits, and quirks.\",\n    excludePageFromSearch: true,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[radix]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Accordion as Accordion,\n  AccordionItem as AccordionItem,\n  AccordionHeader as AccordionHeader,\n  AccordionTrigger as AccordionTrigger,\n  AccordionContent as AccordionContent,\n} from \"@webstudio-is/sdk-components-react-radix\";\nimport {\n  Text as Text,\n  Box as Box,\n  HtmlEmbed as HtmlEmbed,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  let [accordionValue, set$accordionValue] = useVariableState<any>(\"0\");\n  return (\n    <Body className={`w-body`}>\n      <Accordion\n        collapsible={true}\n        value={accordionValue}\n        onValueChange={(value: any) => {\n          accordionValue = value;\n          set$accordionValue(accordionValue);\n        }}\n        className={`w-accordion`}\n      >\n        <AccordionItem data-ws-index=\"0\" className={`w-item c6gjj8k`}>\n          <AccordionHeader className={`w-item-header c13v7j50`}>\n            <AccordionTrigger\n              className={`w-item-trigger c13v7j50 c1qxdkbn chhejm9 c13bviim cpjvam0 c1tw5o2 cqw88jp c44srea c14kxsax cg783j6 c1ad236a c1s4llbc`}\n            >\n              <Text className={`w-text`}>{\"Is it accessible?\"}</Text>\n              <Box\n                className={`w-box c14hansb c6d2sb5 cwwiftc c16vb2zi c1hl6g8z c1xm49r0 c1vri55v`}\n              >\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M4.04 6.284a.65.65 0 0 1 .92.001L8 9.335l3.04-3.05a.65.65 0 1 1 .921.918l-3.5 3.512a.65.65 0 0 1-.921 0L4.039 7.203a.65.65 0 0 1 .001-.92Z\"/></svg>'\n                  }\n                  className={`w-html-embed`}\n                />\n              </Box>\n            </AccordionTrigger>\n          </AccordionHeader>\n          <AccordionContent\n            className={`w-item-content c1j0j9ep c1gl2i2 c44srea c1xcxvc0 c12ct49k`}\n          >\n            {\"Yes. It adheres to the WAI-ARIA design pattern.\"}\n          </AccordionContent>\n        </AccordionItem>\n        <AccordionItem data-ws-index=\"1\" className={`w-item c6gjj8k`}>\n          <AccordionHeader className={`w-item-header c13v7j50`}>\n            <AccordionTrigger\n              className={`w-item-trigger c13v7j50 c1qxdkbn chhejm9 c13bviim cpjvam0 c1tw5o2 cqw88jp c44srea c14kxsax cg783j6 c1ad236a c1s4llbc`}\n            >\n              <Text className={`w-text`}>{\"Is it styled?\"}</Text>\n              <Box\n                className={`w-box c14hansb c6d2sb5 cwwiftc c16vb2zi c1hl6g8z c1xm49r0 c1vri55v`}\n              >\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M4.04 6.284a.65.65 0 0 1 .92.001L8 9.335l3.04-3.05a.65.65 0 1 1 .921.918l-3.5 3.512a.65.65 0 0 1-.921 0L4.039 7.203a.65.65 0 0 1 .001-.92Z\"/></svg>'\n                  }\n                  className={`w-html-embed`}\n                />\n              </Box>\n            </AccordionTrigger>\n          </AccordionHeader>\n          <AccordionContent\n            className={`w-item-content c1j0j9ep c1gl2i2 c44srea c1xcxvc0 c12ct49k`}\n          >\n            {\n              \"Yes. It comes with default styles that matches the other components' aesthetic.\"\n            }\n          </AccordionContent>\n        </AccordionItem>\n        <AccordionItem data-ws-index=\"2\" className={`w-item c6gjj8k`}>\n          <AccordionHeader className={`w-item-header c13v7j50`}>\n            <AccordionTrigger\n              className={`w-item-trigger c13v7j50 c1qxdkbn chhejm9 c13bviim cpjvam0 c1tw5o2 cqw88jp c44srea c14kxsax cg783j6 c1ad236a c1s4llbc`}\n            >\n              <Text className={`w-text`}>{\"Is it animated?\"}</Text>\n              <Box\n                className={`w-box c14hansb c6d2sb5 cwwiftc c16vb2zi c1hl6g8z c1xm49r0 c1vri55v`}\n              >\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M4.04 6.284a.65.65 0 0 1 .92.001L8 9.335l3.04-3.05a.65.65 0 1 1 .921.918l-3.5 3.512a.65.65 0 0 1-.921 0L4.039 7.203a.65.65 0 0 1 .001-.92Z\"/></svg>'\n                  }\n                  className={`w-html-embed`}\n                />\n              </Box>\n            </AccordionTrigger>\n          </AccordionHeader>\n          <AccordionContent\n            className={`w-item-content c1j0j9ep c1gl2i2 c44srea c1xcxvc0 c12ct49k`}\n          >\n            {\n              \"Yes. It's animated by default, but you can disable it if you prefer.\"\n            }\n          </AccordionContent>\n        </AccordionItem>\n      </Accordion>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[resources]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const list_1: ResourceRequest = {\n    name: \"list\",\n    url: \"https://gist.githubusercontent.com/TrySound/56507c301ec85669db5f1541406a9259/raw/a49548730ab592c86b9e7781f5b29beec4765494/collection.json\",\n    searchParams: [],\n    method: \"get\",\n    headers: [],\n  };\n  const _data = new Map<string, ResourceRequest>([[\"list_1\", list_1]]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"resources\",\n    description: \"\",\n    excludePageFromSearch: false,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[resources]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Box as Box,\n  HtmlEmbed as HtmlEmbed,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  let list = useResource(\"list_1\");\n  return (\n    <Body className={`w-body`}>\n      {Object.entries(\n        // @ts-ignore\n        list?.data ?? {}\n      ).map(([_key, collectionItem]: any) => {\n        const index = Array.isArray(list?.data) ? Number(_key) : _key;\n        return (\n          <Fragment key={index}>\n            <Box className={`w-box`}>\n              <HtmlEmbed\n                code={collectionItem?.name}\n                className={`w-html-embed`}\n              />\n            </Box>\n          </Fragment>\n        );\n      })}\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[sitemap.xml]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: 200,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[sitemap.xml]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  XmlNode as XmlNode,\n  XmlTime as XmlTime,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  const system = _props.system;\n  return (\n    <XmlNode\n      tag={\"urlset\"}\n      xmlns={\"http://www.sitemaps.org/schemas/sitemap/0.9\"}\n      xmlns:xhtml={\"http://www.w3.org/TR/xhtml11/xhtml11_schema.html\"}\n    >\n      {Object.entries(\n        // @ts-ignore\n        [\n          {\n            path: \"/\",\n            lastModified: \"2024-05-07\",\n          },\n          {\n            path: \"/olegs-test\",\n            lastModified: \"2024-05-07\",\n          },\n        ] ?? {}\n      ).map(([_key, url]: any) => {\n        const index = Array.isArray([\n          {\n            path: \"/\",\n            lastModified: \"2024-05-07\",\n          },\n          {\n            path: \"/olegs-test\",\n            lastModified: \"2024-05-07\",\n          },\n        ])\n          ? Number(_key)\n          : _key;\n        return (\n          <Fragment key={index}>\n            <XmlNode tag={\"url\"}>\n              <XmlNode tag={\"loc\"}>\n                {`${system?.origin ?? \"${ORIGIN}\"}${url?.path}`}\n              </XmlNode>\n              <XmlNode tag={\"lastmod\"}>{url?.lastModified}</XmlNode>\n              <XmlNode\n                tag={\"xhtml:link\"}\n                rel={\"alternate\"}\n                hreflang={\"en\"}\n                href={`${system?.origin ?? \"${ORIGIN}\"}${url?.path}en`}\n              />\n            </XmlNode>\n          </Fragment>\n        );\n      })}\n      <XmlNode tag={\"url\"}>\n        <XmlNode tag={\"loc\"}>{\"custom-hand-made-location\"}</XmlNode>\n        <XmlNode tag={\"lastmod\"}>\n          <XmlTime datetime={\"1733402818245\"} />\n        </XmlNode>\n        <XmlNode\n          tag={\"xhtml:link\"}\n          rel={\"alternate\"}\n          hreflang={\"en\"}\n          href={\"custom-en-location\"}\n        />\n        <XmlNode tag={\"title\"}>{\"Hello\"}</XmlNode>\n        <XmlNode tag={\"link\"}>{\"https://webstudio.is\"}</XmlNode>\n      </XmlNode>\n    </XmlNode>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[text-duration]._index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Untitled\",\n    description: \"\",\n    excludePageFromSearch: true,\n    language: \"\",\n    socialImageAssetName: undefined,\n    socialImageUrl: \"\",\n    status: undefined,\n    redirect: \"\",\n    custom: [],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/[text-duration]._index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport { Body as Body } from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  AnimateChildren as AnimateChildren,\n  AnimateText as AnimateText,\n} from \"@webstudio-is/sdk-components-animation\";\nimport { Heading as Heading } from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body`}>\n      <AnimateChildren\n        action={{\n          type: \"view\",\n          animations: [\n            {\n              name: \"Parent\",\n              description: \"Parallax the element as it scrolls into the view.\",\n              keyframes: [\n                {\n                  offset: 0,\n                  styles: {\n                    translate: {\n                      type: \"tuple\",\n                      value: [\n                        { type: \"unit\", unit: \"number\", value: 0 },\n                        { type: \"unit\", unit: \"px\", value: 200 },\n                      ],\n                    },\n                  },\n                },\n              ],\n              timing: {\n                easing: \"linear\",\n                fill: \"backwards\",\n                duration: { type: \"unit\", value: 600, unit: \"ms\" },\n                rangeStart: [\"cover\", { type: \"unit\", value: 0, unit: \"%\" }],\n                rangeEnd: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n              },\n            },\n            {\n              name: \"Parallax In\",\n              description: \"Parallax the element as it scrolls into the view.\",\n              keyframes: [\n                {\n                  offset: 0,\n                  styles: { opacity: { type: \"unit\", unit: \"%\", value: 0 } },\n                },\n              ],\n              timing: {\n                easing: \"linear\",\n                fill: \"backwards\",\n                duration: { type: \"unit\", value: 600, unit: \"ms\" },\n                rangeStart: [\"cover\", { type: \"unit\", value: 0, unit: \"%\" }],\n                rangeEnd: [\"cover\", { type: \"unit\", value: 50, unit: \"%\" }],\n              },\n            },\n          ],\n          insetStart: { type: \"keyword\", value: \"auto\" },\n          insetEnd: { type: \"keyword\", value: \"auto\" },\n          isPinned: true,\n          debug: false,\n        }}\n      >\n        <AnimateText className={`w-text-animation`}>\n          <Heading className={`w-heading cjib6ds`}>\n            {\"HELLO WORLD\"}\n            {\"\"}\n            <br />\n            {\"\"}\n            {\"GOOD\"}\n            {\"\"}\n            <br />\n            {\"\"}\n            {\"BAD\"}\n            {\"\"}\n            <br />\n            {\"\"}\n            {\"UGLY\"}\n          </Heading>\n        </AnimateText>\n      </AnimateChildren>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/_index.server.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport type { System, ResourceRequest } from \"@webstudio-is/sdk\";\nexport const getResources = (_props: { system: System }) => {\n  const _data = new Map<string, ResourceRequest>([]);\n  const _action = new Map<string, ResourceRequest>([]);\n  return { data: _data, action: _action };\n};\n\nexport const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"The Ultimate Cat Protection Zone\",\n    description:\n      \"Dive into the world of felines and discover why some whiskers are best left untouched. From intriguing cat behaviors to protective measures, \\nKittyGuardedZone is your go-to hub for all things 'hands-off' in the cat realm.\",\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName:\n      \"_937084ed-a798-49fe-8664-df93a2af605e_uiBk3o6UWdqolyakMvQJ9.jpeg\",\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n      {\n        property: \"fb:app_id\",\n        content: \"app_id_app_id_app_id\",\n      },\n    ],\n  };\n};\n\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params;\n};\n\nexport const contactEmail = \"hello@webstudio.is\";\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/_index.tsx",
    "content": "/* eslint-disable */\n/* This is a auto generated file for building the project */\n\nimport { Fragment, useState } from \"react\";\nimport { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Body as Body,\n  Link as Link,\n  Link as Link_1,\n} from \"@webstudio-is/sdk-components-react-router\";\nimport {\n  Heading as Heading,\n  Box as Box,\n  Paragraph as Paragraph,\n  Image as Image,\n  Text as Text,\n} from \"@webstudio-is/sdk-components-react\";\n\nexport const projectId = \"cddc1d44-af37-4cb6-a430-d300cf6f932d\";\n\nexport const lastPublished = \"2026-01-15T16:19:55.574Z\";\n\nexport const siteName = \"KittyGuardedZone\";\n\nexport const breakpoints = [\n  { id: \"UoTkWyaFuTYJihS3MFYK5\" },\n  { id: \"ZMaWCtWpH-ao0e_kgIHqR\", minWidth: 372 },\n  { id: \"Z8WjyXWkCrr35PXgjHdpY\", minWidth: 472 },\n];\n\nexport const favIconAsset: string | undefined =\n  \"cat_silhouette_BDpTbUFSpVbfUWQZNxbBG.png\";\n\n// Font assets on current page (can be preloaded)\nexport const pageFontAssets: string[] = [];\n\nexport const pageBackgroundImageAssets: string[] = [];\n\nconst Script = ({ children, ...props }: Record<string, string | boolean>) => {\n  if (children == null) {\n    return <script {...props} />;\n  }\n\n  return <script {...props} dangerouslySetInnerHTML={{ __html: children }} />;\n};\nconst Style = ({ children, ...props }: Record<string, string | boolean>) => {\n  if (children == null) {\n    return <style {...props} />;\n  }\n\n  return <style {...props} dangerouslySetInnerHTML={{ __html: children }} />;\n};\n\nexport const CustomCode = () => {\n  return (\n    <>\n      <Script>{\"console.log('KittyGuardedZone')\"}</Script>\n    </>\n  );\n};\n\nconst Page = (_props: { system: any }) => {\n  return (\n    <Body className={`w-body cielobv`}>\n      <Heading className={`w-heading ceva767`}>\n        {\"DO NOT TOUCH THIS PROJECT, IT'S USED FOR FIXTURES\"}\n      </Heading>\n      <Box className={`w-box c13v7j50 cqkqnmd cv6wa71`}>\n        <Box className={`w-box c1t3ybra c1qxdkbn c1ogzcge cv3kvac`}>\n          <Heading className={`w-heading`}>{\"Heading\"}</Heading>\n          <Paragraph className={`w-paragraph`}>\n            {\n              \"a little kitten painted in black and white gouache with a thick brush\"\n            }\n          </Paragraph>\n          <Link_1 href={\"https://github.com/\"} className={`w-element ch2exr5`}>\n            {\"Click here to adore more kittens\"}\n          </Link_1>\n          <Text tag={\"span\"} className={`w-text`}>\n            {\" or \"}\n          </Text>\n          <Link\n            href={\"/assets/small-avif-kitty_FnabJsioMWpBtXZSGf4DR.webp\"}\n            className={`w-link`}\n          >\n            {\"go download this little kitten\"}\n          </Link>\n          <Box className={`w-box`} />\n          <Link href={\"/_route_with_symbols_\"} className={`w-link ch2exr5`}>\n            {\"Symbols in path\"}\n          </Link>\n          <Link\n            href={\"/heading-with-id#my-heading\"}\n            className={`w-link ch2exr5`}\n          >\n            {\"Link to instance\"}\n          </Link>\n        </Box>\n        <Box className={`w-box c1t3ybra c1qxdkbn c1ogzcge cv3kvac`}>\n          <Image\n            src={\n              \"/assets/_937084ed-a798-49fe-8664-df93a2af605e_uiBk3o6UWdqolyakMvQJ9.jpeg\"\n            }\n            className={`w-image c1czoo99`}\n          />\n        </Box>\n      </Box>\n    </Body>\n  );\n};\n\nexport { Page };\n"
  },
  {
    "path": "fixtures/webstudio-features/app/__generated__/index.css",
    "content": "@layer presets {\n  :root {\n    display: grid;\n    min-height: 100%;\n    grid-template-rows: auto;\n    grid-template-columns: 1fr;\n    font-family: Arial, Roboto, sans-serif;\n    font-size: 16px;\n    line-height: 1.2;\n    white-space: pre-wrap;\n    white-space-collapse: preserve;\n  }\n  a.w-element {\n    box-sizing: border-box;\n  }\n  body.w-element {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  body.w-body {\n    box-sizing: border-box;\n    -webkit-font-smoothing: antialiased;\n    -moz-osx-font-smoothing: grayscale;\n    margin: 0;\n  }\n  h1.w-heading {\n    box-sizing: border-box;\n  }\n  h3.w-heading {\n    box-sizing: border-box;\n  }\n  div.w-box {\n    box-sizing: border-box;\n  }\n  p.w-paragraph {\n    box-sizing: border-box;\n  }\n  img.w-image {\n    box-sizing: border-box;\n    max-width: 100%;\n    display: block;\n    height: auto;\n  }\n  a.w-link {\n    box-sizing: border-box;\n    display: inline-block;\n  }\n  div.w-text {\n    box-sizing: border-box;\n    min-height: 1em;\n  }\n  div.w-accordion {\n    box-sizing: border-box;\n  }\n  div.w-item {\n    box-sizing: border-box;\n  }\n  h3.w-item-header {\n    box-sizing: border-box;\n    margin-top: 0px;\n    margin-bottom: 0px;\n  }\n  button.w-item-trigger {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    background-color: transparent;\n    background-image: none;\n    border: 0px solid rgb(226 232 240 / 1);\n    margin: 0;\n    padding: 0px;\n  }\n  div.w-html-embed {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse;\n  }\n  div.w-item-content {\n    box-sizing: border-box;\n  }\n  form.w-webhook-form {\n    box-sizing: border-box;\n  }\n  label.w-input-label {\n    box-sizing: border-box;\n    display: block;\n  }\n  input.w-text-input {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    display: block;\n    margin: 0;\n  }\n  button.w-button {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0;\n  }\n  div.w-text-animation {\n    box-sizing: border-box;\n  }\n}\n@media all {\n  .ceva767 {\n    font-size: 4em;\n  }\n  .c13v7j50 {\n    display: flex;\n  }\n  .cqkqnmd {\n    justify-content: center;\n  }\n  .cv6wa71 {\n    align-items: start;\n  }\n  .c1t3ybra {\n    min-width: 0px;\n  }\n  .c1qxdkbn {\n    flex-grow: 1;\n  }\n  .c1ogzcge {\n    flex-shrink: 0;\n  }\n  .cv3kvac {\n    flex-basis: 0px;\n  }\n  .c1czoo99 {\n    aspect-ratio: 1;\n  }\n  .cielobv {\n    padding: 16px;\n  }\n  .c6gjj8k {\n    border-bottom: 1px solid rgb(226 232 240 / 1);\n  }\n  .chhejm9 {\n    flex-shrink: 1;\n  }\n  .c13bviim {\n    flex-basis: 0%;\n  }\n  .cpjvam0 {\n    align-items: center;\n  }\n  .c1tw5o2 {\n    justify-content: space-between;\n  }\n  .cqw88jp {\n    padding-top: 1rem;\n  }\n  .c44srea {\n    padding-bottom: 1rem;\n  }\n  .c14kxsax {\n    font-weight: 500;\n  }\n  .cg783j6 {\n    --accordion-trigger-icon-transform: 0deg;\n  }\n  .c1ad236a:hover {\n    text-decoration-line: underline;\n  }\n  .c1s4llbc[data-state=\"open\"] {\n    --accordion-trigger-icon-transform: 180deg;\n  }\n  .c14hansb {\n    rotate: var(--accordion-trigger-icon-transform);\n  }\n  .c6d2sb5 {\n    height: 1rem;\n  }\n  .cwwiftc {\n    width: 1rem;\n  }\n  .c16vb2zi {\n    flex-grow: 0;\n  }\n  .c1hl6g8z {\n    transition-property: all;\n  }\n  .c1xm49r0 {\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n  }\n  .c1vri55v {\n    transition-duration: 200ms;\n  }\n  .c1j0j9ep {\n    font-size: 0.875rem;\n  }\n  .c1gl2i2 {\n    line-height: 1.25rem;\n  }\n  .c1xcxvc0 {\n    overflow-x: hidden;\n  }\n  .c12ct49k {\n    overflow-y: hidden;\n  }\n  .ch2exr5 {\n    display: block;\n  }\n  .cm16yxw {\n    width: 300px;\n  }\n  .ctm310 {\n    margin-top: 12px;\n  }\n  .c1jumvji {\n    margin-top: 1em;\n  }\n  .cvdtpev {\n    margin-bottom: 0em;\n  }\n  .cc5h0no {\n    font-size: 1em;\n  }\n  .c620822 {\n    background-color: rgb(251 247 3 / 1);\n  }\n  .ch21dzk {\n    background-color: rgb(89 250 2 / 1);\n  }\n  .c44hi06 {\n    background-color: rgb(2 250 168 / 1);\n  }\n  .cno1tjk {\n    background-color: rgb(2 139 250 / 1);\n  }\n  .c1m04i8w {\n    height: 100dvh;\n  }\n  .cjib6ds {\n    text-align: center;\n  }\n}\n@media all and (min-width: 472px) {\n  .c1jgcte3 {\n    max-width: 900px;\n  }\n  .cncutr5 {\n    width: 100%;\n  }\n  .c1ywt30e {\n    min-width: 0px;\n  }\n  .cqvurle {\n    justify-self: center;\n  }\n  .c1y8ynw5 {\n    display: grid;\n  }\n  .c2gdfrb {\n    grid-auto-flow: column;\n  }\n  .c1yk3skc {\n    grid-auto-columns: 1fr;\n  }\n  .c16asro7 {\n    align-items: start;\n  }\n  .c1pt69cw {\n    align-content: start;\n  }\n  .cq3mp4w {\n    height: 40dvh;\n  }\n  .c1pdroxx {\n    margin-top: 0em;\n  }\n  .cudat22 {\n    margin-bottom: 0em;\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = ({ src }) => {\n  return src;\n};\n"
  },
  {
    "path": "fixtures/webstudio-features/app/extension.ts",
    "content": "import { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"react-router\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[_route_with_symbols_]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[_route_with_symbols_]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[_route_with_symbols_]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[animations]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[animations]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[animations]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[assets1]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[assets1]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[assets1]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[class-names]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[class-names]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[class-names]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[content-block]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[content-block]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[content-block]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[duration]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[duration]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[duration]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[expressions]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[expressions]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[expressions]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[form]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[form]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[form]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[head-tag]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[head-tag]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[head-tag]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[heading-with-id]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[heading-with-id]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[heading-with-id]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[nested].[nested-page]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[nested].[nested-page]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[nested].[nested-page]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[radix]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[radix]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[radix]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[resources]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[resources]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[resources]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[sitemap.xml]._index.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { type LoaderFunctionArgs, redirect } from \"react-router\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  xmlNodeTagSuffix,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { Page, breakpoints } from \"../__generated__/[sitemap.xml]._index\";\nimport {\n  getPageMeta,\n  getRemixParams,\n  getResources,\n} from \"../__generated__/[sitemap.xml]._index.server\";\nimport { assetBaseUrl, imageLoader } from \"../constants.mjs\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    return redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  let text = renderToString(\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      <Page system={system} />\n    </ReactSdkContext.Provider>\n  );\n\n  // Xml is wrapped with <svg> to prevent React from hoisting elements like <title>, <meta>, and <link> out of their intended scope during rendering.\n  // More details: https://github.com/facebook/react/blob/7c8e5e7ab8bb63de911637892392c5efd8ce1d0f/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L3083\n  text = text.replace(/^<svg>/g, \"\").replace(/<\\/svg>$/g, \"\");\n\n  // React has issues rendering certain elements, such as errors when a <link> element has children.\n  // To render XML, we wrap it with an <svg> tag and add a suffix to avoid React's default behavior on these elements.\n  text = text.replaceAll(xmlNodeTagSuffix, \"\");\n\n  return new Response(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${text}`, {\n    headers: { \"Content-Type\": \"application/xml\" },\n  });\n};\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/[text-duration]._index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/[text-duration]._index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/[text-duration]._index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes/_index.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"../__generated__/_index\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"../__generated__/_index.server\";\nimport * as constants from \"../constants.mjs\";\nimport css from \"../__generated__/index.css?url\";\nimport { sitemap } from \"../__generated__/$resources.sitemap.xml\";\nimport { assets } from \"../__generated__/$resources.assets\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "fixtures/webstudio-features/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "fixtures/webstudio-features/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"cli\": \"NODE_OPTIONS='--conditions=webstudio --import=tsx' webstudio\",\n    \"fixtures:link\": \"pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'\",\n    \"fixtures:sync\": \"pnpm cli sync --buildId 1b66ee06-8ea5-4420-9a0e-e0d3a67aca32 && pnpm prettier --write ./.webstudio/\",\n    \"fixtures:build\": \"pnpm cli build --template react-router --template ./.template && pnpm prettier --write ./app/ ./package.json ./tsconfig.json\"\n  },\n  \"private\": true,\n  \"sideEffects\": false,\n  \"dependencies\": {\n    \"@miniflare/html-rewriter\": \"^2.14.4\",\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@react-router/node\": \"^7.5.3\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"vite\": \"^6.3.4\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"fast-glob\": \"^3.3.2\",\n    \"typescript\": \"5.8.2\",\n    \"webstudio\": \"workspace:*\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"name\": \"webstudio-features\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"AGPL-3.0-or-later\"\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/proxy-emulator/dedupe-meta.ts",
    "content": "import { HTMLRewriter } from \"@miniflare/html-rewriter\";\nimport { Plugin } from \"vite\";\n\nexport const dedupeMeta: Plugin = {\n  name: \"html-rewriter-middleware\",\n  configureServer(server) {\n    server.middlewares.use(async (req, res, next) => {\n      if (\n        req.headers[\"sec-fetch-dest\"] !== \"document\" ||\n        req.headers[\"sec-fetch-mode\"] !== \"navigate\"\n      ) {\n        next();\n        return;\n      }\n\n      // Capture the original response\n      const originalWrite = res.write;\n      const originalEnd = res.end;\n\n      const buffers: Buffer[] = [];\n\n      res.write = (chunk) => {\n        buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n        return true;\n      };\n\n      res.end = (chunk) => {\n        if (chunk) {\n          buffers.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n        }\n\n        const body = Buffer.concat(buffers).toString(\"utf8\");\n        const response = new Response(body);\n\n        const metasSet = new Set<string>();\n        let hasTitle = false;\n        let hasCanonicalLink = false;\n\n        const rewriter = new HTMLRewriter()\n          .on(\"meta\", {\n            element(element) {\n              const propertyOrName =\n                element.getAttribute(\"property\") ||\n                element.getAttribute(\"name\");\n\n              if (propertyOrName === null) {\n                return;\n              }\n\n              if (propertyOrName === \"viewport\") {\n                // Allow \"viewport\" property deduplication\n                return;\n              }\n\n              if (metasSet.has(propertyOrName)) {\n                console.info(\n                  `Duplicate meta with name|property = ${propertyOrName} removed`\n                );\n                element.remove();\n                return;\n              }\n\n              metasSet.add(propertyOrName);\n            },\n          })\n          .on(\"title\", {\n            element(element) {\n              if (hasTitle) {\n                console.info(`Duplicate title removed`);\n                element.remove();\n                return;\n              }\n\n              hasTitle = true;\n            },\n          })\n          .on('link[rel=\"canonical\"]', {\n            element(element) {\n              if (hasCanonicalLink) {\n                console.info(`Duplicate link rel canonical removed`);\n                element.remove();\n                return;\n              }\n\n              hasCanonicalLink = true;\n            },\n          });\n        rewriter\n          // @ts-ignore\n          .transform(response)\n          .text()\n          .then((cleanedHtml) => {\n            // Send the modified response\n            res.setHeader(\"Content-Length\", Buffer.byteLength(cleanedHtml));\n            originalWrite.call(res, cleanedHtml, \"utf-8\");\n            originalEnd.call(res, \"\", \"utf-8\");\n          });\n\n        return res;\n      };\n\n      next();\n    });\n  },\n};\n"
  },
  {
    "path": "fixtures/webstudio-features/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\", \"@webstudio-is/react-sdk/placeholder\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "fixtures/webstudio-features/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\n// @ts-ignore\nimport { reactRouter } from \"@react-router/dev/vite\";\n// @ts-ignore\nimport { dedupeMeta } from \"./proxy-emulator/dedupe-meta\";\nimport { existsSync } from \"fs\";\n// @ts-ignore\nimport path from \"path\";\n// @ts-ignore\nimport fg from \"fast-glob\";\n\nconst rootDir = [\"..\", \"../..\", \"../../..\"]\n  .map((dir) => path.join(__dirname, dir))\n  .find((dir) => existsSync(path.join(dir, \".git\")));\n\nconst hasPrivateFolders =\n  fg.sync([path.join(rootDir ?? \"\", \"packages/*/private-src/*\")], {\n    ignore: [\"**/node_modules/**\"],\n  }).length > 0;\n\nconst conditions = hasPrivateFolders\n  ? [\"webstudio-private\", \"webstudio\"]\n  : [\"webstudio\"];\n\nexport default defineConfig({\n  resolve: {\n    conditions: [...conditions, \"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [...conditions, \"node\", \"development|production\"],\n    },\n  },\n  plugins: [reactRouter(), dedupeMeta],\n});\n"
  },
  {
    "path": "https/README.md",
    "content": "# Admin only\n\nBased on this article https://dev.to/istarkov/fast-and-easy-way-to-setup-web-developer-certificates-450e\n\n```bash\nsudo rm -rf /tmp/certbot/\nsudo rm -rf /tmp/letsencrypt/\n\nmkdir -p /tmp/certbot/\nmkdir -p /tmp/letsencrypt/\n\ninfisical login\n# When running infisical init, select: Webstudio > webstudio\ninfisical init\n\nCLOUDFLARE_API_KEY=$(infisical secrets get WSTD_DEV-CLOUDFLARE_ZONE_TOKEN --path='/CLI' --env=staging --plain)\n\n# Create cloudflare.ini with proper formatting and permissions\necho \"dns_cloudflare_api_token = ${CLOUDFLARE_API_KEY}\" > /tmp/certbot/cloudflare.ini\nchmod 600 /tmp/certbot/cloudflare.ini\n\ndocker run -it --rm --name certbot  \\\n-v \"/tmp/letsencrypt/data:/etc/letsencrypt\" \\\n-v \"/tmp/certbot:/local/certbot\" \\\ncertbot/dns-cloudflare certonly \\\n--dns-cloudflare \\\n--dns-cloudflare-credentials /local/certbot/cloudflare.ini \\\n--agree-tos \\\n--noninteractive \\\n-m istarkov@gmail.com \\\n-d wstd.dev \\\n-d '*.wstd.dev'\n\nsudo chown -R $USER:$(id -g) /tmp/letsencrypt\n\ncp /tmp/letsencrypt/data/live/wstd.dev/fullchain.pem ./https/fullchain.pem\ncp /tmp/letsencrypt/data/live/wstd.dev/privkey.pem ./https/privkey.pem\n\n# Haproxy key\ncd https\ncat ./fullchain.pem ./privkey.pem > ./haproxy.pem\n```\n"
  },
  {
    "path": "https/fullchain.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDfjCCAwSgAwIBAgISBgyjDDPAKcgnnNk47aZQVw3VMAoGCCqGSM49BAMDMDIx\nCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF\nNzAeFw0yNjAyMjgyMTMzNDJaFw0yNjA1MjkyMTMzNDFaMBMxETAPBgNVBAMTCHdz\ndGQuZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2LN7eMuYSiZgnBF0xxWA\ncK1Plfq2ngRx6gI1/HA+myke9Ptofw8gVu7jXT+rpzQ5js29cZITQsb1fNnyBPgG\nk6OCAhcwggITMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAM\nBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQJFhJvlmicmlvTt73i8XNe35skYzAfBgNV\nHSMEGDAWgBSuSJ7chx1EoG/aouVgdAR4wpwAgDAyBggrBgEFBQcBAQQmMCQwIgYI\nKwYBBQUHMAKGFmh0dHA6Ly9lNy5pLmxlbmNyLm9yZy8wHwYDVR0RBBgwFoIKKi53\nc3RkLmRldoIId3N0ZC5kZXYwEwYDVR0gBAwwCjAIBgZngQwBAgEwLQYDVR0fBCYw\nJDAioCCgHoYcaHR0cDovL2U3LmMubGVuY3Iub3JnLzg2LmNybDCCAQMGCisGAQQB\n1nkCBAIEgfQEgfEA7wB1AEmcm2neHXzs/DbezYdkprhbrwqHgBnRVVL76esp3fjD\nAAABnKZhe6oAAAQDAEYwRAIgPZ+E37Q00lqDHH4R4WHpEqTc3DuYG1reWCW0GQ1i\n0h4CIFPOXeO8PZ8pVIg51joZBaWrRVmQLR6Ezb8sXgWsg97fAHYAyzj3FYl8hKFE\nX1vB3fvJbvKaWc1HCmkFhbDLFMMUWOcAAAGcpmF7tQAABAMARzBFAiAHmBDky+q4\n22k20Y7CbtzDSXWusolNkPBWpQOx3nRSTwIhAKDWWWzSXCoxFjRKXEeHa1GbobsJ\n1lACLvo88FYtxuLlMAoGCCqGSM49BAMDA2gAMGUCMQD5a0mv4rjV/mjdAkGokBuc\n0uvCBVZ0LCPXNq+/eqswoGReKpK7PC14bgGzRAJwASUCMH0Sjhh4ikIDieaykpAV\nfMTr8Hf+zb5WZQcvni9cKqwxHrBaXg2aAenDJ95TOnmPvg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw\nWhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\nRW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST\nCFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef\nQHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw\ngfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\nATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4\nwpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB\nAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g\nBAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu\nY3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD\naEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF\nh4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG\nyM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr\nOIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o\nyVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S\nM6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ\nUXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq\nPe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I\ntu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ\nYRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty\n+VUwFj9tmWxyR/M=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "https/haproxy.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDfjCCAwSgAwIBAgISBgyjDDPAKcgnnNk47aZQVw3VMAoGCCqGSM49BAMDMDIx\nCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF\nNzAeFw0yNjAyMjgyMTMzNDJaFw0yNjA1MjkyMTMzNDFaMBMxETAPBgNVBAMTCHdz\ndGQuZGV2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2LN7eMuYSiZgnBF0xxWA\ncK1Plfq2ngRx6gI1/HA+myke9Ptofw8gVu7jXT+rpzQ5js29cZITQsb1fNnyBPgG\nk6OCAhcwggITMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAM\nBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQJFhJvlmicmlvTt73i8XNe35skYzAfBgNV\nHSMEGDAWgBSuSJ7chx1EoG/aouVgdAR4wpwAgDAyBggrBgEFBQcBAQQmMCQwIgYI\nKwYBBQUHMAKGFmh0dHA6Ly9lNy5pLmxlbmNyLm9yZy8wHwYDVR0RBBgwFoIKKi53\nc3RkLmRldoIId3N0ZC5kZXYwEwYDVR0gBAwwCjAIBgZngQwBAgEwLQYDVR0fBCYw\nJDAioCCgHoYcaHR0cDovL2U3LmMubGVuY3Iub3JnLzg2LmNybDCCAQMGCisGAQQB\n1nkCBAIEgfQEgfEA7wB1AEmcm2neHXzs/DbezYdkprhbrwqHgBnRVVL76esp3fjD\nAAABnKZhe6oAAAQDAEYwRAIgPZ+E37Q00lqDHH4R4WHpEqTc3DuYG1reWCW0GQ1i\n0h4CIFPOXeO8PZ8pVIg51joZBaWrRVmQLR6Ezb8sXgWsg97fAHYAyzj3FYl8hKFE\nX1vB3fvJbvKaWc1HCmkFhbDLFMMUWOcAAAGcpmF7tQAABAMARzBFAiAHmBDky+q4\n22k20Y7CbtzDSXWusolNkPBWpQOx3nRSTwIhAKDWWWzSXCoxFjRKXEeHa1GbobsJ\n1lACLvo88FYtxuLlMAoGCCqGSM49BAMDA2gAMGUCMQD5a0mv4rjV/mjdAkGokBuc\n0uvCBVZ0LCPXNq+/eqswoGReKpK7PC14bgGzRAJwASUCMH0Sjhh4ikIDieaykpAV\nfMTr8Hf+zb5WZQcvni9cKqwxHrBaXg2aAenDJ95TOnmPvg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEVzCCAj+gAwIBAgIRAKp18eYrjwoiCWbTi7/UuqEwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw\nWhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg\nRW5jcnlwdDELMAkGA1UEAxMCRTcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARB6AST\nCFh/vjcwDMCgQer+VtqEkz7JANurZxLP+U9TCeioL6sp5Z8VRvRbYk4P1INBmbef\nQHJFHCxcSjKmwtvGBWpl/9ra8HW0QDsUaJW2qOJqceJ0ZVFT3hbUHifBM/2jgfgw\ngfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD\nATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSuSJ7chx1EoG/aouVgdAR4\nwpwAgDAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB\nAQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g\nBAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu\nY3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAjx66fDdLk5ywFn3CzA1w1qfylHUD\naEf0QZpXcJseddJGSfbUUOvbNR9N/QQ16K1lXl4VFyhmGXDT5Kdfcr0RvIIVrNxF\nh4lqHtRRCP6RBRstqbZ2zURgqakn/Xip0iaQL0IdfHBZr396FgknniRYFckKORPG\nyM3QKnd66gtMst8I5nkRQlAg/Jb+Gc3egIvuGKWboE1G89NTsN9LTDD3PLj0dUMr\nOIuqVjLB8pEC6yk9enrlrqjXQgkLEYhXzq7dLafv5Vkig6Gl0nuuqjqfp0Q1bi1o\nyVNAlXe6aUXw92CcghC9bNsKEO1+M52YY5+ofIXlS/SEQbvVYYBLZ5yeiglV6t3S\nM6H+vTG0aP9YHzLn/KVOHzGQfXDP7qM5tkf+7diZe7o2fw6O7IvN6fsQXEQQj8TJ\nUXJxv2/uJhcuy/tSDgXwHM8Uk34WNbRT7zGTGkQRX0gsbjAea/jYAoWv0ZvQRwpq\nPe79D/i7Cep8qWnA+7AE/3B3S/3dEEYmc0lpe1366A/6GEgk3ktr9PEoQrLChs6I\ntu3wnNLB2euC8IKGLQFpGtOO/2/hiAKjyajaBP25w1jF0Wl8Bbqne3uZ2q1GyPFJ\nYRmT7/OXpmOH/FVLtwS+8ng1cAmpCujPwteJZNcDG0sF2n/sc0+SQf49fdyUK0ty\n+VUwFj9tmWxyR/M=\n-----END CERTIFICATE-----\n-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/X+VrbTOPtMwBgCQ\n8OzI+Kn4CETfSmi9awZf85igwq2hRANCAATYs3t4y5hKJmCcEXTHFYBwrU+V+rae\nBHHqAjX8cD6bKR70+2h/DyBW7uNdP6unNDmOzb1xkhNCxvV82fIE+AaT\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "https/haproxy.sh",
    "content": "#!/bin/bash\n\n# Proxies https://wstd.dev:4001 to http://localhost:4002\n# Used for debugging when HTTPS is required\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPEM_PATH=\"$SCRIPT_DIR/haproxy.pem\"\nTMP_CFG=$(mktemp /tmp/haproxy.XXXXXX)\n\necho $TMP_CFG\n\ncat <<EOF > \"$TMP_CFG\"\nfrontend proxy-https\n  bind *:4001 ssl crt ${PEM_PATH} alpn h2,http/1.1\n  default_backend proxy\n\nbackend proxy\n  balance roundrobin\n  mode http\n  http-request set-header X-Forwarded-Host %[req.hdr(Host)]\n  server  rgw1 localhost:4002 check\nEOF\n\nexec haproxy -f \"$TMP_CFG\" -db\n"
  },
  {
    "path": "https/privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg/X+VrbTOPtMwBgCQ\n8OzI+Kn4CETfSmi9awZf85igwq2hRANCAATYs3t4y5hKJmCcEXTHFYBwrU+V+rae\nBHHqAjX8cD6bKR70+2h/DyBW7uNdP6unNDmOzb1xkhNCxvV82fIE+AaT\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "lostpixel.config.js",
    "content": "import * as process from \"process\";\n\nexport const config = {\n  lostPixelProjectId: \"cleiive6c0gchi40em3uzx1xv\",\n  apiKey: process.env.LOST_PIXEL_API_KEY,\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"packageManager\": \"pnpm@9.14.4\",\n  \"name\": \"webstudio-root\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"pnpm -r --filter='!./fixtures/*' build\",\n    \"dts\": \"pnpm -r dts\",\n    \"dev\": \"pnpm --filter='@webstudio-is/builder' dev\",\n    \"lint\": \"oxlint --deny-warnings\",\n    \"fixtures\": \"pnpm -r fixtures:link && pnpm -r fixtures:sync && pnpm -r fixtures:build\",\n    \"checks\": \"pnpm -r test && pnpm -r typecheck && pnpm lint && pnpm fixtures\",\n    \"playwright\": \"pnpm -r playwright-init\",\n    \"format\": \"prettier --write \\\"**/*.{ts,tsx,md}\\\"\",\n    \"storybook:dev\": \"storybook dev -p 6006\",\n    \"storybook:build\": \"storybook build\",\n    \"migrations\": \"pnpm --filter=@webstudio-is/prisma-client generate && pnpm --filter=./packages/prisma-client migrations --dev --cwd ../../apps/builder\",\n    \"build-figma-tokens\": \"cd packages/design-system && pnpm build-figma-tokens\",\n    \"prepare\": \"which git && git config core.hooksPath .git/hooks/ && simple-git-hooks || echo git not installed\",\n    \"local:version-snapshot\": \"pnpm -r exec pnpm version prepatch --preid $(cat /dev/urandom | LC_ALL=C tr -dc 'a-z' | fold -w 8 | head -n 1)\",\n    \"local:publish-snapshot\": \"pnpm -r publish --access public --no-git-checks --registry http://localhost:4873\",\n    \"local:dangerously-undo-version-snapshot\": \"git restore --source=$(git branch --show-current) '**/*/package.json'\",\n    \"local:release\": \"pnpm build && pnpm dts && pnpm local:version-snapshot && pnpm local:publish-snapshot && pnpm local:dangerously-undo-version-snapshot && echo \\\"now execute\\npnpm up -r -L '@webstudio-is/*' --registry http://localhost:4873\\\"\"\n  },\n  \"simple-git-hooks\": {\n    \"pre-commit\": \"./node_modules/.bin/nano-staged\"\n  },\n  \"devDependencies\": {\n    \"@fontsource-variable/inter\": \"^5.0.20\",\n    \"@fontsource-variable/manrope\": \"^5.0.20\",\n    \"@fontsource/roboto-mono\": \"^5.0.18\",\n    \"@radix-ui/react-tooltip\": \"^1.2.4\",\n    \"@storybook/addon-actions\": \"^8.6.4\",\n    \"@storybook/addon-backgrounds\": \"^8.6.4\",\n    \"@storybook/addon-controls\": \"^8.6.4\",\n    \"@storybook/react\": \"^8.6.4\",\n    \"@storybook/react-vite\": \"^8.6.4\",\n    \"@types/node\": \"^22.13.10\",\n    \"@types/react\": \"^18.2.70\",\n    \"@typescript/native-preview\": \"7.0.0-dev.20260120.1\",\n    \"esbuild\": \"^0.25.3\",\n    \"nano-staged\": \"^0.8.0\",\n    \"oxlint\": \"^1.56.0\",\n    \"prettier\": \"3.5.3\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"simple-git-hooks\": \"^2.11.1\",\n    \"storybook\": \"^8.6.4\",\n    \"tsx\": \"^4.19.3\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\"\n  },\n  \"engines\": {\n    \"node\": \"22\",\n    \"pnpm\": \"^9.14.0\",\n    \"yarn\": \"This project is configured to use pnpm\"\n  },\n  \"nano-staged\": {\n    \"*.{ts,tsx,js,json,css,md}\": \"prettier --write\"\n  },\n  \"prettier\": {\n    \"trailingComma\": \"es5\",\n    \"overrides\": [\n      {\n        \"files\": [\n          \"*.ts\",\n          \"*.tsx\"\n        ],\n        \"options\": {\n          \"parser\": \"babel-ts\"\n        }\n      }\n    ]\n  },\n  \"pnpm\": {\n    \"overrides\": {\n      \"@types/react\": \"^18.2.70\",\n      \"@types/react-dom\": \"^18.2.25\",\n      \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n      \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n    },\n    \"patchedDependencies\": {\n      \"@stitches/react@1.3.1-1\": \"patches/@stitches__react@1.3.1-1.patch\",\n      \"@radix-ui/react-scroll-area@1.0.5\": \"patches/@radix-ui__react-scroll-area@1.0.5.patch\",\n      \"@remix-run/dev\": \"patches/@remix-run__dev.patch\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/asset-uploader/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/asset-uploader/README.md",
    "content": "# Webstudio Asset Uploader\n\nThe asset uploader is the packages that handles all the logic for our assets upload\n"
  },
  {
    "path": "packages/asset-uploader/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/asset-uploader\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Asset Uploader\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit -p tsconfig.typecheck.json\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@aws-crypto/sha256-js\": \"^5.2.0\",\n    \"@smithy/signature-v4\": \"^5.1.3\",\n    \"@webstudio-is/fonts\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"fontkit\": \"^2.0.4\",\n    \"image-meta\": \"^0.2.1\",\n    \"immer\": \"^10.1.1\",\n    \"nanoid\": \"^5.1.5\",\n    \"warn-once\": \"^0.1.1\"\n  },\n  \"devDependencies\": {\n    \"@types/fontkit\": \"^2.0.8\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"vitest\": \"^3.1.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\"\n    },\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/asset-uploader/src/client.ts",
    "content": "import type { AssetData } from \"./utils/get-asset-data\";\n\nexport type AssetClient = {\n  uploadFile: (\n    name: string,\n    type: string,\n    data: AsyncIterable<Uint8Array>,\n    assetInfoFallback:\n      | { width: number; height: number; format: string }\n      | undefined\n  ) => Promise<AssetData>;\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/clients/fs/fs.ts",
    "content": "import type { AssetClient } from \"../../client\";\nimport { uploadToFs } from \"./upload\";\n\ntype FsClientOptions = {\n  fileDirectory: string;\n  maxUploadSize: number;\n};\n\nexport const createFsClient = (options: FsClientOptions): AssetClient => {\n  return {\n    uploadFile: (name, type, data) =>\n      uploadToFs({\n        name,\n        type,\n        data,\n        maxSize: options.maxUploadSize,\n        fileDirectory: options.fileDirectory,\n      }),\n  };\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/clients/fs/upload.ts",
    "content": "import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { buffer } from \"node:stream/consumers\";\nimport { type AssetData, getAssetData } from \"../../utils/get-asset-data\";\nimport { createSizeLimiter } from \"../../utils/size-limiter\";\n\nexport const uploadToFs = async ({\n  name,\n  type,\n  data: dataStream,\n  maxSize,\n  fileDirectory,\n}: {\n  name: string;\n  type: string;\n  data: AsyncIterable<Uint8Array>;\n  maxSize: number;\n  fileDirectory: string;\n}): Promise<AssetData> => {\n  const filepath = resolve(fileDirectory, name);\n\n  await mkdir(dirname(filepath), { recursive: true }).catch(() => {});\n  const limitSize = createSizeLimiter(maxSize, name);\n\n  const data = await buffer(limitSize(dataStream));\n  await writeFile(filepath, data);\n\n  const assetData = await getAssetData({\n    type: type.startsWith(\"image\")\n      ? \"image\"\n      : type === \"font\"\n        ? \"font\"\n        : \"file\",\n    size: data.byteLength,\n    data,\n    name,\n  });\n\n  return assetData;\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/clients/s3/s3.ts",
    "content": "import { Sha256 } from \"@aws-crypto/sha256-js\";\nimport { SignatureV4 } from \"@smithy/signature-v4\";\nimport type { AssetClient } from \"../../client\";\nimport { uploadToS3 } from \"./upload\";\n\ntype S3ClientOptions = {\n  endpoint: string;\n  region: string;\n  accessKeyId: string;\n  secretAccessKey: string;\n  bucket: string;\n  acl?: string;\n  maxUploadSize: number;\n};\n\nexport const createS3Client = (options: S3ClientOptions): AssetClient => {\n  const signer = new SignatureV4({\n    credentials: {\n      accessKeyId: options.accessKeyId,\n      secretAccessKey: options.secretAccessKey,\n    },\n    region: options.region,\n    service: \"s3\",\n    sha256: Sha256,\n    // should never be enabled when work with s3\n    uriEscapePath: false,\n  });\n\n  const uploadFile: AssetClient[\"uploadFile\"] = async (\n    name,\n    type,\n    data,\n    assetInfoFallback\n  ) => {\n    return uploadToS3({\n      signer,\n      name,\n      type,\n      data,\n      maxSize: options.maxUploadSize,\n      endpoint: options.endpoint,\n      bucket: options.bucket,\n      acl: options.acl,\n      assetInfoFallback,\n    });\n  };\n\n  return {\n    uploadFile,\n  };\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/clients/s3/upload.ts",
    "content": "import { arrayBuffer } from \"node:stream/consumers\";\nimport type { SignatureV4 } from \"@smithy/signature-v4\";\nimport { type AssetData, getAssetData } from \"../../utils/get-asset-data\";\nimport { createSizeLimiter } from \"../../utils/size-limiter\";\nimport { extendedEncodeURIComponent } from \"../../utils/sanitize-s3-key\";\nimport { getMimeTypeByFilename } from \"@webstudio-is/sdk\";\n\nexport const uploadToS3 = async ({\n  signer,\n  name,\n  type,\n  data: dataStream,\n  maxSize,\n  endpoint,\n  bucket,\n  acl,\n  assetInfoFallback,\n}: {\n  signer: SignatureV4;\n  name: string;\n  type: string;\n  data: AsyncIterable<Uint8Array>;\n  maxSize: number;\n  endpoint: string;\n  bucket: string;\n  acl?: string;\n  assetInfoFallback:\n    | { width: number; height: number; format: string }\n    | undefined;\n}): Promise<AssetData> => {\n  const limitSize = createSizeLimiter(maxSize, name);\n\n  // @todo this is going to put the entire file in memory\n  // this has to be a stream that goes directly to s3\n  // Size check has to happen as you stream and interrupted when size is too big\n  // Also check if S3 client has an option to check the size limit\n  const data = await arrayBuffer(limitSize(dataStream));\n\n  const url = new URL(\n    `/${bucket}/${extendedEncodeURIComponent(name)}`,\n    endpoint\n  );\n\n  // Use proper MIME type based on file extension instead of generic type category\n  const contentType = getMimeTypeByFilename(name);\n\n  const s3Request = await signer.sign({\n    method: \"PUT\",\n    protocol: url.protocol,\n    hostname: url.hostname,\n    path: url.pathname,\n    headers: {\n      \"x-amz-date\": new Date().toISOString(),\n      \"Content-Type\": contentType,\n      \"Content-Length\": `${data.byteLength}`,\n      \"Cache-Control\": \"public, max-age=31536004,immutable\",\n      \"x-amz-content-sha256\": \"UNSIGNED-PAYLOAD\",\n      // encodeURIComponent is needed to support special characters like Cyrillic\n      \"x-amz-meta-filename\": encodeURIComponent(name),\n      // when no ACL passed we do not default since some providers do not support it\n      ...(acl ? { \"x-amz-acl\": acl } : {}),\n    },\n    body: data,\n  });\n\n  const response = await fetch(url, {\n    method: s3Request.method,\n    headers: s3Request.headers,\n    body: data,\n  });\n\n  if (response.status !== 200) {\n    throw Error(`Cannot upload file ${name}`);\n  }\n\n  if (type.startsWith(\"video\") && assetInfoFallback !== undefined) {\n    return {\n      size: data.byteLength,\n      format: assetInfoFallback?.format,\n      meta: {\n        width: assetInfoFallback?.width ?? 0,\n        height: assetInfoFallback?.height ?? 0,\n      },\n    };\n  }\n\n  const assetData = await getAssetData({\n    type: type.startsWith(\"image\")\n      ? \"image\"\n      : type === \"font\"\n        ? \"font\"\n        : \"file\",\n    size: data.byteLength,\n    data: new Uint8Array(data),\n    name,\n  });\n\n  return assetData;\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/constants.ts",
    "content": "// By default using Vercel's limit https://vercel.com/docs/concepts/limits/overview#serverless-function-payload-size-limit\nexport const MAX_UPLOAD_SIZE = \"4.5\";\n"
  },
  {
    "path": "packages/asset-uploader/src/db/index.ts",
    "content": "export * from \"./load\";\n"
  },
  {
    "path": "packages/asset-uploader/src/db/load.ts",
    "content": "import {\n  authorizeProject,\n  type AppContext,\n  AuthorizationError,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport { formatAsset } from \"../utils/format-asset\";\n\nexport const loadAssetsByProject = async (\n  projectId: string,\n  context: AppContext,\n  { skipPermissionsCheck = false }: { skipPermissionsCheck?: boolean } = {}\n): Promise<Asset[]> => {\n  const canRead =\n    skipPermissionsCheck ||\n    (await authorizeProject.hasProjectPermit(\n      { projectId, permit: \"view\" },\n      context\n    ));\n\n  if (canRead === false) {\n    throw new AuthorizationError(\n      \"You don't have access to this project assets\"\n    );\n  }\n\n  const assets = await context.postgrest.client\n    .from(\"Asset\")\n    // use inner to filter out assets without file\n    // when file is not uploaded\n    .select(\n      `\n        assetId:id,\n        projectId,\n        filename,\n        description,\n        file:File!inner (*)\n      `\n    )\n    .eq(\"projectId\", projectId)\n    .eq(\"file.status\", \"UPLOADED\")\n    // always sort by primary key to get stable list\n    // required to not break fixtures\n    .order(\"id\");\n\n  const result: Asset[] = [];\n  for (const {\n    assetId,\n    projectId,\n    filename,\n    description,\n    file,\n  } of assets.data ?? []) {\n    if (file) {\n      result.push(\n        formatAsset({ assetId, projectId, filename, description, file })\n      );\n    }\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/delete.ts",
    "content": "import {\n  authorizeProject,\n  type AppContext,\n  AuthorizationError,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport type { Asset } from \"@webstudio-is/sdk\";\n\nexport const deleteAssets = async (\n  props: {\n    ids: Array<Asset[\"id\"]>;\n    projectId: string;\n  },\n  context: AppContext\n): Promise<void> => {\n  const canDelete = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"edit\" },\n    context\n  );\n\n  if (canDelete === false) {\n    throw new AuthorizationError(\n      \"You don't have access to delete this project assets\"\n    );\n  }\n\n  const assets = await context.postgrest.client\n    .from(\"Asset\")\n    .select(\n      `\n        id,\n        projectId,\n        name,\n        file:File!inner (*)\n      `\n    )\n    .in(\"id\", props.ids)\n    .eq(\"projectId\", props.projectId);\n\n  if ((assets.data ?? []).length === 0) {\n    throw new Error(\"Assets not found\");\n  }\n\n  await context.postgrest.client\n    .from(\"Project\")\n    .update({ previewImageAssetId: null })\n    .eq(\"id\", props.projectId)\n    .in(\"previewImageAssetId\", props.ids);\n\n  await context.postgrest.client\n    .from(\"Asset\")\n    .delete()\n    .in(\"id\", props.ids)\n    .eq(\"projectId\", props.projectId);\n\n  // find unused files\n  const unusedFileNames = new Set(assets.data?.map((asset) => asset.name));\n  const assetsByStillUsedFileName = await context.postgrest.client\n    .from(\"Asset\")\n    .select(\"name\")\n    .in(\"name\", Array.from(unusedFileNames));\n  for (const asset of assetsByStillUsedFileName.data ?? []) {\n    unusedFileNames.delete(asset.name);\n  }\n\n  // delete unused files\n  await context.postgrest.client\n    .from(\"File\")\n    .update({ isDeleted: true })\n    .in(\"name\", Array.from(unusedFileNames));\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/index.server.ts",
    "content": "export * from \"./db\";\nexport * from \"./upload\";\nexport * from \"./delete\";\nexport * from \"./patch\";\nexport * from \"./clients/fs/fs\";\nexport * from \"./clients/s3/s3\";\n"
  },
  {
    "path": "packages/asset-uploader/src/index.ts",
    "content": "export * from \"./types\";\nexport * from \"./schema\";\nexport * from \"./constants\";\nexport * from \"./utils/to-bytes\";\nexport * from \"./utils/sanitize-s3-key\";\n"
  },
  {
    "path": "packages/asset-uploader/src/patch.ts",
    "content": "import { type Patch, applyPatches } from \"immer\";\nimport {\n  type AppContext,\n  authorizeProject,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { type Asset, Assets } from \"@webstudio-is/sdk\";\nimport { deleteAssets } from \"./delete\";\nimport { loadAssetsByProject } from \"./db/load\";\n\n/**\n * patchAssets can only delete or add assets\n * update patches are ignored\n */\nexport const patchAssets = async (\n  { projectId }: { projectId: string },\n  patches: Array<Patch>,\n  context: AppContext\n): Promise<void> => {\n  const canEdit = await authorizeProject.hasProjectPermit(\n    { projectId, permit: \"edit\" },\n    context\n  );\n  if (canEdit === false) {\n    throw new Error(\"You don't have edit access to this project\");\n  }\n\n  const assetsList = await loadAssetsByProject(projectId, context);\n  const assets = new Map<Asset[\"id\"], Asset>();\n  for (const asset of assetsList) {\n    assets.set(asset.id, asset);\n  }\n  const patchedAssets = applyPatches(assets, patches);\n  // validate assets without recreating objects\n  // we expect referencial equality to find updated assets\n  Assets.parse(patchedAssets);\n\n  // delete assets no longer existing in patched version\n  const deletedAssetIds: Asset[\"id\"][] = [];\n  for (const assetId of assets.keys()) {\n    if (patchedAssets.has(assetId) === false) {\n      deletedAssetIds.push(assetId);\n    }\n  }\n  if (deletedAssetIds.length !== 0) {\n    deleteAssets({ projectId, ids: deletedAssetIds }, context);\n  }\n\n  // update assets\n  for (const asset of assets.values()) {\n    const patchedAsset = patchedAssets.get(asset.id);\n    if (asset !== patchedAsset && patchedAsset) {\n      const { filename, description } = patchedAsset;\n      await context.postgrest.client\n        .from(\"Asset\")\n        .update({ filename, description })\n        .eq(\"id\", asset.id)\n        .eq(\"projectId\", asset.projectId);\n    }\n  }\n\n  // add new assets found in patched version\n  const addedAssets: Asset[] = [];\n  for (const [assetId, asset] of patchedAssets) {\n    if (assets.has(assetId) === false) {\n      addedAssets.push(asset);\n    }\n  }\n  if (addedAssets.length !== 0) {\n    const files = await context.postgrest.client\n      .from(\"File\")\n      .select()\n      .in(\n        \"name\",\n        addedAssets.map((asset) => asset.name)\n      );\n\n    const fileNames = new Set(files.data?.map((file) => file.name));\n\n    // restore file when undo is triggered\n    await context.postgrest.client\n      .from(\"File\")\n      .update({ isDeleted: false })\n      .in(\"name\", Array.from(fileNames));\n\n    await context.postgrest.client.from(\"Asset\").insert(\n      addedAssets\n        // making sure corresponding file exist before creating an asset that references it\n        .filter((asset) => fileNames.has(asset.name))\n        .map((asset) => ({\n          id: asset.id,\n          projectId,\n          name: asset.name,\n        }))\n    );\n  }\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/schema.ts",
    "content": "import { z } from \"zod\";\nimport { MAX_UPLOAD_SIZE } from \"./constants\";\nimport { toBytes } from \"./utils/to-bytes\";\n\nexport const MaxSize: z.ZodEffects<\n  z.ZodDefault<z.ZodString>,\n  number,\n  string | undefined\n> = z\n  .string()\n  .default(MAX_UPLOAD_SIZE)\n  // user inputs the max value in mb and we transform it to bytes\n  .transform(toBytes);\n\nexport const MaxAssets: z.ZodEffects<\n  z.ZodDefault<z.ZodString>,\n  number,\n  string | undefined\n> = z.string().default(\"50\").transform(Number.parseFloat);\n"
  },
  {
    "path": "packages/asset-uploader/src/types.ts",
    "content": "export type AssetType = \"image\" | \"font\" | \"video\" | \"file\";\n"
  },
  {
    "path": "packages/asset-uploader/src/upload.ts",
    "content": "import {\n  type AppContext,\n  authorizeProject,\n  AuthorizationError,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport type { Asset } from \"@webstudio-is/sdk\";\nimport type { AssetClient } from \"./client\";\nimport { getUniqueFilename } from \"./utils/get-unique-filename\";\nimport { sanitizeS3Key } from \"./utils/sanitize-s3-key\";\nimport { formatAsset } from \"./utils/format-asset\";\n\ntype UploadData = {\n  projectId: string;\n  type: string;\n  filename: string;\n  maxAssetsPerProject: number;\n};\n\nconst UPLOADING_STALE_TIMEOUT = 1000 * 60 * 30; // 30 minutes\n\nexport const createUploadName = async (\n  data: UploadData,\n  context: AppContext\n): Promise<string> => {\n  const { projectId, maxAssetsPerProject, type, filename } = data;\n  const canEdit = await authorizeProject.hasProjectPermit(\n    { projectId, permit: \"edit\" },\n    context\n  );\n  if (canEdit === false) {\n    throw new AuthorizationError(\n      \"You don't have access to create this project assets\"\n    );\n  }\n\n  /**\n   * sometimes for example on request timeout we don't know what happened to the \"UPLOADING\" asset,\n   * so we don't take into account assets with the \"UPLOADING\" status that were created more\n   * than UPLOADING_STALE_TIMEOUT milliseconds ago\n   **/\n\n  const uploadedCount = await context.postgrest.client\n    .from(\"File\")\n    .select(\"*\", { count: \"exact\", head: true })\n    .eq(\"isDeleted\", false)\n    .eq(\"uploaderProjectId\", projectId)\n    .eq(\"status\", \"UPLOADED\");\n\n  const uploadingCount = await context.postgrest.client\n    .from(\"File\")\n    .select(\"*\", { count: \"exact\", head: true })\n    .eq(\"isDeleted\", false)\n    .eq(\"uploaderProjectId\", projectId)\n    .eq(\"status\", \"UPLOADING\")\n    .gt(\n      \"createdAt\",\n      new Date(Date.now() - UPLOADING_STALE_TIMEOUT).toISOString()\n    );\n\n  const count = (uploadedCount.count ?? 0) + (uploadingCount.count ?? 0);\n\n  if (count >= maxAssetsPerProject) {\n    /**\n     * Here is right to write `Max ${MAX_ASSETS_PER_PROJECT}` but see the comment below,\n     * it's probable that the user can exceed the limit a little bit.\n     * So it can be a little bit strange that the limit is 5 but the user already has 7.\n     **/\n    throw new Error(\n      `The maximum number of assets per project is ${maxAssetsPerProject}.`\n    );\n  }\n\n  /**\n   * Create a temporary \"UPLOADING\" asset, so it can be counted in the next query\n   * Assumptions:\n   * - it's possible to create more assets than MAX_ASSETS_PER_PROJECT,\n   *   but for now we assume that the time since the `count` query above and the `create` query below is negligible,\n   *   and some kind of rate limiting exists on API.\n   * Also no locking exists in Prisma, and no raw query locking like\n   * \"SELECT id FROM \"Project\" where id=? FOR UPDATE;\" is shareable between sqlite and postgres.\n   **/\n  const name = getUniqueFilename(sanitizeS3Key(filename));\n\n  await context.postgrest.client.from(\"File\").insert({\n    name,\n    status: \"UPLOADING\",\n    // store content type in related field\n    format: type,\n    size: 0,\n    uploaderProjectId: projectId,\n  });\n  return name;\n};\n\nexport const uploadFile = async (\n  name: string,\n  data: ReadableStream<Uint8Array>,\n  client: AssetClient,\n  context: AppContext,\n  assetInfoFallback:\n    | { width: number; height: number; format: string }\n    | undefined\n): Promise<Asset> => {\n  let file = await context.postgrest.client\n    .from(\"File\")\n    .select(\"*\")\n    .eq(\"name\", name)\n    .eq(\"status\", \"UPLOADING\")\n    .gt(\n      \"createdAt\",\n      new Date(Date.now() - UPLOADING_STALE_TIMEOUT).toISOString()\n    )\n    .single();\n  if (file.data === null) {\n    throw Error(\"File already uploaded or url is expired\");\n  }\n\n  try {\n    const assetData = await client.uploadFile(\n      name,\n      file.data.format,\n      // global web streams types do not define ReadableStream as async iterable\n      data as unknown as AsyncIterable<Uint8Array>,\n      assetInfoFallback\n    );\n    const { meta, format, size } = assetData;\n    file = await context.postgrest.client\n      .from(\"File\")\n      .update({\n        size,\n        format,\n        meta: JSON.stringify(meta),\n        status: \"UPLOADED\",\n      })\n      .eq(\"name\", name)\n      .select()\n      .single();\n    if (file.data === null) {\n      throw Error(\"File not found\");\n    }\n    return formatAsset({\n      assetId: \"\",\n      projectId: file.data.uploaderProjectId as string,\n      filename: null,\n      description: null,\n      file: file.data,\n    });\n  } catch (error) {\n    await context.postgrest.client.from(\"File\").delete().eq(\"name\", name);\n\n    throw error;\n  }\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/font-data.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { parseSubfamily, __testing__ } from \"./font-data\";\n\nconst { normalizeFamily } = __testing__;\n\ndescribe(\"font-data\", () => {\n  describe(\"parseSubfamily()\", () => {\n    test(\"Black Italic\", () => {\n      expect(parseSubfamily(\"Black Italic\")).toEqual({\n        style: \"italic\",\n        weight: 900,\n      });\n    });\n    test(\"Bold\", () => {\n      expect(parseSubfamily(\"Bold\")).toEqual({\n        style: \"normal\",\n        weight: 700,\n      });\n    });\n    test(\"Demi Bold Italic\", () => {\n      expect(parseSubfamily(\"Demi Bold Italic\")).toEqual({\n        style: \"italic\",\n        weight: 600,\n      });\n    });\n    test(\"Light\", () => {\n      expect(parseSubfamily(\"Light\")).toEqual({\n        style: \"normal\",\n        weight: 300,\n      });\n    });\n    test(\"Extra Light\", () => {\n      expect(parseSubfamily(\"Extra Light\")).toEqual({\n        style: \"normal\",\n        weight: 200,\n      });\n    });\n    test(\"Extra Light Italic\", () => {\n      expect(parseSubfamily(\"Extra Light Italic\")).toEqual({\n        style: \"italic\",\n        weight: 200,\n      });\n    });\n    test(\"Heavy Italic\", () => {\n      expect(parseSubfamily(\"Heavy Italic\")).toEqual({\n        style: \"italic\",\n        weight: 900,\n      });\n    });\n    test(\"Medium Italic\", () => {\n      expect(parseSubfamily(\"Medium Italic\")).toEqual({\n        style: \"italic\",\n        weight: 500,\n      });\n    });\n  });\n\n  describe(\"normalizeFamily()\", () => {\n    test(\"basic\", () => {\n      expect(normalizeFamily(\"Roboto Black\", \"Black\", \"font.woff\")).toBe(\n        \"Roboto\"\n      );\n      expect(normalizeFamily(\"Roboto Light\", \"Light Italic\", \"font.woff\")).toBe(\n        \"Roboto\"\n      );\n      expect(normalizeFamily(\"Robolder Bold\", \"Bold\", \"font.woff\")).toBe(\n        \"Robolder\"\n      );\n      expect(normalizeFamily(\" Roboto X Bold \", \"Bold\", \"font.woff\")).toBe(\n        \"Roboto X\"\n      );\n      expect(normalizeFamily(\" 'Roboto X' Bold \", \"Bold\", \"font.woff\")).toBe(\n        \"'Roboto X'\"\n      );\n      expect(normalizeFamily(` \"Roboto X\" Bold `, \"Bold\", \"font.woff\")).toBe(\n        `\"Roboto X\"`\n      );\n      expect(normalizeFamily(`\"Roboto Bold\"`, \"Bold\", \"font.woff\")).toBe(\n        `\"Roboto Bold\"`\n      );\n      expect(normalizeFamily(`\"Roboto Bold\" Bold`, \"Bold\", \"font.woff\")).toBe(\n        `\"Roboto Bold\"`\n      );\n      expect(normalizeFamily(\"\", \"\", \"font.woff\")).toBe(`font`);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/font-data.ts",
    "content": "import { create as createFontKit } from \"fontkit\";\nimport {\n  fontWeights,\n  FONT_STYLES,\n  type FontFormat,\n  type FontWeight,\n  type VariationAxes,\n  type FontStyle,\n} from \"@webstudio-is/fonts\";\n\n// same default fontkit uses internally\nconst defaultLanguage = \"en\";\n\nexport const parseSubfamily = (\n  subfamily: string\n): { style: FontStyle; weight: number } => {\n  const subfamilyLow = subfamily.toLowerCase();\n  let style: FontStyle = \"normal\";\n  for (const possibleStyle of FONT_STYLES) {\n    if (subfamilyLow.includes(possibleStyle)) {\n      style = possibleStyle;\n      break;\n    }\n  }\n  let weight: FontWeight = \"400\";\n  for (weight in fontWeights) {\n    const { names } = fontWeights[weight];\n    if (names.some((name) => subfamilyLow.includes(name))) {\n      break;\n    }\n  }\n  return { style, weight: Number(weight) };\n};\n\nconst splitAndTrim = (string: string) =>\n  string\n    .split(\" \")\n    .map((part) => part.trim())\n    .filter(Boolean);\n\n// Family name can contain additional information like \"Roboto Black\" or \"Roboto Bold\", though we need pure family name \"Roboto\", because the rest is already encoded in weight and style.\n// We need a name we can reference in CSS font-family property, while CSS matches it with the right font-face considering the weight and style.\nconst normalizeFamily = (\n  family: string,\n  subfamily: string,\n  fileName: string\n) => {\n  const familyParts = splitAndTrim(family);\n  const subfamilyParts = splitAndTrim(subfamily.toLowerCase());\n  const familyPartsNormalized = familyParts.filter(\n    (familyPart) => subfamilyParts.includes(familyPart.toLowerCase()) === false\n  );\n  if (familyPartsNormalized.length !== 0) {\n    return familyPartsNormalized.join(\" \");\n  }\n  // Broken fonts may lack any family information, so last resort is to use the file name\n  const extensionIndex = fileName.lastIndexOf(\".\");\n  return extensionIndex === -1 ? fileName : fileName.slice(0, extensionIndex);\n};\n\ntype FontDataStatic = {\n  format: FontFormat;\n  family: string;\n  style: FontStyle;\n  weight: number;\n};\ntype FontDataVariable = {\n  format: FontFormat;\n  family: string;\n  variationAxes: VariationAxes;\n};\ntype FontData = FontDataStatic | FontDataVariable;\n\nexport const getFontData = (data: Uint8Array, fileName: string): FontData => {\n  const font = createFontKit(data as Buffer);\n  if (font.type !== \"TTF\" && font.type !== \"WOFF\" && font.type !== \"WOFF2\") {\n    throw Error(`Unsupported font type ${font.type}`);\n  }\n  const format = font.type.toLowerCase() as FontData[\"format\"];\n  const originalFamily = font.getName(\"fontFamily\", defaultLanguage) ?? \"\";\n  const subfamily =\n    font.getName(\"preferredSubfamily\", defaultLanguage) ??\n    font.getName(\"fontSubfamily\", defaultLanguage) ??\n    \"\";\n  const family = normalizeFamily(originalFamily, subfamily, fileName);\n  const isVariable = Object.keys(font.variationAxes).length !== 0;\n\n  if (isVariable) {\n    return {\n      format,\n      family,\n      variationAxes: font.variationAxes,\n    };\n  }\n\n  return {\n    format,\n    family,\n    ...parseSubfamily(subfamily),\n  };\n};\n\nexport const __testing__: {\n  normalizeFamily: (\n    family: string,\n    subfamily: string,\n    fileName: string\n  ) => string;\n} = { normalizeFamily };\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/format-asset.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { formatAsset } from \"./format-asset\";\n\ndescribe(\"formatAsset\", () => {\n  const baseParams = {\n    assetId: \"test-asset-id\",\n    projectId: \"test-project-id\",\n    filename: \"custom-filename\",\n    description: \"Test description\",\n  };\n\n  test(\"formats font asset correctly\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"Inter-Regular.woff2\",\n        format: \"woff2\",\n        description: null,\n        size: 50000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({\n          family: \"Inter\",\n          style: \"normal\",\n          weight: 400,\n        }),\n      },\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-asset-id\",\n      name: \"Inter-Regular.woff2\",\n      projectId: \"test-project-id\",\n      type: \"font\",\n      format: \"woff2\",\n      size: 50000,\n      meta: {\n        family: \"Inter\",\n        style: \"normal\",\n        weight: 400,\n      },\n    });\n  });\n\n  test(\"formats image asset with width and height correctly\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"photo.jpg\",\n        format: \"jpg\",\n        description: null,\n        size: 100000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({\n          width: 1920,\n          height: 1080,\n        }),\n      },\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-asset-id\",\n      name: \"photo.jpg\",\n      projectId: \"test-project-id\",\n      type: \"image\",\n      format: \"jpg\",\n      size: 100000,\n      meta: {\n        width: 1920,\n        height: 1080,\n      },\n    });\n  });\n\n  test(\"formats video asset with width and height as file type\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"video.mp4\",\n        format: \"mp4\",\n        description: null,\n        size: 5000000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({\n          width: 1920,\n          height: 1080,\n        }),\n      },\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-asset-id\",\n      name: \"video.mp4\",\n      projectId: \"test-project-id\",\n      type: \"file\",\n      format: \"mp4\",\n      size: 5000000,\n      meta: {},\n    });\n  });\n\n  test(\"formats webm video with width and height as file type\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"video.webm\",\n        format: \"webm\",\n        description: null,\n        size: 3000000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({\n          width: 1280,\n          height: 720,\n        }),\n      },\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-asset-id\",\n      name: \"video.webm\",\n      projectId: \"test-project-id\",\n      type: \"file\",\n      format: \"webm\",\n      size: 3000000,\n      meta: {},\n    });\n  });\n\n  test(\"formats audio file as file type\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"audio.mp3\",\n        format: \"mp3\",\n        description: null,\n        size: 2000000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({}),\n      },\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-asset-id\",\n      name: \"audio.mp3\",\n      projectId: \"test-project-id\",\n      type: \"file\",\n      format: \"mp3\",\n      size: 2000000,\n      meta: {},\n    });\n  });\n\n  test(\"formats document file as file type\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"document.pdf\",\n        format: \"pdf\",\n        description: null,\n        size: 500000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({}),\n      },\n    });\n\n    expect(result).toMatchObject({\n      id: \"test-asset-id\",\n      name: \"document.pdf\",\n      projectId: \"test-project-id\",\n      type: \"file\",\n      format: \"pdf\",\n      size: 500000,\n      meta: {},\n    });\n  });\n\n  test(\"handles null filename and description\", () => {\n    const result = formatAsset({\n      assetId: \"test-asset-id\",\n      projectId: \"test-project-id\",\n      filename: null,\n      description: null,\n      file: {\n        name: \"file.pdf\",\n        format: \"pdf\",\n        description: null,\n        size: 100000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({}),\n      },\n    });\n\n    expect(result.filename).toBeUndefined();\n    expect(result.description).toBeUndefined();\n  });\n\n  test(\"formats image without dimensions as file type\", () => {\n    const result = formatAsset({\n      ...baseParams,\n      file: {\n        name: \"image.png\",\n        format: \"png\",\n        description: null,\n        size: 50000,\n        createdAt: \"2024-01-01T00:00:00Z\",\n        meta: JSON.stringify({}),\n      },\n    });\n\n    expect(result).toMatchObject({\n      type: \"file\",\n      format: \"png\",\n      meta: {},\n    });\n  });\n});\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/format-asset.ts",
    "content": "import { type FontFormat, FontMeta, FONT_FORMATS } from \"@webstudio-is/fonts\";\nimport { type Asset, ImageMeta, detectAssetType } from \"@webstudio-is/sdk\";\n\nexport const formatAsset = ({\n  assetId,\n  projectId,\n  filename,\n  description,\n  file,\n}: {\n  assetId: string;\n  projectId: string;\n  filename: string | null;\n  description: string | null;\n  file: {\n    name: string;\n    format: string;\n    description: string | null;\n    size: number;\n    createdAt: string;\n    meta: string;\n  };\n}): Asset => {\n  const isFont = FONT_FORMATS.has(file.format as FontFormat);\n  const parsedMeta = JSON.parse(file.meta);\n\n  if (isFont) {\n    return {\n      id: assetId,\n      name: file.name,\n      projectId,\n      filename: filename ?? undefined,\n      description: description ?? undefined,\n      size: file.size,\n      type: \"font\",\n      createdAt: file.createdAt,\n      format: file.format as FontFormat,\n      meta: FontMeta.parse(parsedMeta),\n    };\n  }\n\n  // Detect actual asset type based on file extension\n  const detectedType = detectAssetType(file.name);\n\n  // Check if it's an image by verifying both metadata AND file extension\n  // Videos also have width/height but should not be treated as images\n  const isImage =\n    detectedType === \"image\" &&\n    parsedMeta &&\n    typeof parsedMeta.width === \"number\" &&\n    typeof parsedMeta.height === \"number\";\n\n  if (isImage) {\n    return {\n      id: assetId,\n      name: file.name,\n      projectId,\n      filename: filename ?? undefined,\n      description: description ?? undefined,\n      size: file.size,\n      type: \"image\",\n      format: file.format,\n      createdAt: file.createdAt,\n      meta: ImageMeta.parse(parsedMeta),\n    };\n  }\n\n  // Default to file type for everything else (including videos)\n  return {\n    id: assetId,\n    name: file.name,\n    projectId,\n    filename: filename ?? undefined,\n    description: description ?? undefined,\n    size: file.size,\n    type: \"file\",\n    format: file.format,\n    createdAt: file.createdAt,\n    meta: {},\n  };\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/get-asset-data.ts",
    "content": "import { z } from \"zod\";\nimport { imageMeta } from \"image-meta\";\nimport { FontMeta } from \"@webstudio-is/fonts\";\nimport { ImageMeta, validateFileName } from \"@webstudio-is/sdk\";\nimport { getFontData } from \"./font-data\";\n\nexport type AssetData = {\n  size: number;\n  format: string;\n  meta: ImageMeta | FontMeta | object;\n};\n\nexport const AssetData: z.ZodType<AssetData> = z.object({\n  size: z.number(),\n  format: z.string(),\n  meta: z.union([ImageMeta, FontMeta, z.object({})]),\n});\n\ntype BaseAssetOptions = {\n  size: number;\n  data: Uint8Array;\n  name: string;\n};\n\ntype AssetOptions =\n  | ({\n      type: \"image\";\n    } & BaseAssetOptions)\n  | ({ type: \"font\" } & BaseAssetOptions)\n  | ({ type: \"file\" } & BaseAssetOptions);\n\nexport const getAssetData = async (\n  options: AssetOptions\n): Promise<AssetData> => {\n  if (options.type === \"image\") {\n    let image: undefined | { format: string; width: number; height: number };\n    try {\n      const parsed = imageMeta(Buffer.from(options.data));\n      if (parsed.type) {\n        image = {\n          format: parsed.type,\n          // SVG images may not have explicit width/height dimensions\n          // (they use viewBox instead), so we default to 0 if missing\n          width: parsed.width ?? 0,\n          height: parsed.height ?? 0,\n        };\n      }\n    } catch {\n      // empty block\n    }\n    if (image === undefined) {\n      throw new Error(\"Unknown image format\");\n    }\n\n    const { format, width, height } = image;\n    return {\n      size: options.size,\n      format,\n      meta: { width, height },\n    };\n  }\n\n  if (options.type === \"font\") {\n    const { format, ...meta } = getFontData(options.data, options.name);\n\n    return {\n      size: options.size,\n      format,\n      meta,\n    };\n  }\n\n  // Validate file name and get extension\n  const { extension } = validateFileName(options.name);\n\n  return {\n    size: options.size,\n    format: extension,\n    meta: {},\n  };\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/get-unique-filename.ts",
    "content": "import * as path from \"node:path\";\nimport { nanoid } from \"nanoid\";\n\nexport const getUniqueFilename = (filename: string): string => {\n  const id = nanoid();\n  const extension = path.extname(filename);\n  const name = path.basename(filename, extension);\n  return `${name}_${id}${extension}`;\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/sanitize-s3-key.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { extendedEncodeURIComponent, sanitizeS3Key } from \"./sanitize-s3-key\";\n\ndescribe(\"sanitizeS3Key\", () => {\n  test(\"Should replace ASCII character ranges 00–1F hex (0–31 decimal) and 7F (127 decimal)\", () => {\n    let path = \"\";\n    for (let i = 0; i < 32; ++i) {\n      path += String.fromCharCode(i) + \"abc\";\n    }\n    path += String.fromCharCode(127);\n\n    expect(sanitizeS3Key(path)).toMatchInlineSnapshot(\n      `\"_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_abc_\"`\n    );\n  });\n\n  test(`Should replace \"&\" \"$\" \"@\" \"=\" \";\" \"/\" \":\" \"+\" \",\" \"?\"`, () => {\n    const path = `test&test$test@test=test;test/test:test+test,test?test`;\n    expect(sanitizeS3Key(path)).toMatchInlineSnapshot(\n      `\"test_test_test_test_test_test_test_test_test_test_test\"`\n    );\n  });\n\n  test(\"Non-printable ASCII characters (128–255 decimal characters)\", () => {\n    let path = \"\";\n    for (let i = 128; i < 256; ++i) {\n      path += String.fromCharCode(i) + \"a\";\n    }\n    path += String.fromCharCode(127);\n\n    expect(sanitizeS3Key(path)).toMatchInlineSnapshot(\n      `\"_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_\"`\n    );\n  });\n\n  test(`Should replace  \"\\\\\" \"{\" \"^\" \"}\" \"%\" \"\\`\" \"]\" \"'\" \"\"\" \"“\" \"”\" \">\" \"[\" \"~\"  \"<\" \"#\" \"|\"`, () => {\n    const path = `x\\\\x{x^x}x%x\\`x]x'x\"x“x”x>x[x~x<x#x|`;\n    expect(sanitizeS3Key(path)).toMatchInlineSnapshot(\n      `\"x_x_x_x_x_x_x_x_x_x_x_x_x_x_x_x_x_\"`\n    );\n  });\n\n  test(`Should do nothing with ordinary utf caracters`, () => {\n    const path = `hello-world-привет-мир-😀-😂ĊĴĈ`;\n    expect(sanitizeS3Key(path)).toMatch(path);\n  });\n});\n\ndescribe(\"extendedEncodeURIComponent\", () => {\n  const encodedValues: [string, string][] = [\n    [\"!\", \"%21\"],\n    [\"'\", \"%27\"],\n    [\"(\", \"%28\"],\n    [\")\", \"%29\"],\n    [\"*\", \"%2A\"],\n  ];\n\n  const verify = (table: [string, string][]) => {\n    test.each(table)(`encodes %s as %s`, (input, output) => {\n      expect(extendedEncodeURIComponent(input)).toStrictEqual(output);\n    });\n  };\n\n  verify(encodedValues);\n  verify([\n    encodedValues.reduce(\n      (acc, [input, output]) => [acc[0].concat(input), acc[1].concat(output)],\n      [\"\", \"\"]\n    ),\n  ]);\n});\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/sanitize-s3-key.ts",
    "content": "/**\n * Based on this: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html\n * Characters that might require special handling\n * ASCII character ranges 00–1F hex (0–31 decimal) and 7F (127 decimal)\n * \"&\",\"$\", \"@\", \"=\", \";\" \"/\", \":\", \"+\", Significant sequences of spaces might be lost in some uses (especially multiple spaces)\n * \",\", \"?\"\n * Characters to avoid\n * Non-printable ASCII characters (128–255 decimal characters)\n * \"\\\", \"{\", \"^\", \"}\", \"%\", \"`\", \"]\", ', \", “, ”, \">\", \"[\", \"~\",  \"<\", \"#\", \"|\"\n **/\n\n// eslint-disable-next-line no-control-regex\nconst specialRegexps = [/[\\x00-\\x1F\\x7F]+/g, /[&$@=;/:+\\s,?]+/g];\n\nconst avoidRegexps = [/[\\x80-\\xFF]+/g, /[\\\\{^}%`\\]'\"“”>[~<#|]+/g];\n\nconst allRegexps = [...specialRegexps, ...avoidRegexps];\n\nconst REPLACE_CHAR = \"_\";\n\n/**\n * Sanitize S3 key based on https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html\n * plz do not use this function for other purposes\n **/\nexport const sanitizeS3Key = (str: string): string => {\n  return allRegexps.reduce((r, reg) => r.replace(reg, REPLACE_CHAR), str);\n};\n\n/**\n * https://github.com/awslabs/smithy-typescript/blob/3d36329c52b44c48c269b962c25f2dc63cd01da6/packages/smithy-client/src/extended-encode-uri-component.ts\n *\n * Function that wraps encodeURIComponent to encode additional characters\n * to fully adhere to RFC 3986.\n *\n * https://datatracker.ietf.org/doc/html/rfc3986\n */\nexport const extendedEncodeURIComponent = (str: string): string => {\n  return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {\n    return \"%\" + c.charCodeAt(0).toString(16).toUpperCase();\n  });\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/size-limiter.ts",
    "content": "export const createSizeLimiter = (maxSize: number, name: string) => {\n  return async function* <T extends ArrayBufferView | ArrayBuffer>(\n    data: AsyncIterable<T>\n  ): AsyncGenerator<T> {\n    let size = 0;\n    for await (const chunk of data) {\n      size += chunk.byteLength;\n      if (size > maxSize) {\n        throw Error(`File \"${name}\" exceeded upload size of ${maxSize} bytes`);\n      }\n      yield chunk;\n    }\n  };\n};\n"
  },
  {
    "path": "packages/asset-uploader/src/utils/to-bytes.ts",
    "content": "// Convert a string value in assumed MB to bytes number\nexport const toBytes = (value: string): number =>\n  Number.parseFloat(value) * 1e6;\n"
  },
  {
    "path": "packages/asset-uploader/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"isolatedDeclarations\": true\n  }\n}\n"
  },
  {
    "path": "packages/asset-uploader/tsconfig.typecheck.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": null,\n    \"isolatedDeclarations\": false\n  }\n}\n"
  },
  {
    "path": "packages/authorization-token/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/authorization-token/README.md",
    "content": "# Webstudio Project\n\nProject related functionalities. This was temporarily placed here to reuse between other packages, but we need to split this into separate packages and probably remove this package entirely or keep it only for \"project\" specific functionality.\n"
  },
  {
    "path": "packages/authorization-token/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/authorization-token\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Authorization Token\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/postgrest\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"type-fest\": \"^4.37.0\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\"\n    },\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/authorization-token/src/db/authorization-token.ts",
    "content": "import type { Database } from \"@webstudio-is/postgrest/index.server\";\nimport {\n  type AppContext,\n  authorizeProject,\n  AuthorizationError,\n} from \"@webstudio-is/trpc-interface/index.server\";\n\ntype AuthorizationToken =\n  Database[\"public\"][\"Tables\"][\"AuthorizationToken\"][\"Row\"];\n\nconst applyTokenPermissions = (\n  token: AuthorizationToken\n): AuthorizationToken => {\n  let result = token;\n\n  // @todo: fix this on SQL level\n  if (token.relation !== \"viewers\") {\n    result = {\n      ...result,\n      canClone: true,\n      canCopy: true,\n    };\n  }\n\n  // @todo: fix this on SQL level\n  if (token.relation === \"viewers\") {\n    result = {\n      ...result,\n      canPublish: false,\n    };\n  }\n\n  // @todo: fix this on SQL level\n  if (token.relation === \"builders\") {\n    result = {\n      ...result,\n      canPublish: false,\n    };\n  }\n\n  // @todo: fix this on SQL level\n  if (token.relation === \"administrators\") {\n    result = {\n      ...result,\n      canPublish: true,\n    };\n  }\n\n  return result;\n};\n\nexport const findMany = async (\n  props: { projectId: string },\n  context: AppContext\n) => {\n  // Only owner of the project can list authorization tokens\n  const canList = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"own\" },\n    context\n  );\n\n  if (canList === false) {\n    throw new AuthorizationError(\n      \"You don't have access to list this project authorization tokens\"\n    );\n  }\n\n  const dbTokens = await context.postgrest.client\n    .from(\"AuthorizationToken\")\n    .select()\n    .eq(\"projectId\", props.projectId)\n    // Stable order\n    .order(\"createdAt\", { ascending: true })\n    .order(\"token\", { ascending: true });\n  if (dbTokens.error) {\n    throw dbTokens.error;\n  }\n\n  return dbTokens.data.map(applyTokenPermissions);\n};\n\nexport const tokenDefaultPermissions = {\n  canClone: true,\n  canCopy: true,\n  canPublish: true,\n};\n\nexport type TokenPermissions = typeof tokenDefaultPermissions;\n\nexport const getTokenInfo = async (\n  token: AuthorizationToken[\"token\"],\n  context: AppContext\n) => {\n  const dbToken = await context.postgrest.client\n    .from(\"AuthorizationToken\")\n    .select()\n    .eq(\"token\", token)\n    .maybeSingle();\n\n  if (dbToken.error) {\n    throw dbToken.error;\n  }\n\n  if (dbToken.data === null) {\n    throw new AuthorizationError(\"Authorization token not found\");\n  }\n\n  return applyTokenPermissions(dbToken.data);\n};\n\nexport const getTokenPermissions = async (\n  props: { projectId: string; token: AuthorizationToken[\"token\"] },\n  context: AppContext\n): Promise<TokenPermissions> => {\n  const dbToken = await getTokenInfo(props.token, context);\n\n  return {\n    canClone: dbToken.canClone,\n    canCopy: dbToken.canCopy,\n    canPublish: dbToken.canPublish,\n  };\n};\n\nexport const create = async (\n  props: {\n    projectId: string;\n    relation: AuthorizationToken[\"relation\"];\n    name: string;\n  },\n  context: AppContext\n) => {\n  const tokenId = crypto.randomUUID();\n\n  // Only owner of the project can create authorization tokens\n  const canCreateToken = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"own\" },\n    context\n  );\n\n  if (canCreateToken === false) {\n    throw new AuthorizationError(\n      \"You don't have access to create this project authorization tokens\"\n    );\n  }\n\n  const dbToken = await context.postgrest.client\n    .from(\"AuthorizationToken\")\n    .insert({\n      projectId: props.projectId,\n      relation: props.relation,\n      token: tokenId,\n      name: props.name,\n    })\n    .select();\n  if (dbToken.error) {\n    throw dbToken.error;\n  }\n\n  return dbToken.data;\n};\n\nexport const update = async (\n  projectId: string,\n  props: Pick<AuthorizationToken, \"token\" | \"relation\"> &\n    Partial<AuthorizationToken>,\n  context: AppContext\n) => {\n  // Only owner of the project can edit authorization tokens\n  const canCreateToken = await authorizeProject.hasProjectPermit(\n    { projectId, permit: \"own\" },\n    context\n  );\n\n  if (canCreateToken === false) {\n    throw new AuthorizationError(\n      \"You don't have access to edit this project authorization tokens\"\n    );\n  }\n\n  const previousToken = await context.postgrest.client\n    .from(\"AuthorizationToken\")\n    .select()\n    .eq(\"projectId\", projectId)\n    .eq(\"token\", props.token)\n    .maybeSingle();\n  if (previousToken.error) {\n    throw previousToken.error;\n  }\n  if (previousToken.data === null) {\n    throw new AuthorizationError(\"Authorization token not found\");\n  }\n\n  const dbToken = await context.postgrest.client\n    .from(\"AuthorizationToken\")\n    .update({\n      name: props.name,\n      relation: props.relation,\n      canClone: props.canClone,\n      canCopy: props.canCopy,\n      canPublish: props.canPublish,\n    })\n    .eq(\"projectId\", projectId)\n    .eq(\"token\", props.token)\n    .select()\n    .single();\n  if (dbToken.error) {\n    throw dbToken.error;\n  }\n\n  return dbToken.data;\n};\n\nexport const remove = async (\n  props: {\n    projectId: string;\n    token: AuthorizationToken[\"token\"];\n  },\n  context: AppContext\n) => {\n  // Only owner of the project can delete authorization tokens\n  const canDeleteToken = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"own\" },\n    context\n  );\n\n  if (canDeleteToken === false) {\n    throw new Error(\n      \"You don't have access to delete this project authorization tokens\"\n    );\n  }\n\n  const dbToken = await context.postgrest.client\n    .from(\"AuthorizationToken\")\n    .delete()\n    .eq(\"projectId\", props.projectId)\n    .eq(\"token\", props.token)\n    .select()\n    .single();\n  if (dbToken.error) {\n    throw dbToken.error;\n  }\n\n  return dbToken.data;\n};\n"
  },
  {
    "path": "packages/authorization-token/src/db/index.ts",
    "content": "export * as db from \"./authorization-token\";\n"
  },
  {
    "path": "packages/authorization-token/src/index.server.ts",
    "content": "export { db } from \"./db\";\nexport * from \"./trpc\";\n"
  },
  {
    "path": "packages/authorization-token/src/index.ts",
    "content": "export type { AuthorizationTokensRouter } from \"./trpc\";\nexport type { TokenPermissions } from \"./db/authorization-token\";\n"
  },
  {
    "path": "packages/authorization-token/src/trpc/authorization-tokens-router.ts",
    "content": "import { z } from \"zod\";\nimport { router, procedure } from \"@webstudio-is/trpc-interface/index.server\";\nimport { db } from \"../db\";\nimport type { IsEqual } from \"type-fest\";\nimport type { Database } from \"@webstudio-is/postgrest/index.server\";\n\ntype Relation =\n  Database[\"public\"][\"Tables\"][\"AuthorizationToken\"][\"Row\"][\"relation\"];\n\nconst TokenProjectRelation = z.enum([\n  \"viewers\",\n  \"editors\",\n  \"builders\",\n  \"administrators\",\n]);\n\n// Check DB types are compatible with zod types\ntype TokenRelation = z.infer<typeof TokenProjectRelation>;\ntrue satisfies IsEqual<TokenRelation, Relation>;\n\nexport const authorizationTokenRouter = router({\n  findMany: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n      })\n    )\n    .query(async ({ input, ctx }) => {\n      return await db.findMany({ projectId: input.projectId }, ctx);\n    }),\n  create: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        relation: TokenProjectRelation,\n        name: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      return await db.create(\n        {\n          projectId: input.projectId,\n          relation: input.relation,\n          name: input.name,\n        },\n        ctx\n      );\n    }),\n  remove: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        token: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      return await db.remove(\n        { projectId: input.projectId, token: input.token },\n        ctx\n      );\n    }),\n  update: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        token: z.string(),\n        name: z.string(),\n        relation: TokenProjectRelation,\n        canClone: z.boolean(),\n        canCopy: z.boolean(),\n        canPublish: z.boolean(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      return await db.update(\n        input.projectId,\n        {\n          token: input.token,\n          name: input.name,\n          relation: input.relation,\n          canPublish: input.canPublish,\n          canClone: input.canClone,\n          canCopy: input.canCopy,\n        },\n        ctx\n      );\n    }),\n});\n\nexport type AuthorizationTokensRouter = typeof authorizationTokenRouter;\n"
  },
  {
    "path": "packages/authorization-token/src/trpc/index.ts",
    "content": "export * from \"./authorization-tokens-router\";\n"
  },
  {
    "path": "packages/authorization-token/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/cli/.gitignore",
    "content": "lib/\n"
  },
  {
    "path": "packages/cli/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/cli/README.md",
    "content": "<a href=\"https://wstd.us/cli-vid\">\n   <img src=\"https://img.youtube.com/vi/eoyB9DfWdT8/0.jpg\" style=\"width:100%;\">\n</a>\n\n## Webstudio CLI\n\nThe Webstudio CLI helps you to link, sync and build projects from Webstudio Cloud. This README will guide you through the process of setting up a Webstudio project on your local machine and continually sync it with the cloud.\n\n## Prerequisites: Installing Node.js\n\nYou need Node.js to use the Webstudio CLI. If Node.js is already installed in your system, you can skip ahead to the section on installing the Webstudio CLI.\n\nTo install Node.js using NVM, first install NVM by running:\n\n```bash\ncurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash\n```\n\nOnce NVM is installed, you can install Node.js version 22 by running:\n\n```bash\nnvm install 22\n```\n\nVerify your Node.js installation by checking its version:\n\n```bash\nnode --version\n```\n\n## Installing the Webstudio CLI\n\nTo get started with the Webstudio CLI:\n\n1. Download and install the CLI using the following command:\n\n   ```bash\n   npm install -g webstudio@latest\n   ```\n\n1. Confirm the installation by checking the CLI version:\n\n   ```bash\n   webstudio --version\n   ```\n\n1. To keep your CLI updated, use the same command used for installation whenever a new release is available.\n\n## Initiating a Webstudio Project\n\nNow, you can run a Webstudio project on your local machine using this command:\n\n```bash\nwebstudio\n```\n\nThis will initiate the flow to connect your Webstudio Cloud project and build it in your local computer. The default flow will guide you through the steps. You can also perform all the operations individually using independent commands.\n\n## Commands\n\nHere is the list of independent commands:\n\n- version\n- help\n- link\n- sync\n- build\n\n### link\n\nThe **`link`** command syncs your local Webstudio project with the project from the cloud. This means that any changes made in the cloud can be synced to the local project, once they are published.\n\nYou can link a project from Webstudio Cloud with the following command:\n\n```bash\nwebstudio link\n```\n\nThis command will prompt you to paste a link which you can create using the _Share_ option in your project.\n\nMake sure to provide _Build_ access when generating the link in Webstudio Cloud.\n\n### sync\n\nOnce the project is linked, use the **`sync`** command to sync it with the cloud:\n\n```bash\nwebstudio sync\n```\n\nMake sure to publish the project in Webstudio Cloud before running the **`sync`** command in your local Webstudio project.\n\n### build\n\nNow, you can build your project with the **`build`** command:\n\n```bash\nwebstudio build\n```\n\nDuring this phase, the CLI establishes the necessary routes and pages, scaffolding the entire application using the default Remix template. Additionally, all assets, such as images and fonts are downloaded to the **`assets`** folder inside the **`public`** directory.\n\nOnce the project is scaffolded, you can run `npm install` and then `npm run dev` to run your app in development mode.\n\nIf you want to build a production version of the app, you can run `npm run build`.\n\n## Deployment\n\n### Vercel\n\nOnce you've built the project locally, you can use the [Vercel CLI](https://vercel.com/docs/cli) to deploy your project directly to [Vercel](https://vercel.com):\n\n```bash\nvercel deploy\n```\n\nFollow the instructions [here](https://vercel.com/docs/cli) to install the `vercel` CLI. We plan to add more deployment targets in future.\n\n### Netlify\n\nIf you want to deploy to netlify, you can use [Netlify CLI](https://docs.netlify.com/cli/get-started/) to deploy your project directly to [Netlify](https://netlify.com/):\n\n```bash\nnetlify deploy\n```\n\nYou can configure the project to support netlify serverless/edge-functions respectively, as deployment target at the time of initially setting up your project. Please check the [initiating-a-webstudio-project](#initiating-a-webstudio-project) section.\n\nYou can manually change it using the `build` command. For serverless functions:\n\n```bash\nwebstudio build --template netlify\n```\n\n## Important Notes\n\nIf you use `vercel build` before `vercel deploy`, make sure to clean your `app` folder in the project afterward.\n\nVercel injects a few [files](https://github.com/vercel/vercel/blob/a8ad176262ef822860ce338927e6f959961d2d32/packages/remix/src/build.ts#L63) to support and deploy Remix using their CLI, but these files are not necessary for your project when you use it locally.\n"
  },
  {
    "path": "packages/cli/bin.js",
    "content": "#!/usr/bin/env node\n\nimport { main } from \"#cli\";\n\nawait main();\n"
  },
  {
    "path": "packages/cli/package.json",
    "content": "{\n  \"name\": \"webstudio\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio CLI\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"webstudio-cli\": \"./bin.js\",\n    \"webstudio\": \"./bin.js\"\n  },\n  \"imports\": {\n    \"#cli\": {\n      \"webstudio\": \"./src/cli.ts\",\n      \"default\": \"./lib/cli.js\"\n    }\n  },\n  \"files\": [\n    \"lib/*\",\n    \"templates/*\",\n    \"bin.js\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"build\": \"rm -rf lib && vite build\",\n    \"test\": \"vitest run\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"dependencies\": {\n    \"@clack/prompts\": \"^0.10.0\",\n    \"@emotion/hash\": \"^0.9.2\",\n    \"acorn\": \"^8.14.1\",\n    \"acorn-walk\": \"^8.3.4\",\n    \"change-case\": \"^5.4.4\",\n    \"deepmerge\": \"^4.3.1\",\n    \"env-paths\": \"^3.0.0\",\n    \"nanoid\": \"^5.1.5\",\n    \"p-limit\": \"^6.2.0\",\n    \"parse5\": \"7.3.0\",\n    \"picocolors\": \"^1.1.1\",\n    \"reserved-identifiers\": \"^1.0.0\",\n    \"tinyexec\": \"^0.3.2\",\n    \"warn-once\": \"^0.1.1\",\n    \"yargs\": \"^17.7.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/vite-plugin\": \"^1.1.0\",\n    \"@netlify/vite-plugin-react-router\": \"^1.0.1\",\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@remix-run/cloudflare\": \"^2.16.5\",\n    \"@remix-run/cloudflare-pages\": \"^2.16.5\",\n    \"@remix-run/dev\": \"^2.16.5\",\n    \"@remix-run/node\": \"^2.16.5\",\n    \"@remix-run/react\": \"^2.16.5\",\n    \"@remix-run/server-runtime\": \"^2.16.5\",\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@types/yargs\": \"^17.0.33\",\n    \"@vercel/react-router\": \"^1.1.0\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/http-client\": \"workspace:*\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-remix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-router\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"h3\": \"^1.15.1\",\n    \"ipx\": \"^3.0.3\",\n    \"isbot\": \"^5.1.25\",\n    \"prettier\": \"3.5.3\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"ts-expect\": \"^1.3.0\",\n    \"vike\": \"^0.4.229\",\n    \"vite\": \"^6.3.4\",\n    \"vitest\": \"^3.1.2\",\n    \"wrangler\": \"^3.63.2\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/src/args.ts",
    "content": "export type File = {\n  name: string;\n  content: string;\n  encoding: \"utf-8\" | \"base64\";\n  merge: boolean;\n};\n\nexport type Folder = {\n  name: string;\n  files: File[];\n  subFolders: Folder[];\n};\n\nexport type ProjectTarget = \"defaults\";\n\nexport const SupportedProjects: Record<ProjectTarget, boolean> = {\n  defaults: true,\n};\n"
  },
  {
    "path": "packages/cli/src/build-utils.ts",
    "content": "import { PROJECT_TEMPLATES, INTERNAL_TEMPLATES } from \"./config\";\n\nexport const mapToTemplatesFromOptions = (values: string[]) => {\n  const templates: string[] = [];\n  for (const value of values) {\n    const template =\n      PROJECT_TEMPLATES.find((item) => item.value === value) ??\n      INTERNAL_TEMPLATES.find((item) => item.value === value);\n\n    if (template == null) {\n      templates.push(value);\n      continue;\n    }\n\n    if (\"expand\" in template && template.expand != null) {\n      templates.push(...template.expand);\n      continue;\n    }\n\n    templates.push(value);\n  }\n\n  return templates;\n};\n"
  },
  {
    "path": "packages/cli/src/cli.ts",
    "content": "import { exit, argv } from \"node:process\";\nimport { hideBin } from \"yargs/helpers\";\nimport { GLOBAL_CONFIG_FILE } from \"./config\";\nimport { createFileIfNotExists } from \"./fs-utils\";\nimport { link, linkOptions } from \"./commands/link\";\nimport { sync, syncOptions } from \"./commands/sync\";\nimport { build, buildOptions } from \"./commands/build\";\nimport { initFlow } from \"./commands/init-flow\";\nimport makeCLI from \"yargs\";\nimport packageJson from \"../package.json\" assert { type: \"json\" };\nimport type { CommonYargsArgv } from \"./commands/yargs-types\";\n\nexport const main = async () => {\n  try {\n    await createFileIfNotExists(GLOBAL_CONFIG_FILE, \"{}\");\n\n    const cmd: CommonYargsArgv = makeCLI(hideBin(argv))\n      .strict()\n      .fail(function (msg, err, yargs) {\n        if (err) {\n          throw err; // preserve stack\n        }\n\n        console.error(msg);\n\n        console.error(yargs.help());\n\n        process.exit(1);\n      })\n      .wrap(null)\n      .option(\"v\", {\n        describe: \"Show version number\",\n        type: \"boolean\",\n      })\n      .option(\"h\", {\n        describe: \"Show all commands\",\n        alias: \"help\",\n        type: \"boolean\",\n      })\n      .scriptName(\"webstudio\")\n      .usage(\n        `Webstudio CLI (${packageJson.version}) allows you to setup, sync, build and preview your project.`\n      );\n\n    cmd.version(packageJson.version).alias(\"v\", \"version\");\n\n    cmd.command(\n      [\"build\"],\n      \"Build the project\",\n      (yargs: CommonYargsArgv) => {\n        return buildOptions(yargs).demandOption(\n          \"template\",\n          \"Please specify a template to use for the build\"\n        );\n      },\n      build\n    );\n    cmd.command([\"link\"], \"Link the project with the cloud\", linkOptions, link);\n    cmd.command([\"sync\"], \"Sync your project\", syncOptions, sync);\n    cmd.command([\"$0\", \"init\"], \"Setup the project\", buildOptions, initFlow);\n\n    await cmd.parse();\n  } catch (error) {\n    console.error(error);\n    exit(1);\n  }\n};\n"
  },
  {
    "path": "packages/cli/src/commands/build.ts",
    "content": "import { access } from \"node:fs/promises\";\nimport { exit } from \"node:process\";\nimport { log } from \"@clack/prompts\";\nimport { prebuild } from \"../prebuild\";\nimport { LOCAL_DATA_FILE, PROJECT_TEMPLATES } from \"../config\";\nimport type {\n  CommonYargsArgv,\n  StrictYargsOptionsToInterface,\n} from \"./yargs-types\";\nimport { mapToTemplatesFromOptions } from \"../build-utils\";\n\nexport const buildOptions = (yargs: CommonYargsArgv) =>\n  yargs\n    .option(\"assets\", {\n      type: \"boolean\",\n      default: true,\n      describe: \"[Experimental] Download assets\",\n    })\n    .option(\"template\", {\n      type: \"array\",\n      string: true,\n      default: [] as string[],\n\n      coerce: mapToTemplatesFromOptions,\n\n      describe: `Template to use for the build [choices: ${PROJECT_TEMPLATES.map(\n        (item) => item.value\n      ).join(\", \")}]`,\n    });\n\n// @todo: use options.assets to define if we need to download assets\nexport const build = async (\n  options: StrictYargsOptionsToInterface<typeof buildOptions>\n) => {\n  try {\n    await access(LOCAL_DATA_FILE);\n  } catch (error: unknown) {\n    if (error instanceof Error && \"code\" in error && error.code === \"ENOENT\") {\n      log.error(\n        `You need to link a webstudio project before building it. Run \\`webstudio link\\` to link a project.`\n      );\n      exit(1);\n    }\n\n    throw error;\n  }\n\n  await prebuild(options);\n};\n"
  },
  {
    "path": "packages/cli/src/commands/init-flow.ts",
    "content": "import { chdir, cwd } from \"node:process\";\nimport { join } from \"node:path\";\nimport pc from \"picocolors\";\nimport { x } from \"tinyexec\";\nimport {\n  cancel,\n  confirm,\n  isCancel,\n  log,\n  select,\n  spinner,\n  text,\n} from \"@clack/prompts\";\nimport { createFolderIfNotExists, isFileExists } from \"../fs-utils\";\nimport { PROJECT_TEMPLATES } from \"../config\";\nimport { link, validateShareLink } from \"./link\";\nimport { sync } from \"./sync\";\nimport { build, buildOptions } from \"./build\";\nimport type { StrictYargsOptionsToInterface } from \"./yargs-types\";\nimport { mapToTemplatesFromOptions } from \"../build-utils\";\n\ntype ProjectTemplates = (typeof PROJECT_TEMPLATES)[number][\"value\"];\n\nconst exitIfCancelled = <Value>(value: Value | symbol): Value => {\n  if (isCancel(value)) {\n    cancel(\"Project initialization is cancelled\");\n    process.exit(1);\n  }\n  return value;\n};\n\nexport const initFlow = async (\n  options: StrictYargsOptionsToInterface<typeof buildOptions>\n) => {\n  const isProjectConfigured = await isFileExists(\".webstudio/config.json\");\n  let shouldInstallDeps = false;\n  let folderName: undefined | string;\n  let projectTemplate: ProjectTemplates | undefined;\n\n  if (isProjectConfigured === false) {\n    const shouldCreateFolder = exitIfCancelled(\n      await confirm({\n        message:\n          \"Would you like to create a project folder? (no to use current folder)\",\n        initialValue: true,\n      })\n    );\n\n    if (shouldCreateFolder === true) {\n      folderName = exitIfCancelled(\n        await text({\n          message: \"Please enter a project name\",\n          validate(value) {\n            if (value.length === 0) {\n              return \"Folder name is required\";\n            }\n          },\n        })\n      );\n\n      await createFolderIfNotExists(join(cwd(), folderName));\n      chdir(join(cwd(), folderName));\n    }\n\n    const shareLink = exitIfCancelled(\n      await text({\n        message: \"Please paste a link from the Share Dialog in the builder\",\n        validate: validateShareLink,\n      })\n    );\n\n    await link({ link: shareLink });\n\n    if (!options.template.length) {\n      projectTemplate = exitIfCancelled(\n        await select({\n          message: \"Where would you like to deploy your project?\",\n          options: PROJECT_TEMPLATES,\n        })\n      );\n    }\n\n    shouldInstallDeps = exitIfCancelled(\n      await confirm({\n        message: \"Would you like to install dependencies? (recommended)\",\n        initialValue: true,\n      })\n    );\n  }\n\n  /*\n    If a project is already linked, we sync direclty without asking for deploy target.\n    We need to request for deploy target here as the current flow is running in a existing project.\n  */\n\n  if (!options.template.length && projectTemplate === undefined) {\n    projectTemplate = exitIfCancelled(\n      await select({\n        message: \"Where would you like to deploy your project?\",\n        options: PROJECT_TEMPLATES,\n      })\n    );\n  }\n\n  await sync({ buildId: undefined, origin: undefined, authToken: undefined });\n\n  await build({\n    ...options,\n    template: projectTemplate\n      ? mapToTemplatesFromOptions([projectTemplate])\n      : options.template,\n  });\n\n  if (shouldInstallDeps === true) {\n    const install = spinner();\n    install.start(\"Installing dependencies\");\n    await x(\"npm\", [\"install\"]);\n    install.stop(\"Installed dependencies\");\n  }\n\n  log.message();\n  log.message(pc.green(pc.bold(`Your project was successfully synced 🎉`)));\n  log.message(\n    [\n      \"Now you can:\",\n      folderName && `Go to your project: ${pc.dim(`cd ${folderName}`)}`,\n      `Run ${pc.dim(\"npm run dev\")} to preview your project on a local server.`,\n      projectTemplate && getDeploymentInstructions(projectTemplate),\n    ]\n      .filter(Boolean)\n      .join(\"\\n\")\n  );\n};\n\nconst getDeploymentInstructions = (\n  deployTarget: ProjectTemplates\n): string | undefined => {\n  switch (deployTarget) {\n    case \"vercel\":\n      return `Run ${pc.dim(\"npx vercel\")} to publish on Vercel.`;\n    case \"netlify\":\n      return [\n        `To deploy to Netlify, run the following commands: `,\n        `Run ${pc.dim(\"npx netlify-cli login\")} to login to Netlify.`,\n        `Run ${pc.dim(\n          \"npx netlify-cli sites:create\"\n        )} to create a new project.`,\n        `Run ${pc.dim(\"npx netlify-cli build\")} to build the project`,\n        `Run ${pc.dim(\"npx netlify-cli deploy\")} to deploy on Netlify.`,\n      ].join(\"\\n\");\n  }\n};\n"
  },
  {
    "path": "packages/cli/src/commands/link.ts",
    "content": "import { cwd, exit } from \"node:process\";\nimport { join } from \"node:path\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { cancel, isCancel, log, text } from \"@clack/prompts\";\nimport { parseBuilderUrl } from \"@webstudio-is/http-client\";\nimport {\n  GLOBAL_CONFIG_FILE,\n  LOCAL_CONFIG_FILE,\n  type GlobalConfig,\n  jsonToGlobalConfig,\n  type LocalConfig,\n} from \"../config\";\nimport { createFileIfNotExists } from \"../fs-utils\";\nimport type {\n  CommonYargsArgv,\n  StrictYargsOptionsToInterface,\n} from \"./yargs-types\";\n\nconst parseShareLink = (value: string) => {\n  // value.replaceAll(\"'\", \"\") is used to remove single quotes from the URL on Windows.\n  // This is necessary because the following pnpm script works on macOS and Linux but fails here on Windows:\n  // \"fixtures:link\": \"pnpm cli link --link https://p-cddc1d44-af37-4cb6-a430-d300cf6f932d-dot-${BUILDER_HOST:-main.development.webstudio.is}'?authToken=1cdc6026-dd5b-4624-b89b-9bd45e9bcc3d'\",\n  // On Windows, single quotes are incorrectly included in the URL.\n  const url = new URL(value.replaceAll(\"'\", \"\"));\n\n  const token = url.searchParams.get(\"authToken\");\n\n  // eslint-disable-next-line prefer-const\n  let { projectId, sourceOrigin } = parseBuilderUrl(url.href);\n\n  if (projectId === undefined) {\n    // Support deprecated links until the end of 2024\n    const segments = url.pathname.split(\"/\").slice(1);\n    if (segments.length !== 2 || segments[0] !== \"builder\") {\n      throw Error(\"Segments not matching\");\n    }\n    projectId = segments[1];\n  }\n\n  if (token == null) {\n    throw Error(\"Token is missing\");\n  }\n\n  return {\n    origin: sourceOrigin,\n    projectId,\n    token,\n  };\n};\n\nexport const validateShareLink = (value: string) => {\n  if (value.length === 0) {\n    return \"Share link is required\";\n  }\n  if (URL.canParse(value) === false) {\n    return \"Share link is invalid\";\n  }\n  try {\n    parseShareLink(value);\n  } catch {\n    return \"Share link is invalid\";\n  }\n};\n\nexport const linkOptions = (yargs: CommonYargsArgv) =>\n  yargs.option(\"link\", {\n    alias: \"l\",\n    type: \"string\",\n    describe: \"Link to a webstudio project\",\n  });\n\nexport const link = async (\n  options: StrictYargsOptionsToInterface<typeof linkOptions> | { link?: string }\n) => {\n  let shareLink: string | symbol;\n  if (options.link) {\n    shareLink = options.link;\n  } else {\n    shareLink = await text({\n      message: \"Please paste a link from the Share Dialog in the builder\",\n      validate: validateShareLink,\n    });\n    if (isCancel(shareLink)) {\n      cancel(\"Project linking is cancelled\");\n      exit(1);\n    }\n  }\n\n  const { origin, projectId, token } = parseShareLink(shareLink);\n\n  const currentConfig = await readFile(GLOBAL_CONFIG_FILE, \"utf-8\");\n  const currentConfigJson = jsonToGlobalConfig(JSON.parse(currentConfig));\n\n  const newConfig: GlobalConfig = {\n    ...currentConfigJson,\n    [projectId]: {\n      origin,\n      token,\n    },\n  };\n\n  await writeFile(GLOBAL_CONFIG_FILE, JSON.stringify(newConfig, null, 2));\n  log.info(`Saved credentials for project ${projectId}.\nYou can find your config at ${GLOBAL_CONFIG_FILE}`);\n\n  const localConfig: LocalConfig = {\n    projectId,\n  };\n\n  await createFileIfNotExists(\n    join(cwd(), LOCAL_CONFIG_FILE),\n    JSON.stringify(localConfig, null, 2)\n  );\n  log.step(\"The project is linked successfully\");\n};\n"
  },
  {
    "path": "packages/cli/src/commands/sync.ts",
    "content": "import { readFile, writeFile } from \"node:fs/promises\";\nimport { cwd } from \"node:process\";\nimport { join } from \"node:path\";\nimport pc from \"picocolors\";\nimport { spinner } from \"@clack/prompts\";\nimport {\n  loadProjectDataByBuildId,\n  loadProjectDataByProjectId,\n  type Data,\n} from \"@webstudio-is/http-client\";\nimport { createFileIfNotExists, isFileExists } from \"../fs-utils\";\nimport {\n  GLOBAL_CONFIG_FILE,\n  LOCAL_CONFIG_FILE,\n  LOCAL_DATA_FILE,\n  jsonToGlobalConfig,\n  jsonToLocalConfig,\n} from \"../config\";\nimport type {\n  CommonYargsArgv,\n  StrictYargsOptionsToInterface,\n} from \"./yargs-types\";\n\nexport const syncOptions = (yargs: CommonYargsArgv) =>\n  yargs\n    .option(\"buildId\", {\n      type: \"string\",\n      describe: \"[Experimental] Project build id to sync\",\n    })\n    .option(\"origin\", {\n      type: \"string\",\n      describe: \"[Experimental] Remote origin to sync with\",\n    })\n    .option(\"authToken\", {\n      type: \"string\",\n      describe: \"[Experimental] Service token\",\n    });\n\nexport const sync = async (\n  options: StrictYargsOptionsToInterface<typeof syncOptions>\n) => {\n  const syncing = spinner();\n\n  let project: Data | undefined;\n  syncing.start(`Synchronizing project data`);\n\n  if (\n    options.buildId !== undefined &&\n    options.origin !== undefined &&\n    options.authToken !== undefined\n  ) {\n    syncing.message(`Synchronizing project data from ${options.origin}`);\n    project = await loadProjectDataByBuildId({\n      buildId: options.buildId,\n      seviceToken: options.authToken,\n      origin: options.origin,\n    });\n    project.origin = options.origin;\n  } else {\n    const globalConfigText = await readFile(GLOBAL_CONFIG_FILE, \"utf-8\");\n    const globalConfig = jsonToGlobalConfig(JSON.parse(globalConfigText));\n\n    if ((await isFileExists(LOCAL_CONFIG_FILE)) === false) {\n      syncing.stop(\n        `Local config file is not found. Please make sure current directory is a webstudio project`,\n        2\n      );\n      return;\n    }\n\n    const localConfigText = await readFile(\n      join(cwd(), LOCAL_CONFIG_FILE),\n      \"utf-8\"\n    );\n\n    const localConfig = jsonToLocalConfig(JSON.parse(localConfigText));\n\n    const projectConfig = globalConfig[localConfig.projectId];\n\n    if (projectConfig === undefined) {\n      syncing.stop(\n        `Project config is not found, please run ${pc.dim(\"webstudio link\")}`,\n        2\n      );\n      return;\n    }\n\n    const { origin, token } = projectConfig;\n    syncing.message(`Synchronizing project data from ${origin}`);\n\n    try {\n      project =\n        options.buildId !== undefined\n          ? await loadProjectDataByBuildId({\n              buildId: options.buildId,\n              authToken: token,\n              origin,\n            })\n          : await loadProjectDataByProjectId({\n              projectId: localConfig.projectId,\n              authToken: token,\n              origin,\n            });\n      project.origin = origin;\n    } catch (error) {\n      // catch errors about unpublished project\n      syncing.stop((error as Error).message, 2);\n\n      throw error;\n    }\n  }\n\n  // Check that project defined\n  project satisfies Data;\n\n  const localBuildFilePath = join(cwd(), LOCAL_DATA_FILE);\n  await createFileIfNotExists(localBuildFilePath);\n  await writeFile(localBuildFilePath, JSON.stringify(project, null, 2), \"utf8\");\n\n  syncing.stop(\"Project data synchronized successfully\");\n};\n"
  },
  {
    "path": "packages/cli/src/commands/yargs-types.ts",
    "content": "/*\nhttps://github.com/cloudflare/workers-sdk/blob/975cbd9b949a1685856e16b4f88400fc04dca38c/packages/wrangler/src/yargs-types.ts#L7\n\nCopyright (c) 2020 Cloudflare, Inc. <wrangler@cloudflare.com>\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n*/\n\nimport type { ArgumentsCamelCase, Argv } from \"yargs\";\n\n/**\n * Yargs options included in every wrangler command.\n */\nexport interface CommonYargsOptions {\n  v: boolean | undefined;\n  h: boolean | undefined;\n  // t: string | undefined;\n}\n\nexport type CommonYargsArgv = Argv<CommonYargsOptions>;\n\nexport type YargvToInterface<T> =\n  T extends Argv<infer P> ? ArgumentsCamelCase<P> : never;\n\n// See http://stackoverflow.com/questions/51465182/how-to-remove-index-signature-using-mapped-types\ntype RemoveIndex<T> = {\n  [K in keyof T as string extends K\n    ? never\n    : number extends K\n      ? never\n      : K]: T[K];\n};\n\n/**\n * Given some Yargs Options function factory, extract the interface\n * that corresponds to the yargs arguments, remove index types, and only allow camelCase\n */\nexport type StrictYargsOptionsToInterface<\n  T extends (yargs: CommonYargsArgv) => Argv,\n> = T extends (yargs: CommonYargsArgv) => Argv<infer P>\n  ? Omit<RemoveIndex<ArgumentsCamelCase<P>>, \"v\" | \"h\" | \"$0\" | \"_\">\n  : never;\n"
  },
  {
    "path": "packages/cli/src/config.ts",
    "content": "import { join } from \"node:path\";\nimport envPaths from \"env-paths\";\nimport { z } from \"zod\";\n\nconst GLOBAL_CONFIG_FOLDER = envPaths(\"webstudio\").config;\nconst GLOBAL_CONFIG_FILE_NAME = \"webstudio-config.json\";\nexport const GLOBAL_CONFIG_FILE = join(\n  GLOBAL_CONFIG_FOLDER,\n  GLOBAL_CONFIG_FILE_NAME\n);\n\nexport const LOCAL_CONFIG_FILE = \".webstudio/config.json\";\nexport const LOCAL_DATA_FILE = \".webstudio/data.json\";\n\nconst zLocalConfig = z.object({\n  projectId: z.string(),\n});\n\nexport type LocalConfig = z.infer<typeof zLocalConfig>;\n\nexport const jsonToLocalConfig = (json: unknown) => {\n  return zLocalConfig.parse(json);\n};\n\nconst zGlobalConfig = z.record(\n  z\n    .union([\n      z.object({\n        // origin mistakenly called host in the past\n        host: z.string(),\n        token: z.string(),\n      }),\n      z.object({\n        origin: z.string(),\n        token: z.string(),\n      }),\n    ])\n    .transform((value) => {\n      if (\"host\" in value) {\n        return {\n          origin: value.host,\n          token: value.token,\n        };\n      }\n      return value;\n    })\n);\n\nexport const jsonToGlobalConfig = (json: unknown) => {\n  return zGlobalConfig.parse(json);\n};\n\nexport type GlobalConfig = z.infer<typeof zGlobalConfig>;\n\nexport const PROJECT_TEMPLATES = [\n  {\n    value: \"docker\" as const,\n    label: \"Docker\",\n    expand: [\"react-router\", \"react-router-docker\"],\n  },\n  {\n    value: \"vercel\" as const,\n    label: \"Vercel\",\n    expand: [\"react-router\", \"react-router-vercel\"],\n  },\n  {\n    value: \"netlify\" as const,\n    label: \"Netlify\",\n    expand: [\"react-router\", \"react-router-netlify\"],\n  },\n  {\n    value: \"ssg\" as const,\n    label: \"Static Site Generation (SSG)\",\n  },\n  {\n    value: \"ssg-netlify\" as const,\n    label: \"Static Site Generation (SSG) Netlify\",\n    expand: [\"ssg\", \"ssg-netlify\"],\n  },\n  {\n    value: \"ssg-vercel\" as const,\n    label: \"Static Site Generation (SSG) Vercel\",\n    expand: [\"ssg\", \"ssg-vercel\"],\n  },\n];\n\n// This feature will be made public eventually; currently, it’s only used for internal tasks.\nexport const INTERNAL_TEMPLATES = [\n  {\n    value: \"cloudflare\",\n    label: \"Cloudflare\",\n    expand: [\"defaults\", \"cloudflare\"],\n  },\n  {\n    value: \"cloudflare-new\",\n    label: \"Cloudflare (new)\",\n    expand: [\"react-router\", \"react-router-cloudflare\"],\n  },\n];\n"
  },
  {
    "path": "packages/cli/src/config.ts-expect.ts",
    "content": "import { expectType, type TypeEqual } from \"ts-expect\";\nimport { Templates } from \"@webstudio-is/sdk\";\nimport { PROJECT_TEMPLATES } from \"./config\";\n\n// We must ensure the validated template type always matches the CLI-supported templates.\n// This is crucial for security, as template names can be used in CLI/bash environments.\n// A TypeScript failure here means the Templates type is not consistent with the CLI templates.\nexpectType<TypeEqual<(typeof PROJECT_TEMPLATES)[number][\"value\"], Templates>>(\n  true\n);\n"
  },
  {
    "path": "packages/cli/src/framework-react-router.ts",
    "content": "import { join } from \"node:path\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { generateRemixRoute } from \"@webstudio-is/react-sdk\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport * as animationComponentMetas from \"@webstudio-is/sdk-components-animation/metas\";\nimport * as radixComponentMetas from \"@webstudio-is/sdk-components-react-radix/metas\";\nimport type { Framework } from \"./framework\";\n\nexport const createFramework = async (): Promise<Framework> => {\n  const routeTemplatesDir = join(\"app\", \"route-templates\");\n\n  const htmlTemplate = await readFile(\n    join(routeTemplatesDir, \"html.tsx\"),\n    \"utf8\"\n  );\n  const xmlTemplate = await readFile(\n    join(routeTemplatesDir, \"xml.tsx\"),\n    \"utf8\"\n  );\n  const defaultSitemapTemplate = await readFile(\n    join(routeTemplatesDir, \"default-sitemap.tsx\"),\n    \"utf8\"\n  );\n  const redirectTemplate = await readFile(\n    join(routeTemplatesDir, \"redirect.tsx\"),\n    \"utf8\"\n  );\n\n  // cleanup route templates after reading to not bloat generated code\n  await rm(routeTemplatesDir, { recursive: true, force: true });\n\n  const base = \"@webstudio-is/sdk-components-react\";\n  const reactRouter = \"@webstudio-is/sdk-components-react-router\";\n  const reactRadix = \"@webstudio-is/sdk-components-react-radix\";\n  const animation = \"@webstudio-is/sdk-components-animation\";\n  const components: Record<string, string> = {};\n  const metas: Record<string, WsComponentMeta> = {};\n  for (const [name, meta] of Object.entries(baseComponentMetas)) {\n    components[name] = `${base}:${name}`;\n    metas[name] = meta;\n  }\n  for (const name of [\"Body\", \"Link\", \"RichTextLink\", \"Form\", \"RemixForm\"]) {\n    components[name] = `${reactRouter}:${name}`;\n  }\n  for (const [name, meta] of Object.entries(radixComponentMetas)) {\n    components[`${reactRadix}:${name}`] = `${reactRadix}:${name}`;\n    metas[`${reactRadix}:${name}`] = meta;\n  }\n  for (const [name, meta] of Object.entries(animationComponentMetas)) {\n    components[`${animation}:${name}`] = `${animation}:${name}`;\n    metas[`${animation}:${name}`] = meta;\n  }\n\n  return {\n    metas,\n    components,\n    tags: {\n      textarea: `${base}:Textarea`,\n      input: `${base}:Input`,\n      select: `${base}:Select`,\n      body: `${reactRouter}:Body`,\n      a: `${reactRouter}:Link`,\n      form: `${reactRouter}:RemixForm`,\n    },\n    html: ({ pagePath }: { pagePath: string }) => [\n      {\n        file: join(\"app\", \"routes\", `${generateRemixRoute(pagePath)}.tsx`),\n        template: htmlTemplate,\n      },\n    ],\n    xml: ({ pagePath }: { pagePath: string }) => [\n      {\n        file: join(\"app\", \"routes\", `${generateRemixRoute(pagePath)}.tsx`),\n        template: xmlTemplate,\n      },\n    ],\n    redirect: ({ pagePath }: { pagePath: string }) => [\n      {\n        file: join(\"app\", \"routes\", `${generateRemixRoute(pagePath)}.ts`),\n        template: redirectTemplate,\n      },\n    ],\n    defaultSitemap: () => [\n      {\n        file: join(\n          \"app\",\n          \"routes\",\n          `${generateRemixRoute(\"/sitemap.xml\")}.tsx`\n        ),\n        template: defaultSitemapTemplate,\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "packages/cli/src/framework-remix.ts",
    "content": "import { join } from \"node:path\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { generateRemixRoute } from \"@webstudio-is/react-sdk\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport * as animationComponentMetas from \"@webstudio-is/sdk-components-animation/metas\";\nimport * as radixComponentMetas from \"@webstudio-is/sdk-components-react-radix/metas\";\nimport type { Framework } from \"./framework\";\n\nexport const createFramework = async (): Promise<Framework> => {\n  const routeTemplatesDir = join(\"app\", \"route-templates\");\n\n  const htmlTemplate = await readFile(\n    join(routeTemplatesDir, \"html.tsx\"),\n    \"utf8\"\n  );\n  const xmlTemplate = await readFile(\n    join(routeTemplatesDir, \"xml.tsx\"),\n    \"utf8\"\n  );\n  const defaultSitemapTemplate = await readFile(\n    join(routeTemplatesDir, \"default-sitemap.tsx\"),\n    \"utf8\"\n  );\n  const redirectTemplate = await readFile(\n    join(routeTemplatesDir, \"redirect.tsx\"),\n    \"utf8\"\n  );\n\n  // cleanup route templates after reading to not bloat generated code\n  await rm(routeTemplatesDir, { recursive: true, force: true });\n\n  const base = \"@webstudio-is/sdk-components-react\";\n  const remix = \"@webstudio-is/sdk-components-react-remix\";\n  const reactRadix = \"@webstudio-is/sdk-components-react-radix\";\n  const animation = \"@webstudio-is/sdk-components-animation\";\n  const components: Record<string, string> = {};\n  const metas: Record<string, WsComponentMeta> = {};\n  for (const [name, meta] of Object.entries(baseComponentMetas)) {\n    components[name] = `${base}:${name}`;\n    metas[name] = meta;\n  }\n  for (const name of [\"Body\", \"Link\", \"RichTextLink\", \"Form\", \"RemixForm\"]) {\n    components[name] = `${remix}:${name}`;\n  }\n  for (const [name, meta] of Object.entries(radixComponentMetas)) {\n    components[`${reactRadix}:${name}`] = `${reactRadix}:${name}`;\n    metas[`${reactRadix}:${name}`] = meta;\n  }\n  for (const [name, meta] of Object.entries(animationComponentMetas)) {\n    components[`${animation}:${name}`] = `${animation}:${name}`;\n    metas[`${animation}:${name}`] = meta;\n  }\n\n  return {\n    metas,\n    components,\n    tags: {\n      textarea: `${base}:Textarea`,\n      input: `${base}:Input`,\n      select: `${base}:Select`,\n      body: `${remix}:Body`,\n      a: `${remix}:Link`,\n      form: `${remix}:RemixForm`,\n    },\n    html: ({ pagePath }: { pagePath: string }) => [\n      {\n        file: join(\"app\", \"routes\", `${generateRemixRoute(pagePath)}.tsx`),\n        template: htmlTemplate,\n      },\n    ],\n    xml: ({ pagePath }: { pagePath: string }) => [\n      {\n        file: join(\"app\", \"routes\", `${generateRemixRoute(pagePath)}.tsx`),\n        template: xmlTemplate,\n      },\n    ],\n    redirect: ({ pagePath }: { pagePath: string }) => [\n      {\n        file: join(\"app\", \"routes\", `${generateRemixRoute(pagePath)}.ts`),\n        template: redirectTemplate,\n      },\n    ],\n    defaultSitemap: () => [\n      {\n        file: join(\n          \"app\",\n          \"routes\",\n          `${generateRemixRoute(\"/sitemap.xml\")}.tsx`\n        ),\n        template: defaultSitemapTemplate,\n      },\n    ],\n  };\n};\n"
  },
  {
    "path": "packages/cli/src/framework-vike-ssg.ts",
    "content": "import { join } from \"node:path\";\nimport { readFile, rm } from \"node:fs/promises\";\nimport { isPathnamePattern, type WsComponentMeta } from \"@webstudio-is/sdk\";\nimport * as baseComponentMetas from \"@webstudio-is/sdk-components-react/metas\";\nimport * as animationComponentMetas from \"@webstudio-is/sdk-components-animation/metas\";\nimport * as radixComponentMetas from \"@webstudio-is/sdk-components-react-radix/metas\";\nimport type { Framework } from \"./framework\";\n\nconst generateVikeRoute = (pagePath: string) => {\n  if (pagePath === \"/\") {\n    return \"index\";\n  }\n  return pagePath;\n};\n\nexport const createFramework = async (): Promise<Framework> => {\n  const routeTemplatesDir = join(\"app\", \"route-templates\");\n\n  const htmlPageTemplate = await readFile(\n    join(routeTemplatesDir, \"html\", \"+Page.tsx\"),\n    \"utf8\"\n  );\n  const htmlHeadTemplate = await readFile(\n    join(routeTemplatesDir, \"html\", \"+Head.tsx\"),\n    \"utf8\"\n  );\n  const htmlDataTemplate = await readFile(\n    join(routeTemplatesDir, \"html\", \"+data.ts\"),\n    \"utf8\"\n  );\n\n  // cleanup route templates after reading to not bloat generated code\n  await rm(routeTemplatesDir, { recursive: true, force: true });\n\n  const base = \"@webstudio-is/sdk-components-react\";\n  const reactRadix = \"@webstudio-is/sdk-components-react-radix\";\n  const animation = \"@webstudio-is/sdk-components-animation\";\n  const components: Record<string, string> = {};\n  const metas: Record<string, WsComponentMeta> = {};\n  for (const [name, meta] of Object.entries(baseComponentMetas)) {\n    components[name] = `${base}:${name}`;\n    metas[name] = meta;\n  }\n  for (const [name, meta] of Object.entries(radixComponentMetas)) {\n    components[`${reactRadix}:${name}`] = `${reactRadix}:${name}`;\n    metas[`${reactRadix}:${name}`] = meta;\n  }\n  for (const [name, meta] of Object.entries(animationComponentMetas)) {\n    components[`${animation}:${name}`] = `${animation}:${name}`;\n    metas[`${animation}:${name}`] = meta;\n  }\n\n  return {\n    metas,\n    components,\n    tags: {\n      textarea: `${base}:Textarea`,\n      input: `${base}:Input`,\n      select: `${base}:Select`,\n    },\n    html: ({ pagePath }: { pagePath: string }) => {\n      // ignore dynamic pages in static export\n      if (isPathnamePattern(pagePath)) {\n        return [];\n      }\n      return [\n        {\n          file: join(\"pages\", generateVikeRoute(pagePath), \"+Page.tsx\"),\n          template: htmlPageTemplate,\n        },\n        {\n          file: join(\"pages\", generateVikeRoute(pagePath), \"+Head.tsx\"),\n          template: htmlHeadTemplate,\n        },\n        {\n          file: join(\"pages\", generateVikeRoute(pagePath), \"+data.ts\"),\n          template: htmlDataTemplate,\n        },\n      ];\n    },\n    xml: () => [],\n    redirect: () => [],\n    defaultSitemap: () => [],\n  };\n};\n"
  },
  {
    "path": "packages/cli/src/framework.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\n\ntype FrameworkTemplateEntry = {\n  file: string;\n  template: string;\n};\n\nexport type Framework = {\n  // instance.component: WsComponentMeta\n  metas: Record<string, WsComponentMeta>;\n  // instance.component: \"importSource:importSpecifier\"\n  components: Record<string, string>;\n  // instance.tag: \"importSource:importSpecifier\"\n  tags: Record<string, string>;\n  html: (params: { pagePath: string }) => FrameworkTemplateEntry[];\n  xml: (params: { pagePath: string }) => FrameworkTemplateEntry[];\n  redirect: (params: { pagePath: string }) => FrameworkTemplateEntry[];\n  defaultSitemap: () => FrameworkTemplateEntry[];\n};\n"
  },
  {
    "path": "packages/cli/src/fs-utils.ts",
    "content": "import { dirname } from \"node:path\";\nimport {\n  access,\n  mkdir,\n  writeFile,\n  constants,\n  readFile,\n} from \"node:fs/promises\";\n\nexport const isFileExists = async (filePath: string) => {\n  try {\n    await access(filePath, constants.F_OK);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport const createFileIfNotExists = async (\n  filePath: string,\n  content?: string\n) => {\n  const dir = dirname(filePath);\n\n  await createFolderIfNotExists(dir);\n\n  try {\n    await access(filePath, constants.F_OK);\n  } catch {\n    await writeFile(filePath, content || \"\", \"utf8\");\n  }\n};\n\nexport const createFolderIfNotExists = async (folderPath: string) => {\n  try {\n    await access(folderPath, constants.F_OK);\n  } catch {\n    await mkdir(folderPath, { recursive: true });\n  }\n};\n\nexport const loadJSONFile = async <T>(filePath: string): Promise<T | null> => {\n  try {\n    const content = await readFile(filePath, \"utf8\");\n    return JSON.parse(content) as T;\n  } catch (error) {\n    return null;\n  }\n};\n"
  },
  {
    "path": "packages/cli/src/html-to-jsx.test.ts",
    "content": "import { test, expect } from \"vitest\";\nimport * as React from \"react\";\nimport ReactDOMServer from \"react-dom/server\";\nimport esbuild from \"esbuild\";\nimport prettier from \"prettier/standalone\";\nimport parserHtml from \"prettier/parser-html\";\n\nimport { htmlToJsx } from \"./html-to-jsx\";\n\n/**\n * Converts HTML to JSX, renders the JSX to HTML.\n */\nconst convertHtmlToJsxAndRenderToHtml = async (htmlCode: string) => {\n  const jsxCode = htmlToJsx(htmlCode);\n\n  const jsxComponentCode = `\n  const Script = ({children, ...props}) => {\n    if (children == null) {\n      return <script {...props} />;\n    }\n\n    return <script {...props} dangerouslySetInnerHTML={{__html: children}} />;\n  };\n\n  const Style = ({children, ...props}) => {\n    if (children == null) {\n      return <style {...props} />;\n    }\n\n    return <style {...props} dangerouslySetInnerHTML={{__html: children}} />;\n  };\n\n  const MyComponent = () => (\n    <>\n    ${jsxCode}\n    </>\n  );`;\n\n  const jsxCompiled = await esbuild.transform(jsxComponentCode, {\n    loader: \"jsx\",\n    format: \"cjs\",\n  });\n\n  const renderFunction = new Function(\n    \"React\",\n    \"ReactDOMServer\",\n    `${jsxCompiled.code}; return ReactDOMServer.renderToString(React.createElement(MyComponent));`\n  );\n\n  const result = renderFunction(React, ReactDOMServer);\n\n  return (\n    result\n      // replace to fix that renderToString(<input type=\"text\" disabled />) results in <input type=\"text\" disabled=\"\">\n      .replace(/\\s(\\w+)=\"\"/g, \" $1\")\n      // &amp; is replaced with & in attributes as its'ok\n      .replace(/(?<=\\w=\"[^\"]*)&amp;(?=[^\"]*\")/g, \"&\")\n      // renderToString can add comment <!-- -->\n      .replace(/\\n\\s*<!-- -->/g, \"\")\n      // <link rel=\"preload is set by react for all eager images\n      .replace(/<link rel=\"preload\".*\\/>/g, \"\")\n  );\n};\n\nconst formatHtml = async (htmlCode: string) => {\n  return await prettier.format(htmlCode, {\n    parser: \"html\",\n    plugins: [parserHtml],\n  });\n};\n\ntest(\"Simple conversion works\", async () => {\n  const htmlCode = `\n    <div id=\"1\" class=\"hello world\">Hello World</div>\n    dsdsd\n    <span>eee</span>\n    <input type=\"text\" disabled />\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Simple script conversion works\", async () => {\n  const htmlCode = `\n    <script>\n      console.log('Hello World');\n    </script>\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Script conversion works with various chars\", async () => {\n  const htmlCode = `\n    <script>\n      const a = {\n        z: 1,\n        y: \\`2\\`,\n      };\n\n      console.log('</'+'Script>' + \"ddd\");\n\n      const z = \\`\n      eee\n      \\`;\n    </script>\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Supports script src and meta\", async () => {\n  const htmlCode = `\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <script src=\"hello/world\" async defer></script>\n  <script src=\"hello/world2\"></script>\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Should not fail\", async () => {\n  const htmlCode = `\n   </s a=\"sd\"><p><a><script src><scri pt>\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n  expect(result).toMatchInlineSnapshot(`\n\"\n   <p><a><script src><scri pt>\n  </script></a></p>\"\n`);\n});\n\ntest(\"noscript works\", async () => {\n  const htmlCode = `\n  <noscript><img height=\"1\" width=\"1\" style=\"display:none\"\n  src=\"https://www.facebook.com/tr?id=1045295173245773&ev=PageView&noscript=1\"\n  /></noscript>\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Skip non modern brosers if\", async () => {\n  const htmlCode = `<!-- [if lt IE 9]><script>console.log('Hello World');</script><![endif] -->`;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n  expect(result).toMatchInlineSnapshot(`\"\"`);\n});\n\ntest(\"Support styles\", async () => {\n  const htmlCode = `\n  <style>\n  .text-field[type=\"email\"]:invalid + div,\n  .text-field[type=\"email\"]:invalid:focus + div,\n  .text-field:valid + label, .form-field-secondary:valid + label\n  {\n      display: block;\n      background-image: linear-gradient(180deg,#c8c7fe,#fbf8ff);\n  }\n  </style>\n\n  <style>\n  @font-face {\n    font-family: \"icons\";\n    src: url(\"data:application/x-font-ttf;charset=utf-8;base64,AAEAAAAA==\") format(\"truetype\");\n    font-weight: normal;\n    font-style: normal;\n  }\n  </style>\n  `;\n\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Supports symbols in attributes\", async () => {\n  const htmlCode = `\n  <script data-cf-beacon='{\"token\": \"124\"}'>\n  </script>\n`;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n\ntest(\"Real User Script Works\", async () => {\n  const htmlCode = `\n  <script type=\"text/javascript\">\n  (function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement(\"script\")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; typeof namespace === \"string\" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); return; } p(cal, ar); }; })(window, \"https://app.cal.com/embed/embed.js\", \"init\");\nCal(\"init\", {origin:\"https://cal.com\"});\n\n  Cal(\"floatingButton\", {\"calLink\":\"fedir-davydov/30min\",\"hideButtonIcon\":true,\"buttonText\":\"Schedule free 30 min call\",\"buttonColor\":\"#0F68EE\",\"buttonTextColor\":\"#FAFAFB\"});\n  Cal(\"ui\", {\"styles\":{\"branding\":{\"brandColor\":\"#000000\"}},\"hideEventTypeDetails\":false,\"layout\":\"month_view\"});\n  </script>\n\n  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':\n  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],\n  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=\n  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);\n  })(window,document,'script','dataLayer','GTM-EEE');</script>\n\n\n  <script>\n  !function(f,b,e,v,n,t,s)\n  {if(f.fbq)return;n=f.fbq=function(){n.callMethod?\n  n.callMethod.apply(n,arguments):n.queue.push(arguments)};\n  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';\n  n.queue=[];t=b.createElement(e);t.async=!0;\n  t.src=v;s=b.getElementsByTagName(e)[0];\n  s.parentNode.insertBefore(t,s)}(window, document,'script',\n  'https://connect.facebook.net/en_US/fbevents.js');\n  fbq('init', '11111');\n  fbq('track', 'PageView');\n  </script>\n\n\n  <script src=\"https://t.usermaven.com/lib.js\"\n      data-key=\"UMdXVreRwT\"\n      data-tracking-host=\"https://events.usermaven.com\"\n      data-autocapture=\"true\"\n      defer>\n  </script>\n  <script>window.usermaven = window.usermaven || (function(){(window.usermavenQ = window.usermavenQ || []).push(arguments);})</script>\n\n  <noscript><img height=\"1\" width=\"1\" style=\"display:none\"\n  src=\"https://www.facebook.com/tr?id=11111&ev=PageView&noscript=1\"\n  /></noscript>\n\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide.min.css\">\n\n  <script type=\"module\">\n  import Chatbot from \"https://cdn.jsdelivr.net/gh/ceelee/FlowiseChatEmbed@latest/dist/web.js\"\n  Chatbot.initFull({\n      chatflowid: \"\",\n      apiHost: \"https://eee.eee-eee-eee-eee.com\",\n      theme: {\n          chatWindow: {\n              welcomeMessage: \"Hi there! How can I help?\",\n          }\n      }\n  })\n  </script>\n  <script>\n    !function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(\".\");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement(\"script\")).type=\"text/javascript\",p.async=!0,p.src=s.api_host.replace(\".i.posthog.com\",\"-assets.i.posthog.com\")+\"/static/array.js\",(r=t.getElementsByTagName(\"script\")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a=\"posthog\",u.people=u.people||[],u.toString=function(t){var e=\"posthog\";return\"posthog\"!==a&&(e+=\".\"+a),t||(e+=\" (stub)\"),e},u.people.toString=function(){return u.toString(1)+\".people (stub)\"},o=\"capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId\".split(\" \"),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);\n    posthog.init('dsdsdsdsdsd',{api_host:'https://us.i.posthog.com', person_profiles: 'identified_only' // or 'always' to create profiles for anonymous users as well\n        })\n  </script>\n  `;\n  const result = await convertHtmlToJsxAndRenderToHtml(htmlCode);\n\n  expect(await formatHtml(htmlCode)).toBe(await formatHtml(result));\n});\n"
  },
  {
    "path": "packages/cli/src/html-to-jsx.ts",
    "content": "import {\n  parseFragment,\n  defaultTreeAdapter,\n  type DefaultTreeAdapterMap,\n} from \"parse5\";\nimport { camelCase } from \"change-case\";\n\nconst BOOLEAN_ATTRIBUTES = new Set([\n  \"async\",\n  \"autofocus\",\n  \"autoplay\",\n  \"checked\",\n  \"contenteditable\",\n  \"controls\",\n  \"default\",\n  \"defer\",\n  \"disabled\",\n  \"formnovalidate\",\n  \"hidden\",\n  \"ismap\",\n  \"itemscope\",\n  \"loop\",\n  \"multiple\",\n  \"muted\",\n  \"nomodule\",\n  \"novalidate\",\n  \"open\",\n  \"playsinline\",\n  \"readonly\",\n  \"required\",\n  \"reversed\",\n  \"scoped\",\n  \"selected\",\n  \"truespeed\",\n]);\n\nconst isBooleanAttr = (name: string) =>\n  BOOLEAN_ATTRIBUTES.has(name.toLowerCase());\n\ntype WalkNode =\n  | { type: \"text\"; value: string }\n  | {\n      type: \"element-start\";\n      tagName: string;\n      attributes: [name: string, value: string][];\n    }\n  | { type: \"element-end\"; tagName: string };\n\n// eslint-disable-next-line func-style\nfunction* walkChildNodes(\n  node: DefaultTreeAdapterMap[\"node\"]\n): IterableIterator<WalkNode> {\n  if (\n    defaultTreeAdapter.isCommentNode(node) ||\n    defaultTreeAdapter.isTextNode(node) ||\n    defaultTreeAdapter.isDocumentTypeNode(node)\n  ) {\n    throw new Error(\"Unsupported node type\");\n  }\n\n  for (const childNode of node.childNodes) {\n    if (defaultTreeAdapter.isCommentNode(childNode)) {\n      continue;\n    }\n\n    if (defaultTreeAdapter.isTextNode(childNode)) {\n      yield { type: \"text\", value: childNode.value };\n      continue;\n    }\n\n    if (false === defaultTreeAdapter.isElementNode(childNode)) {\n      continue;\n    }\n\n    const attributes: [string, string][] = childNode.attrs.map((attr) => [\n      attr.name,\n      attr.value,\n    ]);\n\n    yield { type: \"element-start\", tagName: childNode.tagName, attributes };\n    yield* walkChildNodes(childNode);\n    yield { type: \"element-end\", tagName: childNode.tagName };\n  }\n}\n\nconst convertStyleString = (style: string) => {\n  const styles = style\n    .split(\";\")\n    .map((style) => style.trim())\n    .map((style) => style.split(\":\").map((part) => part.trim()));\n\n  const res: Record<string, string> = {};\n  for (const [name, value] of styles) {\n    res[camelCase(name)] = value;\n  }\n  return JSON.stringify(res);\n};\n\nconst escape = (value: string) => JSON.stringify(value);\n\nconst toAttrString = (name: string, value: string) => {\n  const attName = name.toLowerCase();\n  const jsxName = attName === \"class\" ? \"className\" : attName;\n\n  if (value === \"\" && isBooleanAttr(attName)) {\n    return `${jsxName}`;\n  }\n\n  if (attName === \"style\") {\n    return `${jsxName}={${convertStyleString(value)}}`;\n  }\n\n  return `${jsxName}={${escape(value)}}`;\n};\n\nconst attributesToString = (attributes: [string, string][]) =>\n  attributes\n    .map(([attName, value]) => ` ${toAttrString(attName, value)}`)\n    .join(\"\");\n\nconst convertTagName = (tagName: string) => {\n  const tag = tagName.toLowerCase();\n  if (tag === \"script\") {\n    return \"Script\";\n  }\n\n  if (tag === \"style\") {\n    return \"Style\";\n  }\n\n  return tag;\n};\n\nexport const htmlToJsx = (html: string) => {\n  const parsedHtml = parseFragment(html, { scriptingEnabled: false });\n\n  let result = \"\";\n\n  for (const walkNode of walkChildNodes(parsedHtml)) {\n    switch (walkNode.type) {\n      case \"text\": {\n        const escapedValue = escape(walkNode.value);\n\n        const re = /^\\s$/g;\n        if (re.test(walkNode.value)) {\n          break;\n        }\n\n        result += escapedValue ? \"{\" + escapedValue + \"}\" : \"\";\n        break;\n      }\n\n      case \"element-start\": {\n        const tag = convertTagName(walkNode.tagName);\n        result += `<${tag}${attributesToString(walkNode.attributes)}>`;\n        break;\n      }\n\n      case \"element-end\": {\n        result += `</${convertTagName(walkNode.tagName)}>`;\n        break;\n      }\n    }\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "packages/cli/src/prebuild.ts",
    "content": "import { basename, dirname, join, normalize, relative } from \"node:path\";\nimport { createWriteStream, existsSync } from \"node:fs\";\nimport {\n  rm,\n  access,\n  rename,\n  cp,\n  readFile,\n  writeFile,\n  readdir,\n} from \"node:fs/promises\";\nimport { pipeline } from \"node:stream/promises\";\nimport { cwd, exit } from \"node:process\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport pLimit from \"p-limit\";\nimport { log, spinner } from \"@clack/prompts\";\nimport merge from \"deepmerge\";\nimport {\n  generateWebstudioComponent,\n  type Params,\n  normalizeProps,\n  generateRemixRoute,\n  generateRemixParams,\n} from \"@webstudio-is/react-sdk\";\nimport type {\n  Instance,\n  Prop,\n  Page,\n  DataSource,\n  Deployment,\n  Asset,\n  Resource,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport {\n  createScope,\n  findTreeInstanceIds,\n  getPagePath,\n  generateResources,\n  generatePageMeta,\n  getStaticSiteMapXml,\n  replaceFormActionsWithResources,\n  isCoreComponent,\n  coreMetas,\n  SYSTEM_VARIABLE_ID,\n  generateCss,\n  ROOT_INSTANCE_ID,\n  elementComponent,\n  getAssetUrl,\n  toRuntimeAsset,\n} from \"@webstudio-is/sdk\";\nimport type { Data } from \"@webstudio-is/http-client\";\nimport { LOCAL_DATA_FILE } from \"./config\";\nimport {\n  createFileIfNotExists,\n  createFolderIfNotExists,\n  loadJSONFile,\n} from \"./fs-utils\";\nimport type * as sharedConstants from \"../templates/defaults/app/constants.mjs\";\nimport { htmlToJsx } from \"./html-to-jsx\";\nimport { createFramework as createRemixFramework } from \"./framework-remix\";\nimport { createFramework as createReactRouterFramework } from \"./framework-react-router\";\nimport { createFramework as createVikeSsgFramework } from \"./framework-vike-ssg\";\nimport { compareMedia } from \"@webstudio-is/css-engine\";\n\nconst limit = pLimit(10);\n\ntype SiteDataByPage = {\n  [id: Page[\"id\"]]: {\n    page: Page;\n    build: {\n      props: [Prop[\"id\"], Prop][];\n      instances: [Instance[\"id\"], Instance][];\n      dataSources: [DataSource[\"id\"], DataSource][];\n      resources: [Resource[\"id\"], Resource][];\n      deployment?: Deployment | undefined;\n    };\n    assets: Array<Asset>;\n    params?: Params;\n    pages: Array<Page>;\n  };\n};\n\nexport const downloadAsset = async (\n  url: string,\n  name: string,\n  assetBaseUrl: string\n) => {\n  const assetPath = join(\"public\", assetBaseUrl, name);\n  // fs.rename cannot be used to move a file to a different mount point or drive\n  // Error: EXDEV: cross-device link not permitted\n  const tempAssetPath = `${assetPath}.tmp`;\n\n  try {\n    await access(assetPath);\n  } catch {\n    await createFolderIfNotExists(dirname(assetPath));\n\n    try {\n      const response = await fetch(url);\n      if (!response.ok) {\n        throw new Error(`Failed to fetch ${url}: ${response.statusText}`);\n      }\n\n      const writableStream = createWriteStream(tempAssetPath);\n      /*\n        We need to cast the response body to a NodeJS.ReadableStream.\n        Since the node typings for `@types/node` doesn't add typings for fetch.\n        And it inherits types from lib.dom.d.ts\n      */\n      await pipeline(\n        response.body as unknown as NodeJS.ReadableStream,\n        writableStream\n      );\n\n      await rename(tempAssetPath, assetPath);\n    } catch (error) {\n      console.error(`Error in downloading file ${name} \\n ${error}`);\n    }\n  }\n};\n\nconst mergeJsonInto = async (sourcePath: string, destinationPath: string) => {\n  const sourceJson = await readFile(sourcePath, \"utf8\");\n  const destinationJson = await readFile(destinationPath, \"utf8\").catch(\n    (error) => {\n      if (\n        error instanceof Error &&\n        \"code\" in error &&\n        error.code === \"ENOENT\"\n      ) {\n        return \"{}\";\n      }\n\n      throw new Error(error);\n    }\n  );\n  const content = JSON.stringify(\n    merge(JSON.parse(destinationJson), JSON.parse(sourceJson), {\n      arrayMerge: (_target, source) => source,\n    }),\n    null,\n    \"  \"\n  );\n\n  await writeFile(destinationPath, content, \"utf8\");\n};\n\n/**\n * Check if template is internal cli template or external path\n */\nconst isCliTemplate = async (template: string) => {\n  const currentPath = fileURLToPath(new URL(import.meta.url));\n\n  const templatesPath = normalize(\n    join(dirname(currentPath), \"..\", \"templates\")\n  );\n\n  const dirents = await readdir(templatesPath, { withFileTypes: true });\n\n  for (const dirent of dirents) {\n    if (dirent.isDirectory() && dirent.name === template) {\n      return true;\n    }\n  }\n  return false;\n};\n\n/**\n * template can be internal cli template or external path\n */\nconst getTemplatePath = async (template: string) => {\n  const currentPath = fileURLToPath(new URL(import.meta.url));\n\n  const templatePath = (await isCliTemplate(template))\n    ? normalize(join(dirname(currentPath), \"..\", \"templates\", template))\n    : template;\n\n  return templatePath;\n};\n\nconst copyTemplates = async (template: string) => {\n  const templatePath = await getTemplatePath(template);\n\n  await cp(templatePath, cwd(), {\n    recursive: true,\n    filter: (source) => {\n      const name = basename(source);\n      return name !== \"package.json\" && name !== \"tsconfig.json\";\n    },\n  });\n\n  if (existsSync(join(templatePath, \"package.json\"))) {\n    await mergeJsonInto(\n      join(templatePath, \"package.json\"),\n      join(cwd(), \"package.json\")\n    );\n  }\n  if (existsSync(join(templatePath, \"tsconfig.json\"))) {\n    await mergeJsonInto(\n      join(templatePath, \"tsconfig.json\"),\n      join(cwd(), \"tsconfig.json\")\n    );\n  }\n};\n\nconst importFrom = (importee: string, importer: string) => {\n  return relative(dirname(importer), importee).replaceAll(\"\\\\\", \"/\");\n};\n\nconst npmrc = `force=true\nloglevel=error\naudit=false\nfund=false\n`;\n\nexport const prebuild = async (options: {\n  /**\n   * Do we need download assets\n   **/\n  assets: boolean;\n  /**\n   * Template to use for the build in addition to defaults template\n   **/\n  template: string[];\n}) => {\n  if (options.template.length === 0) {\n    log.error(\n      `Template is not provided\\nPlease check webstudio --help for more details`\n    );\n    exit(1);\n  }\n\n  for (const template of options.template) {\n    // Template is local user template\n    if (template.startsWith(\".\") || template.startsWith(\"/\")) {\n      continue;\n    }\n\n    if ((await isCliTemplate(template)) === false) {\n      log.error(\n        `Template ${options.template} is not available\\nPlease check webstudio --help for more details`\n      );\n      exit(1);\n    }\n  }\n\n  log.step(\"Scaffolding the project files\");\n\n  const appRoot = \"app\";\n\n  const generatedDir = join(appRoot, \"__generated__\");\n  await rm(generatedDir, { recursive: true, force: true });\n\n  const routesDir = join(appRoot, \"routes\");\n  await rm(routesDir, { recursive: true, force: true });\n\n  // force npm to install with not matching peer dependencies\n  await writeFile(join(cwd(), \".npmrc\"), npmrc);\n\n  for (const template of options.template) {\n    await copyTemplates(template);\n  }\n\n  let framework;\n  if (options.template.includes(\"ssg\")) {\n    framework = await createVikeSsgFramework();\n  } else if (options.template.includes(\"react-router\")) {\n    framework = await createReactRouterFramework();\n  } else {\n    framework = await createRemixFramework();\n  }\n\n  const constants: typeof sharedConstants = await import(\n    pathToFileURL(join(cwd(), \"app/constants.mjs\")).href\n  );\n\n  const { assetBaseUrl } = constants;\n\n  const siteData = await loadJSONFile<\n    Data & { user?: { email: string | null } }\n  >(LOCAL_DATA_FILE);\n\n  if (siteData === null) {\n    throw new Error(\n      `Project data is missing, please make sure you the project is synced.`\n    );\n  }\n\n  const usedMetas = new Map<Instance[\"component\"], WsComponentMeta>(\n    Object.entries(coreMetas)\n  );\n  const siteDataByPage: SiteDataByPage = {};\n  const fontAssetsByPage: Record<Page[\"id\"], string[]> = {};\n  const backgroundImageAssetsByPage: Record<Page[\"id\"], string[]> = {};\n\n  // use whole project props to access id props from other pages\n  const normalizedProps = normalizeProps({\n    props: siteData.build.props.map(([_id, prop]) => prop),\n    assetBaseUrl,\n    assets: new Map(siteData.assets.map((asset) => [asset.id, asset])),\n    uploadingImageAssets: [],\n    pages: siteData.build.pages,\n    source: \"prebuild\",\n  });\n\n  for (const page of Object.values(siteData.pages)) {\n    const instanceMap = new Map(siteData.build.instances);\n    const pageInstanceSet = findTreeInstanceIds(\n      instanceMap,\n      page.rootInstanceId\n    );\n    // support global data variables\n    pageInstanceSet.add(ROOT_INSTANCE_ID);\n    // collect used instances and metas\n    const instances: [Instance[\"id\"], Instance][] = [];\n    for (const [_instanceId, instance] of siteData.build.instances) {\n      if (pageInstanceSet.has(instance.id)) {\n        instances.push([instance.id, instance]);\n        const meta = framework.metas[instance.component];\n        if (meta) {\n          usedMetas.set(instance.component, meta);\n        }\n      }\n    }\n\n    const resourceIds = new Set<Resource[\"id\"]>();\n\n    const props: [Prop[\"id\"], Prop][] = [];\n    for (const prop of normalizedProps) {\n      if (pageInstanceSet.has(prop.instanceId)) {\n        props.push([prop.id, prop]);\n        if (prop.type === \"resource\") {\n          resourceIds.add(prop.value);\n        }\n      }\n    }\n\n    const dataSources: [DataSource[\"id\"], DataSource][] = [];\n    for (const [dataSourceId, dataSource] of siteData.build.dataSources) {\n      if (pageInstanceSet.has(dataSource.scopeInstanceId ?? \"\")) {\n        dataSources.push([dataSourceId, dataSource]);\n        if (dataSource.type === \"resource\") {\n          resourceIds.add(dataSource.resourceId);\n        }\n      }\n    }\n\n    const resources: [Resource[\"id\"], Resource][] = [];\n    for (const [resourceId, resource] of siteData.build.resources ?? []) {\n      if (resourceIds.has(resourceId)) {\n        resources.push([resourceId, resource]);\n      }\n    }\n\n    siteDataByPage[page.id] = {\n      build: {\n        props,\n        instances,\n        dataSources,\n        resources,\n      },\n      pages: siteData.pages,\n      page,\n      assets: siteData.assets,\n    };\n\n    // Extract background SVGs and Font assets\n    const styleSourceSelections = siteData.build?.styleSourceSelections ?? [];\n    const pageStyleSourceIds = new Set(\n      styleSourceSelections\n        .filter(([, { instanceId }]) => pageInstanceSet.has(instanceId))\n        .map(([, { values }]) => values)\n        .flat()\n    );\n\n    const pageStyles = siteData.build?.styles?.filter(([, { styleSourceId }]) =>\n      pageStyleSourceIds.has(styleSourceId)\n    );\n\n    // Extract fonts\n    const pageFontFamilySet = new Set(\n      pageStyles\n        .filter(([, { property }]) => property === \"fontFamily\")\n        .map(([, { value }]) =>\n          value.type === \"fontFamily\" ? value.value : undefined\n        )\n        .flat()\n        .filter(<T>(value: T): value is NonNullable<T> => value !== undefined)\n    );\n\n    const pageFontAssets = siteData.assets\n      .filter((asset) => asset.type === \"font\")\n      .filter((fontAsset) => pageFontFamilySet.has(fontAsset.meta.family))\n      .map((asset) => asset.name);\n\n    fontAssetsByPage[page.id] = pageFontAssets;\n\n    // Extract background images\n    // backgroundImage => \"value.type==\"layers\" => value.type == \"image\" => .value (assetId)\n    const backgroundImageAssetIdSet = new Set(\n      pageStyles\n        .filter(([, { property }]) => property === \"backgroundImage\")\n        .map(([, { value }]) =>\n          value.type === \"layers\"\n            ? value.value.map((layer) =>\n                layer.type === \"image\"\n                  ? layer.value.type === \"asset\"\n                    ? layer.value.value\n                    : undefined\n                  : undefined\n              )\n            : undefined\n        )\n        .flat()\n        .filter(<T>(value: T): value is NonNullable<T> => value !== undefined)\n    );\n\n    const backgroundImageAssets = siteData.assets\n      .filter((asset) => asset.type === \"image\")\n      .filter((imageAsset) => backgroundImageAssetIdSet.has(imageAsset.id))\n      .map((asset) => asset.name);\n\n    backgroundImageAssetsByPage[page.id] = backgroundImageAssets;\n  }\n\n  const assetsToDownload: Promise<void>[] = [];\n\n  if (options.assets === true) {\n    const assetOrigin = siteData.origin;\n\n    if (!assetOrigin) {\n      console.warn(\"Warning: Asset origin is not defined in project data.\");\n    }\n\n    for (const asset of siteData.assets) {\n      // Download all assets (images, fonts, videos, audio, documents, etc.)\n      assetsToDownload.push(\n        limit(() =>\n          downloadAsset(\n            getAssetUrl(asset, assetOrigin || \"\").href,\n            asset.name,\n            assetBaseUrl\n          )\n        )\n      );\n    }\n  }\n\n  const assets = new Map(siteData.assets.map((asset) => [asset.id, asset]));\n\n  const { cssText, classes } = generateCss({\n    instances: new Map(siteData.build.instances),\n    props: new Map(siteData.build.props),\n    assets,\n    breakpoints: new Map(siteData.build?.breakpoints),\n    styles: new Map(siteData.build?.styles),\n    styleSourceSelections: new Map(siteData.build?.styleSourceSelections),\n    // pass only used metas to not generate unused preset styles\n    componentMetas: usedMetas,\n    assetBaseUrl,\n    atomic: siteData.build.pages.compiler?.atomicStyles ?? true,\n  });\n\n  await createFileIfNotExists(join(generatedDir, \"index.css\"), cssText);\n\n  for (const page of Object.values(siteData.pages)) {\n    const scope = createScope([\n      // manually maintained list of occupied identifiers\n      \"useState\",\n      \"Fragment\",\n      \"useResource\",\n      \"useVariableState\",\n      \"Page\",\n      \"_props\",\n    ]);\n\n    const pageData = siteDataByPage[page.id];\n    const instances = new Map(pageData.build.instances);\n    const documentType = page.meta.documentType ?? \"html\";\n    let rootInstanceId = page.rootInstanceId;\n\n    // cleanup xml markup\n    if (documentType === \"xml\") {\n      // treat first body child as root\n      const bodyInstance = instances.get(rootInstanceId);\n      // @todo test empty xml\n      const firstChild = bodyInstance?.children.at(0);\n      if (firstChild?.type === \"id\") {\n        rootInstanceId = firstChild.value;\n      }\n      // remove all unexpected components\n      for (const instance of instances.values()) {\n        if (isCoreComponent(instance.component)) {\n          continue;\n        }\n        if (usedMetas.get(instance.component)?.category === \"xml\") {\n          continue;\n        }\n        instances.delete(instance.id);\n      }\n    }\n\n    // generate component imports\n    // Map<importSource, Map<id, importSpecifier>>\n    const imports = new Map<string, Map<string, string>>();\n    for (const instance of instances.values()) {\n      let descriptor = framework.components[instance.component];\n      let id = instance.component;\n      if (instance.component === elementComponent && instance.tag) {\n        descriptor = framework.tags[instance.tag];\n        id = descriptor;\n      }\n      if (descriptor === undefined) {\n        continue;\n      }\n      const [importSource, importSpecifier] = descriptor.split(\":\");\n      let specifiers = imports.get(importSource);\n      if (specifiers === undefined) {\n        specifiers = new Map();\n        imports.set(importSource, specifiers);\n      }\n      specifiers.set(id, importSpecifier);\n    }\n    let importsString = \"\";\n    for (const [importSource, specifiers] of imports) {\n      const specifiersString = Array.from(specifiers)\n        .map(\n          ([id, importSpecifier]) =>\n            `${importSpecifier} as ${scope.getName(id, importSpecifier)}`\n        )\n        .join(\", \");\n      importsString += `import { ${specifiersString} } from \"${importSource}\";\\n`;\n    }\n\n    const pageFontAssets = fontAssetsByPage[page.id];\n    const pageBackgroundImageAssets = backgroundImageAssetsByPage[page.id];\n\n    const props = new Map(pageData.build.props);\n    const dataSources = new Map(pageData.build.dataSources);\n    const resources = new Map(pageData.build.resources);\n    replaceFormActionsWithResources({\n      instances,\n      resources,\n      props,\n    });\n    const pageComponent = generateWebstudioComponent({\n      scope,\n      name: \"Page\",\n      rootInstanceId,\n      parameters: [\n        {\n          id: `page-system`,\n          instanceId: \"\",\n          name: \"system\",\n          type: \"parameter\",\n          value: page.systemDataSourceId ?? \"\",\n        },\n        {\n          id: \"global-system\",\n          type: \"parameter\",\n          instanceId: \"\",\n          name: \"system\",\n          value: SYSTEM_VARIABLE_ID,\n        },\n      ],\n      instances,\n      props,\n      dataSources,\n      classesMap: classes,\n      metas: usedMetas,\n      tagsOverrides: framework.tags,\n    });\n\n    const projectMeta = siteData.build.pages.meta;\n    const contactEmail: undefined | string =\n      // fallback to user email when contact email is empty string\n      projectMeta?.contactEmail || siteData.user?.email || undefined;\n    const favIconAsset = assets.get(projectMeta?.faviconAssetId ?? \"\")?.name;\n\n    const pagePath = getPagePath(page.id, siteData.build.pages);\n\n    const breakpoints = siteData.build.breakpoints\n      .map(([_, value]) => ({\n        id: value.id,\n        minWidth: value.minWidth,\n        maxWidth: value.maxWidth,\n      }))\n      .sort(compareMedia);\n\n    // MARK: - TODO: XML GENERATION\n    const pageExports = `/* eslint-disable */\n      /* This is a auto generated file for building the project */ \\n\n\n      import { Fragment, useState } from \"react\";\n      import { useResource, useVariableState } from \"@webstudio-is/react-sdk/runtime\";\n      ${importsString}\n\n      export const projectId = \"${siteData.build.projectId}\";\n\n      export const lastPublished = \"${new Date(siteData.build.createdAt).toISOString()}\";\n\n      export const siteName = ${JSON.stringify(projectMeta?.siteName)};\n\n      export const breakpoints = ${JSON.stringify(breakpoints)};\n\n      export const favIconAsset: string | undefined =\n        ${JSON.stringify(favIconAsset)};\n\n      // Font assets on current page (can be preloaded)\n      export const pageFontAssets: string[] =\n        ${JSON.stringify(pageFontAssets)}\n\n      export const pageBackgroundImageAssets: string[] =\n        ${JSON.stringify(pageBackgroundImageAssets)}\n\n      ${\n        pagePath === \"/\"\n          ? `\n            ${\n              projectMeta?.code\n                ? `\n            const Script = ({children, ...props}: Record<string, string | boolean>) => {\n              if (children == null) {\n                return <script {...props} />;\n              }\n\n              return <script {...props} dangerouslySetInnerHTML={{__html: children}} />;\n            };\n            const Style = ({children, ...props}: Record<string, string | boolean>) => {\n              if (children == null) {\n                return <style {...props} />;\n              }\n\n              return <style {...props} dangerouslySetInnerHTML={{__html: children}} />;\n            };\n            `\n                : \"\"\n            }\n\n            export const CustomCode = () => {\n              return (<>${projectMeta?.code ? htmlToJsx(projectMeta.code) : \"\"}</>);\n            }\n          `\n          : \"\"\n      }\n\n      ${pageComponent}\n\n      export { Page }\n    `;\n\n    const serverExports = `/* eslint-disable */\n      /* This is a auto generated file for building the project */ \\n\n\n      import type { PageMeta } from \"@webstudio-is/sdk\";\n      ${generateResources({\n        scope,\n        page,\n        dataSources,\n        props,\n        resources,\n      })}\n\n      ${generatePageMeta({\n        globalScope: scope,\n        page,\n        dataSources,\n        assets,\n      })}\n\n      ${generateRemixParams(page.path)}\n\n      export const contactEmail = ${JSON.stringify(contactEmail)};\n    `;\n\n    const generatedBasename = generateRemixRoute(pagePath);\n\n    const clientFile = join(generatedDir, `${generatedBasename}.tsx`);\n    await createFileIfNotExists(clientFile, pageExports);\n\n    const serverFile = join(generatedDir, `${generatedBasename}.server.tsx`);\n    await createFileIfNotExists(serverFile, serverExports);\n\n    const getTemplates =\n      documentType === \"html\" ? framework.html : framework.xml;\n    for (const { file, template } of getTemplates({ pagePath })) {\n      const content = template\n        .replaceAll(\"__CONSTANTS__\", importFrom(\"./app/constants.mjs\", file))\n        .replaceAll(\n          \"__SITEMAP__\",\n          importFrom(`./app/__generated__/$resources.sitemap.xml`, file)\n        )\n        .replaceAll(\n          \"__ASSETS__\",\n          importFrom(`./app/__generated__/$resources.assets`, file)\n        )\n        .replaceAll(\n          \"__CLIENT__\",\n          importFrom(`./app/__generated__/${generatedBasename}`, file)\n        )\n        .replaceAll(\n          \"__SERVER__\",\n          importFrom(`./app/__generated__/${generatedBasename}.server`, file)\n        )\n        .replaceAll(\n          \"__CSS__\",\n          importFrom(`./app/__generated__/index.css`, file)\n        );\n      await createFileIfNotExists(file, content);\n    }\n  }\n\n  // MARK: - Default sitemap.xml\n  for (const { file, template } of framework.defaultSitemap()) {\n    const content = template.replaceAll(\n      \"__SITEMAP__\",\n      importFrom(`./app/__generated__/$resources.sitemap.xml`, file)\n    );\n    await createFileIfNotExists(file, content);\n  }\n\n  await createFileIfNotExists(\n    join(generatedDir, \"$resources.sitemap.xml.ts\"),\n    `\n      export const sitemap = ${JSON.stringify(\n        getStaticSiteMapXml(siteData.build.pages, siteData.build.updatedAt),\n        null,\n        2\n      )};\n    `\n  );\n\n  // Generate assets resource file\n  // Assets use /cgi/ endpoints on both builder and published sites\n  // Use a placeholder origin for URL construction, result will be relative paths\n  const assetsById = Object.fromEntries(\n    siteData.assets.map((asset) => [\n      asset.id,\n      toRuntimeAsset(asset, \"https://placeholder.local\"),\n    ])\n  );\n\n  await createFileIfNotExists(\n    join(generatedDir, \"$resources.assets.ts\"),\n    `\n    export const assets = ${JSON.stringify(assetsById, null, 2)};\n    `\n  );\n\n  const redirects = siteData.build.pages?.redirects;\n  if (redirects !== undefined && redirects.length > 0) {\n    for (const redirect of redirects) {\n      const generatedBasename = generateRemixRoute(redirect.old);\n      await createFileIfNotExists(\n        join(generatedDir, `${generatedBasename}.ts`),\n        `\n        export const url = \"${redirect.new}\";\n        export const status = ${redirect.status ?? 301};\n        `\n      );\n\n      for (const { file, template } of framework.redirect({\n        pagePath: redirect.old,\n      })) {\n        const content = template.replaceAll(\n          \"__REDIRECT__\",\n          importFrom(`./app/__generated__/${generatedBasename}`, file)\n        );\n        await createFileIfNotExists(file, content);\n      }\n    }\n  }\n\n  if (assetsToDownload.length > 0) {\n    const downloading = spinner();\n    downloading.start(\"Downloading fonts and images\");\n    await Promise.all(assetsToDownload);\n    downloading.stop(\"Downloaded fonts and images\");\n  }\n\n  log.step(\"Build finished\");\n};\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/WS_CF_README.md",
    "content": "# Welcome to Remix + Vite!\n\n📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features.\n\n## Typegen\n\nGenerate types for your Cloudflare bindings in `wrangler.toml`:\n\n```sh\nnpm run typegen\n```\n\nYou will need to rerun typegen whenever you make changes to `wrangler.toml`.\n\n## Development\n\nRun the Vite dev server:\n\n```sh\nnpm run dev\n```\n\nTo run Wrangler:\n\n```sh\nnpm run build\nnpm run start\n```\n\n## Deployment\n\n> [!WARNING]  \n> Cloudflare does _not_ use `wrangler.toml` to configure deployment bindings.\n> You **MUST** [configure deployment bindings manually in the Cloudflare dashboard][bindings].\n\nFirst, build your app for production:\n\n```sh\nnpm run build\n```\n\nThen, deploy your app to Cloudflare Pages:\n\n```sh\nnpm run deploy\n```\n\n[bindings]: https://developers.cloudflare.com/pages/functions/bindings/\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/functions/[[path]].ts",
    "content": "import { createPagesFunctionHandler } from \"@remix-run/cloudflare-pages\";\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - the server build file is generated by `remix vite:build`\nimport * as build from \"../build/server\";\n\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - the server build file is generated by `remix vite:build`\nexport const onRequest = createPagesFunctionHandler({ build });\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/load-context.ts",
    "content": "import { type PlatformProxy } from \"wrangler\";\n\ntype Cloudflare = Omit<PlatformProxy<Env>, \"dispose\">;\n\ndeclare module \"@remix-run/cloudflare\" {\n  interface AppLoadContext {\n    cloudflare: Cloudflare;\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/package.json",
    "content": "{\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"remix vite:build\",\n    \"deploy\": \"npm run build && wrangler pages deploy ./build/client\",\n    \"dev\": \"remix vite:dev\",\n    \"start\": \"wrangler pages dev ./build/client\",\n    \"typegen\": \"wrangler types\",\n    \"preview\": \"npm run build && wrangler pages dev ./build/client\",\n    \"build-cf-types\": \"wrangler types\"\n  },\n  \"dependencies\": {\n    \"@remix-run/cloudflare\": \"2.16.5\",\n    \"@remix-run/cloudflare-pages\": \"2.16.5\"\n  },\n  \"devDependencies\": {\n    \"@cloudflare/workers-types\": \"^4.20240620.0\",\n    \"wrangler\": \"^3.63.2\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\"\n  ],\n  \"compilerOptions\": {\n    \"types\": [\n      \"@remix-run/cloudflare\",\n      \"vite/client\",\n      \"@cloudflare/workers-types/2023-07-01\",\n      \"@webstudio-is/react-sdk/placeholder\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/vite.config.ts",
    "content": "import {\n  vitePlugin as remix,\n  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,\n} from \"@remix-run/dev\";\nimport { defineConfig } from \"vite\";\n\nexport default defineConfig(({ mode }) => ({\n  plugins: [\n    // without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)\n    mode === \"production\" ? undefined : remixCloudflareDevProxy(),\n    remix({\n      future: {\n        v3_lazyRouteDiscovery: false,\n        v3_relativeSplatPath: false,\n        v3_singleFetch: false,\n        v3_fetcherPersist: false,\n        v3_throwAbortReason: false,\n      },\n    }),\n  ].filter(Boolean),\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n}));\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/worker-configuration.d.ts",
    "content": "// Generated by Wrangler\n// After adding bindings to `wrangler.toml`, regenerate this interface via `npm build-cf-types`\ninterface Env {}\n"
  },
  {
    "path": "packages/cli/templates/cloudflare/wrangler.toml",
    "content": "#:schema node_modules/wrangler/config-schema.json\n# https://developers.cloudflare.com/pages/functions/wrangler-configuration/\nname = \"webstudio-remix-app\"\ncompatibility_date = \"2024-04-05\"\npages_build_output_dir=\"./build\"\n\n# Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)\n# Note: Use secrets to store sensitive data.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#environment-variables\n# [vars]\n# MY_VARIABLE = \"production_value\"\n\n# Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai\n# [ai]\n# binding = \"AI\"\n\n# Bind a D1 database. D1 is Cloudflare’s native serverless SQL database.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases\n# [[d1_databases]]\n# binding = \"MY_DB\"\n# database_name = \"my-database\"\n# database_id = \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n\n# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.\n# Durable Objects can live for as long as needed. Use these when you need a long-running \"server\", such as in realtime apps.\n# Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects\n# [[durable_objects.bindings]]\n# name = \"MY_DURABLE_OBJECT\"\n# class_name = \"MyDurableObject\"\n# script_name = 'my-durable-object'\n\n# Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces\n# [[kv_namespaces]]\n# binding = \"MY_KV_NAMESPACE\"\n# id = \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\n# Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers\n# [[queues.producers]]\n# binding = \"MY_QUEUE\"\n# queue = \"my-queue\"\n\n# Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets\n# [[r2_buckets]]\n# binding = \"MY_BUCKET\"\n# bucket_name = \"my-bucket\"\n\n# Bind another Worker service. Use this binding to call another Worker without network overhead.\n# Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings\n# [[services]]\n# binding = \"MY_SERVICE\"\n# service = \"my-service\""
  },
  {
    "path": "packages/cli/templates/defaults/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = ({ src }) => {\n  return src;\n};\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/extension.ts",
    "content": "// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nimport { AppLoadContext } from \"@remix-run/server-runtime\";\nimport { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"@remix-run/server-runtime\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"@remix-run/react\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/route-templates/default-sitemap.tsx",
    "content": "import type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\nimport { sitemap } from \"__SITEMAP__\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/route-templates/html.tsx",
    "content": "import {\n  type ServerRuntimeMetaFunction as MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  json,\n  redirect,\n} from \"@remix-run/server-runtime\";\nimport { useLoaderData } from \"@remix-run/react\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  cachedFetch,\n  formIdFieldName,\n  formBotFieldName,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n  PageSettingsCanonicalLink,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"__CLIENT__\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"__SERVER__\";\nimport * as constants from \"__CONSTANTS__\";\nimport css from \"__CSS__?url\";\nimport { sitemap } from \"__SITEMAP__\";\nimport { assets } from \"__ASSETS__\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    return redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return json(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n      <PageSettingsCanonicalLink href={url} />\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/route-templates/redirect.tsx",
    "content": "import { redirect } from \"@remix-run/server-runtime\";\nimport { url, status } from \"__REDIRECT__\";\n\nexport const loader = () => {\n  return redirect(url, status);\n};\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/route-templates/xml.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { type LoaderFunctionArgs, redirect } from \"@remix-run/server-runtime\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  xmlNodeTagSuffix,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { Page, breakpoints } from \"__CLIENT__\";\nimport { getPageMeta, getRemixParams, getResources } from \"__SERVER__\";\nimport { assetBaseUrl, imageLoader } from \"__CONSTANTS__\";\nimport { sitemap } from \"__SITEMAP__\";\nimport { assets } from \"__ASSETS__\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    return redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  let text = renderToString(\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      <Page system={system} />\n    </ReactSdkContext.Provider>\n  );\n\n  // React has issues rendering certain elements, such as errors when a <link> element has children.\n  // To render XML, we wrap it with an <svg> tag and add a suffix to avoid React's default behavior on these elements.\n  text = text.replaceAll(xmlNodeTagSuffix, \"\");\n\n  return new Response(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${text}`, {\n    headers: { \"Content-Type\": \"application/xml\" },\n  });\n};\n"
  },
  {
    "path": "packages/cli/templates/defaults/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"@remix-run/server-runtime\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "packages/cli/templates/defaults/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"remix vite:build\",\n    \"dev\": \"remix vite:dev\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@remix-run/node\": \"2.16.5\",\n    \"@remix-run/react\": \"2.16.5\",\n    \"@remix-run/server-runtime\": \"2.16.5\",\n    \"@webstudio-is/image\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/react-sdk\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-animation\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react-radix\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react-remix\": \"0.0.0-webstudio-version\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"devDependencies\": {\n    \"@remix-run/dev\": \"2.16.5\",\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/defaults/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"@remix-run/node\",\n      \"vite/client\",\n      \"@webstudio-is/react-sdk/placeholder\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/defaults/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { vitePlugin as remix } from \"@remix-run/dev\";\n\nexport default defineConfig({\n  plugins: [\n    remix({\n      future: {\n        v3_lazyRouteDiscovery: false,\n        v3_relativeSplatPath: false,\n        v3_singleFetch: false,\n        v3_fetcherPersist: false,\n        v3_throwAbortReason: false,\n      },\n    }),\n  ],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/templates/internal/.npmrc",
    "content": "force=true\n# to support using NODE_OPTIONS for windows tests\nshell-emulator=true\n"
  },
  {
    "path": "packages/cli/templates/internal/package.json",
    "content": "{\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-animation\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-radix\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react-remix\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/internal/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/extension.ts",
    "content": "import { ResourceRequest } from \"@webstudio-is/sdk\";\n\ndeclare module \"react-router\" {\n  interface AppLoadContext {\n    EXCLUDE_FROM_SEARCH: boolean;\n    getDefaultActionResource?: (options: {\n      url: URL;\n      projectId: string;\n      contactEmail: string;\n      formData: FormData;\n    }) => ResourceRequest;\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/root.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n\nimport { Links, Meta, Outlet, useMatches } from \"react-router\";\n// @todo think about how to make __generated__ typeable\n// @ts-ignore\nimport { CustomCode, projectId, lastPublished } from \"./__generated__/_index\";\n\nconst Root = () => {\n  // Get language from matches\n  const matches = useMatches();\n\n  const lastMatchWithLanguage = matches.findLast((match) => {\n    // @ts-ignore\n    const language = match?.data?.pageMeta?.language;\n    return language != null;\n  });\n\n  // @ts-ignore\n  const lang = lastMatchWithLanguage?.data?.pageMeta?.language ?? \"en\";\n\n  return (\n    <html\n      lang={lang}\n      data-ws-project={projectId}\n      data-ws-last-published={lastPublished}\n    >\n      <head>\n        <meta charSet=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Meta />\n        <Links />\n        <CustomCode />\n      </head>\n      <Outlet />\n    </html>\n  );\n};\n\nexport default Root;\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/route-templates/default-sitemap.tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\nimport { sitemap } from \"__SITEMAP__\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  const urls = sitemap.map((page) => {\n    const url = new URL(`https://${host}${page.path}`);\n\n    return `\n  <url>\n    <loc>${url.href}</loc>\n    <lastmod>${page.lastModified.split(\"T\")[0]}</lastmod>\n  </url>\n    `;\n  });\n\n  return new Response(\n    `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls.join(\"\")}\n</urlset>\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"application/xml\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/route-templates/html.tsx",
    "content": "import {\n  type MetaFunction,\n  type LinksFunction,\n  type LinkDescriptor,\n  type ActionFunctionArgs,\n  type LoaderFunctionArgs,\n  type HeadersFunction,\n  data,\n  redirect,\n  useLoaderData,\n} from \"react-router\";\nimport {\n  isLocalResource,\n  loadResource,\n  loadResources,\n  formIdFieldName,\n  formBotFieldName,\n  cachedFetch,\n} from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  PageSettingsMeta,\n  PageSettingsTitle,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  projectId,\n  Page,\n  siteName,\n  favIconAsset,\n  pageFontAssets,\n  pageBackgroundImageAssets,\n  breakpoints,\n} from \"__CLIENT__\";\nimport {\n  getResources,\n  getPageMeta,\n  getRemixParams,\n  contactEmail,\n} from \"__SERVER__\";\nimport * as constants from \"__CONSTANTS__\";\nimport css from \"__CSS__?url\";\nimport { sitemap } from \"__SITEMAP__\";\nimport { assets } from \"__ASSETS__\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return cachedFetch(projectId, input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return cachedFetch(projectId, input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    throw redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  if (arg.context.EXCLUDE_FROM_SEARCH) {\n    pageMeta.excludePageFromSearch = arg.context.EXCLUDE_FROM_SEARCH;\n  }\n\n  return data(\n    {\n      host,\n      url: url.href,\n      system,\n      resources,\n      pageMeta,\n    },\n    // No way for current information to change, so add cache for 10 minutes\n    // In case of CRM Data, this should be set to 0\n    {\n      status: pageMeta.status,\n      headers: {\n        \"Cache-Control\": \"public, max-age=600\",\n      },\n    }\n  );\n};\n\nexport const headers: HeadersFunction = () => {\n  return {\n    \"Cache-Control\": \"public, max-age=0, must-revalidate\",\n  };\n};\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  const metas: ReturnType<MetaFunction> = [];\n  if (data === undefined) {\n    return metas;\n  }\n\n  const origin = `https://${data.host}`;\n\n  if (siteName) {\n    metas.push({\n      \"script:ld+json\": {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"WebSite\",\n        name: siteName,\n        url: origin,\n      },\n    });\n  }\n\n  return metas;\n};\n\nexport const links: LinksFunction = () => {\n  const result: LinkDescriptor[] = [];\n\n  result.push({\n    rel: \"stylesheet\",\n    href: css,\n  });\n\n  if (favIconAsset) {\n    result.push({\n      rel: \"icon\",\n      href: constants.imageLoader({\n        src: `${constants.assetBaseUrl}${favIconAsset}`,\n        // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n        width: 144,\n        height: 144,\n        fit: \"pad\",\n        quality: 100,\n        format: \"auto\",\n      }),\n      type: undefined,\n    });\n  }\n\n  for (const asset of pageFontAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${asset}`,\n      as: \"font\",\n      crossOrigin: \"anonymous\",\n    });\n  }\n\n  for (const backgroundImageAsset of pageBackgroundImageAssets) {\n    result.push({\n      rel: \"preload\",\n      href: `${constants.assetBaseUrl}${backgroundImageAsset}`,\n      as: \"image\",\n    });\n  }\n\n  return result;\n};\n\nconst getRequestHost = (request: Request): string =>\n  request.headers.get(\"x-forwarded-host\") || request.headers.get(\"host\") || \"\";\n\nexport const action = async ({\n  request,\n  context,\n}: ActionFunctionArgs): Promise<\n  { success: true } | { success: false; errors: string[] }\n> => {\n  try {\n    const url = new URL(request.url);\n    url.host = getRequestHost(request);\n\n    const formData = await request.formData();\n\n    const system = {\n      params: {},\n      search: {},\n      origin: url.origin,\n      pathname: url.pathname,\n    };\n\n    const resourceName = formData.get(formIdFieldName);\n    let resource =\n      typeof resourceName === \"string\"\n        ? getResources({ system }).action.get(resourceName)\n        : undefined;\n\n    const formBotValue = formData.get(formBotFieldName);\n\n    if (formBotValue == null || typeof formBotValue !== \"string\") {\n      throw new Error(\"Form bot field not found\");\n    }\n\n    // Skip timestamp validation for Brave browser\n    // Brave Shields blocks matchMedia fingerprinting detection used in bot protection\n    // See: https://github.com/brave/brave-browser/issues/46541\n    if (formBotValue !== \"brave\") {\n      const submitTime = parseInt(formBotValue, 16);\n      // Assumes that the difference between the server time and the form submission time,\n      // including any client-server time drift, is within a 5-minute range.\n      // Note: submitTime might be NaN because formBotValue can be any string used for logging purposes.\n      // Example: `formBotValue: jsdom`, or `formBotValue: headless-env`\n      if (\n        Number.isNaN(submitTime) ||\n        Math.abs(Date.now() - submitTime) > 1000 * 60 * 5\n      ) {\n        throw new Error(`Form bot value invalid ${formBotValue}`);\n      }\n    }\n\n    formData.delete(formIdFieldName);\n    formData.delete(formBotFieldName);\n\n    if (resource) {\n      resource.body = Object.fromEntries(formData);\n    } else {\n      if (contactEmail === undefined) {\n        throw new Error(\"Contact email not found\");\n      }\n\n      resource = context.getDefaultActionResource?.({\n        url,\n        projectId,\n        contactEmail,\n        formData,\n      });\n    }\n\n    if (resource === undefined) {\n      throw Error(\"Resource not found\");\n    }\n    const { ok, statusText } = await loadResource(fetch, resource);\n    if (ok) {\n      return { success: true };\n    }\n    return { success: false, errors: [statusText] };\n  } catch (error) {\n    console.error(error);\n\n    return {\n      success: false,\n      errors: [error instanceof Error ? error.message : \"Unknown error\"],\n    };\n  }\n};\n\nconst Outlet = () => {\n  const { system, resources, url, pageMeta, host } =\n    useLoaderData<typeof loader>();\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        ...constants,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        host={host}\n        siteName={siteName}\n        imageLoader={constants.imageLoader}\n        assetBaseUrl={constants.assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default Outlet;\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/route-templates/redirect.tsx",
    "content": "import { redirect } from \"react-router\";\nimport { url, status } from \"__REDIRECT__\";\n\nexport const loader = () => {\n  throw redirect(url, status);\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/route-templates/xml.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { type LoaderFunctionArgs, redirect } from \"react-router\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport {\n  ReactSdkContext,\n  xmlNodeTagSuffix,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { Page, breakpoints } from \"__CLIENT__\";\nimport { getPageMeta, getRemixParams, getResources } from \"__SERVER__\";\nimport { assetBaseUrl, imageLoader } from \"__CONSTANTS__\";\nimport { sitemap } from \"__SITEMAP__\";\nimport { assets } from \"__ASSETS__\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"sitemap.xml\")) {\n    // @todo: dynamic import sitemap ???\n    const response = new Response(JSON.stringify(sitemap));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const loader = async (arg: LoaderFunctionArgs) => {\n  const url = new URL(arg.request.url);\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = getRemixParams(arg.params);\n\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  if (pageMeta.redirect) {\n    const status =\n      pageMeta.status === 301 || pageMeta.status === 302\n        ? pageMeta.status\n        : 302;\n    return redirect(pageMeta.redirect, status);\n  }\n\n  // typecheck\n  arg.context.EXCLUDE_FROM_SEARCH satisfies boolean;\n\n  let text = renderToString(\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      <Page system={system} />\n    </ReactSdkContext.Provider>\n  );\n\n  // Xml is wrapped with <svg> to prevent React from hoisting elements like <title>, <meta>, and <link> out of their intended scope during rendering.\n  // More details: https://github.com/facebook/react/blob/7c8e5e7ab8bb63de911637892392c5efd8ce1d0f/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L3083\n  text = text.replace(/^<svg>/g, \"\").replace(/<\\/svg>$/g, \"\");\n\n  // React has issues rendering certain elements, such as errors when a <link> element has children.\n  // To render XML, we wrap it with an <svg> tag and add a suffix to avoid React's default behavior on these elements.\n  text = text.replaceAll(xmlNodeTagSuffix, \"\");\n\n  return new Response(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${text}`, {\n    headers: { \"Content-Type\": \"application/xml\" },\n  });\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/routes/[robots.txt].tsx",
    "content": "import type { LoaderFunctionArgs } from \"react-router\";\n\nexport const loader = (arg: LoaderFunctionArgs) => {\n  const host =\n    arg.request.headers.get(\"x-forwarded-host\") ||\n    arg.request.headers.get(\"host\") ||\n    \"\";\n\n  return new Response(\n    `\nUser-agent: *\nDisallow: /api/\n\nSitemap: https://${host}/sitemap.xml\n\n  `,\n    {\n      headers: {\n        \"Content-Type\": \"text/plain\",\n      },\n      status: 200,\n    }\n  );\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router/app/routes.ts",
    "content": "import { type RouteConfig } from \"@react-router/dev/routes\";\nimport { flatRoutes } from \"@react-router/fs-routes\";\n\nexport default flatRoutes() satisfies RouteConfig;\n"
  },
  {
    "path": "packages/cli/templates/react-router/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"react-router build\",\n    \"dev\": \"react-router dev\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@react-router/dev\": \"^7.5.3\",\n    \"@react-router/fs-routes\": \"^7.5.3\",\n    \"@webstudio-is/image\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/react-sdk\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-animation\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react-radix\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react-router\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react\": \"0.0.0-webstudio-version\",\n    \"isbot\": \"^5.1.25\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-router\": \"^7.5.3\",\n    \"vite\": \"^6.3.4\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"typescript\": \"5.8.2\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\", \"@webstudio-is/react-sdk/placeholder\"],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { reactRouter } from \"@react-router/dev/vite\";\n\nexport default defineConfig({\n  plugins: [reactRouter()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/.gitignore",
    "content": ".DS_Store\n/node_modules/\n\n# cloudflare\nworker-configuration.d.ts\n.wrangler\n\n# React Router\n/.react-router/\n/build/\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (import.meta.env.DEV) {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // @todo https://developers.cloudflare.com/images/transform-images/transform-via-url/\n  return props.src;\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/app/entry.server.tsx",
    "content": "import type { AppLoadContext, EntryContext } from \"react-router\";\nimport { ServerRouter } from \"react-router\";\nimport { isbot } from \"isbot\";\nimport { renderToReadableStream } from \"react-dom/server\";\n\nexport default async function handleRequest(\n  request: Request,\n  responseStatusCode: number,\n  responseHeaders: Headers,\n  routerContext: EntryContext,\n  _loadContext: AppLoadContext\n) {\n  let shellRendered = false;\n  const userAgent = request.headers.get(\"user-agent\");\n\n  const body = await renderToReadableStream(\n    <ServerRouter context={routerContext} url={request.url} />,\n    {\n      onError(error: unknown) {\n        responseStatusCode = 500;\n        // Log streaming rendering errors from inside the shell.  Don't log\n        // errors encountered during initial shell rendering since they'll\n        // reject and get logged in handleDocumentRequest.\n        if (shellRendered) {\n          console.error(error);\n        }\n      },\n    }\n  );\n  shellRendered = true;\n\n  // Ensure requests from bots and SPA Mode renders wait for all content to load before responding\n  // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation\n  if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {\n    await body.allReady;\n  }\n\n  responseHeaders.set(\"Content-Type\", \"text/html\");\n  return new Response(body, {\n    headers: responseHeaders,\n    status: responseStatusCode,\n  });\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/package.json",
    "content": "{\n  \"scripts\": {\n    \"typecheck\": \"wrangler types && tsgo\",\n    \"typegen\": \"wrangler types\",\n    \"preview\": \"react-router build && vite preview\",\n    \"deploy\": \"react-router build && wrangler deploy\"\n  },\n  \"dependencies\": {\n    \"@cloudflare/vite-plugin\": \"^1.1.0\",\n    \"wrangler\": \"^4.14.1\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\n\nexport default {\n  future: {\n    unstable_viteEnvironmentApi: true,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"./worker-configuration.d.ts\",\n      \"vite/client\",\n      \"@webstudio-is/react-sdk/placeholder\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"customConditions\": [\"webstudio\"]\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { cloudflare } from \"@cloudflare/vite-plugin\";\nimport { reactRouter } from \"@react-router/dev/vite\";\n\nexport default defineConfig({\n  plugins: [cloudflare({ viteEnvironment: { name: \"ssr\" } }), reactRouter()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/workers/app.ts",
    "content": "import { createRequestHandler } from \"react-router\";\n\ndeclare module \"react-router\" {\n  export interface AppLoadContext {\n    cloudflare: {\n      env: Env;\n      ctx: ExecutionContext;\n    };\n  }\n}\n\nconst requestHandler = createRequestHandler(\n  // @ts-ignore\n  () => import(\"virtual:react-router/server-build\"),\n  import.meta.env.MODE\n);\n\nexport default {\n  async fetch(request, env, ctx) {\n    return requestHandler(request, {\n      EXCLUDE_FROM_SEARCH: false,\n      getDefaultActionResource: undefined,\n      cloudflare: { env, ctx },\n    });\n  },\n} satisfies ExportedHandler<Env>;\n"
  },
  {
    "path": "packages/cli/templates/react-router-cloudflare/wrangler.jsonc",
    "content": "{\n  \"$schema\": \"node_modules/wrangler/config-schema.json\",\n  \"name\": \"webstudio-app\",\n  \"compatibility_date\": \"2025-04-28\",\n  \"main\": \"./workers/app.ts\"\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-docker/.dockerignore",
    "content": ".react-router\nbuild\nnode_modules\nREADME.md\n"
  },
  {
    "path": "packages/cli/templates/react-router-docker/Dockerfile",
    "content": "FROM node:22-alpine AS dependencies-env\nCOPY .npmrc package.json /app/\nWORKDIR /app\nRUN npm install --omit=dev\n\nFROM dependencies-env AS build-env\nCOPY . /app/\nWORKDIR /app\nRUN npm install\nRUN npm run build\n\nFROM node:22-alpine\nCOPY .npmrc package.json /app/\nCOPY --from=dependencies-env /app/node_modules /app/node_modules\nCOPY --from=build-env /app/build /app/build\nCOPY --from=build-env /app/public /app/public\nWORKDIR /app\n# there is a DOMAINS env with comma separated allowed domains for image processing\nCMD [\"npm\", \"run\", \"start\"]\n"
  },
  {
    "path": "packages/cli/templates/react-router-docker/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * URL.canParse(props.src)\n * @type {(url: string) => boolean}\n */\nconst UrlCanParse = (url) => {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n  // IPX (sharp) does not support ico\n  if (props.src.endsWith('.ico')) {\n    return props.src;\n  }\n  // handle absolute urls\n  const path = UrlCanParse(props.src) ? `/${props.src}` : props.src;\n  // https://github.com/unjs/ipx?tab=readme-ov-file#modifiers\n  return `/_image/w_${props.width},q_${props.quality}${path}`;\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router-docker/app/routes/[_image].$.ts",
    "content": "import { env } from \"node:process\";\nimport type { LoaderFunctionArgs } from \"react-router\";\nimport {\n  createIPX,\n  createIPXH3Handler,\n  ipxFSStorage,\n  ipxHttpStorage,\n} from \"ipx\";\nimport { createApp, toWebHandler } from \"h3\";\n\nconst domains = env.DOMAINS?.split(/\\s*,\\s*/) ?? [];\n\nconst ipx = createIPX({\n  storage: ipxFSStorage({ dir: \"./public\" }),\n  httpStorage: ipxHttpStorage({ domains }),\n});\n\nconst handleRequest = toWebHandler(\n  createApp().use(\"/_image\", createIPXH3Handler(ipx))\n);\n\nexport const loader = async (args: LoaderFunctionArgs) => {\n  return handleRequest(args.request);\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router-docker/package.json",
    "content": "{\n  \"scripts\": {\n    \"start\": \"react-router-serve ./build/server/index.js\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"^7.5.3\",\n    \"@react-router/serve\": \"^7.5.3\",\n    \"h3\": \"^1.15.1\",\n    \"ipx\": \"^3.0.3\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-netlify/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (import.meta.env.DEV) {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://docs.netlify.com/image-cdn/overview/\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  if (props.height) {\n    searchParams.set(\"h\", props.height.toString());\n  }\n  searchParams.set(\"q\", props.quality.toString());\n  // fit=contain by default\n  return `/.netlify/images?${searchParams}`;\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router-netlify/netlify.toml",
    "content": "[build]\ncommand = \"react-router build\"\npublish = \"build/client\"\n\n[dev]\ncommand = \"react-router dev\"\n"
  },
  {
    "path": "packages/cli/templates/react-router-netlify/package.json",
    "content": "{\n  \"scripts\": {\n    \"start\": \"npx netlify-cli serve\",\n    \"deploy\": \"npx netlify-cli deploy --build --prod\"\n  },\n  \"dependencies\": {\n    \"@netlify/vite-plugin-react-router\": \"^1.0.1\",\n    \"@react-router/node\": \"^7.5.3\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-netlify/vite.config.ts",
    "content": "import { reactRouter } from \"@react-router/dev/vite\";\nimport { defineConfig } from \"vite\";\nimport netlifyPlugin from \"@netlify/vite-plugin-react-router\";\n\nexport default defineConfig({\n  plugins: [reactRouter(), netlifyPlugin()],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/templates/react-router-vercel/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (import.meta.env.DEV) {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://vercel.com/blog/build-your-own-web-framework#automatic-image-optimization\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  searchParams.set(\"q\", props.quality.toString());\n  return `/_vercel/image?${searchParams}`;\n};\n"
  },
  {
    "path": "packages/cli/templates/react-router-vercel/package.json",
    "content": "{\n  \"scripts\": {\n    \"deploy\": \"npx vercel deploy\"\n  },\n  \"dependencies\": {\n    \"@react-router/node\": \"^7.5.3\",\n    \"@vercel/react-router\": \"^1.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/react-router-vercel/react-router.config.ts",
    "content": "import type { Config } from \"@react-router/dev/config\";\nimport { vercelPreset } from \"@vercel/react-router/vite\";\n\nexport default {\n  presets: [vercelPreset()],\n} satisfies Config;\n"
  },
  {
    "path": "packages/cli/templates/react-router-vercel/vercel.json",
    "content": "{\n  \"framework\": \"react-router\",\n  \"images\": {\n    \"domains\": [],\n    \"sizes\": [\n      16, 32, 48, 64, 96, 128, 144, 256, 384, 640, 750, 828, 1080, 1200, 1920,\n      2048, 3840\n    ],\n    \"minimumCacheTTL\": 60,\n    \"formats\": [\"image/webp\", \"image/avif\"]\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/saas-helpers/package.json",
    "content": "{\n  \"dependencies\": {\n    \"worktop\": \"0.8.0-next.18\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"fast-glob\": \"^3.3.2\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/saas-helpers/tsconfig.json",
    "content": "{\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"**/.server/**/*.ts\",\n    \"**/.server/**/*.tsx\",\n    \"**/.client/**/*.ts\",\n    \"**/.client/**/*.tsx\"\n  ],\n  \"compilerOptions\": {\n    \"types\": [\n      \"@remix-run/cloudflare\",\n      \"vite/client\",\n      \"@cloudflare/workers-types/2023-07-01\",\n      \"@webstudio-is/react-sdk/placeholder\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/saas-helpers/vite.config.ts",
    "content": "import {\n  vitePlugin as remix,\n  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,\n} from \"@remix-run/dev\";\nimport { defineConfig } from \"vite\";\n\nimport { existsSync } from \"node:fs\";\n// @ts-ignore\nimport path from \"node:path\";\n// @ts-ignore\nimport fg from \"fast-glob\";\n\nconst rootDir = [\"..\", \"../..\", \"../../..\"]\n  .map((dir) => path.join(__dirname, dir))\n  .find((dir) => existsSync(path.join(dir, \".git\")));\n\nconst hasPrivateFolders =\n  fg.sync([path.join(rootDir ?? \"\", \"packages/*/private-src/*\")], {\n    ignore: [\"**/node_modules/**\"],\n  }).length > 0;\n\nconst conditions = hasPrivateFolders\n  ? [\"webstudio-private\", \"webstudio\"]\n  : [\"webstudio\"];\n\nexport default defineConfig(({ mode }) => ({\n  resolve: {\n    conditions: [...conditions, \"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [...conditions, \"node\", \"development|production\"],\n    },\n  },\n  plugins: [\n    // without this, remixCloudflareDevProxy trying to load workerd even for production (it's not needed for production)\n    mode === \"production\" ? undefined : remixCloudflareDevProxy(),\n    remix({\n      future: {\n        v3_lazyRouteDiscovery: false,\n        v3_relativeSplatPath: false,\n        v3_singleFetch: false,\n        v3_fetcherPersist: false,\n        v3_throwAbortReason: false,\n      },\n    }),\n  ].filter(Boolean),\n}));\n"
  },
  {
    "path": "packages/cli/templates/ssg/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\n\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = ({ src }) => {\n  return src;\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg/app/route-templates/html/+Head.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport { assetBaseUrl, imageLoader } from \"__CONSTANTS__\";\nimport {\n  favIconAsset,\n  pageBackgroundImageAssets,\n  pageFontAssets,\n  siteName,\n} from \"__CLIENT__\";\nimport \"__CSS__\";\n\nexport const Head = ({}: { data: PageContext[\"data\"] }) => {\n  const ldJson = {\n    \"@context\": \"https://schema.org\",\n    \"@type\": \"WebSite\",\n    name: siteName,\n  };\n  return (\n    <>\n      {siteName && (\n        <script\n          type=\"application/ld+json\"\n          dangerouslySetInnerHTML={{\n            __html: JSON.stringify(ldJson, null, 2),\n          }}\n        ></script>\n      )}\n      {favIconAsset && (\n        <link\n          rel=\"icon\"\n          href={imageLoader({\n            src: `${assetBaseUrl}${favIconAsset}`,\n            // width,height must be multiple of 48 https://developers.google.com/search/docs/appearance/favicon-in-search\n            width: 144,\n            height: 144,\n            fit: \"pad\",\n            quality: 100,\n            format: \"auto\",\n          })}\n        />\n      )}\n      {pageFontAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"font\"\n          crossOrigin=\"anonymous\"\n        />\n      ))}\n      {pageBackgroundImageAssets.map((asset) => (\n        <link\n          key={asset}\n          rel=\"preload\"\n          href={`${assetBaseUrl}${asset}`}\n          as=\"image\"\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg/app/route-templates/html/+Page.tsx",
    "content": "import type { PageContext } from \"vike/types\";\nimport {\n  PageSettingsMeta,\n  PageSettingsTitle,\n  ReactSdkContext,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { assetBaseUrl, imageLoader } from \"__CONSTANTS__\";\nimport { Page, breakpoints, siteName } from \"__CLIENT__\";\n\nconst PageComponent = ({ data }: { data: PageContext[\"data\"] }) => {\n  const { system, resources, url, pageMeta } = data;\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        imageLoader,\n        assetBaseUrl,\n        resources,\n        breakpoints,\n        onError: console.error,\n      }}\n    >\n      {/* Use the URL as the key to force scripts in HTML Embed to reload on dynamic pages */}\n      <Page key={url} system={system} />\n      <PageSettingsMeta\n        url={url}\n        pageMeta={pageMeta}\n        siteName={siteName}\n        imageLoader={imageLoader}\n        assetBaseUrl={assetBaseUrl}\n      />\n      <PageSettingsTitle>{pageMeta.title}</PageSettingsTitle>\n    </ReactSdkContext.Provider>\n  );\n};\nexport default PageComponent;\n"
  },
  {
    "path": "packages/cli/templates/ssg/app/route-templates/html/+data.ts",
    "content": "import type { PageContextServer } from \"vike/types\";\nimport { isLocalResource, loadResources } from \"@webstudio-is/sdk/runtime\";\nimport { getPageMeta, getResources } from \"__SERVER__\";\nimport { assets } from \"__ASSETS__\";\n\nconst customFetch: typeof fetch = (input, init) => {\n  if (typeof input !== \"string\") {\n    return fetch(input, init);\n  }\n\n  if (isLocalResource(input, \"current-date\")) {\n    const now = new Date();\n    // Normalize to midnight UTC to prevent hydration mismatches\n    const startOfDay = new Date(\n      Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())\n    );\n    const data = {\n      iso: startOfDay.toISOString(),\n      year: startOfDay.getUTCFullYear(),\n      month: startOfDay.getUTCMonth() + 1, // 1-12 instead of 0-11\n      day: startOfDay.getUTCDate(),\n      timestamp: startOfDay.getTime(),\n    };\n    const response = new Response(JSON.stringify(data));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  if (isLocalResource(input, \"assets\")) {\n    const response = new Response(JSON.stringify(assets));\n    response.headers.set(\"content-type\", \"application/json; charset=utf-8\");\n    return Promise.resolve(response);\n  }\n\n  return fetch(input, init);\n};\n\nexport const data = async (pageContext: PageContextServer) => {\n  const url = new URL(pageContext.urlOriginal, \"http://url\");\n  const headers = new Headers(pageContext.headers ?? {});\n  const host = headers.get(\"x-forwarded-host\") || headers.get(\"host\") || \"\";\n  url.host = host;\n  url.protocol = \"https\";\n\n  const params = pageContext.routeParams;\n  const system = {\n    params,\n    search: Object.fromEntries(url.searchParams),\n    origin: url.origin,\n    pathname: url.pathname,\n  };\n\n  const resources = await loadResources(\n    customFetch,\n    getResources({ system }).data\n  );\n  const pageMeta = getPageMeta({ system, resources });\n\n  return {\n    url: url.href,\n    system,\n    resources,\n    pageMeta,\n  } satisfies PageContextServer[\"data\"];\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"dev\": \"vite dev\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/image\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/react-sdk\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-animation\": \"0.0.0-webstudio-version\",\n    \"@webstudio-is/sdk-components-react-radix\": \"0.0.0-webstudio-version\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vike\": \"^0.4.229\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@vitejs/plugin-react\": \"^4.4.1\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"^6.3.4\"\n  },\n  \"engines\": {\n    \"node\": \">=22\"\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/ssg/pages/+config.ts",
    "content": "import type { Config } from \"vike/types\";\n\nexport default {\n  meta: {\n    Head: {\n      env: { server: true, client: true },\n    },\n    lang: {\n      env: { server: true, client: true },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "packages/cli/templates/ssg/renderer/+onRenderClient.tsx",
    "content": "import { type Root, createRoot } from \"react-dom/client\";\nimport type { OnRenderClientSync } from \"vike/types\";\n\nlet root: Root;\n\nexport const onRenderClient: OnRenderClientSync = (pageContext) => {\n  const lang = pageContext.data.pageMeta.language || \"en\";\n  const Head = pageContext.config.Head ?? (() => <></>);\n  const Page = pageContext.Page ?? (() => <></>);\n  const htmlContent = (\n    <>\n      <head>\n        <meta charSet=\"UTF-8\" />\n        <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n        <Head data={pageContext.data} />\n        {/* avoid hydrating custom code on client, it will duplicate all scripts */}\n      </head>\n      <Page data={pageContext.data} />\n    </>\n  );\n  if (root === undefined) {\n    root = createRoot(document.documentElement);\n  }\n  document.documentElement.lang = lang;\n  root.render(htmlContent);\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg/renderer/+onRenderHtml.tsx",
    "content": "import { renderToString } from \"react-dom/server\";\nimport { dangerouslySkipEscape, escapeInject } from \"vike/server\";\nimport type { OnRenderHtmlSync } from \"vike/types\";\nimport {\n  CustomCode,\n  projectId,\n  lastPublished,\n  // @todo think about how to make __generated__ typeable\n  /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */\n  // @ts-ignore\n} from \"../app/__generated__/_index\";\n\nexport const onRenderHtml: OnRenderHtmlSync = (pageContext) => {\n  const lang = pageContext.data.pageMeta.language || \"en\";\n  const Head = pageContext.config.Head ?? (() => <></>);\n  const Page = pageContext.Page ?? (() => <></>);\n  const html = dangerouslySkipEscape(\n    renderToString(\n      <html\n        lang={lang}\n        data-ws-project={projectId}\n        data-ws-last-published={lastPublished}\n      >\n        <head>\n          <meta charSet=\"UTF-8\" />\n          <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n          <Head data={pageContext.data} />\n          <CustomCode />\n        </head>\n        <Page data={pageContext.data} />\n      </html>\n    )\n  );\n  return escapeInject`<!DOCTYPE html>\n${html}\n`;\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg/tsconfig.json",
    "content": "{\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\n      \"vite/client\",\n      \"@webstudio-is/react-sdk/placeholder\",\n      \"./vike.d.ts\"\n    ],\n    \"isolatedModules\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"target\": \"ES2022\",\n    \"strict\": true,\n    \"allowJs\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"skipLibCheck\": true\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/ssg/vike.d.ts",
    "content": "import type { ImageLoader } from \"@webstudio-is/image\";\nimport type { PageMeta, System } from \"@webstudio-is/sdk\";\n\ndeclare global {\n  namespace Vike {\n    interface Config {\n      lang?: (props: { data: PageData }) => string;\n      Head?: (props: { data: PageData }) => React.ReactNode;\n    }\n\n    interface PageContext {\n      constants: {\n        assetBaseUrl: string;\n        imageLoader: ImageLoader;\n      };\n      data: {\n        url: string;\n        system: System;\n        resources: Record<string, unknown>;\n        pageMeta: PageMeta;\n      };\n      Page?: (props: { data: PageData }) => React.ReactNode;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cli/templates/ssg/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport vike from \"vike/plugin\";\n\nexport default defineConfig({\n  plugins: [react(), vike({ prerender: true })],\n  resolve: {\n    conditions: [\"browser\", \"development|production\"],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"node\", \"development|production\"],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/cli/templates/ssg-netlify/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\n\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (process.env.NODE_ENV !== \"production\") {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://docs.netlify.com/image-cdn/overview/\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  if (props.height) {\n    searchParams.set(\"h\", props.height.toString());\n  }\n  searchParams.set(\"q\", props.quality.toString());\n  // fit=contain by default\n  return `/.netlify/images?${searchParams}`;\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg-vercel/app/constants.mjs",
    "content": "/**\n * We use mjs extension as constants in this file is shared with the build script\n * and we use `node --eval` to extract the constants.\n */\n\nexport const assetBaseUrl = \"/assets/\";\n\n/**\n * @type {import(\"@webstudio-is/image\").ImageLoader}\n */\nexport const imageLoader = (props) => {\n  if (process.env.NODE_ENV !== \"production\") {\n    return props.src;\n  }\n\n  if (props.format === \"raw\") {\n    return props.src;\n  }\n\n  // https://vercel.com/blog/build-your-own-web-framework#automatic-image-optimization\n  const searchParams = new URLSearchParams();\n  searchParams.set(\"url\", props.src);\n  searchParams.set(\"w\", props.width.toString());\n  searchParams.set(\"q\", props.quality.toString());\n  return `/_vercel/image?${searchParams}`;\n};\n"
  },
  {
    "path": "packages/cli/templates/ssg-vercel/public/vercel.json",
    "content": "{\n  \"images\": {\n    \"domains\": [],\n    \"sizes\": [\n      16, 32, 48, 64, 96, 128, 144, 256, 384, 640, 750, 828, 1080, 1200, 1920,\n      2048, 3840\n    ],\n    \"minimumCacheTTL\": 60,\n    \"formats\": [\"image/webp\", \"image/avif\"]\n  }\n}\n"
  },
  {
    "path": "packages/cli/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"**/*.mjs\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n    \"types\": [\"vite/client\", \"@webstudio-is/react-sdk/placeholder\"],\n    \"module\": \"esnext\",\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"noUncheckedSideEffectImports\": false\n  }\n}\n"
  },
  {
    "path": "packages/cli/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport pkg from \"./package.json\";\n\nconst isExternal = (id: string, importer: string | undefined) => {\n  if (id.startsWith(\"@webstudio-is/\")) {\n    return false;\n  }\n  if (id.startsWith(\"node:\")) {\n    return true;\n  }\n  if (id.startsWith(\"@\")) {\n    const packageName = id.split(\"/\").slice(0, 2).join(\"/\");\n    if (packageName in pkg.dependencies === false) {\n      throw Error(\n        `${packageName} imported from ${importer} is not found in direct dependencies`\n      );\n    }\n    return true;\n  }\n  if (id.includes(\".\") === false) {\n    const [packageName] = id.split(\"/\");\n    if (packageName in pkg.dependencies === false) {\n      throw Error(\n        `${packageName} imported from ${importer} is not found in direct dependencies`\n      );\n    }\n    return true;\n  }\n  return false;\n};\n\nexport default defineConfig({\n  // resolve only webstudio condition in tests\n  build: {\n    minify: false,\n    lib: {\n      entry: [\"src/cli.ts\"],\n      formats: [\"es\"],\n    },\n    rollupOptions: {\n      external: isExternal,\n      output: {\n        dir: \"lib\",\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/css-data/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/css-data/LICENSE-3RD-PARTY",
    "content": "MIT License\n\nCopyright (c) 2021 Dany Castillo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "packages/css-data/README.md",
    "content": "# CSS Data\n\nGenerated configs and collections from mdn-data into consumable form.\n"
  },
  {
    "path": "packages/css-data/bin/css-to-ws.ts",
    "content": "#!/usr/bin/env tsx\nimport { parseArgs, type ParseArgsConfig } from \"node:util\";\nimport * as path from \"node:path\";\nimport { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { camelCaseProperty, parseCss } from \"../src/parse-css\";\n\nconst cliOptions = {\n  allowPositionals: true,\n  strict: true,\n} as const satisfies ParseArgsConfig;\n\nconst cliArgs = parseArgs({ args: process.argv.slice(2), ...cliOptions });\n\nif (cliArgs.positionals.length < 2) {\n  console.error(\"Please provide the source and destination paths\");\n  process.exit(1);\n}\n\nconst sourcePath = path.resolve(process.cwd(), cliArgs.positionals[0]);\nconst destinationPath = path.resolve(process.cwd(), cliArgs.positionals[1]);\n\nconst objectGroupBy = <Item>(list: Item[], by: (item: Item) => string) => {\n  const records: Record<string, Item[]> = {};\n  for (const item of list) {\n    const key = by(item);\n    let list = records[key];\n    if (list === undefined) {\n      list = [];\n      records[key] = list;\n    }\n    list.push(item);\n  }\n  return records;\n};\n\nconst css = readFileSync(sourcePath, \"utf8\");\nconst parsed = parseCss(css).map(({ property, ...styleDecl }) => ({\n  selector: styleDecl.selector,\n  breakpoint: styleDecl.breakpoint,\n  state: styleDecl.state,\n  property: camelCaseProperty(property),\n  value: styleDecl.value,\n}));\nconst records = objectGroupBy(parsed, (item) => item.selector);\nmkdirSync(path.dirname(destinationPath), { recursive: true });\nconst code = `/* eslint-disable */\n/* This file was generated by css-to-ws.ts */\n\nexport const styles = ${JSON.stringify(records, null, 2)};\n`;\n\nwriteFileSync(destinationPath, code, \"utf8\");\nconsole.info(\"✅ CSS parsed and Webstudio Data generated\");\n"
  },
  {
    "path": "packages/css-data/bin/css-tree-dist-data.d.ts",
    "content": "declare module \"css-tree/dist/data\" {\n  export default {\n    units: Record<string, string[]>,\n  };\n}\n"
  },
  {
    "path": "packages/css-data/bin/html.css.ts",
    "content": "import { mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport type { StyleValue } from \"@webstudio-is/css-engine\";\nimport { parseCss } from \"../src/parse-css\";\n\nconst css = readFileSync(\"./src/html.css\", \"utf8\");\nconst parsed = parseCss(css);\nconst result: [string, StyleValue][] = [];\nfor (const styleDecl of parsed) {\n  result.push([`${styleDecl.selector}:${styleDecl.property}`, styleDecl.value]);\n}\nlet code = \"\";\ncode += `import type { StyleValue } from \"@webstudio-is/css-engine\";\\n\\n`;\nconst type = \"Map<string, StyleValue>\";\ncode += `export const html: ${type} = new Map(${JSON.stringify(result)})`;\n\nmkdirSync(\"./src/__generated__\", { recursive: true });\nwriteFileSync(\"./src/__generated__/html.ts\", code);\n"
  },
  {
    "path": "packages/css-data/bin/mdn-data.ts",
    "content": "import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore @todo add missing type defitions for definitionSyntax, type DSNode, type CssNode\nimport { parse, definitionSyntax, type DSNode, type CssNode } from \"css-tree\";\nimport properties from \"mdn-data/css/properties.json\";\nimport syntaxes from \"mdn-data/css/syntaxes.json\";\nimport selectors from \"mdn-data/css/selectors.json\";\nimport data from \"css-tree/dist/data\";\nimport type {\n  KeywordValue,\n  StyleValue,\n  Unit,\n  UnitValue,\n  UnparsedValue,\n  FontFamilyValue,\n  CssProperty,\n} from \"@webstudio-is/css-engine\";\nimport * as customData from \"../src/custom-data\";\nimport { camelCaseProperty } from \"../src/parse-css\";\n\nconst units: Record<string, Array<string>> = {\n  number: [],\n  // consider % as unit\n  percentage: [\"%\"],\n  ...data.units,\n};\n\ntype Property = keyof typeof properties;\ntype Value = (typeof properties)[Property];\n\nconst autoValue = {\n  type: \"keyword\",\n  value: \"auto\",\n} as const;\n\n// Normalize browser dependant properties.\nconst normalizedValues = {\n  // dependsOnUserAgent\n  \"font-family\": {\n    type: \"fontFamily\",\n    value: [\"serif\"],\n  } satisfies FontFamilyValue,\n  // startOrNamelessValueIfLTRRightIfRTL\n  \"text-align\": { type: \"keyword\", value: \"start\" },\n  // canvastext\n  color: { type: \"keyword\", value: \"black\" },\n  \"column-gap\": {\n    type: \"unit\",\n    value: 0,\n    unit: \"px\",\n  },\n  \"row-gap\": {\n    type: \"unit\",\n    value: 0,\n    unit: \"px\",\n  },\n  \"background-size\": autoValue,\n  \"text-size-adjust\": autoValue,\n} as const;\n\nconst beautifyKeyword = (_property: string, keyword: string) => {\n  /*\n   * The default value is `invert` or `currentcolor` for some css properties.\n   * But that isn't supported in all browsers, example outline-color.\n   * So, going with currentColor for consistency.\n   * https://developer.mozilla.org/en-US/docs/Web/CSS/outline-color#formal_definition\n   */\n  if (keyword === \"currentcolor\" || keyword === \"invertOrCurrentColor\") {\n    return \"currentColor\";\n  }\n  return keyword;\n};\n\nconst convertToStyleValue = (\n  node: CssNode,\n  property: string,\n  value: string,\n  unitGroups: Set<string>\n): undefined | UnitValue | KeywordValue | UnparsedValue => {\n  if (node?.type === \"Identifier\") {\n    return {\n      type: \"keyword\",\n      value: beautifyKeyword(property, node.name),\n    };\n  }\n  if (node?.type === \"Number\") {\n    let unit: Unit = \"number\";\n    // set explicit unit when 0 initial is specified without unit\n    if (unitGroups.has(\"number\") === false) {\n      if (unitGroups.has(\"length\")) {\n        unit = \"px\";\n      } else {\n        throw Error(\n          `Cannot infer unit for \"${value}\" initial value of ${property} property`\n        );\n      }\n    }\n    return {\n      type: \"unit\",\n      unit,\n      value: Number(node.value),\n    };\n  }\n  if (node?.type === \"Percentage\") {\n    return {\n      type: \"unit\",\n      unit: \"%\",\n      value: Number(node.value),\n    };\n  }\n  if (node?.type === \"Dimension\") {\n    return {\n      type: \"unit\",\n      unit: node.unit as Unit,\n      value: Number(node.value),\n    };\n  }\n};\n\nconst parseInitialValue = (\n  property: string,\n  value: string,\n  unitGroups: Set<string>\n): StyleValue => {\n  // Our default values hardcoded because no single standard\n  if (property in normalizedValues) {\n    return normalizedValues[property as keyof typeof normalizedValues];\n  }\n  const ast = parse(value, { context: \"value\" });\n  if (ast.type !== \"Value\") {\n    throw Error(`Unknown parsed type ${ast.type}`);\n  }\n\n  // more than 2 values consider as keyword\n  if (ast.children.first !== ast.children.last) {\n    return {\n      type: \"tuple\",\n      value: ast.children.toArray().map((node) => {\n        const styleValue = convertToStyleValue(\n          node,\n          property,\n          value,\n          unitGroups\n        );\n        if (styleValue !== undefined) {\n          return styleValue;\n        }\n        throw Error(`Cannot find initial for ${property}`);\n      }),\n    };\n  }\n\n  const node = ast.children.first;\n  let styleValue: undefined | StyleValue;\n  if (node) {\n    styleValue = convertToStyleValue(node, property, value, unitGroups);\n  }\n  if (styleValue !== undefined) {\n    return styleValue;\n  }\n\n  throw Error(`Cannot find initial for ${property}`);\n};\n\nconst walkSyntax = (\n  syntax: string,\n  enter: (node: DSNode) => void,\n  parsedSyntaxes = new Set<string>()\n) => {\n  // fix cyclic syntaxes\n  if (parsedSyntaxes.has(syntax)) {\n    return;\n  }\n  parsedSyntaxes.add(syntax);\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore @todo add missing type defitions for definitionSyntax\n  const parsed = definitionSyntax.parse(syntax);\n\n  const walk = (node: DSNode) => {\n    if (node.type === \"Group\") {\n      for (const term of node.terms) {\n        // skip functions and their content as complex values\n        if (term.type === \"Function\") {\n          break;\n        }\n        walk(term);\n      }\n      return;\n    }\n    if (node.type === \"Multiplier\") {\n      walk(node.term);\n      return;\n    }\n    if (node.type === \"Type\") {\n      if (node.name === \"deprecated-system-color\") {\n        return;\n      }\n      const nestedSyntax = syntaxes[node.name as keyof typeof syntaxes]?.syntax;\n      if (nestedSyntax === undefined) {\n        enter(node);\n      } else {\n        // resolve nested syntaxes\n        walkSyntax(nestedSyntax, enter, parsedSyntaxes);\n      }\n      return;\n    }\n    if (node.type === \"Property\") {\n      // resolve other properties references\n      if (node.name in properties) {\n        walkSyntax(\n          properties[node.name as Property].syntax,\n          enter,\n          parsedSyntaxes\n        );\n      }\n      return;\n    }\n    enter(node);\n  };\n\n  walk(parsed);\n};\n\nconst autogeneratedHint = \"// This file was generated by pnpm mdn-data\\n\";\n\nconst writeToFile = (fileName: string, constant: string, data: unknown) => {\n  const content =\n    autogeneratedHint +\n    `export const ${constant} = ` +\n    JSON.stringify(data, null, 2) +\n    \" as const;\";\n\n  writeFileSync(join(targetDir, fileName), content, \"utf8\");\n};\n\nconst supportedExperimentalProperties = [\n  \"field-sizing\",\n  \"text-size-adjust\",\n  \"-webkit-tap-highlight-color\",\n  \"-webkit-overflow-scrolling\",\n];\n\n// Properties we don't support in this form.\nconst unsupportedProperties = [\n  \"--*\",\n  \"-webkit-text-fill-color\",\n  \"-webkit-text-stroke-color\",\n  \"-webkit-text-stroke-width\",\n  // shorthand properties\n  \"all\",\n  \"font-synthesis\",\n  \"font-variant\",\n  \"overflow\",\n  \"white-space\",\n  \"text-wrap\",\n  \"background-position\",\n  \"border-block-style\",\n  \"border-block-width\",\n  \"border-block-color\",\n  \"border-inline-style\",\n  \"border-inline-width\",\n  \"border-inline-color\",\n];\n\ntype FilteredProperties = { [property: string]: Value };\n\nconst filterData = () => {\n  let property: Property;\n  const allLonghands = {} as FilteredProperties;\n  const allShorthands = {} as FilteredProperties;\n  const animatableLonghands = {} as FilteredProperties;\n  const animatableShorthands = {} as FilteredProperties;\n\n  for (property in properties) {\n    if (unsupportedProperties.includes(property)) {\n      continue;\n    }\n\n    const config = properties[property];\n\n    const isStandardProperty =\n      config.status === \"standard\" && \"mdn_url\" in config;\n\n    const isSupportedExperimentalProperty =\n      supportedExperimentalProperties.includes(property);\n\n    if (\n      isStandardProperty === false &&\n      isSupportedExperimentalProperty === false\n    ) {\n      continue;\n    }\n\n    const isAnimatableProperty =\n      property.startsWith(\"-\") === false &&\n      config.animationType !== \"discrete\" &&\n      config.animationType !== \"notAnimatable\";\n\n    const isShorthandProperty = Array.isArray(config.initial);\n\n    if (isShorthandProperty) {\n      allShorthands[property] = config;\n      if (isAnimatableProperty) {\n        animatableShorthands[property] = config;\n      }\n      continue;\n    }\n\n    allLonghands[property] = config;\n    if (isAnimatableProperty) {\n      animatableLonghands[property] = config;\n    }\n  }\n\n  return {\n    allLonghands,\n    allShorthands,\n    animatableLonghands,\n    animatableShorthands,\n  };\n};\n\nconst getPropertiesData = (\n  customPropertiesData: typeof customData.propertiesData,\n  filteredProperties: FilteredProperties\n) => {\n  const propertiesData = { ...customPropertiesData };\n\n  let property: string;\n  for (property in filteredProperties) {\n    const config = filteredProperties[property];\n    const unitGroups = new Set<string>();\n    walkSyntax(config.syntax, (node) => {\n      if (node.type === \"Type\") {\n        if (node.name === \"number-token\" || node.name === \"number\") {\n          unitGroups.add(\"number\");\n          return;\n        }\n\n        // type names match unit groups\n        if (node.name in units) {\n          unitGroups.add(node.name);\n          return;\n        }\n      }\n    });\n\n    if (Array.isArray(config.initial)) {\n      throw new Error(\n        `Property ${property} contains non string initial value ${config.initial.join(\n          \", \"\n        )}`\n      );\n    }\n\n    propertiesData[property] = {\n      unitGroups: Array.from(unitGroups),\n      inherited: config.inherited,\n      initial: parseInitialValue(property, config.initial, unitGroups),\n      ...(\"mdn_url\" in config && { mdnUrl: config.mdn_url }),\n    };\n  }\n\n  return propertiesData;\n};\n\nconst pseudoElements = Object.keys(selectors)\n  .filter((selector) => {\n    return selector.startsWith(\"::\");\n  })\n  .map((selector) => selector.slice(2));\n\nconst pseudoClasses = Object.keys(selectors)\n  .filter((selector) => {\n    return selector.startsWith(\":\") && !selector.startsWith(\"::\");\n  })\n  .map((selector) => selector.slice(1));\n\nconst getKeywordValues = (filteredProperties: FilteredProperties) => {\n  const result = { ...customData.keywordValues };\n  // https://www.w3.org/TR/css-values/#common-keywords\n  const commonKeywords = [\"initial\", \"inherit\", \"unset\"];\n\n  for (const property in filteredProperties) {\n    // prevent merging with custom keywords\n    if (result[property]) {\n      continue;\n    }\n    const keywords = new Set<string>();\n    walkSyntax(filteredProperties[property].syntax, (node) => {\n      if (node.type === \"Keyword\") {\n        keywords.add(beautifyKeyword(property, node.name));\n      }\n    });\n\n    for (const commonKeyword of commonKeywords) {\n      // Delete to add commonKeyword at the end of the set\n      keywords.delete(commonKeyword);\n      keywords.add(commonKeyword);\n    }\n\n    result[property] = [...(result[property] ?? []), ...keywords];\n  }\n\n  return result;\n};\n\nconst getTypes = (propertiesData: typeof customData.propertiesData) => {\n  let types = \"\";\n\n  const camelCasedProperties = Object.keys(propertiesData).map((property) =>\n    JSON.stringify(camelCaseProperty(property as CssProperty))\n  );\n  types += `export type CamelCasedProperty = ${camelCasedProperties.join(\" | \")};\\n\\n`;\n  const hyphenatedProperties = Object.keys(propertiesData).map((property) =>\n    JSON.stringify(property)\n  );\n  types += `export type HyphenatedProperty = ${hyphenatedProperties.join(\" | \")};\\n\\n`;\n\n  const unitLiterals = Object.values(units)\n    .flat()\n    .map((unit) => JSON.stringify(unit));\n  types += `export type Unit = ${unitLiterals.join(\" | \")};\\n`;\n\n  return types;\n};\n\nconst filteredData = filterData();\n\nconst longhandPropertiesData = getPropertiesData(\n  customData.propertiesData,\n  filteredData.allLonghands\n);\n\nconst targetDir = join(process.cwd(), process.argv.slice(2).pop() as string);\nmkdirSync(targetDir, { recursive: true });\nwriteToFile(\"units.ts\", \"units\", units);\n\nconst propertiesData = Object.fromEntries(\n  Object.entries(longhandPropertiesData).map(([property, data]) => [\n    property,\n    data,\n  ])\n);\nconst propertiesContent = `${autogeneratedHint}\nimport type { __HyphenatedProperty, StyleValue } from \"@webstudio-is/css-engine\"\ntype UnitGroup = ${Object.keys(units)\n  .map((item) => JSON.stringify(item))\n  .join(\" | \")}\ntype PropertyData = {\n  unitGroups: UnitGroup[],\n  inherited: boolean,\n  initial: StyleValue,\n  mdnUrl?: string\n}\ntype Properties =\n  Record<__HyphenatedProperty, PropertyData> &\n  Record<\\`--\\${string}\\`, undefined>\nexport const properties: Properties = ${JSON.stringify(propertiesData, null, 2)}\n`;\nwriteFileSync(join(targetDir, \"properties.ts\"), propertiesContent);\n\nconst keywordValues = Object.fromEntries(\n  Object.entries(getKeywordValues(filteredData.allLonghands)).map(\n    ([property, data]) => [property, data]\n  )\n);\nconst keywordValuesContent = `${autogeneratedHint}\nimport type { __HyphenatedProperty } from \"@webstudio-is/css-engine\"\ntype KeywordValues =\n  Record<__HyphenatedProperty, string[]> &\n  Record<\\`--\\${string}\\`, undefined>\nexport const keywordValues: KeywordValues = ${JSON.stringify(keywordValues, null, 2)}\n`;\nwriteFileSync(join(targetDir, \"keyword-values.ts\"), keywordValuesContent);\n\nwriteToFile(\n  \"shorthand-properties.ts\",\n  \"shorthandProperties\",\n  Object.keys(filteredData.allShorthands)\n);\nwriteToFile(\n  \"animatable-properties.ts\",\n  \"animatableProperties\",\n  Object.keys(filteredData.animatableLonghands)\n);\nwriteToFile(\"pseudo-elements.ts\", \"pseudoElements\", pseudoElements);\nwriteToFile(\"pseudo-classes.ts\", \"pseudoClasses\", pseudoClasses);\n\nconst typesFile = join(\n  process.cwd(),\n  \"../css-engine/src/__generated__/types.ts\"\n);\nmkdirSync(dirname(typesFile), { recursive: true });\nwriteFileSync(typesFile, autogeneratedHint + getTypes(longhandPropertiesData));\n"
  },
  {
    "path": "packages/css-data/bin/prompts/declarations.prompt.md",
    "content": "I created a no-code app for building websites. People who don't know HTML and CSS can use it to create sites easily. My app has a styles panel where the can tweak CSS properties and values for the selected element. Every control consists of a label and an input field. The label is the CSS property name whereas the input contains the property value. When the user hover over the property name we show a tooltip with information about the declaration (property and its value).\n\nThe fundamental purpose of tooltip information is to teach users how to use a part of the UI to accomplish their intentions, therefore explanations should be descriptive and educative. Here is an example of a bad explanation and a good one:\n\n- Bad explanation for `align-content: normal`: \"Aligns content as usual.\".\n- Good explanation for `align-content: normal`: \"- The items are packed in their default position as if no align-content value was set.\"\n\nI will now give you a list of CSS declarations and I want you to generate a matching list of explanations that are no longer than 200 characters and teach the user what is the declaration about! Include a description of what both the property and its value do.\n\nHere is the list of CSS declarations:\n\n```\n{declarations}\n```\n\nRespond with a matching list of explanations as a markdown code block.\n\nDon't use CSS syntax or CSS property names or CSS values inside the explanation:\n\nWrong:\n`- text-wrap: wrap - The text wraps within the specified container.`\nCorrect:\n`- The items are packed in their default position as if no align-content value was set.`\nWrong:\n`- Resets the \\\\`view-timeline-inset\\\\` property to its initial value.`\nCorrect:\n`- Resets the property to its initial value.`\n\nDon't use markdown syntax inside the explanation:\n\nWrong:\n`**WebkitFontSmoothing**: Adjusts smoothing of fonts on webkit browsers for better readability.`\nCorrect:\n`- Adjusts smoothing of fonts on webkit browsers for better readability.`\n\nFix grammar mistakes after you generated the explanations.\n\nThe response should start with ```markdown\n"
  },
  {
    "path": "packages/css-data/bin/prompts/properties.prompt.md",
    "content": "I created a no-code app for building websites. People who don't know HTML and CSS can use it to create sites easily. My app has a styles panel where the can tweak CSS properties for the selected element. Every control consists of a label and an input field. The label is the CSS property name. When the user hover over the property name we show a tooltip with information about the property.\n\nThe fundamental purpose of tooltip information is to teach users how to use a part of the UI to accomplish their intentions, therefore explanations should be descriptive and educative. Here is an example of a good explanation and a bad one:\n\n- Good: \"Controls the visual appearance of checkboxes, radio buttons, and other form controls.\"\n- Bad: \"Controls the white space of an element\". That's frustratingly useless because it just repeats the property name (white-space) without adding any teaching information.\n\nI will now give you a list of CSS properties and I want you to generate a matching list of explanations that are no longer than 200 characters and teach the user what is the property about! I looked at https://css-tricks.com/almanac/properties/ and their explanations seem very good!\n\nHere is the list of CSS properties:\n\n```\n{properties}\n```\n\nRespond with a matching list of explanations as a markdown code block.\nVery important: don't repeat the property name at the beginning of the explanation!\n\nThe response should start with ```markdown\n"
  },
  {
    "path": "packages/css-data/bin/prompts/pseudo-selectors.prompt.md",
    "content": "```prompt\nI created a no-code app for building websites. People who don't know HTML and CSS can use it to create sites easily. My app has a styles panel where users can apply styles to pseudo-classes and pseudo-elements. When the user hovers over a pseudo-class or pseudo-element name, we show a tooltip with information about what it does.\n\nThe fundamental purpose of tooltip information is to teach users how to use a part of the UI to accomplish their intentions, therefore explanations should be descriptive and educative. Here is an example of a good explanation and a bad one:\n\n- Good for :hover: \"Matches when the user's pointer is over the element, like when moving a mouse cursor over a button.\"\n- Bad for :hover: \"Applies styles when hovering\". That's frustratingly useless because it just repeats the name without adding any teaching information.\n\nI will now give you a list of CSS pseudo-classes and pseudo-elements and I want you to generate a matching list of explanations that are no longer than 200 characters and teach the user what the selector is about!\n\nHere is the list of CSS pseudo selectors:\n\n```\n\n{selectors}\n\n````\n\nRespond with a matching list of explanations as a markdown code block.\nVery important: don't repeat the selector name at the beginning of the explanation!\n\nThe response should start with ```markdown\n\n````\n"
  },
  {
    "path": "packages/css-data/bin/property-value-descriptions.ts",
    "content": "/* eslint-disable func-style */\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport warnOnce from \"warn-once\";\nimport pRetry from \"p-retry\";\nimport type { CreateChatCompletionResponse } from \"openai\";\nimport { keywordValues } from \"../src/__generated__/keyword-values\";\nimport { shorthandProperties } from \"../src/__generated__/shorthand-properties\";\nimport { customLonghandPropertyNames } from \"../src/custom-data\";\n\nconst propertiesPrompt = fs.readFileSync(\n  path.join(process.cwd(), \"bin\", \"prompts\", \"properties.prompt.md\"),\n  \"utf-8\"\n);\nconst declarationsPrompt = fs.readFileSync(\n  path.join(process.cwd(), \"bin\", \"prompts\", \"declarations.prompt.md\"),\n  \"utf-8\"\n);\n\n/**\n * Using ChatGPT, this scripts generates descriptions for CSS properties and declarations (property-value).\n * It uses `keywordValues` to get a list of all the properties and values.\n *\n * The result is a TS module located at ./src/__generated__/property-value-descriptions.ts which exports:\n * - properties\n * - declarations\n *\n * By default it generates descriptions only for new properties/values, however you can use the --force flag to regenerate everything.\n *\n * The script needs two env variables:\n * - process.env.OPENAI_KEY - Your OpenAI API key https://platform.openai.com/account/api-keys\n * - process.env.OPENAI_ORG - Your OpenAI Org ID https://platform.openai.com/account/org-settings\n */\n\nconst args = process.argv.slice(2);\nlet forceRegenerate = false;\nif (args.includes(\"--force\")) {\n  forceRegenerate = true;\n  args.splice(args.indexOf(\"--force\"), 1);\n}\n\nconst fileName = \"property-value-descriptions.ts\";\nconst targetPath = path.join(process.cwd(), \"src\", \"__generated__\");\n\nlet propertiesGenerated: Record<string, string> = {};\nlet propertiesOverrides: Record<string, string> = {};\nlet declarationsGenerated: Record<string, string> = {};\nlet declarationsOverrides: Record<string, string> = {};\nlet propertySyntaxesGenerated: Record<string, string> = {};\n\ntry {\n  ({\n    propertiesGenerated = {},\n    propertiesOverrides = {},\n    declarationsGenerated = {},\n    declarationsOverrides = {},\n    propertySyntaxesGenerated = {},\n    // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n    // @ts-ignore Fix this else it'll complain that we cannot use top-level await.\n  } = await import(path.join(targetPath, fileName)));\n} catch (error) {\n  //\n}\n\nconst batchSize = 16;\n\n/**\n * Properties descriptions\n */\nconst newPropertiesNames = [\n  ...Object.keys(keywordValues),\n  ...shorthandProperties,\n]\n  // Slice to generate only X - useful for testing.\n  // .slice(0, 30)\n  .filter(\n    (property) =>\n      forceRegenerate ||\n      (typeof propertiesGenerated[property] !== \"string\" &&\n        typeof propertiesOverrides[property] !== \"string\")\n  );\n\nconst newPropertySyntaxes = customLonghandPropertyNames.filter(\n  (property) =>\n    forceRegenerate ||\n    (typeof propertySyntaxesGenerated[property] !== \"string\" &&\n      typeof propertySyntaxesGenerated[property] !== \"string\")\n);\n\nfor (let index = 0; index < newPropertySyntaxes.length; ) {\n  const syntaxes = newPropertySyntaxes.slice(index, index + batchSize);\n\n  console.info(\n    `[${Math.floor(index / batchSize) + 1}/${Math.ceil(\n      newPropertySyntaxes.length / batchSize\n    )}] Generating property syntax descriptions.`\n  );\n\n  if (syntaxes.length === 0) {\n    index += batchSize;\n    continue;\n  }\n\n  const prompt = propertiesPrompt.replace(\n    \"{properties}\",\n    syntaxes.map((name) => `- ${name}`).join(\"\\n\")\n  );\n\n  const result = await generateWithRetry(prompt);\n  const descriptions = grabDescriptions(result);\n\n  if (syntaxes.length !== descriptions.length) {\n    console.info(\n      \"❌ Error: the number of generated descriptions does not match the amount of inputs. Retrying...\"\n    );\n\n    console.info({ input: syntaxes.join(\"\\n\"), output: result });\n    continue;\n  }\n\n  syntaxes.forEach((name, index) => {\n    propertySyntaxesGenerated[name] = (descriptions[index] ?? \"\")\n      .replace(new RegExp(`^\\`?${name}\\`?:`), \"\")\n      .trim();\n  });\n\n  writeFile({\n    propertiesGenerated,\n    propertiesOverrides,\n    propertySyntaxesGenerated,\n    properties: `{ ...propertiesGenerated, ...propertiesOverrides }`,\n    declarationsGenerated,\n    declarationsOverrides,\n    declarations: `{ ...declarationsGenerated, ...declarationsOverrides }`,\n  });\n\n  index += batchSize;\n}\nconsole.info(\"\\n✅ Properties syntax description generated!\\n\");\n\nfor (let index = 0; index < newPropertiesNames.length; ) {\n  const properties = newPropertiesNames.slice(index, index + batchSize);\n\n  console.info(\n    `[${Math.floor(index / batchSize) + 1}/${Math.ceil(\n      newPropertiesNames.length / batchSize\n    )}] Generating properties descriptions.`\n  );\n\n  if (properties.length === 0) {\n    index += batchSize;\n    continue;\n  }\n\n  const result = await generateWithRetry(\n    propertiesPrompt.replace(\n      \"{properties}\",\n      properties.map((name) => `- ${name}`).join(\"\\n\")\n    )\n  );\n  const descriptions = grabDescriptions(result);\n\n  if (properties.length !== descriptions.length) {\n    console.info(\n      \"❌ Error: the number of generated descriptions does not match the amount of inputs. Retrying...\"\n    );\n\n    console.info({ input: properties.join(\"\\n\"), output: result });\n    continue;\n  }\n\n  properties.forEach((name, index) => {\n    propertiesGenerated[name] = (descriptions[index] ?? \"\")\n      .replace(new RegExp(`^\\`?${name}\\`?:`), \"\")\n      .trim();\n  });\n\n  writeFile({\n    propertiesGenerated,\n    propertiesOverrides,\n    propertySyntaxesGenerated,\n    properties: `{ ...propertiesGenerated, ...propertiesOverrides }`,\n    declarationsGenerated,\n    declarationsOverrides,\n    declarations: `{ ...declarationsGenerated, ...declarationsOverrides }`,\n  });\n\n  index += batchSize;\n}\nconsole.info(\"\\n✅ Properties description generated!\\n\");\n\n/**\n * Declarations descriptions\n */\nconst newDeclarationsDescriptions: Record<string, string> = {};\nconst propertyColors: Record<string, string[]> = {};\n\nObject.entries(keywordValues)\n  // Slice to generate only X - useful for testing.\n  // .slice(0, 10)\n  .forEach(([keyword, values]) => {\n    values?.forEach((value) => {\n      const descriptionKey = `${keyword}:${value}`;\n      if (\n        !forceRegenerate &&\n        (typeof declarationsGenerated[descriptionKey] === \"string\" ||\n          typeof declarationsOverrides[descriptionKey] === \"string\")\n      ) {\n        return;\n      }\n\n      const property = toKebabCase(keyword);\n\n      const excludedProperties = [\"boxShadow\"];\n      // We are currently skipping generation for color-related properies. Uncomment the code below to enable colors generation.\n      // Also add the following line to the GPT prompt:\n      // \"When you encounter a declaration where the value is \\`{color}\\` make the description generic: this is a template that I will later use for all my colors.\"\n      //\n      // const nonColorValues = [\n      //   \"auto\",\n      //   \"initial\",\n      //   \"inherit\",\n      //   \"unset\",\n      //   \"currentColor\",\n      //   \"transparent\",\n      //   \"none\"\n      // ];\n      if (excludedProperties.includes(keyword)) {\n        return;\n      } else if (\n        keyword.toLowerCase().includes(\"color\")\n        // && !nonColorValues.includes(value)\n      ) {\n        return;\n        /*\n          When it comes to generating descriptions for declarations whose value is a color eg. `background-color: orange`\n          we generate a description for a generic `property: {color_value}` declaration (e.g. `background-color: {color}`)\n          and GPT will generate a description template with a placeholder `{color_value}`.\n          The script repeats the template for every color value, replacing `{color_value}`.\n          This is to avoid generating a ton of similar description and wasting GPT tokens.\n         */\n        // descriptionKey = `${keyword}:{color}`;\n        // if (!propertyColors[descriptionKey]) {\n        //   propertyColors[descriptionKey] = [];\n        // }\n        // propertyColors[descriptionKey].push(value);\n        // newDeclarationsDescriptions[descriptionKey] = `${property}: {color}`;\n      } else {\n        newDeclarationsDescriptions[descriptionKey] = `${property}: ${value}`;\n      }\n    });\n  });\n\nconst newDeclarationsDescriptionsEntries = Object.entries(\n  newDeclarationsDescriptions\n);\n\nfor (let index = 0; index < newDeclarationsDescriptionsEntries.length; ) {\n  const batch = newDeclarationsDescriptionsEntries.slice(\n    index,\n    index + batchSize\n  );\n\n  const list = batch.map(\n    ([_descriptionKey, declaration]) => `- ${declaration}`\n  );\n\n  console.info(\n    `[${Math.floor(index / batchSize) + 1}/${Math.ceil(\n      newDeclarationsDescriptionsEntries.length / batchSize\n    )}] Generating declarations descriptions.`\n  );\n\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore Fix this else it'll complain that we cannot use top-level await.\n  const result = await generateWithRetry(\n    declarationsPrompt.replace(\"{declarations}\", list.join(\"\\n\"))\n  );\n  const descriptions = grabDescriptions(result);\n\n  if (list.length !== descriptions.length) {\n    console.info(\n      \"❌ Error: the number of generated descriptions does not match the amount of inputs. Retrying...\"\n    );\n\n    console.info({ input: list.join(\"\\n\"), output: result });\n    continue;\n  }\n\n  batch.forEach((value, index) => {\n    const [descriptionKey, decl] = value;\n    const description = (descriptions[index] ?? \"\")\n      .replace(new RegExp(`^\\`${decl}\\` -`), \"\")\n      .trim();\n    if (descriptionKey.endsWith(\":{color}\")) {\n      const colors = propertyColors[descriptionKey];\n      if (colors) {\n        colors.forEach((color) => {\n          declarationsGenerated[descriptionKey.replace(\"{color}\", color)] =\n            description.replace(\"{color}\", color);\n        });\n      } else {\n        console.info(\n          `❌ Error: Expanding the colors for ${descriptionKey} failed because we couldn't find it in propertyColors. Skipping...`\n        );\n      }\n    } else {\n      declarationsGenerated[descriptionKey] = description;\n    }\n  });\n\n  writeFile({\n    propertiesGenerated,\n    propertiesOverrides,\n    propertySyntaxesGenerated,\n    properties: `{ ...propertiesGenerated, ...propertiesOverrides }`,\n    declarationsGenerated,\n    declarationsOverrides,\n    declarations: `{ ...declarationsGenerated, ...declarationsOverrides }`,\n  });\n\n  index += batchSize;\n}\n\nconsole.info(\"\\n✅ Declarations description generated!\\n\");\nconsole.info(\"✨ Done.\");\n\nfunction writeFile(descriptions: Record<string, unknown>) {\n  const autogeneratedHint = \"// This file was auto-generated\\n\";\n\n  const content =\n    autogeneratedHint +\n    Object.entries(descriptions)\n      .map(\n        ([binding, value]) =>\n          `export const ${binding} = ` +\n          (typeof value === \"string\" ? value : JSON.stringify(value, null, 2)) +\n          \" as Record<string, string | undefined>;\"\n      )\n      .join(\"\\n\\n\");\n\n  fs.writeFileSync(path.join(targetPath, fileName), content, \"utf-8\");\n}\n\nasync function generateWithRetry(message: string): Promise<string> {\n  return pRetry(\n    async (attempt) => {\n      const result = await generate(message);\n\n      // Check if result is an array (error response)\n      if (Array.isArray(result)) {\n        if (result[0] === 429) {\n          console.info(\n            `❌ Error: 429 ${result[1]}. Retrying attempt ${attempt}...`\n          );\n          throw new Error(\"Rate limit exceeded\"); // Retry on rate limit exceeded\n        } else {\n          throw new Error(`❌ Error: ${result[0]} ${result[1]}`); // Retry on other errors\n        }\n      }\n\n      // If the result isn't a string, retry\n      if (typeof result !== \"string\") {\n        console.info(\n          `❌ Error: Unexpected result type. Retrying attempt ${attempt}...`\n        );\n        throw new Error(\"Unexpected result type\");\n      }\n\n      // Return the result if it's a valid string\n      return result;\n    },\n    {\n      retries: 5, // Number of retries\n      factor: 2, // Exponential backoff factor\n      minTimeout: 1000, // Minimum wait between retries (1 second)\n      maxTimeout: 5000, // Maximum wait between retries (5 seconds)\n    }\n  );\n}\n\nasync function generate(message: string): Promise<string | [number, string]> {\n  const { OPENAI_ORG, OPENAI_KEY } = process.env;\n\n  if (OPENAI_KEY === undefined) {\n    throw new Error(\"Missing OpenAI key (process.env.OPENAI_KEY)\");\n  }\n\n  const headers: HeadersInit = {\n    \"Content-Type\": \"application/json\",\n    Accept: \"application/json\",\n    Authorization: `Bearer ${OPENAI_KEY}`,\n  };\n\n  if (OPENAI_ORG?.startsWith(\"org-\")) {\n    headers[\"OpenAI-Organization\"] = OPENAI_ORG;\n  } else {\n    warnOnce(\n      true,\n      \"Missing OpenAI org (process.env.OPENAI_ORG) or invalid org\"\n    );\n  }\n\n  const response = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify({\n      model: \"gpt-3.5-turbo\",\n      messages: [{ role: \"user\", content: message }],\n      temperature: 1,\n    }),\n  });\n\n  if (response.ok === false) {\n    return [response.status, response.statusText];\n  }\n\n  const completion = (await response.json()) as CreateChatCompletionResponse;\n\n  const content = completion.choices[0]?.message?.content.trim() || \"\";\n\n  if (content === \"\") {\n    return [404, \"No response\"];\n  }\n\n  return content;\n}\n\nfunction grabDescriptions(message: string) {\n  const descriptions: string[] = [];\n  message.split(\"\\n\").forEach((line) => {\n    if (line.startsWith(\"-\")) {\n      descriptions.push(line.replace(/^-\\s*/, \"\"));\n    }\n  });\n  return descriptions;\n}\n\nfunction toKebabCase(keyword: string) {\n  const label = keyword\n    .trim()\n    .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n    .replace(/\\s+/g, \"-\")\n    .toLowerCase();\n\n  return label;\n}\n"
  },
  {
    "path": "packages/css-data/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/css-data\",\n  \"version\": \"0.0.0\",\n  \"description\": \"CSS Data\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"build:html.css\": \"tsx ./bin/html.css.ts && prettier --write ./src/__generated__/html.ts\",\n    \"build:mdn-data\": \"tsx ./bin/mdn-data.ts ./src/__generated__ &&  prettier --write \\\"./src/__generated__/\\\" \\\"../css-engine/src/__generated__/\\\"\",\n    \"build:descriptions\": \"tsx ./bin/property-value-descriptions.ts && prettier --write ./src/__generated__/property-value-descriptions.ts\",\n    \"test\": \"vitest run\"\n  },\n  \"bin\": {\n    \"css-to-ws\": \"./bin/css-to-ws.ts\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"html-tags\": \"^4.0.0\",\n    \"mdn-data\": \"2.23.0\",\n    \"vitest\": \"^3.1.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"peerDependencies\": {\n    \"zod\": \"^3.19.1\"\n  },\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"dependencies\": {\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"change-case\": \"^5.4.4\",\n    \"colorjs.io\": \"^0.6.1\",\n    \"css-tree\": \"^3.1.0\",\n    \"openai\": \"^3.2.1\",\n    \"p-retry\": \"^6.2.1\",\n    \"warn-once\": \"^0.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/css-data/src/__generated__/animatable-properties.ts",
    "content": "// This file was generated by pnpm mdn-data\nexport const animatableProperties = [\n  \"accent-color\",\n  \"aspect-ratio\",\n  \"backdrop-filter\",\n  \"background-clip\",\n  \"background-color\",\n  \"background-origin\",\n  \"background-position-x\",\n  \"background-position-y\",\n  \"background-size\",\n  \"block-size\",\n  \"border-block-end-color\",\n  \"border-block-end-width\",\n  \"border-block-start-color\",\n  \"border-block-start-width\",\n  \"border-bottom-color\",\n  \"border-bottom-left-radius\",\n  \"border-bottom-right-radius\",\n  \"border-bottom-width\",\n  \"border-end-end-radius\",\n  \"border-end-start-radius\",\n  \"border-image-outset\",\n  \"border-image-slice\",\n  \"border-image-width\",\n  \"border-inline-end-color\",\n  \"border-inline-end-width\",\n  \"border-inline-start-color\",\n  \"border-inline-start-width\",\n  \"border-left-color\",\n  \"border-left-width\",\n  \"border-right-color\",\n  \"border-right-width\",\n  \"border-start-end-radius\",\n  \"border-start-start-radius\",\n  \"border-top-color\",\n  \"border-top-left-radius\",\n  \"border-top-right-radius\",\n  \"border-top-width\",\n  \"bottom\",\n  \"box-shadow\",\n  \"caret-color\",\n  \"clip-path\",\n  \"color\",\n  \"column-count\",\n  \"column-gap\",\n  \"column-rule-color\",\n  \"column-rule-width\",\n  \"column-width\",\n  \"contain-intrinsic-block-size\",\n  \"contain-intrinsic-height\",\n  \"contain-intrinsic-inline-size\",\n  \"contain-intrinsic-width\",\n  \"container-type\",\n  \"content-visibility\",\n  \"counter-increment\",\n  \"counter-reset\",\n  \"counter-set\",\n  \"cx\",\n  \"cy\",\n  \"d\",\n  \"display\",\n  \"fill\",\n  \"fill-opacity\",\n  \"filter\",\n  \"flex-basis\",\n  \"flex-grow\",\n  \"flex-shrink\",\n  \"flood-color\",\n  \"flood-opacity\",\n  \"font-palette\",\n  \"font-size\",\n  \"font-size-adjust\",\n  \"font-style\",\n  \"font-variation-settings\",\n  \"font-weight\",\n  \"grid-auto-columns\",\n  \"grid-auto-rows\",\n  \"grid-template-columns\",\n  \"grid-template-rows\",\n  \"height\",\n  \"hyphenate-limit-chars\",\n  \"initial-letter\",\n  \"inline-size\",\n  \"inset-block-end\",\n  \"inset-block-start\",\n  \"inset-inline-end\",\n  \"inset-inline-start\",\n  \"left\",\n  \"letter-spacing\",\n  \"lighting-color\",\n  \"line-clamp\",\n  \"line-height\",\n  \"margin-block-end\",\n  \"margin-block-start\",\n  \"margin-bottom\",\n  \"margin-inline-end\",\n  \"margin-inline-start\",\n  \"margin-left\",\n  \"margin-right\",\n  \"margin-top\",\n  \"mask-position\",\n  \"mask-size\",\n  \"max-block-size\",\n  \"max-height\",\n  \"max-inline-size\",\n  \"max-width\",\n  \"min-block-size\",\n  \"min-height\",\n  \"min-inline-size\",\n  \"min-width\",\n  \"object-position\",\n  \"offset-anchor\",\n  \"offset-distance\",\n  \"offset-path\",\n  \"offset-position\",\n  \"offset-rotate\",\n  \"opacity\",\n  \"order\",\n  \"orphans\",\n  \"outline-color\",\n  \"outline-offset\",\n  \"outline-style\",\n  \"outline-width\",\n  \"padding-block-end\",\n  \"padding-block-start\",\n  \"padding-bottom\",\n  \"padding-inline-end\",\n  \"padding-inline-start\",\n  \"padding-left\",\n  \"padding-right\",\n  \"padding-top\",\n  \"perspective\",\n  \"perspective-origin\",\n  \"r\",\n  \"right\",\n  \"rotate\",\n  \"row-gap\",\n  \"ruby-align\",\n  \"rx\",\n  \"ry\",\n  \"scale\",\n  \"scroll-margin-block-end\",\n  \"scroll-margin-block-start\",\n  \"scroll-margin-bottom\",\n  \"scroll-margin-inline-end\",\n  \"scroll-margin-inline-start\",\n  \"scroll-margin-left\",\n  \"scroll-margin-right\",\n  \"scroll-margin-top\",\n  \"scroll-padding-block-end\",\n  \"scroll-padding-block-start\",\n  \"scroll-padding-bottom\",\n  \"scroll-padding-inline-end\",\n  \"scroll-padding-inline-start\",\n  \"scroll-padding-left\",\n  \"scroll-padding-right\",\n  \"scroll-padding-top\",\n  \"scrollbar-color\",\n  \"shape-image-threshold\",\n  \"shape-margin\",\n  \"shape-outside\",\n  \"stroke-dasharray\",\n  \"stroke-dashoffset\",\n  \"stroke-miterlimit\",\n  \"stroke-opacity\",\n  \"stroke-width\",\n  \"tab-size\",\n  \"text-decoration-color\",\n  \"text-decoration-thickness\",\n  \"text-emphasis-color\",\n  \"text-indent\",\n  \"text-shadow\",\n  \"text-size-adjust\",\n  \"text-underline-offset\",\n  \"top\",\n  \"transform\",\n  \"transform-origin\",\n  \"translate\",\n  \"vertical-align\",\n  \"visibility\",\n  \"widows\",\n  \"width\",\n  \"word-spacing\",\n  \"x\",\n  \"y\",\n  \"z-index\",\n] as const;\n"
  },
  {
    "path": "packages/css-data/src/__generated__/html.ts",
    "content": "import type { StyleValue } from \"@webstudio-is/css-engine\";\n\nexport const html: Map<string, StyleValue> = new Map([\n  [\"article:display\", { type: \"keyword\", value: \"block\" }],\n  [\"aside:display\", { type: \"keyword\", value: \"block\" }],\n  [\"details:display\", { type: \"keyword\", value: \"block\" }],\n  [\"div:display\", { type: \"keyword\", value: \"block\" }],\n  [\"dt:display\", { type: \"keyword\", value: \"block\" }],\n  [\"figcaption:display\", { type: \"keyword\", value: \"block\" }],\n  [\"footer:display\", { type: \"keyword\", value: \"block\" }],\n  [\"form:display\", { type: \"keyword\", value: \"block\" }],\n  [\"header:display\", { type: \"keyword\", value: \"block\" }],\n  [\"hgroup:display\", { type: \"keyword\", value: \"block\" }],\n  [\"html:display\", { type: \"keyword\", value: \"block\" }],\n  [\"main:display\", { type: \"keyword\", value: \"block\" }],\n  [\"nav:display\", { type: \"keyword\", value: \"block\" }],\n  [\"section:display\", { type: \"keyword\", value: \"block\" }],\n  [\"summary:display\", { type: \"keyword\", value: \"block\" }],\n  [\"body:display\", { type: \"keyword\", value: \"block\" }],\n  [\"body:margin-top\", { type: \"unit\", unit: \"px\", value: 8 }],\n  [\"body:margin-right\", { type: \"unit\", unit: \"px\", value: 8 }],\n  [\"body:margin-bottom\", { type: \"unit\", unit: \"px\", value: 8 }],\n  [\"body:margin-left\", { type: \"unit\", unit: \"px\", value: 8 }],\n  [\"p:display\", { type: \"keyword\", value: \"block\" }],\n  [\"dl:display\", { type: \"keyword\", value: \"block\" }],\n  [\"p:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"dl:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"p:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"dl:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"dd:display\", { type: \"keyword\", value: \"block\" }],\n  [\"dd:margin-left\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"blockquote:display\", { type: \"keyword\", value: \"block\" }],\n  [\"figure:display\", { type: \"keyword\", value: \"block\" }],\n  [\"blockquote:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"blockquote:margin-right\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"blockquote:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"blockquote:margin-left\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"figure:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"figure:margin-right\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"figure:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"figure:margin-left\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"address:display\", { type: \"keyword\", value: \"block\" }],\n  [\"address:font-style\", { type: \"keyword\", value: \"italic\" }],\n  [\"h1:display\", { type: \"keyword\", value: \"block\" }],\n  [\"h1:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"h1:font-size\", { type: \"unit\", unit: \"em\", value: 2 }],\n  [\"h1:margin-top\", { type: \"unit\", unit: \"em\", value: 0.67 }],\n  [\"h1:margin-bottom\", { type: \"unit\", unit: \"em\", value: 0.67 }],\n  [\"h2:display\", { type: \"keyword\", value: \"block\" }],\n  [\"h2:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"h2:font-size\", { type: \"unit\", unit: \"em\", value: 1.5 }],\n  [\"h2:margin-top\", { type: \"unit\", unit: \"em\", value: 0.83 }],\n  [\"h2:margin-bottom\", { type: \"unit\", unit: \"em\", value: 0.83 }],\n  [\"h3:display\", { type: \"keyword\", value: \"block\" }],\n  [\"h3:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"h3:font-size\", { type: \"unit\", unit: \"em\", value: 1.17 }],\n  [\"h3:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"h3:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"h4:display\", { type: \"keyword\", value: \"block\" }],\n  [\"h4:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"h4:margin-top\", { type: \"unit\", unit: \"em\", value: 1.33 }],\n  [\"h4:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1.33 }],\n  [\"h5:display\", { type: \"keyword\", value: \"block\" }],\n  [\"h5:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"h5:font-size\", { type: \"unit\", unit: \"em\", value: 0.83 }],\n  [\"h5:margin-top\", { type: \"unit\", unit: \"em\", value: 1.67 }],\n  [\"h5:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1.67 }],\n  [\"h6:display\", { type: \"keyword\", value: \"block\" }],\n  [\"h6:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"h6:font-size\", { type: \"unit\", unit: \"em\", value: 0.67 }],\n  [\"h6:margin-top\", { type: \"unit\", unit: \"em\", value: 2.33 }],\n  [\"h6:margin-bottom\", { type: \"unit\", unit: \"em\", value: 2.33 }],\n  [\"pre:display\", { type: \"keyword\", value: \"block\" }],\n  [\"pre:white-space-collapse\", { type: \"keyword\", value: \"preserve\" }],\n  [\"pre:text-wrap-mode\", { type: \"keyword\", value: \"nowrap\" }],\n  [\"pre:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"pre:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"table:display\", { type: \"keyword\", value: \"table\" }],\n  [\"table:border-spacing\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"table:border-collapse\", { type: \"keyword\", value: \"separate\" }],\n  [\"table:box-sizing\", { type: \"keyword\", value: \"border-box\" }],\n  [\"table:text-indent\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"caption:display\", { type: \"keyword\", value: \"table-caption\" }],\n  [\"caption:text-align\", { type: \"keyword\", value: \"center\" }],\n  [\"tr:display\", { type: \"keyword\", value: \"table-row\" }],\n  [\"tr:vertical-align\", { type: \"keyword\", value: \"inherit\" }],\n  [\"col:display\", { type: \"keyword\", value: \"table-column\" }],\n  [\"colgroup:display\", { type: \"keyword\", value: \"table-column-group\" }],\n  [\"tbody:display\", { type: \"keyword\", value: \"table-row-group\" }],\n  [\"tbody:vertical-align\", { type: \"keyword\", value: \"middle\" }],\n  [\"thead:display\", { type: \"keyword\", value: \"table-header-group\" }],\n  [\"thead:vertical-align\", { type: \"keyword\", value: \"middle\" }],\n  [\"tfoot:display\", { type: \"keyword\", value: \"table-footer-group\" }],\n  [\"tfoot:vertical-align\", { type: \"keyword\", value: \"middle\" }],\n  [\"td:display\", { type: \"keyword\", value: \"table-cell\" }],\n  [\"td:vertical-align\", { type: \"keyword\", value: \"inherit\" }],\n  [\"td:padding-top\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"td:padding-right\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"td:padding-bottom\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"td:padding-left\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"th:display\", { type: \"keyword\", value: \"table-cell\" }],\n  [\"th:vertical-align\", { type: \"keyword\", value: \"inherit\" }],\n  [\"th:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"th:padding-top\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"th:padding-right\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"th:padding-bottom\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"th:padding-left\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"b:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"strong:font-weight\", { type: \"keyword\", value: \"bold\" }],\n  [\"i:font-style\", { type: \"keyword\", value: \"italic\" }],\n  [\"cite:font-style\", { type: \"keyword\", value: \"italic\" }],\n  [\"em:font-style\", { type: \"keyword\", value: \"italic\" }],\n  [\"var:font-style\", { type: \"keyword\", value: \"italic\" }],\n  [\"dfn:font-style\", { type: \"keyword\", value: \"italic\" }],\n  [\"code:font-family\", { type: \"fontFamily\", value: [\"monospace\"] }],\n  [\"kbd:font-family\", { type: \"fontFamily\", value: [\"monospace\"] }],\n  [\"samp:font-family\", { type: \"fontFamily\", value: [\"monospace\"] }],\n  [\"mark:background-color\", { type: \"keyword\", value: \"yellow\" }],\n  [\"mark:color\", { type: \"keyword\", value: \"black\" }],\n  [\"u:text-decoration-line\", { type: \"keyword\", value: \"underline\" }],\n  [\"ins:text-decoration-line\", { type: \"keyword\", value: \"underline\" }],\n  [\"s:text-decoration-line\", { type: \"keyword\", value: \"line-through\" }],\n  [\"del:text-decoration-line\", { type: \"keyword\", value: \"line-through\" }],\n  [\"sub:vertical-align\", { type: \"keyword\", value: \"sub\" }],\n  [\"sub:font-size\", { type: \"keyword\", value: \"smaller\" }],\n  [\"sup:vertical-align\", { type: \"keyword\", value: \"super\" }],\n  [\"sup:font-size\", { type: \"keyword\", value: \"smaller\" }],\n  [\"a:text-decoration-line\", { type: \"keyword\", value: \"underline\" }],\n  [\"a:cursor\", { type: \"keyword\", value: \"pointer\" }],\n  [\"a:color\", { type: \"rgb\", alpha: 1, r: 0, g: 0, b: 238 }],\n  [\"ul:display\", { type: \"keyword\", value: \"block\" }],\n  [\"ul:list-style-type\", { type: \"keyword\", value: \"disc\" }],\n  [\"ul:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"ul:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"ul:padding-left\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"ol:display\", { type: \"keyword\", value: \"block\" }],\n  [\"ol:list-style-type\", { type: \"keyword\", value: \"decimal\" }],\n  [\"ol:margin-top\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"ol:margin-bottom\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"ol:padding-left\", { type: \"unit\", unit: \"px\", value: 40 }],\n  [\"li:display\", { type: \"keyword\", value: \"list-item\" }],\n  [\"li:text-align\", { type: \"keyword\", value: \"match-parent\" }],\n  [\"hr:color\", { type: \"keyword\", value: \"gray\" }],\n  [\"hr:border-top-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"hr:border-right-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"hr:border-bottom-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"hr:border-left-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"hr:border-top-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"hr:border-right-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"hr:border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"hr:border-left-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"hr:margin-top\", { type: \"unit\", unit: \"em\", value: 0.5 }],\n  [\"hr:margin-right\", { type: \"keyword\", value: \"auto\" }],\n  [\"hr:margin-bottom\", { type: \"unit\", unit: \"em\", value: 0.5 }],\n  [\"hr:margin-left\", { type: \"keyword\", value: \"auto\" }],\n  [\"hr:overflow-x\", { type: \"keyword\", value: \"hidden\" }],\n  [\"hr:overflow-y\", { type: \"keyword\", value: \"hidden\" }],\n  [\"hr:display\", { type: \"keyword\", value: \"block\" }],\n  [\"legend:display\", { type: \"keyword\", value: \"block\" }],\n  [\"legend:padding-left\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"legend:padding-right\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:display\", { type: \"keyword\", value: \"block\" }],\n  [\"fieldset:margin-left\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:margin-right\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:padding-top\", { type: \"unit\", unit: \"em\", value: 0.35 }],\n  [\"fieldset:padding-right\", { type: \"unit\", unit: \"em\", value: 0.75 }],\n  [\"fieldset:padding-bottom\", { type: \"unit\", unit: \"em\", value: 0.625 }],\n  [\"fieldset:padding-left\", { type: \"unit\", unit: \"em\", value: 0.75 }],\n  [\"fieldset:border-top-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:border-right-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:border-bottom-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:border-left-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"fieldset:border-top-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"fieldset:border-right-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"fieldset:border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"fieldset:border-left-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"fieldset:border-top-color\", { type: \"keyword\", value: \"lightgray\" }],\n  [\"fieldset:border-right-color\", { type: \"keyword\", value: \"lightgray\" }],\n  [\"fieldset:border-bottom-color\", { type: \"keyword\", value: \"lightgray\" }],\n  [\"fieldset:border-left-color\", { type: \"keyword\", value: \"lightgray\" }],\n  [\"fieldset:min-width\", { type: \"keyword\", value: \"min-content\" }],\n  [\"label:cursor\", { type: \"keyword\", value: \"default\" }],\n  [\"input:appearance\", { type: \"keyword\", value: \"auto\" }],\n  [\"input:padding-top\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"input:padding-right\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"input:padding-bottom\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"input:padding-left\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"input:border-top-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"input:border-right-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"input:border-bottom-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"input:border-left-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"input:border-top-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"input:border-right-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"input:border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"input:border-left-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"input:background-color\", { type: \"keyword\", value: \"white\" }],\n  [\"input:cursor\", { type: \"keyword\", value: \"text\" }],\n  [\"textarea:color\", { type: \"keyword\", value: \"initial\" }],\n  [\"select:color\", { type: \"keyword\", value: \"initial\" }],\n  [\"button:color\", { type: \"keyword\", value: \"initial\" }],\n  [\"textarea:letter-spacing\", { type: \"keyword\", value: \"normal\" }],\n  [\"select:letter-spacing\", { type: \"keyword\", value: \"normal\" }],\n  [\"button:letter-spacing\", { type: \"keyword\", value: \"normal\" }],\n  [\"textarea:word-spacing\", { type: \"keyword\", value: \"normal\" }],\n  [\"select:word-spacing\", { type: \"keyword\", value: \"normal\" }],\n  [\"button:word-spacing\", { type: \"keyword\", value: \"normal\" }],\n  [\"textarea:line-height\", { type: \"keyword\", value: \"normal\" }],\n  [\"select:line-height\", { type: \"keyword\", value: \"normal\" }],\n  [\"button:line-height\", { type: \"keyword\", value: \"normal\" }],\n  [\"textarea:text-transform\", { type: \"keyword\", value: \"none\" }],\n  [\"select:text-transform\", { type: \"keyword\", value: \"none\" }],\n  [\"button:text-transform\", { type: \"keyword\", value: \"none\" }],\n  [\"textarea:text-indent\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"select:text-indent\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"button:text-indent\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\n    \"textarea:text-shadow\",\n    {\n      type: \"layers\",\n      value: [{ type: \"tuple\", value: [{ type: \"keyword\", value: \"none\" }] }],\n    },\n  ],\n  [\n    \"select:text-shadow\",\n    {\n      type: \"layers\",\n      value: [{ type: \"tuple\", value: [{ type: \"keyword\", value: \"none\" }] }],\n    },\n  ],\n  [\n    \"button:text-shadow\",\n    {\n      type: \"layers\",\n      value: [{ type: \"tuple\", value: [{ type: \"keyword\", value: \"none\" }] }],\n    },\n  ],\n  [\"textarea:display\", { type: \"keyword\", value: \"inline-block\" }],\n  [\"select:display\", { type: \"keyword\", value: \"inline-block\" }],\n  [\"button:display\", { type: \"keyword\", value: \"inline-block\" }],\n  [\"textarea:text-align\", { type: \"keyword\", value: \"start\" }],\n  [\"textarea:appearance\", { type: \"keyword\", value: \"auto\" }],\n  [\"textarea:margin-top\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"textarea:margin-bottom\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"textarea:border-top-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"textarea:border-right-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"textarea:border-bottom-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"textarea:border-left-width\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"textarea:border-top-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"textarea:border-right-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"textarea:border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"textarea:border-left-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"textarea:padding-top\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"textarea:padding-right\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"textarea:padding-bottom\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"textarea:padding-left\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"textarea:background-color\", { type: \"keyword\", value: \"white\" }],\n  [\"textarea:vertical-align\", { type: \"keyword\", value: \"text-bottom\" }],\n  [\"textarea:cursor\", { type: \"keyword\", value: \"text\" }],\n  [\"textarea:resize\", { type: \"keyword\", value: \"both\" }],\n  [\"textarea:white-space-collapse\", { type: \"keyword\", value: \"preserve\" }],\n  [\"textarea:text-wrap-mode\", { type: \"keyword\", value: \"wrap\" }],\n  [\"textarea:word-wrap\", { type: \"keyword\", value: \"break-word\" }],\n  [\"select:text-align\", { type: \"keyword\", value: \"start\" }],\n  [\"select:margin-top\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"select:margin-right\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"select:margin-bottom\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"select:margin-left\", { type: \"unit\", unit: \"number\", value: 0 }],\n  [\"select:padding-top\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"select:padding-right\", { type: \"unit\", unit: \"px\", value: 4 }],\n  [\"select:padding-bottom\", { type: \"unit\", unit: \"px\", value: 1 }],\n  [\"select:padding-left\", { type: \"unit\", unit: \"px\", value: 4 }],\n  [\"select:border-top-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"select:border-right-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"select:border-bottom-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"select:border-left-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"select:border-top-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"select:border-right-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"select:border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"select:border-left-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"select:text-wrap-mode\", { type: \"keyword\", value: \"nowrap\" }],\n  [\"select:word-wrap\", { type: \"keyword\", value: \"normal\" }],\n  [\"select:cursor\", { type: \"keyword\", value: \"default\" }],\n  [\"select:box-sizing\", { type: \"keyword\", value: \"border-box\" }],\n  [\"select:user-select\", { type: \"keyword\", value: \"none\" }],\n  [\"select:overflow-x\", { type: \"keyword\", value: \"clip\" }],\n  [\"select:overflow-y\", { type: \"keyword\", value: \"clip\" }],\n  [\"select:vertical-align\", { type: \"keyword\", value: \"baseline\" }],\n  [\"select:appearance\", { type: \"keyword\", value: \"auto\" }],\n  [\"option:display\", { type: \"keyword\", value: \"block\" }],\n  [\"option:float\", { type: \"keyword\", value: \"none\" }],\n  [\"option:position\", { type: \"keyword\", value: \"static\" }],\n  [\"option:min-height\", { type: \"unit\", unit: \"em\", value: 1 }],\n  [\"option:padding-top\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"option:padding-right\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"option:padding-bottom\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"option:padding-left\", { type: \"unit\", unit: \"px\", value: 4 }],\n  [\"option:user-select\", { type: \"keyword\", value: \"none\" }],\n  [\"option:text-wrap-mode\", { type: \"keyword\", value: \"nowrap\" }],\n  [\"option:word-wrap\", { type: \"keyword\", value: \"normal\" }],\n  [\"button:appearance\", { type: \"keyword\", value: \"auto\" }],\n  [\"button:padding-top\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"button:padding-right\", { type: \"unit\", unit: \"px\", value: 6 }],\n  [\"button:padding-bottom\", { type: \"unit\", unit: \"px\", value: 3 }],\n  [\"button:padding-left\", { type: \"unit\", unit: \"px\", value: 6 }],\n  [\"button:border-top-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"button:border-right-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"button:border-bottom-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"button:border-left-width\", { type: \"unit\", unit: \"px\", value: 2 }],\n  [\"button:border-top-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"button:border-right-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"button:border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"button:border-left-style\", { type: \"keyword\", value: \"solid\" }],\n  [\"button:cursor\", { type: \"keyword\", value: \"default\" }],\n  [\"button:box-sizing\", { type: \"keyword\", value: \"border-box\" }],\n  [\"button:user-select\", { type: \"keyword\", value: \"none\" }],\n  [\"button:text-align\", { type: \"keyword\", value: \"center\" }],\n  [\"button:background-color\", { type: \"keyword\", value: \"lightgray\" }],\n]);\n"
  },
  {
    "path": "packages/css-data/src/__generated__/keyword-values.ts",
    "content": "// This file was generated by pnpm mdn-data\n\nimport type { __HyphenatedProperty } from \"@webstudio-is/css-engine\";\ntype KeywordValues = Record<__HyphenatedProperty, string[]> &\n  Record<`--${string}`, undefined>;\nexport const keywordValues: KeywordValues = {\n  \"-webkit-font-smoothing\": [\n    \"auto\",\n    \"none\",\n    \"antialiased\",\n    \"subpixel-antialiased\",\n  ],\n  \"-moz-osx-font-smoothing\": [\"auto\", \"grayscale\"],\n  \"-webkit-box-orient\": [\"horizontal\", \"vertical\"],\n  \"view-timeline-name\": [],\n  \"scroll-timeline-name\": [],\n  \"view-timeline-inset\": [],\n  \"grid-auto-flow\": [\n    \"row\",\n    \"column\",\n    \"dense\",\n    \"row dense\",\n    \"column dense\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"list-style-type\": [\n    \"disc\",\n    \"circle\",\n    \"square\",\n    \"decimal\",\n    \"georgian\",\n    \"trad-chinese-informal\",\n    \"kannada\",\n    \"none\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"-webkit-line-clamp\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"-webkit-overflow-scrolling\": [\n    \"auto\",\n    \"touch\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"-webkit-tap-highlight-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"accent-color\": [\n    \"auto\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"align-content\": [\n    \"normal\",\n    \"first\",\n    \"last\",\n    \"baseline\",\n    \"space-between\",\n    \"space-around\",\n    \"space-evenly\",\n    \"stretch\",\n    \"unsafe\",\n    \"safe\",\n    \"center\",\n    \"start\",\n    \"end\",\n    \"flex-start\",\n    \"flex-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"align-items\": [\n    \"normal\",\n    \"stretch\",\n    \"first\",\n    \"last\",\n    \"baseline\",\n    \"unsafe\",\n    \"safe\",\n    \"center\",\n    \"start\",\n    \"end\",\n    \"self-start\",\n    \"self-end\",\n    \"flex-start\",\n    \"flex-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"align-self\": [\n    \"auto\",\n    \"normal\",\n    \"stretch\",\n    \"first\",\n    \"last\",\n    \"baseline\",\n    \"unsafe\",\n    \"safe\",\n    \"center\",\n    \"start\",\n    \"end\",\n    \"self-start\",\n    \"self-end\",\n    \"flex-start\",\n    \"flex-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"alignment-baseline\": [\n    \"baseline\",\n    \"alphabetic\",\n    \"ideographic\",\n    \"middle\",\n    \"central\",\n    \"mathematical\",\n    \"text-before-edge\",\n    \"text-after-edge\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"animation-composition\": [\n    \"replace\",\n    \"add\",\n    \"accumulate\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"animation-delay\": [\"initial\", \"inherit\", \"unset\"],\n  \"animation-direction\": [\n    \"normal\",\n    \"reverse\",\n    \"alternate\",\n    \"alternate-reverse\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"animation-duration\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"animation-fill-mode\": [\n    \"none\",\n    \"forwards\",\n    \"backwards\",\n    \"both\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"animation-iteration-count\": [\"infinite\", \"initial\", \"inherit\", \"unset\"],\n  \"animation-name\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"animation-play-state\": [\"running\", \"paused\", \"initial\", \"inherit\", \"unset\"],\n  \"animation-timing-function\": [\n    \"linear\",\n    \"ease\",\n    \"ease-in\",\n    \"ease-out\",\n    \"ease-in-out\",\n    \"step-start\",\n    \"step-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  appearance: [\n    \"none\",\n    \"auto\",\n    \"textfield\",\n    \"menulist-button\",\n    \"searchfield\",\n    \"textarea\",\n    \"push-button\",\n    \"slider-horizontal\",\n    \"checkbox\",\n    \"radio\",\n    \"square-button\",\n    \"menulist\",\n    \"listbox\",\n    \"meter\",\n    \"progress-bar\",\n    \"button\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"aspect-ratio\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"backdrop-filter\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"backface-visibility\": [\"visible\", \"hidden\", \"initial\", \"inherit\", \"unset\"],\n  \"background-attachment\": [\n    \"scroll\",\n    \"fixed\",\n    \"local\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-blend-mode\": [\n    \"normal\",\n    \"multiply\",\n    \"screen\",\n    \"overlay\",\n    \"darken\",\n    \"lighten\",\n    \"color-dodge\",\n    \"color-burn\",\n    \"hard-light\",\n    \"soft-light\",\n    \"difference\",\n    \"exclusion\",\n    \"hue\",\n    \"saturation\",\n    \"color\",\n    \"luminosity\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-clip\": [\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"border-area\",\n    \"text\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-image\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"background-origin\": [\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-position-x\": [\n    \"center\",\n    \"left\",\n    \"right\",\n    \"x-start\",\n    \"x-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-position-y\": [\n    \"center\",\n    \"top\",\n    \"bottom\",\n    \"y-start\",\n    \"y-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-repeat\": [\n    \"repeat-x\",\n    \"repeat-y\",\n    \"repeat\",\n    \"space\",\n    \"round\",\n    \"no-repeat\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"background-size\": [\n    \"auto\",\n    \"cover\",\n    \"contain\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"block-size\": [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-block-end-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-block-end-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-block-end-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-block-start-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-block-start-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-block-start-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-bottom-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-bottom-left-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-bottom-right-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-bottom-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-bottom-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-collapse\": [\"collapse\", \"separate\", \"initial\", \"inherit\", \"unset\"],\n  \"border-end-end-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-end-start-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-image-outset\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-image-repeat\": [\n    \"stretch\",\n    \"repeat\",\n    \"round\",\n    \"space\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-image-slice\": [\"fill\", \"initial\", \"inherit\", \"unset\"],\n  \"border-image-source\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"border-image-width\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"border-inline-end-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-inline-end-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-inline-end-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-inline-start-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-inline-start-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-inline-start-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-left-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-left-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-left-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-right-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-right-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-right-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-spacing\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-start-end-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-start-start-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-top-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-top-left-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-top-right-radius\": [\"initial\", \"inherit\", \"unset\"],\n  \"border-top-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"border-top-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  bottom: [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"box-decoration-break\": [\"slice\", \"clone\", \"initial\", \"inherit\", \"unset\"],\n  \"box-shadow\": [\n    \"none\",\n    \"inset\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"box-sizing\": [\"content-box\", \"border-box\", \"initial\", \"inherit\", \"unset\"],\n  \"break-after\": [\n    \"auto\",\n    \"avoid\",\n    \"always\",\n    \"all\",\n    \"avoid-page\",\n    \"page\",\n    \"left\",\n    \"right\",\n    \"recto\",\n    \"verso\",\n    \"avoid-column\",\n    \"column\",\n    \"avoid-region\",\n    \"region\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"break-before\": [\n    \"auto\",\n    \"avoid\",\n    \"always\",\n    \"all\",\n    \"avoid-page\",\n    \"page\",\n    \"left\",\n    \"right\",\n    \"recto\",\n    \"verso\",\n    \"avoid-column\",\n    \"column\",\n    \"avoid-region\",\n    \"region\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"break-inside\": [\n    \"auto\",\n    \"avoid\",\n    \"avoid-page\",\n    \"avoid-column\",\n    \"avoid-region\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"caption-side\": [\"top\", \"bottom\", \"initial\", \"inherit\", \"unset\"],\n  \"caret-color\": [\n    \"auto\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  clear: [\n    \"none\",\n    \"left\",\n    \"right\",\n    \"both\",\n    \"inline-start\",\n    \"inline-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"clip-path\": [\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"margin-box\",\n    \"fill-box\",\n    \"stroke-box\",\n    \"view-box\",\n    \"none\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"clip-rule\": [\"nonzero\", \"evenodd\", \"initial\", \"inherit\", \"unset\"],\n  color: [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"color-interpolation-filters\": [\n    \"auto\",\n    \"sRGB\",\n    \"linearRGB\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"color-scheme\": [\n    \"normal\",\n    \"light\",\n    \"dark\",\n    \"only\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"column-count\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"column-fill\": [\"auto\", \"balance\", \"initial\", \"inherit\", \"unset\"],\n  \"column-gap\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"column-rule-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"column-rule-style\": [\n    \"none\",\n    \"hidden\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"column-rule-width\": [\n    \"thin\",\n    \"medium\",\n    \"thick\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"column-span\": [\"none\", \"all\", \"initial\", \"inherit\", \"unset\"],\n  \"column-width\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  contain: [\n    \"none\",\n    \"strict\",\n    \"content\",\n    \"size\",\n    \"inline-size\",\n    \"layout\",\n    \"style\",\n    \"paint\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"contain-intrinsic-block-size\": [\n    \"auto\",\n    \"none\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"contain-intrinsic-height\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"contain-intrinsic-inline-size\": [\n    \"auto\",\n    \"none\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"contain-intrinsic-width\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"container-name\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"container-type\": [\n    \"normal\",\n    \"size\",\n    \"inline-size\",\n    \"scroll-state\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  content: [\n    \"normal\",\n    \"none\",\n    \"contents\",\n    \"open-quote\",\n    \"close-quote\",\n    \"no-open-quote\",\n    \"no-close-quote\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"content-visibility\": [\n    \"visible\",\n    \"auto\",\n    \"hidden\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"counter-increment\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"counter-reset\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"counter-set\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  cursor: [\n    \"auto\",\n    \"default\",\n    \"none\",\n    \"context-menu\",\n    \"help\",\n    \"pointer\",\n    \"progress\",\n    \"wait\",\n    \"cell\",\n    \"crosshair\",\n    \"text\",\n    \"vertical-text\",\n    \"alias\",\n    \"copy\",\n    \"move\",\n    \"no-drop\",\n    \"not-allowed\",\n    \"e-resize\",\n    \"n-resize\",\n    \"ne-resize\",\n    \"nw-resize\",\n    \"s-resize\",\n    \"se-resize\",\n    \"sw-resize\",\n    \"w-resize\",\n    \"ew-resize\",\n    \"ns-resize\",\n    \"nesw-resize\",\n    \"nwse-resize\",\n    \"col-resize\",\n    \"row-resize\",\n    \"all-scroll\",\n    \"zoom-in\",\n    \"zoom-out\",\n    \"grab\",\n    \"grabbing\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  cx: [\"initial\", \"inherit\", \"unset\"],\n  cy: [\"initial\", \"inherit\", \"unset\"],\n  d: [\"none\", \"initial\", \"inherit\", \"unset\"],\n  direction: [\"ltr\", \"rtl\", \"initial\", \"inherit\", \"unset\"],\n  display: [\n    \"block\",\n    \"inline\",\n    \"run-in\",\n    \"flow\",\n    \"flow-root\",\n    \"table\",\n    \"flex\",\n    \"grid\",\n    \"ruby\",\n    \"list-item\",\n    \"table-row-group\",\n    \"table-header-group\",\n    \"table-footer-group\",\n    \"table-row\",\n    \"table-cell\",\n    \"table-column-group\",\n    \"table-column\",\n    \"table-caption\",\n    \"ruby-base\",\n    \"ruby-text\",\n    \"ruby-base-container\",\n    \"ruby-text-container\",\n    \"contents\",\n    \"none\",\n    \"inline-block\",\n    \"inline-list-item\",\n    \"inline-table\",\n    \"inline-flex\",\n    \"inline-grid\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"dominant-baseline\": [\n    \"auto\",\n    \"text-bottom\",\n    \"alphabetic\",\n    \"ideographic\",\n    \"middle\",\n    \"central\",\n    \"mathematical\",\n    \"hanging\",\n    \"text-top\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"empty-cells\": [\"show\", \"hide\", \"initial\", \"inherit\", \"unset\"],\n  \"field-sizing\": [\"content\", \"fixed\", \"initial\", \"inherit\", \"unset\"],\n  fill: [\n    \"none\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"context-fill\",\n    \"context-stroke\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"fill-opacity\": [\"initial\", \"inherit\", \"unset\"],\n  \"fill-rule\": [\"nonzero\", \"evenodd\", \"initial\", \"inherit\", \"unset\"],\n  filter: [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"flex-basis\": [\n    \"content\",\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"flex-direction\": [\n    \"row\",\n    \"row-reverse\",\n    \"column\",\n    \"column-reverse\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"flex-grow\": [\"initial\", \"inherit\", \"unset\"],\n  \"flex-shrink\": [\"initial\", \"inherit\", \"unset\"],\n  \"flex-wrap\": [\n    \"nowrap\",\n    \"wrap\",\n    \"wrap-reverse\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  float: [\n    \"left\",\n    \"right\",\n    \"none\",\n    \"inline-start\",\n    \"inline-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"flood-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"flood-opacity\": [\"initial\", \"inherit\", \"unset\"],\n  \"font-family\": [\n    \"serif\",\n    \"sans-serif\",\n    \"system-ui\",\n    \"cursive\",\n    \"fantasy\",\n    \"math\",\n    \"monospace\",\n    \"ui-serif\",\n    \"ui-sans-serif\",\n    \"ui-monospace\",\n    \"ui-rounded\",\n    \"emoji\",\n    \"fangsong\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-feature-settings\": [\n    \"normal\",\n    \"on\",\n    \"off\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-kerning\": [\"auto\", \"normal\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"font-language-override\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"font-optical-sizing\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"font-palette\": [\"normal\", \"light\", \"dark\", \"initial\", \"inherit\", \"unset\"],\n  \"font-size\": [\n    \"xx-small\",\n    \"x-small\",\n    \"small\",\n    \"medium\",\n    \"large\",\n    \"x-large\",\n    \"xx-large\",\n    \"xxx-large\",\n    \"larger\",\n    \"smaller\",\n    \"math\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-size-adjust\": [\n    \"none\",\n    \"ex-height\",\n    \"cap-height\",\n    \"ch-width\",\n    \"ic-width\",\n    \"ic-height\",\n    \"from-font\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-style\": [\"normal\", \"italic\", \"oblique\", \"initial\", \"inherit\", \"unset\"],\n  \"font-synthesis-small-caps\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"font-synthesis-style\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"font-synthesis-weight\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"font-variant-alternates\": [\n    \"normal\",\n    \"historical-forms\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variant-caps\": [\n    \"normal\",\n    \"small-caps\",\n    \"all-small-caps\",\n    \"petite-caps\",\n    \"all-petite-caps\",\n    \"unicase\",\n    \"titling-caps\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variant-east-asian\": [\n    \"normal\",\n    \"jis78\",\n    \"jis83\",\n    \"jis90\",\n    \"jis04\",\n    \"simplified\",\n    \"traditional\",\n    \"full-width\",\n    \"proportional-width\",\n    \"ruby\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variant-emoji\": [\n    \"normal\",\n    \"text\",\n    \"emoji\",\n    \"unicode\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variant-ligatures\": [\n    \"normal\",\n    \"none\",\n    \"common-ligatures\",\n    \"no-common-ligatures\",\n    \"discretionary-ligatures\",\n    \"no-discretionary-ligatures\",\n    \"historical-ligatures\",\n    \"no-historical-ligatures\",\n    \"contextual\",\n    \"no-contextual\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variant-numeric\": [\n    \"normal\",\n    \"lining-nums\",\n    \"oldstyle-nums\",\n    \"proportional-nums\",\n    \"tabular-nums\",\n    \"diagonal-fractions\",\n    \"stacked-fractions\",\n    \"ordinal\",\n    \"slashed-zero\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variant-position\": [\n    \"normal\",\n    \"sub\",\n    \"super\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"font-variation-settings\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"font-weight\": [\n    \"normal\",\n    \"bold\",\n    \"bolder\",\n    \"lighter\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"forced-color-adjust\": [\n    \"auto\",\n    \"none\",\n    \"preserve-parent-color\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"grid-auto-columns\": [\n    \"min-content\",\n    \"max-content\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"grid-auto-rows\": [\n    \"min-content\",\n    \"max-content\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"grid-column-end\": [\"auto\", \"span\", \"initial\", \"inherit\", \"unset\"],\n  \"grid-column-start\": [\"auto\", \"span\", \"initial\", \"inherit\", \"unset\"],\n  \"grid-row-end\": [\"auto\", \"span\", \"initial\", \"inherit\", \"unset\"],\n  \"grid-row-start\": [\"auto\", \"span\", \"initial\", \"inherit\", \"unset\"],\n  \"grid-template-areas\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"grid-template-columns\": [\n    \"none\",\n    \"min-content\",\n    \"max-content\",\n    \"auto\",\n    \"subgrid\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"grid-template-rows\": [\n    \"none\",\n    \"min-content\",\n    \"max-content\",\n    \"auto\",\n    \"subgrid\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"hanging-punctuation\": [\n    \"none\",\n    \"first\",\n    \"force-end\",\n    \"allow-end\",\n    \"last\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  height: [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"hyphenate-character\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"hyphenate-limit-chars\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  hyphens: [\"none\", \"manual\", \"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"image-orientation\": [\"from-image\", \"flip\", \"initial\", \"inherit\", \"unset\"],\n  \"image-rendering\": [\n    \"auto\",\n    \"crisp-edges\",\n    \"pixelated\",\n    \"smooth\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"initial-letter\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"inline-size\": [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"inset-block-end\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"inset-block-start\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"inset-inline-end\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"inset-inline-start\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  isolation: [\"auto\", \"isolate\", \"initial\", \"inherit\", \"unset\"],\n  \"justify-content\": [\n    \"normal\",\n    \"space-between\",\n    \"space-around\",\n    \"space-evenly\",\n    \"stretch\",\n    \"unsafe\",\n    \"safe\",\n    \"center\",\n    \"start\",\n    \"end\",\n    \"flex-start\",\n    \"flex-end\",\n    \"left\",\n    \"right\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"justify-items\": [\n    \"normal\",\n    \"stretch\",\n    \"first\",\n    \"last\",\n    \"baseline\",\n    \"unsafe\",\n    \"safe\",\n    \"center\",\n    \"start\",\n    \"end\",\n    \"self-start\",\n    \"self-end\",\n    \"flex-start\",\n    \"flex-end\",\n    \"left\",\n    \"right\",\n    \"legacy\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"justify-self\": [\n    \"auto\",\n    \"normal\",\n    \"stretch\",\n    \"first\",\n    \"last\",\n    \"baseline\",\n    \"unsafe\",\n    \"safe\",\n    \"center\",\n    \"start\",\n    \"end\",\n    \"self-start\",\n    \"self-end\",\n    \"flex-start\",\n    \"flex-end\",\n    \"left\",\n    \"right\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  left: [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"letter-spacing\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"lighting-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"line-break\": [\n    \"auto\",\n    \"loose\",\n    \"normal\",\n    \"strict\",\n    \"anywhere\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"line-clamp\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"line-height\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"list-style-image\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"list-style-position\": [\"inside\", \"outside\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-block-end\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-block-start\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-bottom\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-inline-end\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-inline-start\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-left\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-right\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"margin-top\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"marker-end\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"marker-mid\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"marker-start\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-border-mode\": [\"luminance\", \"alpha\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-border-outset\": [\"initial\", \"inherit\", \"unset\"],\n  \"mask-border-repeat\": [\n    \"stretch\",\n    \"repeat\",\n    \"round\",\n    \"space\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-border-slice\": [\"fill\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-border-source\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-border-width\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-clip\": [\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"fill-box\",\n    \"stroke-box\",\n    \"view-box\",\n    \"no-clip\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-composite\": [\n    \"add\",\n    \"subtract\",\n    \"intersect\",\n    \"exclude\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-image\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-mode\": [\n    \"alpha\",\n    \"luminance\",\n    \"match-source\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-origin\": [\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"fill-box\",\n    \"stroke-box\",\n    \"view-box\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-position\": [\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-repeat\": [\n    \"repeat-x\",\n    \"repeat-y\",\n    \"repeat\",\n    \"space\",\n    \"round\",\n    \"no-repeat\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mask-size\": [\"auto\", \"cover\", \"contain\", \"initial\", \"inherit\", \"unset\"],\n  \"mask-type\": [\"luminance\", \"alpha\", \"initial\", \"inherit\", \"unset\"],\n  \"math-depth\": [\"auto-add\", \"initial\", \"inherit\", \"unset\"],\n  \"math-style\": [\"normal\", \"compact\", \"initial\", \"inherit\", \"unset\"],\n  \"max-block-size\": [\n    \"none\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"max-height\": [\n    \"none\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"max-inline-size\": [\n    \"none\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"max-width\": [\n    \"none\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"min-block-size\": [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"min-height\": [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"min-inline-size\": [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"min-width\": [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"mix-blend-mode\": [\n    \"normal\",\n    \"multiply\",\n    \"screen\",\n    \"overlay\",\n    \"darken\",\n    \"lighten\",\n    \"color-dodge\",\n    \"color-burn\",\n    \"hard-light\",\n    \"soft-light\",\n    \"difference\",\n    \"exclusion\",\n    \"hue\",\n    \"saturation\",\n    \"color\",\n    \"luminosity\",\n    \"plus-lighter\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"object-fit\": [\n    \"fill\",\n    \"contain\",\n    \"cover\",\n    \"none\",\n    \"scale-down\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"object-position\": [\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"offset-anchor\": [\n    \"auto\",\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"offset-distance\": [\"initial\", \"inherit\", \"unset\"],\n  \"offset-path\": [\n    \"none\",\n    \"closest-side\",\n    \"closest-corner\",\n    \"farthest-side\",\n    \"farthest-corner\",\n    \"sides\",\n    \"contain\",\n    \"at\",\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"fill-box\",\n    \"stroke-box\",\n    \"view-box\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"offset-position\": [\n    \"normal\",\n    \"auto\",\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"offset-rotate\": [\"auto\", \"reverse\", \"initial\", \"inherit\", \"unset\"],\n  opacity: [\"initial\", \"inherit\", \"unset\"],\n  order: [\"initial\", \"inherit\", \"unset\"],\n  orphans: [\"initial\", \"inherit\", \"unset\"],\n  \"outline-color\": [\n    \"auto\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"outline-offset\": [\"initial\", \"inherit\", \"unset\"],\n  \"outline-style\": [\n    \"auto\",\n    \"none\",\n    \"dotted\",\n    \"dashed\",\n    \"solid\",\n    \"double\",\n    \"groove\",\n    \"ridge\",\n    \"inset\",\n    \"outset\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"outline-width\": [\"thin\", \"medium\", \"thick\", \"initial\", \"inherit\", \"unset\"],\n  \"overflow-anchor\": [\"auto\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"overflow-block\": [\n    \"visible\",\n    \"hidden\",\n    \"clip\",\n    \"scroll\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overflow-clip-margin\": [\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overflow-inline\": [\n    \"visible\",\n    \"hidden\",\n    \"clip\",\n    \"scroll\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overflow-wrap\": [\n    \"normal\",\n    \"break-word\",\n    \"anywhere\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overflow-x\": [\n    \"visible\",\n    \"hidden\",\n    \"clip\",\n    \"scroll\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overflow-y\": [\n    \"visible\",\n    \"hidden\",\n    \"clip\",\n    \"scroll\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overscroll-behavior\": [\n    \"contain\",\n    \"none\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overscroll-behavior-block\": [\n    \"contain\",\n    \"none\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overscroll-behavior-inline\": [\n    \"contain\",\n    \"none\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overscroll-behavior-x\": [\n    \"contain\",\n    \"none\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"overscroll-behavior-y\": [\n    \"contain\",\n    \"none\",\n    \"auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"padding-block-end\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-block-start\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-bottom\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-inline-end\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-inline-start\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-left\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-right\": [\"initial\", \"inherit\", \"unset\"],\n  \"padding-top\": [\"initial\", \"inherit\", \"unset\"],\n  page: [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"paint-order\": [\n    \"normal\",\n    \"fill\",\n    \"stroke\",\n    \"markers\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  perspective: [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"perspective-origin\": [\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"pointer-events\": [\n    \"auto\",\n    \"none\",\n    \"visiblePainted\",\n    \"visibleFill\",\n    \"visibleStroke\",\n    \"visible\",\n    \"painted\",\n    \"fill\",\n    \"stroke\",\n    \"all\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  position: [\n    \"static\",\n    \"relative\",\n    \"absolute\",\n    \"sticky\",\n    \"fixed\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"print-color-adjust\": [\"economy\", \"exact\", \"initial\", \"inherit\", \"unset\"],\n  quotes: [\"none\", \"auto\", \"initial\", \"inherit\", \"unset\"],\n  r: [\"initial\", \"inherit\", \"unset\"],\n  resize: [\n    \"none\",\n    \"both\",\n    \"horizontal\",\n    \"vertical\",\n    \"block\",\n    \"inline\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  right: [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  rotate: [\"none\", \"x\", \"y\", \"z\", \"initial\", \"inherit\", \"unset\"],\n  \"row-gap\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"ruby-align\": [\n    \"start\",\n    \"center\",\n    \"space-between\",\n    \"space-around\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"ruby-position\": [\n    \"alternate\",\n    \"over\",\n    \"under\",\n    \"inter-character\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  rx: [\"initial\", \"inherit\", \"unset\"],\n  ry: [\"initial\", \"inherit\", \"unset\"],\n  scale: [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-behavior\": [\"auto\", \"smooth\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-block-end\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-block-start\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-bottom\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-inline-end\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-inline-start\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-left\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-right\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-margin-top\": [\"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-block-end\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-block-start\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-bottom\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-inline-end\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-inline-start\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-left\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-right\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-padding-top\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-snap-align\": [\n    \"none\",\n    \"start\",\n    \"end\",\n    \"center\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"scroll-snap-stop\": [\"normal\", \"always\", \"initial\", \"inherit\", \"unset\"],\n  \"scroll-snap-type\": [\n    \"none\",\n    \"x\",\n    \"y\",\n    \"block\",\n    \"inline\",\n    \"both\",\n    \"mandatory\",\n    \"proximity\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"scrollbar-color\": [\n    \"auto\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"scrollbar-gutter\": [\n    \"auto\",\n    \"stable\",\n    \"both-edges\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"scrollbar-width\": [\"auto\", \"thin\", \"none\", \"initial\", \"inherit\", \"unset\"],\n  \"shape-image-threshold\": [\"initial\", \"inherit\", \"unset\"],\n  \"shape-margin\": [\"initial\", \"inherit\", \"unset\"],\n  \"shape-outside\": [\n    \"none\",\n    \"content-box\",\n    \"padding-box\",\n    \"border-box\",\n    \"margin-box\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"shape-rendering\": [\n    \"auto\",\n    \"optimizeSpeed\",\n    \"crispEdges\",\n    \"geometricPrecision\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"stop-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"stop-opacity\": [\"initial\", \"inherit\", \"unset\"],\n  \"stroke-dasharray\": [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"stroke-dashoffset\": [\"initial\", \"inherit\", \"unset\"],\n  \"stroke-linecap\": [\"butt\", \"round\", \"square\", \"initial\", \"inherit\", \"unset\"],\n  \"stroke-linejoin\": [\n    \"miter\",\n    \"miter-clip\",\n    \"round\",\n    \"bevel\",\n    \"arcs\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"stroke-miterlimit\": [\"initial\", \"inherit\", \"unset\"],\n  \"stroke-opacity\": [\"initial\", \"inherit\", \"unset\"],\n  \"stroke-width\": [\"initial\", \"inherit\", \"unset\"],\n  \"tab-size\": [\"initial\", \"inherit\", \"unset\"],\n  \"table-layout\": [\"auto\", \"fixed\", \"initial\", \"inherit\", \"unset\"],\n  \"text-align\": [\n    \"start\",\n    \"end\",\n    \"left\",\n    \"right\",\n    \"center\",\n    \"justify\",\n    \"match-parent\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-align-last\": [\n    \"auto\",\n    \"start\",\n    \"end\",\n    \"left\",\n    \"right\",\n    \"center\",\n    \"justify\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-anchor\": [\"start\", \"middle\", \"end\", \"initial\", \"inherit\", \"unset\"],\n  \"text-combine-upright\": [\n    \"none\",\n    \"all\",\n    \"digits\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-decoration-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-decoration-line\": [\n    \"none\",\n    \"underline\",\n    \"overline\",\n    \"line-through\",\n    \"blink\",\n    \"spelling-error\",\n    \"grammar-error\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-decoration-skip-ink\": [\n    \"auto\",\n    \"all\",\n    \"none\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-decoration-style\": [\n    \"solid\",\n    \"double\",\n    \"dotted\",\n    \"dashed\",\n    \"wavy\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-decoration-thickness\": [\n    \"auto\",\n    \"from-font\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-emphasis-color\": [\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-emphasis-position\": [\n    \"auto\",\n    \"over\",\n    \"under\",\n    \"right\",\n    \"left\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-emphasis-style\": [\n    \"none\",\n    \"filled\",\n    \"open\",\n    \"dot\",\n    \"circle\",\n    \"double-circle\",\n    \"triangle\",\n    \"sesame\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-indent\": [\"hanging\", \"each-line\", \"initial\", \"inherit\", \"unset\"],\n  \"text-justify\": [\n    \"auto\",\n    \"inter-character\",\n    \"inter-word\",\n    \"none\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-orientation\": [\n    \"mixed\",\n    \"upright\",\n    \"sideways\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-overflow\": [\"clip\", \"ellipsis\", \"initial\", \"inherit\", \"unset\"],\n  \"text-rendering\": [\n    \"auto\",\n    \"optimizeSpeed\",\n    \"optimizeLegibility\",\n    \"geometricPrecision\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-shadow\": [\n    \"none\",\n    \"aliceblue\",\n    \"antiquewhite\",\n    \"aqua\",\n    \"aquamarine\",\n    \"azure\",\n    \"beige\",\n    \"bisque\",\n    \"black\",\n    \"blanchedalmond\",\n    \"blue\",\n    \"blueviolet\",\n    \"brown\",\n    \"burlywood\",\n    \"cadetblue\",\n    \"chartreuse\",\n    \"chocolate\",\n    \"coral\",\n    \"cornflowerblue\",\n    \"cornsilk\",\n    \"crimson\",\n    \"cyan\",\n    \"darkblue\",\n    \"darkcyan\",\n    \"darkgoldenrod\",\n    \"darkgray\",\n    \"darkgreen\",\n    \"darkgrey\",\n    \"darkkhaki\",\n    \"darkmagenta\",\n    \"darkolivegreen\",\n    \"darkorange\",\n    \"darkorchid\",\n    \"darkred\",\n    \"darksalmon\",\n    \"darkseagreen\",\n    \"darkslateblue\",\n    \"darkslategray\",\n    \"darkslategrey\",\n    \"darkturquoise\",\n    \"darkviolet\",\n    \"deeppink\",\n    \"deepskyblue\",\n    \"dimgray\",\n    \"dimgrey\",\n    \"dodgerblue\",\n    \"firebrick\",\n    \"floralwhite\",\n    \"forestgreen\",\n    \"fuchsia\",\n    \"gainsboro\",\n    \"ghostwhite\",\n    \"gold\",\n    \"goldenrod\",\n    \"gray\",\n    \"green\",\n    \"greenyellow\",\n    \"grey\",\n    \"honeydew\",\n    \"hotpink\",\n    \"indianred\",\n    \"indigo\",\n    \"ivory\",\n    \"khaki\",\n    \"lavender\",\n    \"lavenderblush\",\n    \"lawngreen\",\n    \"lemonchiffon\",\n    \"lightblue\",\n    \"lightcoral\",\n    \"lightcyan\",\n    \"lightgoldenrodyellow\",\n    \"lightgray\",\n    \"lightgreen\",\n    \"lightgrey\",\n    \"lightpink\",\n    \"lightsalmon\",\n    \"lightseagreen\",\n    \"lightskyblue\",\n    \"lightslategray\",\n    \"lightslategrey\",\n    \"lightsteelblue\",\n    \"lightyellow\",\n    \"lime\",\n    \"limegreen\",\n    \"linen\",\n    \"magenta\",\n    \"maroon\",\n    \"mediumaquamarine\",\n    \"mediumblue\",\n    \"mediumorchid\",\n    \"mediumpurple\",\n    \"mediumseagreen\",\n    \"mediumslateblue\",\n    \"mediumspringgreen\",\n    \"mediumturquoise\",\n    \"mediumvioletred\",\n    \"midnightblue\",\n    \"mintcream\",\n    \"mistyrose\",\n    \"moccasin\",\n    \"navajowhite\",\n    \"navy\",\n    \"oldlace\",\n    \"olive\",\n    \"olivedrab\",\n    \"orange\",\n    \"orangered\",\n    \"orchid\",\n    \"palegoldenrod\",\n    \"palegreen\",\n    \"paleturquoise\",\n    \"palevioletred\",\n    \"papayawhip\",\n    \"peachpuff\",\n    \"peru\",\n    \"pink\",\n    \"plum\",\n    \"powderblue\",\n    \"purple\",\n    \"rebeccapurple\",\n    \"red\",\n    \"rosybrown\",\n    \"royalblue\",\n    \"saddlebrown\",\n    \"salmon\",\n    \"sandybrown\",\n    \"seagreen\",\n    \"seashell\",\n    \"sienna\",\n    \"silver\",\n    \"skyblue\",\n    \"slateblue\",\n    \"slategray\",\n    \"slategrey\",\n    \"snow\",\n    \"springgreen\",\n    \"steelblue\",\n    \"tan\",\n    \"teal\",\n    \"thistle\",\n    \"tomato\",\n    \"turquoise\",\n    \"violet\",\n    \"wheat\",\n    \"white\",\n    \"whitesmoke\",\n    \"yellow\",\n    \"yellowgreen\",\n    \"transparent\",\n    \"currentColor\",\n    \"AccentColor\",\n    \"AccentColorText\",\n    \"ActiveText\",\n    \"ButtonBorder\",\n    \"ButtonFace\",\n    \"ButtonText\",\n    \"Canvas\",\n    \"CanvasText\",\n    \"Field\",\n    \"FieldText\",\n    \"GrayText\",\n    \"Highlight\",\n    \"HighlightText\",\n    \"LinkText\",\n    \"Mark\",\n    \"MarkText\",\n    \"SelectedItem\",\n    \"SelectedItemText\",\n    \"VisitedText\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-size-adjust\": [\"none\", \"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"text-transform\": [\n    \"none\",\n    \"capitalize\",\n    \"uppercase\",\n    \"lowercase\",\n    \"full-width\",\n    \"full-size-kana\",\n    \"math-auto\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-underline-offset\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"text-underline-position\": [\n    \"auto\",\n    \"from-font\",\n    \"under\",\n    \"left\",\n    \"right\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"text-wrap-mode\": [\"wrap\", \"nowrap\", \"initial\", \"inherit\", \"unset\"],\n  \"text-wrap-style\": [\n    \"auto\",\n    \"balance\",\n    \"stable\",\n    \"pretty\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  top: [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  \"touch-action\": [\n    \"auto\",\n    \"none\",\n    \"pan-x\",\n    \"pan-left\",\n    \"pan-right\",\n    \"pan-y\",\n    \"pan-up\",\n    \"pan-down\",\n    \"pinch-zoom\",\n    \"manipulation\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  transform: [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"transform-box\": [\n    \"content-box\",\n    \"border-box\",\n    \"fill-box\",\n    \"stroke-box\",\n    \"view-box\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"transform-origin\": [\n    \"left\",\n    \"center\",\n    \"right\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"transform-style\": [\"flat\", \"preserve-3d\", \"initial\", \"inherit\", \"unset\"],\n  \"transition-behavior\": [\n    \"normal\",\n    \"allow-discrete\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"transition-delay\": [\"initial\", \"inherit\", \"unset\"],\n  \"transition-duration\": [\"initial\", \"inherit\", \"unset\"],\n  \"transition-property\": [\"none\", \"all\", \"initial\", \"inherit\", \"unset\"],\n  \"transition-timing-function\": [\n    \"linear\",\n    \"ease\",\n    \"ease-in\",\n    \"ease-out\",\n    \"ease-in-out\",\n    \"step-start\",\n    \"step-end\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  translate: [\"none\", \"initial\", \"inherit\", \"unset\"],\n  \"unicode-bidi\": [\n    \"normal\",\n    \"embed\",\n    \"isolate\",\n    \"bidi-override\",\n    \"isolate-override\",\n    \"plaintext\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"user-select\": [\"auto\", \"text\", \"none\", \"all\", \"initial\", \"inherit\", \"unset\"],\n  \"vector-effect\": [\n    \"none\",\n    \"non-scaling-stroke\",\n    \"non-scaling-size\",\n    \"non-rotation\",\n    \"fixed-position\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"vertical-align\": [\n    \"baseline\",\n    \"sub\",\n    \"super\",\n    \"text-top\",\n    \"text-bottom\",\n    \"middle\",\n    \"top\",\n    \"bottom\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"view-transition-name\": [\n    \"none\",\n    \"match-element\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  visibility: [\"visible\", \"hidden\", \"collapse\", \"initial\", \"inherit\", \"unset\"],\n  \"white-space-collapse\": [\n    \"collapse\",\n    \"preserve\",\n    \"preserve-breaks\",\n    \"preserve-spaces\",\n    \"break-spaces\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  widows: [\"initial\", \"inherit\", \"unset\"],\n  width: [\n    \"auto\",\n    \"min-content\",\n    \"max-content\",\n    \"fit-content\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"will-change\": [\n    \"auto\",\n    \"scroll-position\",\n    \"contents\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"word-break\": [\n    \"normal\",\n    \"break-all\",\n    \"keep-all\",\n    \"break-word\",\n    \"auto-phrase\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  \"word-spacing\": [\"normal\", \"initial\", \"inherit\", \"unset\"],\n  \"word-wrap\": [\"normal\", \"break-word\", \"initial\", \"inherit\", \"unset\"],\n  \"writing-mode\": [\n    \"horizontal-tb\",\n    \"vertical-rl\",\n    \"vertical-lr\",\n    \"sideways-rl\",\n    \"sideways-lr\",\n    \"initial\",\n    \"inherit\",\n    \"unset\",\n  ],\n  x: [\"initial\", \"inherit\", \"unset\"],\n  y: [\"initial\", \"inherit\", \"unset\"],\n  \"z-index\": [\"auto\", \"initial\", \"inherit\", \"unset\"],\n  zoom: [\"normal\", \"reset\", \"initial\", \"inherit\", \"unset\"],\n};\n"
  },
  {
    "path": "packages/css-data/src/__generated__/properties.ts",
    "content": "// This file was generated by pnpm mdn-data\n\nimport type {\n  __HyphenatedProperty,\n  StyleValue,\n} from \"@webstudio-is/css-engine\";\ntype UnitGroup =\n  | \"number\"\n  | \"percentage\"\n  | \"angle\"\n  | \"decibel\"\n  | \"flex\"\n  | \"frequency\"\n  | \"length\"\n  | \"resolution\"\n  | \"semitones\"\n  | \"time\";\ntype PropertyData = {\n  unitGroups: UnitGroup[];\n  inherited: boolean;\n  initial: StyleValue;\n  mdnUrl?: string;\n};\ntype Properties = Record<__HyphenatedProperty, PropertyData> &\n  Record<`--${string}`, undefined>;\nexport const properties: Properties = {\n  \"-webkit-font-smoothing\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth\",\n  },\n  \"-moz-osx-font-smoothing\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth\",\n  },\n  \"-webkit-box-orient\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"horizontal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/box-orient\",\n  },\n  \"view-timeline-name\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-name\",\n  },\n  \"scroll-timeline-name\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-timeline-name\",\n  },\n  \"view-timeline-inset\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-inset\",\n  },\n  \"-webkit-line-clamp\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/-webkit-line-clamp\",\n  },\n  \"-webkit-overflow-scrolling\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n  },\n  \"-webkit-tap-highlight-color\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/-webkit-tap-highlight-color\",\n  },\n  \"accent-color\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/accent-color\",\n  },\n  \"align-content\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/align-content\",\n  },\n  \"align-items\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/align-items\",\n  },\n  \"align-self\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/align-self\",\n  },\n  \"alignment-baseline\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"baseline\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/alignment-baseline\",\n  },\n  \"animation-composition\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"replace\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-composition\",\n  },\n  \"animation-delay\": {\n    unitGroups: [\"time\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"s\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-delay\",\n  },\n  \"animation-direction\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-direction\",\n  },\n  \"animation-duration\": {\n    unitGroups: [\"time\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"s\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-duration\",\n  },\n  \"animation-fill-mode\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-fill-mode\",\n  },\n  \"animation-iteration-count\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/animation-iteration-count\",\n  },\n  \"animation-name\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-name\",\n  },\n  \"animation-play-state\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"running\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/animation-play-state\",\n  },\n  \"animation-timing-function\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"ease\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/animation-timing-function\",\n  },\n  appearance: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/appearance\",\n  },\n  \"aspect-ratio\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/aspect-ratio\",\n  },\n  \"backdrop-filter\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/backdrop-filter\",\n  },\n  \"backface-visibility\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"visible\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/backface-visibility\",\n  },\n  \"background-attachment\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"scroll\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-attachment\",\n  },\n  \"background-blend-mode\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-blend-mode\",\n  },\n  \"background-clip\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"border-box\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-clip\",\n  },\n  \"background-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"transparent\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-color\",\n  },\n  \"background-image\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-image\",\n  },\n  \"background-origin\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"padding-box\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-origin\",\n  },\n  \"background-position-x\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-position-x\",\n  },\n  \"background-position-y\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"%\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-position-y\",\n  },\n  \"background-repeat\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"repeat\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-repeat\",\n  },\n  \"background-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/background-size\",\n  },\n  \"block-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/block-size\",\n  },\n  \"border-block-end-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-block-end-color\",\n  },\n  \"border-block-end-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-block-end-style\",\n  },\n  \"border-block-end-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-block-end-width\",\n  },\n  \"border-block-start-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-block-start-color\",\n  },\n  \"border-block-start-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-block-start-style\",\n  },\n  \"border-block-start-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-block-start-width\",\n  },\n  \"border-bottom-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-bottom-color\",\n  },\n  \"border-bottom-left-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-bottom-left-radius\",\n  },\n  \"border-bottom-right-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-bottom-right-radius\",\n  },\n  \"border-bottom-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-bottom-style\",\n  },\n  \"border-bottom-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-bottom-width\",\n  },\n  \"border-collapse\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"separate\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-collapse\",\n  },\n  \"border-end-end-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-end-end-radius\",\n  },\n  \"border-end-start-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-end-start-radius\",\n  },\n  \"border-image-outset\": {\n    unitGroups: [\"length\", \"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-image-outset\",\n  },\n  \"border-image-repeat\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"stretch\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-image-repeat\",\n  },\n  \"border-image-slice\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"%\",\n      value: 100,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-image-slice\",\n  },\n  \"border-image-source\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-image-source\",\n  },\n  \"border-image-width\": {\n    unitGroups: [\"length\", \"percentage\", \"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-image-width\",\n  },\n  \"border-inline-end-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-inline-end-color\",\n  },\n  \"border-inline-end-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-inline-end-style\",\n  },\n  \"border-inline-end-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-inline-end-width\",\n  },\n  \"border-inline-start-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-inline-start-color\",\n  },\n  \"border-inline-start-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-inline-start-style\",\n  },\n  \"border-inline-start-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-inline-start-width\",\n  },\n  \"border-left-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-left-color\",\n  },\n  \"border-left-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-left-style\",\n  },\n  \"border-left-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-left-width\",\n  },\n  \"border-right-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-right-color\",\n  },\n  \"border-right-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-right-style\",\n  },\n  \"border-right-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-right-width\",\n  },\n  \"border-spacing\": {\n    unitGroups: [\"length\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-spacing\",\n  },\n  \"border-start-end-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-start-end-radius\",\n  },\n  \"border-start-start-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-start-start-radius\",\n  },\n  \"border-top-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-top-color\",\n  },\n  \"border-top-left-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-top-left-radius\",\n  },\n  \"border-top-right-radius\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/border-top-right-radius\",\n  },\n  \"border-top-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-top-style\",\n  },\n  \"border-top-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/border-top-width\",\n  },\n  bottom: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/bottom\",\n  },\n  \"box-decoration-break\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"slice\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/box-decoration-break\",\n  },\n  \"box-shadow\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/box-shadow\",\n  },\n  \"box-sizing\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"content-box\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/box-sizing\",\n  },\n  \"break-after\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/break-after\",\n  },\n  \"break-before\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/break-before\",\n  },\n  \"break-inside\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/break-inside\",\n  },\n  \"caption-side\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"top\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/caption-side\",\n  },\n  \"caret-color\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/caret-color\",\n  },\n  clear: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/clear\",\n  },\n  \"clip-path\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/clip-path\",\n  },\n  \"clip-rule\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"nonzero\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/clip-rule\",\n  },\n  color: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/color\",\n  },\n  \"color-interpolation-filters\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"linearRGB\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/color-interpolation-filters\",\n  },\n  \"color-scheme\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/color-scheme\",\n  },\n  \"column-count\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-count\",\n  },\n  \"column-fill\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"balance\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-fill\",\n  },\n  \"column-gap\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      value: 0,\n      unit: \"px\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-gap\",\n  },\n  \"column-rule-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-rule-color\",\n  },\n  \"column-rule-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-rule-style\",\n  },\n  \"column-rule-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-rule-width\",\n  },\n  \"column-span\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-span\",\n  },\n  \"column-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/column-width\",\n  },\n  contain: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/contain\",\n  },\n  \"contain-intrinsic-block-size\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-block-size\",\n  },\n  \"contain-intrinsic-height\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-height\",\n  },\n  \"contain-intrinsic-inline-size\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-inline-size\",\n  },\n  \"contain-intrinsic-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-width\",\n  },\n  \"container-name\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/container-name\",\n  },\n  \"container-type\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/container-type\",\n  },\n  content: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/content\",\n  },\n  \"content-visibility\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"visible\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/content-visibility\",\n  },\n  \"counter-increment\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/counter-increment\",\n  },\n  \"counter-reset\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/counter-reset\",\n  },\n  \"counter-set\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/counter-set\",\n  },\n  cursor: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/cursor\",\n  },\n  cx: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/cx\",\n  },\n  cy: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/cy\",\n  },\n  d: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/d\",\n  },\n  direction: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"ltr\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/direction\",\n  },\n  display: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"inline\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/display\",\n  },\n  \"dominant-baseline\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/dominant-baseline\",\n  },\n  \"empty-cells\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"show\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/empty-cells\",\n  },\n  \"field-sizing\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"fixed\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/field-sizing\",\n  },\n  fill: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/fill\",\n  },\n  \"fill-opacity\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/fill-opacity\",\n  },\n  \"fill-rule\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"nonzero\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/fill-rule\",\n  },\n  filter: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/filter\",\n  },\n  \"flex-basis\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flex-basis\",\n  },\n  \"flex-direction\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"row\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flex-direction\",\n  },\n  \"flex-grow\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flex-grow\",\n  },\n  \"flex-shrink\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flex-shrink\",\n  },\n  \"flex-wrap\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"nowrap\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flex-wrap\",\n  },\n  float: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/float\",\n  },\n  \"flood-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flood-color\",\n  },\n  \"flood-opacity\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/flood-opacity\",\n  },\n  \"font-family\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"fontFamily\",\n      value: [\"serif\"],\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-family\",\n  },\n  \"font-feature-settings\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-feature-settings\",\n  },\n  \"font-kerning\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-kerning\",\n  },\n  \"font-language-override\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-language-override\",\n  },\n  \"font-optical-sizing\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-optical-sizing\",\n  },\n  \"font-palette\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-palette\",\n  },\n  \"font-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-size\",\n  },\n  \"font-size-adjust\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-size-adjust\",\n  },\n  \"font-style\": {\n    unitGroups: [\"angle\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-style\",\n  },\n  \"font-synthesis-small-caps\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-small-caps\",\n  },\n  \"font-synthesis-style\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-style\",\n  },\n  \"font-synthesis-weight\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-synthesis-weight\",\n  },\n  \"font-variant-alternates\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/font-variant-alternates\",\n  },\n  \"font-variant-caps\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-variant-caps\",\n  },\n  \"font-variant-east-asian\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/font-variant-east-asian\",\n  },\n  \"font-variant-emoji\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-variant-emoji\",\n  },\n  \"font-variant-ligatures\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-variant-ligatures\",\n  },\n  \"font-variant-numeric\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-variant-numeric\",\n  },\n  \"font-variant-position\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-variant-position\",\n  },\n  \"font-variation-settings\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/font-variation-settings\",\n  },\n  \"font-weight\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/font-weight\",\n  },\n  \"forced-color-adjust\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/forced-color-adjust\",\n  },\n  \"grid-auto-columns\": {\n    unitGroups: [\"length\", \"percentage\", \"flex\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-auto-columns\",\n  },\n  \"grid-auto-flow\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"row\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-auto-flow\",\n  },\n  \"grid-auto-rows\": {\n    unitGroups: [\"length\", \"percentage\", \"flex\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-auto-rows\",\n  },\n  \"grid-column-end\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-column-end\",\n  },\n  \"grid-column-start\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-column-start\",\n  },\n  \"grid-row-end\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-row-end\",\n  },\n  \"grid-row-start\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-row-start\",\n  },\n  \"grid-template-areas\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-template-areas\",\n  },\n  \"grid-template-columns\": {\n    unitGroups: [\"length\", \"percentage\", \"flex\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-template-columns\",\n  },\n  \"grid-template-rows\": {\n    unitGroups: [\"length\", \"percentage\", \"flex\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/grid-template-rows\",\n  },\n  \"hanging-punctuation\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/hanging-punctuation\",\n  },\n  height: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/height\",\n  },\n  \"hyphenate-character\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/hyphenate-character\",\n  },\n  \"hyphenate-limit-chars\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/hyphenate-limit-chars\",\n  },\n  hyphens: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"manual\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/hyphens\",\n  },\n  \"image-orientation\": {\n    unitGroups: [\"angle\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"from-image\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/image-orientation\",\n  },\n  \"image-rendering\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/image-rendering\",\n  },\n  \"initial-letter\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/initial-letter\",\n  },\n  \"inline-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/inline-size\",\n  },\n  \"inset-block-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/inset-block-end\",\n  },\n  \"inset-block-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/inset-block-start\",\n  },\n  \"inset-inline-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/inset-inline-end\",\n  },\n  \"inset-inline-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/inset-inline-start\",\n  },\n  isolation: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/isolation\",\n  },\n  \"justify-content\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/justify-content\",\n  },\n  \"justify-items\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"legacy\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/justify-items\",\n  },\n  \"justify-self\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/justify-self\",\n  },\n  left: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/left\",\n  },\n  \"letter-spacing\": {\n    unitGroups: [\"length\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/letter-spacing\",\n  },\n  \"lighting-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"white\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/lighting-color\",\n  },\n  \"line-break\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/line-break\",\n  },\n  \"line-clamp\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/line-clamp\",\n  },\n  \"line-height\": {\n    unitGroups: [\"number\", \"length\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/line-height\",\n  },\n  \"list-style-image\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/list-style-image\",\n  },\n  \"list-style-position\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"outside\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/list-style-position\",\n  },\n  \"list-style-type\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"disc\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/list-style-type\",\n  },\n  \"margin-block-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-block-end\",\n  },\n  \"margin-block-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-block-start\",\n  },\n  \"margin-bottom\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-bottom\",\n  },\n  \"margin-inline-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-inline-end\",\n  },\n  \"margin-inline-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-inline-start\",\n  },\n  \"margin-left\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-left\",\n  },\n  \"margin-right\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-right\",\n  },\n  \"margin-top\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/margin-top\",\n  },\n  \"marker-end\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/marker-end\",\n  },\n  \"marker-mid\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/marker-mid\",\n  },\n  \"marker-start\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/marker-start\",\n  },\n  \"mask-border-mode\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"alpha\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-border-mode\",\n  },\n  \"mask-border-outset\": {\n    unitGroups: [\"length\", \"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-border-outset\",\n  },\n  \"mask-border-repeat\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"stretch\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-border-repeat\",\n  },\n  \"mask-border-slice\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-border-slice\",\n  },\n  \"mask-border-source\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-border-source\",\n  },\n  \"mask-border-width\": {\n    unitGroups: [\"length\", \"percentage\", \"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-border-width\",\n  },\n  \"mask-clip\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"border-box\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-clip\",\n  },\n  \"mask-composite\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"add\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-composite\",\n  },\n  \"mask-image\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-image\",\n  },\n  \"mask-mode\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"match-source\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-mode\",\n  },\n  \"mask-origin\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"border-box\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-origin\",\n  },\n  \"mask-position\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"tuple\",\n      value: [\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 0,\n        },\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 0,\n        },\n      ],\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-position\",\n  },\n  \"mask-repeat\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"repeat\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-repeat\",\n  },\n  \"mask-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-size\",\n  },\n  \"mask-type\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"luminance\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mask-type\",\n  },\n  \"math-depth\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/math-depth\",\n  },\n  \"math-style\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/math-style\",\n  },\n  \"max-block-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/max-block-size\",\n  },\n  \"max-height\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/max-height\",\n  },\n  \"max-inline-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/max-inline-size\",\n  },\n  \"max-width\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/max-width\",\n  },\n  \"min-block-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/min-block-size\",\n  },\n  \"min-height\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/min-height\",\n  },\n  \"min-inline-size\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/min-inline-size\",\n  },\n  \"min-width\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/min-width\",\n  },\n  \"mix-blend-mode\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/mix-blend-mode\",\n  },\n  \"object-fit\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"fill\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/object-fit\",\n  },\n  \"object-position\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"tuple\",\n      value: [\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 50,\n        },\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 50,\n        },\n      ],\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/object-position\",\n  },\n  \"offset-anchor\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/offset-anchor\",\n  },\n  \"offset-distance\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/offset-distance\",\n  },\n  \"offset-path\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/offset-path\",\n  },\n  \"offset-position\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/offset-position\",\n  },\n  \"offset-rotate\": {\n    unitGroups: [\"angle\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/offset-rotate\",\n  },\n  opacity: {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/opacity\",\n  },\n  order: {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/order\",\n  },\n  orphans: {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 2,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/orphans\",\n  },\n  \"outline-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/outline-color\",\n  },\n  \"outline-offset\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/outline-offset\",\n  },\n  \"outline-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/outline-style\",\n  },\n  \"outline-width\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"medium\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/outline-width\",\n  },\n  \"overflow-anchor\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-anchor\",\n  },\n  \"overflow-block\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-block\",\n  },\n  \"overflow-clip-margin\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-clip-margin\",\n  },\n  \"overflow-inline\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-inline\",\n  },\n  \"overflow-wrap\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-wrap\",\n  },\n  \"overflow-x\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"visible\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-x\",\n  },\n  \"overflow-y\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"visible\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-y\",\n  },\n  \"overscroll-behavior\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior\",\n  },\n  \"overscroll-behavior-block\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-block\",\n  },\n  \"overscroll-behavior-inline\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-inline\",\n  },\n  \"overscroll-behavior-x\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-x\",\n  },\n  \"overscroll-behavior-y\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-y\",\n  },\n  \"padding-block-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-block-end\",\n  },\n  \"padding-block-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-block-start\",\n  },\n  \"padding-bottom\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-bottom\",\n  },\n  \"padding-inline-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-inline-end\",\n  },\n  \"padding-inline-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-inline-start\",\n  },\n  \"padding-left\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-left\",\n  },\n  \"padding-right\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-right\",\n  },\n  \"padding-top\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/padding-top\",\n  },\n  page: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/page\",\n  },\n  \"paint-order\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/paint-order\",\n  },\n  perspective: {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/perspective\",\n  },\n  \"perspective-origin\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"tuple\",\n      value: [\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 50,\n        },\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 50,\n        },\n      ],\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/perspective-origin\",\n  },\n  \"pointer-events\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/pointer-events\",\n  },\n  position: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"static\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/position\",\n  },\n  \"print-color-adjust\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"economy\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/print-color-adjust\",\n  },\n  quotes: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"dependsOnUserAgent\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/quotes\",\n  },\n  r: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/r\",\n  },\n  resize: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/resize\",\n  },\n  right: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/right\",\n  },\n  rotate: {\n    unitGroups: [\"angle\", \"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/rotate\",\n  },\n  \"row-gap\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      value: 0,\n      unit: \"px\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/row-gap\",\n  },\n  \"ruby-align\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"space-around\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/ruby-align\",\n  },\n  \"ruby-position\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"alternate\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/ruby-position\",\n  },\n  rx: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/rx\",\n  },\n  ry: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/ry\",\n  },\n  scale: {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scale\",\n  },\n  \"scroll-behavior\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-behavior\",\n  },\n  \"scroll-margin-block-end\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block-end\",\n  },\n  \"scroll-margin-block-start\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block-start\",\n  },\n  \"scroll-margin-bottom\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-bottom\",\n  },\n  \"scroll-margin-inline-end\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline-end\",\n  },\n  \"scroll-margin-inline-start\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline-start\",\n  },\n  \"scroll-margin-left\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-left\",\n  },\n  \"scroll-margin-right\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-right\",\n  },\n  \"scroll-margin-top\": {\n    unitGroups: [\"length\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-margin-top\",\n  },\n  \"scroll-padding-block-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block-end\",\n  },\n  \"scroll-padding-block-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block-start\",\n  },\n  \"scroll-padding-bottom\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-bottom\",\n  },\n  \"scroll-padding-inline-end\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline-end\",\n  },\n  \"scroll-padding-inline-start\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline-start\",\n  },\n  \"scroll-padding-left\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-left\",\n  },\n  \"scroll-padding-right\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-right\",\n  },\n  \"scroll-padding-top\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-padding-top\",\n  },\n  \"scroll-snap-align\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-align\",\n  },\n  \"scroll-snap-stop\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-stop\",\n  },\n  \"scroll-snap-type\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scroll-snap-type\",\n  },\n  \"scrollbar-color\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scrollbar-color\",\n  },\n  \"scrollbar-gutter\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scrollbar-gutter\",\n  },\n  \"scrollbar-width\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/scrollbar-width\",\n  },\n  \"shape-image-threshold\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/shape-image-threshold\",\n  },\n  \"shape-margin\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/shape-margin\",\n  },\n  \"shape-outside\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/shape-outside\",\n  },\n  \"shape-rendering\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/shape-rendering\",\n  },\n  \"stop-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stop-color\",\n  },\n  \"stop-opacity\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"black\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stop-opacity\",\n  },\n  \"stroke-dasharray\": {\n    unitGroups: [\"length\", \"percentage\", \"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-dasharray\",\n  },\n  \"stroke-dashoffset\": {\n    unitGroups: [\"length\", \"percentage\", \"number\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-dashoffset\",\n  },\n  \"stroke-linecap\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"butt\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-linecap\",\n  },\n  \"stroke-linejoin\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"miter\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-linejoin\",\n  },\n  \"stroke-miterlimit\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 4,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-miterlimit\",\n  },\n  \"stroke-opacity\": {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-opacity\",\n  },\n  \"stroke-width\": {\n    unitGroups: [\"length\", \"percentage\", \"number\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/stroke-width\",\n  },\n  \"tab-size\": {\n    unitGroups: [\"number\", \"length\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 8,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/tab-size\",\n  },\n  \"table-layout\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/table-layout\",\n  },\n  \"text-align\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"start\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-align\",\n  },\n  \"text-align-last\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-align-last\",\n  },\n  \"text-anchor\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"start\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-anchor\",\n  },\n  \"text-combine-upright\": {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-combine-upright\",\n  },\n  \"text-decoration-color\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-decoration-color\",\n  },\n  \"text-decoration-line\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-decoration-line\",\n  },\n  \"text-decoration-skip-ink\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/text-decoration-skip-ink\",\n  },\n  \"text-decoration-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"solid\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-decoration-style\",\n  },\n  \"text-decoration-thickness\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/text-decoration-thickness\",\n  },\n  \"text-emphasis-color\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"currentColor\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-emphasis-color\",\n  },\n  \"text-emphasis-position\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-emphasis-position\",\n  },\n  \"text-emphasis-style\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-emphasis-style\",\n  },\n  \"text-indent\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-indent\",\n  },\n  \"text-justify\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-justify\",\n  },\n  \"text-orientation\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"mixed\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-orientation\",\n  },\n  \"text-overflow\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"clip\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-overflow\",\n  },\n  \"text-rendering\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-rendering\",\n  },\n  \"text-shadow\": {\n    unitGroups: [\"length\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-shadow\",\n  },\n  \"text-size-adjust\": {\n    unitGroups: [\"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-size-adjust\",\n  },\n  \"text-transform\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-transform\",\n  },\n  \"text-underline-offset\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-underline-offset\",\n  },\n  \"text-underline-position\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/text-underline-position\",\n  },\n  \"text-wrap-mode\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"wrap\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-wrap-mode\",\n  },\n  \"text-wrap-style\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/text-wrap-style\",\n  },\n  top: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/top\",\n  },\n  \"touch-action\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/touch-action\",\n  },\n  transform: {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transform\",\n  },\n  \"transform-box\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"view-box\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transform-box\",\n  },\n  \"transform-origin\": {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"tuple\",\n      value: [\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 50,\n        },\n        {\n          type: \"unit\",\n          unit: \"%\",\n          value: 50,\n        },\n        {\n          type: \"unit\",\n          unit: \"px\",\n          value: 0,\n        },\n      ],\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transform-origin\",\n  },\n  \"transform-style\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"flat\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transform-style\",\n  },\n  \"transition-behavior\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transition-behavior\",\n  },\n  \"transition-delay\": {\n    unitGroups: [\"time\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"s\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transition-delay\",\n  },\n  \"transition-duration\": {\n    unitGroups: [\"time\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"s\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transition-duration\",\n  },\n  \"transition-property\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"all\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/transition-property\",\n  },\n  \"transition-timing-function\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"ease\",\n    },\n    mdnUrl:\n      \"https://developer.mozilla.org/docs/Web/CSS/transition-timing-function\",\n  },\n  translate: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/translate\",\n  },\n  \"unicode-bidi\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/unicode-bidi\",\n  },\n  \"user-select\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/user-select\",\n  },\n  \"vector-effect\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/vector-effect\",\n  },\n  \"vertical-align\": {\n    unitGroups: [\"percentage\", \"length\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"baseline\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/vertical-align\",\n  },\n  \"view-transition-name\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"none\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/view-transition-name\",\n  },\n  visibility: {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"visible\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/visibility\",\n  },\n  \"white-space-collapse\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"collapse\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/white-space-collapse\",\n  },\n  widows: {\n    unitGroups: [\"number\"],\n    inherited: true,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 2,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/widows\",\n  },\n  width: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/width\",\n  },\n  \"will-change\": {\n    unitGroups: [],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/will-change\",\n  },\n  \"word-break\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/word-break\",\n  },\n  \"word-spacing\": {\n    unitGroups: [\"length\"],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/word-spacing\",\n  },\n  \"word-wrap\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"normal\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/overflow-wrap\",\n  },\n  \"writing-mode\": {\n    unitGroups: [],\n    inherited: true,\n    initial: {\n      type: \"keyword\",\n      value: \"horizontal-tb\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/writing-mode\",\n  },\n  x: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/x\",\n  },\n  y: {\n    unitGroups: [\"length\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"px\",\n      value: 0,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/y\",\n  },\n  \"z-index\": {\n    unitGroups: [\"number\"],\n    inherited: false,\n    initial: {\n      type: \"keyword\",\n      value: \"auto\",\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/z-index\",\n  },\n  zoom: {\n    unitGroups: [\"number\", \"percentage\"],\n    inherited: false,\n    initial: {\n      type: \"unit\",\n      unit: \"number\",\n      value: 1,\n    },\n    mdnUrl: \"https://developer.mozilla.org/docs/Web/CSS/zoom\",\n  },\n};\n"
  },
  {
    "path": "packages/css-data/src/__generated__/property-value-descriptions.ts",
    "content": "// This file was auto-generated\nexport const propertiesGenerated = {\n  accentColor: \"Sets the color used for highlighting user interface controls.\",\n  alignContent:\n    \"Aligns and distributes content along the cross/block axis of a flex or grid container.\",\n  alignItems:\n    \"Aligns items along the cross/block axis within a flex or grid container.\",\n  alignSelf: \"Aligns an item along the cross/block axis within its container.\",\n  alignTracks: \"Defines how tracks are aligned in a grid container.\",\n  animationComposition: \"Specifies whether an animation is additive or not.\",\n  animationDelay: \"Defines a delay for the start of an animation.\",\n  animationDirection:\n    \"Defines whether an animation should play forwards, backwards, or alternate.\",\n  animationDuration:\n    \"Specifies how long an animation should take to complete one cycle.\",\n  animationFillMode:\n    \"Sets which values are applied before/after the animation.\",\n  animationIterationCount: \"Defines how many times an animation should play.\",\n  animationName: \"Specifies the name of an animation to apply to an element.\",\n  animationPlayState: \"Specifies whether an animation is running or paused.\",\n  animationTimingFunction:\n    \"Specifies how an animation progresses over one cycle.\",\n  animationTimeline: \"Defines the timeline of an animation.\",\n  appearance:\n    \"Determines whether an element should be styled like a standard HTML element or not.\",\n  aspectRatio: \"Controls the aspect ratio of an element's box.\",\n  backdropFilter: \"Applies a filter effect to the background of an element.\",\n  backfaceVisibility: \"Defines whether the back face of an element is visible.\",\n  backgroundAttachment:\n    \"Sets whether a background image is fixed or scrolls with the rest of the page.\",\n  backgroundBlendMode:\n    \"Defines how background images blend with each other and the element's background color.\",\n  backgroundClip: \"Specifies the area where the background image is visible.\",\n  backgroundColor: \"Sets the background color of an element.\",\n  backgroundImage: \"Sets one or more background images for an element.\",\n  backgroundOrigin: \"Specifies where the background image(s) come from.\",\n  backgroundPosition: \"Sets the starting position of a background image.\",\n  backgroundPositionX:\n    \"Sets the horizontal starting position of a background image.\",\n  backgroundPositionY:\n    \"Sets the vertical starting position of a background image.\",\n  backgroundRepeat: \"Sets how a background image will be repeated.\",\n  backgroundSize: \"Sets the size of a background image.\",\n  blockOverflow:\n    \"Defines how to handle content that overflows the block container.\",\n  blockSize: \"Defines the height of an element's content box.\",\n  borderBlockColor:\n    \"Controls the color of the border of a block-level element.\",\n  borderBlockStyle:\n    \"Controls the style of the border of a block-level element.\",\n  borderBlockWidth:\n    \"Controls the width of the border of a block-level element.\",\n  borderBlockEndColor:\n    \"Controls the color of the border at the end of a block-level element.\",\n  borderBlockEndStyle:\n    \"Controls the style of the border at the end of a block-level element.\",\n  borderBlockEndWidth:\n    \"Controls the width of the border at the end of a block-level element.\",\n  borderBlockStartColor:\n    \"Controls the color of the border at the start of a block-level element.\",\n  borderBlockStartStyle:\n    \"Controls the style of the border at the start of a block-level element.\",\n  borderBlockStartWidth:\n    \"Controls the width of the border at the start of a block-level element.\",\n  borderBottomColor: \"Controls the color of the bottom border of an element.\",\n  borderBottomLeftRadius:\n    \"Controls the radius of the bottom-left corner of an element.\",\n  borderBottomRightRadius:\n    \"Controls the radius of the bottom-right corner of an element.\",\n  borderBottomStyle: \"Controls the style of the bottom border of an element.\",\n  borderBottomWidth: \"Controls the width of the bottom border of an element.\",\n  borderCollapse:\n    \"Controls whether the borders of table cells are collapsed or separated.\",\n  borderEndEndRadius:\n    \"Controls the radius of the end of the border at the end of a block-level element.\",\n  borderEndStartRadius:\n    \"Controls the radius of the rounded border corners at the end and start of a box.\",\n  borderImageOutset:\n    \"Determines the amount by which the border image area extends beyond the border box.\",\n  borderImageRepeat: \"Specifies how the border image is repeated.\",\n  borderImageSlice: \"Defines how to slice the border image.\",\n  borderImageSource: \"Specifies the path to the image to be used as a border.\",\n  borderImageWidth: \"Specifies the width of the border image.\",\n  borderInlineColor: \"Sets the color of the inline border.\",\n  borderInlineStyle: \"Sets the style of the inline border.\",\n  borderInlineWidth: \"Sets the width of the inline border.\",\n  borderInlineEndColor:\n    \"Sets the color of the inline border at the end of a box.\",\n  borderInlineEndStyle:\n    \"Sets the style of the inline border at the end of a box.\",\n  borderInlineEndWidth:\n    \"Sets the width of the inline border at the end of a box.\",\n  borderInlineStartColor:\n    \"Sets the color of the inline border at the start of a box.\",\n  borderInlineStartStyle:\n    \"Sets the style of the inline border at the start of a box.\",\n  borderInlineStartWidth:\n    \"Sets the width of the inline border at the start of a box.\",\n  borderLeftColor: \"Sets the color of the left border.\",\n  borderLeftStyle: \"Style of the left border of an element\",\n  borderLeftWidth: \"Width of the left border of an element\",\n  borderRightColor: \"Color of the right border of an element\",\n  borderRightStyle: \"Style of the right border of an element\",\n  borderRightWidth: \"Width of the right border of an element\",\n  borderSpacing: \"Distance between adjacent borders of elements\",\n  borderStartEndRadius: \"Radius of bottom right corner in block flow\",\n  borderStartStartRadius: \"Radius of top left corner in block flow\",\n  borderTopColor: \"Color of the top border of an element\",\n  borderTopLeftRadius: \"Radius of top left corner of an element\",\n  borderTopRightRadius: \"Radius of top right corner of an element\",\n  borderTopStyle: \"Style of the top border of an element\",\n  borderTopWidth: \"Width of the top border of an element\",\n  bottom:\n    \"Distance between the bottom edge of an element and the bottom edge of its containing block\",\n  boxDecorationBreak:\n    \"Specifies how the background, padding, border, and box shadow of an element are broken across multiple lines\",\n  boxShadow: \"Shadow effect around an element\",\n  boxSizing: \"Determines how the sizing of an element is calculated.\",\n  breakAfter: \"Specifies how page breaks should occur after an element.\",\n  breakBefore: \"Specifies how page breaks should occur before an element.\",\n  breakInside: \"Specifies how page breaks should occur within an element.\",\n  captionSide: \"Specifies the position of a table caption.\",\n  caretColor: \"Specifies the color of the cursor caret.\",\n  caretShape: \"Specifies the shape of the cursor caret.\",\n  clear:\n    \"Specifies which sides of an element where other floating elements are not allowed.\",\n  clip: \"Clips an absolutely positioned element.\",\n  clipPath: \"Clips an element to a specific shape.\",\n  color: \"Specifies the color of text.\",\n  printColorAdjust: \"Specifies how colors are adjusted during printing.\",\n  colorScheme: \"Specifies the preferred color scheme for the user agent.\",\n  columnCount:\n    \"Specifies the number of columns an element should be divided into.\",\n  columnFill: \"Specifies how to fill columns, balancing content.\",\n  columnGap: \"Specifies the gap between columns.\",\n  columnRuleColor: \"Sets the color of the rule between columns.\",\n  columnRuleStyle: \"Sets the style of the rule between columns.\",\n  columnRuleWidth: \"Sets the width of the rule between columns.\",\n  columnSpan: \"Specifies how many columns an element should span across.\",\n  columnWidth: \"Sets the width of columns in multi-column layouts.\",\n  contain: \"Defines the scope of potential element re-layouts.\",\n  containIntrinsicBlockSize: \"Specifies the ideal block size of the element.\",\n  containIntrinsicHeight: \"Specifies the ideal height of the element.\",\n  containIntrinsicInlineSize: \"Specifies the ideal inline size of the element.\",\n  containIntrinsicWidth: \"Specifies the ideal width of the element.\",\n  content:\n    \"Inserts generated content, like icons or text, before or after an element.\",\n  contentVisibility: \"Controls whether an element's content is visible.\",\n  counterIncrement: \"Increments the value of one or more CSS counters.\",\n  counterReset: \"Resets the value of one or more CSS counters.\",\n  counterSet: \"Sets the value of one or more CSS counters.\",\n  cursor:\n    \"Defines the type of cursor to be displayed when pointing over an element.\",\n  direction:\n    \"Sets the direction of text, either left-to-right or right-to-left.\",\n  display: \"Determines how an element is rendered on the page.\",\n  emptyCells:\n    \"Controls the display of borders and backgrounds for empty table cells.\",\n  filter: \"Applies visual effects (blur, grayscale, etc.) to an element.\",\n  flexBasis:\n    \"Sets the initial size of a flex item before free space is distributed.\",\n  flexDirection: \"Sets the direction of the main axis in a flex container.\",\n  flexGrow: \"Determines how much a flex item will grow relative to others.\",\n  flexShrink: \"Determines how much a flex item will shrink relative to others.\",\n  flexWrap:\n    \"Controls whether flex items are forced onto one line or can wrap onto multiple lines.\",\n  float: \"Positions an element to the left or right of its container.\",\n  fontFamily: \"Sets the font family of text content.\",\n  fontFeatureSettings:\n    \"Enables or disables font features like ligatures and kerning.\",\n  fontKerning: \"Controls the usage of kerning in fonts.\",\n  fontLanguageOverride: \"Overrides the language of text content.\",\n  fontOpticalSizing: \"Enables or disables optical sizing of fonts.\",\n  fontVariationSettings:\n    \"Enables or disables font variations like weight and width.\",\n  fontSize: \"The size of the font.\",\n  fontSizeAdjust: \"Preserve the readability of text when font fallback occurs.\",\n  fontStretch: \"The width variation of the font.\",\n  fontStyle: \"The style of the font (italic, oblique, normal).\",\n  fontSynthesis: \"The synthesis method used when rendering fonts.\",\n  fontVariant: \"The variation of the font (normal, small-caps).\",\n  fontVariantAlternates: \"Alternate glyphs for specific characters.\",\n  fontVariantCaps: \"Control of the capitalization of text.\",\n  fontVariantEastAsian: \"Alternate glyphs for East Asian scripts.\",\n  fontVariantLigatures: \"Control of ligatures and contextual forms.\",\n  fontVariantNumeric:\n    \"Alternate glyphs for numbers, fractions, and ordinal markers.\",\n  fontVariantPosition: \"Control of superscript and subscript glyphs.\",\n  fontWeight: \"The weight (boldness) of the font.\",\n  forcedColorAdjust: \"Control of forced background and foreground colors.\",\n  gridAutoColumns: \"The size of columns that are not explicitly set.\",\n  gridAutoFlow: \"The placement of grid items in the grid container.\",\n  gridAutoRows: \"Defines the size of rows not explicitly set in a grid.\",\n  gridColumnEnd: \"Defines the end position of a grid item's column.\",\n  gridColumnStart: \"Defines the start position of a grid item's column.\",\n  gridRowEnd: \"Defines the end position of a grid item's row.\",\n  gridRowStart: \"Defines the start position of a grid item's row.\",\n  gridTemplateAreas: \"Defines named grid areas for the grid container.\",\n  gridTemplateColumns: \"Defines the number and size of columns in a grid.\",\n  gridTemplateRows: \"Defines the number and size of rows in a grid.\",\n  hangingPunctuation:\n    \"Controls whether punctuation marks should hang outside the margin box of a block container.\",\n  height: \"Defines the height of an element.\",\n  hyphenateCharacter: \"Defines the character used as a hyphenation point.\",\n  hyphens: \"Controls hyphenation of text in block elements.\",\n  imageOrientation: \"Defines the orientation of an image.\",\n  imageRendering: \"Controls image scaling and pixelation.\",\n  imageResolution: \"Defines the pixel density of an image.\",\n  initialLetter:\n    \"Controls the styling of the first letter of a block-level element.\",\n  initialLetterAlign:\n    \"Controls the alignment of the first letter or character of an element.\",\n  inlineSize: \"Sets the size of an element in the inline direction.\",\n  inputSecurity:\n    \"Determines whether or not the text entered into a password field is visible.\",\n  insetBlockEnd:\n    \"Sets the block-end position of an element relative to its nearest positioned ancestor.\",\n  insetBlockStart:\n    \"Sets the block-start position of an element relative to its nearest positioned ancestor.\",\n  insetInlineEnd:\n    \"Sets the inline-end position of an element relative to its nearest positioned ancestor.\",\n  insetInlineStart:\n    \"Sets the inline-start position of an element relative to its nearest positioned ancestor.\",\n  isolation:\n    \"Determines whether an element should create a new stacking context.\",\n  justifyContent:\n    \"Aligns and distributes content along the main/inline axis of a flex or grid container.\",\n  justifyItems:\n    \"Aligns items along the inline axis within a flex or grid container.\",\n  justifySelf: \"Aligns an item along the inline axis within its container.\",\n  justifyTracks: \"Aligns grid tracks along the row or column axis.\",\n  left: \"Sets the left position of an element relative to its nearest positioned ancestor.\",\n  letterSpacing: \"Controls the spacing between characters in a block of text.\",\n  lineBreak:\n    \"Specifies how lines should break within words and other elements.\",\n  lineClamp:\n    \"Limits the number of lines of text that can be displayed within an element.\",\n  lineHeight: \"Controls the height of each line of text.\",\n  lineHeightStep: \"Controls the step used to increment line-height.\",\n  listStyleImage: \"Specifies an image as the list-item marker.\",\n  listStylePosition: \"Specifies the position of the list-item markers.\",\n  listStyleType: \"Specifies the type of list-item marker.\",\n  marginBlockEnd: \"Sets the margin at the bottom of a block-level element.\",\n  marginBlockStart: \"Sets the margin at the top of a block-level element.\",\n  marginBottom: \"Sets the margin at the bottom of an element.\",\n  marginInlineEnd:\n    \"Sets the margin on the right side of an inline-level element.\",\n  marginInlineStart:\n    \"Sets the margin on the left side of an inline-level element.\",\n  marginLeft: \"Sets the margin on the left side of an element.\",\n  marginRight: \"Sets the margin on the right side of an element.\",\n  marginTop: \"Sets the margin at the top of an element.\",\n  marginTrim:\n    \"Determines whether a margin is collapsed with its parent element.\",\n  maskBorderMode: \"Specifies how to fill the border of a masked element.\",\n  maskBorderOutset:\n    \"Specifies the distance by which to extend the border image.\",\n  maskBorderRepeat: \"Repeats the mask border image in the mask painting area.\",\n  maskBorderSlice:\n    \"Specifies inward offsets from the top, right, bottom, and left edges of the mask image.\",\n  maskBorderSource: \"Determines the mask border image.\",\n  maskBorderWidth: \"Specifies the widths of the mask border.\",\n  maskClip: \"Limits the painting area to the area inside the mask.\",\n  maskComposite:\n    \"Specifies how the mask image is composited with the element's background.\",\n  maskImage: \"Sets the mask image of an element.\",\n  maskMode: \"Specifies how mask images are combined.\",\n  maskOrigin:\n    \"Determines the position of the mask image relative to the mask position.\",\n  maskPosition: \"Sets the initial position of the mask image.\",\n  maskRepeat: \"Specifies how the mask image is repeated.\",\n  maskSize: \"Specifies the size of the mask image.\",\n  maskType: \"Specifies the type of mask.\",\n  masonryAutoFlow: \"Controls the flow direction and arrangement of grid items.\",\n  mathDepth:\n    \"Sets the minimum height of an element that contains math content.\",\n  mathShift:\n    \"Determines the amount of extra space to add between math content and the surrounding text.\",\n  mathStyle: \"Sets the display style for MathML formulas.\",\n  maxBlockSize: \"Specifies the maximum block-level size of an element.\",\n  maxHeight: \"Sets the maximum height of an element.\",\n  maxInlineSize: \"Specifies the maximum inline size of an element.\",\n  maxLines: \"Sets the maximum number of lines for an element.\",\n  maxWidth: \"Sets the maximum width of an element.\",\n  minBlockSize: \"Specifies the minimum block-level size of an element.\",\n  minHeight: \"Sets the minimum height of an element.\",\n  minInlineSize: \"Specifies the minimum inline size of an element.\",\n  minWidth: \"Sets the minimum width of an element.\",\n  mixBlendMode: \"Specifies how an element blends with its background.\",\n  objectFit: \"Specifies how the content of an element should fit.\",\n  objectPosition:\n    \"Specifies the alignment of the replaced element inside its container.\",\n  offsetAnchor:\n    \"Sets the positioning reference for an absolutely positioned element.\",\n  offsetDistance:\n    \"Specifies the distance between an element and its positioned container.\",\n  offsetPath: \"Specifies a motion path for an absolutely positioned element.\",\n  offsetPosition:\n    \"Controls the position of a positioned element relative to its containing block.\",\n  offsetRotate:\n    \"Controls the rotation of an element around a fixed point in degrees.\",\n  opacity: \"Controls the transparency of an element.\",\n  order:\n    \"Controls the order in which an element appears within its flex or grid container.\",\n  orphans: \"Controls the minimum number of lines for the last line of a block.\",\n  outlineColor: \"Controls the color of an element's outline.\",\n  outlineOffset:\n    \"Controls the space between an element's outline and its border.\",\n  outlineStyle: \"Controls the style of an element's outline.\",\n  outlineWidth: \"Controls the width of an element's outline.\",\n  overflow:\n    \"Controls how content overflows its container when it is too large.\",\n  overflowAnchor:\n    \"Controls whether or not to allow an element to be anchored to a specific point in the viewport.\",\n  overflowBlock: \"Controls how content overflows its container vertically.\",\n  overflowClipMargin:\n    \"Controls the margin that is used to clip the overflow of an element.\",\n  overflowInline: \"Controls how content overflows its container horizontally.\",\n  overflowWrap:\n    \"Controls how words should be wrapped when they are too long for their container.\",\n  overflowX: \"Controls how content overflows its container horizontally only.\",\n  overflowY: \"overflowY\",\n  overscrollBehavior: \"overscrollBehavior\",\n  overscrollBehaviorBlock: \"overscrollBehaviorBlock\",\n  overscrollBehaviorInline: \"overscrollBehaviorInline\",\n  overscrollBehaviorX: \"overscrollBehaviorX\",\n  overscrollBehaviorY: \"overscrollBehaviorY\",\n  paddingBlockEnd: \"paddingBlockEnd\",\n  paddingBlockStart: \"paddingBlockStart\",\n  paddingBottom:\n    \"Defines the space between the content of an element and its bottom border. Can affect layout height.\",\n  paddingInlineEnd: \"paddingInlineEnd\",\n  paddingInlineStart: \"paddingInlineStart\",\n  paddingLeft:\n    \"Defines the space between the content of an element and its left border. Can affect layout width.\",\n  paddingRight:\n    \"Defines the space between the content of an element and its right border. Can affect layout width.\",\n  paddingTop:\n    \"Defines the space between the content of an element and its top border. Can affect layout height.\",\n  pageBreakAfter: \"pageBreakAfter\",\n  pageBreakBefore: \"pageBreakBefore\",\n  pageBreakInside: \"Controls whether a page break occurs inside an element.\",\n  paintOrder: \"Specifies the order in which shapes are filled.\",\n  perspective: \"Defines the perspective from which an element is viewed.\",\n  perspectiveOrigin: \"Defines the position of the perspective element.\",\n  pointerEvents: \"Determines whether an element can be clicked.\",\n  position: \"Sets the position of an element.\",\n  quotes: \"Defines the quotation marks for a blockquote.\",\n  resize: \"Allows users to resize an element.\",\n  right:\n    \"Sets the distance between the right edge of an element and its container.\",\n  rotate: \"Rotates an element.\",\n  rowGap: \"Sets the size of the gap between rows in a grid.\",\n  rubyAlign:\n    \"Aligns ruby text to the start, center, or end of a ruby container.\",\n  rubyMerge: \"Determines how ruby text is merged between adjacent elements.\",\n  rubyPosition: \"Sets the position of the ruby text container.\",\n  scale: \"Scales an element up or down.\",\n  scrollbarColor: \"Sets the color of the scrollbar track and thumb.\",\n  scrollbarGutter:\n    \"Controls the size of the gutter (space) between scrollbar and element.\",\n  scrollbarWidth: \"Controls the width of the scrollbar.\",\n  scrollBehavior: \"Controls the smoothness of a scroll animation.\",\n  scrollMarginBlockStart: \"Controls the margin before the starting of a block.\",\n  scrollMarginBlockEnd: \"Controls the margin after the ending of a block.\",\n  scrollMarginBottom: \"Controls the margin at the bottom of an element.\",\n  scrollMarginInlineStart:\n    \"Controls the margin before the starting of an inline element.\",\n  scrollMarginInlineEnd:\n    \"Controls the margin after the ending of an inline element.\",\n  scrollMarginLeft: \"Controls the margin at the left side of an element.\",\n  scrollMarginRight: \"Controls the margin at the right side of an element.\",\n  scrollMarginTop: \"Controls the margin at the top of an element.\",\n  scrollPaddingBlockStart:\n    \"Controls the padding before the starting of a block.\",\n  scrollPaddingBlockEnd: \"Controls the padding after the ending of a block.\",\n  scrollPaddingBottom: \"Controls the padding at the bottom of an element.\",\n  scrollPaddingInlineStart:\n    \"Controls the padding before the starting of an inline element.\",\n  scrollPaddingInlineEnd:\n    \"Controls the padding after the ending of an inline element.\",\n  scrollPaddingLeft: \"The padding area to the left of a scroll container.\",\n  scrollPaddingRight: \"The padding area to the right of a scroll container.\",\n  scrollPaddingTop: \"The padding area above a scroll container.\",\n  scrollSnapAlign:\n    \"Defines where a snap point will be aligned within a scroll container.\",\n  scrollSnapStop:\n    \"Defines whether the scroll container should stop on a snap point.\",\n  scrollSnapType: \"Defines the type of snap points used in a scroll container.\",\n  scrollTimelineAxis: \"Defines the axis of the scroll timeline.\",\n  scrollTimelineName: \"Defines the name of the scroll timeline.\",\n  shapeImageThreshold:\n    \"Defines the alpha channel threshold for shape detection.\",\n  shapeMargin:\n    \"Defines the margin between a shape and its surrounding content.\",\n  shapeOutside: \"Defines a shape to wrap content around.\",\n  tabSize: \"Defines the width of a tab character.\",\n  tableLayout:\n    \"Defines the algorithm used to lay out table cells, rows, and columns.\",\n  textAlign: \"Defines the horizontal alignment of text.\",\n  textAlignLast:\n    \"Defines the horizontal alignment of the last line of text in a block.\",\n  textCombineUpright:\n    \"Defines the combination of text in vertical orientation.\",\n  textDecorationColor:\n    \"Controls color of underlines, overlines, and strikethroughs.\",\n  textDecorationLine: \"Specifies underlines, overlines, and strikethroughs.\",\n  textDecorationSkip:\n    \"Specifies which parts of text decoration are skipped over.\",\n  textDecorationSkipInk:\n    \"Specifies which parts of text decoration are skipped over, taking into account the ink area.\",\n  textDecorationStyle:\n    \"Specifies the style of underlines, overlines, and strikethroughs.\",\n  textDecorationThickness:\n    \"Specifies the thickness of underlines, overlines, and strikethroughs.\",\n  textEmphasisColor: \"Controls color of text emphasis marks.\",\n  textEmphasisPosition: \"Specifies position of text emphasis marks.\",\n  textEmphasisStyle: \"Specifies style of text emphasis marks.\",\n  textIndent:\n    \"Specifies how much horizontal space is before the first line of text.\",\n  textJustify: \"Specifies how the last line of a text is aligned.\",\n  textOrientation: \"Specifies the orientation of text within a line.\",\n  textOverflow: \"Specifies how overflowed content is displayed.\",\n  textRendering: \"Specifies quality of text rendering.\",\n  textShadow: \"Adds shadow to text.\",\n  textSizeAdjust: \"Controls the sizing of text relative to its container.\",\n  textTransform: \"Controls the capitalization of text.\",\n  textUnderlineOffset:\n    \"Controls the distance between the text and its underline.\",\n  textUnderlinePosition:\n    \"Controls the position of the underline in relation to the text.\",\n  top: \"Controls the vertical position of an element relative to its container.\",\n  touchAction: \"Controls how touch inputs are handled by the browser.\",\n  transform: \"Applies a 2D or 3D transformation to an element.\",\n  transformBox: \"Defines the layout box to which the transform applies.\",\n  transformOrigin: \"Controls the origin point of a transform.\",\n  transformStyle: \"Controls how nested elements are rendered in 3D space.\",\n  transitionDelay: \"Controls the delay before a transition effect starts.\",\n  transitionDuration: \"Controls the duration of a transition effect.\",\n  transitionProperty:\n    \"Controls which CSS properties are affected by a transition effect.\",\n  transitionTimingFunction: \"Controls the speed curve of a transition effect.\",\n  translate: \"Translates an element along the X and/or Y axis.\",\n  unicodeBidi: \"Controls the handling of bidirectional text.\",\n  userSelect: \"Controls whether an element can be selected by the user.\",\n  verticalAlign: \"Aligns inline-level elements vertically.\",\n  visibility: \"Determines whether an element is visible or hidden.\",\n  whiteSpace: \"Determines how white space inside an element is handled.\",\n  widows: \"Sets the minimum number of lines in a block container.\",\n  width: \"Sets the width of an element.\",\n  willChange: \"Lets you inform the browser about property changes in advance.\",\n  wordBreak:\n    \"Determines how words should break when reaching the end of a line.\",\n  wordSpacing: \"Sets the spacing between words.\",\n  wordWrap:\n    \"Determines whether to break words if the text exceeds the container.\",\n  writingMode: \"Sets the direction in which lines of text are written.\",\n  zIndex:\n    \"Sets the stack order of an element. Higher value means closer to the top.\",\n  WebkitFontSmoothing:\n    \"Adjusts how text is rendered to improve readability, especially on non-retina displays.\",\n  MozOsxFontSmoothing:\n    \"Controls the smoothing strategy used while displaying text on macOS and iOS devices.\",\n  animationRangeEnd:\n    \"Sets the offset within the animation cycle where the animation ends.\",\n  animationRangeStart:\n    \"Sets the offset within the animation cycle where the animation starts.\",\n  containerName: \"Specifies the name of the flexible box layout container.\",\n  containerType:\n    \"Defines the display container type for the layout, like block or inline.\",\n  fontPalette: \"Handles color font system options for certain OpenType fonts.\",\n  fontSynthesisPosition:\n    \"Determines the method of synthesizing font variations for the position.\",\n  fontSynthesisSmallCaps:\n    \"Specifies the mechanism for synthesizing small-caps font faces.\",\n  fontSynthesisStyle:\n    \"Sets the synthesis style for a font, like normal or italic.\",\n  fontSynthesisWeight:\n    \"Establishes the font weight synthesis method for non-system fonts.\",\n  fontVariantEmoji:\n    \"Adjusts the usage of emoji characters in fonts to reflect the font variant preference.\",\n  hyphenateLimitChars:\n    \"Specifies the minimum number of characters in a word before hyphenation is allowed.\",\n  overlay: \"Specifies the stacking order for positioned elements that overlap.\",\n  page: \"Sets various properties related to page breaks and print layout.\",\n  textWrap:\n    \"Specifies the behavior of text wrapping in relation to the element's content box dimensions.\",\n  textWrapMode:\n    \"Controls the behavior of how the text wraps when reaching the end of a line\",\n  textWrapStyle: \"Defines the style of wrapping that will be applied to text\",\n  timelineScope:\n    \"Indicates whether a timeline is equivalent or independent from others\",\n  transition: \"Specifies the CSS property to apply a transition effect to\",\n  transitionBehavior: \"Determines how to animate a transition\",\n  viewTimelineAxis: \"Sets the axis used for a timeline view\",\n  viewTimelineInset: \"Sets the inset (left) position in a timeline\",\n  viewTimelineName: \"Sets a custom name for a timeline view\",\n  viewTransitionName: \"Sets a custom name for a transition effect\",\n  whiteSpaceCollapse:\n    \"Specifies how white space inside an element is collapsed\",\n  \"-webkit-box-orient\":\n    \"Controls the direction of flex items in a flex container.\",\n  \"-webkit-line-clamp\":\n    \"Truncates text to a specific number of lines in a container.\",\n  \"-webkit-overflow-scrolling\":\n    \"Controls whether or not touch events trigger overflow scrolling.\",\n  \"-webkit-tap-highlight-color\":\n    \"Sets the highlight color of links and form controls if tapped.\",\n  fieldSizing:\n    \"Controls the algorithm used to calculate the width of form controls.\",\n  zoom: \"Specifies the zoom level of a document.\",\n  animation:\n    \"Animations make it possible to animate transitions from one CSS style configuration to another.\",\n  background:\n    \"The background property is a shorthand for setting multiple background properties at once.\",\n  border:\n    \"Borders outline the border area of an element and are used for decoration and visual separation.\",\n  \"border-block\":\n    \"The border-block property is a shorthand for setting the individual properties for the vertical block axis borders.\",\n  \"border-block-end\":\n    \"The border-block-end property is a shorthand for setting the individual properties for the end block axis border.\",\n  \"border-block-start\":\n    \"The border-block-start property is a shorthand for setting the individual properties for the start block axis border.\",\n  \"border-bottom\":\n    \"The border-bottom property is a shorthand for setting the individual properties for the bottom border.\",\n  \"border-color\":\n    \"The border-color property sets the color of the four borders around an element.\",\n  \"border-image\":\n    \"The border-image property is a shorthand for setting the border-image-source, border-image-slice, border-image-width, and border-image-repeat properties.\",\n  \"border-inline\":\n    \"The border-inline property is a shorthand for setting the individual properties for the inline axis borders.\",\n  \"border-inline-end\":\n    \"The border-inline-end property is a shorthand for setting the individual properties for the end inline axis border.\",\n  \"border-inline-start\":\n    \"The border-inline-start property is a shorthand for setting the individual properties for the start inline axis border.\",\n  \"border-left\":\n    \"The border-left property is a shorthand for setting the individual properties for the left border.\",\n  \"border-radius\":\n    \"Border-radius is used to create rounded corners on an element's box.\",\n  \"border-right\":\n    \"The border-right property is a shorthand for setting the individual properties for the right border.\",\n  \"border-style\":\n    \"The border-style property sets the style of the four borders.\",\n  \"border-top\": \"Controls the top border of an element.\",\n  \"border-width\": \"Sets the width of the borders of an element.\",\n  \"column-rule\": \"Manages the line drawn between columns.\",\n  columns: \"Specifies the number and width of columns.\",\n  \"contain-intrinsic-size\":\n    \"Defines the size of an element's intrinsic content.\",\n  container: \"Establishes a new block formatting context.\",\n  flex: \"Specifies the length of a flexible item.\",\n  \"flex-flow\": \"Sets how flex items are placed in the flex container.\",\n  font: \"Specifies font styles for text.\",\n  gap: \"Sets the gap between grid items.\",\n  grid: \"Defines a grid or a subgrid.\",\n  \"grid-area\": \"Specifies the size of a grid item.\",\n  \"grid-column\": \"Specifies a grid item's position within the grid.\",\n  \"grid-row\": \"Specifies a grid item's row within the grid.\",\n  \"grid-template\": \"Sets the values for grid layout properties.\",\n  inset: \"Specifies the position of an element.\",\n  \"inset-block\":\n    \"Controls the block axis position of an absolutely positioned element.\",\n  \"inset-inline\":\n    \"Controls the inline axis position of an absolutely positioned element.\",\n  \"list-style\": \"Sets all the properties for a list in one declaration.\",\n  margin: \"Sets the margin on all four sides of an element at once.\",\n  \"margin-block\": \"Sets the margin on the block axis of an element.\",\n  \"margin-inline\": \"Sets the margin on the inline axis of an element.\",\n  mask: \"Lets you use an image or a CSS gradient to define the shape of the mask.\",\n  \"mask-border\":\n    \"Lets you use an image or a CSS gradient to define the border image of the mask.\",\n  offset: \"Controls the positioning of a positioned element.\",\n  outline: \"Sets the style of the outline around an element.\",\n  padding: \"Sets the padding on all four sides of an element at once.\",\n  \"padding-block\": \"Sets the padding on the block axis of an element.\",\n  \"padding-inline\": \"Sets the padding on the inline axis of an element.\",\n  \"place-content\": \"Shorthand for align-content and justify-content.\",\n  \"place-items\": \"Shorthand for align-items and justify-items.\",\n  \"place-self\": \"Shorthand for align-self and justify-self.\",\n  \"scroll-margin\": \"Controls the top and bottom sides of the scrollbar\",\n  \"scroll-margin-block\": \"Controls the top and bottom sides of the scrollbar\",\n  \"scroll-margin-inline\": \"Controls the left and right sides of the scrollbar\",\n  \"scroll-padding\":\n    \"Sets the amount of space between the element's content and its padding\",\n  \"scroll-padding-block\":\n    \"Sets the amount of space between the element's content and its top and bottom padding\",\n  \"scroll-padding-inline\":\n    \"Sets the amount of space between the element's content and its left and right padding\",\n  \"text-decoration\":\n    \"Adds decoration to text, such as underlines or line-through\",\n  \"text-emphasis\": \"Sets special emphasis on text, like dots or circles\",\n} as Record<string, string | undefined>;\n\nexport const propertiesOverrides = {} as Record<string, string | undefined>;\n\nexport const propertySyntaxesGenerated = {\n  boxShadowOffsetX: \"Controls the offset distance of the shadow on the X-axis.\",\n  boxShadowOffsetY: \"Controls the offset distance of the shadow on the Y-axis.\",\n  boxShadowBlurRadius: \"Controls the blur radius of the shadow.\",\n  boxShadowSpreadRadius: \"Controls the spread radius of the shadow.\",\n  boxShadowColor: \"Controls the color of the shadow.\",\n  boxShadowPosition: \"Controls the positioning of the shadow.\",\n  textShadowOffsetX:\n    \"Controls the offset distance of the text shadow on the X-axis.\",\n  textShadowOffsetY:\n    \"Controls the offset distance of the text shadow on the Y-axis.\",\n  textShadowBlurRadius: \"Controls the blur radius of the text shadow.\",\n  textShadowColor: \"Controls the color of the text shadow.\",\n  dropShadowOffsetX:\n    \"Controls the offset distance of the drop shadow on the X-axis.\",\n  dropShadowOffsetY:\n    \"Controls the offset distance of the drop shadow on the Y-axis.\",\n  dropShadowBlurRadius: \"Controls the blur radius of the drop shadow.\",\n  dropShadowColor: \"Controls the color of the drop shadow.\",\n  translateX: \"Controls the translation of an element on the X-axis.\",\n  translateY: \"Controls the translation of an element on the Y-axis.\",\n  translateZ: \"Specifies the position of the z axis within the 3D workspace.\",\n  rotateX: \"Applies a 3D rotation to an element around the x-axis.\",\n  rotateY: \"Applies a 3D rotation to an element around the y-axis.\",\n  rotateZ: \"Applies a 3D rotation to an element around the z-axis.\",\n  scaleX: \"Applies a scale transformation to an element on the x-axis.\",\n  scaleY: \"Applies a scale transformation to an element on the y-axis.\",\n  scaleZ: \"Applies a scale transformation to an element on the z-axis.\",\n  skewX: \"Skews an element in the horizontal direction.\",\n  skewY: \"Skews an element in the vertical direction.\",\n  transformOriginX: \"Sets the origin of transformation for the x-axis.\",\n  transformOriginY: \"Sets the origin of transformation for the y-axis.\",\n  transformOriginZ: \"Sets the origin of transformation for the z-axis.\",\n  perspectiveOriginX: \"Sets the origin of perspective for the x-axis.\",\n  perspectiveOriginY: \"Sets the origin of perspective for the y-axis.\",\n} as Record<string, string | undefined>;\n\nexport const properties = {\n  ...propertiesGenerated,\n  ...propertiesOverrides,\n} as Record<string, string | undefined>;\n\nexport const declarationsGenerated = {\n  \"alignContent:normal\":\n    \"Aligns and distributes items along the cross/block axis with default spacing.\",\n  \"alignContent:first\":\n    \"Aligns and distributes items along the cross/block axis, prioritizing the first line.\",\n  \"alignContent:last\":\n    \"Aligns and distributes items along the cross/block axis, prioritizing the last line.\",\n  \"alignContent:baseline\": \"Aligns items with their baselines.\",\n  \"alignContent:space-between\":\n    \"Aligns and distributes items with equal space between them.\",\n  \"alignContent:space-around\":\n    \"Aligns and distributes items with equal space around them.\",\n  \"alignContent:space-evenly\":\n    \"Aligns and distributes items with equal space between them and around the edges.\",\n  \"alignContent:stretch\": \"Stretches items to fill the container.\",\n  \"alignContent:unsafe\": \"Enables alignment modes that are considered risky.\",\n  \"alignContent:safe\": \"Enables alignment modes that are safe to use.\",\n  \"alignContent:center\": \"Centers items along the cross/block axis.\",\n  \"alignContent:start\": \"Aligns items to the start of the container.\",\n  \"alignContent:end\": \"Aligns items to the end of the container.\",\n  \"alignContent:flex-start\":\n    \"Aligns items to the start of the container (same as start).\",\n  \"alignContent:flex-end\":\n    \"Aligns items to the end of the container (same as end).\",\n  \"alignContent:initial\": \"Sets the default alignment for align-content.\",\n  \"alignContent:inherit\":\n    \"The element inherits the align-content value of its parent.\",\n  \"alignContent:unset\":\n    \"The element aligns its content as if it applies the initial values of all properties.\",\n  \"alignItems:normal\":\n    \"The default, stretches the element vertically depending on the height of the container.\",\n  \"alignItems:stretch\":\n    \"Stretches the element vertically to fill the container.\",\n  \"alignItems:first\": \"Aligns items from the first baseline.\",\n  \"alignItems:last\": \"Aligns items from the last baseline.\",\n  \"alignItems:baseline\":\n    \"Aligns the element with the baseline of the parent container.\",\n  \"alignItems:unsafe\":\n    \"Aligns the element with the unsafe region of the container.\",\n  \"alignItems:safe\":\n    \"Aligns the element with the safe region of the container.\",\n  \"alignItems:center\":\n    \"Aligns the element vertically centered with respect to the container element.\",\n  \"alignItems:start\": \"Aligns the element at the start of the container.\",\n  \"alignItems:end\": \"Aligns the element at the end of the container.\",\n  \"alignItems:self-start\": \"Aligns the element at the start of itself.\",\n  \"alignItems:self-end\": \"Aligns the element at the end of itself.\",\n  \"alignItems:flex-start\":\n    \"Aligns items at the start of the container (same as start).\",\n  \"alignItems:flex-end\":\n    \"Aligns items at the end of the container (same as end).\",\n  \"alignItems:initial\": \"Items are aligned to their default position.\",\n  \"alignItems:inherit\":\n    \"Inherits the align-items property from the parent element.\",\n  \"alignItems:unset\": \"Resets the align-items property to its default value.\",\n  \"alignSelf:auto\": \"Uses the parent's align-items value.\",\n  \"alignSelf:normal\":\n    \"Behaves as stretch or start depending on the layout mode.\",\n  \"alignSelf:stretch\": \"Stretches to fill the container.\",\n  \"alignSelf:first\": \"Places the item at the start of the cross/block axis.\",\n  \"alignSelf:last\": \"Places the item at the end of the cross/block axis.\",\n  \"alignSelf:baseline\":\n    \"Aligns the item's baseline with the baseline of other items.\",\n  \"alignSelf:unsafe\": \"Enables a kind of backwards compatibility mode.\",\n  \"alignSelf:safe\": \"Applies all but potentially hazardous alignment values.\",\n  \"alignSelf:center\": \"Centers the item along the cross/block axis.\",\n  \"alignSelf:start\": \"Places the item at the start of the cross/block axis.\",\n  \"alignSelf:end\": \"Places the item at the end of the cross/block axis.\",\n  \"alignSelf:self-start\":\n    \"Places the item at the start of the cross/block axis.\",\n  \"alignSelf:self-end\": \"Places the item at the end of the cross/block axis.\",\n  \"alignSelf:flex-start\": \"Places the item at the start (same as start).\",\n  \"alignSelf:flex-end\": \"Places the item at the end (same as end).\",\n  \"alignSelf:initial\": \"Sets the align-self property to its default value.\",\n  \"alignSelf:inherit\":\n    \"Inherits the align-self property from its parent element.\",\n  \"alignSelf:unset\":\n    \"Sets the align-self property to its inherited value, or to its default value.\",\n  \"alignTracks:normal\":\n    \"Aligns grid tracks using the default alignment of their grid container.\",\n  \"alignTracks:first\":\n    \"Aligns the first grid track to the first baseline of the grid container.\",\n  \"alignTracks:last\":\n    \"Aligns the last grid track to the last baseline of the grid container.\",\n  \"alignTracks:baseline\": \"Aligns grid tracks to their baseline.\",\n  \"alignTracks:space-between\":\n    \"Distributes the grid tracks evenly, with the first track at the start and the last track at the end.\",\n  \"alignTracks:space-around\":\n    \"Distributes the grid tracks evenly, with equal space before and after the first and last tracks.\",\n  \"alignTracks:space-evenly\":\n    \"Distributes the grid tracks evenly, with equal space between them.\",\n  \"alignTracks:stretch\":\n    \"Stretches the grid tracks to fill the grid container.\",\n  \"alignTracks:unsafe\":\n    \"Enables overflow alignment of grid tracks when they exceed the grid container's size.\",\n  \"alignTracks:safe\":\n    \"Disables overflow alignment of grid tracks when they exceed the grid container's size.\",\n  \"alignTracks:center\": \"Centers the grid tracks in the grid container.\",\n  \"alignTracks:start\": \"Aligns the grid tracks to the start of the grid area.\",\n  \"alignTracks:end\": \"Aligns the grid tracks to the end of the grid area.\",\n  \"alignTracks:flex-start\":\n    \"Aligns the grid tracks to the start of the container (same as start).\",\n  \"alignTracks:flex-end\":\n    \"Aligns the grid tracks to the end of the container (same as end).\",\n  \"alignTracks:initial\": \"Sets the alignment to its default value.\",\n  \"alignTracks:inherit\": \"Inherits the alignment from its parent element.\",\n  \"alignTracks:unset\": \"Resets the alignment to its natural value.\",\n  \"animationComposition:initial\":\n    \"Sets the animation's composition to its default value.\",\n  \"animationComposition:inherit\":\n    \"Inherits the animation's composition from its parent element.\",\n  \"animationComposition:unset\":\n    \"Resets the animation's composition to its natural value.\",\n  \"animationDelay:initial\": \"Sets the animation delay to its default value.\",\n  \"animationDelay:inherit\":\n    \"Inherits the animation delay from its parent element.\",\n  \"animationDelay:unset\": \"Resets the animation delay to its natural value.\",\n  \"animationDirection:normal\": \"Plays the animation in its default direction.\",\n  \"animationDirection:reverse\": \"Plays the animation in reverse direction.\",\n  \"animationDirection:alternate\":\n    \"Plays the animation in alternate forward and reverse directions.\",\n  \"animationDirection:alternate-reverse\":\n    \"The animation plays backwards on every odd iteration starting from the last keyframe\",\n  \"animationDirection:initial\":\n    \"Sets an animation to its default values defined by the browser's stylesheet\",\n  \"animationDirection:inherit\":\n    \"Inherits the animation-direction property from the parent element\",\n  \"animationDirection:unset\":\n    \"Inherits the animation-direction property from the parent element, or sets to initial if there is no parent\",\n  \"animationDuration:initial\":\n    \"Sets the duration of an animation to its default value (0)\",\n  \"animationDuration:inherit\":\n    \"Inherits the animation-duration property from the parent element\",\n  \"animationDuration:unset\":\n    \"Inherits the animation-duration property from the parent element, or sets to initial if there is no parent\",\n  \"animationFillMode:none\":\n    \"The animation will not apply any styles to the target before or after its execution\",\n  \"animationFillMode:forwards\":\n    \"The target will retain the styles applied by the last keyframe\",\n  \"animationFillMode:backwards\":\n    \"The target will retain the styles applied by the first keyframe\",\n  \"animationFillMode:both\":\n    \"The target will retain the styles applied by both the first and last keyframe\",\n  \"animationFillMode:initial\":\n    \"Sets the animation-fill-mode property to its default value (none)\",\n  \"animationFillMode:inherit\":\n    \"Inherits the animation-fill-mode property from the parent element\",\n  \"animationFillMode:unset\":\n    \"Inherits the animation-fill-mode property from the parent element, or sets to initial if there is no parent\",\n  \"animationIterationCount:infinite\": \"The animation will repeat indefinitely\",\n  \"animationIterationCount:initial\":\n    \"Sets the number of times an animation should run to its default value (1)\",\n  \"animationIterationCount:inherit\":\n    \"Inherits the iteration count from the parent element.\",\n  \"animationIterationCount:unset\":\n    \"Resets the iteration count to its inherited value.\",\n  \"animationName:none\": \"No animation is applied.\",\n  \"animationName:initial\": \"Specifies the default value of the property.\",\n  \"animationName:inherit\":\n    \"Inherits the animation name from the parent element.\",\n  \"animationName:unset\": \"Resets the animation name to its inherited value.\",\n  \"animationPlayState:running\": \"The animation is playing.\",\n  \"animationPlayState:paused\": \"The animation is paused.\",\n  \"animationPlayState:initial\": \"Specifies the default value of the property.\",\n  \"animationPlayState:inherit\":\n    \"Inherits the play state from the parent element.\",\n  \"animationPlayState:unset\": \"Resets the play state to its inherited value.\",\n  \"animationTimingFunction:linear\": \"The animation has a constant speed.\",\n  \"animationTimingFunction:ease\":\n    \"The animation starts slow, gets faster in the middle, and slows down at the end.\",\n  \"animationTimingFunction:ease-in\":\n    \"The animation starts slow and gets faster.\",\n  \"animationTimingFunction:ease-out\":\n    \"The animation starts fast and slows down.\",\n  \"animationTimingFunction:step-start\":\n    \"Specifies an abrupt change in the animation's progress at the start of each interval.\",\n  \"animationTimingFunction:step-end\":\n    \"Specifies an abrupt change in the animation's progress at the end of each interval.\",\n  \"animationTimingFunction:initial\":\n    \"Sets the animation's timing function to its default value.\",\n  \"animationTimingFunction:inherit\":\n    \"Inherits the animation's timing function from its parent element.\",\n  \"animationTimingFunction:unset\":\n    \"Resets the animation's timing function to its natural value.\",\n  \"animationTimeline:auto\":\n    \"Sets the animation to play forwards and backwards once.\",\n  \"animationTimeline:none\": \"Disables the animation.\",\n  \"animationTimeline:initial\":\n    \"Sets the animation's timeline to its default value.\",\n  \"animationTimeline:inherit\":\n    \"Inherits the animation's timeline from its parent element.\",\n  \"animationTimeline:unset\":\n    \"Resets the animation's timeline to its natural value.\",\n  \"appearance:none\": \"Removes the default appearance of form controls.\",\n  \"appearance:auto\":\n    \"Allows the browser to display the default appearance of form controls.\",\n  \"appearance:textfield\":\n    \"Displays an input field with no border or other decoration.\",\n  \"appearance:menulist-button\":\n    \"Displays a drop-down list box with an arrow button.\",\n  \"appearance:searchfield\":\n    \"Displays a search field with a magnifying glass icon.\",\n  \"appearance:textarea\": \"Displays a multi-line text input box.\",\n  \"appearance:push-button\":\n    \"Renders an element as a clickable button with a border.\",\n  \"appearance:slider-horizontal\": \"Renders a slider in a horizontal direction.\",\n  \"appearance:checkbox\": \"Renders an element as a checkbox.\",\n  \"appearance:radio\": \"Renders an element as a radio button.\",\n  \"appearance:square-button\": \"Renders an element as a square button.\",\n  \"appearance:menulist\": \"Renders an element as a dropdown menu.\",\n  \"appearance:listbox\": \"Renders an element as a list box.\",\n  \"appearance:meter\":\n    \"Renders an element as a progress indicator (like a speedometer).\",\n  \"appearance:progress-bar\": \"Renders an element as a horizontal progress bar.\",\n  \"appearance:button\": \"Renders an element as a button.\",\n  \"appearance:initial\": \"Sets the appearance property to its default value.\",\n  \"appearance:inherit\":\n    \"Inherits the appearance property from its parent element.\",\n  \"appearance:unset\":\n    \"Sets the appearance property to its inherited value if it exists, otherwise its initial value.\",\n  \"aspectRatio:auto\":\n    \"Sets the height of an element based on its width and the aspect ratio.\",\n  \"aspectRatio:initial\": \"Sets the aspect ratio property to its default value.\",\n  \"aspectRatio:inherit\":\n    \"Inherits the aspect ratio property from its parent element.\",\n  \"aspectRatio:unset\": \"Uses the intrinsic aspect ratio of the element.\",\n  \"backdropFilter:none\": \"No backdrop filtering effect is applied.\",\n  \"backdropFilter:initial\":\n    \"Sets backdrop-filter property to its default value.\",\n  \"backdropFilter:inherit\":\n    \"Inherits the backdrop-filter property from its parent element.\",\n  \"backdropFilter:unset\":\n    \"Sets backdrop-filter property to its inherited value.\",\n  \"backfaceVisibility:visible\": \"The back face is visible.\",\n  \"backfaceVisibility:hidden\": \"The back face is hidden.\",\n  \"backfaceVisibility:initial\":\n    \"Sets backface-visibility property to its default value.\",\n  \"backfaceVisibility:inherit\":\n    \"Inherits the backface-visibility property from its parent element.\",\n  \"backfaceVisibility:unset\":\n    \"Sets backface-visibility property to its inherited value.\",\n  \"backgroundAttachment:scroll\":\n    \"The background image will scroll with the page.\",\n  \"backgroundAttachment:fixed\":\n    \"The background image will not scroll with the page.\",\n  \"backgroundAttachment:local\":\n    \"The background image will scroll within the element.\",\n  \"backgroundAttachment:initial\":\n    \"Sets background-attachment property to its default value.\",\n  \"backgroundAttachment:inherit\":\n    \"Inherits the background-attachment property from its parent element.\",\n  \"backgroundAttachment:unset\":\n    \"Sets background-attachment property to its inherited value.\",\n  \"backgroundBlendMode:normal\": \"The background image(s) are displayed as-is.\",\n  \"backgroundBlendMode:multiply\":\n    \"The pixels of the top layer are multiplied with the bottom layer.\",\n  \"backgroundBlendMode:screen\":\n    \"The pixels of the top layer are screened onto the bottom layer.\",\n  \"backgroundBlendMode:overlay\": \"Combines multiply and screen blend modes.\",\n  \"backgroundBlendMode:darken\":\n    \"Displays the darker of the layers at each pixel.\",\n  \"backgroundBlendMode:lighten\":\n    \"Displays the lighter of the layers at each pixel.\",\n  \"backgroundBlendMode:color-dodge\":\n    \"Brightens the bottom layer to reflect the top layer.\",\n  \"backgroundBlendMode:color-burn\":\n    \"Darkens the bottom layer to reflect the top layer.\",\n  \"backgroundBlendMode:hard-light\":\n    \"Multiplies or screens the layers, depending on the top layer.\",\n  \"backgroundBlendMode:soft-light\":\n    \"Darkens or lightens the layers, depending on the top layer.\",\n  \"backgroundBlendMode:difference\":\n    \"Subtracts the darker from the lighter layer.\",\n  \"backgroundBlendMode:exclusion\":\n    \"Produces an effect similar to difference, but with low-contrast.\",\n  \"backgroundBlendMode:hue\":\n    \"Uses the hue of the top layer with the saturation and luminosity of the bottom layer.\",\n  \"backgroundBlendMode:saturation\":\n    \"Uses the saturation of the top layer with the hue and luminosity of the bottom layer.\",\n  \"backgroundBlendMode:color\":\n    \"Uses the hue and saturation of the top layer with the luminosity of the bottom layer.\",\n  \"backgroundBlendMode:luminosity\":\n    \"Uses the luminosity of the top layer with the hue and saturation of the bottom layer.\",\n  \"backgroundBlendMode:initial\":\n    \"The blending mode is set to its initial value.\",\n  \"backgroundBlendMode:inherit\":\n    \"The blending mode is inherited from its parent element.\",\n  \"backgroundBlendMode:unset\": \"The blending mode is set to its default value.\",\n  \"backgroundClip:border-box\":\n    \"The background image is clipped to the border box.\",\n  \"backgroundClip:padding-box\":\n    \"The background image is clipped to the padding box.\",\n  \"backgroundClip:content-box\":\n    \"The background image is clipped to the content box.\",\n  \"backgroundClip:text\": \"The background image is rendered within the text.\",\n  \"backgroundClip:initial\": \"The clipping area is set to the border box.\",\n  \"backgroundClip:inherit\":\n    \"The clipping area is inherited from its parent element.\",\n  \"backgroundClip:unset\": \"The clipping area is set to its default value.\",\n  \"backgroundImage:none\": \"No background image is displayed.\",\n  \"backgroundImage:initial\":\n    \"The background image is set to its initial value.\",\n  \"backgroundImage:inherit\":\n    \"The background image is inherited from its parent element.\",\n  \"backgroundImage:unset\": \"The background image is set to its default value.\",\n  \"backgroundOrigin:border-box\":\n    \"The background image originates from the border box.\",\n  \"backgroundOrigin:padding-box\":\n    \"The background image originates from the padding box.\",\n  \"backgroundOrigin:content-box\":\n    \"Determines the background positioning area as the content-box.\",\n  \"backgroundOrigin:initial\":\n    \"Sets the background positioning area to its default value.\",\n  \"backgroundOrigin:inherit\":\n    \"Inherits the background positioning area from its parent element.\",\n  \"backgroundOrigin:unset\":\n    \"Resets the background positioning area to its inherited value.\",\n  \"backgroundPosition:left\": \"Sets the background image position to the left.\",\n  \"backgroundPosition:center\":\n    \"Sets the background image position to the center.\",\n  \"backgroundPosition:right\":\n    \"Sets the background image position to the right.\",\n  \"backgroundPosition:top\": \"Sets the background image position to the top.\",\n  \"backgroundPosition:bottom\":\n    \"Sets the background image position to the bottom.\",\n  \"backgroundPosition:initial\":\n    \"Sets the background image position to its default values.\",\n  \"backgroundPosition:inherit\":\n    \"Inherits the background image position from its parent element.\",\n  \"backgroundPosition:unset\":\n    \"Resets the background image position to its inherited value.\",\n  \"backgroundPositionX:center\":\n    \"Sets the horizontal position of the background image to the center.\",\n  \"backgroundPositionX:left\":\n    \"Sets the horizontal position of the background image to the left.\",\n  \"backgroundPositionX:right\":\n    \"Sets the horizontal position of the background image to the right.\",\n  \"backgroundPositionX:x-start\":\n    \"Sets the horizontal position of the background image to the left in LTR and right in RTL.\",\n  \"backgroundPositionX:x-end\":\n    \"Sets the horizontal position of the background image's right edge to the right of the container.\",\n  \"backgroundPositionX:initial\":\n    \"Sets the default value for the horizontal position of the background image.\",\n  \"backgroundPositionX:inherit\":\n    \"Inherit the horizontal position of the background image from the parent element.\",\n  \"backgroundPositionX:unset\":\n    \"Resets the horizontal position of the background image to its default value.\",\n  \"backgroundPositionY:center\": \"Vertically centers the background image.\",\n  \"backgroundPositionY:top\":\n    \"Places the background image at the top of the container.\",\n  \"backgroundPositionY:bottom\":\n    \"Places the background image at the bottom of the container.\",\n  \"backgroundPositionY:y-start\":\n    \"Sets the vertical position of the background image's top edge to the top of the container.\",\n  \"backgroundPositionY:y-end\":\n    \"Sets the vertical position of the background image's bottom edge to the bottom of the container.\",\n  \"backgroundPositionY:initial\":\n    \"Sets the default value for the vertical position of the background image.\",\n  \"backgroundPositionY:inherit\":\n    \"Inherit the vertical position of the background image from the parent element.\",\n  \"backgroundPositionY:unset\":\n    \"Resets the vertical position of the background image to its default value.\",\n  \"backgroundRepeat:repeat-x\": \"Repeats the background image horizontally.\",\n  \"backgroundRepeat:repeat-y\": \"Repeats the background image vertically.\",\n  \"backgroundRepeat:repeat\":\n    \"Repeats the background image horizontally and vertically.\",\n  \"backgroundRepeat:space\":\n    \"Repeats the background image horizontally and vertically, spacing out the images so that the last one touches the right and bottom edge of the container.\",\n  \"backgroundRepeat:round\": \"Repeats the background image in a circle.\",\n  \"backgroundRepeat:no-repeat\": \"Does not repeat the background image.\",\n  \"backgroundRepeat:initial\":\n    \"Sets the default value for the background repeat property.\",\n  \"backgroundRepeat:inherit\":\n    \"Inherits the background repeat property from its parent element.\",\n  \"backgroundRepeat:unset\":\n    \"Resets the background repeat property to its default value.\",\n  \"backgroundSize:auto\": \"Sets the background image size to its original size.\",\n  \"backgroundSize:cover\":\n    \"Resizes the background image to cover the entire container.\",\n  \"backgroundSize:contain\":\n    \"Resizes the background image to fit inside the container.\",\n  \"backgroundSize:initial\":\n    \"Sets the default value for the background size property.\",\n  \"backgroundSize:inherit\":\n    \"Inherits the background size property from its parent element.\",\n  \"backgroundSize:unset\":\n    \"Resets the background size property to its default value.\",\n  \"blockOverflow:clip\": \"Clips the overflow content.\",\n  \"blockOverflow:ellipsis\": \"Adds an ellipsis when content overflows.\",\n  \"blockOverflow:initial\": \"Sets the default value for block overflow.\",\n  \"blockOverflow:inherit\": \"Inherits block overflow from its parent.\",\n  \"blockOverflow:unset\": \"Resets block overflow to its default value.\",\n  \"blockSize:auto\": \"The block element fills all available space.\",\n  \"blockSize:min-content\":\n    \"The height is the minimum required to contain contents.\",\n  \"blockSize:max-content\":\n    \"The height is the maximum required to contain contents.\",\n  \"blockSize:fit-content\":\n    \"The height is the smallest required to contain contents.\",\n  \"blockSize:initial\": \"Sets the height to the default value.\",\n  \"blockSize:inherit\": \"Inherits the height from the parent element.\",\n  \"blockSize:unset\": \"Sets the height to its natural value.\",\n  \"borderBlockStyle:none\": \"Disables all borders.\",\n  \"borderBlockStyle:hidden\": \"Border is not visible, but still affects layout.\",\n  \"borderBlockStyle:dotted\": \"Dot-style border.\",\n  \"borderBlockStyle:dashed\": \"Dashed border.\",\n  \"borderBlockStyle:solid\": \"Solid border.\",\n  \"borderBlockStyle:double\": \"Double border.\",\n  \"borderBlockStyle:groove\": \"3D grooved border.\",\n  \"borderBlockStyle:ridge\": \"3D ridged border.\",\n  \"borderBlockStyle:inset\": \"3D inset border.\",\n  \"borderBlockStyle:outset\": \"Adds an outset border to block element.\",\n  \"borderBlockStyle:initial\": \"Resets border to initial value.\",\n  \"borderBlockStyle:inherit\": \"Inherits border style from parent element.\",\n  \"borderBlockStyle:unset\": \"Resets border to inherited or initial value.\",\n  \"borderBlockWidth:thin\": \"Sets a thin border for block element.\",\n  \"borderBlockWidth:medium\": \"Sets a medium border for block element.\",\n  \"borderBlockWidth:thick\": \"Sets a thick border for block element.\",\n  \"borderBlockWidth:initial\": \"Resets border width to initial value.\",\n  \"borderBlockWidth:inherit\": \"Inherits border width from parent element.\",\n  \"borderBlockWidth:unset\":\n    \"Resets border width to inherited or initial value.\",\n  \"borderBlockEndStyle:none\": \"Removes border from the end of a block element.\",\n  \"borderBlockEndStyle:hidden\": \"Hides border at the end of a block element.\",\n  \"borderBlockEndStyle:dotted\":\n    \"Adds a dotted border at the end of a block element.\",\n  \"borderBlockEndStyle:dashed\":\n    \"Adds a dashed border at the end of a block element.\",\n  \"borderBlockEndStyle:solid\":\n    \"Adds a solid border at the end of a block element.\",\n  \"borderBlockEndStyle:double\":\n    \"Adds a double border at the end of a block element.\",\n  \"borderBlockEndStyle:groove\":\n    \"Creates a groove border on the block-end side.\",\n  \"borderBlockEndStyle:ridge\": \"Creates a ridge border on the block-end side.\",\n  \"borderBlockEndStyle:inset\": \"Creates an inset border on the block-end side.\",\n  \"borderBlockEndStyle:outset\":\n    \"Creates an outset border on the block-end side.\",\n  \"borderBlockEndStyle:initial\":\n    \"Sets the border-block-end-style to its default value.\",\n  \"borderBlockEndStyle:inherit\":\n    \"Inherits the border-block-end-style property from the parent.\",\n  \"borderBlockEndStyle:unset\":\n    \"Resets the border-block-end-style property to its inherited value.\",\n  \"borderBlockEndWidth:thin\": \"Creates a thin border on the block-end side.\",\n  \"borderBlockEndWidth:medium\":\n    \"Creates a medium border on the block-end side.\",\n  \"borderBlockEndWidth:thick\": \"Creates a thick border on the block-end side.\",\n  \"borderBlockEndWidth:initial\":\n    \"Sets the border-block-end-width to its default value.\",\n  \"borderBlockEndWidth:inherit\":\n    \"Inherits the border-block-end-width property from the parent.\",\n  \"borderBlockEndWidth:unset\":\n    \"Resets the border-block-end-width property to its inherited value.\",\n  \"borderBlockStartStyle:none\": \"Removes the border on the block-start side.\",\n  \"borderBlockStartStyle:hidden\": \"Hides the border on the block-start side.\",\n  \"borderBlockStartStyle:dotted\":\n    \"Creates a dotted border on the block-start side.\",\n  \"borderBlockStartStyle:dashed\":\n    \"Sets the style of the top border of a block to dashed.\",\n  \"borderBlockStartStyle:solid\":\n    \"Sets the style of the top border of a block to solid.\",\n  \"borderBlockStartStyle:double\":\n    \"Sets the style of the top border of a block to double.\",\n  \"borderBlockStartStyle:groove\":\n    \"Sets the style of the top border of a block to groove.\",\n  \"borderBlockStartStyle:ridge\":\n    \"Sets the style of the top border of a block to ridge.\",\n  \"borderBlockStartStyle:inset\":\n    \"Sets the style of the top border of a block to inset.\",\n  \"borderBlockStartStyle:outset\":\n    \"Sets the style of the top border of a block to outset.\",\n  \"borderBlockStartStyle:initial\":\n    \"Sets the style of the top border of a block to its default value.\",\n  \"borderBlockStartStyle:inherit\":\n    \"Inherits the style of the top border of a block from its parent element.\",\n  \"borderBlockStartStyle:unset\":\n    \"Resets the style of the top border of a block to its inherited value or default if none.\",\n  \"borderBlockStartWidth:thin\":\n    \"Sets the thickness of the top border of a block to thin.\",\n  \"borderBlockStartWidth:medium\":\n    \"Sets the thickness of the top border of a block to medium.\",\n  \"borderBlockStartWidth:thick\":\n    \"Sets the thickness of the top border of a block to thick.\",\n  \"borderBlockStartWidth:initial\":\n    \"Sets the thickness of the top border of a block to its default value.\",\n  \"borderBlockStartWidth:inherit\":\n    \"Inherits the thickness of the top border of a block from its parent element.\",\n  \"borderBlockStartWidth:unset\":\n    \"Resets the thickness of the top border of a block to its inherited value or default if none.\",\n  \"borderBottomLeftRadius:initial\":\n    \"Sets the initial value for the bottom-left corner radius of an element's border.\",\n  \"borderBottomLeftRadius:inherit\":\n    \"Inherits the bottom-left corner radius value from the parent element.\",\n  \"borderBottomLeftRadius:unset\":\n    \"Removes the bottom-left corner radius value, falling back to the defined border radius.\",\n  \"borderBottomRightRadius:initial\":\n    \"Sets the initial value for the bottom-right corner radius of an element's border.\",\n  \"borderBottomRightRadius:inherit\":\n    \"Inherits the bottom-right corner radius value from the parent element.\",\n  \"borderBottomRightRadius:unset\":\n    \"Removes the bottom-right corner radius value, falling back to the defined border radius.\",\n  \"borderBottomStyle:none\":\n    \"Sets the style for the bottom border of an element to have no visible border.\",\n  \"borderBottomStyle:hidden\":\n    \"Sets the style for the bottom border of an element to be hidden, but still take up space.\",\n  \"borderBottomStyle:dotted\":\n    \"Sets the style for the bottom border of an element to be a series of dots.\",\n  \"borderBottomStyle:dashed\":\n    \"Sets the style for the bottom border of an element to be a series of dashed lines.\",\n  \"borderBottomStyle:solid\":\n    \"Sets the style for the bottom border of an element to be a single solid line.\",\n  \"borderBottomStyle:double\":\n    \"Sets the style for the bottom border of an element to be a double line (two parallel lines).\",\n  \"borderBottomStyle:groove\":\n    \"Sets the style for the bottom border of an element to be a three-dimensional groove.\",\n  \"borderBottomStyle:ridge\":\n    \"Sets the style for the bottom border of an element to be a three-dimensional ridge.\",\n  \"borderBottomStyle:inset\":\n    'Sets the style for the bottom border of an element to be an \"inset\" 3D groove.',\n  \"borderBottomStyle:outset\":\n    'Sets the style for the bottom border of an element to be an \"outset\" 3D ridge.',\n  \"borderBottomStyle:initial\": \"Controls the style of the bottom border.\",\n  \"borderBottomStyle:inherit\":\n    \"Sets the bottom border style to its parent's value.\",\n  \"borderBottomStyle:unset\":\n    \"Sets the bottom border style to the default value.\",\n  \"borderBottomWidth:thin\":\n    'Sets the thickness of the bottom border to \"thin\".',\n  \"borderBottomWidth:medium\":\n    'Sets the thickness of the bottom border to \"medium\".',\n  \"borderBottomWidth:thick\":\n    'Sets the thickness of the bottom border to \"thick\".',\n  \"borderBottomWidth:initial\":\n    \"Sets the bottom border width to its default value.\",\n  \"borderBottomWidth:inherit\":\n    \"Sets the bottom border width to its parent's value.\",\n  \"borderBottomWidth:unset\":\n    \"Sets the bottom border width to the default value.\",\n  \"borderCollapse:collapse\": \"Merges the borders of adjacent cells.\",\n  \"borderCollapse:separate\": \"Separates the borders of adjacent cells.\",\n  \"borderCollapse:initial\":\n    \"Sets the border-collpase value to its default value.\",\n  \"borderCollapse:inherit\":\n    \"Sets the border-collapse value to its parent's value.\",\n  \"borderCollapse:unset\":\n    \"Sets the border-collapse value to the default value.\",\n  \"borderEndEndRadius:initial\":\n    \"Sets the end-end corner radius of the border to the default value.\",\n  \"borderEndEndRadius:inherit\":\n    \"Sets the end-end corner radius of the border to its parent's value.\",\n  \"borderEndEndRadius:unset\":\n    \"The border radius of the corner where the end and the bottom edges meet is reset to its initial value.\",\n  \"borderEndStartRadius:initial\":\n    \"The border radius of the corner where the end and the top edges meet is set to its initial value.\",\n  \"borderEndStartRadius:inherit\":\n    \"The border radius of the corner where the end and the top edges meet is inherited from its parent element.\",\n  \"borderEndStartRadius:unset\":\n    \"The border radius of the corner where the end and the top edges meet is reset to its initial value.\",\n  \"borderImageOutset:initial\":\n    \"The border image expands beyond the border box by the width of the border, which is set to its initial value.\",\n  \"borderImageOutset:inherit\":\n    \"The border image expands beyond the border box by the width of the border, which is inherited from its parent element.\",\n  \"borderImageOutset:unset\":\n    \"The border image expands beyond the border box by the width of the border, which is reset to its initial value.\",\n  \"borderImageRepeat:stretch\":\n    \"The border image is stretched to fill the area of the border box.\",\n  \"borderImageRepeat:repeat\":\n    \"The border image is repeated both horizontally and vertically to fill the area of the border box.\",\n  \"borderImageRepeat:round\":\n    \"The border image is rescaled so that it repeats across each side exactly n times.\",\n  \"borderImageRepeat:space\":\n    \"The border image is repeated as much as possible without clipping and it is spaced out along the border.\",\n  \"borderImageRepeat:initial\":\n    \"The border image is set to its initial value, which is 'stretch'.\",\n  \"borderImageRepeat:inherit\":\n    \"The border image is inherited from its parent element.\",\n  \"borderImageRepeat:unset\":\n    \"The border image is reset to its initial value, which is 'stretch'.\",\n  \"borderImageSlice:fill\":\n    \"The entire border image will be displayed, where 'fill' is equivalent to '100%'.\",\n  \"borderImageSlice:initial\":\n    \"The border image is sliced using the default values, which is 100%.\",\n  \"borderImageSlice:inherit\":\n    \"The border image slices use the computed values from the parent element.\",\n  \"borderImageSlice:unset\": \"The border image slices use the initial value.\",\n  \"borderImageSource:none\": \"No image is used for the border.\",\n  \"borderImageSource:initial\":\n    \"The border image source is set to its initial value.\",\n  \"borderImageSource:inherit\":\n    \"The border image source uses the computed value from the parent element.\",\n  \"borderImageSource:unset\": \"The border image source uses the unset value.\",\n  \"borderImageWidth:auto\":\n    \"The border image width is equal to the width of the border area.\",\n  \"borderImageWidth:initial\":\n    \"The border image width is set to its initial value.\",\n  \"borderImageWidth:inherit\":\n    \"The border image width uses the computed value from the parent element.\",\n  \"borderImageWidth:unset\": \"The border image width uses the unset value.\",\n  \"borderInlineStyle:none\": \"No border is displayed.\",\n  \"borderInlineStyle:hidden\": \"A hidden border is displayed.\",\n  \"borderInlineStyle:dotted\": \"A dotted border is displayed.\",\n  \"borderInlineStyle:dashed\": \"A dashed border is displayed.\",\n  \"borderInlineStyle:solid\": \"A solid border is displayed.\",\n  \"borderInlineStyle:double\": \"A double border is displayed.\",\n  \"borderInlineStyle:groove\":\n    \"Sets the style of the inline border to a 3D groove.\",\n  \"borderInlineStyle:ridge\":\n    \"Sets the style of the inline border to a 3D ridge.\",\n  \"borderInlineStyle:inset\":\n    \"Sets the style of the inline border to a 3D inset.\",\n  \"borderInlineStyle:outset\":\n    \"Sets the style of the inline border to a 3D outset.\",\n  \"borderInlineStyle:initial\":\n    \"Sets the style of the inline border to its default value.\",\n  \"borderInlineStyle:inherit\":\n    \"Inherits the style of the inline border from the parent element.\",\n  \"borderInlineStyle:unset\":\n    \"Resets the style of the inline border to its inherited value.\",\n  \"borderInlineWidth:thin\":\n    \"Sets the width of the inline border to a thin line.\",\n  \"borderInlineWidth:medium\":\n    \"Sets the width of the inline border to a medium line.\",\n  \"borderInlineWidth:thick\":\n    \"Sets the width of the inline border to a thick line.\",\n  \"borderInlineWidth:initial\":\n    \"Sets the width of the inline border to its default value.\",\n  \"borderInlineWidth:inherit\":\n    \"Inherits the width of the inline border from the parent element.\",\n  \"borderInlineWidth:unset\":\n    \"Resets the width of the inline border to its inherited value.\",\n  \"borderInlineEndStyle:none\":\n    \"Sets the style of the ending side of the inline border to none.\",\n  \"borderInlineEndStyle:hidden\":\n    \"Sets the style of the ending side of the inline border to hidden.\",\n  \"borderInlineEndStyle:dotted\":\n    \"Sets the style of the ending side of the inline border to a dotted line.\",\n  \"borderInlineEndStyle:dashed\":\n    \"Creates a dashed border on the end inline side.\",\n  \"borderInlineEndStyle:solid\":\n    \"Creates a solid border on the end inline side.\",\n  \"borderInlineEndStyle:double\":\n    \"Creates a double border on the end inline side.\",\n  \"borderInlineEndStyle:groove\":\n    \"Creates a 3D grooved border on the end inline side.\",\n  \"borderInlineEndStyle:ridge\":\n    \"Creates a 3D ridged border on the end inline side.\",\n  \"borderInlineEndStyle:inset\":\n    \"Creates a 3D inset border on the end inline side.\",\n  \"borderInlineEndStyle:outset\":\n    \"Creates a 3D outset border on the end inline side.\",\n  \"borderInlineEndStyle:initial\":\n    \"Sets the border style to its default value on the end inline side.\",\n  \"borderInlineEndStyle:inherit\":\n    \"Inherits the border style from the parent element on the end inline side.\",\n  \"borderInlineEndStyle:unset\":\n    \"Resets the border style to its inherited value on the end inline side.\",\n  \"borderInlineEndWidth:thin\":\n    \"Sets the border width to a thin size on the end inline side.\",\n  \"borderInlineEndWidth:medium\":\n    \"Sets the border width to a medium size on the end inline side.\",\n  \"borderInlineEndWidth:thick\":\n    \"Sets the border width to a thick size on the end inline side.\",\n  \"borderInlineEndWidth:initial\":\n    \"Sets the border width to its default value on the end inline side.\",\n  \"borderInlineEndWidth:inherit\":\n    \"Inherits the border width from the parent element on the end inline side.\",\n  \"borderInlineEndWidth:unset\":\n    \"Resets the border width to its inherited value on the end inline side.\",\n  \"borderInlineStartStyle:none\":\n    \"No border on the starting side of an inline element\",\n  \"borderInlineStartStyle:hidden\":\n    \"Border is the same as `none`, but still takes up space\",\n  \"borderInlineStartStyle:dotted\":\n    \"Dotted border on the starting side of an inline element\",\n  \"borderInlineStartStyle:dashed\":\n    \"Dashed border on the starting side of an inline element\",\n  \"borderInlineStartStyle:solid\":\n    \"Solid border on the starting side of an inline element\",\n  \"borderInlineStartStyle:double\":\n    \"Double border on the starting side of an inline element\",\n  \"borderInlineStartStyle:groove\":\n    \"3D grooved border on the starting side of an inline element\",\n  \"borderInlineStartStyle:ridge\":\n    \"3D ridged border on the starting side of an inline element\",\n  \"borderInlineStartStyle:inset\":\n    \"3D inset border on the starting side of an inline element\",\n  \"borderInlineStartStyle:outset\":\n    \"3D outset border on the starting side of an inline element\",\n  \"borderInlineStartStyle:initial\": \"Sets the property to its default value\",\n  \"borderInlineStartStyle:inherit\":\n    \"Inherits the property from its parent element\",\n  \"borderInlineStartStyle:unset\": \"Resets the property to its inherited value\",\n  \"borderInlineStartWidth:thin\":\n    \"Thin width border on the starting side of an inline element\",\n  \"borderInlineStartWidth:medium\":\n    \"Medium width border on the starting side of an inline element\",\n  \"borderInlineStartWidth:thick\":\n    \"Thick width border on the starting side of an inline element\",\n  \"borderInlineStartWidth:initial\":\n    \"The inline start border width is set to its initial value.\",\n  \"borderInlineStartWidth:inherit\":\n    \"The inline start border width is inherited from its parent.\",\n  \"borderInlineStartWidth:unset\":\n    \"The inline start border width is set to its default value.\",\n  \"borderLeftStyle:none\": \"No left border is shown.\",\n  \"borderLeftStyle:hidden\": \"Same as none, but takes up space.\",\n  \"borderLeftStyle:dotted\": \"A dotted border is shown.\",\n  \"borderLeftStyle:dashed\": \"A dashed border is shown.\",\n  \"borderLeftStyle:solid\": \"A solid border is shown.\",\n  \"borderLeftStyle:double\": \"A double border is shown.\",\n  \"borderLeftStyle:groove\": \"A 3D grooved border is shown.\",\n  \"borderLeftStyle:ridge\": \"A 3D ridged border is shown.\",\n  \"borderLeftStyle:inset\": \"A 3D inset border is shown.\",\n  \"borderLeftStyle:outset\": \"A 3D outset border is shown.\",\n  \"borderLeftStyle:initial\":\n    \"The left border style is set to its initial value.\",\n  \"borderLeftStyle:inherit\":\n    \"The left border style is inherited from its parent.\",\n  \"borderLeftStyle:unset\": \"The left border style is set to its default value.\",\n  \"borderLeftWidth:thin\":\n    \"Sets the thickness of the left border to a thin line.\",\n  \"borderLeftWidth:medium\":\n    \"Sets the thickness of the left border to a medium line.\",\n  \"borderLeftWidth:thick\":\n    \"Sets the thickness of the left border to a thick line.\",\n  \"borderLeftWidth:initial\":\n    \"Sets the thickness of the left border to its default value.\",\n  \"borderLeftWidth:inherit\":\n    \"Inherits the thickness of the left border from its parent element.\",\n  \"borderLeftWidth:unset\":\n    \"Sets the thickness of the left border to its default value, unless it's inherited.\",\n  \"borderRightStyle:none\": \"Removes the right border.\",\n  \"borderRightStyle:hidden\": \"Same as none, but occupies space.\",\n  \"borderRightStyle:dotted\": \"Sets the right border to a dotted line.\",\n  \"borderRightStyle:dashed\": \"Sets the right border to a dashed line.\",\n  \"borderRightStyle:solid\": \"Sets the right border to a solid line.\",\n  \"borderRightStyle:double\": \"Sets the right border to a double line.\",\n  \"borderRightStyle:groove\": \"Sets the right border to a 3D groove.\",\n  \"borderRightStyle:ridge\": \"Sets the right border to a 3D ridge.\",\n  \"borderRightStyle:inset\": \"Sets the right border to a 3D inset.\",\n  \"borderRightStyle:outset\": \"Sets the right border to a 3D outset.\",\n  \"borderRightStyle:initial\": \"Sets the border style to its default value.\",\n  \"borderRightStyle:inherit\":\n    \"Inherits the border style from its parent element.\",\n  \"borderRightStyle:unset\": \"Resets the border style to its initial value.\",\n  \"borderRightWidth:thin\": \"Sets the width of the right border to a thin size.\",\n  \"borderRightWidth:medium\":\n    \"Sets the width of the right border to a medium size.\",\n  \"borderRightWidth:thick\":\n    \"Sets the width of the right border to a thick size.\",\n  \"borderRightWidth:initial\":\n    \"Sets the width of the right border to its default value.\",\n  \"borderRightWidth:inherit\":\n    \"Inherits the width of the right border from its parent element.\",\n  \"borderRightWidth:unset\":\n    \"Resets the width of the right border to its initial value.\",\n  \"borderSpacing:initial\":\n    \"Sets the distance between cells to its default value.\",\n  \"borderSpacing:inherit\":\n    \"Inherits the distance between cells from its parent element.\",\n  \"borderSpacing:unset\":\n    \"Resets the distance between cells to its initial value.\",\n  \"borderStartEndRadius:initial\":\n    \"Sets the border radius of the end and start edges to its default value.\",\n  \"borderStartEndRadius:inherit\":\n    \"Inherits the border radius of the end and start edges from its parent element.\",\n  \"borderStartEndRadius:unset\":\n    \"Resets the border radius of the end and start edges to its initial value.\",\n  \"borderStartStartRadius:initial\":\n    \"Sets the border radius of the start edge to its default value.\",\n  \"borderStartStartRadius:inherit\":\n    \"Inherits the border radius of the parent element for the top left corner.\",\n  \"borderStartStartRadius:unset\":\n    \"Resets the border radius of the top left corner to the default value.\",\n  \"borderTopLeftRadius:initial\":\n    \"Sets the top left corner of the border to the default radius value.\",\n  \"borderTopLeftRadius:inherit\":\n    \"Inherits the top left border radius of the parent element.\",\n  \"borderTopLeftRadius:unset\":\n    \"Resets the top left corner of the border to its default value.\",\n  \"borderTopRightRadius:initial\":\n    \"Sets the top right corner of the border to the default radius value.\",\n  \"borderTopRightRadius:inherit\":\n    \"Inherits the top right corner border radius of the parent element.\",\n  \"borderTopRightRadius:unset\":\n    \"Resets the top right corner of the border to its default value.\",\n  \"borderTopStyle:none\": \"Displays no top border.\",\n  \"borderTopStyle:hidden\": \"Hides the top border.\",\n  \"borderTopStyle:dotted\": \"Displays a dotted top border.\",\n  \"borderTopStyle:dashed\": \"Displays a dashed top border.\",\n  \"borderTopStyle:solid\": \"Displays a solid top border.\",\n  \"borderTopStyle:double\": \"Displays a double top border.\",\n  \"borderTopStyle:groove\": \"Displays a 3D grooved top border.\",\n  \"borderTopStyle:ridge\": \"Displays a 3D ridged top border.\",\n  \"borderTopStyle:inset\":\n    'Creates an \"inset\" border, which makes it look like the content is carved into the page.',\n  \"borderTopStyle:outset\":\n    'Creates an \"outset\" border, which makes it look like the content is popping out of the page.',\n  \"borderTopStyle:initial\":\n    \"Sets the border-style property to its default value.\",\n  \"borderTopStyle:inherit\":\n    \"Inherits the border-style property from its parent element.\",\n  \"borderTopStyle:unset\":\n    \"Resets the border-style property to its inherited value if it was set, otherwise sets it to the initial value.\",\n  \"borderTopWidth:thin\": \"Sets the width of the top border to a thin size.\",\n  \"borderTopWidth:medium\": \"Sets the width of the top border to a medium size.\",\n  \"borderTopWidth:thick\": \"Sets the width of the top border to a thick size.\",\n  \"borderTopWidth:initial\":\n    \"Sets the border-width property to its default value.\",\n  \"borderTopWidth:inherit\":\n    \"Inherits the border-width property from its parent element.\",\n  \"borderTopWidth:unset\":\n    \"Resets the border-width property to its inherited value if it was set, otherwise sets it to the initial value.\",\n  \"bottom:auto\":\n    \"Sets the bottom position to be automatically determined by the browser.\",\n  \"bottom:initial\": \"Sets the bottom position to its default value.\",\n  \"bottom:inherit\": \"Inherits the bottom position from its parent element.\",\n  \"bottom:unset\":\n    \"Resets the bottom position to its inherited value if it was set, otherwise sets it to the initial value.\",\n  \"boxDecorationBreak:slice\":\n    \"The background, padding, and border of a box are broken and wrapped around each line of text.\",\n  \"boxDecorationBreak:clone\":\n    \"The element's background, border, and box-shadow are cloned to each fragment.\",\n  \"boxDecorationBreak:initial\": \"The default value is applied.\",\n  \"boxDecorationBreak:inherit\":\n    \"The value is inherited from the parent element.\",\n  \"boxDecorationBreak:unset\":\n    \"The value is inherited, unless there is no inherited value, then it's set to initial.\",\n  \"boxSizing:content-box\":\n    \"The width and height properties only apply to the content, not including padding or border.\",\n  \"boxSizing:border-box\":\n    \"The width and height properties apply to the content, including padding and border.\",\n  \"boxSizing:initial\": \"The default value is applied.\",\n  \"boxSizing:inherit\": \"The value is inherited from the parent element.\",\n  \"boxSizing:unset\":\n    \"The value is inherited, unless there is no inherited value, then it's set to initial.\",\n  \"breakAfter:auto\":\n    \"The browser determines where to insert a page break after the element.\",\n  \"breakAfter:avoid\": \"Avoids page breaks after the element.\",\n  \"breakAfter:always\": \"Inserts a page break after the element.\",\n  \"breakAfter:all\": \"Inserts a page break after all elements.\",\n  \"breakAfter:avoid-page\":\n    \"Avoids page breaks within a page spanned by the element.\",\n  \"breakAfter:page\":\n    \"Inserts a page break after the element, forcing the next page to be right-hand.\",\n  \"breakAfter:left\":\n    \"Inserts a page break after the element, forcing the next page to be left-hand.\",\n  \"breakAfter:right\": \"Forces a page break immediately after the element.\",\n  \"breakAfter:recto\": \"Forces a page break after a right-handed page.\",\n  \"breakAfter:verso\": \"Forces a page break after a left-handed page.\",\n  \"breakAfter:avoid-column\": \"Avoids a page break inside a column.\",\n  \"breakAfter:column\": \"Forces a page break inside a column.\",\n  \"breakAfter:avoid-region\": \"Avoids a page break inside a region.\",\n  \"breakAfter:region\": \"Forces a page break inside a region.\",\n  \"breakAfter:initial\": \"Sets this property to its default value.\",\n  \"breakAfter:inherit\": \"Inherits this property from its parent element.\",\n  \"breakAfter:unset\": \"Sets this property to its parent's computed value.\",\n  \"breakBefore:auto\": \"Allows a page break before or after the element.\",\n  \"breakBefore:avoid\": \"Avoids a page break before or after the element.\",\n  \"breakBefore:always\": \"Forces a page break before the element.\",\n  \"breakBefore:all\":\n    \"Forces the break of a multi-column element to the next page.\",\n  \"breakBefore:avoid-page\": \"Avoids a page break before the element.\",\n  \"breakBefore:page\": \"Forces a page break before the element.\",\n  \"breakBefore:left\":\n    \"Forces a page break so that the next page starts on the left side of a physical paper.\",\n  \"breakBefore:right\":\n    \"Forces a page break so that the next page starts on the right side of a physical paper.\",\n  \"breakBefore:recto\":\n    \"Forces a page break so that the next page starts on the recto side of a paper (the front side).\",\n  \"breakBefore:verso\":\n    \"Forces a page break so that the next page starts on the verso side of a paper (the back side).\",\n  \"breakBefore:avoid-column\":\n    \"Avoids a page break before the element, if it would cause it to be split across columns.\",\n  \"breakBefore:column\":\n    \"Forces a page break before the element, always starting a new column.\",\n  \"breakBefore:avoid-region\":\n    \"Avoids a page break before the element, if it would cause it to be split across regions.\",\n  \"breakBefore:region\":\n    \"Forces a page break before the element, always starting a new region.\",\n  \"breakBefore:initial\": \"Sets the break-before property to its default value.\",\n  \"breakBefore:inherit\":\n    \"Inherits the break-before property from its parent element.\",\n  \"breakBefore:unset\":\n    \"Resets the break-before property to its inherited value, or to the default value if there is no inherited value.\",\n  \"breakInside:auto\":\n    \"Allows the element to be broken across pages or columns as necessary.\",\n  \"breakInside:avoid\":\n    \"Avoids a page or column break inside the element, if possible.\",\n  \"breakInside:avoid-page\":\n    \"Avoids a page break inside the element, if possible.\",\n  \"breakInside:avoid-column\":\n    \"Avoids a column break inside the element, if possible.\",\n  \"breakInside:avoid-region\":\n    \"Avoids a region break inside the element, if possible.\",\n  \"breakInside:initial\":\n    \"Fragmentation behavior is determined by the parent element.\",\n  \"breakInside:inherit\":\n    \"Inherits the fragmentation behavior of the parent element.\",\n  \"breakInside:unset\":\n    \"Fragmentation behavior is determined by the parent, but can be overridden.\",\n  \"captionSide:top\": \"Sets the position of the table caption above the table.\",\n  \"captionSide:bottom\":\n    \"Sets the position of the table caption below the table.\",\n  \"captionSide:block-start\":\n    \"Sets the position of the table caption at the start of the block.\",\n  \"captionSide:block-end\":\n    \"Sets the position of the table caption at the end of the block.\",\n  \"captionSide:inline-start\":\n    \"Sets the position of the table caption at the start of the inline content.\",\n  \"captionSide:inline-end\":\n    \"Sets the position of the table caption at the end of the inline content.\",\n  \"captionSide:initial\":\n    \"Sets the position of the table caption to the default value.\",\n  \"captionSide:inherit\":\n    \"Inherits the position of the table caption from the parent element.\",\n  \"captionSide:unset\":\n    \"Sets the position of the table caption to the initial value.\",\n  \"caretShape:auto\": \"The shape of the input caret is browser specific.\",\n  \"caretShape:bar\": \"The input caret is a vertical bar.\",\n  \"caretShape:block\": \"The input caret is a solid square block.\",\n  \"caretShape:underscore\": \"The input caret is a horizontal underscore.\",\n  \"caretShape:initial\":\n    \"Displays a caret that resembles a solid triangle pointing up.\",\n  \"caretShape:inherit\": \"Inherits the caret shape from its parent element.\",\n  \"caretShape:unset\": \"Resets the caret shape to its default value.\",\n  \"clear:none\": \"The element is not moved down to clear past floated elements.\",\n  \"clear:left\":\n    \"The element is moved down to clear past left floated elements.\",\n  \"clear:right\":\n    \"The element is moved down to clear past right floated elements.\",\n  \"clear:both\":\n    \"The element is moved down to clear past both left and right floated elements.\",\n  \"clear:inline-start\":\n    \"The element is moved down to clear past left floated elements if the element is an inline-start.\",\n  \"clear:inline-end\":\n    \"The element is moved down to clear past right floated elements if the element is an inline-end.\",\n  \"clear:initial\":\n    \"The element is moved down to clear past both left and right floated elements.\",\n  \"clear:inherit\": \"Inherits the clearance from its parent element.\",\n  \"clear:unset\": \"Resets the clearance to its default value.\",\n  \"clip:auto\": \"The element is not clipped.\",\n  \"clip:initial\": \"Resets the clipping to its default value.\",\n  \"clip:inherit\": \"Inherits the clipping from its parent element.\",\n  \"clip:unset\": \"Resets the clipping to its default value.\",\n  \"clipPath:border-box\": \"Applies clipping to the border-box of an element.\",\n  \"clipPath:padding-box\": \"Applies clipping to the padding-box of an element.\",\n  \"clipPath:content-box\": \"Applies clipping to the content-box of an element.\",\n  \"clipPath:margin-box\": \"Applies clipping to the margin-box of an element.\",\n  \"clipPath:fill-box\": \"Applies clipping to the fill-box of an element.\",\n  \"clipPath:stroke-box\": \"Applies clipping to the stroke-box of an element.\",\n  \"clipPath:view-box\": \"Applies clipping to the view-box of an element.\",\n  \"clipPath:none\": \"Defines no clip path.\",\n  \"clipPath:initial\": \"Sets the clip path to its default value.\",\n  \"clipPath:inherit\":\n    \"Specifies that the clip path should be inherited from the parent element.\",\n  \"clipPath:unset\": \"Resets the clip-path property to its inherited value.\",\n  \"columnCount:auto\":\n    \"The number of columns will be determined by other CSS properties.\",\n  \"columnCount:initial\": \"Sets the number of columns to its default value.\",\n  \"columnCount:inherit\":\n    \"Specifies that the number of columns should be inherited from the parent element.\",\n  \"columnCount:unset\": \"Resets the number of columns to its inherited value.\",\n  \"columnFill:auto\":\n    \"Fills columns progressively and balances content between them.\",\n  \"columnFill:balance\": \"Distributes content evenly between columns.\",\n  \"columnFill:balance-all\":\n    \"Distributes content evenly, even when using `break-after` or `break-before`.\",\n  \"columnFill:initial\": \"Resets the property to its default value.\",\n  \"columnFill:inherit\": \"Inherits the property from its parent element.\",\n  \"columnFill:unset\": \"Resets the property to its inherited value.\",\n  \"columnGap:normal\": \"Sets the normal gap between columns.\",\n  \"columnGap:initial\": \"Resets the property to its default value.\",\n  \"columnGap:inherit\": \"Inherits the property from its parent element.\",\n  \"columnGap:unset\": \"Resets the property to its inherited value.\",\n  \"columnRuleStyle:none\": \"No column rule style is displayed.\",\n  \"columnRuleStyle:hidden\": \"Same as `none`, but still takes up space.\",\n  \"columnRuleStyle:dotted\": \"Sets the column rule to a dotted line.\",\n  \"columnRuleStyle:dashed\": \"Sets the column rule to a dashed line.\",\n  \"columnRuleStyle:solid\": \"Sets the column rule to a solid line.\",\n  \"columnRuleStyle:double\": \"Sets the column rule to a double line.\",\n  \"columnRuleStyle:groove\": \"Sets the column rule to a 3D grooved line.\",\n  \"columnRuleStyle:ridge\":\n    \"The column rule will be displayed with a ridge-like pattern.\",\n  \"columnRuleStyle:inset\":\n    \"The column rule will be displayed with an inward pointing pattern.\",\n  \"columnRuleStyle:outset\":\n    \"The column rule will be displayed with an outward pointing pattern.\",\n  \"columnRuleStyle:initial\":\n    \"Resets the column rule style back to its default value.\",\n  \"columnRuleStyle:inherit\":\n    \"Inherits the column rule style from its parent element.\",\n  \"columnRuleStyle:unset\":\n    \"Resets the column rule style to its inherited value or to the default value if there is no inherited value.\",\n  \"columnRuleWidth:thin\":\n    \"The column rule will be displayed with a thin width.\",\n  \"columnRuleWidth:medium\":\n    \"The column rule will be displayed with a medium width.\",\n  \"columnRuleWidth:thick\":\n    \"The column rule will be displayed with a thick width.\",\n  \"columnRuleWidth:initial\":\n    \"Resets the column rule width back to its default value.\",\n  \"columnRuleWidth:inherit\":\n    \"Inherits the column rule width from its parent element.\",\n  \"columnRuleWidth:unset\":\n    \"Resets the column rule width to its inherited value or to the default value if there is no inherited value.\",\n  \"columnSpan:none\": \"The element will span across only one column.\",\n  \"columnSpan:all\": \"The element will span across all columns.\",\n  \"columnSpan:initial\":\n    \"Resets the column span value back to its default value.\",\n  \"columnSpan:inherit\":\n    \"Inherits the column span value from its parent element.\",\n  \"columnSpan:unset\":\n    \"The element is treated as normal and can span multiple columns.\",\n  \"columnWidth:auto\":\n    \"The width of each column is determined by the content it contains.\",\n  \"columnWidth:initial\": \"Sets the width of the columns to the default value.\",\n  \"columnWidth:inherit\":\n    \"Inherits the width of the columns from the parent element.\",\n  \"columnWidth:unset\": \"Sets the width of the columns to the default value.\",\n  \"contain:none\": \"The element does not have any boxes nested inside.\",\n  \"contain:strict\": \"The element has a fully contained subtree.\",\n  \"contain:content\": \"The element has a partially contained subtree.\",\n  \"contain:size\": \"The element fully contains all its content.\",\n  \"contain:inline-size\": \"The element fully contains its inline content.\",\n  \"contain:layout\": \"The element does not affect the layout of other elements.\",\n  \"contain:style\": \"The element does not affect the styles of other elements.\",\n  \"contain:paint\":\n    \"The element does not affect the painting of other elements.\",\n  \"contain:initial\": \"Sets the containment to its default value.\",\n  \"contain:inherit\": \"Inherits the containment from the parent element.\",\n  \"contain:unset\": \"Sets the containment to its default value.\",\n  \"containIntrinsicBlockSize:none\":\n    \"The size of the element is not an intrinsic size.\",\n  \"containIntrinsicBlockSize:auto\":\n    \"The intrinsic size of the element is its content size.\",\n  \"containIntrinsicBlockSize:initial\":\n    \"Resets the property to its initial value.\",\n  \"containIntrinsicBlockSize:inherit\":\n    \"Inherits the property from its parent element.\",\n  \"containIntrinsicBlockSize:unset\":\n    \"Resets the property to its inherited value, or to the default value.\",\n  \"containIntrinsicHeight:none\":\n    \"The height of the element is not an intrinsic size.\",\n  \"containIntrinsicHeight:auto\":\n    \"The intrinsic size of the element is its content height.\",\n  \"containIntrinsicHeight:initial\": \"Resets the property to its initial value.\",\n  \"containIntrinsicHeight:inherit\":\n    \"Inherits the property from its parent element.\",\n  \"containIntrinsicHeight:unset\":\n    \"Resets the property to its inherited value, or to the default value.\",\n  \"containIntrinsicInlineSize:none\":\n    \"The size of the element is not an intrinsic size.\",\n  \"containIntrinsicInlineSize:auto\":\n    \"The intrinsic size of the element is its content size.\",\n  \"containIntrinsicInlineSize:initial\":\n    \"Resets the property to its initial value.\",\n  \"containIntrinsicInlineSize:inherit\":\n    \"Inherits the property from its parent element.\",\n  \"containIntrinsicInlineSize:unset\":\n    \"Resets the property to its inherited value, or to the default value.\",\n  \"containIntrinsicWidth:none\":\n    \"The width of the element is not an intrinsic size.\",\n  \"containIntrinsicWidth:auto\":\n    \"contain-intrinsic-width: auto - Element can use its intrinsic width to determine size.\",\n  \"containIntrinsicWidth:initial\":\n    \"contain-intrinsic-width: initial - Set intrinsic width as the maximum width.\",\n  \"containIntrinsicWidth:inherit\":\n    \"contain-intrinsic-width: inherit - Takes the contain-intrinsic-width of the parent element.\",\n  \"containIntrinsicWidth:unset\":\n    \"contain-intrinsic-width: unset - Inherits its value from the parent element if available.\",\n  \"content:normal\": \"content: normal - Uses normal content.\",\n  \"content:none\": \"content: none - No content is generated.\",\n  \"content:contents\":\n    \"content: contents - Parent becomes the parent of the first child, instead of its actual parent.\",\n  \"content:open-quote\":\n    \"content: open-quote - The opening quote is generated before the content.\",\n  \"content:close-quote\":\n    \"content: close-quote - The closing quote is generated after the content.\",\n  \"content:no-open-quote\": \"content: no-open-quote - Quotes are not generated.\",\n  \"content:no-close-quote\":\n    \"content: no-close-quote - Quotes are not generated.\",\n  \"content:initial\":\n    \"content: initial - Sets the property to its default value.\",\n  \"content:inherit\":\n    \"content: inherit - Inherits the property from its parents element.\",\n  \"content:unset\":\n    \"content: unset - Inherits the property from its parents element if it exists, else initial.\",\n  \"contentVisibility:visible\":\n    \"content-visibility: visible - The element is visible.\",\n  \"contentVisibility:auto\":\n    \"content-visibility: auto - User agent makes the content visible only when needed.\",\n  \"contentVisibility:hidden\":\n    \"Hides an element and its content from rendering.\",\n  \"contentVisibility:initial\":\n    \"Sets the element's content to visible by default.\",\n  \"contentVisibility:inherit\":\n    \"Inherits the content-visibility value from the parent.\",\n  \"contentVisibility:unset\":\n    \"Unsets the content-visibility value and uses the default behavior.\",\n  \"counterIncrement:none\": \"Disables incrementing of a CSS counter.\",\n  \"counterIncrement:initial\": \"Sets the counter to 0 for the current element.\",\n  \"counterIncrement:inherit\":\n    \"Inherits the counter value from the parent element.\",\n  \"counterIncrement:unset\":\n    \"Removes the counter limit and resets the value to its initial state.\",\n  \"counterReset:none\": \"Disables resetting of a CSS counter.\",\n  \"counterReset:initial\":\n    \"Resets the counter value to 0 for the current element.\",\n  \"counterReset:inherit\": \"Inherits the counter value from the parent element.\",\n  \"counterReset:unset\":\n    \"Removes the counter limit and resets the value to its initial state.\",\n  \"counterSet:none\": \"Removes the specified counter value.\",\n  \"counterSet:initial\":\n    \"Sets the counter value to the default value defined in the content's counter style.\",\n  \"counterSet:inherit\": \"Inherits the counter value from the parent element.\",\n  \"counterSet:unset\":\n    \"Removes the counter limit and resets the value to its initial state.\",\n  \"cursor:auto\": \"Uses the platform's default cursor.\",\n  \"cursor:default\": \"Sets default arrow cursor.\",\n  \"cursor:none\": \"Hides the cursor.\",\n  \"cursor:context-menu\": \"Indicates context menu is available.\",\n  \"cursor:help\": \"Indicates help is available.\",\n  \"cursor:pointer\": \"Sets cursor as a hand.\",\n  \"cursor:progress\": \"Indicates an operation in progress.\",\n  \"cursor:wait\": \"Indicates the app is busy.\",\n  \"cursor:cell\": \"Sets cursor as a cell.\",\n  \"cursor:crosshair\": \"Sets cursor as a crosshair.\",\n  \"cursor:text\": \"Sets cursor as a text.\",\n  \"cursor:vertical-text\": \"Sets cursor as a vertical text.\",\n  \"cursor:alias\": \"Sets cursor as an alias.\",\n  \"cursor:copy\": \"Sets cursor as copy pointer.\",\n  \"cursor:move\": \"Indicates the object can be moved.\",\n  \"cursor:not-allowed\": 'Changes the cursor to a \"not allowed\" sign.',\n  \"cursor:e-resize\": \"Changes the cursor to an east-resize arrow.\",\n  \"cursor:n-resize\": \"Changes the cursor to a north-resize arrow.\",\n  \"cursor:ne-resize\": \"Changes the cursor to a northeast-resize arrow.\",\n  \"cursor:nw-resize\": \"Changes the cursor to a northwest-resize arrow.\",\n  \"cursor:s-resize\": \"Changes the cursor to a south-resize arrow.\",\n  \"cursor:se-resize\": \"Changes the cursor to a southeast-resize arrow.\",\n  \"cursor:sw-resize\": \"Changes the cursor to a southwest-resize arrow.\",\n  \"cursor:w-resize\": \"Changes the cursor to a west-resize arrow.\",\n  \"cursor:ew-resize\": \"Changes the cursor to an east-west-resize arrow.\",\n  \"cursor:ns-resize\": \"Changes the cursor to a north-south-resize arrow.\",\n  \"cursor:nesw-resize\":\n    \"Changes the cursor to a northeast-southwest-resize arrow.\",\n  \"cursor:nwse-resize\":\n    \"Changes the cursor to a northwest-southeast-resize arrow.\",\n  \"cursor:col-resize\":\n    \"Changes the cursor to a vertical resize arrow over columns.\",\n  \"cursor:row-resize\":\n    \"Changes the cursor to a horizontal resize arrow over rows.\",\n  \"cursor:all-scroll\": \"Changes the cursor to a move arrow in all directions.\",\n  \"cursor:zoom-in\": \"Changes the cursor to a zoom-in icon.\",\n  \"cursor:zoom-out\": \"Changes the cursor to a zoom-out icon.\",\n  \"cursor:grab\": \"Changes the cursor to a hand icon when you hover over it.\",\n  \"cursor:grabbing\":\n    \"Changes the cursor to a grabbing hand icon when you click and hold on it.\",\n  \"cursor:initial\": \"Sets the cursor to its initial value.\",\n  \"cursor:inherit\": \"Inherits the cursor value from its parent element.\",\n  \"cursor:unset\":\n    \"Unsets the cursor value, letting the browser choose the appropriate one.\",\n  \"direction:ltr\": \"Sets text to flow from left-to-right.\",\n  \"direction:rtl\": \"Sets text to flow from right-to-left.\",\n  \"direction:initial\": \"Sets direction to its initial value.\",\n  \"direction:inherit\": \"Inherits direction value from its parent element.\",\n  \"direction:unset\":\n    \"Unsets direction value, letting the browser choose the appropriate one.\",\n  \"display:block\": \"Displays an element as a block-level element.\",\n  \"display:inline\": \"Displays an element as an inline-level element.\",\n  \"display:run-in\":\n    \"Displays an element as either block or inline, depending on context.\",\n  \"display:flow\":\n    \"Displays an element as a flow container block, but the box itself is not a block box.\",\n  \"display:flow-root\": \"Establishes a new block formatting context.\",\n  \"display:table\": \"Displays an element as a table.\",\n  \"display:flex\": \"Displays an element as a block-level flex container.\",\n  \"display:grid\": \"Displays an element as a grid container.\",\n  \"display:ruby\": \"Displays an element as a block container box with a marker.\",\n  \"display:list-item\": \"Displays an element as a list-item.\",\n  \"display:table-row-group\": \"Displays an element as a table row group.\",\n  \"display:table-header-group\":\n    \"Displays an element as a table header row group.\",\n  \"display:table-footer-group\":\n    \"Displays an element as a table footer row group.\",\n  \"display:table-row\": \"Displays an element as a table row.\",\n  \"display:table-cell\": \"Displays an element as a table cell.\",\n  \"display:table-column-group\": \"Displays an element as a table column group.\",\n  \"display:table-column\": \"Displays an element as a table column.\",\n  \"display:table-caption\": \"Displays an element as a table caption.\",\n  \"display:ruby-base\": \"Displays an element as a ruby base container box.\",\n  \"display:ruby-text\": \"Displays an element as a ruby text container box.\",\n  \"display:ruby-base-container\": \"Makes the element a ruby base container.\",\n  \"display:ruby-text-container\": \"Makes the element a ruby text container.\",\n  \"display:contents\": \"Makes children the box's content.\",\n  \"display:none\": \"The element is not displayed.\",\n  \"display:inline-block\": \"Makes the element an inline-level block container.\",\n  \"display:inline-list-item\":\n    \"Makes the element an inline-level list-item container.\",\n  \"display:inline-table\": \"Makes the element an inline-level table container.\",\n  \"display:inline-flex\": \"Makes the element an inline-level flex container.\",\n  \"display:inline-grid\": \"Makes the element an inline-level grid container.\",\n  \"display:initial\": \"Sets display to its default value.\",\n  \"display:inherit\": \"Inherits display from its parent element.\",\n  \"display:unset\":\n    \"Sets display to its default value if it is inherited, otherwise to inherit.\",\n  \"emptyCells:show\": \"Shows borders and backgrounds of empty table cells.\",\n  \"emptyCells:hide\": \"Hides borders and backgrounds of empty table cells.\",\n  \"emptyCells:initial\": \"Sets empty-cells to its default value.\",\n  \"emptyCells:inherit\": \"Inherits empty-cells from its parent element.\",\n  \"emptyCells:unset\":\n    \"The table cells will adopt their parent's `empty-cells` behavior when unset.\",\n  \"filter:unset\":\n    \"The element's initial main size axis is set to its content size.\",\n  \"flexBasis:content\":\n    \"The element's initial main size axis is set to auto, based on content and flex-grow.\",\n  \"flexBasis:auto\":\n    \"The element's initial main size axis is set to its minimum content size.\",\n  \"flexBasis:min-content\":\n    \"The element's initial main size axis is set to its maximum content size.\",\n  \"flexBasis:max-content\":\n    \"The element's initial main size axis is set to the available space.\",\n  \"flexBasis:fit-content\":\n    \"The element's initial main size axis is set to its default value.\",\n  \"flexBasis:initial\":\n    \"The element inherits its main size axis value from its parent element.\",\n  \"flexBasis:inherit\":\n    \"The element's main size axis value is determined by the browser.\",\n  \"flexBasis:unset\":\n    \"The element's main size axis value is unset, adopting the default value.\",\n  \"flexDirection:row\":\n    \"The flex items's main axis layout is set to a horizontal direction (left to right).\",\n  \"flexDirection:row-reverse\":\n    \"The flex items's main axis layout is set to a horizontal direction (right to left).\",\n  \"flexDirection:column\":\n    \"The flex items's main axis layout is set to a vertical direction (top to bottom).\",\n  \"flexDirection:column-reverse\":\n    \"The main axis runs vertically and starts from the bottom with flex-direction: column-reverse.\",\n  \"flexDirection:initial\":\n    \"Sets the initial value of the flex-direction property.\",\n  \"flexDirection:inherit\":\n    \"Inherits the flex-direction property from its parent element.\",\n  \"flexDirection:unset\":\n    \"Resets the flex-direction property value to the browser's default setting.\",\n  \"flexGrow:initial\": \"Sets the initial value of the flex-grow property.\",\n  \"flexGrow:inherit\":\n    \"Inherits the flex-grow property from its parent element.\",\n  \"flexGrow:unset\":\n    \"Resets the flex-grow property value to the browser's default setting.\",\n  \"flexShrink:initial\": \"Sets the initial value of the flex-shrink property.\",\n  \"flexShrink:inherit\":\n    \"Inherits the flex-shrink property from its parent element.\",\n  \"flexShrink:unset\":\n    \"Resets the flex-shrink property value to the browser's default setting.\",\n  \"flexWrap:nowrap\": \"No wrapping occurs with flex-wrap: nowrap.\",\n  \"flexWrap:wrap\":\n    \"Wraps items onto multiple lines from row to row with flex-wrap: wrap.\",\n  \"flexWrap:wrap-reverse\":\n    \"Wraps items onto multiple lines from row to row in reverse with flex-wrap: wrap-reverse.\",\n  \"flexWrap:initial\": \"Sets the initial value for the flex-wrap property.\",\n  \"flexWrap:inherit\":\n    \"Inherits the flex-wrap property from its parent element.\",\n  \"flexWrap:unset\":\n    \"Resets the flex-wrap property to the browser's default setting.\",\n  \"float:left\": \"The element moves to the left of its container.\",\n  \"float:right\": \"The element moves to the right of its container.\",\n  \"float:none\": \"The element does not float.\",\n  \"float:inline-start\": \"The element moves to the start of the line.\",\n  \"float:inline-end\": \"The element moves to the end of the line.\",\n  \"float:initial\": \"The element floats according to the parent element.\",\n  \"float:inherit\":\n    \"The element inherits the float value from its parent element.\",\n  \"float:unset\":\n    \"The element floats according to the parent element, or it is not floated.\",\n  \"fontFamily:serif\": \"Sets the font to a serif typeface.\",\n  \"fontFamily:sans-serif\": \"Sets the font to a sans-serif typeface.\",\n  \"fontFamily:cursive\": \"Sets the font to a cursive typeface.\",\n  \"fontFamily:fantasy\": \"Sets the font to a fantasy typeface.\",\n  \"fontFamily:monospace\": \"Sets the font to a monospace typeface.\",\n  \"fontFamily:initial\": \"Sets the font to its initial value.\",\n  \"fontFamily:inherit\": \"Inherits the font family from its parent element.\",\n  \"fontFamily:unset\":\n    \"Sets the font family to the parent element value or default if no parent value.\",\n  \"fontFeatureSettings:normal\": \"Disables all OpenType font features.\",\n  \"fontFeatureSettings:on\": \"Enables all OpenType font features.\",\n  \"fontFeatureSettings:off\": \"Disables all OpenType font features.\",\n  \"fontFeatureSettings:initial\":\n    \"Sets to default OpenType font feature settings.\",\n  \"fontFeatureSettings:inherit\":\n    \"Inherits OpenType font feature settings from parent.\",\n  \"fontFeatureSettings:unset\": \"Unsets OpenType font feature settings.\",\n  \"fontKerning:auto\": \"Browser decides if to use kerning or not.\",\n  \"fontKerning:normal\": \"Enables font kerning where applicable.\",\n  \"fontKerning:none\": \"Disables font kerning.\",\n  \"fontKerning:initial\": \"Sets font kerning to default value.\",\n  \"fontKerning:inherit\": \"Inherits font kerning from parent element.\",\n  \"fontKerning:unset\": \"Unsets font kerning.\",\n  \"fontLanguageOverride:normal\": \"No language-based glyph substitution occurs.\",\n  \"fontLanguageOverride:initial\":\n    \"Sets font language override to default value.\",\n  \"fontLanguageOverride:inherit\":\n    \"Inherits font language override from parent element.\",\n  \"fontLanguageOverride:unset\": \"Removes language-based glyph substitution.\",\n  \"fontOpticalSizing:auto\":\n    \"The font size is adjusted based on the font family and the document's font size.\",\n  \"fontOpticalSizing:none\":\n    \"Do not adjust the font size based on the font family.\",\n  \"fontOpticalSizing:initial\": \"Sets the font size to its default value.\",\n  \"fontOpticalSizing:inherit\":\n    \"Inherits the font size from the parent element.\",\n  \"fontOpticalSizing:unset\":\n    \"Sets the font size to its inherited value if it exists, else it sets it to its default value.\",\n  \"fontVariationSettings:normal\":\n    \"Reset all font variation settings to their normal values.\",\n  \"fontVariationSettings:initial\":\n    \"Sets the font variation settings to its default value.\",\n  \"fontVariationSettings:inherit\":\n    \"Inherits the font variation settings from the parent element.\",\n  \"fontVariationSettings:unset\":\n    \"Sets the font variation settings to its inherited value if it exists, else it sets it to its default value.\",\n  \"fontSize:xx-small\": \"Sets the font size to an extremely small size.\",\n  \"fontSize:x-small\": \"Sets the font size to an extra small size.\",\n  \"fontSize:small\": \"Sets the font size to a small size.\",\n  \"fontSize:medium\": \"Sets the font size to a medium size.\",\n  \"fontSize:large\": \"Sets the font size to a large size.\",\n  \"fontSize:x-large\": \"Sets the font size to an extra large size.\",\n  \"fontSize:xx-large\": \"Sets the font size to an extremely large size.\",\n  \"fontSize:xxx-large\":\n    \"font-size: xxx-large - Sets font size to the largest size available.\",\n  \"fontSize:larger\":\n    \"font-size: larger - Increases font size relative to parent element.\",\n  \"fontSize:smaller\":\n    \"font-size: smaller - Decreases font size relative to parent element.\",\n  \"fontSize:initial\":\n    \"font-size: initial - Sets font size to its default value.\",\n  \"fontSize:inherit\":\n    \"font-size: inherit - Inherits font size from parent element.\",\n  \"fontSize:unset\":\n    \"font-size: unset - Sets font size to its default value or inherits from parent element.\",\n  \"fontSizeAdjust:none\":\n    \"font-size-adjust: none - Disables font size adjustment.\",\n  \"fontSizeAdjust:ex-height\":\n    \"font-size-adjust: ex-height - Adjusts font size based on x-height of font.\",\n  \"fontSizeAdjust:cap-height\":\n    \"font-size-adjust: cap-height - Adjusts font size based on capital letter height.\",\n  \"fontSizeAdjust:ch-width\":\n    \"font-size-adjust: ch-width - Adjusts font size based on character width.\",\n  \"fontSizeAdjust:ic-width\":\n    \"font-size-adjust: ic-width - Adjusts font size based on ideographic character width.\",\n  \"fontSizeAdjust:ic-height\":\n    \"font-size-adjust: ic-height - Adjusts font size based on ideographic character height.\",\n  \"fontSizeAdjust:from-font\":\n    \"font-size-adjust: from-font - Adjusts font size based on the font's own metrics.\",\n  \"fontSizeAdjust:initial\":\n    \"font-size-adjust: initial - Sets font size adjustment to its default value.\",\n  \"fontSizeAdjust:inherit\":\n    \"font-size-adjust: inherit - Inherits font size adjustment from parent element.\",\n  \"fontSizeAdjust:unset\":\n    \"font-size-adjust: unset - Sets font size adjustment to its default value or inherits from parent element.\",\n  \"fontStretch:normal\": \"The font is not stretched or condensed.\",\n  \"fontStretch:ultra-condensed\": \"The font is ultra-condensed.\",\n  \"fontStretch:extra-condensed\": \"The font is extra-condensed.\",\n  \"fontStretch:condensed\": \"The font is condensed.\",\n  \"fontStretch:semi-condensed\": \"The font is somewhat condensed.\",\n  \"fontStretch:semi-expanded\": \"The font is somewhat expanded.\",\n  \"fontStretch:expanded\": \"The font is expanded.\",\n  \"fontStretch:extra-expanded\": \"The font is extra-expanded.\",\n  \"fontStretch:ultra-expanded\": \"The font is ultra-expanded.\",\n  \"fontStretch:initial\": \"Sets font-stretch to its default value.\",\n  \"fontStretch:inherit\": \"Inherits font-stretch from the parent element.\",\n  \"fontStretch:unset\": \"Resets font-stretch to its inherited value.\",\n  \"fontStyle:normal\": \"The font is in its normal style.\",\n  \"fontStyle:italic\": \"The font is in italic style.\",\n  \"fontStyle:oblique\": \"The font is tilted to the right using oblique style.\",\n  \"fontStyle:initial\": \"Sets font-style to its default value.\",\n  \"fontStyle:inherit\": \"Inherits font style from parent element.\",\n  \"fontStyle:unset\": \"Resets font style to inherit value.\",\n  \"fontSynthesis:none\":\n    \"Disables font synthesis for weight, style and small-caps.\",\n  \"fontSynthesis:weight\": \"Allows font synthesis for font weight.\",\n  \"fontSynthesis:style\": \"Allows font synthesis for font style.\",\n  \"fontSynthesis:small-caps\": \"Allows font synthesis for small-caps.\",\n  \"fontSynthesis:initial\": \"Sets font synthesis to its default value.\",\n  \"fontSynthesis:inherit\": \"Inherits font synthesis from parent element.\",\n  \"fontSynthesis:unset\": \"Resets font synthesis to inherit value.\",\n  \"fontVariant:normal\": \"Displays text in normal font variant.\",\n  \"fontVariant:none\": \"Disables font variants.\",\n  \"fontVariant:common-ligatures\": \"Enables common-ligatures font variant.\",\n  \"fontVariant:no-common-ligatures\": \"Disables common-ligatures font variant.\",\n  \"fontVariant:discretionary-ligatures\":\n    \"Enables discretionary-ligatures font variant.\",\n  \"fontVariant:no-discretionary-ligatures\":\n    \"Disables discretionary-ligatures font variant.\",\n  \"fontVariant:historical-ligatures\":\n    \"Enables historical-ligatures font variant.\",\n  \"fontVariant:no-historical-ligatures\":\n    \"Disables display of historical ligatures.\",\n  \"fontVariant:contextual\": \"Enables display of contextual alternates.\",\n  \"fontVariant:no-contextual\": \"Disables display of contextual alternates.\",\n  \"fontVariant:historical-forms\": \"Enables display of historical forms.\",\n  \"fontVariant:small-caps\": \"Uses small caps instead of lowercase letters.\",\n  \"fontVariant:all-small-caps\": \"Uses small caps for all letters.\",\n  \"fontVariant:petite-caps\": \"Uses a smaller caps font for lowercase.\",\n  \"fontVariant:all-petite-caps\": \"Uses a smaller caps font for all letters.\",\n  \"fontVariant:unicase\":\n    \"Uses uppercase and lowercase letters as equal height.\",\n  \"fontVariant:titling-caps\": \"Uses uppercase letters with increased height.\",\n  \"fontVariant:lining-nums\": \"Uses lining figures.\",\n  \"fontVariant:oldstyle-nums\": \"Uses figures with varying heights.\",\n  \"fontVariant:proportional-nums\":\n    \"Uses figures with widths proportional to regular characters.\",\n  \"fontVariant:tabular-nums\": \"Uses figures with equal widths.\",\n  \"fontVariant:diagonal-fractions\": \"Uses diagonal fraction glyphs.\",\n  \"fontVariant:stacked-fractions\": \"Uses horizontally stacked fraction glyphs.\",\n  \"fontVariant:ordinal\":\n    \"Uses small caps for uppercase letters and replaces lowercase letters with lowercase small caps.\",\n  \"fontVariant:slashed-zero\":\n    \"Uses a slash through the zero to distinguish it from the capital letter O.\",\n  \"fontVariant:jis78\": \"Sets the font to use the JIS78 character set.\",\n  \"fontVariant:jis83\": \"Sets the font to use the JIS83 character set.\",\n  \"fontVariant:jis90\": \"Sets the font to use the JIS90 character set.\",\n  \"fontVariant:jis04\": \"Sets the font to use the JIS2004 character set.\",\n  \"fontVariant:simplified\": \"Uses simplified Chinese characters.\",\n  \"fontVariant:traditional\": \"Uses traditional Chinese characters.\",\n  \"fontVariant:full-width\": \"Expands the characters to full width.\",\n  \"fontVariant:proportional-width\": \"Uses characters with proportional width.\",\n  \"fontVariant:ruby\": \"Adds annotations to East Asian text.\",\n  \"fontVariant:initial\": \"Sets the font-variant to its default value.\",\n  \"fontVariant:inherit\": \"Inherits the font-variant from the parent element.\",\n  \"fontVariant:unset\": \"Resets the font-variant to its inherited value.\",\n  \"fontVariantAlternates:normal\": \"Uses normal font variant alternates.\",\n  \"fontVariantAlternates:historical-forms\":\n    \"Uses historical font variant alternates.\",\n  \"fontVariantAlternates:initial\":\n    \"font-variant-alternates: initial - No alternate glyphs.\",\n  \"fontVariantAlternates:inherit\":\n    \"font-variant-alternates: inherit - Inherits the value from its parent element.\",\n  \"fontVariantAlternates:unset\":\n    \"font-variant-alternates: unset - Resets to its natural value.\",\n  \"fontVariantCaps:normal\":\n    \"font-variant-caps: normal - No effect on capitalization.\",\n  \"fontVariantCaps:small-caps\":\n    \"font-variant-caps: small-caps - Small caps for lowercase letters.\",\n  \"fontVariantCaps:all-small-caps\":\n    \"font-variant-caps: all-small-caps - All lowercase letters converted to small caps.\",\n  \"fontVariantCaps:petite-caps\":\n    \"font-variant-caps: petite-caps - Shorter small caps than small-caps.\",\n  \"fontVariantCaps:all-petite-caps\":\n    \"font-variant-caps: all-petite-caps - All lowercase letters converted to shorter small caps than small-caps.\",\n  \"fontVariantCaps:unicase\":\n    \"font-variant-caps: unicase - A mixture of uppercase and lowercase letters.\",\n  \"fontVariantCaps:titling-caps\":\n    \"font-variant-caps: titling-caps - All-capital letters.\",\n  \"fontVariantCaps:initial\": \"font-variant-caps: initial - Default value.\",\n  \"fontVariantCaps:inherit\":\n    \"font-variant-caps: inherit - Inherits the value from its parent element.\",\n  \"fontVariantCaps:unset\":\n    \"font-variant-caps: unset - Resets to its natural value.\",\n  \"fontVariantEastAsian:normal\":\n    \"font-variant-east-asian: normal - Uses the default glyphs for East Asian languages.\",\n  \"fontVariantEastAsian:jis78\":\n    \"font-variant-east-asian: jis78 - Uses JIS78 glyphs for East Asian languages.\",\n  \"fontVariantEastAsian:jis83\":\n    \"font-variant-east-asian: jis83 - Uses JIS83 glyphs for East Asian languages.\",\n  \"fontVariantEastAsian:jis90\": \"Use JIS 90 font variants for East Asian text.\",\n  \"fontVariantEastAsian:jis04\":\n    \"Use JIS 2004 font variants for East Asian text.\",\n  \"fontVariantEastAsian:simplified\":\n    \"Use simplified Chinese font variants for East Asian text.\",\n  \"fontVariantEastAsian:traditional\":\n    \"Use traditional Chinese font variants for East Asian text.\",\n  \"fontVariantEastAsian:full-width\":\n    \"Use full-width font variants for East Asian text.\",\n  \"fontVariantEastAsian:proportional-width\":\n    \"Use proportional-width font variants for East Asian text.\",\n  \"fontVariantEastAsian:ruby\":\n    \"Use ruby (pronunciation aids) font variants for East Asian text.\",\n  \"fontVariantEastAsian:initial\":\n    \"Sets the font-variant-east-asian property to its default value.\",\n  \"fontVariantEastAsian:inherit\":\n    \"Inherits the font-variant-east-asian property from its parent element.\",\n  \"fontVariantEastAsian:unset\":\n    \"Resets the font-variant-east-asian property to its inherited value.\",\n  \"fontVariantLigatures:normal\": \"Enables common ligatures for font display.\",\n  \"fontVariantLigatures:none\": \"Disables all ligatures for font display.\",\n  \"fontVariantLigatures:common-ligatures\":\n    \"Enables common ligatures for font display.\",\n  \"fontVariantLigatures:no-common-ligatures\":\n    \"Disables common ligatures for font display.\",\n  \"fontVariantLigatures:discretionary-ligatures\":\n    \"Enables discretionary ligatures for font display.\",\n  \"fontVariantLigatures:no-discretionary-ligatures\":\n    \"Disables discretionary ligatures for font display.\",\n  \"fontVariantLigatures:historical-ligatures\":\n    \"Enables historical ligatures for text.\",\n  \"fontVariantLigatures:no-historical-ligatures\":\n    \"Disables historical ligatures for text.\",\n  \"fontVariantLigatures:contextual\":\n    \"Enables ligatures for contextual alternatives.\",\n  \"fontVariantLigatures:no-contextual\":\n    \"Disables ligatures for contextual alternatives.\",\n  \"fontVariantLigatures:initial\":\n    \"Sets font-variant-ligatures to its default value.\",\n  \"fontVariantLigatures:inherit\":\n    \"Inherits font-variant-ligatures from parent element.\",\n  \"fontVariantLigatures:unset\":\n    \"Resets font-variant-ligatures to its inherited value.\",\n  \"fontVariantNumeric:normal\":\n    \"Resets all font-variant-numeric values to their defaults.\",\n  \"fontVariantNumeric:lining-nums\":\n    \"Replaces lowercase numbers with uppercase numbers.\",\n  \"fontVariantNumeric:oldstyle-nums\":\n    \"Replaces numbers with ones that have varying heights.\",\n  \"fontVariantNumeric:proportional-nums\":\n    \"Replaces numbers with ones that have varying widths.\",\n  \"fontVariantNumeric:tabular-nums\":\n    \"Replaces numbers with ones that align vertically.\",\n  \"fontVariantNumeric:diagonal-fractions\":\n    \"Replaces numbers with diagonal fractions.\",\n  \"fontVariantNumeric:stacked-fractions\":\n    \"Replaces numbers with stacked fractions.\",\n  \"fontVariantNumeric:ordinal\": \"Renders superscript characters correctly.\",\n  \"fontVariantNumeric:slashed-zero\": \"Replaces 0 with a slashed character.\",\n  \"fontVariantNumeric:initial\": \"The font displays numbers as normal.\",\n  \"fontVariantNumeric:inherit\":\n    \"The font inherits its parent's numeric font variant.\",\n  \"fontVariantNumeric:unset\":\n    \"The font inherits its parent's numeric font variant, or is set to normal if there is no inheritance.\",\n  \"fontVariantPosition:normal\": \"No variation on glyph position.\",\n  \"fontVariantPosition:sub\": \"The element displays its text as a subscript.\",\n  \"fontVariantPosition:super\":\n    \"The element displays its text as a superscript.\",\n  \"fontVariantPosition:initial\":\n    \"The element displays its text as subscript or superscript.\",\n  \"fontWeight:normal\": \"The font displays normal weight.\",\n  \"fontWeight:bold\": \"The font displays bold weight.\",\n  \"fontWeight:bolder\":\n    \"The font displays a weight higher than the parent value.\",\n  \"fontWeight:lighter\":\n    \"The font displays a weight lighter than the parent value.\",\n  \"fontWeight:initial\": \"The font displays its normal weight.\",\n  \"fontWeight:inherit\": \"The font inherits the weight of its parent.\",\n  \"fontWeight:unset\": \"The font inherits the weight of its parent.\",\n  \"gridAutoColumns:min-content\":\n    \"Grid auto columns are set to the minimum value necessary to fit the grid items.\",\n  \"gridAutoColumns:max-content\":\n    \"Grid auto columns are set to the maximum value to fit their grid items.\",\n  \"gridAutoColumns:auto\":\n    \"Grid auto columns are set to their default value based on the size of their grid items.\",\n  \"gridAutoColumns:initial\":\n    \"Grid auto columns are set to their default value.\",\n  \"gridAutoColumns:inherit\":\n    \"Grid auto columns inherit their value from their parent element.\",\n  \"gridAutoColumns:unset\":\n    \"Grid auto columns are set to their default value if it exists, otherwise it behaves like inherit.\",\n  \"gridAutoFlow:row\":\n    \"Grid items are placed along the grid's rows and positioned in the order they appear in the source code.\",\n  \"gridAutoFlow:column\":\n    \"Grid items are placed along the grid's columns and positioned in the order they appear in the source code.\",\n  \"gridAutoFlow:dense\":\n    \"Grid items are placed along both grid axes and empty cells are filled in the order they appear in the source code.\",\n  \"gridAutoFlow:initial\": \"Grid auto flow is set to its initial value.\",\n  \"gridAutoFlow:inherit\":\n    \"Grid auto flow inherits its value from its parent element.\",\n  \"gridAutoFlow:unset\":\n    \"Grid auto flow is set to its default value if it exists, otherwise it behaves like inherit.\",\n  \"gridAutoRows:min-content\":\n    \"Grid auto rows are set to the minimum value necessary to fit the grid items.\",\n  \"gridAutoRows:max-content\":\n    \"Grid auto rows are set to the maximum value to fit their grid items.\",\n  \"gridAutoRows:auto\":\n    \"Grid auto rows are set to their default value based on the size of their grid items.\",\n  \"gridAutoRows:initial\": \"Grid auto rows are set to their default value.\",\n  \"gridAutoRows:inherit\": \"Rows sized by parent\",\n  \"gridAutoRows:unset\": \"Use default row size\",\n  \"gridColumnEnd:auto\": \"End at next implicit grid line\",\n  \"gridColumnEnd:span\": \"End after spanned columns\",\n  \"gridColumnEnd:initial\": \"Use default column end position\",\n  \"gridColumnEnd:inherit\": \"Inherit column end position\",\n  \"gridColumnEnd:unset\": \"Unset column end position\",\n  \"gridColumnStart:auto\": \"Start at next implicit grid line\",\n  \"gridColumnStart:span\": \"Start from spanned columns\",\n  \"gridColumnStart:initial\": \"Use default column start position\",\n  \"gridColumnStart:inherit\": \"Inherit column start position\",\n  \"gridColumnStart:unset\": \"Unset column start position\",\n  \"gridRowEnd:auto\": \"End at next implicit grid line\",\n  \"gridRowEnd:span\": \"End after spanned rows\",\n  \"gridRowEnd:initial\": \"Use default row end position\",\n  \"gridRowEnd:inherit\": \"Inherit row end position\",\n  \"gridRowEnd:unset\": \"The grid item extends to the last row line.\",\n  \"gridRowStart:auto\": \"The grid item starts on the next grid row.\",\n  \"gridRowStart:span\": \"The grid item spans across a number of grid rows.\",\n  \"gridRowStart:initial\": \"The grid item starts at the first row.\",\n  \"gridRowStart:inherit\":\n    \"The grid item inherits its start position from its parent.\",\n  \"gridRowStart:unset\": \"The grid item starts at the first row line.\",\n  \"gridTemplateAreas:none\": \"The grid container has no named grid areas.\",\n  \"gridTemplateAreas:initial\":\n    \"The grid container uses the initial value for grid areas.\",\n  \"gridTemplateAreas:inherit\":\n    \"The grid container inherits the grid areas value from its parent.\",\n  \"gridTemplateAreas:unset\": \"The grid container has no named grid areas.\",\n  \"gridTemplateColumns:none\":\n    \"The grid container has no fixed number of grid columns.\",\n  \"gridTemplateColumns:min-content\":\n    \"The columns size to fit the content with minimum width.\",\n  \"gridTemplateColumns:max-content\":\n    \"The columns size to fit the content with maximum width.\",\n  \"gridTemplateColumns:auto\":\n    \"The columns size according to their intrinsic width.\",\n  \"gridTemplateColumns:subgrid\":\n    \"The grid container inherits grid columns from its parent.\",\n  \"gridTemplateColumns:initial\":\n    \"The grid container uses the initial value for grid columns.\",\n  \"gridTemplateColumns:inherit\":\n    \"The grid-template-columns value is inherited from the parent element.\",\n  \"gridTemplateColumns:unset\":\n    \"The grid-template-columns value is set to the initial value.\",\n  \"gridTemplateRows:none\": \"No grid rows are created.\",\n  \"gridTemplateRows:min-content\":\n    \"Grid rows are set to their minimum content height.\",\n  \"gridTemplateRows:max-content\":\n    \"Grid rows are set to their maximum content height.\",\n  \"gridTemplateRows:auto\": \"Grid rows are set to their default auto value.\",\n  \"gridTemplateRows:subgrid\":\n    \"The grid-template-rows value is inherited from a named grid.\",\n  \"gridTemplateRows:initial\":\n    \"The grid-template-rows value is set to the initial value.\",\n  \"gridTemplateRows:inherit\":\n    \"The grid-template-rows value is inherited from the parent element.\",\n  \"gridTemplateRows:unset\":\n    \"The grid-template-rows value is set to the initial value.\",\n  \"hangingPunctuation:none\": \"No hanging punctuation is allowed.\",\n  \"hangingPunctuation:first\": \"Only the first line ending is allowed to hang.\",\n  \"hangingPunctuation:force-end\":\n    \"All lines are forced to hang, even with whitelines.\",\n  \"hangingPunctuation:allow-end\":\n    \"Only the last line ending is allowed to hang.\",\n  \"hangingPunctuation:last\": \"Only the last line ending is allowed to hang.\",\n  \"hangingPunctuation:initial\":\n    \"Hanging punctuation is set to the initial value.\",\n  \"hangingPunctuation:inherit\":\n    \"Inherits the hanging punctuation behavior from the parent element.\",\n  \"hangingPunctuation:unset\":\n    \"Resets the hanging punctuation to its default behavior.\",\n  \"height:auto\": \"Sets the height to fit the content automatically.\",\n  \"height:min-content\":\n    \"Sets the height to the minimum height required for the content to fit.\",\n  \"height:max-content\":\n    \"Sets the height to the maximum height required for the content to fit.\",\n  \"height:fit-content\":\n    \"Sets the height to the smallest possible size while still fitting the content.\",\n  \"height:initial\": \"Sets the height to its default value.\",\n  \"height:inherit\": \"Inherits the height value from the parent element.\",\n  \"height:unset\": \"Resets the height to its default value.\",\n  \"hyphenateCharacter:auto\": \"Determines the automatic hyphenation character.\",\n  \"hyphenateCharacter:initial\":\n    \"Sets the hyphenation character to its default value.\",\n  \"hyphenateCharacter:inherit\":\n    \"Inherits the hyphenation character from the parent element.\",\n  \"hyphenateCharacter:unset\":\n    \"Resets the hyphenation character to its default value.\",\n  \"hyphens:none\": \"Disables hyphenation.\",\n  \"hyphens:manual\": \"Specifies where hyphens should be added, if at all.\",\n  \"hyphens:auto\": \"Allows the browser to automatically hyphenate words.\",\n  \"hyphens:initial\": \"Words are not divided into hyphens.\",\n  \"hyphens:inherit\": \"Words inherit hyphenation from their parent element.\",\n  \"hyphens:unset\": \"The property is set to its inherited value.\",\n  \"imageOrientation:from-image\": \"Orientation set by image metadata.\",\n  \"imageOrientation:flip\": \"Vertical flip of the image orientation.\",\n  \"imageOrientation:initial\": \"Orientation sets to its default value.\",\n  \"imageOrientation:inherit\": \"Inherit orientation from parent element.\",\n  \"imageOrientation:unset\": \"The property is set to its inherited value.\",\n  \"imageRendering:auto\": \"Default rendering, with optimization.\",\n  \"imageRendering:crisp-edges\": \"No anti-aliasing on the image.\",\n  \"imageRendering:pixelated\": \"Pixelated image rendering.\",\n  \"imageRendering:initial\": \"Property will inherit its initial value.\",\n  \"imageRendering:inherit\": \"Inherit value from parent element.\",\n  \"imageRendering:unset\": \"The property is set to its inherited value.\",\n  \"imageResolution:from-image\": \"Determines pixels per inch from the image.\",\n  \"imageResolution:snap\": \"Image resolution snaps to the nearest integer.\",\n  \"imageResolution:initial\": \"Sets the image resolution to the default value.\",\n  \"imageResolution:inherit\": \"Inherits image resolution from parent element.\",\n  \"imageResolution:unset\": \"Resets image resolution to the default value.\",\n  \"initialLetter:normal\":\n    \"Sets default styling for the dropped initial letter.\",\n  \"initialLetter:initial\": \"Sets the initial letter to its default value.\",\n  \"initialLetter:inherit\":\n    \"Inherits the dropped initial letter style from the parent element.\",\n  \"initialLetter:unset\":\n    \"Resets the dropped initial letter style to the default value.\",\n  \"initialLetterAlign:auto\": \"Aligns initial letter as specified by the font.\",\n  \"initialLetterAlign:alphabetic\":\n    \"Aligns initial letter to the alphabetic baseline.\",\n  \"initialLetterAlign:hanging\": \"Aligns initial letter to hanging baseline.\",\n  \"initialLetterAlign:ideographic\":\n    \"Aligns initial letter to the ideographic baseline.\",\n  \"initialLetterAlign:initial\":\n    \"Sets initial letter alignment to default value.\",\n  \"initialLetterAlign:inherit\":\n    \"Inherits initial-letter alignment from the parent element.\",\n  \"initialLetterAlign:unset\":\n    \"Resets initial-letter alignment to default value.\",\n  \"inlineSize:auto\": \"Sets the inline size to the content's default size.\",\n  \"inlineSize:min-content\":\n    \"Sets the inline size to the minimum size required to contain content.\",\n  \"inlineSize:max-content\":\n    \"Sets the inline size of an element to fit its content up to a maximum size.\",\n  \"inlineSize:fit-content\":\n    \"Sets the inline size of an element to fit its content as closely as possible.\",\n  \"inlineSize:initial\":\n    \"Sets the inline size of an element to its default value.\",\n  \"inlineSize:inherit\": \"Inherits the inline size of the parent element.\",\n  \"inlineSize:unset\": \"Removes any previously set inline size value.\",\n  \"inputSecurity:auto\":\n    \"Allows the browser to determine the security level for form input fields.\",\n  \"inputSecurity:none\": \"Disables security measures for form input fields.\",\n  \"inputSecurity:initial\":\n    \"Sets the security level of form input fields to its default value.\",\n  \"inputSecurity:inherit\":\n    \"Inherits the security level of the parent element for form input fields.\",\n  \"inputSecurity:unset\":\n    \"Removes any previously set security level value for form input fields.\",\n  \"insetBlockEnd:auto\":\n    \"Sets the inset block end (bottom) to its default value, auto.\",\n  \"insetBlockEnd:initial\":\n    \"Sets the inset block end (bottom) to its initial value.\",\n  \"insetBlockEnd:inherit\":\n    \"Inherits the inset block end (bottom) from the parent element.\",\n  \"insetBlockEnd:unset\":\n    \"Removes any previously set inset block end (bottom) value.\",\n  \"insetBlockStart:auto\":\n    \"Sets the inset block start (top) to its default value, auto.\",\n  \"insetBlockStart:initial\":\n    \"Sets the inset block start (top) to its initial value.\",\n  \"insetBlockStart:inherit\":\n    \"The block start position of an element is inherited from its parent.\",\n  \"insetBlockStart:unset\": \"The block start position of an element is unset.\",\n  \"insetInlineEnd:auto\":\n    \"The inline end position of an element is automatically determined.\",\n  \"insetInlineEnd:initial\":\n    \"The inline end position of an element is set to its initial value.\",\n  \"insetInlineEnd:inherit\":\n    \"The inline end position of an element is inherited from its parent.\",\n  \"insetInlineEnd:unset\": \"The inline end position of an element is unset.\",\n  \"insetInlineStart:auto\":\n    \"The inline start position of an element is automatically determined.\",\n  \"insetInlineStart:initial\":\n    \"The inline start position of an element is set to its initial value.\",\n  \"insetInlineStart:inherit\":\n    \"The inline start position of an element is inherited from its parent.\",\n  \"insetInlineStart:unset\": \"The inline start position of an element is unset.\",\n  \"isolation:auto\":\n    \"Determines whether an element must create a new stacking context.\",\n  \"isolation:isolate\": \"Forces an element to create a new stacking context.\",\n  \"isolation:initial\": \"Sets the isolation of an element to its initial value.\",\n  \"isolation:inherit\":\n    \"The isolation of an element is inherited from its parent.\",\n  \"isolation:unset\": \"The isolation of an element is unset.\",\n  \"justifyContent:normal\":\n    \"The items are packed in their default position as if no justify-content value was set.\",\n  \"justifyContent:space-between\":\n    \"Items are evenly distributed with equal space between them.\",\n  \"justifyContent:space-around\":\n    \"Items are evenly distributed with equal space on both sides of them.\",\n  \"justifyContent:space-evenly\":\n    \"Items are evenly distributed with equal space between them and around the edges.\",\n  \"justifyContent:stretch\": \"Items are stretched to fit container.\",\n  \"justifyContent:unsafe\":\n    \"Allows content to potentially overlap container boundary. Use with caution.\",\n  \"justifyContent:safe\":\n    \"Ensures that content does not overlap container boundary.\",\n  \"justifyContent:center\": \"Items are centered horizontally.\",\n  \"justifyContent:start\": \"Items start at the beginning of the container.\",\n  \"justifyContent:end\": \"Items end at the end of the container.\",\n  \"justifyContent:flex-start\": \"Items align to the start of the cross axis.\",\n  \"justifyContent:flex-end\": \"Items align to the end of the cross axis.\",\n  \"justifyContent:left\": \"Items are aligned to the left of the container.\",\n  \"justifyContent:right\": \"Items are aligned to the right of the container.\",\n  \"justifyContent:initial\": \"Sets the property to its default value.\",\n  \"justifyContent:inherit\": \"Inherits the property from its parent element.\",\n  \"justifyContent:unset\":\n    \"Resets the property to its default value if it has been set before, otherwise behaves like inherit.\",\n  \"justifyItems:normal\":\n    \"Items are aligned according to the default alignment.\",\n  \"justifyItems:stretch\": \"Items are stretched to fill the container.\",\n  \"justifyItems:first\": \"Invalid value.\",\n  \"justifyItems:last\": \"Invalid value.\",\n  \"justifyItems:baseline\": \"Items are aligned such that their baselines align.\",\n  \"justifyItems:unsafe\": \"Allows alignment that may cause overflow.\",\n  \"justifyItems:safe\": \"Prevents alignment that may cause overflow.\",\n  \"justifyItems:center\": \"Items are centered along the inline axis.\",\n  \"justifyItems:start\": \"Items are aligned to the start of the inline axis.\",\n  \"justifyItems:end\": \"Items are aligned to the end of the inline axis.\",\n  \"justifyItems:self-start\":\n    \"Items are aligned to the start of their respective inline axis.\",\n  \"justifyItems:self-end\":\n    \"Items are aligned to the end of their respective inline axis.\",\n  \"justifyItems:flex-start\": \"Items are aligned to the start (same as start).\",\n  \"justifyItems:flex-end\": \"Items are aligned to the end (same as end).\",\n  \"justifyItems:left\": \"Items are aligned to the left of the container.\",\n  \"justifyItems:right\": \"Items are aligned to the right of the container.\",\n  \"justifyItems:legacy\": \"Aligns items according to their legacy alignment.\",\n  \"justifyItems:initial\": \"Sets the alignment to its default value.\",\n  \"justifyItems:inherit\": \"Inherits the alignment from its parent element.\",\n  \"justifyItems:unset\": \"Resets the alignment to its inherited value.\",\n  \"justifySelf:auto\": \"Uses the parent's justify-items value.\",\n  \"justifySelf:normal\":\n    \"The item is placed according to the container's alignment rules.\",\n  \"justifySelf:stretch\": \"Stretches the item to fit the container.\",\n  \"justifySelf:first\": \"Places the item at the start of the inline axis.\",\n  \"justifySelf:last\": \"Places the item at the end of the inline axis.\",\n  \"justifySelf:baseline\": \"Aligns the item along the baseline.\",\n  \"justifySelf:unsafe\": \"Allows alignment that may cause overflow.\",\n  \"justifySelf:safe\": \"Prevents alignment that may cause overflow.\",\n  \"justifySelf:center\": \"Centers the item along the inline axis.\",\n  \"justifySelf:start\": \"Places the item at the start of the inline axis.\",\n  \"justifySelf:end\": \"Places the item at the end of the inline axis.\",\n  \"justifySelf:self-start\": \"Aligns the item at the start of its inline axis.\",\n  \"justifySelf:self-end\": \"Aligns the item at the end of its inline axis.\",\n  \"justifySelf:flex-start\": \"Places the item at the start (same as start).\",\n  \"justifySelf:flex-end\": \"Places the item at the end (same as end).\",\n  \"justifySelf:left\":\n    \"Aligns the content of an element to the left of its container.\",\n  \"justifySelf:right\":\n    \"Aligns the content of an element to the right of its container.\",\n  \"justifySelf:initial\":\n    \"Sets the alignment of the element to its default (initial) value.\",\n  \"justifySelf:inherit\":\n    \"Inherits the alignment of the element from its parent element.\",\n  \"justifySelf:unset\":\n    \"Resets the alignment of the element to its natural (unset) value.\",\n  \"justifyTracks:normal\":\n    \"Aligns grid items according to their order in the grid container.\",\n  \"justifyTracks:space-between\":\n    \"Distributes the grid items with equal space between them, but not around the outer edges.\",\n  \"justifyTracks:space-around\":\n    \"Distributes the grid items with equal space around them.\",\n  \"justifyTracks:space-evenly\":\n    \"Distributes the grid items with equal space around them, including the outer edges.\",\n  \"justifyTracks:stretch\":\n    \"Stretches the grid items to fill the available space in the grid container.\",\n  \"justifyTracks:unsafe\":\n    \"Enables user agents to optimize the grid by increasing the number of tracks.\",\n  \"justifyTracks:safe\":\n    \"Disables user agents from optimizing the grid and sets the tracks to their minimum size.\",\n  \"justifyTracks:center\":\n    \"Centers the grid items horizontally in the grid container.\",\n  \"justifyTracks:start\":\n    \"Aligns justification content to the start of the grid area.\",\n  \"justifyTracks:end\":\n    \"Aligns justification content to the end of the grid area.\",\n  \"justifyTracks:flex-start\":\n    \"Aligns tracks to the start of the container (same as start).\",\n  \"justifyTracks:flex-end\":\n    \"Aligns tracks to the end of the container (same as end).\",\n  \"justifyTracks:left\":\n    \"Aligns justification content to the left of the grid area.\",\n  \"justifyTracks:right\":\n    \"Aligns justification content to the right of the grid area.\",\n  \"justifyTracks:initial\": \"Sets justification content to its default value.\",\n  \"justifyTracks:inherit\":\n    \"Inherits the justification content from the parent element.\",\n  \"justifyTracks:unset\":\n    \"Unsets the justification content, allowing inheritance.\",\n  \"left:auto\": \"Sets the left position to be determined by the browser.\",\n  \"left:initial\": \"Sets the left position to its default value.\",\n  \"left:inherit\": \"Inherits the left position from the parent element.\",\n  \"left:unset\": \"Unsets the left position, allowing inheritance.\",\n  \"letterSpacing:normal\": \"Sets normal spacing between characters.\",\n  \"letterSpacing:initial\": \"Sets the letter-spacing to its default value.\",\n  \"letterSpacing:inherit\":\n    \"Inherits the letter-spacing from the parent element.\",\n  \"letterSpacing:unset\":\n    \"Sets the spacing between characters to the parent element's value.\",\n  \"lineBreak:auto\": \"Lines can break between any two letters.\",\n  \"lineBreak:loose\": \"Lines can break between any two grapheme clusters.\",\n  \"lineBreak:normal\":\n    \"Only break at allowed break points like spaces and hyphens.\",\n  \"lineBreak:strict\":\n    \"Breaks are allowed only between characters with a mandatory break opportunity.\",\n  \"lineBreak:anywhere\": \"Lines can break at any character.\",\n  \"lineBreak:initial\": \"Sets the line break behavior to its default value.\",\n  \"lineBreak:inherit\":\n    \"Inherits the line break behavior from the parent element.\",\n  \"lineBreak:unset\": \"Sets the line break behavior to its inherited value.\",\n  \"lineClamp:none\":\n    \"The block container is shown as many lines as determined by its content.\",\n  \"lineClamp:initial\":\n    \"Sets the number of lines that a block container should display to its default value.\",\n  \"lineClamp:inherit\":\n    \"Inherits the number of lines that a block container should display from the parent element.\",\n  \"lineClamp:unset\":\n    \"Sets the number of lines that a block container should display to its inherited value.\",\n  \"lineHeight:normal\":\n    \"Sets the line height to a normal value. The default value depends on the user agent.\",\n  \"lineHeight:initial\": \"Sets the line height to its default value.\",\n  \"lineHeight:inherit\": \"Inherits the line height from the parent element.\",\n  \"lineHeight:unset\": \"Sets the line height to the browser's default value.\",\n  \"lineHeightStep:initial\":\n    \"Sets the increment used in line-height computations to the CSS initial value.\",\n  \"lineHeightStep:inherit\":\n    \"Sets the increment used in line-height computations to the parent element's value.\",\n  \"lineHeightStep:unset\":\n    \"Sets the increment used in line-height computations to the browser's default value.\",\n  \"listStyleImage:none\":\n    \"Removes the marker from a list item for the CSS unset value.\",\n  \"listStyleImage:initial\":\n    \"Sets the marker to the initial value defined by the user agent for a list item.\",\n  \"listStyleImage:inherit\":\n    \"Sets the marker to the value specified in the parent element for a list item.\",\n  \"listStyleImage:unset\":\n    \"Resets the marker to its default none value for a list item.\",\n  \"listStylePosition:inside\":\n    \"Sets the marker position to the inside of a list item.\",\n  \"listStylePosition:outside\":\n    \"Sets the marker position to the outside of a list item.\",\n  \"listStylePosition:initial\":\n    \"Sets the marker position to its default value for a list item.\",\n  \"listStylePosition:inherit\":\n    \"Inherits the marker position from the parent element for a list item.\",\n  \"listStylePosition:unset\":\n    \"Resets the marker position to the browser's default value for a list item.\",\n  \"listStyleType:none\": \"Removes the marker from a list item.\",\n  \"listStyleType:initial\":\n    \"Sets the marker to the initial value defined by the user agent.\",\n  \"listStyleType:inherit\": \"Inherits the marker from the parent element.\",\n  \"listStyleType:unset\": \"Removes the list item marker and uses default.\",\n  \"marginBlockEnd:auto\": \"Automatically computes the bottom margin.\",\n  \"marginBlockEnd:initial\": \"Sets the bottom margin to its default value.\",\n  \"marginBlockEnd:inherit\": \"Sets the bottom margin to its parent's value.\",\n  \"marginBlockEnd:unset\": \"Inherits or sets the bottom margin to its default.\",\n  \"marginBlockStart:auto\": \"Automatically computes the top margin.\",\n  \"marginBlockStart:initial\": \"Sets the top margin to its default value.\",\n  \"marginBlockStart:inherit\": \"Sets the top margin to its parent's value.\",\n  \"marginBlockStart:unset\": \"Inherits or sets the top margin to its default.\",\n  \"marginBottom:auto\": \"Automatically computes the bottom margin.\",\n  \"marginBottom:initial\": \"Sets the bottom margin to its default value.\",\n  \"marginBottom:inherit\": \"Sets the bottom margin to its parent's value.\",\n  \"marginBottom:unset\": \"Inherits or sets the bottom margin to its default.\",\n  \"marginInlineEnd:auto\": \"Automatically computes the right/left margin.\",\n  \"marginInlineEnd:initial\": \"Sets the right/left margin to its default value.\",\n  \"marginInlineEnd:inherit\":\n    \"Sets the right/left margin to its parent's value.\",\n  \"marginInlineEnd:unset\": \"Removes margin on the end inline side.\",\n  \"marginInlineStart:auto\":\n    \"Sets margin to automatic on the inline start side.\",\n  \"marginInlineStart:initial\":\n    \"Sets margin to default on the inline start side.\",\n  \"marginInlineStart:inherit\":\n    \"Inherits margin from the parent element on the inline start side.\",\n  \"marginInlineStart:unset\": \"Removes margin on the start inline side.\",\n  \"marginLeft:auto\": \"Sets margin to automatic on the left side.\",\n  \"marginLeft:initial\": \"Sets margin to default on the left side.\",\n  \"marginLeft:inherit\":\n    \"Inherits margin from the parent element on the left side.\",\n  \"marginLeft:unset\": \"Removes margin on the left side.\",\n  \"marginRight:auto\": \"Sets margin to automatic on the right side.\",\n  \"marginRight:initial\": \"Sets margin to default on the right side.\",\n  \"marginRight:inherit\":\n    \"Inherits margin from the parent element on the right side.\",\n  \"marginRight:unset\": \"Removes margin on the right side.\",\n  \"marginTop:auto\": \"Sets margin to automatic on the top side.\",\n  \"marginTop:initial\": \"Sets margin to default on the top side.\",\n  \"marginTop:inherit\":\n    \"Inherits margin from the parent element on the top side.\",\n  \"marginTop:unset\": \"Removes margin on top side of the element.\",\n  \"marginTrim:none\": \"No trimming of margin.\",\n  \"marginTrim:in-flow\": \"Trims only the margin perpendicular to the flow.\",\n  \"marginTrim:all\": \"Trims all margins.\",\n  \"marginTrim:initial\": \"Takes the default value of ‘none’\",\n  \"marginTrim:inherit\": \"Inherits the property from its parent element.\",\n  \"marginTrim:unset\": \"Unsets the value of this property.\",\n  \"maskBorderMode:luminance\":\n    \"The mask border uses the luminance of the mask and the image\",\n  \"maskBorderMode:alpha\": \"The mask border uses the alpha values of the mask.\",\n  \"maskBorderMode:initial\": \"Uses the default value.\",\n  \"maskBorderMode:inherit\": \"Inherits from the parent element.\",\n  \"maskBorderMode:unset\": \"Unsets value of the property.\",\n  \"maskBorderOutset:initial\": \"Uses the initial value.\",\n  \"maskBorderOutset:inherit\": \"Inherits from the parent element.\",\n  \"maskBorderOutset:unset\": \"Unsets the value of the property.\",\n  \"maskBorderRepeat:stretch\":\n    \"Stretches the image to fill the size of the mask.\",\n  \"maskBorderRepeat:repeat\": \"Repeats the mask border image.\",\n  \"maskBorderRepeat:round\": \"Rounds the mask border image.\",\n  \"maskBorderRepeat:space\": \"Spaces the mask border image.\",\n  \"maskBorderRepeat:initial\":\n    \"Sets the mask border repeat to its default value.\",\n  \"maskBorderRepeat:inherit\":\n    \"Inherits the mask border repeat from the parent element.\",\n  \"maskBorderRepeat:unset\":\n    \"Sets the mask border repeat to its inherited value, or default if there is no inheritance.\",\n  \"maskBorderSlice:fill\":\n    \"Fills the mask border slice with the mask border image.\",\n  \"maskBorderSlice:initial\": \"Sets the mask border slice to its default value.\",\n  \"maskBorderSlice:inherit\":\n    \"Inherits the mask border slice from the parent element.\",\n  \"maskBorderSlice:unset\":\n    \"Sets the mask border slice to its inherited value, or default if there is no inheritance.\",\n  \"maskBorderSource:none\": \"Specifies no mask border source image.\",\n  \"maskBorderSource:initial\":\n    \"Sets the mask border source to its default value.\",\n  \"maskBorderSource:inherit\":\n    \"Inherits the mask border source from the parent element.\",\n  \"maskBorderSource:unset\":\n    \"Sets the mask border source to its inherited value, or default if there is no inheritance.\",\n  \"maskBorderWidth:auto\": \"Sets the mask border width to its default value.\",\n  \"maskBorderWidth:initial\": \"Sets the mask border width to its default value.\",\n  \"maskBorderWidth:inherit\":\n    \"Inherits the mask border width from the parent element.\",\n  \"maskBorderWidth:unset\":\n    \"Returns the mask border width to its default value.\",\n  \"maskClip:border-box\": \"Clips the mask image to the border box.\",\n  \"maskClip:padding-box\": \"Clips the mask image to the padding box.\",\n  \"maskClip:content-box\": \"Clips the mask image to the content box.\",\n  \"maskClip:margin-box\": \"Clips the mask image to the margin box.\",\n  \"maskClip:fill-box\": \"Clips the mask image to the painted area.\",\n  \"maskClip:stroke-box\": \"Clips the mask image to the stroke area.\",\n  \"maskClip:view-box\": \"Clips the mask image to the viewport.\",\n  \"maskClip:no-clip\": \"No clip is applied to the mask image.\",\n  \"maskClip:initial\": \"Sets the mask clip to its default value.\",\n  \"maskClip:inherit\": \"Inherits the mask clip value from the parent element.\",\n  \"maskClip:unset\": \"Returns the mask clip to its default value.\",\n  \"maskComposite:add\": \"Adds the mask image to the destination image.\",\n  \"maskComposite:subtract\":\n    \"Subtracts the mask image from the destination image.\",\n  \"maskComposite:intersect\":\n    \"Intersects the mask image with the destination image.\",\n  \"maskComposite:exclude\":\n    \"The source and mask images are combined by excluding the overlapping parts of the mask.\",\n  \"maskComposite:initial\":\n    \"Sets the mask composite property to its default value.\",\n  \"maskComposite:inherit\":\n    \"Inherits the mask composite property from a parent element.\",\n  \"maskComposite:unset\":\n    \"Resets the mask composite property to its inherited value, or to its initial value if it has no inherited value.\",\n  \"maskImage:none\": \"No mask image is displayed.\",\n  \"maskImage:initial\": \"Sets the mask image property to its default value.\",\n  \"maskImage:inherit\":\n    \"Inherits the mask image property from a parent element.\",\n  \"maskImage:unset\":\n    \"Resets the mask image property to its inherited value, or to its initial value if it has no inherited value.\",\n  \"maskMode:alpha\": \"The mask is treated as an alpha mask.\",\n  \"maskMode:luminance\": \"The mask is treated as a luminance mask.\",\n  \"maskMode:match-source\": \"The mask is treated as a luminance+alpha mask.\",\n  \"maskMode:initial\": \"Sets the mask mode property to its default value.\",\n  \"maskMode:inherit\": \"Inherits the mask mode property from a parent element.\",\n  \"maskMode:unset\":\n    \"Resets the mask mode property to its inherited value, or to its initial value if it has no inherited value.\",\n  \"maskOrigin:border-box\":\n    \"The mask is positioned relative to the border box of the element.\",\n  \"maskOrigin:padding-box\":\n    \"The mask is positioned relative to the padding box of the element.\",\n  \"maskOrigin:content-box\":\n    \"The image of the mask is positioned relative to the content box.\",\n  \"maskOrigin:margin-box\":\n    \"The image of the mask is positioned relative to the margin box.\",\n  \"maskOrigin:fill-box\":\n    \"The image of the mask is positioned relative to the fill box.\",\n  \"maskOrigin:stroke-box\":\n    \"The image of the mask is positioned relative to the stroke box.\",\n  \"maskOrigin:view-box\":\n    \"The image of the mask is positioned relative to the viewBox of the referenced SVG.\",\n  \"maskOrigin:initial\": \"Sets the mask-origin property to its default value.\",\n  \"maskOrigin:inherit\":\n    \"Inherits the mask-origin property from its parent element.\",\n  \"maskOrigin:unset\": \"Resets the mask-origin property to its inherited value.\",\n  \"maskPosition:left\": \"Positions the mask image horizontally to the left.\",\n  \"maskPosition:center\": \"Positions the mask image horizontally to the center.\",\n  \"maskPosition:right\": \"Positions the mask image horizontally to the right.\",\n  \"maskPosition:top\": \"Positions the mask image vertically to the top.\",\n  \"maskPosition:bottom\": \"Positions the mask image vertically to the bottom.\",\n  \"maskPosition:initial\":\n    \"Sets the mask-position property to its default value.\",\n  \"maskPosition:inherit\":\n    \"Inherits the mask-position property from its parent element.\",\n  \"maskPosition:unset\":\n    \"Resets the mask-position property to its inherited value.\",\n  \"maskRepeat:repeat-x\": \"Repeats the mask image horizontally.\",\n  \"maskRepeat:repeat-y\": \"Repeats the mask image vertically.\",\n  \"maskRepeat:repeat\": \"Repeats the mask image in both directions.\",\n  \"maskRepeat:space\":\n    \"Scales the mask image to fill space between repetitions.\",\n  \"maskRepeat:round\":\n    \"Scales the mask image to fit the space between repetitions.\",\n  \"maskRepeat:no-repeat\": \"Displays the mask image only once.\",\n  \"maskRepeat:initial\": \"Sets the mask repeat property to default value.\",\n  \"maskRepeat:inherit\":\n    \"Inherits the mask repeat property from the parent element.\",\n  \"maskRepeat:unset\":\n    \"Resets the mask repeat property to its inherited value, if any, or initial value.\",\n  \"maskSize:auto\": \"Sets the size of the mask image to its original size.\",\n  \"maskSize:cover\": \"Scales the mask image to cover the entire element.\",\n  \"maskSize:contain\": \"Scales the mask image to fit within the element.\",\n  \"maskSize:initial\": \"Sets the mask size property to default value.\",\n  \"maskSize:inherit\":\n    \"Inherits the mask size property from the parent element.\",\n  \"maskSize:unset\":\n    \"Resets the mask size property to its inherited value, if any, or initial value.\",\n  \"maskType:luminance\":\n    \"Defines the luminance threshold used for an SVG mask image.\",\n  \"maskType:alpha\": \"Applies an alpha mask to the element.\",\n  \"maskType:initial\": \"Sets the mask type to its default value.\",\n  \"maskType:inherit\": \"Inherits the mask type from the parent element.\",\n  \"maskType:unset\": \"Resets the mask type to its inherited value.\",\n  \"masonryAutoFlow:pack\": \"Places items in the grid as tightly as possible.\",\n  \"masonryAutoFlow:next\":\n    \"Places items in the next available position in the grid.\",\n  \"masonryAutoFlow:definite-first\":\n    \"Places items in their source order until space runs out.\",\n  \"masonryAutoFlow:ordered\":\n    \"Sorts and places items in the grid as defined in the source order.\",\n  \"masonryAutoFlow:initial\": \"Sets the flow to its default value.\",\n  \"masonryAutoFlow:inherit\": \"Inherits the flow from the parent element.\",\n  \"masonryAutoFlow:unset\": \"Resets the flow to its inherited value.\",\n  \"mathDepth:auto-add\":\n    \"Automatically adds a math script element to the document body if the depth is exceeded.\",\n  \"mathDepth:initial\": \"Sets the math depth to its default value.\",\n  \"mathDepth:inherit\": \"Inherits the math depth from the parent element.\",\n  \"mathDepth:unset\": \"Resets the math depth to its inherited value.\",\n  \"mathShift:normal\": \"Resets the math shift to its default value.\",\n  \"mathShift:compact\":\n    \"Reduces the amount of whitespace between math elements.\",\n  \"mathShift:initial\": \"Sets the math shift to its default value.\",\n  \"mathShift:inherit\":\n    \"Sets the math shift to the same value as its parent element.\",\n  \"mathShift:unset\":\n    \"Sets the math shift to its inherited value if it exists, otherwise its initial value.\",\n  \"mathStyle:normal\":\n    \"Sets the rendering style to normal, which is typically upright.\",\n  \"mathStyle:compact\":\n    \"Sets the rendering style to a more compact, condensed version.\",\n  \"mathStyle:initial\": \"Sets the math style to its default value.\",\n  \"mathStyle:inherit\":\n    \"Sets the math style to the same value as its parent element.\",\n  \"mathStyle:unset\":\n    \"Sets the math style to its inherited value if it exists, otherwise its initial value.\",\n  \"maxBlockSize:none\": \"Sets no maximum height for the block element.\",\n  \"maxBlockSize:min-content\":\n    \"Sets the maximum height to the minimum content height.\",\n  \"maxBlockSize:max-content\":\n    \"Sets the maximum height to the maximum content height.\",\n  \"maxBlockSize:fit-content\":\n    \"Sets the maximum height to fit within the available space.\",\n  \"maxBlockSize:initial\": \"Sets the maximum height to its default value.\",\n  \"maxBlockSize:inherit\":\n    \"Sets the maximum height to the same value as its parent element.\",\n  \"maxBlockSize:unset\":\n    \"Sets the maximum height to its inherited value if it exists, otherwise its initial value.\",\n  \"maxHeight:none\": \"No limit on the maximum height.\",\n  \"maxHeight:min-content\":\n    \"Maximum height is the smallest possible height to show content.\",\n  \"maxHeight:max-content\":\n    \"Maximum height is the maximum height required to show all content.\",\n  \"maxHeight:fit-content\":\n    \"Maximum height is the height required to fit all content, with a max limit if set.\",\n  \"maxHeight:initial\":\n    'Maximum height is the initial value set by the browser (usually \"none\").',\n  \"maxHeight:inherit\": \"Maximum height is inherited from the parent element.\",\n  \"maxHeight:unset\":\n    'Maximum height is unset, which means it behaves as \"initial\" in most cases.',\n  \"maxInlineSize:none\":\n    \"No limit on the maximum size of an inline-level element.\",\n  \"maxInlineSize:min-content\":\n    \"Maximum size is the smallest possible width to show content.\",\n  \"maxInlineSize:max-content\":\n    \"Maximum size is the maximum width required to show all content.\",\n  \"maxInlineSize:fit-content\":\n    \"Maximum size is the width required to fit all content, with a max limit if set.\",\n  \"maxInlineSize:initial\":\n    'Maximum size is the initial value set by the browser (usually \"auto\").',\n  \"maxInlineSize:inherit\": \"Maximum size is inherited from the parent element.\",\n  \"maxInlineSize:unset\":\n    'Maximum size is unset, which means it behaves as \"initial\" in most cases.',\n  \"maxLines:none\": \"No limit on the number of lines an element can have.\",\n  \"maxLines:initial\":\n    'The number of lines is the initial value set by the browser (usually \"none\").',\n  \"maxLines:inherit\": \"Inherits max-lines value from the parent element.\",\n  \"maxLines:unset\": \"Allows the max-lines value to be inherited or cascaded.\",\n  \"maxWidth:none\": \"Element has no maximum width limit.\",\n  \"maxWidth:min-content\":\n    \"Element's width is determined by its smallest-sized content.\",\n  \"maxWidth:max-content\":\n    \"Element's width is determined by its largest-sized content.\",\n  \"maxWidth:fit-content\":\n    \"Element's width is determined by wrapping its content.\",\n  \"maxWidth:initial\": \"Sets the initial value of max-width property.\",\n  \"maxWidth:inherit\": \"Inherits the max-width value from the parent element.\",\n  \"maxWidth:unset\": \"Allows the max-width value to be inherited or cascaded.\",\n  \"minBlockSize:auto\": \"Element's block size is determined by its content.\",\n  \"minBlockSize:min-content\":\n    \"Element's block size is determined by its smallest-sized content.\",\n  \"minBlockSize:max-content\":\n    \"Element's block size is determined by its largest-sized content.\",\n  \"minBlockSize:fit-content\":\n    \"Element's block size is determined by wrapping its content.\",\n  \"minBlockSize:initial\": \"Sets the initial value of min-block-size property.\",\n  \"minBlockSize:inherit\":\n    \"Inherits the min-block-size value from the parent element.\",\n  \"minBlockSize:unset\":\n    \"Allows the min-block-size value to be inherited or cascaded.\",\n  \"minHeight:auto\":\n    \"The element's minimum height is determined by its content.\",\n  \"minHeight:min-content\":\n    \"The element's minimum height is set to the smallest height required by its content.\",\n  \"minHeight:max-content\":\n    \"The element's minimum height is set to the largest height required by its content.\",\n  \"minHeight:fit-content\":\n    \"The element's minimum height is set to the height required by its content, but limited by the specified maximum height.\",\n  \"minHeight:initial\":\n    \"The element's minimum height is set to its default value.\",\n  \"minHeight:inherit\":\n    \"The element's minimum height is inherited from its parent element.\",\n  \"minHeight:unset\":\n    \"The element's minimum height is set to the value of its parent element if it has one, otherwise it's set to its initial value.\",\n  \"minInlineSize:auto\":\n    \"The element's minimum inline size is determined by its content.\",\n  \"minInlineSize:min-content\":\n    \"The element's minimum inline size is set to the smallest width required by its content.\",\n  \"minInlineSize:max-content\":\n    \"The element's minimum inline size is set to the largest width required by its content.\",\n  \"minInlineSize:fit-content\":\n    \"The element's minimum inline size is set to the width required by its content, but limited by the specified maximum width.\",\n  \"minInlineSize:initial\":\n    \"The element's minimum inline size is set to its default value.\",\n  \"minInlineSize:inherit\":\n    \"The element's minimum inline size is inherited from its parent element.\",\n  \"minInlineSize:unset\":\n    \"The element's minimum inline size is set to the value of its parent element if it has one, otherwise it's set to its initial value.\",\n  \"minWidth:auto\": \"The element's minimum width is determined by its content.\",\n  \"minWidth:min-content\":\n    \"The element's minimum width is set to the smallest width required by its content.\",\n  \"minWidth:max-content\":\n    \"The minimum width is set to the intrinsic width of the content.\",\n  \"minWidth:fit-content\":\n    \"The minimum width is set to the fit-content of the element.\",\n  \"minWidth:initial\":\n    \"The minimum width is set to the default value defined by the browser.\",\n  \"minWidth:inherit\": \"The minimum width is inherited from the parent element.\",\n  \"minWidth:unset\":\n    \"The minimum width is set to the default value or inherited from the parent element.\",\n  \"mixBlendMode:normal\": \"The element is displayed with normal blending mode.\",\n  \"mixBlendMode:multiply\":\n    \"The element is displayed with multiply blending mode.\",\n  \"mixBlendMode:screen\": \"The element is displayed with screen blending mode.\",\n  \"mixBlendMode:overlay\":\n    \"The element is displayed with overlay blending mode.\",\n  \"mixBlendMode:darken\": \"The element is displayed with darken blending mode.\",\n  \"mixBlendMode:lighten\":\n    \"The element is displayed with lighten blending mode.\",\n  \"mixBlendMode:color-dodge\":\n    \"The element is displayed with color-dodge blending mode.\",\n  \"mixBlendMode:color-burn\":\n    \"The element is displayed with color-burn blending mode.\",\n  \"mixBlendMode:hard-light\":\n    \"The element is displayed with hard-light blending mode.\",\n  \"mixBlendMode:soft-light\":\n    \"The element is displayed with soft-light blending mode.\",\n  \"mixBlendMode:difference\":\n    \"The element is displayed with difference blending mode.\",\n  \"mixBlendMode:exclusion\": \"Blends elements by excluding the colors.\",\n  \"mixBlendMode:hue\": \"Blends elements by keeping the hue of the top layer.\",\n  \"mixBlendMode:saturation\":\n    \"Blends elements by keeping the saturation of the top layer.\",\n  \"mixBlendMode:color\":\n    \"Blends elements by keeping the color of the top layer.\",\n  \"mixBlendMode:luminosity\":\n    \"Blends elements by keeping the luminosity of the top layer.\",\n  \"mixBlendMode:plus-lighter\": \"Blends elements by adding the colors.\",\n  \"mixBlendMode:initial\": \"Sets the mix-blend-mode to its default value.\",\n  \"mixBlendMode:inherit\":\n    \"Inherits the mix-blend-mode from the parent element.\",\n  \"mixBlendMode:unset\": \"Resets the mix-blend-mode to its inherited value.\",\n  \"objectFit:fill\": \"Resizes the element to fill the container.\",\n  \"objectFit:contain\":\n    \"Resizes the element to fit within the container while preserving its aspect ratio.\",\n  \"objectFit:cover\":\n    \"Resizes the element to cover the container while preserving its aspect ratio.\",\n  \"objectFit:none\":\n    \"Resizes the element without regard to the container or its aspect ratio.\",\n  \"objectFit:scale-down\":\n    \"Resizes the element to the smaller of its natural size and the container.\",\n  \"objectFit:initial\": \"Sets the object-fit to its default value.\",\n  \"objectFit:inherit\": \"Inherits the object-fit from the parent element.\",\n  \"objectFit:unset\":\n    \"object-fit: unset - The replaced element retains its intrinsic size and aspect ratio.\",\n  \"objectPosition:left\":\n    \"object-position: left - Position the replaced element in the left.\",\n  \"objectPosition:center\":\n    \"object-position: center - Position the replaced element in the center.\",\n  \"objectPosition:right\":\n    \"object-position: right - Position the replaced element in the right.\",\n  \"objectPosition:top\":\n    \"object-position: top - Position the replaced element in the top.\",\n  \"objectPosition:bottom\":\n    \"object-position: bottom - Position the replaced element in the bottom.\",\n  \"objectPosition:initial\":\n    \"object-position: initial - Sets the object-position to its default value.\",\n  \"objectPosition:inherit\":\n    \"object-position: inherit - Inherits the object-position from its parent element.\",\n  \"objectPosition:unset\":\n    \"object-position: unset - Inherits the object-position from its parent element, or its own default value.\",\n  \"offsetAnchor:auto\":\n    \"offset-anchor: auto - The object is placed at a specified offset from a specific point on the region.\",\n  \"offsetAnchor:left\":\n    \"offset-anchor: left - The offset is calculated relative to the left edge of the region.\",\n  \"offsetAnchor:center\":\n    \"offset-anchor: center - The offset is calculated relative to the center of the region.\",\n  \"offsetAnchor:right\":\n    \"offset-anchor: right - The offset is calculated relative to the right edge of the region.\",\n  \"offsetAnchor:top\":\n    \"offset-anchor: top - The offset is calculated relative to the top edge of the region.\",\n  \"offsetAnchor:bottom\":\n    \"offset-anchor: bottom - The offset is calculated relative to the bottom edge of the region.\",\n  \"offsetAnchor:initial\":\n    \"offset-anchor: initial - Sets the property to its default value.\",\n  \"offsetAnchor:inherit\":\n    \"Sets the anchor point to the computed value of the parent.\",\n  \"offsetAnchor:unset\": \"Resets the anchor point to the initial value.\",\n  \"offsetDistance:initial\": \"Sets the distance to the initial value.\",\n  \"offsetDistance:inherit\":\n    \"Sets the distance to the computed value of the parent.\",\n  \"offsetDistance:unset\": \"Resets the distance to the initial value.\",\n  \"offsetPath:none\": \"Disables any transformation.\",\n  \"offsetPath:border-box\":\n    \"Applies the transformation to the border box of the element.\",\n  \"offsetPath:padding-box\":\n    \"Applies the transformation to the padding box of the element.\",\n  \"offsetPath:content-box\":\n    \"Applies the transformation to the content box of the element.\",\n  \"offsetPath:margin-box\":\n    \"Applies the transformation to the margin box of the element.\",\n  \"offsetPath:fill-box\":\n    \"Applies the transformation to the fill box of the element.\",\n  \"offsetPath:stroke-box\":\n    \"Applies the transformation to the stroke box of the element.\",\n  \"offsetPath:view-box\":\n    \"Applies the transformation to the nearest SVG container.\",\n  \"offsetPath:initial\": \"Sets the path to the initial value.\",\n  \"offsetPath:inherit\": \"Sets the path to the computed value of the parent.\",\n  \"offsetPath:unset\": \"Resets the path to the initial value.\",\n  \"offsetPosition:auto\":\n    \"The element is placed according to normal inline/block layout.\",\n  \"offsetPosition:left\":\n    \"The element is positioned to the left of the container.\",\n  \"offsetPosition:center\":\n    \"The element is positioned in the center of the container.\",\n  \"offsetPosition:right\":\n    \"The element is positioned to the right of the container.\",\n  \"offsetPosition:top\":\n    \"The element is positioned to the top of the container.\",\n  \"offsetPosition:bottom\":\n    \"The element is positioned to the bottom of the container.\",\n  \"offsetPosition:initial\": \"The property to its default value.\",\n  \"offsetPosition:inherit\":\n    \"The property is inherited from the parent element.\",\n  \"offsetPosition:unset\":\n    \"The property acts as either inherit or initial depending on the presence of a parent rule.\",\n  \"offsetRotate:auto\": \"The element is placed in its default rotation.\",\n  \"offsetRotate:reverse\": \"The element is placed in reverse order.\",\n  \"offsetRotate:initial\": \"The property to its default value.\",\n  \"offsetRotate:inherit\": \"The property is inherited from the parent element.\",\n  \"offsetRotate:unset\":\n    \"The property acts as either inherit or initial depending on the presence of a parent rule.\",\n  \"opacity:initial\": \"The element is transparent.\",\n  \"opacity:inherit\": \"The property is inherited from the parent element.\",\n  \"opacity:unset\": \"Sets the opacity to its initial value (1).\",\n  \"order:initial\": \"Sets the order of the element to its default value (0).\",\n  \"order:inherit\":\n    \"Sets the order of the element to be the same as its parent.\",\n  \"order:unset\":\n    \"Sets the order of the element to its inherited value or 0 if none.\",\n  \"orphans:initial\":\n    \"Sets the minimum number of lines in a block container that must be left in a single page to its initial value (2).\",\n  \"orphans:inherit\":\n    \"Inherits the minimum number of lines in a block container that must be left in a single page from its parent.\",\n  \"orphans:unset\":\n    \"Sets the minimum number of lines in a block container that must be left in a single page to its inherited value or 2 if none.\",\n  \"outlineOffset:initial\":\n    \"Sets the space between an outline and the edge of its containing block to its initial value (0).\",\n  \"outlineOffset:inherit\":\n    \"Inherits the space between an outline and the edge of its containing block from its parent.\",\n  \"outlineOffset:unset\":\n    \"Sets the space between an outline and the edge of its containing block to its inherited value or 0 if none.\",\n  \"outlineStyle:auto\":\n    \"Sets the style of the outline to a style determined by the browser.\",\n  \"outlineStyle:none\": \"Removes the outline of an element.\",\n  \"outlineStyle:hidden\": \"Same as none except for accessibility purposes.\",\n  \"outlineStyle:dotted\": \"Sets the style of the outline to a series of dots.\",\n  \"outlineStyle:dashed\": \"Sets the style of the outline to a series of dashes.\",\n  \"outlineStyle:solid\": \"Sets the style of the outline to a solid line.\",\n  \"outlineStyle:double\": \"Creates a double line outline.\",\n  \"outlineStyle:groove\": \"Creates a 3D engraved outline.\",\n  \"outlineStyle:ridge\": \"Creates a 3D embossed outline.\",\n  \"outlineStyle:inset\": \"Creates an inset style outline.\",\n  \"outlineStyle:outset\": \"Creates an outset style outline.\",\n  \"outlineStyle:initial\": \"Sets the outline style to default.\",\n  \"outlineStyle:inherit\": \"Inherits the outline style from the parent element.\",\n  \"outlineStyle:unset\": \"Resets the outline style to its inherited value.\",\n  \"outlineWidth:thin\": \"Creates a thin outline.\",\n  \"outlineWidth:medium\": \"Creates a medium width outline.\",\n  \"outlineWidth:thick\": \"Creates a thick outline.\",\n  \"outlineWidth:initial\": \"Sets the outline width to default.\",\n  \"outlineWidth:inherit\": \"Inherits the outline width from the parent element.\",\n  \"outlineWidth:unset\": \"Resets the outline width to its inherited value.\",\n  \"overflow:visible\": \"Content overflows the element box.\",\n  \"overflow:hidden\": \"Hides the content that overflows the element box.\",\n  \"overflow:clip\": \"Clips the content with no scrollbars.\",\n  \"overflow:scroll\": \"Adds scrollbars if content overflows.\",\n  \"overflow:auto\": \"Adds scrollbars only if content overflows.\",\n  \"overflow:initial\": \"Sets the default value for an element.\",\n  \"overflow:inherit\": \"Inherits overflow value from parent element.\",\n  \"overflow:unset\": \"Sets the value to its natural behavior.\",\n  \"overflowAnchor:auto\": \"Automatically chooses the best anchor point.\",\n  \"overflowAnchor:none\": \"No anchor point is used.\",\n  \"overflowAnchor:initial\": \"Sets the default value for an element.\",\n  \"overflowAnchor:inherit\": \"Inherits from the parent element.\",\n  \"overflowAnchor:unset\": \"Sets the value to its natural behavior.\",\n  \"overflowBlock:visible\": \"Lets the content overflow the block.\",\n  \"overflowBlock:hidden\": \"Clips the content at the block's edge.\",\n  \"overflowBlock:clip\": \"Refuses to paint at or outside the block.\",\n  \"overflowBlock:scroll\": \"Adds scrollbars when content overflows.\",\n  \"overflowBlock:auto\": \"Adds scrollbars if content overflows.\",\n  \"overflowBlock:initial\":\n    \"Specifies the default value for the overflow-block property.\",\n  \"overflowBlock:inherit\":\n    \"Specifies that the value of the overflow-block property should be inherited from its parent element.\",\n  \"overflowBlock:unset\":\n    \"Specifies that the value of the overflow-block property should be inherited if possible, or else behave like auto.\",\n  \"overflowClipMargin:content-box\":\n    \"Specifies that the overflow area should not include the element's padding, only its content box.\",\n  \"overflowClipMargin:padding-box\":\n    \"Specifies that the overflow area should include the element's padding box, but not its border.\",\n  \"overflowClipMargin:border-box\":\n    \"Specifies that the overflow area should include the element's entire box, including its border.\",\n  \"overflowClipMargin:initial\":\n    \"Specifies the default value for the overflow-clip-margin property.\",\n  \"overflowClipMargin:inherit\":\n    \"Specifies that the value of the overflow-clip-margin property should be inherited from its parent element.\",\n  \"overflowClipMargin:unset\":\n    \"Specifies that the value of the overflow-clip-margin property should be inherited if possible, or else behave like auto.\",\n  \"overflowInline:visible\":\n    \"Specifies that the content is not clipped and may be rendered outside the element's box.\",\n  \"overflowInline:hidden\":\n    \"Specifies that the content is clipped and not visible outside the element's box.\",\n  \"overflowInline:clip\":\n    \"Specifies that the content is clipped and not visible outside the element's box.\",\n  \"overflowInline:scroll\":\n    \"Specifies that the content is clipped and a scrollbar is added to see the rest of the content.\",\n  \"overflowInline:auto\":\n    \"Specifies that the content is clipped if necessary and a scrollbar is added to see the rest of the content.\",\n  \"overflowInline:initial\":\n    \"Specifies the default value for the overflow-inline property.\",\n  \"overflowInline:inherit\":\n    \"Specifies that the value of the overflow-inline property should be inherited from its parent element.\",\n  \"overflowInline:unset\":\n    \"The inline content is not clipped and it flows out of the container.\",\n  \"overflowWrap:normal\":\n    \"Breaks words only when necessary, keeping whole words together.\",\n  \"overflowWrap:break-word\":\n    \"Breaks words at any character, when they exceed the container width.\",\n  \"overflowWrap:anywhere\":\n    \"Allows word breaking at arbitrary points, including within words.\",\n  \"overflowWrap:initial\": \"Sets the default value of a property.\",\n  \"overflowWrap:inherit\": \"Inherits a property from its parent element.\",\n  \"overflowWrap:unset\": \"Resets to its default value.\",\n  \"overflowX:visible\":\n    \"Content is fully visible and extends beyond the container if it exceeds its size.\",\n  \"overflowX:hidden\":\n    \"Content that exceeds the container’s size is hidden without scrollbars. It can be scrolled with JavaScript or via a scroll-timeline animation.\",\n  \"overflowX:clip\":\n    \"Content that exceeds the container’s size is clipped without scrollbars. It can not be scrolled with JavaScript or via a scroll-timeline animation.\",\n  \"overflowX:scroll\":\n    \"Creates a scrolling mechanism for the content outside the box.\",\n  \"overflowX:auto\":\n    \"Scrollbars are added to the container only when necessary, based on the content size.\",\n  \"overflowX:initial\": \"Sets the default value of a property.\",\n  \"overflowX:inherit\": \"Inherits a property from its parent element.\",\n  \"overflowX:unset\": \"Resets to its default value.\",\n  \"overflowY:visible\":\n    \"Content is fully visible and extends beyond the container if it exceeds its size.\",\n  \"overflowY:hidden\":\n    \"Content that exceeds the container’s size is hidden without scrollbars. It can be scrolled with JavaScript or via a scroll-timeline animation.\",\n  \"overflowY:clip\":\n    \"Content that exceeds the container’s size is clipped without scrollbars. It can not be scrolled with JavaScript or via a scroll-timeline animation.\",\n  \"overflowY:scroll\":\n    \"Creates a scrolling mechanism for the content outside the box.\",\n  \"overflowY:auto\":\n    \"Scrollbars are added to the container only when necessary, based on the content size.\",\n  \"overscrollBehavior:contain\":\n    \"Prevents pull-to-refresh and rubber band scrolling outside of the element.\",\n  \"overscrollBehavior:none\":\n    \"Disables all overscrolling of the element's content.\",\n  \"overscrollBehavior:auto\":\n    \"Enables overscrolling of the element's content if there is any.\",\n  \"overscrollBehaviorBlock:contain\":\n    \"Disallows scrolling of the vertical axis of the element, unless it's a scroll container.\",\n  \"overscrollBehaviorBlock:none\":\n    \"Enables rubber-banding of the vertical axis of the element during a paginated sequence.\",\n  \"overscrollBehaviorBlock:auto\":\n    \"Enables rubber-banding of the vertical axis of the element, only if there is any overscroll.\",\n  \"overscrollBehaviorBlock:initial\":\n    \"Default value, scroll of the element is constrained by its parent's scrollable area.\",\n  \"overscrollBehaviorBlock:inherit\":\n    \"The value is inherited from the parent element.\",\n  \"overscrollBehaviorBlock:unset\":\n    \"The value is either `inherit` or `initial` depending on the inherited value.\",\n  \"overscrollBehaviorInline:contain\":\n    \"Scroll chaining is disabled when the scroll reaches an end boundary.\",\n  \"overscrollBehaviorInline:none\":\n    \"Scroll chaining is disabled in inline direction.\",\n  \"overscrollBehaviorInline:auto\":\n    \"Default value, scroll chaining occurs as normal.\",\n  \"overscrollBehaviorInline:initial\":\n    \"Scroll chaining is enabled as normal in inline direction.\",\n  \"overscrollBehaviorInline:inherit\":\n    \"The value is inherited from the parent element.\",\n  \"overscrollBehaviorInline:unset\":\n    \"The value is either `inherit` or `initial` depending on the inherited value.\",\n  \"overscrollBehaviorX:contain\":\n    \"Scroll chaining is disabled when the scroll reaches an end boundary for horizontal direction.\",\n  \"overscrollBehaviorX:none\":\n    \"Scroll chaining is disabled in horizontal direction.\",\n  \"overscrollBehaviorX:auto\":\n    \"Default value, scroll chaining occurs as normal in horizontal direction.\",\n  \"overscrollBehaviorX:initial\":\n    \"Scroll chaining is enabled as normal in horizontal direction.\",\n  \"overscrollBehaviorX:inherit\":\n    \"The value is inherited from the parent element.\",\n  \"overscrollBehaviorX:unset\":\n    \"The value is either `inherit` or `initial` depending on the inherited value.\",\n  \"overscrollBehaviorY:contain\":\n    \"Scroll chaining is disabled when the scroll reaches an end boundary for vertical direction.\",\n  \"overscrollBehaviorY:none\":\n    \"Disables overshooting scrolling behavior on the Y axis.\",\n  \"overscrollBehaviorY:auto\":\n    \"Enables overshooting scrolling behavior on the Y axis.\",\n  \"overscrollBehaviorY:initial\":\n    \"Sets the Y axis overshooting behavior to its default value.\",\n  \"overscrollBehaviorY:inherit\":\n    \"Sets the Y axis overshooting behavior to be the same as its parent.\",\n  \"overscrollBehaviorY:unset\":\n    \"Sets the Y axis overshooting behavior to its inherited value, or default value if there is no inherited value.\",\n  \"paddingBlockEnd:initial\":\n    \"Sets the end padding of a block-level element to its default value.\",\n  \"paddingBlockEnd:inherit\":\n    \"Sets the end padding of a block-level element to be the same as its parent.\",\n  \"paddingBlockEnd:unset\":\n    \"Sets the end padding of a block-level element to its inherited value, or default value if there is no inherited value.\",\n  \"paddingBlockStart:initial\":\n    \"Sets the start padding of a block-level element to its default value.\",\n  \"paddingBlockStart:inherit\":\n    \"Sets the start padding of a block-level element to be the same as its parent.\",\n  \"paddingBlockStart:unset\":\n    \"Sets the start padding of a block-level element to its inherited value, or default value if there is no inherited value.\",\n  \"paddingBottom:initial\":\n    \"Sets the bottom padding of an element to its default value.\",\n  \"paddingBottom:inherit\":\n    \"Sets the bottom padding of an element to be the same as its parent.\",\n  \"paddingBottom:unset\":\n    \"Sets the bottom padding of an element to its inherited value, or default value if there is no inherited value.\",\n  \"paddingInlineEnd:initial\":\n    \"Sets the end padding of an inline-level element to its default value.\",\n  \"paddingInlineEnd:inherit\":\n    \"Sets the end padding of an inline-level element to be the same as its parent.\",\n  \"paddingInlineEnd:unset\":\n    \"Removes all padding on the end side of an inline element.\",\n  \"paddingInlineStart:initial\":\n    \"Sets the padding on the start side of an inline element to its default value.\",\n  \"paddingInlineStart:inherit\":\n    \"Sets the padding on the start side of an inline element to the computed value of its parent.\",\n  \"paddingInlineStart:unset\":\n    \"Removes all padding on the start side of an inline element.\",\n  \"paddingLeft:initial\":\n    \"Sets the left padding of an element to its default value.\",\n  \"paddingLeft:inherit\":\n    \"Sets the left padding of an element to the computed value of its parent.\",\n  \"paddingLeft:unset\": \"Removes the left padding of an element.\",\n  \"paddingRight:initial\":\n    \"Sets the right padding of an element to its default value.\",\n  \"paddingRight:inherit\":\n    \"Sets the right padding of an element to the computed value of its parent.\",\n  \"paddingRight:unset\": \"Removes the right padding of an element.\",\n  \"paddingTop:initial\":\n    \"Sets the top padding of an element to its default value.\",\n  \"paddingTop:inherit\":\n    \"Sets the top padding of an element to the computed value of its parent.\",\n  \"paddingTop:unset\": \"Removes the top padding of an element.\",\n  \"pageBreakAfter:auto\": \"Adds a page break after an element if necessary.\",\n  \"pageBreakAfter:always\": \"Adds a page break after an element always.\",\n  \"pageBreakAfter:avoid\": \"Avoids a page break after an element if possible.\",\n  \"pageBreakAfter:left\":\n    \"Forces a page break after the element, placing the next page left.\",\n  \"pageBreakAfter:right\":\n    \"Forces a page break after the element, placing the next page right.\",\n  \"pageBreakAfter:recto\":\n    \"Forces a page break after the element, placing the next page on a right-hand page.\",\n  \"pageBreakAfter:verso\":\n    \"Forces a page break after the element, placing the next page on a left-hand page.\",\n  \"pageBreakAfter:initial\": \"Sets the property to its default value `auto`.\",\n  \"pageBreakAfter:inherit\": \"Inherits the property from its parent element.\",\n  \"pageBreakAfter:unset\": \"Resets the property to its natural value.\",\n  \"pageBreakBefore:auto\":\n    \"Determines if a page break should occur before the element.\",\n  \"pageBreakBefore:always\": \"Forces a page break before the element.\",\n  \"pageBreakBefore:avoid\": \"Avoids a page break before the element.\",\n  \"pageBreakBefore:left\":\n    \"Forces a page break before the element, placing the next page left.\",\n  \"pageBreakBefore:right\":\n    \"Forces a page break before the element, placing the next page right.\",\n  \"pageBreakBefore:recto\":\n    \"Forces a page break before the element, placing the next page on a right-hand page.\",\n  \"pageBreakBefore:verso\":\n    \"Forces a page break before the element, placing the next page on a left-hand page.\",\n  \"pageBreakBefore:initial\": \"Sets the property to its default value `auto`.\",\n  \"pageBreakBefore:inherit\": \"Inherits the property from its parent element.\",\n  \"pageBreakInside:auto\": \"Allows breaks inside the element if necessary.\",\n  \"pageBreakInside:avoid\": \"Avoids breaks inside the element if possible.\",\n  \"pageBreakInside:initial\": \"Sets the default value.\",\n  \"pageBreakInside:inherit\": \"Inherits the property from its parent element.\",\n  \"pageBreakInside:unset\": \"Resets to its default value.\",\n  \"paintOrder:normal\": \"Renders elements in default order.\",\n  \"paintOrder:fill\": \"Renders fills before strokes and markers.\",\n  \"paintOrder:stroke\": \"Renders strokes before fills and markers.\",\n  \"paintOrder:markers\":\n    \"Renders markers after filling and stroking, regardless of their order in the code.\",\n  \"paintOrder:initial\": \"Sets the default value.\",\n  \"paintOrder:inherit\": \"Inherits the property from its parent element.\",\n  \"paintOrder:unset\": \"Resets to its default value.\",\n  \"perspective:none\": \"Disables the perspective.\",\n  \"perspective:initial\": \"Sets the default value.\",\n  \"perspective:inherit\": \"Inherits the property from its parent element.\",\n  \"perspective:unset\": \"perspective: unset - Resets the perspective property.\",\n  \"perspectiveOrigin:left\":\n    \"perspective-origin: left - Defines the position of the observer.\",\n  \"perspectiveOrigin:center\":\n    \"perspective-origin: center - Defines the position of the observer.\",\n  \"perspectiveOrigin:right\":\n    \"perspective-origin: right - Defines the position of the observer.\",\n  \"perspectiveOrigin:top\":\n    \"perspective-origin: top - Defines the position of the observer.\",\n  \"perspectiveOrigin:bottom\":\n    \"perspective-origin: bottom - Defines the position of the observer.\",\n  \"perspectiveOrigin:initial\":\n    \"perspective-origin: initial - Sets the initial value of the property.\",\n  \"perspectiveOrigin:inherit\":\n    \"perspective-origin: inherit - Inherits the property from its parent.\",\n  \"perspectiveOrigin:unset\":\n    \"perspective-origin: unset - Resets the property to its default value.\",\n  \"pointerEvents:auto\":\n    \"pointer-events: auto - Specifes that the element should behave as usual.\",\n  \"pointerEvents:none\":\n    \"pointer-events: none - Disables all pointer events on the element.\",\n  \"pointerEvents:visiblePainted\":\n    \"pointer-events: visiblePainted - Allows events on visible painted areas.\",\n  \"pointerEvents:visibleFill\":\n    \"pointer-events: visibleFill - Allows events on visible filled areas.\",\n  \"pointerEvents:visibleStroke\":\n    \"pointer-events: visibleStroke - Allows events on visible stroked areas.\",\n  \"pointerEvents:visible\":\n    \"pointer-events: visible - Allows events on visible areas.\",\n  \"pointerEvents:painted\":\n    \"pointer-events: painted - Allows events on painted areas.\",\n  \"pointerEvents:fill\": \"Only the fill area of the element is clickable\",\n  \"pointerEvents:stroke\": \"Only the stroke area of the element is clickable\",\n  \"pointerEvents:all\": \"The element and its contents are clickable\",\n  \"pointerEvents:initial\": \"The pointer behavior is inherited from the parent\",\n  \"pointerEvents:inherit\": \"The pointer behavior is inherited from the parent\",\n  \"pointerEvents:unset\": \"The pointer behavior is the default\",\n  \"position:static\": \"The element is positioned according to normal flows\",\n  \"position:relative\":\n    \"The element is positioned relative to its normal position\",\n  \"position:absolute\":\n    \"The element is positioned relative to its first positioned ancestor\",\n  \"position:sticky\":\n    \"The element is positioned relative to the closest scrolling ancestor\",\n  \"position:fixed\": \"The element is positioned relative to the browser window\",\n  \"position:initial\": \"The position property is set to its default value\",\n  \"position:inherit\":\n    \"The position property is inherited from the parent element\",\n  \"position:unset\": \"The position property is set to the default value\",\n  \"quotes:none\": \"No quotes are added to the content\",\n  \"quotes:auto\": \"Quotes are added to the content automatically\",\n  \"quotes:initial\":\n    \"Sets the quotes for embedded content marks to their initial value.\",\n  \"quotes:inherit\":\n    \"Inherits quotes for embedded content marks from the parent element.\",\n  \"quotes:unset\": \"Removes any inherited quotes for embedded content marks.\",\n  \"resize:none\": \"Prevents resizing of the element in any direction.\",\n  \"resize:both\":\n    \"Allows resizing of the element both horizontally and vertically.\",\n  \"resize:horizontal\": \"Allows horizontal resizing of the element only.\",\n  \"resize:vertical\": \"Allows vertical resizing of the element only.\",\n  \"resize:block\": \"Allows resizing of the element as a block box.\",\n  \"resize:inline\": \"Allows resizing of the element as an inline box.\",\n  \"resize:initial\": \"Sets resizing for the element to its initial value.\",\n  \"resize:inherit\":\n    \"Inherits resizing for the element from the parent element.\",\n  \"resize:unset\": \"Removes any inherited resizing for the element.\",\n  \"right:auto\": \"Aligns the element to the right of its containing element.\",\n  \"right:initial\":\n    \"Sets the right position of the element to its initial value.\",\n  \"right:inherit\":\n    \"Inherits the right position of the element from the parent element.\",\n  \"right:unset\": \"Removes any inherited right position for the element.\",\n  \"rotate:none\": \"Element stays in its initial position.\",\n  \"rotate:x\": \"Element is rotated along the x-axis.\",\n  \"rotate:y\": \"Element is rotated along the y-axis.\",\n  \"rotate:z\": \"Element is rotated along the z-axis.\",\n  \"rotate:initial\": \"Element is not rotated.\",\n  \"rotate:inherit\": \"Inherits the rotation value from the parent element.\",\n  \"rotate:unset\": \"Element uses the browser default value.\",\n  \"rowGap:normal\": \"Default gap between rows in a grid.\",\n  \"rowGap:initial\": \"Reset to the default gap between rows.\",\n  \"rowGap:inherit\": \"Gets the gap value from the parent element.\",\n  \"rowGap:unset\": \"Uses the browser default gap between rows.\",\n  \"rubyAlign:start\": \"Aligns ruby text to the start of each base element.\",\n  \"rubyAlign:center\": \"Centers ruby text over each base element.\",\n  \"rubyAlign:space-between\":\n    \"Spaces ruby text evenly between each base element.\",\n  \"rubyAlign:space-around\": \"Spaces ruby text evenly around each base element.\",\n  \"rubyAlign:initial\": \"Resets ruby text alignment to the default value.\",\n  \"rubyAlign:inherit\":\n    \"The ruby alignment is inherited from the parent element\",\n  \"rubyAlign:unset\": \"The ruby alignment is determined by normal CSS rules\",\n  \"rubyMerge:separate\": \"The ruby text is separate from surrounding text\",\n  \"rubyMerge:collapse\": \"The ruby text is merged with surrounding text\",\n  \"rubyMerge:auto\": \"The ruby text is merged if the browser supports it\",\n  \"rubyMerge:initial\": \"The ruby text is separate from surrounding text\",\n  \"rubyMerge:inherit\":\n    \"The ruby text merge value is inherited from the parent element\",\n  \"rubyMerge:unset\": \"The ruby text merge is determined by normal CSS rules\",\n  \"rubyPosition:alternate\": \"Alternate ruby text above and below base text\",\n  \"rubyPosition:over\":\n    \"Vertical ruby text above base text, centered relative to it\",\n  \"rubyPosition:under\": \"Ruby text under base text\",\n  \"rubyPosition:inter-character\":\n    \"Ruby text as annotations to specific characters\",\n  \"rubyPosition:initial\": \"The ruby position is above base text\",\n  \"rubyPosition:inherit\":\n    \"The ruby position is inherited from the parent element\",\n  \"rubyPosition:unset\": \"The ruby position is determined by normal CSS rules\",\n  \"scale:none\": \"The element is not resized\",\n  \"scale:initial\": \"Scale is set to its initial value.\",\n  \"scale:inherit\": \"Scale is inherited from its parent element.\",\n  \"scale:unset\": \"Scale is either inherited or set to its initial value.\",\n  \"scrollbarGutter:auto\": \"Gutter appears when necessary.\",\n  \"scrollbarGutter:stable\": \"Gutter is always visible.\",\n  \"scrollbarGutter:both-edges\": \"Gutter is placed on both edges.\",\n  \"scrollbarGutter:initial\": \"Gutter adopts default behavior.\",\n  \"scrollbarGutter:inherit\": \"Gutter inherits value from parent.\",\n  \"scrollbarGutter:unset\":\n    \"Gutter is either inherited or adopts default behavior.\",\n  \"scrollbarWidth:auto\": \"Width varies by browser or device.\",\n  \"scrollbarWidth:thin\": \"Width is set to a thin value.\",\n  \"scrollbarWidth:none\": \"Width is set to zero.\",\n  \"scrollbarWidth:initial\": \"Width adopts default behavior.\",\n  \"scrollbarWidth:inherit\": \"Width inherits value from parent.\",\n  \"scrollbarWidth:unset\":\n    \"Width is either inherited or adopts default behavior.\",\n  \"scrollBehavior:auto\": \"Scroll animation is browser-dependent.\",\n  \"scrollBehavior:smooth\": \"Enables a smooth scrolling effect.\",\n  \"scrollBehavior:initial\": \"Sets scrolling behavior to default.\",\n  \"scrollBehavior:inherit\": \"Inherits scrolling behavior from parent element.\",\n  \"scrollBehavior:unset\":\n    \"Sets scrolling behavior to default or inherited value.\",\n  \"scrollMarginBlockStart:initial\": \"Sets block-start margin to default.\",\n  \"scrollMarginBlockStart:inherit\":\n    \"Inherits block-start margin from parent element.\",\n  \"scrollMarginBlockStart:unset\":\n    \"Sets block-start margin to default or inherited value.\",\n  \"scrollMarginBlockEnd:initial\": \"Sets block-end margin to default.\",\n  \"scrollMarginBlockEnd:inherit\":\n    \"Inherits block-end margin from parent element.\",\n  \"scrollMarginBlockEnd:unset\":\n    \"Sets block-end margin to default or inherited value.\",\n  \"scrollMarginBottom:initial\": \"Sets bottom margin to default.\",\n  \"scrollMarginBottom:inherit\": \"Inherits bottom margin from parent element.\",\n  \"scrollMarginBottom:unset\":\n    \"Sets bottom margin to default or inherited value.\",\n  \"scrollMarginInlineStart:initial\": \"Sets inline-start margin to default.\",\n  \"scrollMarginInlineStart:inherit\":\n    \"Inherits inline-start margin from parent element.\",\n  \"scrollMarginInlineStart:unset\":\n    \"Sets inline-start margin to default or inherited value.\",\n  \"scrollMarginInlineEnd:initial\":\n    \"Sets inline end margin to its default value.\",\n  \"scrollMarginInlineEnd:inherit\":\n    \"Inherits inline end margin from its parent element.\",\n  \"scrollMarginInlineEnd:unset\":\n    \"Resets inline end margin to its inherited value.\",\n  \"scrollMarginLeft:initial\": \"Sets left margin to its default value.\",\n  \"scrollMarginLeft:inherit\": \"Inherits left margin from its parent element.\",\n  \"scrollMarginLeft:unset\": \"Resets left margin to its inherited value.\",\n  \"scrollMarginRight:initial\": \"Sets right margin to its default value.\",\n  \"scrollMarginRight:inherit\": \"Inherits right margin from its parent element.\",\n  \"scrollMarginRight:unset\": \"Resets right margin to its inherited value.\",\n  \"scrollMarginTop:initial\": \"Sets top margin to its default value.\",\n  \"scrollMarginTop:inherit\": \"Inherits top margin from its parent element.\",\n  \"scrollMarginTop:unset\": \"Resets top margin to its inherited value.\",\n  \"scrollPaddingBlockStart:auto\": 'Sets the block start padding to \"auto\".',\n  \"scrollPaddingBlockStart:initial\":\n    \"Sets the block start padding to its default value.\",\n  \"scrollPaddingBlockStart:inherit\":\n    \"Inherits the block start padding from its parent element.\",\n  \"scrollPaddingBlockStart:unset\":\n    \"Resets the block start padding to its inherited value.\",\n  \"scrollPaddingBlockEnd:auto\":\n    \"The padding for the bottom of the scroll area adapts to the available space.\",\n  \"scrollPaddingBlockEnd:initial\": \"Sets the padding to its default value.\",\n  \"scrollPaddingBlockEnd:inherit\":\n    \"The padding will be inherited from its parent element.\",\n  \"scrollPaddingBlockEnd:unset\":\n    \"The padding will be set to its inherited value if any, otherwise it will be set to its initial value (`0`).\",\n  \"scrollPaddingBottom:auto\":\n    \"The padding for the bottom of the scroll area adapts to the available space.\",\n  \"scrollPaddingBottom:initial\": \"Sets the padding to its default value.\",\n  \"scrollPaddingBottom:inherit\":\n    \"The padding will be inherited from its parent element.\",\n  \"scrollPaddingBottom:unset\":\n    \"The padding will be set to its inherited value if any, otherwise it will be set to its initial value (`0`).\",\n  \"scrollPaddingInlineStart:auto\":\n    \"The padding for the start of the scroll area adapts to available space.\",\n  \"scrollPaddingInlineStart:initial\": \"Sets the padding to its default value.\",\n  \"scrollPaddingInlineStart:inherit\":\n    \"The padding will be inherited from its parent element.\",\n  \"scrollPaddingInlineStart:unset\":\n    \"The padding will be set to its inherited value if any, otherwise it will be set to its initial value (`0`).\",\n  \"scrollPaddingInlineEnd:auto\":\n    \"The padding for the end of the scroll area adapts to available space.\",\n  \"scrollPaddingInlineEnd:initial\": \"Sets the padding to its default value.\",\n  \"scrollPaddingInlineEnd:inherit\":\n    \"The padding will be inherited from its parent element.\",\n  \"scrollPaddingInlineEnd:unset\":\n    \"The padding will be set to its inherited value if any, otherwise it will be set to its initial value (`0`).\",\n  \"scrollPaddingLeft:auto\":\n    \"Automatically sets the padding-left to allow for scrollbars.\",\n  \"scrollPaddingLeft:initial\": \"Sets padding-left to its default value.\",\n  \"scrollPaddingLeft:inherit\": \"Sets padding-left to the value of its parent.\",\n  \"scrollPaddingLeft:unset\":\n    \"Sets padding-left to its inherited value, or default if none.\",\n  \"scrollPaddingRight:auto\":\n    \"Automatically sets the padding-right to allow for scrollbars.\",\n  \"scrollPaddingRight:initial\": \"Sets padding-right to its default value.\",\n  \"scrollPaddingRight:inherit\":\n    \"Sets padding-right to the value of its parent.\",\n  \"scrollPaddingRight:unset\":\n    \"Sets padding-right to its inherited value, or default if none.\",\n  \"scrollPaddingTop:auto\":\n    \"Automatically sets the padding-top to allow for scrollbars.\",\n  \"scrollPaddingTop:initial\": \"Sets padding-top to its default value.\",\n  \"scrollPaddingTop:inherit\": \"Sets padding-top to the value of its parent.\",\n  \"scrollPaddingTop:unset\":\n    \"Sets padding-top to its inherited value, or default if none.\",\n  \"scrollSnapAlign:none\": \"Disables snapping behavior.\",\n  \"scrollSnapAlign:start\":\n    \"Snaps to the start of each snap area, respecting directionality.\",\n  \"scrollSnapAlign:end\":\n    \"Snaps to the end of each snap area, respecting directionality.\",\n  \"scrollSnapAlign:center\": \"Snaps to the center of each snap area.\",\n  \"scrollSnapAlign:initial\": \"The snap point gets aligned to the nearest one.\",\n  \"scrollSnapAlign:inherit\":\n    \"Inherits the scroll-snap-align value from the parent element.\",\n  \"scrollSnapAlign:unset\":\n    \"Resets the scroll-snap-align value to its default value.\",\n  \"scrollSnapStop:normal\": \"Snap points act normally.\",\n  \"scrollSnapStop:always\":\n    \"Interpolates the scroll position to the nearest snap point.\",\n  \"scrollSnapStop:initial\":\n    \"Resets the scroll-snap-stop value to its default value.\",\n  \"scrollSnapStop:inherit\":\n    \"Inherits the scroll-snap-stop value from the parent element.\",\n  \"scrollSnapStop:unset\":\n    \"Resets the scroll-snap-stop value to its default value.\",\n  \"scrollSnapType:none\": \"No snapping is performed.\",\n  \"scrollSnapType:x\": \"The scroll-snap-align applies on the horizontal axis.\",\n  \"scrollSnapType:y\": \"The scroll-snap-align applies on the vertical axis.\",\n  \"scrollSnapType:block\":\n    \"The scroll-snap-align applies to the whole block container.\",\n  \"scrollSnapType:inline\":\n    \"The scroll-snap-align applies to the inline content.\",\n  \"scrollSnapType:both\": \"The scroll-snap-align applies on both axes.\",\n  \"scrollSnapType:mandatory\": \"Forces snap points to snap.\",\n  \"scrollSnapType:proximity\":\n    \"Snap points must be at a certain proximity to snap.\",\n  \"scrollSnapType:initial\": \"Defines how scroll positions are snapped.\",\n  \"scrollSnapType:inherit\":\n    \"Inherits the scroll-snapping type from the parent element.\",\n  \"scrollSnapType:unset\":\n    \"Resets the scroll-snapping type to its initial value.\",\n  \"scrollTimelineAxis:block\":\n    \"Defines the scrolling axis for a scroll timeline.\",\n  \"scrollTimelineAxis:inline\":\n    \"Defines the scrolling axis for a scroll timeline.\",\n  \"scrollTimelineAxis:vertical\":\n    \"Defines the scrolling axis for a scroll timeline.\",\n  \"scrollTimelineAxis:horizontal\":\n    \"Defines the scrolling axis for a scroll timeline.\",\n  \"scrollTimelineAxis:initial\": \"Sets the scrolling axis to its default value.\",\n  \"scrollTimelineAxis:inherit\":\n    \"Inherits the scrolling axis from the parent element.\",\n  \"scrollTimelineAxis:unset\": \"Resets the scrolling axis to its initial value.\",\n  \"scrollTimelineName:none\": \"Removes a previously set scroll timeline.\",\n  \"scrollTimelineName:initial\":\n    \"Sets the scroll timeline to its default value.\",\n  \"scrollTimelineName:inherit\":\n    \"Inherits the scroll timeline name from the parent element.\",\n  \"scrollTimelineName:unset\":\n    \"Resets the scroll timeline name to its initial value.\",\n  \"shapeImageThreshold:initial\":\n    \"Sets the transparency threshold for the `shape-outside` property.\",\n  \"shapeImageThreshold:inherit\":\n    \"Inherits the transparency threshold from the parent element.\",\n  \"shapeImageThreshold:unset\":\n    \"Specifies the alpha channel threshold used to extract shape.\",\n  \"shapeMargin:initial\": \"Specifies the margin shape of an element.\",\n  \"shapeMargin:inherit\":\n    \"Inherits the shape-margin property from its parent element.\",\n  \"shapeMargin:unset\": \"Resets the margin shape to its default value.\",\n  \"shapeOutside:none\": \"No shape is created around an element.\",\n  \"shapeOutside:border-box\": \"The shape extends to border box of the element.\",\n  \"shapeOutside:padding-box\":\n    \"The shape extends to padding box of the element.\",\n  \"shapeOutside:content-box\":\n    \"The shape extends to content box of the element.\",\n  \"shapeOutside:margin-box\": \"The shape extends to margin box of the element.\",\n  \"shapeOutside:initial\": \"Sets the shape to its default value.\",\n  \"shapeOutside:inherit\":\n    \"Inherits the shape-outside property from its parent element.\",\n  \"shapeOutside:unset\": \"Resets the shape to its default value.\",\n  \"tabSize:initial\":\n    \"Specifies the default tab size for the text within an element.\",\n  \"tabSize:inherit\": \"Inherits the tab-size property from its parent element.\",\n  \"tabSize:unset\": \"Resets the tab-size property to its default value.\",\n  \"tableLayout:auto\": \"Default table layout algorithm is used for the table.\",\n  \"tableLayout:fixed\": \"Table layout algorithm uses fixed layout.\",\n  \"tableLayout:initial\": \"Sets default value for table-layout.\",\n  \"tableLayout:inherit\":\n    \"The value of the table-layout property is inherited from its parent element.\",\n  \"tableLayout:unset\":\n    \"The value of this property is inherited from the parent element if it exists, or else it is set to its initial value.\",\n  \"textAlign:start\": \"Aligns the text at the start of the line.\",\n  \"textAlign:end\": \"Aligns the text at the end of the line.\",\n  \"textAlignLast:end\":\n    \"Aligns the last line of a text block to the right side.\",\n  \"textAlignLast:left\":\n    \"Aligns the last line of a text block to the left side.\",\n  \"textAlignLast:right\":\n    \"Aligns the last line of a text block to the right side.\",\n  \"textAlignLast:center\": \"Aligns the last line of a text block to the center.\",\n  \"textAlignLast:justify\":\n    \"Stretches the last line of a text block to fill the available width.\",\n  \"textAlignLast:initial\": \"Sets the text alignment to its default value.\",\n  \"textAlignLast:inherit\":\n    \"Inherits the text alignment property from its parent element.\",\n  \"textAlignLast:unset\": \"Resets the text alignment to its inherited value.\",\n  \"textCombineUpright:none\":\n    \"Displays upright text as usual (without combining characters).\",\n  \"textCombineUpright:all\":\n    \"Combines all upright text, including punctuation and symbols.\",\n  \"textCombineUpright:digits\": \"Combines only digits in upright text.\",\n  \"textCombineUpright:initial\":\n    \"Sets the upright text combination to its default value.\",\n  \"textCombineUpright:inherit\":\n    \"Inherits the upright text combination property from its parent element.\",\n  \"textCombineUpright:unset\":\n    \"Resets the upright text combination to its inherited value.\",\n  \"textDecorationLine:none\": \"No text decoration is applied.\",\n  \"textDecorationLine:underline\": \"Adds an underline decoration to the text.\",\n  \"textDecorationLine:overline\": \"Adds an overline to the text.\",\n  \"textDecorationLine:line-through\": \"Adds a line through the text.\",\n  \"textDecorationLine:blink\":\n    \"Makes the text blink on and off (not widely supported).\",\n  \"textDecorationLine:spelling-error\":\n    \"Underlines text in red to indicate spelling error.\",\n  \"textDecorationLine:grammar-error\":\n    \"Underlines text in green to indicate grammar error.\",\n  \"textDecorationLine:initial\":\n    \"Sets the text decoration to its default value.\",\n  \"textDecorationLine:inherit\":\n    \"Inherits the text decoration from the parent element.\",\n  \"textDecorationSkip:none\": \"No part of the text decoration is skipped.\",\n  \"textDecorationSkip:objects\":\n    \"Skips any element with an object-fit or clip property.\",\n  \"textDecorationSkip:spaces\": \"Skips white spaces.\",\n  \"textDecorationSkip:leading-spaces\": \"Skips leading white spaces.\",\n  \"textDecorationSkip:trailing-spaces\": \"Skips trailing white spaces.\",\n  \"textDecorationSkip:edges\": \"Skips the edges of inline boxes.\",\n  \"textDecorationSkip:box-decoration\":\n    \"Skips any element with a box-decoration-break property.\",\n  \"textDecorationSkip:initial\":\n    \"Sets the text decoration skip to its default value.\",\n  \"textDecorationSkip:inherit\":\n    \"Inherits the text-decoration-skip value from the parent element.\",\n  \"textDecorationSkip:unset\":\n    \"Removes the text-decoration-skip value defined in the parent element.\",\n  \"textDecorationSkipInk:auto\":\n    \"Allows the browser to determine what part of the text should be skipped.\",\n  \"textDecorationSkipInk:all\": \"Skips the entire ink area of the text.\",\n  \"textDecorationSkipInk:none\": \"Text decoration is not skipped.\",\n  \"textDecorationSkipInk:initial\":\n    \"Sets the text decoration skip ink to its default value.\",\n  \"textDecorationSkipInk:inherit\":\n    \"Inherits the text-decoration-skip-ink value from the parent element.\",\n  \"textDecorationSkipInk:unset\":\n    \"Removes the text-decoration-skip-ink value from the parent element.\",\n  \"textDecorationStyle:solid\": \"Creates a solid underline for text decoration.\",\n  \"textDecorationStyle:double\":\n    \"Creates a double underline for text decoration.\",\n  \"textDecorationStyle:dotted\":\n    \"Creates a dotted underline for text decoration.\",\n  \"textDecorationStyle:dashed\":\n    \"Creates a dashed underline for text decoration.\",\n  \"textDecorationStyle:wavy\": \"Creates a wavy underline for text decoration.\",\n  \"textDecorationStyle:initial\":\n    \"Sets the text decoration style to its default value.\",\n  \"textDecorationStyle:inherit\":\n    \"Inherits the text-decoration-style value from the parent element.\",\n  \"textDecorationStyle:unset\":\n    \"Removes the text-decoration-style value defined in the parent element.\",\n  \"textDecorationThickness:auto\": \"Thickness determined by the browser.\",\n  \"textDecorationThickness:from-font\": \"Thickness based on the font size.\",\n  \"textDecorationThickness:initial\": \"Use default value.\",\n  \"textDecorationThickness:inherit\":\n    \"Inherits the value of the parent element.\",\n  \"textDecorationThickness:unset\": \"Resets the value to its default.\",\n  \"textEmphasisPosition:over\": \"Emphasis mark above the text.\",\n  \"textEmphasisPosition:under\": \"Emphasis mark below the text.\",\n  \"textEmphasisPosition:right\": \"Emphasis mark to the right of the text.\",\n  \"textEmphasisPosition:left\": \"Emphasis mark to the left of the text.\",\n  \"textEmphasisPosition:initial\": \"Use default value.\",\n  \"textEmphasisPosition:inherit\": \"Inherits the value of the parent element.\",\n  \"textEmphasisPosition:unset\": \"Resets the value to its default.\",\n  \"textEmphasisStyle:none\": \"No emphasis mark.\",\n  \"textEmphasisStyle:filled\": \"Filled emphasis mark.\",\n  \"textEmphasisStyle:open\": \"Hollow emphasis mark.\",\n  \"textEmphasisStyle:dot\": \"Small dot emphasis mark.\",\n  \"textEmphasisStyle:circle\": \"Adds a circular emphasis mark to text.\",\n  \"textEmphasisStyle:double-circle\":\n    \"Adds double circular emphasis mark to text.\",\n  \"textEmphasisStyle:triangle\": \"Adds a triangular emphasis mark to text.\",\n  \"textEmphasisStyle:sesame\": \"Adds a sesame seed emphasis mark to text.\",\n  \"textEmphasisStyle:initial\":\n    \"Sets the text emphasis style to its default value.\",\n  \"textEmphasisStyle:inherit\":\n    \"Inherits the text emphasis style from its parent element.\",\n  \"textEmphasisStyle:unset\":\n    \"Resets the text emphasis style to its inherited value or initial if none.\",\n  \"textIndent:hanging\":\n    \"The first line of text indents to the left of the subsequent lines.\",\n  \"textIndent:each-line\": \"Text wraps and subsequent lines indent.\",\n  \"textIndent:initial\": \"Sets the text indentation to its default value.\",\n  \"textIndent:inherit\":\n    \"Inherits the text indentation from its parent element.\",\n  \"textIndent:unset\":\n    \"Resets the text indentation to its inherited value or initial if none.\",\n  \"textJustify:auto\": \"Default justification algorithm is used.\",\n  \"textJustify:inter-character\":\n    \"Character-by-character justification is used.\",\n  \"textJustify:inter-word\": \"Word-by-word justification is used.\",\n  \"textJustify:none\":\n    \"Spaces do not expand and words may be hyphenated to justify.\",\n  \"textJustify:initial\": \"Justifies text according to browser default.\",\n  \"textJustify:inherit\": \"Inherits the text justification from parent element.\",\n  \"textJustify:unset\": \"Resets text justification to its natural value.\",\n  \"textOrientation:mixed\": \"Allows horizontal and vertical text in same block.\",\n  \"textOrientation:upright\": \"Vertically aligns text upright.\",\n  \"textOrientation:sideways\": \"Rotates letters on their side.\",\n  \"textOrientation:initial\":\n    \"Sets text orientation according to browser default.\",\n  \"textOrientation:inherit\": \"Inherits text orientation from parent element.\",\n  \"textOrientation:unset\": \"Resets text orientation to its natural value.\",\n  \"textOverflow:clip\": \"Clips overflowing text.\",\n  \"textOverflow:ellipsis\": \"Shows an ellipsis when text overflows.\",\n  \"textOverflow:initial\": \"Sets text overflow to default.\",\n  \"textOverflow:inherit\": \"Inherits overflow behavior from parent element.\",\n  \"textOverflow:unset\": \"Resets text overflow to its natural value.\",\n  \"textRendering:auto\": \"Uses the browser's default text rendering method.\",\n  \"textRendering:optimizeSpeed\": \"Optimizes the text rendered for speed.\",\n  \"textRendering:optimizeLegibility\":\n    \"Font is rendered for maximum readability.\",\n  \"textRendering:geometricPrecision\":\n    \"Font is rendered with geometric precision.\",\n  \"textRendering:initial\": \"Sets the font rendering to its default value.\",\n  \"textRendering:inherit\":\n    \"Sets the font rendering to that of its parent element.\",\n  \"textRendering:unset\": \"Resets font rendering to its inherited value.\",\n  \"textShadow:none\": \"Disables text shadow.\",\n  \"textShadow:transparent\": \"Adds a transparent text shadow.\",\n  \"textShadow:aliceblue\": \"Adds a light blue text shadow.\",\n  \"textShadow:antiquewhite\": \"Adds an antique white text shadow.\",\n  \"textShadow:aqua\": \"Adds an aqua text shadow.\",\n  \"textShadow:aquamarine\": \"Adds an aquamarine text shadow.\",\n  \"textShadow:azure\": \"Adds an azure text shadow.\",\n  \"textShadow:beige\": \"Adds a beige text shadow.\",\n  \"textShadow:bisque\": \"Adds a bisque text shadow.\",\n  \"textShadow:black\": \"Adds a black text shadow.\",\n  \"textShadow:blanchedalmond\": \"Adds a blanched almond text shadow.\",\n  \"textShadow:blue\": \"Adds a blue shadow to text.\",\n  \"textShadow:blueviolet\": \"Adds a blue-violet shadow to text.\",\n  \"textShadow:brown\": \"Adds a brown shadow to text.\",\n  \"textShadow:burlywood\": \"Adds a burlywood shadow to text.\",\n  \"textShadow:cadetblue\": \"Adds a cadet blue shadow to text.\",\n  \"textShadow:chartreuse\": \"Adds a chartreuse shadow to text.\",\n  \"textShadow:chocolate\": \"Adds a chocolate shadow to text.\",\n  \"textShadow:coral\": \"Adds a coral shadow to text.\",\n  \"textShadow:cornflowerblue\": \"Adds a cornflower blue shadow to text.\",\n  \"textShadow:cornsilk\": \"Adds a cornsilk shadow to text.\",\n  \"textShadow:crimson\": \"Adds a crimson shadow to text.\",\n  \"textShadow:cyan\": \"Adds a cyan shadow to text.\",\n  \"textShadow:darkblue\": \"Adds a dark blue shadow to text.\",\n  \"textShadow:darkcyan\": \"Adds a dark cyan shadow to text.\",\n  \"textShadow:darkgoldenrod\": \"Adds a dark goldenrod shadow to text.\",\n  \"textShadow:darkgray\": \"Adds a dark gray shadow to text.\",\n  \"textShadow:darkgreen\": \"Adds a dark green shadow to the text.\",\n  \"textShadow:darkgrey\": \"Adds a dark grey shadow to the text.\",\n  \"textShadow:darkkhaki\": \"Adds a dark khaki shadow to the text.\",\n  \"textShadow:darkmagenta\": \"Adds a dark magenta shadow to the text.\",\n  \"textShadow:darkolivegreen\": \"Adds a dark olive green shadow to the text.\",\n  \"textShadow:darkorange\": \"Adds a dark orange shadow to the text.\",\n  \"textShadow:darkorchid\": \"Adds a dark orchid shadow to the text.\",\n  \"textShadow:darkred\": \"Adds a dark red shadow to the text.\",\n  \"textShadow:darksalmon\": \"Adds a dark salmon shadow to the text.\",\n  \"textShadow:darkseagreen\": \"Adds a dark sea green shadow to the text.\",\n  \"textShadow:darkslateblue\": \"Adds a dark slate blue shadow to the text.\",\n  \"textShadow:darkslategray\": \"Adds a dark slate gray shadow to the text.\",\n  \"textShadow:darkslategrey\": \"Adds a dark slate grey shadow to the text.\",\n  \"textShadow:darkturquoise\": \"Adds a dark turquoise shadow to the text.\",\n  \"textShadow:darkviolet\": \"Adds a dark violet shadow to the text.\",\n  \"textShadow:deeppink\": \"Adds a deep pink shadow to the text.\",\n  \"textShadow:deepskyblue\": \"Adds a deep sky blue shadow to text.\",\n  \"textShadow:dimgray\": \"Adds a dim gray shadow to text.\",\n  \"textShadow:dimgrey\": \"Adds a dim gray shadow to text.\",\n  \"textShadow:dodgerblue\": \"Adds a dodger blue shadow to text.\",\n  \"textShadow:firebrick\": \"Adds a fire brick shadow to text.\",\n  \"textShadow:floralwhite\": \"Adds a floral white shadow to text.\",\n  \"textShadow:forestgreen\": \"Adds a forest green shadow to text.\",\n  \"textShadow:fuchsia\": \"Adds a fuchsia shadow to text.\",\n  \"textShadow:gainsboro\": \"Adds a gainsboro shadow to text.\",\n  \"textShadow:ghostwhite\": \"Adds a ghost white shadow to text.\",\n  \"textShadow:gold\": \"Adds a gold shadow to text.\",\n  \"textShadow:goldenrod\": \"Adds a goldenrod shadow to text.\",\n  \"textShadow:gray\": \"Adds a gray shadow to text.\",\n  \"textShadow:green\": \"Adds a green shadow to text.\",\n  \"textShadow:greenyellow\": \"Adds a green yellow shadow to text.\",\n  \"textShadow:grey\": \"Adds a gray shadow to text.\",\n  \"textShadow:honeydew\": \"Adds a honeydew-colored shadow to the text.\",\n  \"textShadow:hotpink\": \"Adds a hot pink-colored shadow to the text.\",\n  \"textShadow:indianred\": \"Adds an Indian red-colored shadow to the text.\",\n  \"textShadow:indigo\": \"Adds an indigo-colored shadow to the text.\",\n  \"textShadow:ivory\": \"Adds an ivory-colored shadow to the text.\",\n  \"textShadow:khaki\": \"Adds a khaki-colored shadow to the text.\",\n  \"textShadow:lavender\": \"Adds a lavender-colored shadow to the text.\",\n  \"textShadow:lavenderblush\":\n    \"Adds a lavender blush-colored shadow to the text.\",\n  \"textShadow:lawngreen\": \"Adds a lawn green-colored shadow to the text.\",\n  \"textShadow:lemonchiffon\": \"Adds a lemon chiffon-colored shadow to the text.\",\n  \"textShadow:lightblue\": \"Adds a light blue-colored shadow to the text.\",\n  \"textShadow:lightcoral\": \"Adds a light coral-colored shadow to the text.\",\n  \"textShadow:lightcyan\": \"Adds a light cyan-colored shadow to the text.\",\n  \"textShadow:lightgoldenrodyellow\":\n    \"Adds a light goldenrod yellow-colored shadow to the text.\",\n  \"textShadow:lightgray\": \"Adds a light gray-colored shadow to the text.\",\n  \"textShadow:lightgreen\": \"Adds a light green-colored shadow to the text.\",\n  \"textShadow:lightgrey\": \"Adds a light grey shadow to text.\",\n  \"textShadow:lightpink\": \"Adds a light pink shadow to text.\",\n  \"textShadow:lightsalmon\": \"Adds a light salmon shadow to text.\",\n  \"textShadow:lightseagreen\": \"Adds a light sea green shadow to text.\",\n  \"textShadow:lightskyblue\": \"Adds a light sky blue shadow to text.\",\n  \"textShadow:lightslategray\": \"Adds a light slate gray shadow to text.\",\n  \"textShadow:lightslategrey\": \"Adds a light slate grey shadow to text.\",\n  \"textShadow:lightsteelblue\": \"Adds a light steel blue shadow to text.\",\n  \"textShadow:lightyellow\": \"Adds a light yellow shadow to text.\",\n  \"textShadow:lime\": \"Adds a lime shadow to text.\",\n  \"textShadow:limegreen\": \"Adds a lime green shadow to text.\",\n  \"textShadow:linen\": \"Adds a linen colored shadow to text.\",\n  \"textShadow:magenta\": \"Adds a magenta shadow to text.\",\n  \"textShadow:maroon\": \"Adds a maroon shadow to text.\",\n  \"textShadow:mediumaquamarine\": \"Adds a medium aquamarine shadow to text.\",\n  \"textShadow:mediumblue\": \"Adds a medium blue shadow to text.\",\n  \"textShadow:mediumorchid\": \"Adds a medium orchid shadow to text.\",\n  \"textShadow:mediumpurple\": \"Adds a medium purple shadow to text.\",\n  \"textShadow:mediumseagreen\": \"Adds a medium sea green shadow to text.\",\n  \"textShadow:mediumslateblue\": \"Adds a medium slate blue shadow to text.\",\n  \"textShadow:mediumspringgreen\": \"Adds a medium spring green shadow to text.\",\n  \"textShadow:mediumturquoise\": \"Adds a medium turquoise shadow to text.\",\n  \"textShadow:mediumvioletred\": \"Adds a medium violet red shadow to text.\",\n  \"textShadow:midnightblue\": \"Adds a midnight blue shadow to text.\",\n  \"textShadow:mintcream\": \"Adds a mint cream shadow to text.\",\n  \"textShadow:mistyrose\": \"Adds a misty rose shadow to text.\",\n  \"textShadow:moccasin\": \"Adds a moccasin shadow to text.\",\n  \"textShadow:navajowhite\": \"Adds a navajo white shadow to text.\",\n  \"textShadow:navy\": \"Adds a navy shadow to text.\",\n  \"textShadow:oldlace\": \"Adds an old lace shadow to text.\",\n  \"textShadow:olive\": \"Adds an olive shadow to text.\",\n  \"textShadow:olivedrab\": \"Adds an olive drab shadow to text.\",\n  \"textShadow:orange\": \"Applies an orange shadow to text.\",\n  \"textShadow:orangered\": \"Applies an orange-red shadow to text.\",\n  \"textShadow:orchid\": \"Applies a purple-pink shadow to text.\",\n  \"textShadow:palegoldenrod\": \"Applies a pale goldenrod shadow to text.\",\n  \"textShadow:palegreen\": \"Applies a pale green shadow to text.\",\n  \"textShadow:paleturquoise\": \"Applies a pale turquoise shadow to text.\",\n  \"textShadow:palevioletred\": \"Applies a pale violet-red shadow to text.\",\n  \"textShadow:papayawhip\": \"Applies a papaya-whip shadow to text.\",\n  \"textShadow:peachpuff\": \"Applies a peach-puff shadow to text.\",\n  \"textShadow:peru\": \"Applies a dark orange-brown shadow to text.\",\n  \"textShadow:pink\": \"Applies a pink shadow to text.\",\n  \"textShadow:plum\": \"Applies a purple shadow to text.\",\n  \"textShadow:powderblue\": \"Applies a powder-blue shadow to text.\",\n  \"textShadow:purple\": \"Applies a dark purple shadow to text.\",\n  \"textShadow:rebeccapurple\": \"Applies a dark purple shadow to text.\",\n  \"textShadow:red\": \"Applies a red shadow to text.\",\n  \"textShadow:rosybrown\": \"Adds a rosybrown shadow to text.\",\n  \"textShadow:royalblue\": \"Adds a royalblue shadow to text.\",\n  \"textShadow:saddlebrown\": \"Adds a saddlebrown shadow to text.\",\n  \"textShadow:salmon\": \"Adds a salmon shadow to text.\",\n  \"textShadow:sandybrown\": \"Adds a sandybrown shadow to text.\",\n  \"textShadow:seagreen\": \"Adds a seagreen shadow to text.\",\n  \"textShadow:seashell\": \"Adds a seashell shadow to text.\",\n  \"textShadow:sienna\": \"Adds a sienna shadow to text.\",\n  \"textShadow:silver\": \"Adds a silver shadow to text.\",\n  \"textShadow:skyblue\": \"Adds a skyblue shadow to text.\",\n  \"textShadow:slateblue\": \"Adds a slateblue shadow to text.\",\n  \"textShadow:slategray\": \"Adds a slategray shadow to text.\",\n  \"textShadow:slategrey\": \"Adds a slategrey shadow to text.\",\n  \"textShadow:snow\": \"Adds a snow shadow to text.\",\n  \"textShadow:springgreen\": \"Adds a springgreen shadow to text.\",\n  \"textShadow:steelblue\": \"Adds a steelblue shadow to text.\",\n  \"textShadow:tan\": \"Adds a tan shadow to text.\",\n  \"textShadow:teal\": \"Adds a teal shadow to text.\",\n  \"textShadow:thistle\": \"Adds a thistle shadow to text.\",\n  \"textShadow:tomato\": \"Adds a tomato shadow to text.\",\n  \"textShadow:turquoise\": \"Adds a turquoise shadow to text.\",\n  \"textShadow:violet\": \"Adds a violet shadow to text.\",\n  \"textShadow:wheat\": \"Adds a wheat shadow to text.\",\n  \"textShadow:white\": \"Adds a white shadow to text.\",\n  \"textShadow:whitesmoke\": \"Adds a whitesmoke shadow to text.\",\n  \"textShadow:yellow\": \"Adds a yellow shadow to text.\",\n  \"textShadow:yellowgreen\": \"Adds a yellowgreen shadow to text.\",\n  \"textShadow:currentColor\": \"Adds a shadow with the same color as the text.\",\n  \"textShadow:initial\": \"Sets the default value of the property.\",\n  \"textShadow:inherit\": \"Inherits the value from the parent element.\",\n  \"textShadow:unset\": \"Resets the value to its default state.\",\n  \"textSizeAdjust:none\":\n    \"Prevents mobile devices from adjusting font size based on user preferences.\",\n  \"textSizeAdjust:auto\": \"Allows users to adjust the font size.\",\n  \"textSizeAdjust:initial\": \"Specifies the default font size behavior.\",\n  \"textSizeAdjust:inherit\": \"Inherits font size adjustment behavior.\",\n  \"textSizeAdjust:unset\": \"Uses default value if no value is declared.\",\n  \"textTransform:none\": \"No text capitalization or transformation.\",\n  \"textTransform:capitalize\": \"Capitalizes first letter of each word.\",\n  \"textTransform:uppercase\": \"Converts text to UPPERCASE.\",\n  \"textTransform:lowercase\": \"Converts text to lowercase.\",\n  \"textTransform:full-width\": \"Converts to full width characters.\",\n  \"textTransform:full-size-kana\": \"Changes to full width for Japanese.\",\n  \"textTransform:initial\": \"Resets to initial capitalization or conversion.\",\n  \"textTransform:inherit\": \"Inherits text capitalization or conversion.\",\n  \"textTransform:unset\": \"Uses default value if no value is declared.\",\n  \"textUnderlineOffset:auto\": \"Allows the browser to adjust underline offset.\",\n  \"textUnderlineOffset:initial\": \"Specifies default underline offset.\",\n  \"textUnderlineOffset:inherit\": \"Inherits underline offset behavior.\",\n  \"textUnderlineOffset:unset\": \"The default offset of underlines.\",\n  \"textUnderlinePosition:auto\": \"Automatically aligns underlines.\",\n  \"textUnderlinePosition:from-font\": \"Uses the font's underline position.\",\n  \"textUnderlinePosition:under\": \"Underlines text.\",\n  \"textUnderlinePosition:left\": \"Underlines text, aligned to the left.\",\n  \"textUnderlinePosition:right\": \"Underlines text, aligned to the right.\",\n  \"textUnderlinePosition:initial\": \"Sets underline position to the default.\",\n  \"textUnderlinePosition:inherit\":\n    \"Inherits underline position from the parent element.\",\n  \"textUnderlinePosition:unset\": \"Removes any set underline position.\",\n  \"top:auto\": \"The browser determines the top position.\",\n  \"top:initial\": \"Sets the top position to its default value.\",\n  \"top:inherit\": \"Inherits the top position from the parent element.\",\n  \"top:unset\": \"Removes any set top position.\",\n  \"touchAction:auto\": \"The browser determines the touch action.\",\n  \"touchAction:none\": \"Disables touch actions on the specified element.\",\n  \"touchAction:pan-x\": \"Allows horizontal panning through touch actions.\",\n  \"touchAction:pan-left\": \"Allows panning to the left.\",\n  \"touchAction:pan-right\": \"Allows panning to the right.\",\n  \"touchAction:pan-y\": \"Allows panning on the y-axis only.\",\n  \"touchAction:pan-up\": \"Allows panning upwards.\",\n  \"touchAction:pan-down\": \"Allows panning downwards.\",\n  \"touchAction:pinch-zoom\": \"Allows pinch zooming on touch devices.\",\n  \"touchAction:manipulation\": \"Allows any touch-gesture to be performed.\",\n  \"touchAction:initial\": \"Uses the default touch action.\",\n  \"touchAction:inherit\":\n    \"Inherits the touch-action value from the parent element.\",\n  \"touchAction:unset\": \"Resets the touch-action value to its original value.\",\n  \"transform:none\": \"Disables any transformations.\",\n  \"transform:initial\": \"Applies the default transform values.\",\n  \"transform:inherit\": \"Inherits the transform value from the parent element.\",\n  \"transform:unset\": \"Resets the transform value to its original value.\",\n  \"transformBox:content-box\": \"Sets the transform origin to the content box.\",\n  \"transformBox:border-box\": \"Sets the transform origin to the border box.\",\n  \"transformBox:fill-box\": \"Applies transformations to the fill bounding box.\",\n  \"transformBox:stroke-box\":\n    \"Applies transformations to the stroke bounding box.\",\n  \"transformBox:view-box\":\n    \"Applies transformations to the nearest SVG viewport.\",\n  \"transformBox:initial\": \"Sets the property to its default value.\",\n  \"transformBox:inherit\": \"Inherits the property from its parent element.\",\n  \"transformBox:unset\":\n    \"Resets the property to its inherited value (or default if no inheritable value).\",\n  \"transformOrigin:left\":\n    \"Sets the horizontal position of the transform origin to the left.\",\n  \"transformOrigin:center\":\n    \"Sets the horizontal position of the transform origin to the center.\",\n  \"transformOrigin:right\":\n    \"Sets the horizontal position of the transform origin to the right.\",\n  \"transformOrigin:top\":\n    \"Sets the vertical position of the transform origin to the top.\",\n  \"transformOrigin:bottom\":\n    \"Sets the vertical position of the transform origin to the bottom.\",\n  \"transformOrigin:initial\": \"Sets the property to its default value.\",\n  \"transformOrigin:inherit\": \"Inherits the property from its parent element.\",\n  \"transformOrigin:unset\":\n    \"Resets the property to its inherited value (or default if no inheritable value).\",\n  \"transformStyle:flat\": \"Disables 3D rendering effects on child elements.\",\n  \"transformStyle:preserve-3d\":\n    \"Allows child elements to be transformed in 3D perspective.\",\n  \"transformStyle:initial\":\n    \"The child elements don't preserve the 3D transformation of the parent.\",\n  \"transformStyle:inherit\":\n    \"The child elements inherit the 3D transformation of the parent.\",\n  \"transformStyle:unset\":\n    \"The property takes its parent value or initial if there is no parent except `transform-style: inherit`.\",\n  \"transitionDelay:initial\":\n    \"No delay between the transition effect and the initiation.\",\n  \"transitionDelay:inherit\": \"Inherits the delay time from the parent element.\",\n  \"transitionDelay:unset\":\n    \"Sets to the property's parent value if any, or initial.\",\n  \"transitionDuration:initial\":\n    \"Duration takes no time to run the transition effect.\",\n  \"transitionDuration:inherit\":\n    \"Inherits the duration time from the parent element.\",\n  \"transitionDuration:unset\":\n    \"Sets to the property's parent value if any, or initial.\",\n  \"transitionProperty:none\":\n    \"No CSS properties mentioned for transition effect.\",\n  \"transitionProperty:all\":\n    \"Involves all CSS properties mentioned in the effect.\",\n  \"transitionProperty:initial\":\n    \"Includes only the initial property value in the effect.\",\n  \"transitionProperty:inherit\":\n    \"Inherits the properties from the parent element.\",\n  \"transitionProperty:unset\":\n    \"Sets to the property's parent value if any, or initial.\",\n  \"transitionTimingFunction:linear\":\n    \"The transition effect runs at a constant speed.\",\n  \"transitionTimingFunction:ease\":\n    \"The transition effect starts slowly, runs quickly in the middle, and ends slowly at the end.\",\n  \"transitionTimingFunction:ease-in\":\n    \"Specifies a transition effect with a slow start.\",\n  \"transitionTimingFunction:ease-out\":\n    \"Specifies a transition effect with a slow end.\",\n  \"transitionTimingFunction:ease-in-out\":\n    \"Specifies a transition effect with a slow start and end.\",\n  \"transitionTimingFunction:step-start\":\n    \"Specifies a transition effect with an immediate start.\",\n  \"transitionTimingFunction:step-end\":\n    \"Specifies a transition effect with an immediate end.\",\n  \"transitionTimingFunction:initial\":\n    \"Sets a CSS property to its default value.\",\n  \"transitionTimingFunction:inherit\":\n    \"Inherits the CSS property from its parent element.\",\n  \"transitionTimingFunction:unset\":\n    \"Resets the CSS property to its inherited value.\",\n  \"translate:none\": \"Specifies no translation.\",\n  \"translate:initial\": \"Sets a CSS property to its default value.\",\n  \"translate:inherit\": \"Inherits the CSS property from its parent element.\",\n  \"translate:unset\": \"Resets the CSS property to its inherited value.\",\n  \"unicodeBidi:normal\":\n    \"The element does not override the base direction of the text.\",\n  \"unicodeBidi:embed\": \"The element overrides the base direction of the text.\",\n  \"unicodeBidi:isolate\":\n    \"The element isolates the text from its surrounding text.\",\n  \"unicodeBidi:bidi-override\":\n    \"Overrides the base direction of the text and sets the directionality to right-to-left or left-to-right as determined by the first strong directional character.\",\n  \"unicodeBidi:isolate-override\":\n    \"Overrides the element's bidirectional algorithm.\",\n  \"unicodeBidi:plaintext\":\n    \"The element's content is treated as embedded in a left-to-right direction block.\",\n  \"unicodeBidi:initial\": \"Sets this property to its default value.\",\n  \"unicodeBidi:inherit\": \"Inherits this property from its parent element.\",\n  \"unicodeBidi:unset\":\n    \"Sets this property to its inherited value if it exists, otherwise, it'll be the same as the initial value.\",\n  \"userSelect:auto\":\n    \"The default value, enables text selection within the element.\",\n  \"userSelect:text\":\n    \"Allows the selection of text contained within the element.\",\n  \"userSelect:none\": \"Disables user selection of the element's content.\",\n  \"userSelect:contain\":\n    \"Allows selection of an element's children, but not the own element content.\",\n  \"userSelect:all\":\n    \"Enables selecting the element's content and child elements.\",\n  \"userSelect:initial\": \"Sets the property to its default value of `auto`.\",\n  \"userSelect:inherit\": \"Inherits this property from parent element.\",\n  \"userSelect:unset\":\n    \"Inherits the property if possible; otherwise defaults to `auto`.\",\n  \"verticalAlign:baseline\":\n    \"Aligns the baseline of the element with the baseline of its parent text element.\",\n  \"verticalAlign:sub\":\n    \"Aligns the element vertically to the subscript baseline of the parent.\",\n  \"verticalAlign:super\":\n    \"Aligns the element vertically to the superscript baseline of the parent.\",\n  \"verticalAlign:text-top\":\n    \"Aligns the top of the element with the top of the parent's content.\",\n  \"verticalAlign:text-bottom\":\n    \"Aligns the bottom of the element with the bottom of the parent's content.\",\n  \"verticalAlign:middle\":\n    \"Aligns the middle of the element with the baseline plus half the x-height of the parent.\",\n  \"verticalAlign:top\":\n    \"Aligns the top of the element with the top of the tallest element in the parent.\",\n  \"verticalAlign:bottom\":\n    \"Aligns the bottom of the element with the lowest element in the parent.\",\n  \"verticalAlign:initial\": \"Sets the property to its default value.\",\n  \"verticalAlign:inherit\": \"Inherits the value from the parent element.\",\n  \"verticalAlign:unset\": \"Resets the property to its natural value.\",\n  \"visibility:visible\": \"The element is visible.\",\n  \"visibility:hidden\": \"The element is hidden but takes up space.\",\n  \"visibility:collapse\": \"The element is hidden and does not take up space.\",\n  \"visibility:initial\": \"Sets the property value to its default value.\",\n  \"visibility:inherit\": \"Inherits the value from the parent element.\",\n  \"visibility:unset\": \"Resets the property to its natural value.\",\n  \"whiteSpace:normal\":\n    \"Sequences of whitespace are collapsed. Newlines are treated as spaces.\",\n  \"whiteSpace:pre\":\n    \"Sequences of whitespace are preserved, newlines are treated as line breaks.\",\n  \"whiteSpace:nowrap\": \"Keeps text in a single line without wrapping.\",\n  \"whiteSpace:pre-wrap\": \"Allows wrapping at specified points.\",\n  \"whiteSpace:pre-line\": \"Allows different lines to have different indents.\",\n  \"whiteSpace:break-spaces\": \"Allows long words to break at any character.\",\n  \"whiteSpace:initial\":\n    \"Sets the white-space declaration to its initial value.\",\n  \"whiteSpace:inherit\":\n    \"Inherits the white-space value from the parent element.\",\n  \"whiteSpace:unset\":\n    \"The white-space property is set to its parent's computed value.\",\n  \"widows:initial\": \"Sets the minimum number of lines in a block container.\",\n  \"widows:inherit\": \"Uses the widows value from the parent element.\",\n  \"widows:unset\": \"Resets the widows value to its initial value.\",\n  \"width:auto\": \"Sets the width property to its default value (auto).\",\n  \"width:min-content\": \"Expands the elements to fit their entire content.\",\n  \"width:max-content\": \"Shrinks the element to fit its minimum content.\",\n  \"width:fit-content\": \"Expands the element to fit the available width.\",\n  \"width:initial\": \"Sets the width property to its initial value.\",\n  \"width:inherit\": \"Inherits the width value from the parent element.\",\n  \"width:unset\": \"The element inherits its width property from its parent.\",\n  \"willChange:auto\": \"The browser decides which properties to animate.\",\n  \"willChange:scroll-position\": \"The position of the element will be animated.\",\n  \"willChange:contents\": \"Changes in element content will be animated.\",\n  \"willChange:initial\": \"Sets the default property value.\",\n  \"willChange:inherit\": \"Inherits the will-change property value.\",\n  \"willChange:unset\": \"Resets the will-change property value.\",\n  \"wordBreak:normal\": \"Breaks words based on browser default.\",\n  \"wordBreak:break-all\": \"Allows unbroken words to be broken.\",\n  \"wordBreak:keep-all\": \"Keeps all words on the same line.\",\n  \"wordBreak:break-word\": \"Breaks words only when necessary.\",\n  \"wordBreak:initial\": \"Sets the default property value.\",\n  \"wordBreak:inherit\": \"Inherits the word-break property value.\",\n  \"wordBreak:unset\": \"Resets the word-break property value.\",\n  \"wordSpacing:normal\": \"Words are spaced based on browser default.\",\n  \"wordSpacing:initial\": \"Sets the default property value.\",\n  \"wordSpacing:inherit\": \"Inherits word-spacing from parent element.\",\n  \"wordSpacing:unset\": \"Removes inherited word-spacing.\",\n  \"wordWrap:normal\": \"Default behavior, no word wrapping.\",\n  \"wordWrap:break-word\": \"Breaks long words if they don't fit.\",\n  \"wordWrap:initial\": \"Sets the property to its default value.\",\n  \"wordWrap:inherit\": \"Inherits the property from parent element.\",\n  \"wordWrap:unset\": \"Removes inherited word-wrap.\",\n  \"writingMode:horizontal-tb\": \"Default horizontal, top to bottom.\",\n  \"writingMode:vertical-rl\": \"Vertical, right to left.\",\n  \"writingMode:vertical-lr\": \"Vertical, left to right.\",\n  \"writingMode:sideways-rl\": \"Letters laid on their right side, top to bottom.\",\n  \"writingMode:sideways-lr\": \"Letters laid on their left side, top to bottom.\",\n  \"writingMode:initial\": \"Sets the property to its default value.\",\n  \"writingMode:inherit\": \"Inherits the property from parent element.\",\n  \"writingMode:unset\": \"Removes inherited writing-mode.\",\n  \"zIndex:auto\": \"Default, elements stack in order of appearance.\",\n  \"zIndex:initial\": \"Sets the z-index to its default value.\",\n  \"zIndex:inherit\": \"Inherits the z-index from its parent element.\",\n  \"zIndex:unset\":\n    \"Resets the z-index to its inherited value, or default if there is none.\",\n  \"WebkitFontSmoothing:auto\":\n    \"Adjusts smoothing of fonts on webkit browsers for better readability.\",\n  \"WebkitFontSmoothing:none\":\n    \"Disables font smoothing on webkit browsers for a pixelated effect.\",\n  \"WebkitFontSmoothing:antialiased\":\n    \"Applies subpixel antialiasing to fonts on webkit browsers.\",\n  \"WebkitFontSmoothing:subpixel-antialiased\":\n    \"Applies subpixel antialiasing to fonts on webkit browsers.\",\n  \"MozOsxFontSmoothing:auto\":\n    \"Adjusts font smoothing for better readability on macOS.\",\n  \"MozOsxFontSmoothing:grayscale\":\n    \"Renders fonts on macOS in grayscale for better readability.\",\n  \"listStyleType:disc\": \"Sets the list item marker to a filled circle shape.\",\n  \"listStyleType:circle\": \"Sets the list item marker to a hollow circle shape.\",\n  \"listStyleType:square\": \"Sets the list item marker to a filled square shape.\",\n  \"listStyleType:decimal\": \"Sets the list item marker to a decimal number.\",\n  \"listStyleType:georgian\":\n    \"Sets the list item marker to a Georgian character.\",\n  \"listStyleType:trad-chinese-informal\":\n    \"Sets the list item marker to a traditional Chinese character.\",\n  \"listStyleType:kannada\": \"Sets the list item marker to a Kannada character.\",\n  \"animationRangeEnd:normal\":\n    \"Specifies the ending position of the animation sequence.\",\n  \"animationRangeEnd:cover\":\n    \"Makes the animation cover the element's area when ending.\",\n  \"animationRangeEnd:contain\":\n    \"Keeps the animation contained within the element's area when ending.\",\n  \"animationRangeEnd:entry\":\n    \"The animation ends when reaching the entry point.\",\n  \"animationRangeEnd:exit\": \"The animation ends when reaching the exit point.\",\n  \"animationRangeEnd:entry-crossing\":\n    \"The animation ends when crossing the entry point.\",\n  \"animationRangeEnd:exit-crossing\":\n    \"The animation ends when crossing the exit point.\",\n  \"animationRangeEnd:initial\": \"Resets the animation to its initial end point.\",\n  \"animationRangeEnd:inherit\":\n    \"Inherits the animation end point from its parent.\",\n  \"animationRangeEnd:unset\": \"Unsets the animation end point to its default.\",\n  \"animationRangeStart:normal\": \"The animation starts at its normal position.\",\n  \"animationRangeStart:cover\": \"The animation starts at the cover position.\",\n  \"animationRangeStart:contain\":\n    \"The animation starts at the contain position.\",\n  \"animationRangeStart:entry\":\n    \"The animation starts when reaching the entry point.\",\n  \"animationRangeStart:exit\":\n    \"The animation starts when reaching the exit point.\",\n  \"animationRangeStart:entry-crossing\":\n    \"The animation starts when crossing the entry point.\",\n  \"animationRangeStart:exit-crossing\":\n    \"The animation starts when crossing the exit point.\",\n  \"animationRangeStart:initial\":\n    \"Resets the animation to its initial start point.\",\n  \"animationRangeStart:inherit\":\n    \"Inherits the animation start point from its parent.\",\n  \"animationRangeStart:unset\": \"Resets the property to its initial value.\",\n  \"containerName:none\": \"No name is assigned to the container.\",\n  \"containerName:initial\": \"The container takes on its default value.\",\n  \"containerName:inherit\": \"The container inherits the value from its parent.\",\n  \"containerName:unset\": \"The container has no defined value.\",\n  \"containerType:normal\": \"Specifies the layout algorithm for the container.\",\n  \"containerType:size\": \"Specifies the size of the container.\",\n  \"containerType:inline-size\": \"Specifies the inline size of the container.\",\n  \"containerType:initial\": \"The container takes on its default size.\",\n  \"containerType:inherit\": \"The container inherits the size from its parent.\",\n  \"containerType:unset\": \"The container has no defined size.\",\n  \"fontPalette:normal\": \"Sets the font palette to a normal value.\",\n  \"fontPalette:light\": \"Sets the font palette to a light value.\",\n  \"fontPalette:dark\": \"Sets the font palette to a dark value.\",\n  \"fontPalette:initial\": \"The font palette takes on its default setting.\",\n  \"fontPalette:inherit\": \"The font palette inherits the value from its parent.\",\n  \"fontPalette:unset\": \"Resets the font palette to its initial value.\",\n  \"fontSynthesis:position\":\n    \"Specifies how synthetic style variations are aplied.\",\n  \"fontSynthesisPosition:auto\": \"The item has no synthesized style variations.\",\n  \"fontSynthesisPosition:none\":\n    \"The item has auto synthesized style variations.\",\n  \"fontSynthesisPosition:initial\":\n    \"The item has initial synthesized style variations.\",\n  \"fontSynthesisPosition:inherit\":\n    \"The item has inherited synthesized style variations.\",\n  \"fontSynthesisPosition:unset\":\n    \"The item has unset synthesized style variations.\",\n  \"fontSynthesisSmallCaps:auto\":\n    \"The item has no synthesized small-caps variations.\",\n  \"fontSynthesisSmallCaps:none\":\n    \"The item has auto synthesized small-caps variations.\",\n  \"fontSynthesisSmallCaps:initial\":\n    \"The item has initial synthesized small-caps variations.\",\n  \"fontSynthesisSmallCaps:inherit\":\n    \"The item has inherited synthesized small-caps variations.\",\n  \"fontSynthesisSmallCaps:unset\":\n    \"The item has unset synthesized small-caps variations.\",\n  \"fontSynthesisStyle:auto\": \"The item has no synthesized style variations.\",\n  \"fontSynthesisStyle:none\": \"The item has auto synthesized style variations.\",\n  \"fontSynthesisStyle:initial\":\n    \"The item has initial synthesized style variations.\",\n  \"fontSynthesisStyle:inherit\":\n    \"The item has inherited synthesized style variations.\",\n  \"fontSynthesisStyle:unset\":\n    \"Adjusts font synthesis for style to its initial value.\",\n  \"fontSynthesisWeight:auto\":\n    \"Adjusts font synthesis for weight to automatic calculation.\",\n  \"fontSynthesisWeight:none\":\n    \"Adjusts font synthesis for weight to no adjustments made.\",\n  \"fontSynthesisWeight:initial\":\n    \"Adjusts font synthesis for weight to the property's initial value.\",\n  \"fontSynthesisWeight:inherit\":\n    \"Adjusts font synthesis for weight to be inherited from parent.\",\n  \"fontSynthesisWeight:unset\":\n    \"Adjusts font synthesis for weight to its initial value.\",\n  \"fontVariantEmoji:normal\": \"Renders emoji characters in the default style.\",\n  \"fontVariantEmoji:text\": \"Renders emoji characters as standard text.\",\n  \"fontVariantEmoji:emoji\": \"Renders emoji characters as emoji symbols.\",\n  \"fontVariantEmoji:unicode\": \"Renders emoji characters as unicode characters.\",\n  \"fontVariantEmoji:initial\":\n    \"Renders emoji characters with the property's initial value.\",\n  \"fontVariantEmoji:inherit\": \"Renders emoji characters with inherited style.\",\n  \"fontVariantEmoji:unset\":\n    \"Renders emoji characters with property set to its initial value.\",\n  \"hyphenateLimitChars:auto\":\n    \"Sets the maximum number of characters that can be hyphenated.\",\n  \"hyphenateLimitChars:initial\":\n    \"Sets the maximum number of characters to the property's initial value.\",\n  \"hyphenateLimitChars:inherit\":\n    \"Sets the maximum number of characters to be inherited.\",\n  \"hyphenateLimitChars:unset\":\n    \"Resets the maximum number of characters in hyphenated words to its initial value.\",\n  \"offsetPosition:normal\":\n    \"Sets the offset position to the normal position within the containing block.\",\n  \"overlay:none\": \"Removes any overlay effect on the element.\",\n  \"overlay:auto\": \"Automatically applies an overlay effect on the element.\",\n  \"overlay:initial\": \"Sets the overlay effect to its initial value.\",\n  \"overlay:inherit\": \"Inherits the overlay effect from its parent element.\",\n  \"overlay:unset\":\n    \"Unsets the overlay effect, allowing it to cascade down the DOM tree.\",\n  \"page:auto\":\n    \"Automatically determines the page size based on available space.\",\n  \"page:initial\": \"Sets the page size to the initial default value.\",\n  \"page:inherit\": \"Inherits the page size from its parent element.\",\n  \"page:unset\":\n    \"Unsets the page size, allowing it to be determined by the content.\",\n  \"scrollTimelineAxis:x\": \"Sets the scroll timeline axis to horizontal.\",\n  \"scrollTimelineAxis:y\": \"Sets the scroll timeline axis to vertical.\",\n  \"textWrap:wrap\": \"Allows text to wrap within the specified container.\",\n  \"textWrap:nowrap\":\n    \"Prevents text from wrapping, causing it to overflow the container.\",\n  \"textWrap:balance\":\n    \"Distributes text between multiple lines to achieve a visually balanced layout.\",\n  \"textWrap:stable\": \"The text is stable within the specified container.\",\n  \"textWrap:pretty\": \"The text is pretty within the specified container.\",\n  \"textWrap:initial\": \"Resets the property to its initial value.\",\n  \"textWrap:inherit\":\n    \"Sets the property to the containing element's computed value.\",\n  \"textWrap:unset\": \"Resets the property to its initial value.\",\n  \"textWrapMode:auto\":\n    \"The text wraps automatically within the specified container.\",\n  \"textWrapMode:wrap\": \"The text wraps within the specified container.\",\n  \"textWrapMode:nowrap\":\n    \"The text does not wrap within the specified container.\",\n  \"textWrapMode:initial\": \"Resets the property to its initial value.\",\n  \"textWrapMode:inherit\":\n    \"Sets the property to the containing element's computed value.\",\n  \"textWrapMode:unset\": \"Resets the property to its initial value.\",\n  \"textWrapStyle:auto\": \"The text style is automatically adjusted as needed.\",\n  \"textWrapStyle:balance\":\n    \"The text style is balanced within the specified container.\",\n  \"textWrapStyle:stable\":\n    \"The text style is stable within the specified container.\",\n  \"textWrapStyle:pretty\":\n    \"The text style is pretty within the specified container.\",\n  \"textWrapStyle:initial\": \"Resets the property to its initial value.\",\n  \"textWrapStyle:inherit\":\n    \"Inherit the text wrap style from the parent element.\",\n  \"textWrapStyle:unset\": \"Resets the text wrap style to its initial value.\",\n  \"timelineScope:none\": \"Specifies that no animation should be applied.\",\n  \"timelineScope:initial\": \"Resets the property to its initial value.\",\n  \"timelineScope:inherit\":\n    \"Inherits the timeline scope from the parent element.\",\n  \"timelineScope:unset\": \"Resets the timeline scope to its initial value.\",\n  \"transition:none\": \"No transition effect is applied to the element.\",\n  \"transition:all\": \"Smooth transition effect for all properties.\",\n  \"transition:linear\": \"Linear transition effect for all properties.\",\n  \"transition:ease\": \"Transition effect with ease timing function.\",\n  \"transition:ease-in\": \"Transition effect with ease-in timing function.\",\n  \"transition:ease-out\": \"Transition effect with ease-out timing function.\",\n  \"transition:ease-in-out\":\n    \"Transition effect with ease-in-out timing function.\",\n  \"transition:step-start\":\n    \"Transition effect that jumps to the final state instantly.\",\n  \"transition:step-end\":\n    \"Transition effect that jumps to the final state gradually.\",\n  \"transition:normal\": \"Default transition effect is applied.\",\n  \"transition:allow-discrete\":\n    \"Alters the style of the view transition to discrete intervals.\",\n  \"transition:initial\": \"Resets the transition property to its initial value.\",\n  \"transition:inherit\":\n    \"Inherits the transition property from its parent element.\",\n  \"transition:unset\": \"Resets the transition property to its default value.\",\n  \"transitionBehavior:normal\":\n    \"Specifies the behavior of the view transition as normal.\",\n  \"transitionBehavior:allow-discrete\":\n    \"Alters the style of the view transition to discrete intervals.\",\n  \"transitionBehavior:initial\": \"Resets the property to its initial value.\",\n  \"transitionBehavior:inherit\":\n    \"Inherits the property from its parent element.\",\n  \"transitionBehavior:unset\": \"Resets the property to its default value.\",\n  \"viewTimelineAxis:block\":\n    \"Specifies the orientation of the view timeline as a block.\",\n  \"viewTimelineAxis:inline\":\n    \"Specifies the orientation of the view timeline as inline.\",\n  \"viewTimelineAxis:x\":\n    \"Specifies the orientation of the view timeline along the x-axis.\",\n  \"viewTimelineAxis:y\":\n    \"Specifies the orientation of the view timeline along the y-axis.\",\n  \"viewTimelineAxis:initial\": \"Resets the property to its initial value.\",\n  \"viewTimelineAxis:inherit\": \"Inherits the property from its parent element.\",\n  \"viewTimelineAxis:unset\": \"Resets the property to its default value.\",\n  \"viewTimelineInset:auto\": \"Resets the property to its default value.\",\n  \"viewTimelineInset:initial\": \"Resets the property to its initial value.\",\n  \"viewTimelineInset:inherit\":\n    \"Inherits the property value from its parent element.\",\n  \"viewTimelineInset:unset\": \"Resets the property to its initial value.\",\n  \"viewTimelineName:none\": \"Hides the timeline name from view.\",\n  \"viewTimelineName:initial\": \"Resets the property to its initial value.\",\n  \"viewTimelineName:inherit\":\n    \"Inherits the property value from its parent element.\",\n  \"viewTimelineName:unset\": \"Resets the property to its initial value.\",\n  \"viewTransitionName:none\": \"Hides the transition name from view.\",\n  \"viewTransitionName:initial\": \"Resets the property to its initial value.\",\n  \"viewTransitionName:inherit\":\n    \"Inherits the property value from its parent element.\",\n  \"viewTransitionName:unset\": \"Resets the property to its initial value.\",\n  \"whiteSpace:collapse\": \"Collapses white space within the element.\",\n  \"whiteSpace:discard\": \"Discards white space within the element.\",\n  \"whiteSpace:preserve\": \"Preserves all white space within the element.\",\n  \"whiteSpace:preserve-breaks\": \"Preserves white space allowing line breaks.\",\n  \"whiteSpace:preserve-spaces\":\n    \"The content will be displayed without any modifications.\",\n  \"whiteSpace:wrap\":\n    \"The content will wrap to fit within the specified container.\",\n  \"whiteSpace:balance\":\n    \"The content will be distributed so that lines are of equal length.\",\n  \"whiteSpace:stable\":\n    \"The content will be distributed so that lines remain stable.\",\n  \"whiteSpace:pretty\":\n    \"The content is styled with extra care for better readability.\",\n  \"whiteSpaceCollapse:collapse\": \"Collapse adjacent spaces in the content.\",\n  \"whiteSpaceCollapse:discard\": \"Discard white spaces in the content.\",\n  \"whiteSpaceCollapse:preserve\": \"Preserve the white spaces in the content.\",\n  \"whiteSpaceCollapse:preserve-breaks\":\n    \"Preserve line breaks and white spaces in the content.\",\n  \"whiteSpaceCollapse:preserve-spaces\": \"Preserve white spaces in the content.\",\n  \"whiteSpaceCollapse:break-spaces\": \"Break white spaces between text.\",\n  \"whiteSpaceCollapse:initial\": \"Sets content to the initial value.\",\n  \"whiteSpaceCollapse:inherit\":\n    \"Inherits the white-space-collapse property from the parent element.\",\n  \"whiteSpaceCollapse:unset\":\n    \"Unsets the white-space-collapse property from the parent element.\",\n  \"animationTimingFunction:ease-in-out\":\n    \"Determines the speed curve of the animation: ease-in-out.\",\n  \"cursor:no-drop\": \"Indicates that the cursor should not be used: no-drop.\",\n  \"filter:none\": \"Resets the filter property to its initial value: none.\",\n  \"filter:initial\": \"Resets the filter property to its initial value: initial.\",\n  \"filter:inherit\":\n    \"Resets the filter property to its inherited value: inherit.\",\n  \"fontVariantPosition:inherit\":\n    \"Resets the font-variant-position to its inherited value.\",\n  \"fontVariantPosition:unset\":\n    \"Resets the font-variant-position to its unset value.\",\n  \"overflowY:initial\": \"Resets the overflow-y property to its initial value.\",\n  \"overflowY:inherit\": \"Resets the overflow-y property to its inherited value.\",\n  \"overflowY:unset\": \"Resets the overflow-y property to its unset value.\",\n  \"overscrollBehavior:initial\":\n    \"Resets the overscroll-behavior to its initial value.\",\n  \"overscrollBehavior:inherit\":\n    \"Resets the overscroll-behavior to its inherited value.\",\n  \"overscrollBehavior:unset\":\n    \"Resets the overscroll-behavior to its unset value.\",\n  \"pageBreakBefore:unset\": \"Resets the page-break-before to its unset value.\",\n  \"textAlign:left\": \"Sets the text to be aligned to the left.\",\n  \"textAlign:right\": \"Sets the text to be aligned to the right.\",\n  \"textAlign:center\": \"Aligns text to the center within its container.\",\n  \"textAlign:justify\": \"Spreads text evenly across the line.\",\n  \"textAlign:match-parent\":\n    \"Aligns text to the parent element matching its alignment.\",\n  \"textAlign:initial\": \"Resets the text alignment to its default value.\",\n  \"textAlign:inherit\": \"Inherits the text alignment from its parent element.\",\n  \"textAlign:unset\":\n    \"Resets the text alignment to its default value without inheriting.\",\n  \"textAlignLast:auto\":\n    \"Automatically determines the alignment of the last line.\",\n  \"textAlignLast:start\":\n    \"Aligns the last line of text to the start of the line.\",\n  \"textDecorationLine:unset\":\n    \"Resets the text decoration line to its initial value.\",\n  \"-webkit-box-orient:horizontal\":\n    \"Specifies the box orientation as horizontal.\",\n  \"-webkit-box-orient:vertical\": \"Specifies the box orientation as vertical.\",\n  \"-webkit-line-clamp:none\": \"Limits the number of lines for text to none.\",\n  \"-webkit-line-clamp:initial\":\n    \"Resets the number of lines for text to its initial value.\",\n  \"-webkit-line-clamp:inherit\":\n    \"Sets the number of lines for text to inherit from the parent element.\",\n  \"-webkit-line-clamp:unset\":\n    \"Sets the number of lines for text to the initial value.\",\n  \"-webkit-overflow-scrolling:auto\":\n    \"Enables smooth scrolling with the overflow behavior auto.\",\n  \"-webkit-overflow-scrolling:touch\":\n    \"Enables smooth scrolling with the overflow behavior touch.\",\n  \"-webkit-overflow-scrolling:initial\":\n    \"Resets the overflow behavior to the initial value.\",\n  \"-webkit-overflow-scrolling:inherit\":\n    \"Sets the overflow behavior to inherit from the parent element.\",\n  \"-webkit-overflow-scrolling:unset\":\n    \"Sets the overflow behavior to the initial value.\",\n  \"fieldSizing:content\": \"Adjusts the sizing of form elements to content.\",\n  \"fieldSizing:fixed\": \"Adjusts the sizing of form elements to a fixed value.\",\n  \"fieldSizing:initial\":\n    \"Resets the sizing of form elements to the initial value.\",\n  \"fieldSizing:inherit\":\n    \"Sets the sizing of form elements to inherit from the parent element.\",\n  \"fieldSizing:unset\": \"Sets the sizing of form elements to the initial value.\",\n  \"zoom:normal\": \"Zooms the specified element to its initial size.\",\n  \"zoom:reset\": \"Resets the zoom level to the default value.\",\n  \"zoom:initial\": \"Sets the zoom level to the default value.\",\n  \"zoom:inherit\": \"Inherits the zoom level from the parent element.\",\n  \"zoom:unset\": \"Unsets the zoom level, allowing the browser to decide.\",\n} as Record<string, string | undefined>;\n\nexport const declarationsOverrides = {\n  \"gridAutoFlow:row dense\":\n    \"Grid items are placed along rows, filling in holes earlier in the grid when smaller items appear later.\",\n  \"gridAutoFlow:column dense\":\n    \"Grid items are placed along columns, filling in holes earlier in the grid when smaller items appear later.\",\n} as Record<string, string | undefined>;\n\nexport const declarations = {\n  ...declarationsGenerated,\n  ...declarationsOverrides,\n} as Record<string, string | undefined>;\n"
  },
  {
    "path": "packages/css-data/src/__generated__/pseudo-classes.ts",
    "content": "// This file was generated by pnpm mdn-data\nexport const pseudoClasses = [\n  \"active\",\n  \"active-view-transition\",\n  \"active-view-transition-type()\",\n  \"any-link\",\n  \"autofill\",\n  \"blank\",\n  \"buffering\",\n  \"checked\",\n  \"current\",\n  \"default\",\n  \"defined\",\n  \"dir()\",\n  \"disabled\",\n  \"empty\",\n  \"enabled\",\n  \"first\",\n  \"first-child\",\n  \"first-of-type\",\n  \"focus\",\n  \"focus-visible\",\n  \"focus-within\",\n  \"fullscreen\",\n  \"future\",\n  \"has()\",\n  \"has-slotted\",\n  \"host\",\n  \"host()\",\n  \"host-context()\",\n  \"hover\",\n  \"in-range\",\n  \"indeterminate\",\n  \"invalid\",\n  \"is()\",\n  \"lang()\",\n  \"last-child\",\n  \"last-of-type\",\n  \"left\",\n  \"link\",\n  \"local-link\",\n  \"modal\",\n  \"muted\",\n  \"not()\",\n  \"nth-child()\",\n  \"nth-last-child()\",\n  \"nth-last-of-type()\",\n  \"nth-of-type()\",\n  \"only-child\",\n  \"only-of-type\",\n  \"open\",\n  \"optional\",\n  \"out-of-range\",\n  \"past\",\n  \"paused\",\n  \"picture-in-picture\",\n  \"placeholder-shown\",\n  \"playing\",\n  \"popover-open\",\n  \"read-only\",\n  \"read-write\",\n  \"required\",\n  \"right\",\n  \"root\",\n  \"scope\",\n  \"seeking\",\n  \"stalled\",\n  \"state()\",\n  \"target\",\n  \"target-current\",\n  \"target-within\",\n  \"user-invalid\",\n  \"user-valid\",\n  \"valid\",\n  \"visited\",\n  \"volume-locked\",\n  \"where()\",\n  \"xr-overlay\",\n] as const;\n"
  },
  {
    "path": "packages/css-data/src/__generated__/pseudo-elements.ts",
    "content": "// This file was generated by pnpm mdn-data\nexport const pseudoElements = [\n  \"-ms-browse\",\n  \"-ms-check\",\n  \"-ms-clear\",\n  \"-ms-expand\",\n  \"-ms-fill\",\n  \"-ms-fill-lower\",\n  \"-ms-fill-upper\",\n  \"-ms-reveal\",\n  \"-ms-thumb\",\n  \"-ms-ticks-after\",\n  \"-ms-ticks-before\",\n  \"-ms-tooltip\",\n  \"-ms-track\",\n  \"-ms-value\",\n  \"-moz-progress-bar\",\n  \"-moz-range-progress\",\n  \"-moz-range-thumb\",\n  \"-moz-range-track\",\n  \"-webkit-progress-bar\",\n  \"-webkit-progress-inner-value\",\n  \"-webkit-progress-value\",\n  \"-webkit-slider-runnable-track\",\n  \"-webkit-slider-thumb\",\n  \"after\",\n  \"backdrop\",\n  \"before\",\n  \"checkmark\",\n  \"cue\",\n  \"cue()\",\n  \"cue-region\",\n  \"cue-region()\",\n  \"details-content\",\n  \"file-selector-button\",\n  \"first-letter\",\n  \"first-line\",\n  \"grammar-error\",\n  \"highlight()\",\n  \"marker\",\n  \"part()\",\n  \"picker-icon\",\n  \"picker()\",\n  \"placeholder\",\n  \"scroll-marker\",\n  \"scroll-marker-group\",\n  \"selection\",\n  \"slotted()\",\n  \"spelling-error\",\n  \"target-text\",\n  \"view-transition\",\n  \"view-transition-group()\",\n  \"view-transition-image-pair()\",\n  \"view-transition-new()\",\n  \"view-transition-old()\",\n] as const;\n"
  },
  {
    "path": "packages/css-data/src/__generated__/pseudo-selector-descriptions.ts",
    "content": "// This file was auto-generated\n// Descriptions for CSS pseudo-classes\nexport const pseudoClassDescriptions: Record<string, string> = {\n  \":active\":\n    \"Matches when the element is being activated, like when a button is being pressed.\",\n  \":any-link\": \"Matches links that are either visited or unvisited.\",\n  \":autofill\":\n    \"Matches form elements that have been auto-filled by the browser.\",\n  \":checked\":\n    \"Matches checkboxes, radio buttons, or options that are currently selected or toggled on.\",\n  \":default\":\n    \"Matches the default form element in a group, like a submit button.\",\n  \":defined\":\n    \"Matches any element that has been defined, including custom elements.\",\n  \":disabled\":\n    \"Matches form elements that are currently disabled and cannot be interacted with.\",\n  \":empty\": \"Matches elements that have no children, including text nodes.\",\n  \":enabled\":\n    \"Matches form elements that are currently enabled and can be interacted with.\",\n  \":first-child\": \"Matches an element that is the first child of its parent.\",\n  \":first-of-type\": \"Matches the first element of its type among siblings.\",\n  \":focus\":\n    \"Matches when the element has received focus, like when clicked or tabbed to.\",\n  \":focus-visible\":\n    \"Matches when the element has focus and the browser determines focus should be visible.\",\n  \":focus-within\":\n    \"Matches when the element or any of its descendants has focus.\",\n  \":fullscreen\":\n    \"Matches when the element is currently displayed in fullscreen mode.\",\n  \":has()\":\n    \"Matches elements that contain elements matching the specified selector.\",\n  \":hover\":\n    \"Matches when the user's pointer is over the element, like when moving a mouse cursor over it.\",\n  \":in-range\": \"Matches input elements with values within the specified range.\",\n  \":indeterminate\":\n    \"Matches form elements in an indeterminate state, like partially-checked checkboxes.\",\n  \":invalid\":\n    \"Matches form elements with invalid content according to their validation rules.\",\n  \":is()\":\n    \"Matches elements that match any of the selectors in the list. Useful for grouping.\",\n  \":lang()\":\n    \"Matches elements based on the language they are determined to be in.\",\n  \":last-child\": \"Matches an element that is the last child of its parent.\",\n  \":last-of-type\": \"Matches the last element of its type among siblings.\",\n  \":link\": \"Matches links that have not yet been visited.\",\n  \":modal\":\n    \"Matches an element that is in a modal state, blocking interaction with others.\",\n  \":not()\": \"Matches elements that do not match the specified selector.\",\n  \":nth-child()\":\n    \"Matches elements based on their position among siblings using a formula.\",\n  \":nth-last-child()\":\n    \"Matches elements based on position from the end among siblings.\",\n  \":nth-last-of-type()\":\n    \"Matches elements of the same type by position from the end.\",\n  \":nth-of-type()\":\n    \"Matches elements of the same type based on position among siblings.\",\n  \":only-child\": \"Matches an element that is the only child of its parent.\",\n  \":only-of-type\":\n    \"Matches an element that is the only one of its type among siblings.\",\n  \":open\":\n    \"Matches elements like details or dialog when they are in an open state.\",\n  \":optional\": \"Matches form elements that are not required.\",\n  \":out-of-range\":\n    \"Matches input elements with values outside the specified range.\",\n  \":placeholder-shown\":\n    \"Matches input elements currently showing placeholder text.\",\n  \":read-only\": \"Matches elements that are not editable by the user.\",\n  \":read-write\": \"Matches elements that are editable by the user.\",\n  \":required\":\n    \"Matches form elements that must be filled out before submitting.\",\n  \":root\":\n    \"Matches the root element of the document, typically the <html> element.\",\n  \":target\":\n    \"Matches an element whose ID matches the URL's fragment identifier.\",\n  \":user-invalid\":\n    \"Matches form elements with invalid input after user interaction.\",\n  \":user-valid\":\n    \"Matches form elements with valid input after user interaction.\",\n  \":valid\":\n    \"Matches form elements with valid content according to their validation rules.\",\n  \":visited\": \"Matches links that have been visited by the user.\",\n  \":where()\":\n    \"Like :is() but with zero specificity. Matches any selector in the list.\",\n  // Media-related\n  \":paused\":\n    \"Matches media elements like video or audio when playback is paused.\",\n  \":playing\":\n    \"Matches media elements like video or audio when playback is active.\",\n  \":muted\": \"Matches media elements that are currently muted.\",\n  \":buffering\": \"Matches media elements that are buffering content.\",\n  \":seeking\":\n    \"Matches media elements when the user is seeking to a new position.\",\n  \":stalled\": \"Matches media elements when the download has stalled.\",\n  \":volume-locked\": \"Matches media elements when volume cannot be changed.\",\n  // Picture-in-picture\n  \":picture-in-picture\":\n    \"Matches the element currently displayed in picture-in-picture mode.\",\n  // Popover\n  \":popover-open\": \"Matches popover elements that are currently shown.\",\n};\n\n// Descriptions for CSS pseudo-elements\nexport const pseudoElementDescriptions: Record<string, string> = {\n  \"::after\":\n    \"Creates a virtual element as the last child of the selected element for inserting content.\",\n  \"::before\":\n    \"Creates a virtual element as the first child of the selected element for inserting content.\",\n  \"::backdrop\":\n    \"Styles the box behind a fullscreen or modal element, covering the entire viewport.\",\n  \"::cue\": \"Styles the WebVTT cues (captions/subtitles) for media elements.\",\n  \"::file-selector-button\":\n    \"Styles the button of file input elements that opens the file picker.\",\n  \"::first-letter\":\n    \"Applies styles to the first letter of a block-level element.\",\n  \"::first-line\": \"Applies styles to the first line of a block-level element.\",\n  \"::grammar-error\":\n    \"Styles text that the browser has flagged as grammatically incorrect.\",\n  \"::highlight()\":\n    \"Styles custom highlighted ranges created via the CSS Custom Highlight API.\",\n  \"::marker\": \"Styles the marker box of list items, like bullets or numbers.\",\n  \"::part()\":\n    \"Styles elements within a shadow tree that have been exposed via the part attribute.\",\n  \"::placeholder\":\n    \"Styles the placeholder text in input and textarea elements.\",\n  \"::selection\":\n    \"Styles the portion of an element that is selected by the user.\",\n  \"::slotted()\": \"Styles elements placed into a slot within a shadow tree.\",\n  \"::spelling-error\": \"Styles text that the browser has flagged as misspelled.\",\n  \"::target-text\":\n    \"Styles the text fragment that was scrolled to via a URL fragment.\",\n  // View transitions\n  \"::view-transition\": \"Represents the root of the view transition overlay.\",\n  \"::view-transition-group()\":\n    \"Represents a single view transition snapshot group.\",\n  \"::view-transition-image-pair()\":\n    \"Represents the old and new views of a transition snapshot.\",\n  \"::view-transition-new()\":\n    \"Represents the new view state during a view transition.\",\n  \"::view-transition-old()\":\n    \"Represents the old view state during a view transition.\",\n  // Scroll markers\n  \"::scroll-marker\":\n    \"Styles scroll markers generated for scroll snap containers.\",\n  \"::scroll-marker-group\": \"Styles the container for scroll markers.\",\n  // Details\n  \"::details-content\":\n    \"Styles the expandable content portion of a details element.\",\n  // Picker\n  \"::picker-icon\": \"Styles the icon in picker controls like date inputs.\",\n  \"::picker()\": \"Styles the popup picker interface for form controls.\",\n  // Checkmark\n  \"::checkmark\": \"Styles the checkmark in checkbox or option elements.\",\n};\n"
  },
  {
    "path": "packages/css-data/src/__generated__/shorthand-properties.ts",
    "content": "// This file was generated by pnpm mdn-data\nexport const shorthandProperties = [\n  \"-webkit-text-stroke\",\n  \"animation\",\n  \"background\",\n  \"border\",\n  \"border-block\",\n  \"border-block-end\",\n  \"border-block-start\",\n  \"border-bottom\",\n  \"border-color\",\n  \"border-image\",\n  \"border-inline\",\n  \"border-inline-end\",\n  \"border-inline-start\",\n  \"border-left\",\n  \"border-radius\",\n  \"border-right\",\n  \"border-style\",\n  \"border-top\",\n  \"border-width\",\n  \"column-rule\",\n  \"columns\",\n  \"contain-intrinsic-size\",\n  \"container\",\n  \"flex\",\n  \"flex-flow\",\n  \"font\",\n  \"gap\",\n  \"grid\",\n  \"grid-area\",\n  \"grid-column\",\n  \"grid-row\",\n  \"grid-template\",\n  \"inset\",\n  \"inset-block\",\n  \"inset-inline\",\n  \"list-style\",\n  \"margin\",\n  \"margin-block\",\n  \"margin-inline\",\n  \"marker\",\n  \"mask\",\n  \"mask-border\",\n  \"offset\",\n  \"outline\",\n  \"padding\",\n  \"padding-block\",\n  \"padding-inline\",\n  \"place-content\",\n  \"place-items\",\n  \"place-self\",\n  \"scroll-margin\",\n  \"scroll-margin-block\",\n  \"scroll-margin-inline\",\n  \"scroll-padding\",\n  \"scroll-padding-block\",\n  \"scroll-padding-inline\",\n  \"stroke\",\n  \"text-decoration\",\n  \"text-emphasis\",\n  \"transition\",\n] as const;\n"
  },
  {
    "path": "packages/css-data/src/__generated__/units.ts",
    "content": "// This file was generated by pnpm mdn-data\nexport const units = {\n  number: [],\n  percentage: [\"%\"],\n  angle: [\"deg\", \"grad\", \"rad\", \"turn\"],\n  decibel: [\"db\"],\n  flex: [\"fr\"],\n  frequency: [\"hz\", \"khz\"],\n  length: [\n    \"cm\",\n    \"mm\",\n    \"q\",\n    \"in\",\n    \"pt\",\n    \"pc\",\n    \"px\",\n    \"em\",\n    \"rem\",\n    \"ex\",\n    \"rex\",\n    \"cap\",\n    \"rcap\",\n    \"ch\",\n    \"rch\",\n    \"ic\",\n    \"ric\",\n    \"lh\",\n    \"rlh\",\n    \"vw\",\n    \"svw\",\n    \"lvw\",\n    \"dvw\",\n    \"vh\",\n    \"svh\",\n    \"lvh\",\n    \"dvh\",\n    \"vi\",\n    \"svi\",\n    \"lvi\",\n    \"dvi\",\n    \"vb\",\n    \"svb\",\n    \"lvb\",\n    \"dvb\",\n    \"vmin\",\n    \"svmin\",\n    \"lvmin\",\n    \"dvmin\",\n    \"vmax\",\n    \"svmax\",\n    \"lvmax\",\n    \"dvmax\",\n    \"cqw\",\n    \"cqh\",\n    \"cqi\",\n    \"cqb\",\n    \"cqmin\",\n    \"cqmax\",\n  ],\n  resolution: [\"dpi\", \"dpcm\", \"dppx\", \"x\"],\n  semitones: [\"st\"],\n  time: [\"s\", \"ms\"],\n} as const;\n"
  },
  {
    "path": "packages/css-data/src/custom-data.ts",
    "content": "import type { StyleValue } from \"@webstudio-is/css-engine\";\n\n// Data type used before we generate a the constants.\ntype RawPropertyData = {\n  unitGroups: Array<string>;\n  inherited: boolean;\n  initial: StyleValue;\n  mdnUrl?: string;\n};\n\nexport const propertiesData: { [property: string]: RawPropertyData } = {};\nexport const keywordValues: { [property: string]: Array<string> } = {};\n\npropertiesData[\"-webkit-font-smoothing\"] = {\n  unitGroups: [],\n  inherited: true,\n  initial: {\n    type: \"keyword\",\n    value: \"auto\",\n  },\n  mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth\",\n};\nkeywordValues[\"-webkit-font-smoothing\"] = [\n  \"auto\",\n  \"none\",\n  \"antialiased\",\n  \"subpixel-antialiased\",\n];\n\npropertiesData[\"-moz-osx-font-smoothing\"] = {\n  unitGroups: [],\n  inherited: true,\n  initial: {\n    type: \"keyword\",\n    value: \"auto\",\n  },\n  mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth\",\n};\nkeywordValues[\"-moz-osx-font-smoothing\"] = [\"auto\", \"grayscale\"];\n\npropertiesData[\"-webkit-box-orient\"] = {\n  unitGroups: [],\n  inherited: false,\n  initial: { type: \"keyword\", value: \"horizontal\" },\n  mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/box-orient\",\n};\nkeywordValues[\"-webkit-box-orient\"] = [\"horizontal\", \"vertical\"];\n\npropertiesData[\"view-timeline-name\"] = {\n  unitGroups: [],\n  inherited: false,\n  initial: {\n    type: \"keyword\",\n    value: \"none\",\n  },\n  mdnUrl: \"https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-name\",\n};\nkeywordValues[\"view-timeline-name\"] = [];\npropertiesData[\"scroll-timeline-name\"] = {\n  unitGroups: [],\n  inherited: false,\n  initial: {\n    type: \"keyword\",\n    value: \"none\",\n  },\n  mdnUrl:\n    \"https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-timeline-name\",\n};\nkeywordValues[\"scroll-timeline-name\"] = [];\n\npropertiesData[\"view-timeline-inset\"] = {\n  unitGroups: [\"length\", \"percentage\"],\n  inherited: false,\n  initial: {\n    type: \"keyword\",\n    value: \"auto\",\n  },\n  mdnUrl:\n    \"https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-inset\",\n};\nkeywordValues[\"view-timeline-inset\"] = [];\n\n// Add combined values for grid-auto-flow that mdn-data doesn't include\nkeywordValues[\"grid-auto-flow\"] = [\n  \"row\",\n  \"column\",\n  \"dense\",\n  \"row dense\",\n  \"column dense\",\n  \"initial\",\n  \"inherit\",\n  \"unset\",\n];\n\nkeywordValues[\"list-style-type\"] = [\n  \"disc\",\n  \"circle\",\n  \"square\",\n  \"decimal\",\n  \"georgian\",\n  \"trad-chinese-informal\",\n  \"kannada\",\n  \"none\",\n  \"initial\",\n  \"inherit\",\n  \"unset\",\n];\n\nexport const customLonghandPropertyNames = [\n  \"boxShadowOffsetX\",\n  \"boxShadowOffsetY\",\n  \"boxShadowBlurRadius\",\n  \"boxShadowSpreadRadius\",\n  \"boxShadowColor\",\n  \"boxShadowPosition\",\n  \"textShadowOffsetX\",\n  \"textShadowOffsetY\",\n  \"textShadowBlurRadius\",\n  \"textShadowColor\",\n  \"dropShadowOffsetX\",\n  \"dropShadowOffsetY\",\n  \"dropShadowBlurRadius\",\n  \"dropShadowColor\",\n  \"translateX\",\n  \"translateY\",\n  \"translateZ\",\n  \"rotateX\",\n  \"rotateY\",\n  \"rotateZ\",\n  \"scaleX\",\n  \"scaleY\",\n  \"scaleZ\",\n  \"skewX\",\n  \"skewY\",\n  \"transformOriginX\",\n  \"transformOriginY\",\n  \"transformOriginZ\",\n  \"perspectiveOriginX\",\n  \"perspectiveOriginY\",\n] as const;\n"
  },
  {
    "path": "packages/css-data/src/html.css",
    "content": "/*\n\nall styles are taken from following source\nhttps://searchfox.org/mozilla-central/source/layout/style/res/html.css\nhttps://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css\nhttps://trac.webkit.org/browser/trunk/Source/WebCore/css/html.css\n\n*/\n\n/* blocks */\n\narticle,\naside,\ndetails,\ndiv,\ndt,\nfigcaption,\nfooter,\nform,\nheader,\nhgroup,\nhtml,\nmain,\nnav,\nsection,\nsummary {\n  display: block;\n}\n\nbody {\n  display: block;\n  margin: 8px;\n}\n\np,\ndl {\n  display: block;\n  margin-top: 1em;\n  margin-bottom: 1em;\n}\n\ndd {\n  display: block;\n  margin-left: 40px;\n}\n\nblockquote,\nfigure {\n  display: block;\n  margin: 1em 40px;\n}\n\naddress {\n  display: block;\n  font-style: italic;\n}\n\n/*\n\nh1 font-size, margin-top and margin-bottom depend on parent tags\nso better define statically in preset styles\n\n*/\n\nh1 {\n  display: block;\n  font-weight: bold;\n  font-size: 2em;\n  margin-top: 0.67em;\n  margin-bottom: 0.67em;\n}\n\nh2 {\n  display: block;\n  font-weight: bold;\n  font-size: 1.5em;\n  margin-top: 0.83em;\n  margin-bottom: 0.83em;\n}\n\nh3 {\n  display: block;\n  font-weight: bold;\n  font-size: 1.17em;\n  margin-top: 1em;\n  margin-bottom: 1em;\n}\n\nh4 {\n  display: block;\n  font-weight: bold;\n  margin-top: 1.33em;\n  margin-bottom: 1.33em;\n}\n\nh5 {\n  display: block;\n  font-weight: bold;\n  font-size: 0.83em;\n  margin-top: 1.67em;\n  margin-bottom: 1.67em;\n}\n\nh6 {\n  display: block;\n  font-weight: bold;\n  font-size: 0.67em;\n  margin-top: 2.33em;\n  margin-bottom: 2.33em;\n}\n\npre {\n  display: block;\n  white-space-collapse: preserve;\n  text-wrap-mode: nowrap;\n  margin-top: 1em;\n  margin-bottom: 1em;\n}\n\n/* tables */\n\ntable {\n  display: table;\n  border-spacing: 2px;\n  border-collapse: separate;\n  box-sizing: border-box;\n  text-indent: 0;\n}\n\ncaption {\n  display: table-caption;\n  text-align: center;\n}\n\ntr {\n  display: table-row;\n  vertical-align: inherit;\n}\n\ncol {\n  display: table-column;\n}\n\ncolgroup {\n  display: table-column-group;\n}\n\ntbody {\n  display: table-row-group;\n  vertical-align: middle;\n}\n\nthead {\n  display: table-header-group;\n  vertical-align: middle;\n}\n\ntfoot {\n  display: table-footer-group;\n  vertical-align: middle;\n}\n\ntd {\n  display: table-cell;\n  vertical-align: inherit;\n  padding: 1px;\n}\n\nth {\n  display: table-cell;\n  vertical-align: inherit;\n  font-weight: bold;\n  padding: 1px;\n}\n\n/* inlines */\n\nb,\nstrong {\n  /* in firefox defined as bolder */\n  font-weight: bold;\n}\n\ni,\ncite,\nem,\nvar,\ndfn {\n  font-style: italic;\n}\n\ncode,\nkbd,\nsamp {\n  /* in firefox defined as -moz-fixed */\n  font-family: monospace;\n}\n\nmark {\n  /* in firefox defined as Mark */\n  background-color: yellow;\n  /* in firefox defined as MarkText */\n  color: black;\n}\n\nu,\nins {\n  text-decoration-line: underline;\n}\n\ns,\ndel {\n  text-decoration-line: line-through;\n}\n\nsub {\n  vertical-align: sub;\n  font-size: smaller;\n}\n\nsup {\n  vertical-align: super;\n  font-size: smaller;\n}\n\n/*\n\nactive and visited states are not defined as usually overriden with stateless color\nand modeling var-like defaults is too complex\n\n*/\na {\n  text-decoration-line: underline;\n  cursor: pointer;\n  color: rgb(0 0 238 / 1);\n}\n\n/* lists */\n\n/*\n\nnested lists have no top/bottom margins\nso better redefine statically in preset\n\n*/\n\nul {\n  display: block;\n  list-style-type: disc;\n  margin-top: 1em;\n  margin-bottom: 1em;\n  padding-left: 40px;\n}\n\nol {\n  display: block;\n  list-style-type: decimal;\n  margin-top: 1em;\n  margin-bottom: 1em;\n  padding-left: 40px;\n}\n\nli {\n  display: list-item;\n  text-align: match-parent;\n}\n\n/* leafs */\n\nhr {\n  color: gray;\n  border-width: 1px;\n  /* in browsers defined as inset */\n  border-style: solid;\n  margin: 0.5em auto;\n  /* firefox only */\n  overflow: hidden;\n  /* This is not really per spec but all browsers define it */\n  display: block;\n}\n\n/**\n *\nforms\nhttps://searchfox.org/mozilla-central/source/layout/style/res/forms.css\n\n*/\n\nlegend {\n  display: block;\n  padding-left: 2px;\n  padding-right: 2px;\n}\n\nfieldset {\n  display: block;\n  margin-left: 2px;\n  margin-right: 2px;\n  padding: 0.35em 0.75em 0.625em;\n  /* in browsers defined as groove style with ThreeDFace color */\n  border: 2px solid lightgray;\n  min-width: min-content;\n}\n\nlabel {\n  cursor: default;\n}\n\ninput {\n  appearance: auto;\n  padding: 1px;\n  border-width: 2px;\n  /* in browsers defined as inset */\n  border-style: solid;\n  /* in firefox defined as Field */\n  background-color: white;\n  cursor: text;\n}\n\n/* font controls reset */\ntextarea,\nselect,\nbutton {\n  /* in firefox defined as FieldText */\n  color: initial;\n  letter-spacing: normal;\n  word-spacing: normal;\n  line-height: normal;\n  text-transform: none;\n  text-indent: 0;\n  text-shadow: none;\n  display: inline-block;\n}\n\ntextarea {\n  text-align: start;\n  appearance: auto;\n  margin-top: 1px;\n  margin-bottom: 1px;\n  /* in firefox 2px */\n  border-width: 1px;\n  /*  in browsers defined as inset */\n  border-style: solid;\n  padding: 2px;\n  /* in firefox defined as Field */\n  background-color: white;\n  vertical-align: text-bottom;\n  cursor: text;\n  resize: both;\n  white-space-collapse: preserve;\n  text-wrap-mode: wrap;\n  word-wrap: break-word;\n}\n\nselect {\n  text-align: start;\n  margin: 0;\n  padding: 1px 4px;\n  border-width: 2px;\n  /* in browsers defined as inset */\n  border-style: solid;\n  text-wrap-mode: nowrap;\n  word-wrap: normal;\n  cursor: default;\n  box-sizing: border-box;\n  user-select: none;\n  overflow: clip;\n  vertical-align: baseline;\n  appearance: auto;\n}\n\noption {\n  display: block;\n  float: none;\n  position: static;\n  min-height: 1em;\n  padding: 2px 2px 2px 4px;\n  user-select: none;\n  text-wrap-mode: nowrap;\n  word-wrap: normal;\n}\n\nbutton {\n  appearance: auto;\n  /* in firefox defined as 1px 8px */\n  padding: 2px 6px 3px;\n  border-width: 2px;\n  /* in browsers defined as outset */\n  border-style: solid;\n  cursor: default;\n  box-sizing: border-box;\n  user-select: none;\n  text-align: center;\n  background-color: lightgray;\n}\n"
  },
  {
    "path": "packages/css-data/src/index.ts",
    "content": "export { html } from \"./__generated__/html\";\nexport * from \"./__generated__/keyword-values\";\nexport * from \"./__generated__/units\";\nexport {\n  properties as propertyDescriptions,\n  declarations as declarationDescriptions,\n  propertySyntaxesGenerated as propertySyntaxes,\n} from \"./__generated__/property-value-descriptions\";\nexport * from \"./__generated__/animatable-properties\";\nexport * from \"./__generated__/pseudo-elements\";\nexport * from \"./__generated__/pseudo-classes\";\nexport {\n  pseudoClassDescriptions,\n  pseudoElementDescriptions,\n} from \"./__generated__/pseudo-selector-descriptions\";\nexport * from \"./property-parsers\";\n\n// shorthand property parsers\nexport * from \"./parse-css-value\";\nexport * from \"./parse-css\";\nexport * from \"./shorthands\";\nexport * from \"./media-condition-simulator\";\nexport { shorthandProperties } from \"./__generated__/shorthand-properties\";\n\nexport { properties as propertiesData } from \"./__generated__/properties\";\n\n// Utility functions\nimport { pseudoElements } from \"./__generated__/pseudo-elements\";\n\n/**\n * Check if a state string represents a pseudo-element (e.g., \"::before\", \"::after\")\n * rather than a pseudo-class (e.g., \":hover\", \":focus\")\n */\nexport const isPseudoElement = (state: string): boolean => {\n  if (!state) {\n    return false;\n  }\n\n  // Pseudo-elements start with :: (or single : for legacy syntax)\n  // Remove the colons and check against the list\n  const normalized = state.replace(/^::?/, \"\");\n  return (pseudoElements as readonly string[]).includes(normalized);\n};\n\nexport {\n  validateSelector,\n  type SelectorValidationResult,\n} from \"./selector-validation\";\n"
  },
  {
    "path": "packages/css-data/src/media-condition-simulator.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { parseMediaCondition } from \"./media-condition-simulator\";\n\ndescribe(\"parseMediaCondition\", () => {\n  test(\"parses simple condition without parentheses\", () => {\n    const result = parseMediaCondition(\"prefers-color-scheme:dark\");\n    expect(result).toEqual({ feature: \"prefers-color-scheme\", value: \"dark\" });\n  });\n\n  test(\"parses condition with parentheses\", () => {\n    const result = parseMediaCondition(\"(prefers-color-scheme: dark)\");\n    expect(result).toEqual({ feature: \"prefers-color-scheme\", value: \"dark\" });\n  });\n\n  test(\"parses width condition\", () => {\n    const result = parseMediaCondition(\"min-width:768px\");\n    expect(result).toEqual({ feature: \"min-width\", value: \"768px\" });\n  });\n\n  test(\"parses hover condition\", () => {\n    const result = parseMediaCondition(\"hover:none\");\n    expect(result).toEqual({ feature: \"hover\", value: \"none\" });\n  });\n\n  test(\"parses orientation condition\", () => {\n    const result = parseMediaCondition(\"orientation:portrait\");\n    expect(result).toEqual({ feature: \"orientation\", value: \"portrait\" });\n  });\n\n  test(\"normalizes feature and value to lowercase\", () => {\n    const result = parseMediaCondition(\"Prefers-Color-Scheme:DARK\");\n    expect(result).toEqual({ feature: \"prefers-color-scheme\", value: \"dark\" });\n  });\n\n  test(\"handles spaces around colon\", () => {\n    const result = parseMediaCondition(\"prefers-color-scheme : dark\");\n    expect(result).toEqual({ feature: \"prefers-color-scheme\", value: \"dark\" });\n  });\n\n  test(\"returns undefined for invalid condition\", () => {\n    expect(parseMediaCondition(\"invalid\")).toBeUndefined();\n    expect(parseMediaCondition(\"\")).toBeUndefined();\n    expect(parseMediaCondition(\":::\")).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/media-condition-simulator.ts",
    "content": "import * as csstree from \"css-tree\";\n\n/**\n * Parse a condition string like \"prefers-color-scheme:dark\" into feature and value.\n */\nexport const parseMediaCondition = (\n  condition: string\n): { feature: string; value: string } | undefined => {\n  try {\n    const queryText = condition.startsWith(\"(\") ? condition : `(${condition})`;\n    const ast = csstree.parse(queryText, { context: \"mediaQuery\" });\n\n    let feature: string | undefined;\n    let value: string | undefined;\n\n    csstree.walk(ast, (node) => {\n      if (node.type === \"Feature\" && node.value) {\n        feature = node.name.toLowerCase();\n        value = csstree.generate(node.value).toLowerCase();\n      }\n    });\n\n    if (feature !== undefined && value !== undefined) {\n      return { feature, value };\n    }\n  } catch {\n    return;\n  }\n};\n"
  },
  {
    "path": "packages/css-data/src/parse-css-value.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { parseCssValue } from \"./parse-css-value\";\nimport { toValue, type CssProperty } from \"@webstudio-is/css-engine\";\n\ndescribe(\"Parse CSS value\", () => {\n  describe(\"number value\", () => {\n    test(\"unitless\", () => {\n      expect(parseCssValue(\"line-height\", \"10\")).toEqual({\n        type: \"unit\",\n        unit: \"number\",\n        value: 10,\n      });\n    });\n  });\n\n  describe(\"unit value\", () => {\n    test(\"with unit\", () => {\n      expect(parseCssValue(\"width\", \"10px\")).toEqual({\n        type: \"unit\",\n        unit: \"px\",\n        value: 10,\n      });\n    });\n\n    test(\"empty input\", () => {\n      expect(parseCssValue(\"width\", \"\")).toEqual({\n        type: \"invalid\",\n        value: \"\",\n      });\n    });\n  });\n\n  describe(\"keyword value\", () => {\n    test(\"keyword\", () => {\n      expect(parseCssValue(\"width\", \"auto\")).toEqual({\n        type: \"keyword\",\n        value: \"auto\",\n      });\n    });\n\n    test(\"keyword display block\", () => {\n      expect(parseCssValue(\"display\", \"block\")).toEqual({\n        type: \"keyword\",\n        value: \"block\",\n      });\n    });\n\n    test(\"keyword with unit\", () => {\n      expect(parseCssValue(\"width\", \"autopx\")).toEqual({\n        type: \"invalid\",\n        value: \"autopx\",\n      });\n    });\n\n    test(\"invalid\", () => {\n      // This will return px as a fallback unit, as number is not valid for width.\n      expect(parseCssValue(\"width\", \"10\")).toEqual({\n        type: \"invalid\",\n        value: \"10\",\n      });\n    });\n  });\n\n  describe(\"Unparesd valid values\", () => {\n    test(\"Simple valid function values\", () => {\n      expect(parseCssValue(\"width\", \"calc(4px + 16em)\")).toEqual({\n        type: \"unparsed\",\n        value: \"calc(4px + 16em)\",\n      });\n    });\n\n    test(\"Invalid function values\", () => {\n      expect(parseCssValue(\"width\", \"blur(4)\")).toEqual({\n        type: \"invalid\",\n        value: \"blur(4)\",\n      });\n    });\n  });\n\n  describe(\"Tuples\", () => {\n    test(\"objectPosition\", () => {\n      expect(parseCssValue(\"object-position\", \"left top\")).toEqual({\n        type: \"tuple\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"left\",\n          },\n          {\n            type: \"keyword\",\n            value: \"top\",\n          },\n        ],\n      });\n    });\n  });\n\n  describe(\"Colors\", () => {\n    test(\"Color rgba values\", () => {\n      expect(parseCssValue(\"background-color\", \"rgba(0,0,0,0)\")).toEqual({\n        type: \"color\",\n        colorSpace: \"srgb\",\n        alpha: 0,\n        components: [0, 0, 0],\n      });\n    });\n\n    test(\"modern format\", () => {\n      expect(parseCssValue(\"background-color\", \"rgb(99 102 241/0.5)\")).toEqual({\n        type: \"color\",\n        colorSpace: \"srgb\",\n        alpha: 0.5,\n        components: [0.3882, 0.4, 0.9451],\n      });\n    });\n\n    test(\"Color rgba values\", () => {\n      expect(parseCssValue(\"background-color\", \"#00220011\")).toEqual({\n        type: \"color\",\n        colorSpace: \"srgb\",\n        alpha: 0.0667,\n        components: [0, 0.1333, 0],\n      });\n    });\n\n    test(\"Color rgba values\", () => {\n      expect(parseCssValue(\"color\", \"red\")).toEqual({\n        type: \"keyword\",\n        value: \"red\",\n      });\n    });\n\n    test(\"oklch color\", () => {\n      expect(\n        parseCssValue(\"background-color\", \"oklch(59.686% 0.1009 29.234)\")\n      ).toEqual({\n        type: \"color\",\n        colorSpace: \"oklch\",\n        alpha: 1,\n        components: [0.5969, 0.1009, 29.234],\n      });\n    });\n\n    test(\"oklch color with alpha\", () => {\n      expect(\n        parseCssValue(\"background-color\", \"oklch(59.686% 0.1009 29.234 / 0.5)\")\n      ).toEqual({\n        type: \"color\",\n        colorSpace: \"oklch\",\n        alpha: 0.5,\n        components: [0.5969, 0.1009, 29.234],\n      });\n    });\n\n    test(\"oklch color with modern format\", () => {\n      expect(parseCssValue(\"color\", \"oklch(80% 0.15 240)\")).toEqual({\n        type: \"color\",\n        colorSpace: \"oklch\",\n        alpha: 1,\n        components: [0.8, 0.15, 240],\n      });\n    });\n  });\n\n  test(\"fallback to unparsed type when color has variable inside\", () => {\n    expect(\n      parseCssValue(\"color\", \"rgb(24 24 27 / var(--tw-bg-opacity))\")\n    ).toEqual({\n      type: \"unparsed\",\n      value: \"rgb(24 24 27 / var(--tw-bg-opacity))\",\n    });\n  });\n});\n\ntest(\"parse background-image property as layers\", () => {\n  expect(\n    parseCssValue(\n      \"background-image\",\n      `linear-gradient(180deg, hsla(0, 0.00%, 0.00%, 0.11), white), url(\"https://667d0b7769e0cc3754b584f6\"), none, url(\"https://667d0fe180995eadc1534a26\")`\n    )\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      {\n        type: \"unparsed\",\n        value: \"linear-gradient(180deg,hsla(0,0.00%,0.00%,0.11),white)\",\n      },\n      {\n        type: \"image\",\n        value: { type: \"url\", url: \"https://667d0b7769e0cc3754b584f6\" },\n      },\n      {\n        type: \"keyword\",\n        value: \"none\",\n      },\n      {\n        type: \"image\",\n        value: { type: \"url\", url: \"https://667d0fe180995eadc1534a26\" },\n      },\n    ],\n  });\n});\n\ntest(\"parse background-position-* properties as layers\", () => {\n  expect(\n    parseCssValue(\"background-position-x\", `0px, 550px, 0px, 0px`)\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unit\", unit: \"px\", value: 0 },\n      { type: \"unit\", unit: \"px\", value: 550 },\n      { type: \"unit\", unit: \"px\", value: 0 },\n      { type: \"unit\", unit: \"px\", value: 0 },\n    ],\n  });\n  expect(parseCssValue(\"background-position-y\", `0px, 0px, 0px, 0px`)).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unit\", unit: \"px\", value: 0 },\n      { type: \"unit\", unit: \"px\", value: 0 },\n      { type: \"unit\", unit: \"px\", value: 0 },\n      { type: \"unit\", unit: \"px\", value: 0 },\n    ],\n  });\n});\n\ntest(\"parse background-size property as layers\", () => {\n  expect(parseCssValue(\"background-size\", `auto, contain, auto, auto`)).toEqual(\n    {\n      type: \"layers\",\n      value: [\n        { type: \"keyword\", value: \"auto\" },\n        { type: \"keyword\", value: \"contain\" },\n        { type: \"keyword\", value: \"auto\" },\n        { type: \"keyword\", value: \"auto\" },\n      ],\n    }\n  );\n  expect(\n    parseCssValue(\"background-repeat\", `repeat, no-repeat, repeat, repeat`)\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"keyword\", value: \"repeat\" },\n      { type: \"keyword\", value: \"no-repeat\" },\n      { type: \"keyword\", value: \"repeat\" },\n      { type: \"keyword\", value: \"repeat\" },\n    ],\n  });\n});\n\ntest(\"parse background-attachment property as layers\", () => {\n  expect(\n    parseCssValue(\"background-attachment\", `scroll, fixed, scroll, scroll`)\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"keyword\", value: \"scroll\" },\n      { type: \"keyword\", value: \"fixed\" },\n      { type: \"keyword\", value: \"scroll\" },\n      { type: \"keyword\", value: \"scroll\" },\n    ],\n  });\n});\n\ntest(\"parse repeated value with css wide keywords\", () => {\n  expect(parseCssValue(\"background-attachment\", \"initial\")).toEqual({\n    type: \"keyword\",\n    value: \"initial\",\n  });\n  expect(parseCssValue(\"background-attachment\", \"INHERIT\")).toEqual({\n    type: \"keyword\",\n    value: \"inherit\",\n  });\n  expect(parseCssValue(\"background-attachment\", \"unset\")).toEqual({\n    type: \"keyword\",\n    value: \"unset\",\n  });\n  expect(parseCssValue(\"background-attachment\", \"revert\")).toEqual({\n    type: \"keyword\",\n    value: \"revert\",\n  });\n  expect(parseCssValue(\"background-attachment\", \"revert-layer\")).toEqual({\n    type: \"keyword\",\n    value: \"revert-layer\",\n  });\n});\n\ntest(\"parse transition-property property\", () => {\n  expect(parseCssValue(\"transition-property\", \"none\")).toEqual({\n    type: \"keyword\",\n    value: \"none\",\n  });\n  expect(parseCssValue(\"transition-property\", \"opacity, width, all\")).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unparsed\", value: \"opacity\" },\n      { type: \"unparsed\", value: \"width\" },\n      { type: \"keyword\", value: \"all\" },\n    ],\n  });\n  expect(\n    parseCssValue(\"transition-property\", \"opacity, none, unknown\")\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unparsed\", value: \"opacity\" },\n      { type: \"unparsed\", value: \"none\" },\n      { type: \"unparsed\", value: \"unknown\" },\n    ],\n  });\n});\n\ntest(\"parse transition-duration property\", () => {\n  expect(parseCssValue(\"transition-duration\", `10ms, 10ms`)).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"unit\", unit: \"ms\", value: 10 },\n      { type: \"unit\", unit: \"ms\", value: 10 },\n    ],\n  });\n  expect(parseCssValue(\"transition-duration\", `10ms, foo`)).toEqual({\n    type: \"invalid\",\n    value: \"10ms, foo\",\n  });\n});\n\ntest(\"parse transition-timing-function property\", () => {\n  const parsedValue = parseCssValue(\n    \"transition-timing-function\",\n    \"ease, ease-in, cubic-bezier(0.68,-0.6,.32,1.6), steps(4, jump-start)\"\n  );\n  expect(parsedValue).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"keyword\", value: \"ease\" },\n      { type: \"keyword\", value: \"ease-in\" },\n      {\n        type: \"function\",\n        name: \"cubic-bezier\",\n        args: {\n          type: \"layers\",\n          value: [\n            { type: \"unit\", value: 0.68, unit: \"number\" },\n            { type: \"unit\", value: -0.6, unit: \"number\" },\n            { type: \"unit\", value: 0.32, unit: \"number\" },\n            { type: \"unit\", value: 1.6, unit: \"number\" },\n          ],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"steps\",\n        args: {\n          type: \"layers\",\n          value: [\n            { type: \"unit\", value: 4, unit: \"number\" },\n            { type: \"keyword\", value: \"jump-start\" },\n          ],\n        },\n      },\n    ],\n  });\n  expect(toValue(parsedValue)).toMatchInlineSnapshot(\n    `\"ease, ease-in, cubic-bezier(0.68, -0.6, 0.32, 1.6), steps(4, jump-start)\"`\n  );\n  expect(parseCssValue(\"transition-timing-function\", \"ease, testing\")).toEqual({\n    type: \"invalid\",\n    value: \"ease, testing\",\n  });\n  expect(\n    parseCssValue(\"transition-timing-function\", \"linear(0 0%, 1 100%)\")\n  ).toEqual({\n    type: \"layers\",\n    value: [{ type: \"unparsed\", value: \"linear(0 0%,1 100%)\" }],\n  });\n});\n\ntest(\"parse transition-behavior property as layers\", () => {\n  expect(parseCssValue(\"transition-behavior\", `normal`)).toEqual({\n    type: \"layers\",\n    value: [{ type: \"keyword\", value: \"normal\" }],\n  });\n  expect(\n    parseCssValue(\"transition-behavior\", `NORMAL, allow-discrete`)\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"keyword\", value: \"normal\" },\n      { type: \"keyword\", value: \"allow-discrete\" },\n    ],\n  });\n  expect(parseCssValue(\"transition-behavior\", `normal, invalid`)).toEqual({\n    type: \"invalid\",\n    value: \"normal, invalid\",\n  });\n});\n\ntest(\"parse unknown properties as unparsed\", () => {\n  expect(parseCssValue(\"animation-timeline\" as CssProperty, \"auto\")).toEqual({\n    type: \"unparsed\",\n    value: \"auto\",\n  });\n  expect(\n    parseCssValue(\"animation-range-start\" as CssProperty, \"normal\")\n  ).toEqual({\n    type: \"unparsed\",\n    value: \"normal\",\n  });\n  expect(parseCssValue(\"animation-range-end\" as CssProperty, \"normal\")).toEqual(\n    {\n      type: \"unparsed\",\n      value: \"normal\",\n    }\n  );\n  expect(\n    parseCssValue(\"animation-timing-function\", \"linear(0 0%, 1 100%)\")\n  ).toEqual({ type: \"unparsed\", value: \"linear(0 0%, 1 100%)\" });\n});\n\ntest(\"parse transform property as tuple\", () => {\n  expect(\n    parseCssValue(\"transform\", \"rotateX(45deg) rotateY(30deg) rotateZ(60deg)\")\n  ).toEqual({\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"rotateX\",\n        args: {\n          type: \"layers\",\n          value: [{ type: \"unit\", value: 45, unit: \"deg\" }],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"rotateY\",\n        args: {\n          type: \"layers\",\n          value: [{ type: \"unit\", value: 30, unit: \"deg\" }],\n        },\n      },\n      {\n        type: \"function\",\n        name: \"rotateZ\",\n        args: {\n          type: \"layers\",\n          value: [{ type: \"unit\", value: 60, unit: \"deg\" }],\n        },\n      },\n    ],\n  });\n\n  expect(parseCssValue(\"transform\", \"skew(30deg, 20deg)\")).toEqual({\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"skew\",\n        args: {\n          type: \"layers\",\n          value: [\n            { type: \"unit\", value: 30, unit: \"deg\" },\n            { type: \"unit\", value: 20, unit: \"deg\" },\n          ],\n        },\n      },\n    ],\n  });\n\n  expect(\n    parseCssValue(\"transform\", \"translate3d(-100px, 50px, -150px)\")\n  ).toEqual({\n    type: \"tuple\",\n    value: [\n      {\n        type: \"function\",\n        name: \"translate3d\",\n        args: {\n          type: \"layers\",\n          value: [\n            { type: \"unit\", value: -100, unit: \"px\" },\n            { type: \"unit\", value: 50, unit: \"px\" },\n            { type: \"unit\", value: -150, unit: \"px\" },\n          ],\n        },\n      },\n    ],\n  });\n});\n\ntest(\"parses transform values and returns invalid for invalid values\", () => {\n  expect(parseCssValue(\"transform\", \"scale(1.5, 50px)\")).toEqual({\n    type: \"invalid\",\n    value: \"scale(1.5, 50px)\",\n  });\n\n  expect(parseCssValue(\"transform\", \"matrix(1, 0.5, -0.5, 1, 100)\")).toEqual({\n    type: \"invalid\",\n    value: \"matrix(1, 0.5, -0.5, 1, 100)\",\n  });\n});\n\ntest(\"parses a valid translate value\", () => {\n  expect(parseCssValue(\"translate\", \"100px\")).toEqual({\n    type: \"tuple\",\n    value: [{ type: \"unit\", unit: \"px\", value: 100 }],\n  });\n  expect(parseCssValue(\"translate\", \"100px 200px\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unit\", unit: \"px\", value: 100 },\n      { type: \"unit\", unit: \"px\", value: 200 },\n    ],\n  });\n  expect(parseCssValue(\"translate\", \"10em 10em 10em\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unit\", unit: \"em\", value: 10 },\n      { type: \"unit\", unit: \"em\", value: 10 },\n      { type: \"unit\", unit: \"em\", value: 10 },\n    ],\n  });\n});\n\ntest(\"parses and returns invalid for invalid translate values\", () => {\n  expect(parseCssValue(\"translate\", \"foo bar\")).toEqual({\n    type: \"invalid\",\n    value: \"foo bar\",\n  });\n  expect(parseCssValue(\"translate\", \"100px 200px 300px 400px\")).toEqual({\n    type: \"invalid\",\n    value: \"100px 200px 300px 400px\",\n  });\n  expect(parseCssValue(\"translate\", \"100%, 200%\")).toEqual({\n    type: \"invalid\",\n    value: \"100%, 200%\",\n  });\n});\n\ntest(\"parses a valid scale value\", () => {\n  expect(parseCssValue(\"scale\", \"1.5\")).toEqual({\n    type: \"tuple\",\n    value: [{ type: \"unit\", value: 1.5, unit: \"number\" }],\n  });\n  expect(parseCssValue(\"scale\", \"5 10 15\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unit\", value: 5, unit: \"number\" },\n      { type: \"unit\", value: 10, unit: \"number\" },\n      { type: \"unit\", value: 15, unit: \"number\" },\n    ],\n  });\n  expect(parseCssValue(\"scale\", \"50%\")).toEqual({\n    type: \"tuple\",\n    value: [{ type: \"unit\", value: 50, unit: \"%\" }],\n  });\n});\n\ntest(\"throws error for invalid scale proeprty values\", () => {\n  expect(parseCssValue(\"scale\", \"10 foo\")).toEqual({\n    type: \"invalid\",\n    value: \"10 foo\",\n  });\n  expect(parseCssValue(\"scale\", \"5 10 15 20\")).toEqual({\n    type: \"invalid\",\n    value: \"5 10 15 20\",\n  });\n  expect(parseCssValue(\"scale\", \"5, 15\")).toEqual({\n    type: \"invalid\",\n    value: \"5, 15\",\n  });\n  expect(parseCssValue(\"scale\", \"5px\")).toEqual({\n    type: \"invalid\",\n    value: \"5px\",\n  });\n});\n\ntest(\"support custom properties as unparsed values\", () => {\n  expect(parseCssValue(\"--my-property\", \"blue\")).toEqual({\n    type: \"unparsed\",\n    value: \"blue\",\n  });\n  expect(parseCssValue(\"--my-property\", \"url(https://my-image.com)\")).toEqual({\n    type: \"unparsed\",\n    value: \"url(https://my-image.com)\",\n  });\n  expect(parseCssValue(\"--my-property\", \"blue red\")).toEqual({\n    type: \"unparsed\",\n    value: \"blue red\",\n  });\n  expect(parseCssValue(\"--my-property\", \"blue, red\")).toEqual({\n    type: \"unparsed\",\n    value: \"blue, red\",\n  });\n});\n\ntest(\"support custom properties var reference\", () => {\n  expect(parseCssValue(\"color\", \"var(--color)\")).toEqual({\n    type: \"var\",\n    value: \"color\",\n  });\n  expect(parseCssValue(\"color\", \"var(--color, red)\")).toEqual({\n    type: \"var\",\n    value: \"color\",\n    fallback: { type: \"unparsed\", value: \"red\" },\n  });\n});\n\ntest(\"support unit in custom property\", () => {\n  expect(parseCssValue(\"--size\", \"10\")).toEqual({\n    type: \"unit\",\n    value: 10,\n    unit: \"number\",\n  });\n  expect(parseCssValue(\"--size\", \"10px\")).toEqual({\n    type: \"unit\",\n    value: 10,\n    unit: \"px\",\n  });\n  expect(parseCssValue(\"--size\", \"10%\")).toEqual({\n    type: \"unit\",\n    value: 10,\n    unit: \"%\",\n  });\n});\n\ntest(\"support color in custom property\", () => {\n  expect(parseCssValue(\"--color\", \"rgb(61 77 4)\")).toEqual({\n    type: \"color\",\n    colorSpace: \"srgb\",\n    alpha: 1,\n    components: [0.2392, 0.302, 0.0157],\n  });\n  expect(parseCssValue(\"--color\", \"rgba(61, 77, 4, 0.5)\")).toEqual({\n    type: \"color\",\n    colorSpace: \"srgb\",\n    alpha: 0.5,\n    components: [0.2392, 0.302, 0.0157],\n  });\n  expect(parseCssValue(\"--color\", \"#3d4d04\")).toEqual({\n    type: \"color\",\n    colorSpace: \"srgb\",\n    alpha: 1,\n    components: [0.2392, 0.302, 0.0157],\n  });\n  expect(parseCssValue(\"--color\", \"red\")).toEqual({\n    type: \"unparsed\",\n    value: \"red\",\n  });\n});\n\ntest(\"support custom properties var reference in custom property\", () => {\n  expect(parseCssValue(\"--bg\", \"var(--color)\")).toEqual({\n    type: \"var\",\n    value: \"color\",\n  });\n  expect(parseCssValue(\"--bg\", \"var(--color, red)\")).toEqual({\n    type: \"var\",\n    value: \"color\",\n    fallback: { type: \"unparsed\", value: \"red\" },\n  });\n});\n\ntest(\"parse empty custom property as empty unparsed\", () => {\n  expect(parseCssValue(\"--inset\", \"\")).toEqual({\n    type: \"unparsed\",\n    value: \"\",\n  });\n});\n\ntest(\"parse single var in repeated value without layers or tuples\", () => {\n  expect(parseCssValue(\"background-image\", \"var(--gradient)\")).toEqual({\n    type: \"var\",\n    value: \"gradient\",\n  });\n  expect(parseCssValue(\"filter\", \"var(--noise)\")).toEqual({\n    type: \"var\",\n    value: \"noise\",\n  });\n});\n\ntest(\"parse multiple var in repeated value as layers and tuples\", () => {\n  expect(\n    parseCssValue(\"background-image\", \"var(--gradient-1), var(--gradient-2)\")\n  ).toEqual({\n    type: \"layers\",\n    value: [\n      { type: \"var\", value: \"gradient-1\" },\n      { type: \"var\", value: \"gradient-2\" },\n    ],\n  });\n  expect(parseCssValue(\"filter\", \"var(--noise-1) var(--noise-2)\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"var\", value: \"noise-1\" },\n      { type: \"var\", value: \"noise-2\" },\n    ],\n  });\n});\n\ndescribe(\"parse shadows\", () => {\n  test(\"parses value and returns invalid when used a invalid boxShadow is passed\", () => {\n    expect(parseCssValue(\"box-shadow\", `10px 10px 5px foo`)).toEqual({\n      type: \"invalid\",\n      value: \"10px 10px 5px foo\",\n    });\n  });\n\n  test(\"parses value and returns invalid when a invalid textShadow is passed\", () => {\n    expect(parseCssValue(\"text-shadow\", `10px 10px 5px foo`)).toEqual({\n      type: \"invalid\",\n      value: \"10px 10px 5px foo\",\n    });\n  });\n\n  test(\"throws error when passed a value without a unit\", () => {\n    expect(parseCssValue(\"box-shadow\", `10 10px 5px red`)).toEqual({\n      type: \"invalid\",\n      value: \"10 10px 5px red\",\n    });\n  });\n\n  test(\"parses values and returns a layer when a valid textShadow is passes\", () => {\n    expect(parseCssValue(\"text-shadow\", \"1px 1px 2px black\")).toEqual({\n      type: \"layers\",\n      value: [\n        {\n          type: \"shadow\",\n          position: \"outset\",\n          offsetX: { type: \"unit\", unit: \"px\", value: 1 },\n          offsetY: { type: \"unit\", unit: \"px\", value: 1 },\n          blur: { type: \"unit\", unit: \"px\", value: 2 },\n          color: { type: \"keyword\", value: \"black\" },\n        },\n      ],\n    });\n  });\n\n  test(\"inset and color values can be interchanged\", () => {\n    expect(parseCssValue(\"box-shadow\", `inset 10px 10px 5px black`)).toEqual({\n      type: \"layers\",\n      value: [\n        {\n          type: \"shadow\",\n          position: \"inset\",\n          offsetX: { type: \"unit\", unit: \"px\", value: 10 },\n          offsetY: { type: \"unit\", unit: \"px\", value: 10 },\n          blur: { type: \"unit\", unit: \"px\", value: 5 },\n          color: { type: \"keyword\", value: \"black\" },\n        },\n      ],\n    });\n  });\n\n  test(\"parses value when inset is used but missing blur-radius\", () => {\n    expect(parseCssValue(\"box-shadow\", `inset 5em 1em gold`)).toEqual({\n      type: \"layers\",\n      value: [\n        {\n          type: \"shadow\",\n          position: \"inset\",\n          offsetX: { type: \"unit\", unit: \"em\", value: 5 },\n          offsetY: { type: \"unit\", unit: \"em\", value: 1 },\n          color: { type: \"keyword\", value: \"gold\" },\n        },\n      ],\n    });\n  });\n\n  test(\"parses value when offsetX and offsetY are used\", () => {\n    expect(parseCssValue(\"box-shadow\", `60px -16px teal`)).toEqual({\n      type: \"layers\",\n      value: [\n        {\n          type: \"shadow\",\n          position: \"outset\",\n          offsetX: { type: \"unit\", unit: \"px\", value: 60 },\n          offsetY: { type: \"unit\", unit: \"px\", value: -16 },\n          color: { type: \"keyword\", value: \"teal\" },\n        },\n      ],\n    });\n  });\n\n  test(\"parses value from figma\", () => {\n    expect(\n      parseCssValue(\n        \"box-shadow\",\n        \"0 60px 80px rgba(0,0,0,0.60), 0 45px 26px rgba(0,0,0,0.14)\"\n      )\n    ).toEqual({\n      type: \"layers\",\n      value: [\n        {\n          type: \"shadow\",\n          position: \"outset\",\n          offsetX: { type: \"unit\", unit: \"number\", value: 0 },\n          offsetY: { type: \"unit\", unit: \"px\", value: 60 },\n          blur: { type: \"unit\", unit: \"px\", value: 80 },\n          color: {\n            type: \"color\",\n            colorSpace: \"srgb\",\n            alpha: 0.6,\n            components: [0, 0, 0],\n          },\n        },\n        {\n          type: \"shadow\",\n          position: \"outset\",\n          offsetX: { type: \"unit\", unit: \"number\", value: 0 },\n          offsetY: { type: \"unit\", unit: \"px\", value: 45 },\n          blur: { type: \"unit\", unit: \"px\", value: 26 },\n          color: {\n            type: \"color\",\n            colorSpace: \"srgb\",\n            alpha: 0.14,\n            components: [0, 0, 0],\n          },\n        },\n      ],\n    });\n  });\n\n  test(`parses multiple layers of box-shadow property`, () => {\n    expect(\n      parseCssValue(\n        \"box-shadow\",\n        `\n        0 0 5px rgba(0, 0, 0, 0.2),\n        inset 0 0 10px rgba(0, 0, 0, 0.3),\n        0 0 15px rgba(0, 0, 0, 0.4)\n        `\n      )\n    ).toEqual({\n      type: \"layers\",\n      value: [\n        {\n          type: \"shadow\",\n          position: \"outset\",\n          offsetX: { type: \"unit\", unit: \"number\", value: 0 },\n          offsetY: { type: \"unit\", unit: \"number\", value: 0 },\n          blur: { type: \"unit\", unit: \"px\", value: 5 },\n          color: {\n            type: \"color\",\n            colorSpace: \"srgb\",\n            alpha: 0.2,\n            components: [0, 0, 0],\n          },\n        },\n        {\n          type: \"shadow\",\n          position: \"inset\",\n          offsetX: { type: \"unit\", unit: \"number\", value: 0 },\n          offsetY: { type: \"unit\", unit: \"number\", value: 0 },\n          blur: { type: \"unit\", unit: \"px\", value: 10 },\n          color: {\n            type: \"color\",\n            colorSpace: \"srgb\",\n            alpha: 0.3,\n            components: [0, 0, 0],\n          },\n        },\n        {\n          type: \"shadow\",\n          position: \"outset\",\n          offsetX: { type: \"unit\", unit: \"number\", value: 0 },\n          offsetY: { type: \"unit\", unit: \"number\", value: 0 },\n          blur: { type: \"unit\", unit: \"px\", value: 15 },\n          color: {\n            type: \"color\",\n            colorSpace: \"srgb\",\n            alpha: 0.4,\n            components: [0, 0, 0],\n          },\n        },\n      ],\n    });\n  });\n\n  test(\"parse var in box-shadow\", () => {\n    expect(parseCssValue(\"box-shadow\", \"var(--shadow)\")).toEqual({\n      type: \"var\",\n      value: \"shadow\",\n    });\n    expect(\n      parseCssValue(\"box-shadow\", \"var(--shadow-1), var(--shadow-2)\")\n    ).toEqual({\n      type: \"layers\",\n      value: [\n        { type: \"var\", value: \"shadow-1\" },\n        { type: \"var\", value: \"shadow-2\" },\n      ],\n    });\n  });\n});\n\ndescribe(\"parse filters\", () => {\n  test(\"parse values and returns the valid style property values\", () => {\n    expect(parseCssValue(\"filter\", \"blur(4px)\")).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"blur\",\n          args: {\n            type: \"tuple\",\n            value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n          },\n        },\n      ],\n    });\n    expect(\n      parseCssValue(\"filter\", \"drop-shadow(10px 10px 25px rgba(0, 0, 255, 1))\")\n    ).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"drop-shadow\",\n          args: {\n            type: \"shadow\",\n            position: \"outset\",\n            offsetX: { type: \"unit\", unit: \"px\", value: 10 },\n            offsetY: { type: \"unit\", unit: \"px\", value: 10 },\n            blur: { type: \"unit\", unit: \"px\", value: 25 },\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              alpha: 1,\n              components: [0, 0, 1],\n            },\n          },\n        },\n      ],\n    });\n\n    expect(\n      parseCssValue(\"filter\", \"drop-shadow(10px 10px 25px  #0000FF)\")\n    ).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"drop-shadow\",\n          args: {\n            type: \"shadow\",\n            position: \"outset\",\n            offsetX: { type: \"unit\", unit: \"px\", value: 10 },\n            offsetY: { type: \"unit\", unit: \"px\", value: 10 },\n            blur: { type: \"unit\", unit: \"px\", value: 25 },\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              alpha: 1,\n              components: [0, 0, 1],\n            },\n          },\n        },\n      ],\n    });\n    expect(\n      parseCssValue(\"filter\", \"drop-shadow(10px 10px 25px var(--color))\")\n    ).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"drop-shadow\",\n          args: {\n            type: \"unparsed\",\n            value: \"10px 10px 25px var(--color)\",\n          },\n        },\n      ],\n    });\n  });\n\n  test(\"parse backdrop-filter\", () => {\n    expect(parseCssValue(\"backdrop-filter\", \"blur(4px)\")).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"blur\",\n          args: {\n            type: \"tuple\",\n            value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n          },\n        },\n      ],\n    });\n  });\n\n  test(\"Multiple valid function values\", () => {\n    expect(\n      parseCssValue(\n        \"filter\",\n        \"blur(4px) drop-shadow(16px 16px 20px blue) opacity(25%)\"\n      )\n    ).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"blur\",\n          args: {\n            type: \"tuple\",\n            value: [{ type: \"unit\", unit: \"px\", value: 4 }],\n          },\n        },\n        {\n          type: \"function\",\n          name: \"drop-shadow\",\n          args: {\n            type: \"shadow\",\n            position: \"outset\",\n            offsetX: { type: \"unit\", unit: \"px\", value: 16 },\n            offsetY: { type: \"unit\", unit: \"px\", value: 16 },\n            blur: { type: \"unit\", unit: \"px\", value: 20 },\n            color: { type: \"keyword\", value: \"blue\" },\n          },\n        },\n        {\n          type: \"function\",\n          name: \"opacity\",\n          args: {\n            type: \"tuple\",\n            value: [{ type: \"unit\", unit: \"%\", value: 25 }],\n          },\n        },\n      ],\n    });\n  });\n\n  // parsers are used to use copied value. At the moment, we don't have support\n  // for complex functions in the UI like the one below like calc(4px + 16em)\n  test(\"Using complex functions inside filter function\", () => {\n    expect(parseCssValue(\"filter\", \"blur(calc(4px + 16em))\")).toEqual({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"function\",\n          name: \"blur\",\n          args: {\n            type: \"tuple\",\n            value: [],\n          },\n        },\n      ],\n    });\n  });\n});\n\ndescribe(\"aspect-ratio\", () => {\n  test(\"support single numeric value\", () => {\n    expect(parseCssValue(\"aspect-ratio\", \"10\")).toEqual({\n      type: \"unit\",\n      unit: \"number\",\n      value: 10,\n    });\n  });\n  test(\"support keyword\", () => {\n    expect(parseCssValue(\"aspect-ratio\", \"auto\")).toEqual({\n      type: \"keyword\",\n      value: \"auto\",\n    });\n  });\n  test(\"support two values\", () => {\n    expect(parseCssValue(\"aspect-ratio\", \"16 / 9\")).toEqual({\n      type: \"unparsed\",\n      value: \"16 / 9\",\n    });\n  });\n});\n\ndescribe(\"font-family\", () => {\n  test(\"support single value\", () => {\n    expect(parseCssValue(\"font-family\", \"sans-serif\")).toEqual({\n      type: \"fontFamily\",\n      value: [\"sans-serif\"],\n    });\n  });\n\n  test(\"support multiple values\", () => {\n    expect(parseCssValue(\"font-family\", \"serif, sans-serif\")).toEqual({\n      type: \"fontFamily\",\n      value: [\"serif\", \"sans-serif\"],\n    });\n  });\n\n  test(\"support space separated values\", () => {\n    expect(parseCssValue(\"font-family\", \"Song Ti, Hei Ti\")).toEqual({\n      type: \"fontFamily\",\n      value: [\"Song Ti\", \"Hei Ti\"],\n    });\n    // only two keywords\n    expect(parseCssValue(\"font-family\", \"Song Ti\")).toEqual({\n      type: \"fontFamily\",\n      value: [\"Song Ti\"],\n    });\n  });\n\n  test(\"support quoted values\", () => {\n    expect(parseCssValue(\"font-family\", \"\\\"Song Ti\\\", 'Hei Ti'\")).toEqual({\n      type: \"fontFamily\",\n      value: [\"Song Ti\", \"Hei Ti\"],\n    });\n  });\n});\n\ntest(\"parse transform-origin\", () => {\n  expect(parseCssValue(\"transform-origin\", \"bottom\")).toEqual({\n    type: \"tuple\",\n    value: [{ type: \"keyword\", value: \"bottom\" }],\n  });\n\n  expect(parseCssValue(\"transform-origin\", \"left 2px\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"keyword\", value: \"left\" },\n      { type: \"unit\", value: 2, unit: \"px\" },\n    ],\n  });\n\n  expect(parseCssValue(\"transform-origin\", \"right top\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"keyword\", value: \"right\" },\n      { type: \"keyword\", value: \"top\" },\n    ],\n  });\n\n  expect(parseCssValue(\"transform-origin\", \"2px 30% 10px\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unit\", value: 2, unit: \"px\" },\n      { type: \"unit\", value: 30, unit: \"%\" },\n      { type: \"unit\", value: 10, unit: \"px\" },\n    ],\n  });\n\n  expect(parseCssValue(\"transform-origin\", \"top left right\")).toEqual({\n    type: \"invalid\",\n    value: \"top left right\",\n  });\n});\n\ntest(\"parse perspective-origin\", () => {\n  expect(parseCssValue(\"perspective-origin\", \"center\")).toEqual({\n    type: \"tuple\",\n    value: [{ type: \"keyword\", value: \"center\" }],\n  });\n\n  expect(parseCssValue(\"perspective-origin\", \"bottom right\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"keyword\", value: \"bottom\" },\n      { type: \"keyword\", value: \"right\" },\n    ],\n  });\n\n  expect(parseCssValue(\"perspective-origin\", \"bottom 55%\")).toEqual({\n    type: \"invalid\",\n    value: \"bottom 55%\",\n  });\n\n  expect(parseCssValue(\"perspective-origin\", \"75% bottom\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unit\", value: 75, unit: \"%\" },\n      { type: \"keyword\", value: \"bottom\" },\n    ],\n  });\n\n  expect(parseCssValue(\"perspective-origin\", \"-175%\")).toEqual({\n    type: \"tuple\",\n    value: [{ type: \"unit\", value: -175, unit: \"%\" }],\n  });\n\n  expect(parseCssValue(\"perspective-origin\", \"50% 50%\")).toEqual({\n    type: \"tuple\",\n    value: [\n      { type: \"unit\", value: 50, unit: \"%\" },\n      { type: \"unit\", value: 50, unit: \"%\" },\n    ],\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/parse-css-value.ts",
    "content": "import * as colorjs from \"colorjs.io/fn\";\nimport {\n  type CssNode,\n  type FunctionNode,\n  generate,\n  lexer,\n  List,\n  parse,\n} from \"css-tree\";\nimport warnOnce from \"warn-once\";\nimport {\n  cssWideKeywords,\n  type ImageValue,\n  type KeywordValue,\n  type LayersValue,\n  type TupleValue,\n  type UnitValue,\n  type LayerValueItem,\n  type RgbValue,\n  type ColorValue,\n  type StyleValue,\n  type Unit,\n  type VarValue,\n  type FunctionValue,\n  type TupleValueItem,\n  type CssProperty,\n  type ShadowValue,\n  type UnparsedValue,\n} from \"@webstudio-is/css-engine\";\nimport { keywordValues } from \"./__generated__/keyword-values\";\nimport { units } from \"./__generated__/units\";\n\ncolorjs.ColorSpace.register(colorjs.sRGB);\ncolorjs.ColorSpace.register(colorjs.sRGB_Linear);\ncolorjs.ColorSpace.register(colorjs.HSL);\ncolorjs.ColorSpace.register(colorjs.HWB);\ncolorjs.ColorSpace.register(colorjs.Lab);\ncolorjs.ColorSpace.register(colorjs.LCH);\ncolorjs.ColorSpace.register(colorjs.OKLab);\ncolorjs.ColorSpace.register(colorjs.OKLCH);\ncolorjs.ColorSpace.register(colorjs.P3);\ncolorjs.ColorSpace.register(colorjs.A98RGB);\ncolorjs.ColorSpace.register(colorjs.ProPhoto);\ncolorjs.ColorSpace.register(colorjs.REC_2020);\ncolorjs.ColorSpace.register(colorjs.XYZ_D65);\ncolorjs.ColorSpace.register(colorjs.XYZ_D50);\n\nexport const cssTryParseValue = (input: string): undefined | CssNode => {\n  try {\n    const ast = parse(input, { context: \"value\" });\n    return ast;\n  } catch {\n    return;\n  }\n};\n\nconst splitRepeated = (nodes: CssNode[]) => {\n  const lists: Array<CssNode[]> = [[]];\n  for (const node of nodes) {\n    if (node.type === \"Operator\" && node.value === \",\") {\n      lists.push([]);\n    } else {\n      lists.at(-1)?.push(node);\n    }\n  }\n  return lists;\n};\n\n// Because csstree parser has bugs we use CSSStyleValue to validate css properties if available\n// and fall back to csstree.\nexport const isValidDeclaration = (\n  property: CssProperty,\n  value: string\n): boolean => {\n  if (property.startsWith(\"--\") || value.includes(\"var(\")) {\n    return true;\n  }\n\n  // these properties have poor support in browser\n  // though rendered styles are merged as shorthand\n  // so validate artifically\n  if (\n    property === \"white-space-collapse\" ||\n    property === \"text-wrap-mode\" ||\n    property === \"text-wrap-style\"\n  ) {\n    return keywordValues[property].includes(value);\n  }\n\n  // @todo remove after csstree fixes\n  // - https://github.com/csstree/csstree/issues/246\n  // - https://github.com/csstree/csstree/issues/164\n  if (typeof CSSStyleValue !== \"undefined\") {\n    try {\n      CSSStyleValue.parse(property, value);\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  const ast = cssTryParseValue(value);\n\n  if (ast == null) {\n    return false;\n  }\n\n  if (\n    property === \"transition-timing-function\" ||\n    property === \"animation-timing-function\"\n  ) {\n    if (\n      lexer.match(\"linear( [ <number> && <percentage>{0,2} ]# )\", ast).matched\n    ) {\n      return true;\n    }\n  }\n\n  const matchResult = lexer.matchProperty(property, ast);\n\n  // allow to parse unknown properties as unparsed\n  if (matchResult.error?.message.includes(\"Unknown property\")) {\n    return true;\n  }\n\n  return matchResult.matched != null;\n};\n\nconst repeatedProps = new Set<CssProperty>([\n  \"background-attachment\",\n  \"background-clip\",\n  \"background-blend-mode\",\n  \"background-origin\",\n  \"background-position-x\",\n  \"background-position-y\",\n  \"background-repeat\",\n  \"background-size\",\n  \"background-image\",\n  \"transition-property\",\n  \"transition-duration\",\n  \"transition-delay\",\n  \"transition-timing-function\",\n  \"transition-behavior\",\n  \"box-shadow\",\n  \"text-shadow\",\n]);\n\nconst tupleProps = new Set<CssProperty>([\n  \"scale\",\n  \"translate\",\n  \"rotate\",\n  \"transform\",\n  \"filter\",\n  \"backdrop-filter\",\n  \"transform-origin\",\n  \"perspective-origin\",\n]);\n\nconst availableUnits = new Set<string>(Object.values(units).flat());\n\n// Map color space names to supported ColorValue color spaces\nconst colorSpace: Record<string, ColorValue[\"colorSpace\"]> = {\n  srgb: \"srgb\",\n  \"srgb-linear\": \"srgb-linear\",\n  \"display-p3\": \"p3\",\n  p3: \"p3\",\n  hsl: \"hsl\",\n  hwb: \"hwb\",\n  lab: \"lab\",\n  lch: \"lch\",\n  oklab: \"oklab\",\n  oklch: \"oklch\",\n  \"a98-rgb\": \"a98rgb\",\n  a98rgb: \"a98rgb\",\n  \"prophoto-rgb\": \"prophoto\",\n  prophoto: \"prophoto\",\n  rec2020: \"rec2020\",\n  \"xyz-d65\": \"xyz-d65\",\n  \"xyz-d50\": \"xyz-d50\",\n  xyz: \"xyz-d65\", // default to d65\n};\n\nconst toColorComponent = (value: undefined | null | number) =>\n  Math.round((value ?? 0) * 10000) / 10000;\n\nexport const parseColor = (colorString: string): undefined | ColorValue => {\n  // does not match css variables which are incorrectly treated by colorjs.io\n  if (!lexer.match(\"<color>\", colorString).matched) {\n    return;\n  }\n  try {\n    const color = colorjs.parse(colorString);\n    return {\n      type: \"color\",\n      colorSpace: colorSpace[color.spaceId],\n      components: color.coords.map(\n        toColorComponent\n      ) as ColorValue[\"components\"],\n      alpha: toColorComponent(color.alpha),\n    };\n  } catch {\n    // Invalid colors or relative color syntax are treated as unparsed\n  }\n};\n\nconst parseShadow = (\n  nodes: CssNode[],\n  input: string\n): ShadowValue | UnparsedValue => {\n  // https://drafts.csswg.org/css-borders-4/#box-shadow-position\n  let position: \"inset\" | \"outset\" = \"outset\";\n  let color: undefined | ColorValue | RgbValue | KeywordValue;\n  const units: UnitValue[] = [];\n  for (const node of nodes) {\n    const item = parseLiteral(node, [\"inset\"]);\n    if (item?.type === \"keyword\" && item.value === \"inset\") {\n      position = item.value;\n    } else if (item?.type === \"keyword\" && parseColor(item.value)) {\n      color = item;\n    } else if (item?.type === \"color\") {\n      color = item;\n    } else if (item?.type === \"rgb\") {\n      color = item;\n    } else if (item?.type === \"unit\") {\n      units.push(item);\n    } else {\n      return { type: \"unparsed\", value: input };\n    }\n  }\n  if (units.length < 2) {\n    return { type: \"unparsed\", value: input };\n  }\n  const shadowValue: ShadowValue = {\n    type: \"shadow\",\n    position,\n    offsetX: units[0],\n    offsetY: units[1],\n  };\n  if (units.length > 2) {\n    shadowValue.blur = units[2];\n  }\n  if (units.length > 3) {\n    shadowValue.spread = units[3];\n  }\n  if (color) {\n    shadowValue.color = color;\n  }\n  return shadowValue;\n};\n\nexport const parseCssVar = (node: FunctionNode): undefined | VarValue => {\n  const [name, _comma, ...fallback] = node.children;\n  const fallbackString = generate({\n    type: \"Value\",\n    loc: null,\n    children: new List<CssNode>().fromArray(fallback),\n  }).trim();\n  if (name.type === \"Identifier\") {\n    const value: VarValue = {\n      type: \"var\",\n      value: name.name.slice(\"--\".length),\n    };\n    if (fallback.length > 0) {\n      value.fallback = { type: \"unparsed\", value: fallbackString };\n    }\n    return value;\n  }\n};\n\nconst parseLiteral = (\n  node: undefined | null | CssNode,\n  keywords?: readonly string[]\n):\n  | undefined\n  | UnitValue\n  | KeywordValue\n  | ImageValue\n  | RgbValue\n  | ColorValue\n  | VarValue\n  | FunctionValue => {\n  if (node?.type === \"Number\") {\n    return {\n      type: \"unit\",\n      unit: \"number\",\n      value: Number(node.value),\n    };\n  }\n  if (node?.type === \"Dimension\" && availableUnits.has(node.unit)) {\n    return {\n      type: \"unit\",\n      unit: node.unit as Unit,\n      value: Number(node.value),\n    };\n  }\n  if (node?.type === \"Percentage\") {\n    return {\n      type: \"unit\",\n      unit: \"%\",\n      value: Number(node.value),\n    };\n  }\n  if (node?.type === \"Identifier\") {\n    const name = node.name.toLowerCase();\n    if (\n      keywords?.map((keyword) => keyword.toLowerCase()).includes(name) ||\n      parseColor(name)\n    ) {\n      return {\n        type: \"keyword\",\n        value: name,\n      };\n    }\n  }\n  if (node?.type === \"Url\") {\n    return {\n      type: \"image\",\n      value: {\n        type: \"url\",\n        url: node.value,\n      },\n    };\n  }\n  if (node?.type === \"Hash\") {\n    const color = parseColor(`#${node.value}`);\n    if (color) {\n      return color;\n    }\n  }\n  if (node?.type === \"Function\") {\n    // <color-function>\n    if (\n      node.name === \"hsl\" ||\n      node.name === \"hsla\" ||\n      node.name === \"rgb\" ||\n      node.name === \"rgba\" ||\n      node.name === \"oklch\" ||\n      node.name === \"oklab\" ||\n      node.name === \"lch\" ||\n      node.name === \"lab\" ||\n      node.name === \"hwb\" ||\n      node.name === \"color\"\n    ) {\n      const color = parseColor(generate(node));\n      if (color) {\n        return color;\n      }\n    }\n    if (node.name === \"var\") {\n      return parseCssVar(node);\n    }\n\n    // functions with comma-separated arguments\n    if (\n      // <transform-function>\n      // 2d\n      node.name === \"matrix\" ||\n      node.name === \"translate\" ||\n      node.name === \"translateX\" ||\n      node.name === \"translateY\" ||\n      node.name === \"scale\" ||\n      node.name === \"scaleX\" ||\n      node.name === \"scaleY\" ||\n      node.name === \"rotate\" ||\n      node.name === \"skew\" ||\n      node.name === \"skewX\" ||\n      node.name === \"skewY\" ||\n      // 3d\n      node.name === \"matrix3d\" ||\n      node.name === \"translate3d\" ||\n      node.name === \"translateZ\" ||\n      node.name === \"scale3d\" ||\n      node.name === \"scaleZ\" ||\n      node.name === \"rotate3d\" ||\n      node.name === \"rotateX\" ||\n      node.name === \"rotateY\" ||\n      node.name === \"rotateZ\" ||\n      node.name === \"perspective\" ||\n      // <easing-function>\n      node.name === \"cubic-bezier\" ||\n      node.name === \"steps\"\n      // treat linear function as unparsed\n    ) {\n      const args: LayersValue = { type: \"layers\", value: [] };\n      for (const arg of node.children) {\n        const matchedValue = parseLiteral(arg);\n        if (matchedValue) {\n          args.value.push(matchedValue as LayerValueItem);\n        }\n        if (arg.type === \"Identifier\") {\n          args.value.push({ type: \"keyword\", value: arg.name });\n        }\n      }\n      return { type: \"function\", args, name: node.name };\n    }\n\n    // functions with space separated arguments\n    if (\n      // <filter-function>\n      node.name === \"blur\" ||\n      node.name === \"brightness\" ||\n      node.name === \"contrast\" ||\n      node.name === \"grayscale\" ||\n      node.name === \"hue-rotate\" ||\n      node.name === \"invert\" ||\n      node.name === \"opacity\" ||\n      node.name === \"sepia\" ||\n      node.name === \"saturate\"\n    ) {\n      const args: TupleValue = { type: \"tuple\", value: [] };\n      for (const arg of node.children) {\n        const matchedValue = parseLiteral(arg);\n        if (matchedValue) {\n          args.value.push(matchedValue as TupleValueItem);\n        }\n      }\n      return { type: \"function\", args, name: node.name };\n    }\n    if (node.name === \"drop-shadow\") {\n      return {\n        type: \"function\",\n        args: parseShadow(\n          node.children.toArray(),\n          generate({ type: \"Value\", loc: null, children: node.children })\n        ),\n        name: node.name,\n      };\n    }\n  }\n};\n\nexport const parseCssValue = (\n  property: CssProperty, // Handles only long-hand values.\n  input: string,\n  topLevel = true\n): StyleValue => {\n  const potentialKeyword = input.toLowerCase().trim();\n  if (cssWideKeywords.has(potentialKeyword)) {\n    return { type: \"keyword\", value: potentialKeyword };\n  }\n\n  if (property === \"transition-property\" && potentialKeyword === \"none\") {\n    if (topLevel) {\n      return { type: \"keyword\", value: potentialKeyword };\n    } else {\n      // none is not valid layer keyword\n      return { type: \"unparsed\", value: potentialKeyword };\n    }\n  }\n\n  const invalidValue = {\n    type: \"invalid\",\n    value: input,\n  } as const;\n\n  if (input.length === 0) {\n    // custom properties can be empty\n    // in case interpolated value need to be avoided\n    if (property.startsWith(\"--\")) {\n      return { type: \"unparsed\", value: \"\" };\n    }\n    return invalidValue;\n  }\n\n  if (isValidDeclaration(property, input) === false) {\n    return invalidValue;\n  }\n\n  const ast = cssTryParseValue(input);\n\n  if (ast == null) {\n    warnOnce(\n      true,\n      `Can't parse css property \"${property}\" with value \"${input}\"`\n    );\n    return invalidValue;\n  }\n  const nodes = \"children\" in ast ? (ast.children?.toArray() ?? []) : [ast];\n\n  // support only following types in custom properties\n  if (property.startsWith(\"--\")) {\n    if (nodes.length === 1) {\n      const parsedValue = parseLiteral(nodes[0]);\n      if (\n        parsedValue?.type === \"var\" ||\n        parsedValue?.type === \"unit\" ||\n        parsedValue?.type === \"rgb\" ||\n        parsedValue?.type === \"color\"\n      ) {\n        return parsedValue;\n      }\n    }\n    return { type: \"unparsed\", value: input };\n  }\n\n  // parse single var() without wrapping with layers or tuples\n  // which can possibly get nested when variables are computed\n  if (\n    nodes.length === 1 &&\n    nodes[0].type === \"Function\" &&\n    nodes[0].name === \"var\"\n  ) {\n    return parseLiteral(nodes[0]) ?? invalidValue;\n  }\n\n  // prevent infinite splitting into layers for items\n  if (repeatedProps.has(property) && topLevel) {\n    let invalid = false;\n    const layersValue: StyleValue = {\n      type: \"layers\",\n      value: splitRepeated(nodes).map((nodes) => {\n        const value = generate({\n          type: \"Value\",\n          loc: null,\n          children: new List<CssNode>().fromArray(nodes),\n        });\n        const parsed = parseCssValue(property, value, false) as LayerValueItem;\n        if (parsed.type === \"invalid\") {\n          invalid = true;\n        }\n        return parsed;\n      }),\n    };\n    // at least one layer is invalid then whole value is invalid\n    if (invalid) {\n      return invalidValue;\n    }\n    return layersValue;\n  }\n\n  if (property === \"font-family\") {\n    return {\n      type: \"fontFamily\",\n      value: splitRepeated(nodes).map((nodes) => {\n        // unquote values\n        if (nodes.length === 1 && nodes[0].type === \"String\") {\n          return nodes[0].value;\n        }\n        return generate({\n          type: \"Value\",\n          loc: null,\n          children: new List<CssNode>().fromArray(nodes),\n        });\n      }),\n    };\n  }\n\n  if (property === \"box-shadow\" || property === \"text-shadow\") {\n    return parseShadow(nodes, input);\n  }\n\n  // Probably a tuple like background-size or box-shadow\n  if (\n    ast.type === \"Value\" &&\n    (ast.children.size === 2 || tupleProps.has(property))\n  ) {\n    const tuple: TupleValue = {\n      type: \"tuple\",\n      value: [],\n    };\n    for (const node of ast.children) {\n      // output any values with unhandled operators like slash or comma as unparsed\n      if (node.type === \"Operator\") {\n        return { type: \"unparsed\", value: input };\n      }\n      const matchedValue = parseLiteral(node, keywordValues[property]);\n      if (matchedValue) {\n        tuple.value.push(matchedValue as never);\n      } else {\n        tuple.value.push({ type: \"unparsed\", value: generate(node) });\n      }\n    }\n    return tuple;\n  }\n\n  if (ast.type === \"Value\" && ast.children.size === 1) {\n    // Try extract units from 1st children\n    const first = ast.children.first;\n    const matchedValue = parseLiteral(first, keywordValues[property]);\n    if (matchedValue) {\n      return matchedValue;\n    }\n  }\n\n  return {\n    type: \"unparsed\",\n    value: input,\n  };\n};\n"
  },
  {
    "path": "packages/css-data/src/parse-css.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport {\n  camelCaseProperty,\n  parseClassBasedSelector,\n  parseCss,\n  parseMediaQuery,\n} from \"./parse-css\";\n\ndescribe(\"Parse CSS\", () => {\n  test(\"longhand property name with keyword value\", () => {\n    expect(parseCss(`.test { background-color: red }`)).toEqual([\n      {\n        selector: \".test\",\n        property: \"background-color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"one class selector rules\", () => {\n    expect(parseCss(`.test { color: #ff0000 }`)).toEqual([\n      {\n        selector: \".test\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  // @todo this is wrong\n  test.skip(\"parse declaration with missing value\", () => {\n    expect(parseCss(`.test { color:;}`)).toEqual([\n      {\n        selector: \".test\",\n        property: \"color\",\n        value: { type: \"guaranteedInvalid\" },\n      },\n    ]);\n  });\n\n  test(\"parse supported shorthand values\", () => {\n    const css = `\n      .test {\n        background: #ff0000 linear-gradient(180deg, #11181C 0%, rgba(17, 24, 28, 0) 36.09%), #EBFFFC;\n      }\n    `;\n    expect(parseCss(css)).toEqual([\n      {\n        selector: \".test\",\n        property: \"background-image\",\n        value: {\n          type: \"layers\",\n          value: [\n            {\n              type: \"unparsed\",\n              value:\n                \"linear-gradient(180deg,#11181C 0%,rgba(17,24,28,0) 36.09%)\",\n            },\n            { type: \"keyword\", value: \"none\" },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-position-x\",\n        value: {\n          type: \"layers\",\n          value: [\n            { type: \"unit\", unit: \"%\", value: 0 },\n            { type: \"unit\", unit: \"%\", value: 0 },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-position-y\",\n        value: {\n          type: \"layers\",\n          value: [\n            { type: \"unit\", unit: \"%\", value: 0 },\n            { type: \"unit\", unit: \"%\", value: 0 },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-size\",\n        value: {\n          type: \"layers\",\n          value: [\n            {\n              type: \"tuple\",\n              value: [\n                { type: \"keyword\", value: \"auto\" },\n                { type: \"keyword\", value: \"auto\" },\n              ],\n            },\n            {\n              type: \"tuple\",\n              value: [\n                { type: \"keyword\", value: \"auto\" },\n                { type: \"keyword\", value: \"auto\" },\n              ],\n            },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-repeat\",\n        value: {\n          type: \"layers\",\n          value: [\n            { type: \"keyword\", value: \"repeat\" },\n            { type: \"keyword\", value: \"repeat\" },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-attachment\",\n        value: {\n          type: \"layers\",\n          value: [\n            { type: \"keyword\", value: \"scroll\" },\n            { type: \"keyword\", value: \"scroll\" },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-origin\",\n        value: {\n          type: \"layers\",\n          value: [\n            { type: \"keyword\", value: \"padding-box\" },\n            { type: \"keyword\", value: \"padding-box\" },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-clip\",\n        value: {\n          type: \"layers\",\n          value: [\n            { type: \"keyword\", value: \"border-box\" },\n            { type: \"keyword\", value: \"border-box\" },\n          ],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [0.9216, 1, 0.9882],\n        },\n      },\n    ]);\n  });\n\n  test(\"parses single layer\", () => {\n    const css = `\n      .test {\n          background-image: none; background-position: 0px 0px; background-size: auto;\n      }\n    `;\n    expect(parseCss(css)).toEqual([\n      {\n        selector: \".test\",\n        property: \"background-image\",\n        value: {\n          type: \"layers\",\n          value: [{ type: \"keyword\", value: \"none\" }],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-position-x\",\n        value: {\n          type: \"layers\",\n          value: [{ type: \"unit\", unit: \"px\", value: 0 }],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-position-y\",\n        value: {\n          type: \"layers\",\n          value: [{ type: \"unit\", unit: \"px\", value: 0 }],\n        },\n      },\n      {\n        selector: \".test\",\n        property: \"background-size\",\n        value: {\n          type: \"layers\",\n          value: [{ type: \"keyword\", value: \"auto\" }],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse state\", () => {\n    expect(parseCss(`a:hover { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"a\",\n        state: \":hover\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"attribute selector\", () => {\n    expect(parseCss(`[class^=\"a\"] { color: #ff0000 }`)).toEqual([\n      {\n        selector: '[class^=\"a\"]',\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse first pseudo class as selector\", () => {\n    // E.g. :root\n    expect(parseCss(`:first-pseudo:my-state { color: #ff0000 }`)).toEqual([\n      {\n        selector: \":first-pseudo\",\n        state: \":my-state\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse pseudo element\", () => {\n    expect(parseCss(`input::placeholder { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"input\",\n        state: \"::placeholder\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse multiple selectors, one with state\", () => {\n    expect(parseCss(`a, a:hover { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n      {\n        selector: \"a\",\n        state: \":hover\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse multiple selectors, both with state\", () => {\n    expect(parseCss(`a:active, a:hover { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"a\",\n        state: \":active\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n      {\n        selector: \"a\",\n        state: \":hover\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse multiple rules\", () => {\n    expect(parseCss(`a { color: red} a:hover { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"color\",\n        value: {\n          type: \"keyword\",\n          value: \"red\",\n        },\n      },\n      {\n        selector: \"a\",\n        state: \":hover\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse multiple rules, remove overwritten properties\", () => {\n    const css = `\n      h1 {\n        margin-bottom: 5px;\n        font-size: 2em;\n      }\n      h1 {\n        margin-bottom: 10px;\n        font-weight: bold;\n      }\n\n      h1 {\n        margin-top: 20px;\n        font-size: 38px;\n        line-height: 44px;\n      }\n    `;\n    expect(parseCss(css)).toEqual([\n      {\n        selector: \"h1\",\n        property: \"margin-bottom\",\n        value: { type: \"unit\", unit: \"px\", value: 10 },\n      },\n      {\n        selector: \"h1\",\n        property: \"font-size\",\n        value: { type: \"unit\", unit: \"px\", value: 38 },\n      },\n      {\n        selector: \"h1\",\n        property: \"font-weight\",\n        value: { type: \"keyword\", value: \"bold\" },\n      },\n      {\n        selector: \"h1\",\n        property: \"margin-top\",\n        value: { type: \"unit\", unit: \"px\", value: 20 },\n      },\n      {\n        selector: \"h1\",\n        property: \"line-height\",\n        value: { type: \"unit\", unit: \"px\", value: 44 },\n      },\n    ]);\n  });\n\n  test(\"parse shorthand\", () => {\n    expect(parseCss(`a { border: 1px solid red }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"border-top-width\",\n        value: { type: \"unit\", unit: \"px\", value: 1 },\n      },\n      {\n        selector: \"a\",\n        property: \"border-right-width\",\n        value: { type: \"unit\", unit: \"px\", value: 1 },\n      },\n      {\n        selector: \"a\",\n        property: \"border-bottom-width\",\n        value: { type: \"unit\", unit: \"px\", value: 1 },\n      },\n      {\n        selector: \"a\",\n        property: \"border-left-width\",\n        value: { type: \"unit\", unit: \"px\", value: 1 },\n      },\n      {\n        selector: \"a\",\n        property: \"border-top-style\",\n        value: { type: \"keyword\", value: \"solid\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-right-style\",\n        value: { type: \"keyword\", value: \"solid\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-bottom-style\",\n        value: { type: \"keyword\", value: \"solid\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-left-style\",\n        value: { type: \"keyword\", value: \"solid\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-top-color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-right-color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-bottom-color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n      {\n        selector: \"a\",\n        property: \"border-left-color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"parse custom property\", () => {\n    expect(parseCss(`a { --my-property: red; }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"--my-property\",\n        value: { type: \"unparsed\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"parse empty custom property\", () => {\n    expect(parseCss(`a { --my-property: ; }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"--my-property\",\n        value: { type: \"unparsed\", value: \"\" },\n      },\n    ]);\n  });\n\n  test(\"parse variable as var value\", () => {\n    expect(\n      parseCss(\n        `\n        a {\n          color: var(--color);\n          background-color: var(--color, red);\n        }\n        `\n      )\n    ).toEqual([\n      {\n        selector: \"a\",\n        property: \"color\",\n        value: { type: \"var\", value: \"color\" },\n      },\n      {\n        selector: \"a\",\n        property: \"background-color\",\n        value: {\n          type: \"var\",\n          value: \"color\",\n          fallback: { type: \"unparsed\", value: \"red\" },\n        },\n      },\n    ]);\n  });\n\n  test(\"parse empty value as unset\", () => {\n    expect(parseCss(`a { color: ; background-color: red }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"unset\" },\n      },\n      {\n        selector: \"a\",\n        property: \"background-color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"unprefix property that doesn't need a prefix\", () => {\n    expect(parseCss(`a { -webkit-color: red; }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"color\",\n        value: { type: \"keyword\", value: \"red\" },\n      },\n    ]);\n  });\n\n  test(\"keep prefix for property that needs one\", () => {\n    expect(parseCss(`a { -webkit-box-orient: horizontal; }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"-webkit-box-orient\",\n        value: { type: \"keyword\", value: \"horizontal\" },\n      },\n    ]);\n  });\n\n  test(\"keep prefix for -webkit-text-stroke\", () => {\n    // shorthand is kept as-is (not expanded) but prefix is preserved\n    expect(parseCss(`a { -webkit-text-stroke: 1px black; }`)).toEqual([\n      {\n        selector: \"a\",\n        property: \"-webkit-text-stroke\",\n        value: {\n          type: \"tuple\",\n          value: [\n            { type: \"unit\", unit: \"px\", value: 1 },\n            { type: \"keyword\", value: \"black\" },\n          ],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse child combinator\", () => {\n    expect(parseCss(`a > b { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"a > b\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse space combinator\", () => {\n    expect(parseCss(`.a b { color: #ff0000 }`)).toEqual([\n      {\n        selector: \".a b\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n\n  test(\"parse nested selectors as one token\", () => {\n    expect(parseCss(`a b c.d { color: #ff0000 }`)).toEqual([\n      {\n        selector: \"a b c.d\",\n        property: \"color\",\n        value: {\n          type: \"color\",\n          colorSpace: \"srgb\",\n          alpha: 1,\n          components: [1, 0, 0],\n        },\n      },\n    ]);\n  });\n});\n\ntest(\"parse font-smooth properties\", () => {\n  expect(\n    parseCss(`\n      a {\n        font-smoothing: auto;\n      }\n      b {\n        -webkit-font-smoothing: auto;\n      }\n      c {\n        -moz-osx-font-smoothing: auto;\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"-webkit-font-smoothing\",\n      value: { type: \"keyword\", value: \"auto\" },\n    },\n    {\n      selector: \"b\",\n      property: \"-webkit-font-smoothing\",\n      value: { type: \"keyword\", value: \"auto\" },\n    },\n    {\n      selector: \"c\",\n      property: \"-moz-osx-font-smoothing\",\n      value: { type: \"keyword\", value: \"auto\" },\n    },\n  ]);\n});\n\ntest(\"parse incorrectly unprefixed tap-highlight-color\", () => {\n  expect(\n    parseCss(`\n      a {\n        -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n      }\n      b {\n        tap-highlight-color: transparent;\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"-webkit-tap-highlight-color\",\n      value: {\n        type: \"color\",\n        colorSpace: \"srgb\",\n        alpha: 0,\n        components: [0, 0, 0],\n      },\n    },\n    {\n      selector: \"b\",\n      property: \"-webkit-tap-highlight-color\",\n      value: { type: \"keyword\", value: \"transparent\" },\n    },\n  ]);\n});\n\ntest(\"parse top level rules and media all as base query\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: red;\n      }\n      @media all {\n        b {\n          width: auto;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      selector: \"b\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    },\n  ]);\n});\n\ntest(\"parse media queries\", () => {\n  expect(\n    parseCss(`\n      @media  ( max-width:  768px )  {\n        a {\n          color: red;\n        }\n      }\n      @media (min-width: 768px) {\n        a {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(max-width:768px)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpoint: `(min-width:768px)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n  ]);\n});\n\ntest(\"support only screen media type\", () => {\n  expect(\n    parseCss(`\n      @media all {\n        a {\n          color: yellow;\n        }\n      }\n      @media all and ( min-width: 768px )  {\n        a {\n          color: red;\n        }\n      }\n      @media screen and (min-width: 1024px) {\n        a {\n          color: green;\n        }\n      }\n      @media print and (min-width: 1280px) {\n        a {\n          color: blue;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"yellow\" },\n    },\n    {\n      breakpoint: `(min-width:768px)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpoint: `(min-width:1024px)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n  ]);\n});\n\ntest(\"parse previously unsupported media queries\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: red;\n      }\n      @media (min-width: 768px) and (max-width: 1024px) {\n        b {\n          color: green;\n        }\n      }\n      @media (min-width: 768px) and (max-width: 1024px) {\n        c {\n          color: blue;\n        }\n      }\n      @media (hover: hover) {\n        d {\n          color: yellow;\n        }\n      }\n      @media (min-width: 40rem)  {\n        e {\n          color: orange;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpoint: \"(min-width:768px) and (max-width:1024px)\",\n      selector: \"b\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n    {\n      breakpoint: \"(min-width:768px) and (max-width:1024px)\",\n      selector: \"c\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n    {\n      breakpoint: \"(hover:hover)\",\n      selector: \"d\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"yellow\" },\n    },\n    // @media (min-width: 40rem) is still ignored (non-px unit)\n  ]);\n});\n\ntest(\"parse nested media queries by flattening\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px)  {\n        a {\n          color: green;\n        }\n        @media (max-width: 1024px) {\n          a {\n            color: red;\n          }\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(min-width:768px)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n    {\n      breakpoint: \"(min-width:768px) and (max-width:1024px)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"ignore unsupported at rules\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: red;\n      }\n      @supports (display: grid) {\n        b {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse condition-based media queries\", () => {\n  expect(\n    parseCss(`\n      @media (prefers-color-scheme: dark) {\n        a {\n          color: white;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(prefers-color-scheme:dark)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"white\" },\n    },\n  ]);\n});\n\ntest(\"parse hover media feature\", () => {\n  expect(\n    parseCss(`\n      @media (hover: hover) {\n        a {\n          color: blue;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(hover:hover)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"parse orientation media feature\", () => {\n  expect(\n    parseCss(`\n      @media (orientation: portrait) {\n        a {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(orientation:portrait)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n  ]);\n});\n\ntest(\"parse prefers-reduced-motion media feature\", () => {\n  expect(\n    parseCss(`\n      @media (prefers-reduced-motion: reduce) {\n        a {\n          color: red;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(prefers-reduced-motion:reduce)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse combined min-width and max-width media query\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) and (max-width: 1024px) {\n        a {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(min-width:768px) and (max-width:1024px)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n  ]);\n});\n\ntest(\"parse min-width combined with condition feature\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) and (orientation: landscape) {\n        a {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(min-width:768px) and (orientation:landscape)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n  ]);\n});\n\ntest(\"parse multiple condition features in media query\", () => {\n  expect(\n    parseCss(`\n      @media (prefers-color-scheme: dark) and (prefers-contrast: more) {\n        a {\n          color: white;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: `(prefers-color-scheme:dark) and (prefers-contrast:more)`,\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"white\" },\n    },\n  ]);\n});\n\ntest(\"parse nested media queries\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) {\n        a {\n          color: green;\n        }\n        @media (max-width: 1024px) {\n          a {\n            color: red;\n          }\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(min-width:768px)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n    {\n      breakpoint: \"(min-width:768px) and (max-width:1024px)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse nested media with condition inside width\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) {\n        a {\n          color: green;\n        }\n        @media (prefers-color-scheme: dark) {\n          a {\n            color: white;\n          }\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(min-width:768px)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n    {\n      breakpoint: \"(min-width:768px) and (prefers-color-scheme:dark)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"white\" },\n    },\n  ]);\n});\n\ntest(\"parse deeply nested media queries\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) {\n        @media (orientation: landscape) {\n          @media (hover: hover) {\n            a {\n              color: red;\n            }\n          }\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint:\n        \"(min-width:768px) and (orientation:landscape) and (hover:hover)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse condition and base styles together\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: black;\n      }\n      @media (prefers-color-scheme: dark) {\n        a {\n          color: white;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"black\" },\n    },\n    {\n      breakpoint: \"(prefers-color-scheme:dark)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"white\" },\n    },\n  ]);\n});\n\ntest(\"still ignore non-px units in media queries\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: red;\n      }\n      @media (min-width: 40rem) {\n        a {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"still ignore @media print\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: red;\n      }\n      @media print {\n        a {\n          color: black;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"still ignore @supports\", () => {\n  expect(\n    parseCss(`\n      a {\n        color: red;\n      }\n      @supports (display: grid) {\n        a {\n          color: green;\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse &:pseudo-classes as state\", () => {\n  expect(\n    parseCss(`\n      &:hover {\n        color: red;\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"\",\n      state: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse &[attribute=selector] as state\", () => {\n  expect(\n    parseCss(`\n      &[data-state=active] {\n        color: red;\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"\",\n      state: \"[data-state=active]\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\n// ---- Selector types ----\n\ntest(\"parse class selector with pseudo-class\", () => {\n  expect(parseCss(`.card:hover { color: red }`)).toEqual([\n    {\n      selector: \".card\",\n      state: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse class selector with pseudo-element\", () => {\n  expect(parseCss(`.card::before { content: none }`)).toEqual([\n    {\n      selector: \".card\",\n      state: \"::before\",\n      property: \"content\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n  ]);\n});\n\ntest(\"parse compound class selector\", () => {\n  expect(parseCss(`.card.active { opacity: 1 }`)).toEqual([\n    {\n      selector: \".card.active\",\n      property: \"opacity\",\n      value: { type: \"unit\", unit: \"number\", value: 1 },\n    },\n  ]);\n});\n\ntest(\"parse compound class selector with pseudo-class\", () => {\n  expect(parseCss(`.card.active:hover { opacity: 0.5 }`)).toEqual([\n    {\n      selector: \".card.active\",\n      state: \":hover\",\n      property: \"opacity\",\n      value: { type: \"unit\", unit: \"number\", value: 0.5 },\n    },\n  ]);\n});\n\ntest(\"parse class with attribute selector\", () => {\n  expect(parseCss(`.btn[disabled] { opacity: 0.5 }`)).toEqual([\n    {\n      selector: \".btn[disabled]\",\n      property: \"opacity\",\n      value: { type: \"unit\", unit: \"number\", value: 0.5 },\n    },\n  ]);\n});\n\ntest(\"parse class + attribute + pseudo-class\", () => {\n  expect(parseCss(`.btn[disabled]:focus { outline: none }`)).toEqual([\n    {\n      selector: \".btn[disabled]\",\n      state: \":focus\",\n      property: \"outline-width\",\n      value: { type: \"keyword\", value: \"medium\" },\n    },\n    {\n      selector: \".btn[disabled]\",\n      state: \":focus\",\n      property: \"outline-style\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n    {\n      selector: \".btn[disabled]\",\n      state: \":focus\",\n      property: \"outline-color\",\n      value: { type: \"keyword\", value: \"currentcolor\" },\n    },\n  ]);\n});\n\ntest(\"parse ID selector\", () => {\n  expect(parseCss(`#hero { display: flex }`)).toEqual([\n    {\n      selector: \"#hero\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    },\n  ]);\n});\n\ntest(\"parse element + class compound selector\", () => {\n  expect(parseCss(`div.card { display: flex }`)).toEqual([\n    {\n      selector: \"div.card\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    },\n  ]);\n});\n\ntest(\"parse sibling combinator +\", () => {\n  expect(parseCss(`.a + .b { color: red }`)).toEqual([\n    {\n      selector: \".a + .b\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse general sibling combinator ~\", () => {\n  expect(parseCss(`.a ~ .b { color: red }`)).toEqual([\n    {\n      selector: \".a ~ .b\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse :root as pseudo-class selector\", () => {\n  expect(parseCss(`:root { --color: blue }`)).toEqual([\n    {\n      selector: \":root\",\n      property: \"--color\",\n      value: { type: \"unparsed\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"parse universal selector *\", () => {\n  expect(parseCss(`* { box-sizing: border-box }`)).toEqual([\n    {\n      selector: \"*\",\n      property: \"box-sizing\",\n      value: { type: \"keyword\", value: \"border-box\" },\n    },\n  ]);\n});\n\ntest(\"parse &::pseudo-element as state\", () => {\n  expect(parseCss(`&::after { content: none }`)).toEqual([\n    {\n      selector: \"\",\n      state: \"::after\",\n      property: \"content\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n  ]);\n});\n\ntest(\"parse &:functional-pseudo as state\", () => {\n  expect(parseCss(`&:nth-child(2) { color: red }`)).toEqual([\n    {\n      selector: \"\",\n      state: \":nth-child\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\n// ---- Edge cases ----\n\ntest(\"parse empty string returns empty array\", () => {\n  expect(parseCss(\"\")).toEqual([]);\n});\n\ntest(\"parse malformed CSS returns empty array\", () => {\n  expect(parseCss(\"{{{{\")).toEqual([]);\n});\n\ntest(\"parse CSS with no declarations returns empty array\", () => {\n  expect(parseCss(\".card {}\")).toEqual([]);\n});\n\ntest(\"parse multiple properties from one rule\", () => {\n  const result = parseCss(`.card { display: flex; color: red; opacity: 1 }`);\n  expect(result).toEqual([\n    {\n      selector: \".card\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    },\n    {\n      selector: \".card\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      selector: \".card\",\n      property: \"opacity\",\n      value: { type: \"unit\", unit: \"number\", value: 1 },\n    },\n  ]);\n});\n\ntest(\"parse comma-separated class selectors\", () => {\n  expect(parseCss(`.a, .b { color: red }`)).toEqual([\n    {\n      selector: \".a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      selector: \".b\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"parse comma-separated mixed selectors (class + element)\", () => {\n  expect(parseCss(`.card, div { display: flex }`)).toEqual([\n    {\n      selector: \".card\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    },\n    {\n      selector: \"div\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    },\n  ]);\n});\n\ntest(\"parse comma-separated selectors with different states\", () => {\n  expect(parseCss(`.a:hover, .b:focus { color: blue }`)).toEqual([\n    {\n      selector: \".a\",\n      state: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n    {\n      selector: \".b\",\n      state: \":focus\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\n// ---- At-rules not supported ----\n\ntest(\"ignore @keyframes\", () => {\n  expect(\n    parseCss(`\n      .card { color: red }\n      @keyframes fade { from { opacity: 1 } to { opacity: 0 } }\n   `)\n  ).toEqual([\n    {\n      selector: \".card\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"ignore @font-face\", () => {\n  expect(\n    parseCss(`\n      .card { color: red }\n      @font-face { font-family: \"Custom\"; src: url(font.woff2); }\n   `)\n  ).toEqual([\n    {\n      selector: \".card\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"ignore @supports nested inside @media\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) {\n        .card { color: green }\n        @supports (display: grid) {\n          .card { display: grid }\n        }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(min-width:768px)\",\n      selector: \".card\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    },\n  ]);\n});\n\n// ---- Media query edge cases ----\n\ntest(\"parse bare screen media type as base query\", () => {\n  expect(\n    parseCss(`\n      @media screen {\n        a { color: red }\n      }\n   `)\n  ).toEqual([\n    {\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"ignore @media print with min-width\", () => {\n  expect(\n    parseCss(`\n      @media print and (min-width: 768px) {\n        a { color: black }\n      }\n   `)\n  ).toEqual([]);\n});\n\ntest(\"parse non-px units in non-width features are allowed\", () => {\n  expect(\n    parseCss(`\n      @media (min-resolution: 2dppx) {\n        a { color: red }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(min-resolution:2dppx)\",\n      selector: \"a\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"class selector inside media query preserves both breakpoint and selector\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 640px) {\n        .card:hover { color: blue }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(min-width:640px)\",\n      selector: \".card\",\n      state: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"compound class inside media query\", () => {\n  expect(\n    parseCss(`\n      @media (max-width: 768px) {\n        .card.featured { display: block }\n      }\n   `)\n  ).toEqual([\n    {\n      breakpoint: \"(max-width:768px)\",\n      selector: \".card.featured\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    },\n  ]);\n});\n\ntest(\"nested media with non-px inner is rejected\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) {\n        @media (min-width: 40em) {\n          a { color: red }\n        }\n      }\n   `)\n  ).toEqual([]);\n});\n\ntest(\"nested media with print inner is rejected\", () => {\n  expect(\n    parseCss(`\n      @media (min-width: 768px) {\n        @media print {\n          a { color: red }\n        }\n      }\n   `)\n  ).toEqual([]);\n});\n\ndescribe(\"parseMediaQuery\", () => {\n  test(\"simple min-width\", () => {\n    expect(parseMediaQuery(`(min-width: 768px)`)).toEqual({\n      minWidth: 768,\n    });\n  });\n\n  test(\"simple max-width\", () => {\n    expect(parseMediaQuery(`(max-width: 768px)`)).toEqual({\n      maxWidth: 768,\n    });\n  });\n\n  test(\"orientation portrait condition\", () => {\n    expect(parseMediaQuery(`(orientation: portrait)`)).toEqual({\n      condition: \"orientation:portrait\",\n    });\n  });\n\n  test(\"orientation landscape condition\", () => {\n    expect(parseMediaQuery(`(orientation: landscape)`)).toEqual({\n      condition: \"orientation:landscape\",\n    });\n  });\n\n  test(\"hover condition\", () => {\n    expect(parseMediaQuery(`(hover: hover)`)).toEqual({\n      condition: \"hover:hover\",\n    });\n  });\n\n  test(\"whitespace normalization\", () => {\n    expect(parseMediaQuery(`(orientation:portrait)`)).toEqual({\n      condition: \"orientation:portrait\",\n    });\n    expect(parseMediaQuery(`(  orientation  :  portrait  )`)).toEqual({\n      condition: \"orientation:portrait\",\n    });\n  });\n\n  test(\"multiple conditions\", () => {\n    expect(\n      parseMediaQuery(`(orientation: portrait) and (hover: hover)`)\n    ).toEqual({\n      condition: \"orientation:portrait and hover:hover\",\n    });\n  });\n\n  test(\"prefers-color-scheme dark\", () => {\n    expect(parseMediaQuery(`(prefers-color-scheme: dark)`)).toEqual({\n      condition: \"prefers-color-scheme:dark\",\n    });\n  });\n\n  test(\"prefers-color-scheme light\", () => {\n    expect(parseMediaQuery(`(prefers-color-scheme: light)`)).toEqual({\n      condition: \"prefers-color-scheme:light\",\n    });\n  });\n\n  test(\"pointer coarse\", () => {\n    expect(parseMediaQuery(`(pointer: coarse)`)).toEqual({\n      condition: \"pointer:coarse\",\n    });\n  });\n\n  test(\"prefers-reduced-motion\", () => {\n    expect(parseMediaQuery(`(prefers-reduced-motion: reduce)`)).toEqual({\n      condition: \"prefers-reduced-motion:reduce\",\n    });\n  });\n\n  test(\"prefers-contrast\", () => {\n    expect(parseMediaQuery(`(prefers-contrast: more)`)).toEqual({\n      condition: \"prefers-contrast:more\",\n    });\n  });\n\n  test(\"display-mode\", () => {\n    expect(parseMediaQuery(`(display-mode: standalone)`)).toEqual({\n      condition: \"display-mode:standalone\",\n    });\n  });\n\n  test(\"any-hover\", () => {\n    expect(parseMediaQuery(`(any-hover: hover)`)).toEqual({\n      condition: \"any-hover:hover\",\n    });\n  });\n\n  test(\"any-pointer\", () => {\n    expect(parseMediaQuery(`(any-pointer: fine)`)).toEqual({\n      condition: \"any-pointer:fine\",\n    });\n  });\n\n  test(\"combined min-width and max-width\", () => {\n    expect(\n      parseMediaQuery(`(min-width: 768px) and (max-width: 1024px)`)\n    ).toEqual({\n      minWidth: 768,\n      maxWidth: 1024,\n    });\n  });\n\n  test(\"combined max-width and min-width (reversed order)\", () => {\n    expect(\n      parseMediaQuery(`(max-width: 1024px) and (min-width: 768px)`)\n    ).toEqual({\n      minWidth: 768,\n      maxWidth: 1024,\n    });\n  });\n\n  test(\"min-width combined with condition feature\", () => {\n    expect(\n      parseMediaQuery(`(min-width: 768px) and (orientation: landscape)`)\n    ).toEqual({\n      minWidth: 768,\n      condition: \"orientation:landscape\",\n    });\n  });\n\n  test(\"max-width combined with condition feature\", () => {\n    expect(parseMediaQuery(`(max-width: 480px) and (hover: none)`)).toEqual({\n      maxWidth: 480,\n      condition: \"hover:none\",\n    });\n  });\n\n  test(\"min-width, max-width, and condition feature combined\", () => {\n    expect(\n      parseMediaQuery(\n        `(min-width: 768px) and (max-width: 1024px) and (orientation: portrait)`\n      )\n    ).toEqual({\n      minWidth: 768,\n      maxWidth: 1024,\n      condition: \"orientation:portrait\",\n    });\n  });\n\n  test(\"returns undefined for non-px units\", () => {\n    expect(parseMediaQuery(`(min-width: 40rem)`)).toBeUndefined();\n    expect(parseMediaQuery(`(min-width: 50em)`)).toBeUndefined();\n  });\n\n  test(\"returns undefined for media types without features\", () => {\n    expect(parseMediaQuery(`print`)).toBeUndefined();\n    expect(parseMediaQuery(`screen`)).toBeUndefined();\n  });\n});\n\ntest(\"camel case css property\", () => {\n  expect(camelCaseProperty(\"margin-top\")).toEqual(\"marginTop\");\n  expect(camelCaseProperty(\"-webkit-font-smoothing\")).toEqual(\n    \"WebkitFontSmoothing\"\n  );\n  expect(camelCaseProperty(\"-moz-osx-font-smoothing\")).toEqual(\n    \"MozOsxFontSmoothing\"\n  );\n});\n\ntest(\"camel case css property multiple times\", () => {\n  expect(camelCaseProperty(camelCaseProperty(\"margin-top\"))).toEqual(\n    \"marginTop\"\n  );\n  expect(\n    camelCaseProperty(camelCaseProperty(\"-webkit-font-smoothing\"))\n  ).toEqual(\"WebkitFontSmoothing\");\n  expect(\n    camelCaseProperty(camelCaseProperty(\"-moz-osx-font-smoothing\"))\n  ).toEqual(\"MozOsxFontSmoothing\");\n});\n\ndescribe(\"parseClassBasedSelector\", () => {\n  test(\"simple class selector\", () => {\n    expect(parseClassBasedSelector(\".card\")).toEqual({\n      tokenName: \"card\",\n      classNames: [\"card\"],\n    });\n  });\n\n  test(\"compound class selector\", () => {\n    expect(parseClassBasedSelector(\".card.active\")).toEqual({\n      tokenName: \"card.active\",\n      classNames: [\"card\", \"active\"],\n    });\n  });\n\n  test(\"triple compound class selector\", () => {\n    expect(parseClassBasedSelector(\".a.b.c\")).toEqual({\n      tokenName: \"a.b.c\",\n      classNames: [\"a\", \"b\", \"c\"],\n    });\n  });\n\n  test(\"class with attribute selector\", () => {\n    expect(parseClassBasedSelector(\".btn[disabled]\")).toEqual({\n      tokenName: \"btn\",\n      classNames: [\"btn\"],\n      states: [\"[disabled]\"],\n    });\n  });\n\n  test(\"compound class with attribute selector\", () => {\n    expect(parseClassBasedSelector(\".card.active[open]\")).toEqual({\n      tokenName: \"card.active\",\n      classNames: [\"card\", \"active\"],\n      states: [\"[open]\"],\n    });\n  });\n\n  test(\"rejects element selector\", () => {\n    expect(parseClassBasedSelector(\"div\")).toBeUndefined();\n  });\n\n  test(\"rejects id selector\", () => {\n    expect(parseClassBasedSelector(\"#hero\")).toBeUndefined();\n  });\n\n  test(\"rejects :root pseudo selector\", () => {\n    expect(parseClassBasedSelector(\":root\")).toBeUndefined();\n  });\n\n  test(\"descendant combinator: .card .inner\", () => {\n    expect(parseClassBasedSelector(\".card .inner\")).toEqual({\n      tokenName: \"card__inner\",\n      classNames: [\"inner\"],\n      ancestors: [{ classNames: [\"card\"], combinator: \"descendant\" }],\n    });\n  });\n\n  test(\"child combinator: .card>.inner\", () => {\n    expect(parseClassBasedSelector(\".card>.inner\")).toEqual({\n      tokenName: \"card__inner\",\n      classNames: [\"inner\"],\n      ancestors: [{ classNames: [\"card\"], combinator: \"child\" }],\n    });\n  });\n\n  test(\"rejects sibling combinator\", () => {\n    expect(parseClassBasedSelector(\".a+.b\")).toBeUndefined();\n  });\n\n  test(\"rejects general sibling combinator\", () => {\n    expect(parseClassBasedSelector(\".a~.b\")).toBeUndefined();\n  });\n\n  test(\"rejects universal selector\", () => {\n    expect(parseClassBasedSelector(\"*\")).toBeUndefined();\n  });\n\n  test(\"rejects empty string\", () => {\n    expect(parseClassBasedSelector(\"\")).toBeUndefined();\n  });\n\n  test(\"attribute selector with value\", () => {\n    expect(parseClassBasedSelector(\".input[type=text]\")).toEqual({\n      tokenName: \"input\",\n      classNames: [\"input\"],\n      states: [\"[type=text]\"],\n    });\n  });\n\n  test(\"class with :hover pseudo-class\", () => {\n    expect(parseClassBasedSelector(\".card:hover\")).toEqual({\n      tokenName: \"card\",\n      classNames: [\"card\"],\n      states: [\":hover\"],\n    });\n  });\n\n  test(\"class with ::before pseudo-element\", () => {\n    expect(parseClassBasedSelector(\".card::before\")).toEqual({\n      tokenName: \"card\",\n      classNames: [\"card\"],\n      states: [\"::before\"],\n    });\n  });\n\n  test(\"class with functional pseudo-class :nth-child(2n+1)\", () => {\n    expect(parseClassBasedSelector(\".item:nth-child(2n+1)\")).toEqual({\n      tokenName: \"item\",\n      classNames: [\"item\"],\n      states: [\":nth-child(2n+1)\"],\n    });\n  });\n\n  test(\"class with multiple states: attribute + pseudo\", () => {\n    expect(parseClassBasedSelector(\".btn[disabled]:hover\")).toEqual({\n      tokenName: \"btn\",\n      classNames: [\"btn\"],\n      states: [\"[disabled]\", \":hover\"],\n    });\n  });\n\n  test(\"compound class with pseudo-class and pseudo-element\", () => {\n    expect(parseClassBasedSelector(\".card.active:hover::after\")).toEqual({\n      tokenName: \"card.active\",\n      classNames: [\"card\", \"active\"],\n      states: [\":hover\", \"::after\"],\n    });\n  });\n\n  test(\"class with multiple attribute selectors\", () => {\n    expect(parseClassBasedSelector(\".input[type=text][required]\")).toEqual({\n      tokenName: \"input\",\n      classNames: [\"input\"],\n      states: [\"[type=text]\", \"[required]\"],\n    });\n  });\n\n  test(\"class with :focus-within pseudo-class\", () => {\n    expect(parseClassBasedSelector(\".form:focus-within\")).toEqual({\n      tokenName: \"form\",\n      classNames: [\"form\"],\n      states: [\":focus-within\"],\n    });\n  });\n\n  test(\"rejects element.class (type selector prefix)\", () => {\n    expect(parseClassBasedSelector(\"div.card\")).toBeUndefined();\n  });\n\n  test(\"rejects nesting selector &.class\", () => {\n    expect(parseClassBasedSelector(\"&.card\")).toBeUndefined();\n  });\n\n  // Nested selector tests\n  test(\"multiple ancestors: .a .b .c\", () => {\n    expect(parseClassBasedSelector(\".a .b .c\")).toEqual({\n      tokenName: \"a__b__c\",\n      classNames: [\"c\"],\n      ancestors: [\n        { classNames: [\"a\"], combinator: \"descendant\" },\n        { classNames: [\"b\"], combinator: \"descendant\" },\n      ],\n    });\n  });\n\n  test(\"compound segments in nested: .card.active .title.bold\", () => {\n    expect(parseClassBasedSelector(\".card.active .title.bold\")).toEqual({\n      tokenName: \"card.active__title.bold\",\n      classNames: [\"title\", \"bold\"],\n      ancestors: [{ classNames: [\"card\", \"active\"], combinator: \"descendant\" }],\n    });\n  });\n\n  test(\"mixed combinators: .a .b > .c\", () => {\n    expect(parseClassBasedSelector(\".a .b > .c\")).toEqual({\n      tokenName: \"a__b__c\",\n      classNames: [\"c\"],\n      ancestors: [\n        { classNames: [\"a\"], combinator: \"descendant\" },\n        { classNames: [\"b\"], combinator: \"child\" },\n      ],\n    });\n  });\n\n  test(\"nested with state on target: .card > .title:hover\", () => {\n    expect(parseClassBasedSelector(\".card > .title:hover\")).toEqual({\n      tokenName: \"card__title\",\n      classNames: [\"title\"],\n      states: [\":hover\"],\n      ancestors: [{ classNames: [\"card\"], combinator: \"child\" }],\n    });\n  });\n\n  test(\"nested with pseudo-element: .card .title::before\", () => {\n    expect(parseClassBasedSelector(\".card .title::before\")).toEqual({\n      tokenName: \"card__title\",\n      classNames: [\"title\"],\n      states: [\"::before\"],\n      ancestors: [{ classNames: [\"card\"], combinator: \"descendant\" }],\n    });\n  });\n\n  test(\"rejects type selector in ancestor: h1 .card\", () => {\n    expect(parseClassBasedSelector(\"h1 .card\")).toBeUndefined();\n  });\n\n  test(\"rejects type selector in target: .card h1\", () => {\n    expect(parseClassBasedSelector(\".card h1\")).toBeUndefined();\n  });\n\n  test(\"rejects id selector in target: .card #hero\", () => {\n    expect(parseClassBasedSelector(\".card #hero\")).toBeUndefined();\n  });\n\n  test(\"rejects attribute selector in ancestor: .card[disabled] .title\", () => {\n    expect(parseClassBasedSelector(\".card[disabled] .title\")).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/parse-css.ts",
    "content": "import { camelCase } from \"change-case\";\nimport * as csstree from \"css-tree\";\nimport type {\n  StyleValue,\n  CssProperty,\n  StyleProperty,\n} from \"@webstudio-is/css-engine\";\nimport { parseCssValue as parseCssValueLonghand } from \"./parse-css-value\";\nimport { expandShorthands } from \"./shorthands\";\n\nexport type ParsedStyleDecl = {\n  breakpoint?: string;\n  selector: string;\n  state?: string;\n  property: CssProperty;\n  value: StyleValue;\n};\n\n// @todo we don't parse correctly most of them if not all\nconst prefixedProperties = [\n  \"-webkit-box-orient\",\n  \"-webkit-line-clamp\",\n  \"-webkit-font-smoothing\",\n  \"-moz-osx-font-smoothing\",\n  \"-webkit-tap-highlight-color\",\n  \"-webkit-overflow-scrolling\",\n  \"-webkit-text-stroke\",\n  \"-webkit-text-stroke-color\",\n  \"-webkit-text-stroke-width\",\n];\nconst prefixes = [\"webkit\", \"moz\", \"ms\", \"o\"];\nconst prefixRegex = new RegExp(`^-(${prefixes.join(\"|\")})-`);\n\nconst normalizeProperty = (property: string): CssProperty => {\n  // convert unprefixed used by webflow version into prefixed one\n  if (property === \"tap-highlight-color\") {\n    return \"-webkit-tap-highlight-color\";\n  }\n  if (property === \"font-smoothing\") {\n    return \"-webkit-font-smoothing\";\n  }\n  if (prefixedProperties.includes(property)) {\n    return property as CssProperty;\n  }\n  // remove old or unexpected prefixes\n  return property.replace(prefixRegex, \"\") as CssProperty;\n};\n\n/**\n * Store prefixed properties without change\n * and convert to camel case only unprefixed properties\n * @todo stop converting to camel case and use hyphenated format\n */\nexport const camelCaseProperty = (\n  property: CssProperty | StyleProperty\n): StyleProperty => {\n  property = normalizeProperty(property) as CssProperty | StyleProperty;\n  // these are manually added with pascal case\n  if (\n    property === \"-webkit-font-smoothing\" ||\n    property === \"WebkitFontSmoothing\"\n  ) {\n    return \"WebkitFontSmoothing\";\n  }\n  if (\n    property === \"-moz-osx-font-smoothing\" ||\n    property === \"MozOsxFontSmoothing\"\n  ) {\n    return \"MozOsxFontSmoothing\";\n  }\n  if (property.startsWith(\"-\")) {\n    return property as StyleProperty;\n  }\n  return camelCase(property) as StyleProperty;\n};\n\nconst parseCssValue = (property: CssProperty, value: string) => {\n  const expanded = new Map(expandShorthands([[property, value]]));\n  const final = new Map<CssProperty, StyleValue>();\n  for (const [property, value] of expanded) {\n    final.set(property, parseCssValueLonghand(property, value));\n  }\n  return final;\n};\n\nconst cssTreeTryParse = (input: string) => {\n  try {\n    const ast = csstree.parse(input);\n    return ast;\n  } catch {\n    return;\n  }\n};\n\ntype Selector = {\n  name: string;\n  state?: string;\n};\n\nexport const parseCss = (css: string): ParsedStyleDecl[] => {\n  const ast = cssTreeTryParse(css);\n  const styles = new Map<string, ParsedStyleDecl>();\n\n  if (ast === undefined) {\n    return [];\n  }\n\n  // Track context as we traverse — use a stack to support nested @media\n  const atruleStack: csstree.Atrule[] = [];\n  let currentRule: csstree.Rule | undefined;\n\n  csstree.walk(ast, {\n    enter(node) {\n      if (node.type === \"Atrule\") {\n        atruleStack.push(node);\n      } else if (node.type === \"Rule\") {\n        currentRule = node;\n      }\n    },\n    leave(node) {\n      if (node.type === \"Atrule\") {\n        atruleStack.pop();\n      } else if (node.type === \"Rule\") {\n        currentRule = undefined;\n      }\n\n      // Process declarations\n      if (\n        node.type !== \"Declaration\" ||\n        !currentRule ||\n        currentRule.prelude.type === undefined\n      ) {\n        return;\n      }\n\n      // All enclosing at-rules must be @media — reject @supports, @keyframes, etc.\n      if (atruleStack.some((atrule) => atrule.name !== \"media\")) {\n        return;\n      }\n\n      let breakpoint: undefined | string;\n      let invalidBreakpoint = false;\n\n      // Collect and flatten all enclosing @media preludes\n      const flattenedParts: string[] = [];\n      for (const atrule of atruleStack) {\n        if (atrule.prelude?.type === \"AtrulePrelude\") {\n          let hasNonPxWidthUnit = false;\n          let hasPrintMedia = false;\n          let currentFeatureName: string | undefined;\n          csstree.walk(atrule.prelude, {\n            enter: (node) => {\n              if (node.type === \"MediaQuery\") {\n                // Mutates AST in-place to normalize media type for breakpoint string generation;\n                // the AST is discarded after parseCss returns so this is safe.\n                if (node.mediaType === \"screen\" || node.mediaType === \"all\") {\n                  node.mediaType = null;\n                }\n                if (node.mediaType === \"print\") {\n                  hasPrintMedia = true;\n                }\n              }\n              if (node.type === \"Feature\") {\n                currentFeatureName = node.name;\n              }\n              // Only reject non-px units on width features (min-width, max-width)\n              if (\n                node.type === \"Dimension\" &&\n                node.unit !== \"px\" &&\n                (currentFeatureName === \"min-width\" ||\n                  currentFeatureName === \"max-width\")\n              ) {\n                hasNonPxWidthUnit = true;\n              }\n            },\n            leave: (node) => {\n              if (node.type === \"Feature\") {\n                currentFeatureName = undefined;\n              }\n            },\n          });\n          if (hasPrintMedia || hasNonPxWidthUnit) {\n            invalidBreakpoint = true;\n            break;\n          }\n          const generated = csstree.generate(atrule.prelude);\n          if (generated) {\n            flattenedParts.push(generated);\n          }\n        }\n      }\n\n      if (!invalidBreakpoint && flattenedParts.length > 0) {\n        breakpoint = flattenedParts.join(\" and \");\n      }\n\n      if (invalidBreakpoint || currentRule.prelude.type !== \"SelectorList\") {\n        return;\n      }\n\n      const selectors: Selector[] = [];\n\n      for (const node of currentRule.prelude.children) {\n        if (node.type !== \"Selector\") {\n          continue;\n        }\n        let selector: Selector | undefined = undefined;\n        const children = node.children.toArray();\n        const startsWithNesting = children[0]?.type === \"NestingSelector\";\n        for (let index = 0; index < children.length; index += 1) {\n          const childNode = children[index];\n          let name: string = \"\";\n          let state: string | undefined;\n          switch (childNode.type) {\n            case \"TypeSelector\":\n              name = childNode.name;\n              break;\n            case \"ClassSelector\":\n              name = `.${childNode.name}`;\n              break;\n            case \"IdSelector\":\n              name = `#${childNode.name}`;\n              break;\n            case \"AttributeSelector\":\n              // for example &[data-state=active]\n              if (startsWithNesting && index === 1 && children.length === 2) {\n                state = csstree.generate(childNode);\n              } else {\n                name = csstree.generate(childNode);\n              }\n              break;\n            case \"PseudoClassSelector\": {\n              // First pseudo selector is not a state but an element selector, e.g. :root\n              if (selector) {\n                state = `:${childNode.name}`;\n              } else {\n                name = `:${childNode.name}`;\n              }\n              break;\n            }\n            case \"PseudoElementSelector\":\n              state = `::${childNode.name}`;\n              break;\n            case \"Combinator\":\n              // \" \" vs \" > \"\n              name =\n                childNode.name === \" \" ? childNode.name : ` ${childNode.name} `;\n              break;\n          }\n\n          if (selector) {\n            selector.name += name;\n            if (state) {\n              selector.state = state;\n            }\n          } else {\n            selector = { name, state };\n          }\n        }\n        if (selector) {\n          selectors.push(selector);\n          selector = undefined;\n        }\n      }\n\n      const stringValue = csstree.generate(node.value);\n\n      const parsedCss = parseCssValue(\n        normalizeProperty(node.property),\n        stringValue\n      );\n\n      for (const { name: selector, state } of selectors) {\n        for (const [property, value] of parsedCss) {\n          const normalizedProperty = normalizeProperty(property);\n          const styleDecl: ParsedStyleDecl = {\n            selector,\n            property: normalizedProperty,\n            value,\n          };\n          if (breakpoint) {\n            styleDecl.breakpoint = breakpoint;\n          }\n          if (state) {\n            styleDecl.state = state;\n          }\n\n          // deduplicate styles within selector and state by using map\n          styles.set(\n            `${breakpoint}:${selector}:${state}:${normalizedProperty}`,\n            styleDecl\n          );\n        }\n      }\n    },\n  });\n\n  return Array.from(styles.values());\n};\n\nexport type ParsedClassSelector = {\n  /** Token name: \"card\" for .card, \"card__title\" for .card .title */\n  tokenName: string;\n  /** Target class names (the last segment): [\"card\"] or [\"title\"] */\n  classNames: string[];\n  /**\n   * Non-class selector suffixes on the target: attribute selectors, pseudo-classes, pseudo-elements.\n   * e.g. [\"[disabled]\"] for .btn[disabled], [\":hover\"] for .card:hover\n   */\n  states?: string[];\n  /**\n   * Ancestor constraints for nested selectors (e.g., .card .title).\n   * Ordered from outermost to innermost ancestor.\n   * Each entry's combinator describes the relationship to the next segment.\n   */\n  ancestors?: Array<{\n    classNames: string[];\n    combinator: \"descendant\" | \"child\";\n  }>;\n};\n\n/**\n * Parse a selector string and determine if it's a class-based selector\n * suitable for token extraction. Uses css-tree to handle the full CSS\n * selector spec.\n *\n * Supports simple selectors, compound selectors, and nested selectors\n * using descendant (\" \") and child (\">\") combinators where ALL segments\n * are class-based.\n *\n * Supported patterns:\n *   .card                      → { tokenName: \"card\", classNames: [\"card\"] }\n *   .card.active               → { tokenName: \"card.active\", classNames: [\"card\", \"active\"] }\n *   .btn[disabled]             → { tokenName: \"btn\", ..., states: [\"[disabled]\"] }\n *   .card:hover                → { tokenName: \"card\", ..., states: [\":hover\"] }\n *   .card .title               → { tokenName: \"card__title\", ..., ancestors: [{classNames:[\"card\"], combinator:\"descendant\"}] }\n *   .card > .title             → { tokenName: \"card__title\", ..., ancestors: [{classNames:[\"card\"], combinator:\"child\"}] }\n *   .a .b .c                   → { tokenName: \"a__b__c\", ..., ancestors: [{..,\"descendant\"},{..,\"descendant\"}] }\n *   .card > .title:hover       → { tokenName: \"card__title\", ..., states: [\":hover\"], ancestors: [...] }\n *\n * Returns undefined for:\n * - Selectors with sibling combinators (+, ~)\n * - Selectors with non-class nodes in ancestor segments (e.g., .card h1, h1 .card)\n * - Pure element/id selectors\n */\nexport const parseClassBasedSelector = (\n  selector: string\n): ParsedClassSelector | undefined => {\n  let ast: csstree.CssNode;\n  try {\n    ast = csstree.parse(selector, { context: \"selector\" });\n  } catch {\n    return undefined;\n  }\n\n  if (ast.type !== \"Selector\") {\n    return undefined;\n  }\n\n  const children = ast.children.toArray();\n  if (children.length === 0) {\n    return undefined;\n  }\n\n  // First node must be a ClassSelector\n  if (children[0].type !== \"ClassSelector\") {\n    return undefined;\n  }\n\n  // Split children into segments at Combinator nodes\n  type Segment = {\n    nodes: csstree.CssNode[];\n    combinator?: \"descendant\" | \"child\";\n  };\n  const segments: Segment[] = [];\n  let currentNodes: csstree.CssNode[] = [];\n\n  for (const child of children) {\n    if (child.type === \"Combinator\") {\n      if (child.name === \" \") {\n        segments.push({ nodes: currentNodes, combinator: \"descendant\" });\n      } else if (child.name === \">\") {\n        segments.push({ nodes: currentNodes, combinator: \"child\" });\n      } else {\n        // Sibling combinators (+, ~) — reject\n        return undefined;\n      }\n      currentNodes = [];\n    } else {\n      currentNodes.push(child);\n    }\n  }\n  // Push the last (target) segment\n  segments.push({ nodes: currentNodes });\n\n  // Validate all segments and extract class names\n  const ancestors: Array<{\n    classNames: string[];\n    combinator: \"descendant\" | \"child\";\n  }> = [];\n\n  // Process ancestor segments (all except the last)\n  for (let i = 0; i < segments.length - 1; i++) {\n    const segment = segments[i];\n    const segClassNames: string[] = [];\n    for (const node of segment.nodes) {\n      if (node.type === \"ClassSelector\") {\n        segClassNames.push(node.name);\n      } else {\n        // Non-class node in an ancestor segment — reject\n        return undefined;\n      }\n    }\n    if (segClassNames.length === 0) {\n      return undefined;\n    }\n    ancestors.push({\n      classNames: segClassNames,\n      combinator: segment.combinator!,\n    });\n  }\n\n  // Process the last (target) segment\n  const lastSegment = segments[segments.length - 1];\n  const classNames: string[] = [];\n  const states: string[] = [];\n\n  for (const node of lastSegment.nodes) {\n    switch (node.type) {\n      case \"ClassSelector\":\n        classNames.push(node.name);\n        break;\n      case \"AttributeSelector\":\n      case \"PseudoClassSelector\":\n      case \"PseudoElementSelector\":\n        states.push(csstree.generate(node));\n        break;\n      default:\n        return undefined;\n    }\n  }\n\n  if (classNames.length === 0) {\n    return undefined;\n  }\n\n  // Build token name: join segment class names with \"__\"\n  const segmentNames = [\n    ...ancestors.map((a) =>\n      a.classNames.length === 1 ? a.classNames[0] : a.classNames.join(\".\")\n    ),\n    classNames.length === 1 ? classNames[0] : classNames.join(\".\"),\n  ];\n  const tokenName = segmentNames.join(\"__\");\n\n  return {\n    tokenName,\n    classNames,\n    ...(states.length > 0 ? { states } : {}),\n    ...(ancestors.length > 0 ? { ancestors } : {}),\n  };\n};\n\ntype ParsedBreakpoint = {\n  minWidth?: number;\n  maxWidth?: number;\n  condition?: string;\n};\n\nexport const parseMediaQuery = (\n  mediaQuery: string\n): undefined | ParsedBreakpoint => {\n  const ast = csstree.parse(mediaQuery, { context: \"mediaQuery\" });\n  let minWidth: undefined | number;\n  let maxWidth: undefined | number;\n  let currentWidthProperty: undefined | \"minWidth\" | \"maxWidth\";\n  const otherFeatures: string[] = [];\n\n  csstree.walk(ast, (node) => {\n    if (node.type === \"Feature\") {\n      if (node.name === \"min-width\") {\n        currentWidthProperty = \"minWidth\";\n      } else if (node.name === \"max-width\") {\n        currentWidthProperty = \"maxWidth\";\n      } else {\n        currentWidthProperty = undefined;\n        // Capture any other media feature as custom condition\n        const generated = csstree.generate(node);\n        // Remove outer parentheses if present\n        let cleaned =\n          generated.startsWith(\"(\") && generated.endsWith(\")\")\n            ? generated.slice(1, -1)\n            : generated;\n        // Normalize whitespace: remove spaces around colons for consistency\n        cleaned = cleaned.replace(/\\s*:\\s*/g, \":\");\n        otherFeatures.push(cleaned);\n      }\n    }\n    if (node.type === \"Dimension\" && node.unit === \"px\") {\n      const value = Number(node.value);\n      if (currentWidthProperty === \"minWidth\") {\n        minWidth = value;\n      } else if (currentWidthProperty === \"maxWidth\") {\n        maxWidth = value;\n      }\n      currentWidthProperty = undefined;\n    }\n  });\n\n  const condition =\n    otherFeatures.length > 0 ? otherFeatures.join(\" and \") : undefined;\n\n  const hasWidth = minWidth !== undefined || maxWidth !== undefined;\n\n  // If there's a custom condition and no width, return only condition\n  if (condition !== undefined && !hasWidth) {\n    return { condition };\n  }\n\n  if (!hasWidth && condition === undefined) {\n    return;\n  }\n\n  return {\n    ...(minWidth !== undefined ? { minWidth } : {}),\n    ...(maxWidth !== undefined ? { maxWidth } : {}),\n    ...(condition !== undefined ? { condition } : {}),\n  };\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/conic-gradient.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { parseConicGradient, formatConicGradient } from \"./conic-gradient\";\n\ntype ConicGradient = NonNullable<ReturnType<typeof parseConicGradient>>;\nconst conic = (gradient: Omit<ConicGradient, \"type\">): ConicGradient => ({\n  type: \"conic\",\n  ...gradient,\n});\n\ndescribe(\"parse conic-gradient\", () => {\n  test(\"parses gradient without angle or position\", () => {\n    expect(\n      parseConicGradient(\"conic-gradient(red 0%, blue 50%, yellow 100%)\")\n    ).toEqual(\n      conic({\n        angle: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 50 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with angle\", () => {\n    expect(\n      parseConicGradient(\"conic-gradient(from 135deg, orange 0%, cyan 100%)\")\n    ).toEqual(\n      conic({\n        angle: { type: \"unit\", unit: \"deg\", value: 135 },\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.6471, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 1, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with position\", () => {\n    expect(\n      parseConicGradient(\"conic-gradient(at center, red 0%, blue 100%)\")\n    ).toEqual(\n      conic({\n        angle: undefined,\n        position: \"center\",\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with angle positions\", () => {\n    expect(\n      parseConicGradient(\n        \"conic-gradient(from 0deg at 50% 50%, rgba(255,126,95,1) 0deg, rgba(254,180,123,1) 120deg, rgba(134,168,231,1) 240deg, rgba(255,126,95,1) 360deg)\"\n      )\n    ).toEqual(\n      conic({\n        angle: { type: \"unit\", unit: \"deg\", value: 0 },\n        position: \"50% 50%\",\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.4941, 0.3725],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"deg\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0.9961, 0.7059, 0.4824],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"deg\", value: 120 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0.5255, 0.6588, 0.9059],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"deg\", value: 240 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.4941, 0.3725],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"deg\", value: 360 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses repeating conic gradient\", () => {\n    const parsed = parseConicGradient(\n      \"repeating-conic-gradient(red 0% 25%, blue 50% 75%)\"\n    );\n    expect(parsed).toEqual(\n      conic({\n        angle: undefined,\n        position: undefined,\n        repeating: true,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: { type: \"unit\", unit: \"%\", value: 25 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 50 },\n            hint: { type: \"unit\", unit: \"%\", value: 75 },\n          },\n        ],\n      })\n    );\n    expect(formatConicGradient(parsed!)).toBe(\n      \"repeating-conic-gradient(rgb(255 0 0 / 1) 0% 25%, rgb(0 0 255 / 1) 50% 75%)\"\n    );\n  });\n\n  test(\"parses gradient with css variables\", () => {\n    expect(\n      parseConicGradient(\n        \"conic-gradient(from var(--angle, 45deg), var(--start-color, red) var(--start, 0%) var(--start-hint, 5%), var(--end-color) var(--end))\"\n      )\n    ).toEqual(\n      conic({\n        angle: {\n          type: \"var\",\n          value: \"angle\",\n          fallback: { type: \"unparsed\", value: \"45deg\" },\n        },\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"var\",\n              value: \"start-color\",\n              fallback: { type: \"unparsed\", value: \"red\" },\n            },\n            position: {\n              type: \"var\",\n              value: \"start\",\n              fallback: { type: \"unparsed\", value: \"0%\" },\n            },\n            hint: {\n              type: \"var\",\n              value: \"start-hint\",\n              fallback: { type: \"unparsed\", value: \"5%\" },\n            },\n          },\n          {\n            color: { type: \"var\", value: \"end-color\" },\n            position: { type: \"var\", value: \"end\" },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with variable angle without fallback\", () => {\n    expect(\n      parseConicGradient(\"conic-gradient(from var(--angle), red 0%, blue 100%)\")\n    ).toEqual(\n      conic({\n        angle: {\n          type: \"var\",\n          value: \"angle\",\n        },\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with variable positions and hints\", () => {\n    const parsed = parseConicGradient(\n      \"conic-gradient(red var(--start, 10%) var(--hint, 20%), blue var(--end))\"\n    );\n    expect(parsed).toEqual(\n      conic({\n        angle: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: {\n              type: \"var\",\n              value: \"start\",\n              fallback: { type: \"unparsed\", value: \"10%\" },\n            },\n            hint: {\n              type: \"var\",\n              value: \"hint\",\n              fallback: { type: \"unparsed\", value: \"20%\" },\n            },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"var\", value: \"end\" },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n    expect(formatConicGradient(parsed!)).toBe(\n      \"conic-gradient(rgb(255 0 0 / 1) var(--start, 10%) var(--hint, 20%), rgb(0 0 255 / 1) var(--end))\"\n    );\n  });\n\n  test(\"parses keyword colors\", () => {\n    const parsed = parseConicGradient(\n      \"conic-gradient(green 0%, blue 50%, yellow 100%)\"\n    );\n    expect(parsed).toEqual(\n      conic({\n        angle: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0.502, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 50 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n    expect(formatConicGradient(parsed!)).toBe(\n      \"conic-gradient(rgb(0 128 0 / 1) 0%, rgb(0 0 255 / 1) 50%, rgb(255 255 0 / 1) 100%)\"\n    );\n  });\n\n  test(\"parses gradient with rgb colors\", () => {\n    const parsed = parseConicGradient(\n      \"conic-gradient(rgb(255, 0, 0), rgb(0, 255, 0), rgb(0, 0, 255))\"\n    );\n    expect(parsed).toEqual(\n      conic({\n        angle: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 1, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n    expect(formatConicGradient(parsed!)).toBe(\n      \"conic-gradient(rgb(255 0 0 / 1), rgb(0 255 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"returns undefined for invalid input\", () => {\n    expect(parseConicGradient(\"conic-gradient()\")).toBeUndefined();\n    expect(parseConicGradient(\"conic-gradient(var())\")).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/conic-gradient.ts",
    "content": "import * as csstree from \"css-tree\";\nimport { cssTryParseValue } from \"../parse-css-value\";\nimport {\n  type UnitValue,\n  toValue,\n  type VarValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  getColor,\n  isAngleUnit,\n  isColorStop,\n  mapLengthPercentageOrVar,\n  formatGradientStops,\n  normalizeRepeatingGradient,\n  forEachGradientParts,\n} from \"./gradient-utils\";\nimport type { GradientStop, ParsedConicGradient } from \"./types\";\n\ntype GradientPartResult =\n  | { type: \"angle\"; value: UnitValue | VarValue }\n  | { type: \"position\"; value: string }\n  | { type: \"stop\"; value: GradientStop }\n  | { type: \"hint\"; value: GradientStop[\"hint\"] };\n\nconst isIdentifier = (node: csstree.CssNode, value: string) =>\n  node.type === \"Identifier\" && node.name.toLowerCase() === value;\n\nconst parseGradientPart = (\n  nodes: csstree.CssNode[]\n): GradientPartResult | undefined => {\n  const filtered = nodes.filter((node) => node.type !== \"WhiteSpace\");\n  if (filtered.length === 0) {\n    return;\n  }\n\n  if (isIdentifier(filtered[0], \"from\")) {\n    const angleNode = filtered[1];\n    const mapped = mapLengthPercentageOrVar(angleNode);\n    if (\n      mapped !== undefined &&\n      ((mapped.type === \"unit\" && isAngleUnit(mapped.unit)) ||\n        mapped.type === \"var\")\n    ) {\n      return { type: \"angle\", value: mapped };\n    }\n    return;\n  }\n\n  if (isIdentifier(filtered[0], \"at\")) {\n    const css = filtered\n      .slice(1)\n      .map((item) => csstree.generate(item))\n      .join(\" \")\n      .trim();\n    if (css.length > 0) {\n      return { type: \"position\", value: css };\n    }\n    return;\n  }\n\n  const colorNode = filtered.find(isColorStop);\n  if (colorNode !== undefined) {\n    const color = getColor(colorNode);\n    const colorIndex = filtered.indexOf(colorNode);\n    const positionNode = filtered[colorIndex + 1];\n    const hintNode = filtered[colorIndex + 2];\n\n    const stop: GradientStop = {\n      color,\n      position: mapLengthPercentageOrVar(positionNode),\n      hint: mapLengthPercentageOrVar(hintNode),\n    };\n\n    return { type: \"stop\", value: stop };\n  }\n\n  const hint = mapLengthPercentageOrVar(filtered[0]);\n  if (hint !== undefined) {\n    return { type: \"hint\", value: hint };\n  }\n};\n\nexport const parseConicGradient = (\n  gradient: string\n): ParsedConicGradient | undefined => {\n  const { normalized: normalizedGradient, isRepeating } =\n    normalizeRepeatingGradient(\n      gradient,\n      \"repeating-conic-gradient\",\n      \"conic-gradient\"\n    );\n\n  const ast = cssTryParseValue(normalizedGradient);\n  if (ast === undefined) {\n    return;\n  }\n\n  // css-tree grammar doesn't allow angles in color stops, so we fall back to\n  // manual parsing even when the built-in matcher rejects the value.\n  csstree.lexer.match(\n    \"conic-gradient( [ from <angle> ]? [ at <position> ]? , <color-stop-list> )\",\n    ast\n  );\n\n  let angle: UnitValue | VarValue | undefined;\n  let position: string | undefined;\n  const stops: GradientStop[] = [];\n\n  forEachGradientParts(ast, \"conic-gradient\", (gradientParts) => {\n    const filteredParts = gradientParts.filter(\n      (node) => node.type !== \"WhiteSpace\"\n    );\n    const containsStop = filteredParts.some(isColorStop);\n\n    let handledDirective = false;\n    const fromIndex = filteredParts.findIndex((node) =>\n      isIdentifier(node, \"from\")\n    );\n    if (fromIndex !== -1) {\n      const angleNode = filteredParts[fromIndex + 1];\n      const mapped = mapLengthPercentageOrVar(angleNode);\n      if (\n        mapped !== undefined &&\n        ((mapped.type === \"unit\" && isAngleUnit(mapped.unit)) ||\n          mapped.type === \"var\")\n      ) {\n        angle = mapped;\n        handledDirective = true;\n      }\n    }\n\n    const atIndex = filteredParts.findIndex((node) => isIdentifier(node, \"at\"));\n    if (atIndex !== -1) {\n      const css = filteredParts\n        .slice(atIndex + 1)\n        .map((item) => csstree.generate(item))\n        .join(\" \")\n        .trim();\n      if (css.length > 0) {\n        position = css;\n        handledDirective = true;\n      }\n    }\n\n    if (handledDirective && containsStop === false) {\n      return;\n    }\n\n    const parsedPart = parseGradientPart(filteredParts);\n    if (parsedPart === undefined) {\n      return;\n    }\n    if (parsedPart.type === \"angle\") {\n      angle = parsedPart.value;\n    } else if (parsedPart.type === \"position\") {\n      position = parsedPart.value;\n    } else if (parsedPart.type === \"stop\") {\n      stops.push(parsedPart.value);\n    } else if (parsedPart.type === \"hint\") {\n      stops.push({ hint: parsedPart.value });\n    }\n  });\n\n  if (stops.length === 0) {\n    return;\n  }\n\n  const parsed: ParsedConicGradient = {\n    type: \"conic\",\n    angle,\n    position,\n    stops,\n  };\n  if (isRepeating) {\n    parsed.repeating = true;\n  }\n\n  return parsed;\n};\n\nexport const formatConicGradient = (parsed: ParsedConicGradient): string => {\n  const segments: string[] = [];\n  if (parsed.angle) {\n    segments.push(`from ${toValue(parsed.angle)}`);\n  }\n  if (parsed.position) {\n    segments.push(`at ${parsed.position}`);\n  }\n\n  const stops = formatGradientStops(parsed.stops);\n\n  const functionName =\n    parsed.repeating === true ? \"repeating-conic-gradient\" : \"conic-gradient\";\n  const prefix = segments.length > 0 ? `${segments.join(\" \")}, ` : \"\";\n\n  return `${functionName}(${prefix}${stops})`;\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/gradient-utils.ts",
    "content": "import * as csstree from \"css-tree\";\nimport { parseColor, parseCssValue } from \"../parse-css-value\";\nimport {\n  type UnitValue,\n  type Unit,\n  type VarValue,\n  toValue,\n} from \"@webstudio-is/css-engine\";\nimport type { GradientColorValue, GradientStop } from \"./types\";\n\nexport const angleUnitIdentifiers = [\"deg\", \"grad\", \"rad\", \"turn\"] as const;\nconst angleUnitSet = new Set<string>(angleUnitIdentifiers);\n\nexport const isAngleUnit = (unit: string): boolean => angleUnitSet.has(unit);\n\nexport const isAngleDimension = (\n  node: csstree.CssNode\n): node is csstree.Dimension =>\n  node.type === \"Dimension\" && isAngleUnit(node.unit);\n\nexport const isAngleLikeFallback = (\n  fallback: VarValue[\"fallback\"]\n): boolean => {\n  if (fallback === undefined) {\n    return false;\n  }\n\n  if (fallback.type === \"unit\") {\n    return isAngleUnit(fallback.unit);\n  }\n\n  if (fallback.type === \"keyword\") {\n    const normalized = fallback.value.trim().toLowerCase();\n    return normalized.startsWith(\"to \");\n  }\n\n  if (fallback.type === \"unparsed\") {\n    const normalized = fallback.value.trim().toLowerCase();\n    if (normalized.startsWith(\"to \")) {\n      return true;\n    }\n    return angleUnitIdentifiers.some((unit) => normalized.endsWith(unit));\n  }\n\n  return false;\n};\n\nexport const isVarAngle = (value: VarValue): boolean => {\n  if (isAngleLikeFallback(value.fallback)) {\n    return true;\n  }\n\n  const name = value.value.toLowerCase();\n  return (\n    name.includes(\"angle\") || name.includes(\"direction\") || name.includes(\"deg\")\n  );\n};\n\nexport const mapLengthPercentageOrVar = (\n  node?: csstree.CssNode\n): UnitValue | VarValue | undefined => {\n  if (node === undefined) {\n    return;\n  }\n\n  if (node.type === \"Percentage\" || node.type === \"Dimension\") {\n    return {\n      type: \"unit\",\n      value: Number.parseFloat(node.value),\n      unit: node.type === \"Percentage\" ? \"%\" : (node.unit as Unit),\n    } satisfies UnitValue;\n  }\n\n  if (node.type === \"Number\") {\n    return {\n      type: \"unit\",\n      value: Number.parseFloat(node.value),\n      unit: \"number\",\n    } satisfies UnitValue;\n  }\n\n  if (node.type === \"Function\" && node.name === \"var\") {\n    const css = csstree.generate(node).trim();\n    if (css.length === 0) {\n      return;\n    }\n    const parsed = parseCssValue(\"margin-left\", css);\n    if (parsed.type === \"var\") {\n      return parsed;\n    }\n    if (parsed.type === \"unit\") {\n      return parsed;\n    }\n  }\n};\n\nexport const getColor = (\n  node: csstree.CssNode\n): GradientColorValue | undefined => {\n  if (\n    node.type !== \"Function\" &&\n    node.type !== \"Identifier\" &&\n    node.type !== \"Hash\"\n  ) {\n    return;\n  }\n  const css = csstree.generate(node).trim();\n  if (css.length === 0) {\n    return;\n  }\n  const parsed = parseCssValue(\"color\", css);\n  if (\n    parsed.type === \"color\" ||\n    parsed.type === \"rgb\" ||\n    parsed.type === \"var\"\n  ) {\n    return parsed;\n  }\n  return parseColor(css);\n};\n\nexport const isColorStop = (node: csstree.CssNode): boolean =>\n  getColor(node) !== undefined;\n\nexport const formatGradientStops = (stops: GradientStop[]): string =>\n  stops\n    .map((stop) => {\n      let result = toValue(stop.color);\n      if (stop.position) {\n        result += ` ${toValue(stop.position)}`;\n      }\n      if (stop.hint) {\n        result += ` ${toValue(stop.hint)}`;\n      }\n      return result;\n    })\n    .join(\", \");\n\nexport const normalizeRepeatingGradient = (\n  gradient: string,\n  repeatingName: string,\n  baseName: string\n): {\n  normalized: string;\n  isRepeating: boolean;\n} => {\n  const pattern = new RegExp(`^(\\\\s*)${repeatingName}`, \"i\");\n  const normalized = gradient.replace(\n    pattern,\n    (_match, leadingWhitespace: string) => `${leadingWhitespace}${baseName}`\n  );\n  return {\n    normalized,\n    isRepeating: normalized !== gradient,\n  };\n};\n\nexport const forEachGradientParts = (\n  ast: csstree.CssNode,\n  functionName: string,\n  callback: (parts: csstree.CssNode[]) => void\n): void => {\n  csstree.walk(ast, (node) => {\n    if (node.type !== \"Function\" || node.name !== functionName) {\n      return;\n    }\n\n    let gradientParts: csstree.CssNode[] = [];\n    for (const item of node.children) {\n      if (item.type !== \"Operator\") {\n        gradientParts.push(item);\n      }\n\n      const isSeparator =\n        (item.type === \"Operator\" && item.value === \",\") ||\n        node.children.last === item;\n\n      if (isSeparator === false) {\n        continue;\n      }\n\n      callback(gradientParts);\n      gradientParts = [];\n    }\n  });\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/grid-template-areas.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { parseGridAreas } from \"./grid-template-areas\";\n\ndescribe(\"parseGridAreas\", () => {\n  test(\"parses empty or none value\", () => {\n    expect(parseGridAreas(\"\")).toEqual([]);\n    expect(parseGridAreas(\"none\")).toEqual([]);\n  });\n\n  test(\"parses single area\", () => {\n    const result = parseGridAreas('\"header\"');\n    expect(result).toEqual([\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 2,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ]);\n  });\n\n  test(\"parses area spanning multiple columns\", () => {\n    const result = parseGridAreas('\"header header header\"');\n    expect(result).toEqual([\n      {\n        name: \"header\",\n        columnStart: 1,\n        columnEnd: 4,\n        rowStart: 1,\n        rowEnd: 2,\n      },\n    ]);\n  });\n\n  test(\"parses multiple areas in one row\", () => {\n    const result = parseGridAreas('\"sidebar main\"');\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n  });\n\n  test(\"parses area spanning multiple rows\", () => {\n    const result = parseGridAreas('\"sidebar main\" \"sidebar footer\"');\n    expect(result).toHaveLength(3);\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 3,\n    });\n  });\n\n  test(\"parses complex layout with multiple rows and columns\", () => {\n    const result = parseGridAreas(\n      '\"header header\" \"sidebar main\" \"footer footer\"'\n    );\n    expect(result).toHaveLength(4);\n    expect(result).toContainEqual({\n      name: \"header\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"sidebar\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"footer\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 3,\n      rowEnd: 4,\n    });\n  });\n\n  test(\"ignores dots (empty cells)\", () => {\n    const result = parseGridAreas('\"header .\" \". main\"');\n    expect(result).toHaveLength(2);\n    expect(result).toContainEqual({\n      name: \"header\",\n      columnStart: 1,\n      columnEnd: 2,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"main\",\n      columnStart: 2,\n      columnEnd: 3,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n  });\n\n  test(\"returns empty array for invalid value\", () => {\n    expect(parseGridAreas(\"not-valid-areas\")).toEqual([]);\n  });\n\n  test(\"parses area spanning both rows and columns\", () => {\n    const result = parseGridAreas(\n      '\"logo logo nav\" \"logo logo content\" \"footer footer footer\"'\n    );\n    expect(result).toContainEqual({\n      name: \"logo\",\n      columnStart: 1,\n      columnEnd: 3,\n      rowStart: 1,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"nav\",\n      columnStart: 3,\n      columnEnd: 4,\n      rowStart: 1,\n      rowEnd: 2,\n    });\n    expect(result).toContainEqual({\n      name: \"content\",\n      columnStart: 3,\n      columnEnd: 4,\n      rowStart: 2,\n      rowEnd: 3,\n    });\n    expect(result).toContainEqual({\n      name: \"footer\",\n      columnStart: 1,\n      columnEnd: 4,\n      rowStart: 3,\n      rowEnd: 4,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/grid-template-areas.ts",
    "content": "import { cssTryParseValue } from \"../parse-css-value\";\n\nexport type AreaInfo = {\n  name: string;\n  columnStart: number;\n  columnEnd: number;\n  rowStart: number;\n  rowEnd: number;\n};\n\n/**\n * Parse grid-template-areas CSS string into structured area information.\n * Uses css-tree to parse the value into String AST nodes,\n * then extracts cell names from each row string.\n *\n * @example\n * parseGridAreas('\"header header\" \"sidebar main\"')\n * // Returns: [\n * //   { name: 'header', columnStart: 1, columnEnd: 3, rowStart: 1, rowEnd: 2 },\n * //   { name: 'sidebar', columnStart: 1, columnEnd: 2, rowStart: 2, rowEnd: 3 },\n * //   { name: 'main', columnStart: 2, columnEnd: 3, rowStart: 2, rowEnd: 3 }\n * // ]\n */\nexport const parseGridAreas = (value: string): AreaInfo[] => {\n  if (!value || value === \"none\") {\n    return [];\n  }\n\n  const ast = cssTryParseValue(value);\n  if (ast === undefined || ast.type !== \"Value\") {\n    return [];\n  }\n\n  const areaMap = new Map<string, AreaInfo>();\n  let rowIndex = 0;\n\n  for (const node of ast.children) {\n    // Each row in grid-template-areas is a quoted string → String node\n    if (node.type !== \"String\") {\n      continue;\n    }\n\n    const names = node.value.split(/\\s+/).filter(Boolean);\n    for (let colIndex = 0; colIndex < names.length; colIndex++) {\n      const name = names[colIndex];\n      if (name === \".\") {\n        continue;\n      }\n      const existing = areaMap.get(name);\n      if (existing === undefined) {\n        areaMap.set(name, {\n          name,\n          columnStart: colIndex + 1,\n          columnEnd: colIndex + 2,\n          rowStart: rowIndex + 1,\n          rowEnd: rowIndex + 2,\n        });\n      } else {\n        existing.columnEnd = Math.max(existing.columnEnd, colIndex + 2);\n        existing.rowEnd = Math.max(existing.rowEnd, rowIndex + 2);\n      }\n    }\n\n    rowIndex++;\n  }\n\n  return Array.from(areaMap.values());\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/grid-template-tracks.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  parseGridTemplateTrackList,\n  serializeGridTemplateTrackList,\n  parseMinmax,\n  serializeMinmax,\n  checkGridTemplateSupport,\n  getGridAxisMode,\n  isImplicitGridMode,\n  isEditableGridMode,\n  getGridAxisLabel,\n} from \"./grid-template-tracks\";\n\ndescribe(\"parseGridTemplateTrackList\", () => {\n  test(\"returns empty array for 'none'\", () => {\n    expect(parseGridTemplateTrackList(\"none\")).toEqual([]);\n  });\n\n  test(\"returns empty array for empty string\", () => {\n    expect(parseGridTemplateTrackList(\"\")).toEqual([]);\n  });\n\n  test(\"parses simple fr units\", () => {\n    expect(parseGridTemplateTrackList(\"1fr\")).toEqual([{ value: \"1fr\" }]);\n    expect(parseGridTemplateTrackList(\"1fr 2fr\")).toEqual([\n      { value: \"1fr\" },\n      { value: \"2fr\" },\n    ]);\n    expect(parseGridTemplateTrackList(\"1fr 2fr 3fr\")).toEqual([\n      { value: \"1fr\" },\n      { value: \"2fr\" },\n      { value: \"3fr\" },\n    ]);\n  });\n\n  test(\"parses pixel values\", () => {\n    expect(parseGridTemplateTrackList(\"100px\")).toEqual([{ value: \"100px\" }]);\n    expect(parseGridTemplateTrackList(\"100px 200px\")).toEqual([\n      { value: \"100px\" },\n      { value: \"200px\" },\n    ]);\n  });\n\n  test(\"parses percentage values\", () => {\n    expect(parseGridTemplateTrackList(\"50%\")).toEqual([{ value: \"50%\" }]);\n    expect(parseGridTemplateTrackList(\"25% 75%\")).toEqual([\n      { value: \"25%\" },\n      { value: \"75%\" },\n    ]);\n  });\n\n  test(\"parses mixed units\", () => {\n    expect(parseGridTemplateTrackList(\"100px 1fr 50%\")).toEqual([\n      { value: \"100px\" },\n      { value: \"1fr\" },\n      { value: \"50%\" },\n    ]);\n  });\n\n  test(\"parses auto keyword\", () => {\n    expect(parseGridTemplateTrackList(\"auto\")).toEqual([{ value: \"auto\" }]);\n    expect(parseGridTemplateTrackList(\"auto 1fr auto\")).toEqual([\n      { value: \"auto\" },\n      { value: \"1fr\" },\n      { value: \"auto\" },\n    ]);\n  });\n\n  test(\"parses min-content and max-content\", () => {\n    expect(parseGridTemplateTrackList(\"min-content\")).toEqual([\n      { value: \"min-content\" },\n    ]);\n    expect(parseGridTemplateTrackList(\"max-content\")).toEqual([\n      { value: \"max-content\" },\n    ]);\n    expect(parseGridTemplateTrackList(\"min-content 1fr max-content\")).toEqual([\n      { value: \"min-content\" },\n      { value: \"1fr\" },\n      { value: \"max-content\" },\n    ]);\n  });\n\n  test(\"parses minmax() function\", () => {\n    expect(parseGridTemplateTrackList(\"minmax(100px, 1fr)\")).toEqual([\n      { value: \"minmax(100px,1fr)\" },\n    ]);\n    expect(\n      parseGridTemplateTrackList(\"minmax(100px, 1fr) minmax(200px, 2fr)\")\n    ).toEqual([{ value: \"minmax(100px,1fr)\" }, { value: \"minmax(200px,2fr)\" }]);\n  });\n\n  test(\"parses fit-content() function\", () => {\n    expect(parseGridTemplateTrackList(\"fit-content(200px)\")).toEqual([\n      { value: \"fit-content(200px)\" },\n    ]);\n  });\n\n  test(\"expands repeat() with number\", () => {\n    expect(parseGridTemplateTrackList(\"repeat(3, 1fr)\")).toEqual([\n      { value: \"1fr\" },\n      { value: \"1fr\" },\n      { value: \"1fr\" },\n    ]);\n  });\n\n  test(\"expands repeat() with multiple tracks\", () => {\n    expect(parseGridTemplateTrackList(\"repeat(2, 100px 1fr)\")).toEqual([\n      { value: \"100px\" },\n      { value: \"1fr\" },\n      { value: \"100px\" },\n      { value: \"1fr\" },\n    ]);\n  });\n\n  test(\"keeps repeat(auto-fill) as single track\", () => {\n    const result = parseGridTemplateTrackList(\"repeat(auto-fill, 100px)\");\n    expect(result).toEqual([{ value: \"repeat(auto-fill,100px)\" }]);\n  });\n\n  test(\"keeps repeat(auto-fit) as single track\", () => {\n    const result = parseGridTemplateTrackList(\n      \"repeat(auto-fit, minmax(100px, 1fr))\"\n    );\n    expect(result).toEqual([{ value: \"repeat(auto-fit,minmax(100px,1fr))\" }]);\n  });\n\n  test(\"handles complex mixed values\", () => {\n    expect(\n      parseGridTemplateTrackList(\n        \"100px repeat(2, 1fr) minmax(100px, auto) 50px\"\n      )\n    ).toEqual([\n      { value: \"100px\" },\n      { value: \"1fr\" },\n      { value: \"1fr\" },\n      { value: \"minmax(100px,auto)\" },\n      { value: \"50px\" },\n    ]);\n  });\n\n  test(\"ignores line names\", () => {\n    expect(\n      parseGridTemplateTrackList(\"[header] 1fr [content] 2fr [footer]\")\n    ).toEqual([{ value: \"1fr\" }, { value: \"2fr\" }]);\n  });\n\n  test(\"ignores line names with repeat\", () => {\n    expect(parseGridTemplateTrackList(\"repeat(2, [col] 1fr)\")).toEqual([\n      { value: \"1fr\" },\n      { value: \"1fr\" },\n    ]);\n  });\n});\n\ndescribe(\"serializeGridTemplateTrackList\", () => {\n  test(\"returns 'none' for empty array\", () => {\n    expect(serializeGridTemplateTrackList([])).toBe(\"none\");\n  });\n\n  test(\"joins tracks with spaces\", () => {\n    expect(\n      serializeGridTemplateTrackList([{ value: \"1fr\" }, { value: \"2fr\" }])\n    ).toBe(\"1fr 2fr\");\n  });\n\n  test(\"handles single track\", () => {\n    expect(serializeGridTemplateTrackList([{ value: \"100px\" }])).toBe(\"100px\");\n  });\n\n  test(\"preserves complex values\", () => {\n    expect(\n      serializeGridTemplateTrackList([\n        { value: \"100px\" },\n        { value: \"minmax(100px,1fr)\" },\n        { value: \"auto\" },\n      ])\n    ).toBe(\"100px minmax(100px,1fr) auto\");\n  });\n});\n\ndescribe(\"round-trip parsing and serialization\", () => {\n  test(\"simple values round-trip correctly\", () => {\n    const original = \"1fr 2fr 3fr\";\n    const parsed = parseGridTemplateTrackList(original);\n    const serialized = serializeGridTemplateTrackList(parsed);\n    expect(serialized).toBe(original);\n  });\n\n  test(\"mixed values round-trip correctly\", () => {\n    const original = \"100px 1fr auto\";\n    const parsed = parseGridTemplateTrackList(original);\n    const serialized = serializeGridTemplateTrackList(parsed);\n    expect(serialized).toBe(original);\n  });\n});\n\ndescribe(\"parseMinmax\", () => {\n  test(\"parses simple minmax\", () => {\n    expect(parseMinmax(\"minmax(100px, 1fr)\")).toEqual({\n      min: \"100px\",\n      max: \"1fr\",\n    });\n  });\n\n  test(\"parses minmax with auto\", () => {\n    expect(parseMinmax(\"minmax(auto, 1fr)\")).toEqual({\n      min: \"auto\",\n      max: \"1fr\",\n    });\n  });\n\n  test(\"parses minmax with min-content/max-content\", () => {\n    expect(parseMinmax(\"minmax(min-content, max-content)\")).toEqual({\n      min: \"min-content\",\n      max: \"max-content\",\n    });\n  });\n\n  test(\"parses minmax with percentage\", () => {\n    expect(parseMinmax(\"minmax(10%, 50%)\")).toEqual({\n      min: \"10%\",\n      max: \"50%\",\n    });\n  });\n\n  test(\"returns undefined for non-minmax values\", () => {\n    expect(parseMinmax(\"1fr\")).toBeUndefined();\n    expect(parseMinmax(\"100px\")).toBeUndefined();\n    expect(parseMinmax(\"auto\")).toBeUndefined();\n  });\n\n  test(\"returns undefined for other functions\", () => {\n    expect(parseMinmax(\"fit-content(200px)\")).toBeUndefined();\n    expect(parseMinmax(\"repeat(3, 1fr)\")).toBeUndefined();\n  });\n\n  test(\"returns undefined for invalid values\", () => {\n    expect(parseMinmax(\"\")).toBeUndefined();\n    expect(parseMinmax(\"minmax()\")).toBeUndefined();\n    expect(parseMinmax(\"minmax(100px)\")).toBeUndefined();\n  });\n});\n\ndescribe(\"serializeMinmax\", () => {\n  test(\"creates minmax string\", () => {\n    expect(serializeMinmax({ min: \"100px\", max: \"1fr\" })).toBe(\n      \"minmax(100px,1fr)\"\n    );\n  });\n\n  test(\"handles auto values\", () => {\n    expect(serializeMinmax({ min: \"auto\", max: \"1fr\" })).toBe(\n      \"minmax(auto,1fr)\"\n    );\n  });\n\n  test(\"handles content values\", () => {\n    expect(serializeMinmax({ min: \"min-content\", max: \"max-content\" })).toBe(\n      \"minmax(min-content,max-content)\"\n    );\n  });\n});\n\ndescribe(\"parseMinmax and serializeMinmax round-trip\", () => {\n  test(\"minmax round-trips correctly\", () => {\n    const original = \"minmax(100px,1fr)\";\n    const parsed = parseMinmax(original);\n    expect(parsed).not.toBeUndefined();\n    const serialized = serializeMinmax(parsed!);\n    expect(serialized).toBe(original);\n  });\n});\n\ndescribe(\"checkGridTemplateSupport\", () => {\n  test(\"supports empty and none values\", () => {\n    expect(checkGridTemplateSupport(\"\")).toEqual({ supported: true });\n    expect(checkGridTemplateSupport(\"none\")).toEqual({ supported: true });\n  });\n\n  test(\"supports simple track values\", () => {\n    expect(checkGridTemplateSupport(\"1fr\")).toEqual({ supported: true });\n    expect(checkGridTemplateSupport(\"100px 1fr auto\")).toEqual({\n      supported: true,\n    });\n  });\n\n  test(\"supports minmax()\", () => {\n    expect(checkGridTemplateSupport(\"minmax(100px, 1fr)\")).toEqual({\n      supported: true,\n    });\n  });\n\n  test(\"supports repeat() with number\", () => {\n    expect(checkGridTemplateSupport(\"repeat(3, 1fr)\")).toEqual({\n      supported: true,\n    });\n  });\n\n  test(\"rejects subgrid\", () => {\n    const result = checkGridTemplateSupport(\"subgrid\");\n    expect(result.supported).toBe(false);\n    expect(result).toHaveProperty(\"reason\");\n    expect(result).toHaveProperty(\"type\", \"subgrid\");\n  });\n\n  test(\"rejects masonry\", () => {\n    const result = checkGridTemplateSupport(\"masonry\");\n    expect(result.supported).toBe(false);\n    expect(result).toHaveProperty(\"reason\");\n    expect(result).toHaveProperty(\"type\", \"masonry\");\n  });\n\n  test(\"supports CSS variables (should be resolved via computedValue)\", () => {\n    // CSS variables are supported when using computedValue which resolves them\n    const result = checkGridTemplateSupport(\"var(--grid-cols)\");\n    expect(result.supported).toBe(true);\n  });\n\n  test(\"rejects repeat(auto-fill)\", () => {\n    const result = checkGridTemplateSupport(\"repeat(auto-fill, 100px)\");\n    expect(result.supported).toBe(false);\n    expect(result).toHaveProperty(\"reason\");\n    expect(result).toHaveProperty(\"type\", \"auto-fill\");\n  });\n\n  test(\"rejects repeat(auto-fit)\", () => {\n    const result = checkGridTemplateSupport(\n      \"repeat(auto-fit, minmax(100px, 1fr))\"\n    );\n    expect(result.supported).toBe(false);\n    expect(result).toHaveProperty(\"reason\");\n    expect(result).toHaveProperty(\"type\", \"auto-fit\");\n  });\n\n  test(\"rejects line names\", () => {\n    const result = checkGridTemplateSupport(\"[header] 1fr [content] 2fr\");\n    expect(result.supported).toBe(false);\n    expect(result).toHaveProperty(\"reason\");\n    expect(result).toHaveProperty(\"type\", \"line-names\");\n  });\n\n  test(\"rejects mixed auto-fill with other tracks\", () => {\n    const result = checkGridTemplateSupport(\n      \"100px repeat(auto-fill, 1fr) 100px\"\n    );\n    expect(result.supported).toBe(false);\n    expect(result).toHaveProperty(\"reason\");\n    expect(result).toHaveProperty(\"type\", \"auto-fill\");\n  });\n});\n\ndescribe(\"getGridAxisMode\", () => {\n  test(\"returns 'none' for empty or none values\", () => {\n    expect(getGridAxisMode(\"\")).toBe(\"none\");\n    expect(getGridAxisMode(\"none\")).toBe(\"none\");\n  });\n\n  test(\"returns 'auto' for single auto keyword\", () => {\n    expect(getGridAxisMode(\"auto\")).toBe(\"auto\");\n  });\n\n  test(\"returns 'explicit' for regular track values\", () => {\n    expect(getGridAxisMode(\"1fr 2fr\")).toBe(\"explicit\");\n    expect(getGridAxisMode(\"100px 200px\")).toBe(\"explicit\");\n    expect(getGridAxisMode(\"repeat(3, 1fr)\")).toBe(\"explicit\");\n    expect(getGridAxisMode(\"minmax(100px, 1fr)\")).toBe(\"explicit\");\n  });\n\n  test(\"returns 'auto-fill' for repeat(auto-fill, ...)\", () => {\n    expect(getGridAxisMode(\"repeat(auto-fill, 100px)\")).toBe(\"auto-fill\");\n    expect(getGridAxisMode(\"repeat(auto-fill, minmax(100px, 1fr))\")).toBe(\n      \"auto-fill\"\n    );\n  });\n\n  test(\"returns 'auto-fit' for repeat(auto-fit, ...)\", () => {\n    expect(getGridAxisMode(\"repeat(auto-fit, 100px)\")).toBe(\"auto-fit\");\n    expect(getGridAxisMode(\"repeat(auto-fit, minmax(100px, 1fr))\")).toBe(\n      \"auto-fit\"\n    );\n  });\n\n  test(\"returns 'subgrid' for subgrid\", () => {\n    expect(getGridAxisMode(\"subgrid\")).toBe(\"subgrid\");\n    expect(getGridAxisMode(\"subgrid [header] [content]\")).toBe(\"subgrid\");\n  });\n\n  test(\"returns 'masonry' for masonry\", () => {\n    expect(getGridAxisMode(\"masonry\")).toBe(\"masonry\");\n  });\n\n  test(\"returns 'line-names' for values with line names\", () => {\n    expect(getGridAxisMode(\"[header] 1fr [content]\")).toBe(\"line-names\");\n  });\n});\n\ndescribe(\"isImplicitGridMode\", () => {\n  test(\"returns true for implicit modes\", () => {\n    expect(isImplicitGridMode(\"auto-fill\")).toBe(true);\n    expect(isImplicitGridMode(\"auto-fit\")).toBe(true);\n    expect(isImplicitGridMode(\"auto\")).toBe(true);\n    expect(isImplicitGridMode(\"none\")).toBe(true);\n  });\n\n  test(\"returns false for explicit modes\", () => {\n    expect(isImplicitGridMode(\"explicit\")).toBe(false);\n    expect(isImplicitGridMode(\"subgrid\")).toBe(false);\n    expect(isImplicitGridMode(\"masonry\")).toBe(false);\n    expect(isImplicitGridMode(\"line-names\")).toBe(false);\n  });\n});\n\ndescribe(\"isEditableGridMode\", () => {\n  test(\"returns true for editable modes\", () => {\n    expect(isEditableGridMode(\"explicit\")).toBe(true);\n    expect(isEditableGridMode(\"none\")).toBe(true);\n    expect(isEditableGridMode(\"auto\")).toBe(true);\n  });\n\n  test(\"returns false for non-editable modes\", () => {\n    expect(isEditableGridMode(\"auto-fill\")).toBe(false);\n    expect(isEditableGridMode(\"auto-fit\")).toBe(false);\n    expect(isEditableGridMode(\"subgrid\")).toBe(false);\n    expect(isEditableGridMode(\"masonry\")).toBe(false);\n    expect(isEditableGridMode(\"line-names\")).toBe(false);\n  });\n});\n\ndescribe(\"getGridAxisLabel\", () => {\n  test(\"returns mode name for implicit modes\", () => {\n    expect(getGridAxisLabel(\"auto-fill\", 5)).toBe(\"auto-fill\");\n    expect(getGridAxisLabel(\"auto-fit\", 3)).toBe(\"auto-fit\");\n    expect(getGridAxisLabel(\"auto\", 2)).toBe(\"auto\");\n    expect(getGridAxisLabel(\"none\", 2)).toBe(\"none\");\n  });\n\n  test(\"returns track count for explicit mode\", () => {\n    expect(getGridAxisLabel(\"explicit\", 3)).toBe(\"3\");\n    expect(getGridAxisLabel(\"explicit\", 5)).toBe(\"5\");\n  });\n\n  test(\"returns mode name for advanced modes\", () => {\n    expect(getGridAxisLabel(\"subgrid\", 4)).toBe(\"subgrid\");\n    expect(getGridAxisLabel(\"masonry\", 3)).toBe(\"masonry\");\n    expect(getGridAxisLabel(\"line-names\", 2)).toBe(\"[…]\");\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/grid-template-tracks.ts",
    "content": "import * as csstree from \"css-tree\";\nimport { cssTryParseValue } from \"../parse-css-value\";\n\n/**\n * Represents a single grid track.\n * Can be a simple value like \"1fr\", \"100px\", \"auto\"\n * or a complex value like \"minmax(100px, 1fr)\"\n */\nexport type GridTrack = {\n  /** The raw CSS value of the track */\n  value: string;\n};\n\n/**\n * Parse a grid-template-columns or grid-template-rows value into an array of tracks.\n *\n * Handles:\n * - Simple values: \"1fr\", \"100px\", \"auto\", \"min-content\", \"max-content\"\n * - Functions: \"minmax(100px, 1fr)\", \"fit-content(200px)\"\n * - Repeat: \"repeat(3, 1fr)\" -> expands to [\"1fr\", \"1fr\", \"1fr\"]\n * - Line names: \"[header] 1fr [content] 2fr\" -> ignores line names, returns [\"1fr\", \"2fr\"]\n *\n * @param value - The CSS value string for grid-template-columns/rows\n * @returns Array of track values, or empty array if value is \"none\" or invalid\n */\nexport const parseGridTemplateTrackList = (value: string): GridTrack[] => {\n  if (!value || value === \"none\") {\n    return [];\n  }\n\n  const ast = cssTryParseValue(value);\n  if (ast === undefined || ast.type !== \"Value\") {\n    return [];\n  }\n\n  const tracks: GridTrack[] = [];\n  const children = ast.children.toArray();\n\n  for (const node of children) {\n    // Skip line names (bracketed identifiers like [header])\n    if (node.type === \"Brackets\") {\n      continue;\n    }\n\n    // Handle simple identifiers: auto, min-content, max-content\n    if (node.type === \"Identifier\") {\n      tracks.push({ value: node.name });\n      continue;\n    }\n\n    // Handle dimensions: 100px, 1fr, 50%\n    if (node.type === \"Dimension\") {\n      tracks.push({ value: `${node.value}${node.unit}` });\n      continue;\n    }\n\n    // Handle percentages\n    if (node.type === \"Percentage\") {\n      tracks.push({ value: `${node.value}%` });\n      continue;\n    }\n\n    // Handle numbers (rare but valid)\n    if (node.type === \"Number\") {\n      tracks.push({ value: node.value });\n      continue;\n    }\n\n    // Handle functions: minmax(), fit-content(), repeat()\n    if (node.type === \"Function\") {\n      if (node.name === \"repeat\") {\n        // Expand repeat() function\n        const repeatTracks = expandRepeat(node);\n        tracks.push(...repeatTracks);\n      } else {\n        // For other functions (minmax, fit-content), keep as-is\n        tracks.push({ value: csstree.generate(node) });\n      }\n      continue;\n    }\n  }\n\n  return tracks;\n};\n\n/**\n * Expand a repeat() function into individual tracks.\n * e.g., repeat(3, 1fr) -> [{ value: \"1fr\" }, { value: \"1fr\" }, { value: \"1fr\" }]\n * e.g., repeat(2, 100px 1fr) -> [{ value: \"100px\" }, { value: \"1fr\" }, { value: \"100px\" }, { value: \"1fr\" }]\n *\n * Note: repeat(auto-fill, ...) and repeat(auto-fit, ...) cannot be expanded\n * and will return the original function as a single track.\n */\nconst expandRepeat = (node: csstree.FunctionNode): GridTrack[] => {\n  const children = node.children.toArray();\n\n  // First child should be the count (number or auto-fill/auto-fit)\n  const countNode = children[0];\n\n  // Handle auto-fill and auto-fit - cannot expand, return as single track\n  if (countNode?.type === \"Identifier\") {\n    if (countNode.name === \"auto-fill\" || countNode.name === \"auto-fit\") {\n      return [{ value: csstree.generate(node) }];\n    }\n  }\n\n  // Get the repeat count\n  let count = 1;\n  if (countNode?.type === \"Number\") {\n    count = parseInt(countNode.value, 10);\n    if (isNaN(count) || count < 1) {\n      return [{ value: csstree.generate(node) }];\n    }\n  }\n\n  // Find the comma separator and get track template after it\n  const trackTemplateNodes: csstree.CssNode[] = [];\n  let foundComma = false;\n  for (const child of children) {\n    if (child.type === \"Operator\" && child.value === \",\") {\n      foundComma = true;\n      continue;\n    }\n    if (foundComma) {\n      trackTemplateNodes.push(child);\n    }\n  }\n\n  if (trackTemplateNodes.length === 0) {\n    return [{ value: csstree.generate(node) }];\n  }\n\n  // Parse the track template\n  const templateTracks: GridTrack[] = [];\n  for (const templateNode of trackTemplateNodes) {\n    // Skip line names\n    if (templateNode.type === \"Brackets\") {\n      continue;\n    }\n\n    if (templateNode.type === \"Identifier\") {\n      templateTracks.push({ value: templateNode.name });\n    } else if (templateNode.type === \"Dimension\") {\n      templateTracks.push({\n        value: `${templateNode.value}${templateNode.unit}`,\n      });\n    } else if (templateNode.type === \"Percentage\") {\n      templateTracks.push({ value: `${templateNode.value}%` });\n    } else if (templateNode.type === \"Number\") {\n      templateTracks.push({ value: templateNode.value });\n    } else if (templateNode.type === \"Function\") {\n      templateTracks.push({ value: csstree.generate(templateNode) });\n    }\n  }\n\n  // Expand the template by the count\n  const expandedTracks: GridTrack[] = [];\n  for (let i = 0; i < count; i++) {\n    expandedTracks.push(...templateTracks);\n  }\n\n  return expandedTracks;\n};\n\n/**\n * Serialize an array of tracks back to a CSS value string.\n *\n * @param tracks - Array of track values\n * @returns CSS string like \"1fr 100px auto\" or \"none\" if empty\n */\nexport const serializeGridTemplateTrackList = (tracks: GridTrack[]): string => {\n  if (tracks.length === 0) {\n    return \"none\";\n  }\n  return tracks.map((track) => track.value).join(\" \");\n};\n\n/**\n * Represents a minmax() function with min and max values.\n */\nexport type Minmax = {\n  min: string;\n  max: string;\n};\n\n/**\n * Parse a minmax() function value into its min and max parts.\n * Returns undefined if the value is not a valid minmax() function.\n *\n * @example\n * parseMinmax(\"minmax(100px, 1fr)\") // { min: \"100px\", max: \"1fr\" }\n * parseMinmax(\"1fr\") // undefined\n */\nexport const parseMinmax = (value: string): Minmax | undefined => {\n  const ast = cssTryParseValue(value);\n  if (ast === undefined || ast.type !== \"Value\") {\n    return undefined;\n  }\n\n  const children = ast.children.toArray();\n  if (children.length !== 1) {\n    return undefined;\n  }\n\n  const node = children[0];\n  if (node.type !== \"Function\" || node.name !== \"minmax\") {\n    return undefined;\n  }\n\n  const args = node.children.toArray();\n  // minmax has two arguments separated by a comma\n  // Structure: [min-value, Operator(\",\"), max-value]\n  const values: string[] = [];\n  for (const arg of args) {\n    if (arg.type === \"Operator\" && arg.value === \",\") {\n      continue;\n    }\n    values.push(csstree.generate(arg));\n  }\n\n  if (values.length !== 2) {\n    return undefined;\n  }\n\n  return { min: values[0], max: values[1] };\n};\n\n/**\n * Create a minmax() CSS value from min and max parts.\n *\n * @example\n * serializeMinmax({ min: \"100px\", max: \"1fr\" }) // \"minmax(100px,1fr)\"\n */\nexport const serializeMinmax = (minmax: Minmax): string => {\n  return `minmax(${minmax.min},${minmax.max})`;\n};\n\n/**\n * Result of checking if a grid template value is supported by the visual editor.\n */\nexport type GridTemplateSupport =\n  | { supported: true }\n  | {\n      supported: false;\n      reason: string;\n      type: \"subgrid\" | \"masonry\" | \"line-names\" | \"auto-fill\" | \"auto-fit\";\n    };\n\n/**\n * Check if a grid-template-columns or grid-template-rows value can be\n * represented and edited by the visual grid UI.\n *\n * Unsupported values:\n * - `subgrid` - requires dedicated UI\n * - `masonry` - experimental, not a track definition\n * - `repeat(auto-fill, ...)` / `repeat(auto-fit, ...)` - dynamic track count\n * - Line names like `[header]` - would be silently discarded on edit\n *\n * Note: CSS variables are supported - use computedValue which resolves them.\n *\n * @param value - The CSS value string for grid-template-columns/rows\n * @returns Object indicating if supported, with reason and type if not\n */\nexport const checkGridTemplateSupport = (\n  value: string\n): GridTemplateSupport => {\n  if (!value || value === \"none\") {\n    return { supported: true };\n  }\n\n  // Check for subgrid\n  if (value.includes(\"subgrid\")) {\n    return {\n      supported: false,\n      type: \"subgrid\",\n      reason: \"Subgrid is not supported in the visual editor\",\n    };\n  }\n\n  // Check for masonry\n  if (value.includes(\"masonry\")) {\n    return {\n      supported: false,\n      type: \"masonry\",\n      reason: \"Masonry layout is not supported in the visual editor\",\n    };\n  }\n\n  const ast = cssTryParseValue(value);\n  if (ast === undefined || ast.type !== \"Value\") {\n    return { supported: true }; // Let invalid values pass through\n  }\n\n  const children = ast.children.toArray();\n\n  for (const node of children) {\n    // Check for line names (bracketed identifiers)\n    if (node.type === \"Brackets\") {\n      return {\n        supported: false,\n        type: \"line-names\",\n        reason: \"Named grid lines are not supported in the visual editor\",\n      };\n    }\n\n    // Check for auto-fill/auto-fit in repeat()\n    if (node.type === \"Function\" && node.name === \"repeat\") {\n      const repeatChildren = node.children.toArray();\n      const countNode = repeatChildren[0];\n      if (countNode?.type === \"Identifier\") {\n        if (countNode.name === \"auto-fill\") {\n          return {\n            supported: false,\n            type: \"auto-fill\",\n            reason: \"Dynamic grid tracks (auto-fill) cannot be edited visually\",\n          };\n        }\n        if (countNode.name === \"auto-fit\") {\n          return {\n            supported: false,\n            type: \"auto-fit\",\n            reason: \"Dynamic grid tracks (auto-fit) cannot be edited visually\",\n          };\n        }\n      }\n    }\n  }\n\n  return { supported: true };\n};\n\n/**\n * Describes the mode of a grid axis (columns or rows).\n *\n * - \"explicit\": Regular explicit tracks (e.g., \"1fr 2fr 100px\")\n * - \"auto-fill\": Dynamic tracks via repeat(auto-fill, ...)\n * - \"auto-fit\": Dynamic tracks via repeat(auto-fit, ...)\n * - \"auto\": Single \"auto\" keyword - implicit tracks based on children\n * - \"none\": No explicit tracks defined\n * - \"subgrid\": Subgrid inherits tracks from parent\n * - \"masonry\": Experimental masonry layout\n * - \"line-names\": Has named grid lines like [header]\n */\nexport type GridAxisMode =\n  | \"explicit\"\n  | \"auto-fill\"\n  | \"auto-fit\"\n  | \"auto\"\n  | \"none\"\n  | \"subgrid\"\n  | \"masonry\"\n  | \"line-names\";\n\n/**\n * Analyze a grid template value to determine its mode.\n *\n * @param value - The CSS value string for grid-template-columns/rows\n * @returns The axis mode describing the type of grid track definition\n *\n * @example\n * getGridAxisMode(\"1fr 2fr 100px\") // \"explicit\"\n * getGridAxisMode(\"repeat(auto-fill, minmax(200px, 1fr))\") // \"auto-fill\"\n * getGridAxisMode(\"none\") // \"none\"\n * getGridAxisMode(\"auto\") // \"auto\"\n * getGridAxisMode(\"subgrid\") // \"subgrid\"\n */\nexport const getGridAxisMode = (value: string): GridAxisMode => {\n  if (!value || value === \"none\" || value === \"\") {\n    return \"none\";\n  }\n\n  if (value === \"auto\") {\n    return \"auto\";\n  }\n\n  if (value.includes(\"subgrid\")) {\n    return \"subgrid\";\n  }\n\n  if (value.includes(\"masonry\")) {\n    return \"masonry\";\n  }\n\n  const support = checkGridTemplateSupport(value);\n  if (!support.supported) {\n    // Map the unsupported type directly to mode\n    return support.type;\n  }\n\n  return \"explicit\";\n};\n\n/**\n * Check if a grid axis mode represents a dynamic/implicit grid that\n * requires DOM probing to determine actual track count.\n *\n * @param mode - The grid axis mode\n * @returns true if the mode requires DOM-based track counting\n */\nexport const isImplicitGridMode = (mode: GridAxisMode): boolean => {\n  return (\n    mode === \"auto-fill\" ||\n    mode === \"auto-fit\" ||\n    mode === \"auto\" ||\n    mode === \"none\"\n  );\n};\n\n/**\n * Check if a grid axis mode is editable in the visual grid UI.\n *\n * @param mode - The grid axis mode\n * @returns true if the mode can be edited visually\n */\nexport const isEditableGridMode = (mode: GridAxisMode): boolean => {\n  return mode === \"explicit\" || mode === \"none\" || mode === \"auto\";\n};\n\n/**\n * Get a display label for a grid axis mode (for the grid generator).\n *\n * @param mode - The grid axis mode\n * @param trackCount - The actual track count (for explicit modes)\n * @returns Display string like \"3\", \"auto-fit\", \"none\"\n */\nexport const getGridAxisLabel = (\n  mode: GridAxisMode,\n  trackCount: number\n): string => {\n  switch (mode) {\n    case \"auto-fill\":\n      return \"auto-fill\";\n    case \"auto-fit\":\n      return \"auto-fit\";\n    case \"auto\":\n      return \"auto\";\n    case \"none\":\n      return \"none\";\n    case \"subgrid\":\n      return \"subgrid\";\n    case \"masonry\":\n      return \"masonry\";\n    case \"line-names\":\n      return \"[…]\";\n    default:\n      return String(trackCount);\n  }\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/index.ts",
    "content": "export * from \"./linear-gradient\";\nexport * from \"./conic-gradient\";\nexport * from \"./radial-gradient\";\nexport * from \"./grid-template-tracks\";\nexport * from \"./grid-template-areas\";\nexport * from \"./types\";\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/linear-gradient.test.ts",
    "content": "import { test, describe, expect } from \"vitest\";\nimport { parseLinearGradient, formatLinearGradient } from \"./linear-gradient\";\n\ntype LinearGradient = NonNullable<ReturnType<typeof parseLinearGradient>>;\nconst linear = (gradient: Omit<LinearGradient, \"type\">): LinearGradient => ({\n  type: \"linear\",\n  ...gradient,\n});\n\ndescribe(\"parses linear-gradient\", () => {\n  test(\"parses gradient without angle, sides and color-stops\", () => {\n    expect(parseLinearGradient(\"linear-gradient(red, blue, yellow)\")).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with angle and color-stops\", () => {\n    expect(\n      parseLinearGradient(\"linear-gradient(135deg, orange 60% 20%, 40%, cyan)\")\n    ).toEqual(\n      linear({\n        angle: { type: \"unit\", unit: \"deg\", value: 135 },\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.6471, 0],\n              alpha: 1,\n            },\n            hint: { type: \"unit\", unit: \"%\", value: 20 },\n            position: { type: \"unit\", unit: \"%\", value: 60 },\n          },\n          { hint: { type: \"unit\", unit: \"%\", value: 40 } },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 1, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with side-or-corer and multiple colors without stops\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(to top right, orange, yellow, blue, green)\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: { type: \"keyword\", value: \"to top right\" },\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.6471, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0.502, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with multiple angles and color-stops\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(to right, red 20%, orange 20% 40%, yellow 40% 60%, green 60% 80%, blue 80% )\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: { type: \"keyword\", value: \"to right\" },\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"unit\", unit: \"%\", value: 20 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.6471, 0],\n              alpha: 1,\n            },\n            hint: { type: \"unit\", unit: \"%\", value: 40 },\n            position: { type: \"unit\", unit: \"%\", value: 20 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            hint: { type: \"unit\", unit: \"%\", value: 60 },\n            position: { type: \"unit\", unit: \"%\", value: 40 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0.502, 0],\n              alpha: 1,\n            },\n            hint: { type: \"unit\", unit: \"%\", value: 80 },\n            position: { type: \"unit\", unit: \"%\", value: 60 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"unit\", unit: \"%\", value: 80 },\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with rgb values\", () => {\n    const parsed = parseLinearGradient(\n      \"linear-gradient(rgb(255, 0, 0), rgb(0, 255, 0), rgb(0, 0, 255))\"\n    );\n    if (parsed === undefined) {\n      throw new Error(\"parsed is undefined\");\n    }\n\n    expect(formatLinearGradient(parsed)).toEqual(\n      \"linear-gradient(rgb(255 0 0 / 1), rgb(0 255 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"parses repeating-linear-gradient\", () => {\n    const parsed = parseLinearGradient(\"repeating-linear-gradient(red, blue)\");\n    if (parsed === undefined) {\n      throw new Error(\"parsed is undefined\");\n    }\n\n    expect(parsed.repeating).toBe(true);\n    expect(formatLinearGradient(parsed)).toEqual(\n      \"repeating-linear-gradient(rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"parses linear-gradient with css variables\", () => {\n    expect(\n      parseLinearGradient(\"linear-gradient(var(--brand-color), blue)\")\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"var\",\n              value: \"brand-color\",\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with css variables and fallbacks\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(var(--heading-color, #ff0000) 25% 50%, yellow)\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"var\",\n              value: \"heading-color\",\n              fallback: { type: \"unparsed\", value: \"#ff0000\" },\n            },\n            position: { type: \"unit\", unit: \"%\", value: 25 },\n            hint: { type: \"unit\", unit: \"%\", value: 50 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with keyword and variable mix\", () => {\n    const parsed = parseLinearGradient(\n      \"linear-gradient(to bottom, green 0%, var(--accent))\"\n    );\n    expect(parsed).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: { type: \"keyword\", value: \"to bottom\" },\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0.502, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n          },\n          {\n            color: { type: \"var\", value: \"accent\" },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n    if (parsed) {\n      expect(formatLinearGradient(parsed)).toEqual(\n        \"linear-gradient(to bottom, rgb(0 128 0 / 1) 0%, var(--accent))\"\n      );\n    }\n  });\n\n  test(\"parses linear-gradient with css variable positions\", () => {\n    const parsed = parseLinearGradient(\n      \"linear-gradient(rgba(255, 0, 0, 1) var(--start, 10%), blue var(--end))\"\n    );\n    expect(parsed).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: {\n              type: \"var\",\n              value: \"start\",\n              fallback: { type: \"unparsed\", value: \"10%\" },\n            },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"var\", value: \"end\" },\n          },\n        ],\n      })\n    );\n    if (parsed) {\n      expect(formatLinearGradient(parsed)).toEqual(\n        \"linear-gradient(rgb(255 0 0 / 1) var(--start, 10%), rgb(0 0 255 / 1) var(--end))\"\n      );\n    }\n  });\n\n  test(\"parses linear-gradient with css variable hints\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(red 0% var(--hint-position, 20%), blue)\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: {\n              type: \"var\",\n              value: \"hint-position\",\n              fallback: { type: \"unparsed\", value: \"20%\" },\n            },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with variable colors, positions, and hints\", () => {\n    const gradient =\n      \"linear-gradient(var(--primary, red) var(--start, 5%) var(--hint-start, 10%), var(--secondary) var(--end) var(--hint-end))\";\n    const parsed = parseLinearGradient(gradient);\n    expect(parsed).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"var\",\n              value: \"primary\",\n              fallback: { type: \"unparsed\", value: \"red\" },\n            },\n            position: {\n              type: \"var\",\n              value: \"start\",\n              fallback: { type: \"unparsed\", value: \"5%\" },\n            },\n            hint: {\n              type: \"var\",\n              value: \"hint-start\",\n              fallback: { type: \"unparsed\", value: \"10%\" },\n            },\n          },\n          {\n            color: { type: \"var\", value: \"secondary\" },\n            position: { type: \"var\", value: \"end\" },\n            hint: { type: \"var\", value: \"hint-end\" },\n          },\n        ],\n      })\n    );\n    if (parsed) {\n      expect(formatLinearGradient(parsed)).toEqual(gradient);\n    }\n  });\n\n  test(\"returns undefined for invalid gradient input\", () => {\n    expect(parseLinearGradient(\"linear-gradient(var())\")).toBeUndefined();\n    expect(parseLinearGradient(\"linear-gradient(, , ,)\")).toBeUndefined();\n  });\n\n  test(\"parses linear-gradient with variable angle\", () => {\n    const parsed = parseLinearGradient(\n      \"linear-gradient(var(--angle, 45deg), red, blue)\"\n    );\n    if (parsed === undefined) {\n      throw new Error(\"parsed is undefined\");\n    }\n\n    expect(parsed.angle).toMatchObject({\n      type: \"var\",\n      value: \"angle\",\n    });\n    expect(formatLinearGradient(parsed)).toEqual(\n      \"linear-gradient(var(--angle, 45deg), rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n\n  test(\"parses linear-gradient with radian angle\", () => {\n    expect(parseLinearGradient(\"linear-gradient(0.5rad, red, blue)\")).toEqual(\n      linear({\n        angle: { type: \"unit\", unit: \"rad\", value: 0.5 },\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with oklch colors\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(oklch(0.6 0.1 180), oklch(0.8 0.15 240))\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"oklch\",\n              components: [0.6, 0.1, 180],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"oklch\",\n              components: [0.8, 0.15, 240],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with hsl colors\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(hsl(180 100% 50%), hsl(240 100% 50%))\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"hsl\",\n              components: [180, 100, 50],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"hsl\",\n              components: [240, 100, 50],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with color() function\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(color(srgb 1 0 0), color(srgb 0 0 1))\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with mixed modern color spaces\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(90deg, oklch(0.7 0.2 30) 0%, hsl(120deg 50% 60%) 50%, color(display-p3 0 1 0) 100%)\"\n      )\n    ).toEqual(\n      linear({\n        angle: { type: \"unit\", unit: \"deg\", value: 90 },\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"oklch\",\n              components: [0.7, 0.2, 30],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"hsl\",\n              components: [120, 50, 60],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"unit\", unit: \"%\", value: 50 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"p3\",\n              components: [0, 1, 0],\n              alpha: 1,\n            },\n            hint: undefined,\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses linear-gradient with oklch colors with alpha\", () => {\n    expect(\n      parseLinearGradient(\n        \"linear-gradient(oklch(0.6 0.1 180 / 0.5), oklch(0.8 0.15 240 / 0.8))\"\n      )\n    ).toEqual(\n      linear({\n        angle: undefined,\n        sideOrCorner: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"oklch\",\n              components: [0.6, 0.1, 180],\n              alpha: 0.5,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"oklch\",\n              components: [0.8, 0.15, 240],\n              alpha: 0.8,\n            },\n            hint: undefined,\n            position: undefined,\n          },\n        ],\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/linear-gradient.ts",
    "content": "import * as csstree from \"css-tree\";\nimport { cssTryParseValue } from \"../parse-css-value\";\nimport {\n  type UnitValue,\n  toValue,\n  KeywordValue,\n  type VarValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  getColor,\n  isAngleDimension,\n  isAngleUnit,\n  isColorStop,\n  isVarAngle,\n  mapLengthPercentageOrVar,\n  formatGradientStops,\n  normalizeRepeatingGradient,\n  forEachGradientParts,\n} from \"./gradient-utils\";\n\nexport type {\n  GradientColorValue,\n  GradientStop,\n  ParsedGradient,\n  ParsedLinearGradient,\n  ParsedConicGradient,\n} from \"./types\";\n\nimport type { GradientStop, ParsedLinearGradient } from \"./types\";\n\nconst sideOrCorderIdentifiers = [\"to\", \"top\", \"bottom\", \"left\", \"right\"];\n\n// We are currently not supporting color-interpolation-method from the linear-gradient syntax.\n// There are multiple reasons to not support it:\n// - mdn-data does not have any information about it. There is a PR that is opened about it.\n//   https://github.com/mdn/data/pull/766 which needs to be released first.\n// - we can't use css-tree parser directly and by-pass using css-tree lexer.match.But there are again multiple issues\n//   css-tree package don't have information about <color-interpolation-method> for css-tree@2.3.1.\n//   They only added it after css-tree@3.0.0 which has breaking changes for us to upgrade directly.\n// - patching the css-tree package is a solution. But the issue was, we need to import esm build of the package.\n//   But in esm build the patch.json file is merged in build file. So, even if patch the json file for syntaxes it will not help.\n\nexport const parseLinearGradient = (\n  gradient: string\n): ParsedLinearGradient | undefined => {\n  const { normalized: normalizedGradient, isRepeating } =\n    normalizeRepeatingGradient(\n      gradient,\n      \"repeating-linear-gradient\",\n      \"linear-gradient\"\n    );\n\n  const ast = cssTryParseValue(normalizedGradient);\n  if (ast === undefined) {\n    return;\n  }\n\n  const match = csstree.lexer.match(\n    \"linear-gradient( [ <angle> | to <side-or-corner> ]? , <color-stop-list> )\",\n    ast\n  );\n  const containsVar = normalizedGradient.includes(\"var(\");\n  if (match.matched === null && containsVar === false) {\n    return;\n  }\n\n  let angle: UnitValue | VarValue | undefined;\n  let sideOrCorner: KeywordValue | undefined;\n  const stops: GradientStop[] = [];\n  forEachGradientParts(ast, \"linear-gradient\", (gradientParts) => {\n    let handledAsAngle = false;\n\n    if (gradientParts.length === 1) {\n      const singlePart = gradientParts[0];\n      const mappedValue = mapLengthPercentageOrVar(singlePart);\n      const isAngleValue =\n        (isAngleDimension(singlePart) &&\n          mappedValue?.type === \"unit\" &&\n          isAngleUnit(mappedValue.unit)) ||\n        (singlePart.type === \"Function\" &&\n          singlePart.name === \"var\" &&\n          mappedValue?.type === \"var\" &&\n          isVarAngle(mappedValue));\n\n      if (isAngleValue && mappedValue !== undefined) {\n        angle = mappedValue;\n        handledAsAngle = true;\n      } else if (isColorStop(singlePart) === false) {\n        if (mappedValue !== undefined) {\n          stops.push({\n            hint: mappedValue,\n          });\n        }\n      }\n    }\n\n    if (gradientParts.length && isSideOrCorner(gradientParts[0])) {\n      const value = gradientParts\n        .map((item) => csstree.generate(item))\n        .join(\" \");\n      sideOrCorner = { type: \"keyword\", value };\n    }\n\n    if (handledAsAngle === false) {\n      const colorStop = gradientParts.find(isColorStop);\n      if (colorStop !== undefined) {\n        const colorIndex = gradientParts.indexOf(colorStop);\n        const position = gradientParts[colorIndex + 1];\n        const hint = gradientParts[colorIndex + 2];\n\n        const stop: GradientStop = {\n          color: getColor(colorStop),\n          position: mapLengthPercentageOrVar(position),\n          hint: mapLengthPercentageOrVar(hint),\n        };\n\n        stops.push(stop);\n      }\n    }\n  });\n\n  if (stops.length === 0) {\n    return;\n  }\n\n  const parsedGradient: ParsedLinearGradient = {\n    type: \"linear\",\n    angle,\n    sideOrCorner,\n    stops,\n  };\n  if (isRepeating) {\n    parsedGradient.repeating = true;\n  }\n\n  return parsedGradient;\n};\n\nconst isSideOrCorner = (node: csstree.CssNode): node is csstree.Identifier =>\n  node.type === \"Identifier\" && sideOrCorderIdentifiers.includes(node.name);\n\nexport const formatLinearGradient = (parsed: ParsedLinearGradient): string => {\n  const direction = parsed.angle || parsed.sideOrCorner;\n  const stops = formatGradientStops(parsed.stops);\n\n  const functionName =\n    parsed.repeating === true ? \"repeating-linear-gradient\" : \"linear-gradient\";\n\n  return `${functionName}(${direction ? toValue(direction) + \", \" : \"\"}${stops})`;\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/radial-gradient.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { parseRadialGradient, formatRadialGradient } from \"./radial-gradient\";\n\n// casts helper similar to other gradient tests\ntype RadialGradient = NonNullable<ReturnType<typeof parseRadialGradient>>;\nconst radial = (gradient: Omit<RadialGradient, \"type\">): RadialGradient => ({\n  type: \"radial\",\n  ...gradient,\n});\n\ndescribe(\"parse radial-gradient\", () => {\n  test(\"parses gradient with single color stop\", () => {\n    expect(parseRadialGradient(\"radial-gradient(red)\")).toEqual(\n      radial({\n        shape: undefined,\n        size: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with shape and multiple stops\", () => {\n    expect(\n      parseRadialGradient(\"radial-gradient(circle, red, blue, yellow)\")\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"circle\" },\n        size: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 1, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with shape, size, position and stops\", () => {\n    expect(\n      parseRadialGradient(\n        \"radial-gradient(circle closest-side at center, orange 0%, cyan 100%)\"\n      )\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"circle\" },\n        size: \"closest-side\",\n        position: \"center\",\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0.6471, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 1, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses ellipse with keyword size and keyword position\", () => {\n    expect(\n      parseRadialGradient(\n        \"radial-gradient(ellipse farthest-corner at left top, red 0%, blue 100%)\"\n      )\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"ellipse\" },\n        size: \"farthest-corner\",\n        position: \"left top\",\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 0 },\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 100 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with size keyword only\", () => {\n    expect(\n      parseRadialGradient(\"radial-gradient(closest-side, red, blue)\")\n    ).toEqual(\n      radial({\n        shape: undefined,\n        size: \"closest-side\",\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses circle with explicit radius length\", () => {\n    expect(\n      parseRadialGradient(\"radial-gradient(circle 25px, red, blue)\")\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"circle\" },\n        size: \"25px\",\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses ellipse with explicit radii\", () => {\n    expect(\n      parseRadialGradient(\"radial-gradient(ellipse 20% 40%, red, blue)\")\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"ellipse\" },\n        size: \"20% 40%\",\n        position: undefined,\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses descriptors regardless of ordering\", () => {\n    expect(\n      parseRadialGradient(\n        \"radial-gradient(farthest-side circle at 40px 40px, red, blue)\"\n      )\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"circle\" },\n        size: \"farthest-side\",\n        position: \"40px 40px\",\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses gradient with explicit size values and position coordinates\", () => {\n    expect(\n      parseRadialGradient(\n        \"radial-gradient(20px 40px at 10px 20px, red 10% 40%, blue 80%)\"\n      )\n    ).toEqual(\n      radial({\n        shape: undefined,\n        size: \"20px 40px\",\n        position: \"10px 20px\",\n        stops: [\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [1, 0, 0],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 10 },\n            hint: { type: \"unit\", unit: \"%\", value: 40 },\n          },\n          {\n            color: {\n              type: \"color\",\n              colorSpace: \"srgb\",\n              components: [0, 0, 1],\n              alpha: 1,\n            },\n            position: { type: \"unit\", unit: \"%\", value: 80 },\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses radial-gradient with css variables\", () => {\n    expect(\n      parseRadialGradient(\n        \"radial-gradient(circle, var(--brand-color), var(--accent-color))\"\n      )\n    ).toEqual(\n      radial({\n        shape: { type: \"keyword\", value: \"circle\" },\n        size: undefined,\n        position: undefined,\n        stops: [\n          {\n            color: { type: \"var\", value: \"brand-color\" },\n            position: undefined,\n            hint: undefined,\n          },\n          {\n            color: { type: \"var\", value: \"accent-color\" },\n            position: undefined,\n            hint: undefined,\n          },\n        ],\n      })\n    );\n  });\n\n  test(\"parses repeating radial-gradient and formats back\", () => {\n    const parsed = parseRadialGradient(\n      \"repeating-radial-gradient(red 0%, blue 100%)\"\n    );\n    if (parsed === undefined) {\n      throw new Error(\"parsed radial gradient is undefined\");\n    }\n\n    expect(parsed.repeating).toBe(true);\n    expect(formatRadialGradient(parsed)).toEqual(\n      \"repeating-radial-gradient(rgb(255 0 0 / 1) 0%, rgb(0 0 255 / 1) 100%)\"\n    );\n  });\n\n  test(\"formats gradient with descriptors in order\", () => {\n    const parsed = parseRadialGradient(\n      \"radial-gradient(circle 40px at top, red, blue)\"\n    );\n    if (parsed === undefined) {\n      throw new Error(\"parsed radial gradient is undefined\");\n    }\n\n    expect(formatRadialGradient(parsed)).toEqual(\n      \"radial-gradient(circle 40px at top, rgb(255 0 0 / 1), rgb(0 0 255 / 1))\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/radial-gradient.ts",
    "content": "import * as csstree from \"css-tree\";\nimport { cssTryParseValue } from \"../parse-css-value\";\nimport { toValue, type KeywordValue } from \"@webstudio-is/css-engine\";\nimport {\n  getColor,\n  isColorStop,\n  mapLengthPercentageOrVar,\n  formatGradientStops,\n  normalizeRepeatingGradient,\n  forEachGradientParts,\n} from \"./gradient-utils\";\nimport type { GradientStop, ParsedRadialGradient } from \"./types\";\n\nconst shapeKeywords = new Set([\"circle\", \"ellipse\"]);\n\nconst isIdentifier = (node: csstree.CssNode, value: string) =>\n  node.type === \"Identifier\" && node.name.toLowerCase() === value;\n\nconst toKeywordValue = (value: string): KeywordValue => ({\n  type: \"keyword\",\n  value,\n});\n\nconst toCssText = (nodes: csstree.CssNode[]): string =>\n  nodes\n    .map((item) => csstree.generate(item))\n    .join(\" \")\n    .trim();\n\nexport const parseRadialGradient = (\n  gradient: string\n): ParsedRadialGradient | undefined => {\n  const { normalized: normalizedGradient, isRepeating } =\n    normalizeRepeatingGradient(\n      gradient,\n      \"repeating-radial-gradient\",\n      \"radial-gradient\"\n    );\n\n  const ast = cssTryParseValue(normalizedGradient);\n  if (ast === undefined) {\n    return;\n  }\n\n  const containsVar = normalizedGradient.includes(\"var(\");\n  let matchesGrammar = true;\n  try {\n    const match = csstree.lexer.match(\n      \"radial-gradient( [ <ending-shape> || <ending-size> ]? [ at <position> ]? , <color-stop-list> )\",\n      ast\n    );\n    matchesGrammar = match.matched !== null;\n  } catch {\n    // css-tree 2.3.1 does not define <ending-size>, so skip validation\n    matchesGrammar = true;\n  }\n  if (matchesGrammar === false && containsVar === false) {\n    return;\n  }\n\n  let shape: KeywordValue | undefined;\n  let size: string | undefined;\n  let position: string | undefined;\n  const stops: GradientStop[] = [];\n\n  forEachGradientParts(ast, \"radial-gradient\", (gradientParts) => {\n    const filtered = gradientParts.filter((node) => node.type !== \"WhiteSpace\");\n    if (filtered.length === 0) {\n      return;\n    }\n\n    const colorStopNode = filtered.find(isColorStop);\n    if (colorStopNode !== undefined) {\n      const color = getColor(colorStopNode);\n      const colorIndex = filtered.indexOf(colorStopNode);\n      const positionNode = filtered[colorIndex + 1];\n      const hintNode = filtered[colorIndex + 2];\n      const stop: GradientStop = {\n        color,\n        position: mapLengthPercentageOrVar(positionNode),\n        hint: mapLengthPercentageOrVar(hintNode),\n      };\n      stops.push(stop);\n      return;\n    }\n\n    const directiveTokens = [...filtered];\n    const atIndex = directiveTokens.findIndex((node) =>\n      isIdentifier(node, \"at\")\n    );\n    if (atIndex !== -1) {\n      const positionTokens = directiveTokens.slice(atIndex + 1);\n      const css = toCssText(positionTokens);\n      if (css.length > 0) {\n        position = css;\n      }\n      directiveTokens.length = atIndex;\n    }\n\n    if (directiveTokens.length === 0) {\n      return;\n    }\n\n    const remainderTokens: csstree.CssNode[] = [];\n    for (const token of directiveTokens) {\n      if (\n        token.type === \"Identifier\" &&\n        shapeKeywords.has(token.name.toLowerCase()) &&\n        shape === undefined\n      ) {\n        shape = toKeywordValue(token.name);\n        continue;\n      }\n      remainderTokens.push(token);\n    }\n\n    const sizeCss = toCssText(remainderTokens);\n    if (sizeCss.length > 0) {\n      size = sizeCss;\n      return;\n    }\n\n    const hint = mapLengthPercentageOrVar(directiveTokens[0]);\n    if (hint !== undefined) {\n      stops.push({ hint });\n    }\n  });\n\n  if (stops.length === 0) {\n    return;\n  }\n\n  const parsed: ParsedRadialGradient = {\n    type: \"radial\",\n    shape,\n    size,\n    position,\n    stops,\n  };\n  if (isRepeating) {\n    parsed.repeating = true;\n  }\n\n  return parsed;\n};\n\nexport const formatRadialGradient = (parsed: ParsedRadialGradient): string => {\n  const shapeSizeParts: string[] = [];\n  if (parsed.shape) {\n    shapeSizeParts.push(toValue(parsed.shape));\n  }\n  if (parsed.size) {\n    shapeSizeParts.push(parsed.size);\n  }\n  const descriptors: string[] = [];\n  const shapeSize = shapeSizeParts.join(\" \").trim();\n  if (shapeSize.length > 0) {\n    descriptors.push(shapeSize);\n  }\n  if (parsed.position) {\n    descriptors.push(`at ${parsed.position}`);\n  }\n\n  const functionName =\n    parsed.repeating === true ? \"repeating-radial-gradient\" : \"radial-gradient\";\n  const descriptorPrefix =\n    descriptors.length > 0 ? `${descriptors.join(\" \")}, ` : \"\";\n  const stops = formatGradientStops(parsed.stops);\n\n  return `${functionName}(${descriptorPrefix}${stops})`;\n};\n"
  },
  {
    "path": "packages/css-data/src/property-parsers/types.ts",
    "content": "import {\n  type ColorValue,\n  type KeywordValue,\n  type RgbValue,\n  type UnitValue,\n  type VarValue,\n} from \"@webstudio-is/css-engine\";\n\nexport type GradientColorValue =\n  | ColorValue\n  | RgbValue\n  | KeywordValue\n  | VarValue;\n\nexport type GradientStop = {\n  color?: GradientColorValue;\n  position?: UnitValue | VarValue;\n  hint?: UnitValue | VarValue;\n};\n\nexport type ParsedGradientBase = {\n  stops: GradientStop[];\n  repeating?: boolean;\n};\n\nexport type ParsedLinearGradient = ParsedGradientBase & {\n  type: \"linear\";\n  angle?: UnitValue | VarValue;\n  sideOrCorner?: KeywordValue;\n};\n\nexport type ParsedConicGradient = ParsedGradientBase & {\n  type: \"conic\";\n  angle?: UnitValue | VarValue;\n  position?: string;\n};\n\nexport type ParsedRadialGradient = ParsedGradientBase & {\n  type: \"radial\";\n  shape?: KeywordValue;\n  size?: string;\n  position?: string;\n};\n\nexport type ParsedGradient =\n  | ParsedLinearGradient\n  | ParsedConicGradient\n  | ParsedRadialGradient;\n"
  },
  {
    "path": "packages/css-data/src/selector-validation.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { validateSelector } from \"./selector-validation\";\n\ndescribe(\"validateSelector\", () => {\n  describe(\"valid selectors\", () => {\n    test(\"simple pseudo-classes\", () => {\n      const selectors = [\n        \":hover\",\n        \":focus\",\n        \":active\",\n        \":visited\",\n        \":disabled\",\n        \":checked\",\n        \":focus-visible\",\n        \":focus-within\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"simple pseudo-elements\", () => {\n      const selectors = [\n        \"::before\",\n        \"::after\",\n        \"::placeholder\",\n        \"::first-line\",\n        \"::first-letter\",\n        \"::selection\",\n        \"::marker\",\n        \"::backdrop\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-element\");\n        }\n      }\n    });\n\n    test(\"legacy single-colon pseudo-elements\", () => {\n      const selectors = [\":before\", \":after\", \":first-line\", \":first-letter\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-element\");\n        }\n      }\n    });\n\n    test(\"functional pseudo-classes with simple selectors\", () => {\n      const selectors = [\":not(div)\", \":is(a)\", \":where(span)\", \":has(p)\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"functional pseudo-classes with An+B notation\", () => {\n      const selectors = [\n        \":nth-child(2n)\",\n        \":nth-last-child(odd)\",\n        \":nth-of-type(3n+1)\",\n        \":nth-last-of-type(even)\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"functional pseudo-classes with nested selectors\", () => {\n      const selectors = [\n        \":not(.class)\",\n        \":is(#id)\",\n        \":where([attr])\",\n        \":has(> div)\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"shadow DOM pseudo-classes\", () => {\n      const selectors = [\":host\", \":host(.class)\", \":host-context(.class)\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"linguistic pseudo-classes\", () => {\n      const selectors = [\":lang(en)\", \":dir(rtl)\", \":dir(ltr)\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"custom state pseudo-class\", () => {\n      const result = validateSelector(\":state(custom)\");\n      expect(result.success).toBe(true);\n      if (result.success) {\n        expect(result.type).toBe(\"pseudo-class\");\n      }\n    });\n\n    test(\"time-dimensional pseudo-classes\", () => {\n      const selectors = [\":current\", \":past\", \":future\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-class\");\n        }\n      }\n    });\n\n    test(\"less common pseudo-elements from generated list\", () => {\n      const selectors = [\n        \"::grammar-error\",\n        \"::spelling-error\",\n        \"::target-text\",\n        \"::file-selector-button\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-element\");\n        }\n      }\n    });\n\n    test(\"functional pseudo-elements\", () => {\n      const selectors = [\"::part(tab)\", \"::slotted(span)\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"pseudo-element\");\n        }\n      }\n    });\n  });\n\n  describe(\"attribute selectors\", () => {\n    test(\"basic attribute selectors\", () => {\n      const selectors = [\n        \"[data-state]\",\n        \"[data-state=open]\",\n        \"[data-state=closed]\",\n        \"[aria-expanded=true]\",\n        \"[aria-hidden=false]\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"attribute\");\n        }\n      }\n    });\n\n    test(\"attribute selectors with operators\", () => {\n      const selectors = [\n        \"[class~=foo]\",\n        \"[lang|=en]\",\n        \"[href^=https]\",\n        '[src$=\".png\"]',\n        \"[title*=hello]\",\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"attribute\");\n        }\n      }\n    });\n\n    test(\"attribute selectors with quotes\", () => {\n      const selectors = [\n        '[data-state=\"open\"]',\n        \"[data-state='closed']\",\n        '[aria-label=\"menu button\"]',\n      ];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"attribute\");\n        }\n      }\n    });\n\n    test(\"case-insensitive attribute selectors\", () => {\n      const selectors = [\"[type=text i]\", \"[href$=PDF i]\"];\n\n      for (const selector of selectors) {\n        const result = validateSelector(selector);\n        expect(result.success).toBe(true);\n        if (result.success) {\n          expect(result.type).toBe(\"attribute\");\n        }\n      }\n    });\n  });\n\n  describe(\"invalid selectors\", () => {\n    test(\"empty selector\", () => {\n      const result = validateSelector(\"\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Selector cannot be empty\");\n      }\n    });\n\n    test(\"whitespace-only selector\", () => {\n      const result = validateSelector(\"   \");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Selector cannot be empty\");\n      }\n    });\n\n    test(\"selector not starting with colon or bracket\", () => {\n      const result = validateSelector(\"hover\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\n          \"Selector must start with a colon (:) for pseudo-classes, double colon (::) for pseudo-elements, or bracket ([) for attribute selectors\"\n        );\n      }\n    });\n\n    test(\"invalid pseudo-class name\", () => {\n      const result = validateSelector(\":not-a-real-pseudo-class\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Invalid pseudo-class\");\n      }\n    });\n\n    test(\"invalid functional pseudo-class name\", () => {\n      const result = validateSelector(\":blablubb(div)\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Invalid pseudo-class\");\n      }\n    });\n\n    test(\"invalid pseudo-element name\", () => {\n      const result = validateSelector(\"::not-a-real-pseudo-element\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Invalid pseudo-element\");\n      }\n    });\n\n    test(\"unbalanced parentheses in functional pseudo-class\", () => {\n      const result = validateSelector(\":not(div\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Invalid CSS selector syntax\");\n      }\n    });\n\n    test(\"unbalanced parentheses - too many closing\", () => {\n      const result = validateSelector(\":not(div))\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Invalid CSS selector syntax\");\n      }\n    });\n\n    test(\"invalid syntax inside functional pseudo-class\", () => {\n      // css-tree is lenient and will parse this\n      // The browser will ultimately reject truly invalid selectors\n      const result = validateSelector(\":has(>>invalid)\");\n      // This actually parses in css-tree (it's lenient)\n      expect(result.success).toBe(true);\n    });\n\n    test(\"unbalanced brackets in attribute selector\", () => {\n      const result = validateSelector(\"[data-state\");\n      expect(result.success).toBe(false);\n      if (!result.success) {\n        expect(result.error).toBe(\"Invalid CSS selector syntax\");\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/css-data/src/selector-validation.ts",
    "content": "import * as csstree from \"css-tree\";\nimport { pseudoElements } from \"./__generated__/pseudo-elements\";\nimport { pseudoClasses } from \"./__generated__/pseudo-classes\";\n\nexport type SelectorValidationResult =\n  | { success: false; error: string }\n  | { success: true; type: \"pseudo-class\" | \"pseudo-element\" | \"attribute\" };\n\n/**\n * Validates a CSS pseudo-class, pseudo-element, or attribute selector\n * @param selector - The selector to validate (e.g., \":hover\", \"::before\", \"[data-state=open]\")\n * @returns SelectorValidationResult with success status and type or error message\n */\nexport const validateSelector = (\n  selector: string\n): SelectorValidationResult => {\n  if (!selector || selector.trim() === \"\") {\n    return { success: false, error: \"Selector cannot be empty\" };\n  }\n\n  // Must start with a colon or bracket\n  if (!selector.startsWith(\":\") && !selector.startsWith(\"[\")) {\n    return {\n      success: false,\n      error:\n        \"Selector must start with a colon (:) for pseudo-classes, double colon (::) for pseudo-elements, or bracket ([) for attribute selectors\",\n    };\n  }\n\n  try {\n    // Parse as a rule to validate syntax\n    // We wrap it in a dummy selector because css-tree requires a full rule\n    const ast = csstree.parse(`dummy${selector} {}`);\n\n    let foundPseudoClass = false;\n    let foundPseudoElement = false;\n    let foundAttribute = false;\n    let pseudoClassName: string | undefined;\n    let pseudoElementName: string | undefined;\n\n    // Walk the AST to find pseudo-class, pseudo-element, or attribute selectors\n    csstree.walk(ast, (node) => {\n      if (node.type === \"PseudoClassSelector\") {\n        foundPseudoClass = true;\n        pseudoClassName = node.name;\n      }\n      if (node.type === \"PseudoElementSelector\") {\n        foundPseudoElement = true;\n        pseudoElementName = node.name;\n      }\n      if (node.type === \"AttributeSelector\") {\n        foundAttribute = true;\n      }\n    });\n\n    // Determine the type based on what we found\n    if (foundPseudoElement) {\n      // Validate that the pseudo-element actually exists\n      // Some pseudo-elements are functional (e.g., ::part(), ::slotted())\n      // The list includes both \"part\" and \"part()\" variants\n      if (pseudoElementName) {\n        const withParens = `${pseudoElementName}()`;\n        if (\n          (pseudoElements as readonly string[]).includes(pseudoElementName) ||\n          (pseudoElements as readonly string[]).includes(withParens)\n        ) {\n          return { success: true, type: \"pseudo-element\" };\n        }\n      }\n      // Invalid pseudo-element name\n      return {\n        success: false,\n        error: \"Invalid pseudo-element\",\n      };\n    }\n\n    if (foundPseudoClass) {\n      // Check if it's actually a legacy pseudo-element (single colon)\n      // Legacy syntax: :before, :after, :first-line, :first-letter\n      if (\n        pseudoClassName &&\n        (pseudoElements as readonly string[]).includes(pseudoClassName)\n      ) {\n        return { success: true, type: \"pseudo-element\" };\n      }\n\n      // Validate that the pseudo-class actually exists\n      // Some pseudo-classes are functional (e.g., :has(), :not())\n      // The list includes both \"has\" and \"has()\" variants\n      if (pseudoClassName) {\n        const withParens = `${pseudoClassName}()`;\n        if (\n          (pseudoClasses as readonly string[]).includes(pseudoClassName) ||\n          (pseudoClasses as readonly string[]).includes(withParens)\n        ) {\n          return { success: true, type: \"pseudo-class\" };\n        }\n      }\n\n      // Invalid pseudo-class name\n      return {\n        success: false,\n        error: \"Invalid pseudo-class\",\n      };\n    }\n\n    if (foundAttribute) {\n      return { success: true, type: \"attribute\" };\n    }\n\n    // If we get here, something unexpected happened\n    return {\n      success: false,\n      error: \"Invalid CSS selector syntax\",\n    };\n  } catch {\n    return {\n      success: false,\n      error: \"Invalid CSS selector syntax\",\n    };\n  }\n};\n"
  },
  {
    "path": "packages/css-data/src/shorthands.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { expandShorthands } from \"./shorthands\";\n\ntest(\"ignore all shorthand to not bloat webstudio data\", () => {\n  expect(\n    expandShorthands([\n      [\"all\", \"initial\"],\n      [\"color\", \"red\"],\n    ])\n  ).toEqual([[\"color\", \"red\"]]);\n});\n\ntest(\"expand border\", () => {\n  expect(expandShorthands([[\"border\", \"1px solid red\"]])).toEqual([\n    [\"border-top-width\", \"1px\"],\n    [\"border-right-width\", \"1px\"],\n    [\"border-bottom-width\", \"1px\"],\n    [\"border-left-width\", \"1px\"],\n    [\"border-top-style\", \"solid\"],\n    [\"border-right-style\", \"solid\"],\n    [\"border-bottom-style\", \"solid\"],\n    [\"border-left-style\", \"solid\"],\n    [\"border-top-color\", \"red\"],\n    [\"border-right-color\", \"red\"],\n    [\"border-bottom-color\", \"red\"],\n    [\"border-left-color\", \"red\"],\n  ]);\n  // logical\n  expect(expandShorthands([[\"border-inline\", \"1px solid red\"]])).toEqual([\n    [\"border-inline-start-width\", \"1px\"],\n    [\"border-inline-end-width\", \"1px\"],\n    [\"border-inline-start-style\", \"solid\"],\n    [\"border-inline-end-style\", \"solid\"],\n    [\"border-inline-start-color\", \"red\"],\n    [\"border-inline-end-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-block\", \"1px solid red\"]])).toEqual([\n    [\"border-block-start-width\", \"1px\"],\n    [\"border-block-end-width\", \"1px\"],\n    [\"border-block-start-style\", \"solid\"],\n    [\"border-block-end-style\", \"solid\"],\n    [\"border-block-start-color\", \"red\"],\n    [\"border-block-end-color\", \"red\"],\n  ]);\n});\n\ntest(\"expand border with css-wide keywords\", () => {\n  expect(expandShorthands([[\"border\", \"INHERIT\"]])).toEqual([\n    [\"border-top-width\", \"INHERIT\"],\n    [\"border-right-width\", \"INHERIT\"],\n    [\"border-bottom-width\", \"INHERIT\"],\n    [\"border-left-width\", \"INHERIT\"],\n    [\"border-top-style\", \"INHERIT\"],\n    [\"border-right-style\", \"INHERIT\"],\n    [\"border-bottom-style\", \"INHERIT\"],\n    [\"border-left-style\", \"INHERIT\"],\n    [\"border-top-color\", \"INHERIT\"],\n    [\"border-right-color\", \"INHERIT\"],\n    [\"border-bottom-color\", \"INHERIT\"],\n    [\"border-left-color\", \"INHERIT\"],\n  ]);\n});\n\ntest(\"expand border edges\", () => {\n  expect(expandShorthands([[\"border-top\", \"1px solid red\"]])).toEqual([\n    [\"border-top-width\", \"1px\"],\n    [\"border-top-style\", \"solid\"],\n    [\"border-top-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-right\", \"1px solid red\"]])).toEqual([\n    [\"border-right-width\", \"1px\"],\n    [\"border-right-style\", \"solid\"],\n    [\"border-right-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-bottom\", \"1px solid red\"]])).toEqual([\n    [\"border-bottom-width\", \"1px\"],\n    [\"border-bottom-style\", \"solid\"],\n    [\"border-bottom-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-left\", \"1px solid red\"]])).toEqual([\n    [\"border-left-width\", \"1px\"],\n    [\"border-left-style\", \"solid\"],\n    [\"border-left-color\", \"red\"],\n  ]);\n  // logical\n  expect(expandShorthands([[\"border-inline-start\", \"1px solid red\"]])).toEqual([\n    [\"border-inline-start-width\", \"1px\"],\n    [\"border-inline-start-style\", \"solid\"],\n    [\"border-inline-start-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-inline-end\", \"1px solid red\"]])).toEqual([\n    [\"border-inline-end-width\", \"1px\"],\n    [\"border-inline-end-style\", \"solid\"],\n    [\"border-inline-end-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-block-start\", \"1px solid red\"]])).toEqual([\n    [\"border-block-start-width\", \"1px\"],\n    [\"border-block-start-style\", \"solid\"],\n    [\"border-block-start-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-block-end\", \"1px solid red\"]])).toEqual([\n    [\"border-block-end-width\", \"1px\"],\n    [\"border-block-end-style\", \"solid\"],\n    [\"border-block-end-color\", \"red\"],\n  ]);\n  // omit values\n  expect(expandShorthands([[\"border-top\", \"1px\"]])).toEqual([\n    [\"border-top-width\", \"1px\"],\n    [\"border-top-style\", \"none\"],\n    [\"border-top-color\", \"currentcolor\"],\n  ]);\n  expect(expandShorthands([[\"border-top\", \"red\"]])).toEqual([\n    [\"border-top-width\", \"medium\"],\n    [\"border-top-style\", \"none\"],\n    [\"border-top-color\", \"red\"],\n  ]);\n});\n\ntest(\"expand border types\", () => {\n  expect(expandShorthands([[\"border-width\", \"1px\"]])).toEqual([\n    [\"border-top-width\", \"1px\"],\n    [\"border-right-width\", \"1px\"],\n    [\"border-bottom-width\", \"1px\"],\n    [\"border-left-width\", \"1px\"],\n  ]);\n  expect(expandShorthands([[\"border-style\", \"solid\"]])).toEqual([\n    [\"border-top-style\", \"solid\"],\n    [\"border-right-style\", \"solid\"],\n    [\"border-bottom-style\", \"solid\"],\n    [\"border-left-style\", \"solid\"],\n  ]);\n  expect(expandShorthands([[\"border-color\", \"red\"]])).toEqual([\n    [\"border-top-color\", \"red\"],\n    [\"border-right-color\", \"red\"],\n    [\"border-bottom-color\", \"red\"],\n    [\"border-left-color\", \"red\"],\n  ]);\n  // logical\n  expect(expandShorthands([[\"border-inline-width\", \"1px\"]])).toEqual([\n    [\"border-inline-start-width\", \"1px\"],\n    [\"border-inline-end-width\", \"1px\"],\n  ]);\n  expect(expandShorthands([[\"border-block-width\", \"1px\"]])).toEqual([\n    [\"border-block-start-width\", \"1px\"],\n    [\"border-block-end-width\", \"1px\"],\n  ]);\n  expect(expandShorthands([[\"border-inline-style\", \"solid\"]])).toEqual([\n    [\"border-inline-start-style\", \"solid\"],\n    [\"border-inline-end-style\", \"solid\"],\n  ]);\n  expect(expandShorthands([[\"border-block-style\", \"solid\"]])).toEqual([\n    [\"border-block-start-style\", \"solid\"],\n    [\"border-block-end-style\", \"solid\"],\n  ]);\n  expect(expandShorthands([[\"border-inline-color\", \"red\"]])).toEqual([\n    [\"border-inline-start-color\", \"red\"],\n    [\"border-inline-end-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"border-block-color\", \"red\"]])).toEqual([\n    [\"border-block-start-color\", \"red\"],\n    [\"border-block-end-color\", \"red\"],\n  ]);\n});\n\ntest(\"expand border-radius\", () => {\n  expect(expandShorthands([[\"border-radius\", \"5px\"]])).toEqual([\n    [\"border-top-left-radius\", \"5px\"],\n    [\"border-top-right-radius\", \"5px\"],\n    [\"border-bottom-right-radius\", \"5px\"],\n    [\"border-bottom-left-radius\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"border-radius\", \"1px 2px 3px 4px\"]])).toEqual([\n    [\"border-top-left-radius\", \"1px\"],\n    [\"border-top-right-radius\", \"2px\"],\n    [\"border-bottom-right-radius\", \"3px\"],\n    [\"border-bottom-left-radius\", \"4px\"],\n  ]);\n  expect(expandShorthands([[\"border-radius\", \"5px / 3px\"]])).toEqual([\n    [\"border-top-left-radius\", \"5px 3px\"],\n    [\"border-top-right-radius\", \"5px 3px\"],\n    [\"border-bottom-right-radius\", \"5px 3px\"],\n    [\"border-bottom-left-radius\", \"5px 3px\"],\n  ]);\n  expect(expandShorthands([[\"border-radius\", \"5px 2px / 3px 4px\"]])).toEqual([\n    [\"border-top-left-radius\", \"5px 3px\"],\n    [\"border-top-right-radius\", \"2px 4px\"],\n    [\"border-bottom-right-radius\", \"5px 3px\"],\n    [\"border-bottom-left-radius\", \"2px 4px\"],\n  ]);\n});\n\ntest(\"expand border-radius with css-wide keywords\", () => {\n  expect(expandShorthands([[\"border-radius\", \"inherit\"]])).toEqual([\n    [\"border-top-left-radius\", \"inherit\"],\n    [\"border-top-right-radius\", \"inherit\"],\n    [\"border-bottom-right-radius\", \"inherit\"],\n    [\"border-bottom-left-radius\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand outline\", () => {\n  expect(expandShorthands([[\"outline\", \"1px solid red\"]])).toEqual([\n    [\"outline-width\", \"1px\"],\n    [\"outline-style\", \"solid\"],\n    [\"outline-color\", \"red\"],\n  ]);\n  expect(expandShorthands([[\"outline\", \"1px solid\"]])).toEqual([\n    [\"outline-width\", \"1px\"],\n    [\"outline-style\", \"solid\"],\n    [\"outline-color\", \"currentcolor\"],\n  ]);\n});\n\ntest(\"expand outline with css-wide keywords\", () => {\n  expect(expandShorthands([[\"outline\", \"inherit\"]])).toEqual([\n    [\"outline-width\", \"inherit\"],\n    [\"outline-style\", \"inherit\"],\n    [\"outline-color\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand margin/padding\", () => {\n  expect(expandShorthands([[\"margin\", \"5px\"]])).toEqual([\n    [\"margin-top\", \"5px\"],\n    [\"margin-right\", \"5px\"],\n    [\"margin-bottom\", \"5px\"],\n    [\"margin-left\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"margin\", \"1px 2px\"]])).toEqual([\n    [\"margin-top\", \"1px\"],\n    [\"margin-right\", \"2px\"],\n    [\"margin-bottom\", \"1px\"],\n    [\"margin-left\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"margin\", \"1px 2px 3px\"]])).toEqual([\n    [\"margin-top\", \"1px\"],\n    [\"margin-right\", \"2px\"],\n    [\"margin-bottom\", \"3px\"],\n    [\"margin-left\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"margin\", \"1px 2px 3px 4px\"]])).toEqual([\n    [\"margin-top\", \"1px\"],\n    [\"margin-right\", \"2px\"],\n    [\"margin-bottom\", \"3px\"],\n    [\"margin-left\", \"4px\"],\n  ]);\n  expect(expandShorthands([[\"padding\", \"5px\"]])).toEqual([\n    [\"padding-top\", \"5px\"],\n    [\"padding-right\", \"5px\"],\n    [\"padding-bottom\", \"5px\"],\n    [\"padding-left\", \"5px\"],\n  ]);\n  // logical\n  expect(expandShorthands([[\"margin-inline\", \"5px\"]])).toEqual([\n    [\"margin-inline-start\", \"5px\"],\n    [\"margin-inline-end\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"margin-inline\", \"1px 2px\"]])).toEqual([\n    [\"margin-inline-start\", \"1px\"],\n    [\"margin-inline-end\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"margin-block\", \"5px\"]])).toEqual([\n    [\"margin-block-start\", \"5px\"],\n    [\"margin-block-end\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"margin-block\", \"1px 2px\"]])).toEqual([\n    [\"margin-block-start\", \"1px\"],\n    [\"margin-block-end\", \"2px\"],\n  ]);\n});\n\ntest(\"expand margin/padding with css-wide keywords\", () => {\n  expect(expandShorthands([[\"margin\", \"inherit\"]])).toEqual([\n    [\"margin-top\", \"inherit\"],\n    [\"margin-right\", \"inherit\"],\n    [\"margin-bottom\", \"inherit\"],\n    [\"margin-left\", \"inherit\"],\n  ]);\n  expect(expandShorthands([[\"padding\", \"inherit\"]])).toEqual([\n    [\"padding-top\", \"inherit\"],\n    [\"padding-right\", \"inherit\"],\n    [\"padding-bottom\", \"inherit\"],\n    [\"padding-left\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand inset\", () => {\n  expect(expandShorthands([[\"inset\", \"5px\"]])).toEqual([\n    [\"top\", \"5px\"],\n    [\"right\", \"5px\"],\n    [\"bottom\", \"5px\"],\n    [\"left\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"inset\", \"1px 2px\"]])).toEqual([\n    [\"top\", \"1px\"],\n    [\"right\", \"2px\"],\n    [\"bottom\", \"1px\"],\n    [\"left\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"inset\", \"1px 2px 3px\"]])).toEqual([\n    [\"top\", \"1px\"],\n    [\"right\", \"2px\"],\n    [\"bottom\", \"3px\"],\n    [\"left\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"inset\", \"1px 2px 3px 4px\"]])).toEqual([\n    [\"top\", \"1px\"],\n    [\"right\", \"2px\"],\n    [\"bottom\", \"3px\"],\n    [\"left\", \"4px\"],\n  ]);\n  // logical\n  expect(expandShorthands([[\"inset-inline\", \"5px\"]])).toEqual([\n    [\"inset-inline-start\", \"5px\"],\n    [\"inset-inline-end\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"inset-inline\", \"1px 2px\"]])).toEqual([\n    [\"inset-inline-start\", \"1px\"],\n    [\"inset-inline-end\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"inset-block\", \"5px\"]])).toEqual([\n    [\"inset-block-start\", \"5px\"],\n    [\"inset-block-end\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"inset-block\", \"1px 2px\"]])).toEqual([\n    [\"inset-block-start\", \"1px\"],\n    [\"inset-block-end\", \"2px\"],\n  ]);\n});\n\ntest(\"expand inset with css-wide keywords\", () => {\n  expect(expandShorthands([[\"inset\", \"inherit\"]])).toEqual([\n    [\"top\", \"inherit\"],\n    [\"right\", \"inherit\"],\n    [\"bottom\", \"inherit\"],\n    [\"left\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand gap and grid-gap\", () => {\n  expect(expandShorthands([[\"gap\", \"5px\"]])).toEqual([\n    [\"row-gap\", \"5px\"],\n    [\"column-gap\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"gap\", \"1px 2px\"]])).toEqual([\n    [\"row-gap\", \"1px\"],\n    [\"column-gap\", \"2px\"],\n  ]);\n  expect(expandShorthands([[\"grid-gap\", \"5px\"]])).toEqual([\n    [\"row-gap\", \"5px\"],\n    [\"column-gap\", \"5px\"],\n  ]);\n  // remove grid- prefix\n  expect(expandShorthands([[\"grid-row-gap\", \"5px\"]])).toEqual([\n    [\"row-gap\", \"5px\"],\n  ]);\n  expect(expandShorthands([[\"grid-column-gap\", \"5px\"]])).toEqual([\n    [\"column-gap\", \"5px\"],\n  ]);\n});\n\ntest(\"expand gap with css-wide keywords\", () => {\n  expect(expandShorthands([[\"gap\", \"inherit\"]])).toEqual([\n    [\"row-gap\", \"inherit\"],\n    [\"column-gap\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand border-image\", () => {\n  expect(\n    expandShorthands([\n      [\n        \"border-image\",\n        `url(\"/images/border.png\") 27 23 / 50px 30px / 1rem round space`,\n      ],\n    ])\n  ).toEqual([\n    [\"border-image-source\", \"url(/images/border.png)\"],\n    [\"border-image-slice\", \"27 23\"],\n    [\"border-image-width\", \"50px 30px\"],\n    [\"border-image-outset\", \"1rem\"],\n    [\"border-image-repeat\", \"round space\"],\n  ]);\n  // shuffled\n  expect(\n    expandShorthands([\n      [\n        \"border-image\",\n        `round space url(\"/images/border.png\") 27 23 / 50px 30px / 1rem`,\n      ],\n    ])\n  ).toEqual([\n    [\"border-image-source\", \"url(/images/border.png)\"],\n    [\"border-image-slice\", \"27 23\"],\n    [\"border-image-width\", \"50px 30px\"],\n    [\"border-image-outset\", \"1rem\"],\n    [\"border-image-repeat\", \"round space\"],\n  ]);\n  // invalid extra nodes and missing syntaxes\n  // can lead to infinite loop\n  expect(\n    expandShorthands([\n      [\n        \"border-image\",\n        `url(\"/images/border.png\") 27 23 / 50px 30px / 1rem unknown keywords`,\n      ],\n    ])\n  ).toEqual([\n    [\"border-image-source\", \"url(/images/border.png)\"],\n    [\"border-image-slice\", \"27 23\"],\n    [\"border-image-width\", \"50px 30px\"],\n    [\"border-image-outset\", \"1rem\"],\n    [\"border-image-repeat\", \"initial\"],\n  ]);\n  // omitted types should be initial\n  expect(\n    expandShorthands([[\"border-image\", `linear-gradient(red, blue) 27`]])\n  ).toEqual([\n    [\"border-image-source\", \"linear-gradient(red,blue)\"],\n    [\"border-image-slice\", \"27\"],\n    [\"border-image-width\", \"initial\"],\n    [\"border-image-outset\", \"initial\"],\n    [\"border-image-repeat\", \"initial\"],\n  ]);\n});\n\ntest(\"expand border-image with css-wide keywords\", () => {\n  expect(expandShorthands([[\"border-image\", `inherit`]])).toEqual([\n    [\"border-image-source\", \"inherit\"],\n    [\"border-image-slice\", \"inherit\"],\n    [\"border-image-width\", \"inherit\"],\n    [\"border-image-outset\", \"inherit\"],\n    [\"border-image-repeat\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand place properties\", () => {\n  expect(\n    expandShorthands([\n      [\"place-content\", \"center\"],\n      [\"place-items\", \"center\"],\n      [\"place-self\", \"center\"],\n    ])\n  ).toEqual([\n    [\"align-content\", \"center\"],\n    [\"justify-content\", \"center\"],\n    [\"align-items\", \"center\"],\n    [\"justify-items\", \"center\"],\n    [\"align-self\", \"center\"],\n    [\"justify-self\", \"center\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\"place-content\", \"start end\"],\n      [\"place-items\", \"start end\"],\n      [\"place-self\", \"start end\"],\n    ])\n  ).toEqual([\n    [\"align-content\", \"start\"],\n    [\"justify-content\", \"end\"],\n    [\"align-items\", \"start\"],\n    [\"justify-items\", \"end\"],\n    [\"align-self\", \"start\"],\n    [\"justify-self\", \"end\"],\n  ]);\n});\n\ntest(\"expand place properties with css-wide keywords\", () => {\n  expect(\n    expandShorthands([\n      [\"place-content\", \"inherit\"],\n      [\"place-items\", \"inherit\"],\n      [\"place-self\", \"inherit\"],\n    ])\n  ).toEqual([\n    [\"align-content\", \"inherit\"],\n    [\"justify-content\", \"inherit\"],\n    [\"align-items\", \"inherit\"],\n    [\"justify-items\", \"inherit\"],\n    [\"align-self\", \"inherit\"],\n    [\"justify-self\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand font\", () => {\n  expect(\n    expandShorthands([\n      [\n        \"font\",\n        `ultra-condensed small-caps bold italic 1.2em \"Fira Sans\", sans-serif`,\n      ],\n    ])\n  ).toEqual([\n    [\"font-style\", \"italic\"],\n    [\"font-variant-caps\", \"small-caps\"],\n    [\"font-weight\", \"bold\"],\n    [\"font-stretch\", \"ultra-condensed\"],\n    [\"font-size\", \"1.2em\"],\n    [\"line-height\", \"initial\"],\n    [\"font-family\", '\"Fira Sans\",sans-serif'],\n  ]);\n  expect(\n    expandShorthands([[\"font\", `1.2em/2 \"Fira Sans\", sans-serif`]])\n  ).toEqual([\n    [\"font-style\", \"initial\"],\n    [\"font-variant-caps\", \"initial\"],\n    [\"font-weight\", \"initial\"],\n    [\"font-stretch\", \"initial\"],\n    [\"font-size\", \"1.2em\"],\n    [\"line-height\", \"2\"],\n    [\"font-family\", '\"Fira Sans\",sans-serif'],\n  ]);\n});\n\ntest(\"expand font with css-wide keywords\", () => {\n  expect(expandShorthands([[\"font\", `inherit`]])).toEqual([\n    [\"font-style\", \"inherit\"],\n    [\"font-variant-caps\", \"inherit\"],\n    [\"font-weight\", \"inherit\"],\n    [\"font-stretch\", \"inherit\"],\n    [\"font-size\", \"inherit\"],\n    [\"line-height\", \"inherit\"],\n    [\"font-family\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand font-synthesis\", () => {\n  expect(expandShorthands([[\"font-synthesis\", `none`]])).toEqual([\n    [\"font-synthesis-weight\", \"none\"],\n    [\"font-synthesis-style\", \"none\"],\n    [\"font-synthesis-small-caps\", \"none\"],\n    [\"font-synthesis-position\", \"none\"],\n  ]);\n  expect(expandShorthands([[\"font-synthesis\", `style`]])).toEqual([\n    [\"font-synthesis-weight\", \"none\"],\n    [\"font-synthesis-style\", \"auto\"],\n    [\"font-synthesis-small-caps\", \"none\"],\n    [\"font-synthesis-position\", \"none\"],\n  ]);\n  expect(\n    expandShorthands([[\"font-synthesis\", `style small-caps weight position`]])\n  ).toEqual([\n    [\"font-synthesis-weight\", \"auto\"],\n    [\"font-synthesis-style\", \"auto\"],\n    [\"font-synthesis-small-caps\", \"auto\"],\n    [\"font-synthesis-position\", \"auto\"],\n  ]);\n});\n\ntest(\"expand font-synthesis with css-wide keywords\", () => {\n  expect(expandShorthands([[\"font-synthesis\", `inherit`]])).toEqual([\n    [\"font-synthesis-weight\", \"inherit\"],\n    [\"font-synthesis-style\", \"inherit\"],\n    [\"font-synthesis-small-caps\", \"inherit\"],\n    [\"font-synthesis-position\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand font-variant\", () => {\n  expect(expandShorthands([[\"font-variant\", `normal`]])).toEqual([\n    [\"font-variant-ligatures\", \"normal\"],\n    [\"font-variant-caps\", \"normal\"],\n    [\"font-variant-alternates\", \"normal\"],\n    [\"font-variant-numeric\", \"normal\"],\n    [\"font-variant-east-asian\", \"normal\"],\n    [\"font-variant-position\", \"normal\"],\n    [\"font-variant-emoji\", \"normal\"],\n  ]);\n  expect(expandShorthands([[\"font-variant\", `none`]])).toEqual([\n    [\"font-variant-ligatures\", \"none\"],\n    [\"font-variant-caps\", \"normal\"],\n    [\"font-variant-alternates\", \"normal\"],\n    [\"font-variant-numeric\", \"normal\"],\n    [\"font-variant-east-asian\", \"normal\"],\n    [\"font-variant-position\", \"normal\"],\n    [\"font-variant-emoji\", \"normal\"],\n  ]);\n  expect(\n    expandShorthands([[\"font-variant\", `common-ligatures small-caps`]])\n  ).toEqual([\n    [\"font-variant-ligatures\", \"common-ligatures\"],\n    [\"font-variant-caps\", \"small-caps\"],\n    [\"font-variant-alternates\", \"normal\"],\n    [\"font-variant-numeric\", \"normal\"],\n    [\"font-variant-east-asian\", \"normal\"],\n    [\"font-variant-position\", \"normal\"],\n    [\"font-variant-emoji\", \"normal\"],\n  ]);\n});\n\ntest(\"expand font-variant with css-wide keywords\", () => {\n  expect(expandShorthands([[\"font-variant\", `inherit`]])).toEqual([\n    [\"font-variant-ligatures\", \"inherit\"],\n    [\"font-variant-caps\", \"inherit\"],\n    [\"font-variant-alternates\", \"inherit\"],\n    [\"font-variant-numeric\", \"inherit\"],\n    [\"font-variant-east-asian\", \"inherit\"],\n    [\"font-variant-position\", \"inherit\"],\n    [\"font-variant-emoji\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand text-decoration\", () => {\n  expect(expandShorthands([[\"text-decoration\", `underline`]])).toEqual([\n    [\"text-decoration-line\", \"underline\"],\n    [\"text-decoration-style\", \"solid\"],\n    [\"text-decoration-color\", \"currentcolor\"],\n  ]);\n  expect(expandShorthands([[\"text-decoration\", `underline dotted`]])).toEqual([\n    [\"text-decoration-line\", \"underline\"],\n    [\"text-decoration-style\", \"dotted\"],\n    [\"text-decoration-color\", \"currentcolor\"],\n  ]);\n  expect(\n    expandShorthands([[\"text-decoration\", `green wavy underline`]])\n  ).toEqual([\n    [\"text-decoration-line\", \"underline\"],\n    [\"text-decoration-style\", \"wavy\"],\n    [\"text-decoration-color\", \"green\"],\n  ]);\n});\n\ntest(\"expand text-emphasis\", () => {\n  expect(\n    expandShorthands([[\"text-emphasis\", \"filled double-circle #ffb703\"]])\n  ).toEqual([\n    [\"text-emphasis-style\", \"filled double-circle\"],\n    [\"text-emphasis-color\", \"#ffb703\"],\n  ]);\n  expect(expandShorthands([[\"text-emphasis\", \"none\"]])).toEqual([\n    [\"text-emphasis-style\", \"none\"],\n    [\"text-emphasis-color\", \"initial\"],\n  ]);\n});\n\ntest(\"expand flex\", () => {\n  expect(expandShorthands([[\"flex\", \"auto\"]])).toEqual([\n    [\"flex-grow\", \"1\"],\n    [\"flex-shrink\", \"1\"],\n    [\"flex-basis\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"flex\", \"none\"]])).toEqual([\n    [\"flex-grow\", \"0\"],\n    [\"flex-shrink\", \"0\"],\n    [\"flex-basis\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"flex\", \"10px\"]])).toEqual([\n    [\"flex-grow\", \"1\"],\n    [\"flex-shrink\", \"1\"],\n    [\"flex-basis\", \"10px\"],\n  ]);\n  expect(expandShorthands([[\"flex\", \"2\"]])).toEqual([\n    [\"flex-grow\", \"2\"],\n    [\"flex-shrink\", \"1\"],\n    [\"flex-basis\", \"0\"],\n  ]);\n  expect(expandShorthands([[\"flex\", \"2 3\"]])).toEqual([\n    [\"flex-grow\", \"2\"],\n    [\"flex-shrink\", \"3\"],\n    [\"flex-basis\", \"0\"],\n  ]);\n});\n\ntest(\"expand flex with css-wide keywords\", () => {\n  expect(expandShorthands([[\"flex\", \"inherit\"]])).toEqual([\n    [\"flex-grow\", \"inherit\"],\n    [\"flex-shrink\", \"inherit\"],\n    [\"flex-basis\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand flex-flow\", () => {\n  expect(expandShorthands([[\"flex-flow\", \"row\"]])).toEqual([\n    [\"flex-direction\", \"row\"],\n    [\"flex-wrap\", \"initial\"],\n  ]);\n  expect(expandShorthands([[\"flex-flow\", \"nowrap\"]])).toEqual([\n    [\"flex-direction\", \"initial\"],\n    [\"flex-wrap\", \"nowrap\"],\n  ]);\n  expect(expandShorthands([[\"flex-flow\", \"row nowrap\"]])).toEqual([\n    [\"flex-direction\", \"row\"],\n    [\"flex-wrap\", \"nowrap\"],\n  ]);\n});\n\ntest(\"expand columns\", () => {\n  expect(expandShorthands([[\"columns\", \"4 20px\"]])).toEqual([\n    [\"column-width\", \"20px\"],\n    [\"column-count\", \"4\"],\n  ]);\n  expect(expandShorthands([[\"columns\", \"4\"]])).toEqual([\n    [\"column-width\", \"initial\"],\n    [\"column-count\", \"4\"],\n  ]);\n  expect(expandShorthands([[\"columns\", \"20px\"]])).toEqual([\n    [\"column-width\", \"20px\"],\n    [\"column-count\", \"initial\"],\n  ]);\n});\n\ntest(\"expand column-rule\", () => {\n  expect(expandShorthands([[\"column-rule\", \"thick inset blue\"]])).toEqual([\n    [\"column-rule-width\", \"thick\"],\n    [\"column-rule-style\", \"inset\"],\n    [\"column-rule-color\", \"blue\"],\n  ]);\n});\n\ntest(\"expand list-style\", () => {\n  expect(\n    expandShorthands([\n      [\"list-style\", `lower-roman url(\"img/shape.png\") outside`],\n    ])\n  ).toEqual([\n    [\"list-style-position\", \"outside\"],\n    [\"list-style-image\", \"url(img/shape.png)\"],\n    [\"list-style-type\", \"lower-roman\"],\n  ]);\n  expect(expandShorthands([[\"list-style\", `square`]])).toEqual([\n    [\"list-style-position\", \"outside\"],\n    [\"list-style-image\", \"none\"],\n    [\"list-style-type\", \"square\"],\n  ]);\n  expect(expandShorthands([[\"list-style\", `inside`]])).toEqual([\n    [\"list-style-position\", \"inside\"],\n    [\"list-style-image\", \"none\"],\n    [\"list-style-type\", \"disc\"],\n  ]);\n  expect(expandShorthands([[\"list-style\", `none`]])).toEqual([\n    [\"list-style-position\", \"outside\"],\n    [\"list-style-image\", \"none\"],\n    [\"list-style-type\", \"none\"],\n  ]);\n});\n\ntest(\"expand animation\", () => {\n  expect(\n    expandShorthands([\n      [\"animation\", `3s ease-in 1s 2 reverse both paused slidein`],\n    ])\n  ).toEqual([\n    [\"animation-duration\", \"3s\"],\n    [\"animation-timing-function\", \"ease-in\"],\n    [\"animation-delay\", \"1s\"],\n    [\"animation-iteration-count\", \"2\"],\n    [\"animation-direction\", \"reverse\"],\n    [\"animation-fill-mode\", \"both\"],\n    [\"animation-play-state\", \"paused\"],\n    [\"animation-name\", \"slidein\"],\n    [\"animation-timeline\", \"auto\"],\n    [\"animation-range-start\", \"normal\"],\n    [\"animation-range-end\", \"normal\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\"animation\", `3s linear slidein, 3s ease-out 5s slideout`],\n    ])\n  ).toEqual([\n    [\"animation-duration\", \"3s,3s\"],\n    [\"animation-timing-function\", \"linear,ease-out\"],\n    [\"animation-delay\", \"0s,5s\"],\n    [\"animation-iteration-count\", \"1,1\"],\n    [\"animation-direction\", \"normal,normal\"],\n    [\"animation-fill-mode\", \"none,none\"],\n    [\"animation-play-state\", \"running,running\"],\n    [\"animation-name\", \"slidein,slideout\"],\n    [\"animation-timeline\", \"auto\"],\n    [\"animation-range-start\", \"normal\"],\n    [\"animation-range-end\", \"normal\"],\n  ]);\n});\n\ntest(\"expand animation with css-wide keywords\", () => {\n  expect(expandShorthands([[\"animation\", `inherit`]])).toEqual([\n    [\"animation-duration\", \"inherit\"],\n    [\"animation-timing-function\", \"inherit\"],\n    [\"animation-delay\", \"inherit\"],\n    [\"animation-iteration-count\", \"inherit\"],\n    [\"animation-direction\", \"inherit\"],\n    [\"animation-fill-mode\", \"inherit\"],\n    [\"animation-play-state\", \"inherit\"],\n    [\"animation-name\", \"inherit\"],\n    [\"animation-timeline\", \"inherit\"],\n    [\"animation-range-start\", \"inherit\"],\n    [\"animation-range-end\", \"inherit\"],\n  ]);\n});\n\ntest(\"expand animation-range\", () => {\n  expect(expandShorthands([[\"animation-range\", \"normal\"]])).toEqual([\n    [\"animation-range-start\", \"normal\"],\n    [\"animation-range-end\", \"normal\"],\n  ]);\n  expect(expandShorthands([[\"animation-range\", \"100px\"]])).toEqual([\n    [\"animation-range-start\", \"100px\"],\n    [\"animation-range-end\", \"normal\"],\n  ]);\n  expect(expandShorthands([[\"animation-range\", \"entry 10% exit\"]])).toEqual([\n    [\"animation-range-start\", \"entry 10%\"],\n    [\"animation-range-end\", \"exit\"],\n  ]);\n  expect(expandShorthands([[\"animation-range\", \"100px, 200px 50px\"]])).toEqual([\n    [\"animation-range-start\", \"100px,200px\"],\n    [\"animation-range-end\", \"normal,50px\"],\n  ]);\n});\n\ntest(\"expand view-timeline\", () => {\n  expect(expandShorthands([[\"view-timeline\", `none`]])).toEqual([\n    [\"view-timeline-name\", \"none\"],\n    [\"view-timeline-axis\", \"block\"],\n    [\"view-timeline-inset\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"view-timeline\", `none inline 200px`]])).toEqual([\n    [\"view-timeline-name\", \"none\"],\n    [\"view-timeline-axis\", \"inline\"],\n    [\"view-timeline-inset\", \"200px\"],\n  ]);\n  expect(\n    expandShorthands([[\"view-timeline\", `--custom_name_for_timeline inline`]])\n  ).toEqual([\n    [\"view-timeline-name\", \"--custom_name_for_timeline\"],\n    [\"view-timeline-axis\", \"inline\"],\n    [\"view-timeline-inset\", \"auto\"],\n  ]);\n  expect(\n    expandShorthands([[\"view-timeline\", `none inline, --custom y`]])\n  ).toEqual([\n    [\"view-timeline-name\", \"none,--custom\"],\n    [\"view-timeline-axis\", \"inline,y\"],\n    [\"view-timeline-inset\", \"auto,auto\"],\n  ]);\n});\n\ntest(\"expand transition\", () => {\n  expect(\n    expandShorthands([[\"transition\", `margin-right 4s ease-in-out 1s`]])\n  ).toEqual([\n    [\"transition-property\", \"margin-right\"],\n    [\"transition-duration\", \"4s\"],\n    [\"transition-timing-function\", \"ease-in-out\"],\n    [\"transition-delay\", \"1s\"],\n    [\"transition-behavior\", \"normal\"],\n  ]);\n  expect(\n    expandShorthands([[\"transition\", `margin-right 4s, color 1s`]])\n  ).toEqual([\n    [\"transition-property\", \"margin-right,color\"],\n    [\"transition-duration\", \"4s,1s\"],\n    [\"transition-timing-function\", \"ease,ease\"],\n    [\"transition-delay\", \"0s,0s\"],\n    [\"transition-behavior\", \"normal,normal\"],\n  ]);\n  expect(\n    expandShorthands([[\"transition\", `display 4s allow-discrete`]])\n  ).toEqual([\n    [\"transition-property\", \"display\"],\n    [\"transition-duration\", \"4s\"],\n    [\"transition-timing-function\", \"ease\"],\n    [\"transition-delay\", \"0s\"],\n    [\"transition-behavior\", \"allow-discrete\"],\n  ]);\n});\n\ntest(\"expand mask\", () => {\n  expect(expandShorthands([[\"mask\", `none`]])).toEqual([\n    [\"mask-image\", \"none\"],\n    [\"mask-position\", \"0% 0%\"],\n    [\"mask-size\", \"auto\"],\n    [\"mask-repeat\", \"repeat\"],\n    [\"mask-origin\", \"border-box\"],\n    [\"mask-clip\", \"border-box\"],\n    [\"mask-composite\", \"add\"],\n    [\"mask-mode\", \"match-source\"],\n  ]);\n  expect(expandShorthands([[\"mask\", `url(mask.png)`]])).toEqual([\n    [\"mask-image\", \"url(mask.png)\"],\n    [\"mask-position\", \"0% 0%\"],\n    [\"mask-size\", \"auto\"],\n    [\"mask-repeat\", \"repeat\"],\n    [\"mask-origin\", \"border-box\"],\n    [\"mask-clip\", \"border-box\"],\n    [\"mask-composite\", \"add\"],\n    [\"mask-mode\", \"match-source\"],\n  ]);\n  expect(\n    expandShorthands([[\"mask\", `url(masks.svg#star) 0 0/50px 50px`]])\n  ).toEqual([\n    [\"mask-image\", \"url(masks.svg#star)\"],\n    [\"mask-position\", \"0 0\"],\n    [\"mask-size\", \"50px 50px\"],\n    [\"mask-repeat\", \"repeat\"],\n    [\"mask-origin\", \"border-box\"],\n    [\"mask-clip\", \"border-box\"],\n    [\"mask-composite\", \"add\"],\n    [\"mask-mode\", \"match-source\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\n        \"mask\",\n        `url(masks.svg#star) left / 16px repeat-y, url(masks.svg#circle) right / 16px repeat-y`,\n      ],\n    ])\n  ).toEqual([\n    [\"mask-image\", \"url(masks.svg#star),url(masks.svg#circle)\"],\n    [\"mask-position\", \"left,right\"],\n    [\"mask-size\", \"16px,16px\"],\n    [\"mask-repeat\", \"repeat-y,repeat-y\"],\n    [\"mask-origin\", \"border-box,border-box\"],\n    [\"mask-clip\", \"border-box,border-box\"],\n    [\"mask-composite\", \"add,add\"],\n    [\"mask-mode\", \"match-source,match-source\"],\n  ]);\n});\n\ntest(\"expand mask-border\", () => {\n  expect(\n    expandShorthands([[\"mask-border\", `url(\"border-mask.png\") 25`]])\n  ).toEqual([\n    [\"mask-border-source\", \"url(border-mask.png)\"],\n    [\"mask-border-slice\", \"25\"],\n    [\"mask-border-width\", \"initial\"],\n    [\"mask-border-outset\", \"initial\"],\n    [\"mask-border-repeat\", \"initial\"],\n    [\"mask-border-mode\", \"initial\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\"mask-border\", `url(\"border-mask.png\") 25 / 35px / 12px space alpha`],\n    ])\n  ).toEqual([\n    [\"mask-border-source\", \"url(border-mask.png)\"],\n    [\"mask-border-slice\", \"25\"],\n    [\"mask-border-width\", \"35px\"],\n    [\"mask-border-outset\", \"12px\"],\n    [\"mask-border-repeat\", \"space\"],\n    [\"mask-border-mode\", \"alpha\"],\n  ]);\n});\n\ntest(\"expand grid-area\", () => {\n  expect(expandShorthands([[\"grid-area\", \"a / b / c / d\"]])).toEqual([\n    [\"grid-row-start\", \"a\"],\n    [\"grid-column-start\", \"b\"],\n    [\"grid-row-end\", \"c\"],\n    [\"grid-column-end\", \"d\"],\n  ]);\n  expect(expandShorthands([[\"grid-area\", \"a / b / c\"]])).toEqual([\n    [\"grid-row-start\", \"a\"],\n    [\"grid-column-start\", \"b\"],\n    [\"grid-row-end\", \"c\"],\n    [\"grid-column-end\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"grid-area\", \"a / b\"]])).toEqual([\n    [\"grid-row-start\", \"a\"],\n    [\"grid-column-start\", \"b\"],\n    [\"grid-row-end\", \"auto\"],\n    [\"grid-column-end\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"grid-area\", \"a\"]])).toEqual([\n    [\"grid-row-start\", \"a\"],\n    [\"grid-column-start\", \"auto\"],\n    [\"grid-row-end\", \"auto\"],\n    [\"grid-column-end\", \"auto\"],\n  ]);\n});\n\ntest(\"expand grid-row and grid-column\", () => {\n  expect(\n    expandShorthands([\n      [\"grid-row\", \"1\"],\n      [\"grid-column\", \"1\"],\n    ])\n  ).toEqual([\n    [\"grid-row-start\", \"1\"],\n    [\"grid-row-end\", \"auto\"],\n    [\"grid-column-start\", \"1\"],\n    [\"grid-column-end\", \"auto\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\"grid-row\", \"1 / 2\"],\n      [\"grid-column\", \"3 / 4\"],\n    ])\n  ).toEqual([\n    [\"grid-row-start\", \"1\"],\n    [\"grid-row-end\", \"2\"],\n    [\"grid-column-start\", \"3\"],\n    [\"grid-column-end\", \"4\"],\n  ]);\n});\n\ntest(\"expand overflow\", () => {\n  expect(expandShorthands([[\"overflow\", \"hidden\"]])).toEqual([\n    [\"overflow-x\", \"hidden\"],\n    [\"overflow-y\", \"hidden\"],\n  ]);\n  expect(expandShorthands([[\"overflow\", \"hidden auto\"]])).toEqual([\n    [\"overflow-x\", \"hidden\"],\n    [\"overflow-y\", \"auto\"],\n  ]);\n});\n\ntest(\"expand offset\", () => {\n  expect(\n    expandShorthands([[\"offset\", `path(\"M 100 100 L 300 100 L 200 300 z\")`]])\n  ).toEqual([\n    [\"offset-position\", \"normal\"],\n    [\"offset-path\", 'path(\"M 100 100 L 300 100 L 200 300 z\")'],\n    [\"offset-distance\", \"0\"],\n    [\"offset-rotate\", \"auto\"],\n    [\"offset-anchor\", \"auto\"],\n  ]);\n  expect(\n    expandShorthands([[\"offset\", `url(arc.svg) 30deg / 50px 100px`]])\n  ).toEqual([\n    [\"offset-position\", \"normal\"],\n    [\"offset-path\", \"url(arc.svg)\"],\n    [\"offset-distance\", \"0\"],\n    [\"offset-rotate\", \"30deg\"],\n    [\"offset-anchor\", \"50px 100px\"],\n  ]);\n  expect(expandShorthands([[\"offset\", `url(circle.svg) 40%`]])).toEqual([\n    [\"offset-position\", \"normal\"],\n    [\"offset-path\", \"url(circle.svg)\"],\n    [\"offset-distance\", \"40%\"],\n    [\"offset-rotate\", \"auto\"],\n    [\"offset-anchor\", \"auto\"],\n  ]);\n});\n\ntest(\"expand scroll-timeline\", () => {\n  expect(expandShorthands([[\"scroll-timeline\", `none`]])).toEqual([\n    [\"scroll-timeline-name\", \"none\"],\n    [\"scroll-timeline-axis\", \"block\"],\n  ]);\n  expect(expandShorthands([[\"scroll-timeline\", `none inline`]])).toEqual([\n    [\"scroll-timeline-name\", \"none\"],\n    [\"scroll-timeline-axis\", \"inline\"],\n  ]);\n  expect(\n    expandShorthands([[\"scroll-timeline\", `--custom_name_for_timeline inline`]])\n  ).toEqual([\n    [\"scroll-timeline-name\", \"--custom_name_for_timeline\"],\n    [\"scroll-timeline-axis\", \"inline\"],\n  ]);\n  expect(\n    expandShorthands([[\"scroll-timeline\", `none inline, --custom y`]])\n  ).toEqual([\n    [\"scroll-timeline-name\", \"none,--custom\"],\n    [\"scroll-timeline-axis\", \"inline,y\"],\n  ]);\n});\n\ntest(\"expand scroll-margin/scroll-padding\", () => {\n  expect(expandShorthands([[\"scroll-margin\", \"10px\"]])).toEqual([\n    [\"scroll-margin-top\", \"10px\"],\n    [\"scroll-margin-right\", \"10px\"],\n    [\"scroll-margin-bottom\", \"10px\"],\n    [\"scroll-margin-left\", \"10px\"],\n  ]);\n  expect(expandShorthands([[\"scroll-margin-block\", \"10px\"]])).toEqual([\n    [\"scroll-margin-block-start\", \"10px\"],\n    [\"scroll-margin-block-end\", \"10px\"],\n  ]);\n  expect(expandShorthands([[\"scroll-margin-inline\", \"10px\"]])).toEqual([\n    [\"scroll-margin-inline-start\", \"10px\"],\n    [\"scroll-margin-inline-end\", \"10px\"],\n  ]);\n  expect(expandShorthands([[\"scroll-padding\", \"10px\"]])).toEqual([\n    [\"scroll-padding-top\", \"10px\"],\n    [\"scroll-padding-right\", \"10px\"],\n    [\"scroll-padding-bottom\", \"10px\"],\n    [\"scroll-padding-left\", \"10px\"],\n  ]);\n  expect(expandShorthands([[\"scroll-padding-block\", \"10px\"]])).toEqual([\n    [\"scroll-padding-block-start\", \"10px\"],\n    [\"scroll-padding-block-end\", \"10px\"],\n  ]);\n  expect(expandShorthands([[\"scroll-padding-inline\", \"10px\"]])).toEqual([\n    [\"scroll-padding-inline-start\", \"10px\"],\n    [\"scroll-padding-inline-end\", \"10px\"],\n  ]);\n});\n\ntest(\"expand grid-template\", () => {\n  expect(expandShorthands([[\"grid-template\", `none`]])).toEqual([\n    [\"grid-template-areas\", \"none\"],\n    [\"grid-template-rows\", \"none\"],\n    [\"grid-template-columns\", \"none\"],\n  ]);\n  expect(expandShorthands([[\"grid-template\", `100px 1fr / 50px 1fr`]])).toEqual(\n    [\n      [\"grid-template-areas\", \"none\"],\n      [\"grid-template-rows\", \"100px 1fr\"],\n      [\"grid-template-columns\", \"50px 1fr\"],\n    ]\n  );\n  expect(\n    expandShorthands([\n      [\n        \"grid-template\",\n        `\n        [header-top] \"a a a\" [header-bottom]\n        [main-top] \"b b b\" 1fr [main-bottom]\n        / auto 1fr auto\n        `,\n      ],\n    ])\n  ).toEqual([\n    [\"grid-template-areas\", `\"a a a\"\"b b b\"`],\n    [\n      \"grid-template-rows\",\n      \"[header-top][header-bottom][main-top]1fr[main-bottom]\",\n    ],\n    [\"grid-template-columns\", \"auto 1fr auto\"],\n  ]);\n});\n\ntest(\"expand grid\", () => {\n  expect(expandShorthands([[\"grid\", `none`]])).toEqual([\n    [\"grid-template-areas\", \"none\"],\n    [\"grid-template-rows\", \"none\"],\n    [\"grid-template-columns\", \"none\"],\n    [\"grid-auto-flow.\", \"row\"],\n    [\"grid-auto-rows\", \"auto\"],\n    [\"grid-auto-columns\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"grid\", `100px 1fr / 50px 1fr`]])).toEqual([\n    [\"grid-template-areas\", \"none\"],\n    [\"grid-template-rows\", \"100px 1fr\"],\n    [\"grid-template-columns\", \"50px 1fr\"],\n    [\"grid-auto-flow.\", \"row\"],\n    [\"grid-auto-rows\", \"auto\"],\n    [\"grid-auto-columns\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"grid\", `200px / auto-flow`]])).toEqual([\n    [\"grid-template-areas\", \"none\"],\n    [\"grid-template-rows\", \"200px\"],\n    [\"grid-template-columns\", \"none\"],\n    [\"grid-auto-flow.\", \"column\"],\n    [\"grid-auto-rows\", \"auto\"],\n    [\"grid-auto-columns\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"grid\", `auto-flow dense / 30%`]])).toEqual([\n    [\"grid-template-areas\", \"none\"],\n    [\"grid-template-rows\", \"none\"],\n    [\"grid-template-columns\", \"30%\"],\n    [\"grid-auto-flow.\", \"row dense\"],\n    [\"grid-auto-rows\", \"auto\"],\n    [\"grid-auto-columns\", \"auto\"],\n  ]);\n});\n\ntest(\"expand container\", () => {\n  expect(expandShorthands([[\"container\", \"my-layout\"]])).toEqual([\n    [\"container-name\", \"my-layout\"],\n    [\"container-type\", \"normal\"],\n  ]);\n  expect(expandShorthands([[\"container\", \"my-layout / size\"]])).toEqual([\n    [\"container-name\", \"my-layout\"],\n    [\"container-type\", \"size\"],\n  ]);\n});\n\ntest(\"expand contain-intrinsic-size\", () => {\n  expect(expandShorthands([[\"contain-intrinsic-size\", \"auto 300px\"]])).toEqual([\n    [\"contain-intrinsic-width\", \"auto 300px\"],\n    [\"contain-intrinsic-height\", \"auto 300px\"],\n  ]);\n  expect(expandShorthands([[\"contain-intrinsic-size\", \"1000px\"]])).toEqual([\n    [\"contain-intrinsic-width\", \"1000px\"],\n    [\"contain-intrinsic-height\", \"1000px\"],\n  ]);\n  expect(\n    expandShorthands([[\"contain-intrinsic-size\", \"1000px 1.5em\"]])\n  ).toEqual([\n    [\"contain-intrinsic-width\", \"1000px\"],\n    [\"contain-intrinsic-height\", \"1.5em\"],\n  ]);\n  expect(\n    expandShorthands([[\"contain-intrinsic-size\", \"auto 300px auto 4rem\"]])\n  ).toEqual([\n    [\"contain-intrinsic-width\", \"auto 300px\"],\n    [\"contain-intrinsic-height\", \"auto 4rem\"],\n  ]);\n});\n\ntest(\"expand white-space\", () => {\n  expect(expandShorthands([[\"white-space\", \"normal\"]])).toEqual([\n    [\"white-space-collapse\", \"collapse\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"pre\"]])).toEqual([\n    [\"white-space-collapse\", \"preserve\"],\n    [\"text-wrap-mode\", \"nowrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"pre-wrap\"]])).toEqual([\n    [\"white-space-collapse\", \"preserve\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"pre-line\"]])).toEqual([\n    [\"white-space-collapse\", \"preserve-breaks\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  // white-space-collapse values\n  expect(expandShorthands([[\"white-space\", \"collapse\"]])).toEqual([\n    [\"white-space-collapse\", \"collapse\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"preserve\"]])).toEqual([\n    [\"white-space-collapse\", \"preserve\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"preserve-breaks\"]])).toEqual([\n    [\"white-space-collapse\", \"preserve-breaks\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"preserve-spaces\"]])).toEqual([\n    [\"white-space-collapse\", \"preserve-spaces\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"break-spaces\"]])).toEqual([\n    [\"white-space-collapse\", \"break-spaces\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  // text-wrap-mode values\n  expect(expandShorthands([[\"white-space\", \"wrap\"]])).toEqual([\n    [\"white-space-collapse\", \"collapse\"],\n    [\"text-wrap-mode\", \"wrap\"],\n  ]);\n  expect(expandShorthands([[\"white-space\", \"nowrap\"]])).toEqual([\n    [\"white-space-collapse\", \"collapse\"],\n    [\"text-wrap-mode\", \"nowrap\"],\n  ]);\n});\n\ntest(\"expand text-wrap\", () => {\n  // text-wrap-mode values\n  expect(expandShorthands([[\"text-wrap\", \"wrap\"]])).toEqual([\n    [\"text-wrap-mode\", \"wrap\"],\n    [\"text-wrap-style\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"text-wrap\", \"nowrap\"]])).toEqual([\n    [\"text-wrap-mode\", \"nowrap\"],\n    [\"text-wrap-style\", \"auto\"],\n  ]);\n  // text-wrap-style values\n  expect(expandShorthands([[\"text-wrap\", \"balance\"]])).toEqual([\n    [\"text-wrap-mode\", \"wrap\"],\n    [\"text-wrap-style\", \"balance\"],\n  ]);\n  expect(expandShorthands([[\"text-wrap\", \"stable\"]])).toEqual([\n    [\"text-wrap-mode\", \"wrap\"],\n    [\"text-wrap-style\", \"stable\"],\n  ]);\n  expect(expandShorthands([[\"text-wrap\", \"pretty\"]])).toEqual([\n    [\"text-wrap-mode\", \"wrap\"],\n    [\"text-wrap-style\", \"pretty\"],\n  ]);\n});\n\ntest(\"expand background-position\", () => {\n  expect(expandShorthands([[\"background-position\", \"initial\"]])).toEqual([\n    [\"background-position-x\", \"initial\"],\n    [\"background-position-y\", \"initial\"],\n  ]);\n  expect(expandShorthands([[\"background-position\", \"0\"]])).toEqual([\n    [\"background-position-x\", \"0\"],\n    [\"background-position-y\", \"center\"],\n  ]);\n  expect(expandShorthands([[\"background-position\", \"top left\"]])).toEqual([\n    [\"background-position-x\", \"left\"],\n    [\"background-position-y\", \"top\"],\n  ]);\n  expect(expandShorthands([[\"background-position\", \"right bottom\"]])).toEqual([\n    [\"background-position-x\", \"right\"],\n    [\"background-position-y\", \"bottom\"],\n  ]);\n  expect(expandShorthands([[\"background-position\", \"25% 75%\"]])).toEqual([\n    [\"background-position-x\", \"25%\"],\n    [\"background-position-y\", \"75%\"],\n  ]);\n  expect(\n    expandShorthands([[\"background-position\", \"bottom 10px right\"]])\n  ).toEqual([\n    [\"background-position-x\", \"right\"],\n    [\"background-position-y\", \"bottom 10px\"],\n  ]);\n  expect(\n    expandShorthands([[\"background-position\", \"center right 10px\"]])\n  ).toEqual([\n    [\"background-position-x\", \"right 10px\"],\n    [\"background-position-y\", \"center\"],\n  ]);\n  expect(\n    expandShorthands([[\"background-position\", \"center bottom 10px\"]])\n  ).toEqual([\n    [\"background-position-x\", \"center\"],\n    [\"background-position-y\", \"bottom 10px\"],\n  ]);\n  expect(expandShorthands([[\"background-position\", \"top right 10px\"]])).toEqual(\n    [\n      [\"background-position-x\", \"right 10px\"],\n      [\"background-position-y\", \"top\"],\n    ]\n  );\n  expect(\n    expandShorthands([[\"background-position\", \"bottom 10px right 20px\"]])\n  ).toEqual([\n    [\"background-position-x\", \"right 20px\"],\n    [\"background-position-y\", \"bottom 10px\"],\n  ]);\n  expect(expandShorthands([[\"background-position\", \"0 10px, center\"]])).toEqual(\n    [\n      [\"background-position-x\", \"0,center\"],\n      [\"background-position-y\", \"10px,center\"],\n    ]\n  );\n});\n\ntest(\"expand background\", () => {\n  expect(expandShorthands([[\"background\", `none`]])).toEqual([\n    [\"background-image\", \"none\"],\n    [\"background-position-x\", \"0%\"],\n    [\"background-position-y\", \"0%\"],\n    [\"background-size\", \"auto auto\"],\n    [\"background-repeat\", \"repeat\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"padding-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"transparent\"],\n  ]);\n  expect(expandShorthands([[\"background\", `green`]])).toEqual([\n    [\"background-image\", \"none\"],\n    [\"background-position-x\", \"0%\"],\n    [\"background-position-y\", \"0%\"],\n    [\"background-size\", \"auto auto\"],\n    [\"background-repeat\", \"repeat\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"padding-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"green\"],\n  ]);\n  expect(expandShorthands([[\"background\", `transparent`]])).toEqual([\n    [\"background-image\", \"none\"],\n    [\"background-position-x\", \"0%\"],\n    [\"background-position-y\", \"0%\"],\n    [\"background-size\", \"auto auto\"],\n    [\"background-repeat\", \"repeat\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"padding-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"transparent\"],\n  ]);\n  expect(\n    expandShorthands([[\"background\", `url(\"test.jpg\") repeat-y`]])\n  ).toEqual([\n    [\"background-image\", \"url(test.jpg)\"],\n    [\"background-position-x\", \"0%\"],\n    [\"background-position-y\", \"0%\"],\n    [\"background-size\", \"auto auto\"],\n    [\"background-repeat\", \"repeat-y\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"padding-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"transparent\"],\n  ]);\n  expect(expandShorthands([[\"background\", `border-box red`]])).toEqual([\n    [\"background-image\", \"none\"],\n    [\"background-position-x\", \"0%\"],\n    [\"background-position-y\", \"0%\"],\n    [\"background-size\", \"auto auto\"],\n    [\"background-repeat\", \"repeat\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"border-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"red\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\"background\", `no-repeat center/80% url(\"../img/image.png\")`],\n    ])\n  ).toEqual([\n    [\"background-image\", \"url(../img/image.png)\"],\n    [\"background-position-x\", \"center\"],\n    [\"background-position-y\", \"center\"],\n    [\"background-size\", \"80%\"],\n    [\"background-repeat\", \"no-repeat\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"padding-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"transparent\"],\n  ]);\n  expect(\n    expandShorthands([\n      [\n        \"background\",\n        `repeat scroll 0% 0% / auto padding-box border-box none transparent`,\n      ],\n    ])\n  ).toEqual([\n    [\"background-image\", \"none\"],\n    [\"background-position-x\", \"0%\"],\n    [\"background-position-y\", \"0%\"],\n    [\"background-size\", \"auto\"],\n    [\"background-repeat\", \"repeat\"],\n    [\"background-attachment\", \"scroll\"],\n    [\"background-origin\", \"padding-box\"],\n    [\"background-clip\", \"border-box\"],\n    [\"background-color\", \"transparent\"],\n  ]);\n});\n\ntest(\"expand caret\", () => {\n  expect(expandShorthands([[\"caret\", \"red\"]])).toEqual([\n    [\"caret-color\", \"red\"],\n    [\"caret-shape\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"caret\", \"block\"]])).toEqual([\n    [\"caret-color\", \"auto\"],\n    [\"caret-shape\", \"block\"],\n  ]);\n  expect(expandShorthands([[\"caret\", \"block red\"]])).toEqual([\n    [\"caret-color\", \"red\"],\n    [\"caret-shape\", \"block\"],\n  ]);\n});\n\ntest(\"expand overscroll-behavior\", () => {\n  expect(expandShorthands([[\"overscroll-behavior\", \"auto\"]])).toEqual([\n    [\"overscroll-behavior-x\", \"auto\"],\n    [\"overscroll-behavior-y\", \"auto\"],\n  ]);\n  expect(expandShorthands([[\"overscroll-behavior\", \"contain\"]])).toEqual([\n    [\"overscroll-behavior-x\", \"contain\"],\n    [\"overscroll-behavior-y\", \"contain\"],\n  ]);\n  expect(expandShorthands([[\"overscroll-behavior\", \"contain none\"]])).toEqual([\n    [\"overscroll-behavior-x\", \"contain\"],\n    [\"overscroll-behavior-y\", \"none\"],\n  ]);\n});\n\ntest(\"expand position-try\", () => {\n  expect(expandShorthands([[\"position-try\", \"none\"]])).toEqual([\n    [\"position-try-order\", \"normal\"],\n    [\"position-try-options\", \"none\"],\n  ]);\n  expect(expandShorthands([[\"position-try\", \"most-width none\"]])).toEqual([\n    [\"position-try-order\", \"most-width\"],\n    [\"position-try-options\", \"none\"],\n  ]);\n  expect(expandShorthands([[\"position-try\", \"--dashed-ident\"]])).toEqual([\n    [\"position-try-order\", \"normal\"],\n    [\"position-try-options\", \"--dashed-ident\"],\n  ]);\n});\n\ntest(\"replace empty value with unset\", () => {\n  expect(expandShorthands([[\"color\", \"\"]])).toEqual([[\"color\", \"unset\"]]);\n  expect(expandShorthands([[\"transition\", \"\"]])).toEqual([\n    [\"transition-property\", \"unset\"],\n    [\"transition-duration\", \"unset\"],\n    [\"transition-timing-function\", \"unset\"],\n    [\"transition-delay\", \"unset\"],\n    [\"transition-behavior\", \"unset\"],\n  ]);\n});\n\ntest(\"does not fail on empty value\", () => {\n  expect(() => expandShorthands([[\"transition\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"font\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"font-synthesis\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"font-variant\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"text-decoration\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"text-emphasis\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-width\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-style\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-color\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-inline-width\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-inline-style\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-inline-color\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-block-width\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-block-style\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-block-color\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-radius\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"border-image\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"outline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"mask\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"mask-border\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"margin\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"padding\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"margin-inline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"margin-block\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"padding-inline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"padding-block\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"inset\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"inset-inline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"inset-block\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"gap\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-gap\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-row-gap\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-column-gap\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-area\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-row\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-column\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid-template\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"grid\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"flex\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"flex-flow\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"place-content\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"place-items\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"place-self\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"columns\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"column-rule\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"list-style\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"animation\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"animation-range\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"transition\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"offset\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-timeline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"view-timeline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-margin\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-padding\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-margin-inline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-margin-block\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-padding-inline\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"scroll-padding-block\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"overflow\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"container\", \"\"]])).not.toThrow();\n  expect(() =>\n    expandShorthands([[\"contain-intrinsic-size\", \"\"]])\n  ).not.toThrow();\n  expect(() => expandShorthands([[\"white-space\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"text-wrap\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"caret\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"background-position\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"background\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"overscroll-behavior\", \"\"]])).not.toThrow();\n  expect(() => expandShorthands([[\"position-try\", \"\"]])).not.toThrow();\n});\n"
  },
  {
    "path": "packages/css-data/src/shorthands.ts",
    "content": "import { cssWideKeywords, type CssProperty } from \"@webstudio-is/css-engine\";\nimport {\n  List,\n  parse,\n  lexer,\n  generate,\n  type CssNode,\n  type Value,\n} from \"css-tree\";\nimport warnOnce from \"warn-once\";\n\nconst cssWideKeywordsSyntax = Array.from(cssWideKeywords).join(\" | \");\n\nconst createValueNode = (data?: CssNode[]): Value => ({\n  type: \"Value\",\n  loc: null,\n  children: new List<CssNode>().fromArray(data ?? []),\n});\n\nconst createNumber = (value: string): Value => {\n  const list = new List<CssNode>();\n  list.appendData({\n    type: \"Number\",\n    value,\n    loc: null,\n  });\n  return {\n    type: \"Value\",\n    loc: null,\n    children: list,\n  };\n};\n\nconst createDimension = (value: string, unit: string): Value => {\n  const list = new List<CssNode>();\n  list.appendData({\n    type: \"Dimension\",\n    value,\n    unit,\n    loc: null,\n  });\n  return {\n    type: \"Value\",\n    loc: null,\n    children: list,\n  };\n};\n\nconst createIdentifier = (name: string): Value => {\n  const list = new List<CssNode>();\n  list.appendData({\n    type: \"Identifier\",\n    name,\n    loc: null,\n  });\n  return {\n    type: \"Value\",\n    loc: null,\n    children: list,\n  };\n};\n\nconst createInitialNode = () => createIdentifier(\"initial\");\n\nconst getValueList = (value: CssNode): CssNode[] => {\n  const children = \"children\" in value ? value.children?.toArray() : undefined;\n  return children ?? [value];\n};\n\nconst splitByOperator = (node: CssNode, operator: string) => {\n  const list = getValueList(node);\n  const lists: Array<CssNode[]> = [[]];\n  for (const node of list) {\n    if (node.type === \"Operator\" && node.value === operator) {\n      lists.push([]);\n    } else {\n      lists.at(-1)?.push(node);\n    }\n  }\n  return lists\n    .filter((list) => list.length > 0)\n    .map((list) => createValueNode(list));\n};\n\nconst joinByOperator = (list: List<CssNode> | CssNode[], operator: string) => {\n  const joined: CssNode[] = [];\n  for (const node of list) {\n    if (joined.length > 0) {\n      joined.push({ type: \"Operator\", value: operator, loc: null });\n    }\n    joined.push(...getValueList(node));\n  }\n  return joined;\n};\n\nconst parseRepeated = (\n  value: CssNode,\n  parseSingle: (single: Value) => Value[]\n): Value[] => {\n  const values: CssNode[][] = [];\n  for (const single of splitByOperator(value, \",\")) {\n    if (single === undefined) {\n      continue;\n    }\n    const singleValues = parseSingle(single);\n    singleValues.forEach((singleValue, index) => {\n      values[index] = values[index] ?? [];\n      values[index].push(singleValue);\n    });\n  }\n  return values.map((list) => createValueNode(joinByOperator(list, \",\")));\n};\n\n/**\n * Match the list of specified syntaxes with nodes\n * Matches can be placed in different order than the list\n * All specified matches are optional\n * Value Definition Syntax use <Type> || <Type> operator for describe this\n */\nconst parseUnordered = (syntaxes: string[], value: CssNode) => {\n  const matched = new Map<number, Value>();\n  const nodes = getValueList(value);\n  let cursor = 0;\n  while (matched.size < syntaxes.length && cursor < nodes.length) {\n    let newCursor = cursor;\n    for (let syntaxIndex = 0; syntaxIndex < syntaxes.length; syntaxIndex += 1) {\n      if (matched.has(syntaxIndex)) {\n        continue;\n      }\n      const syntax = syntaxes[syntaxIndex];\n      const buffer = [];\n      let value: undefined | Value;\n      for (let nodeIndex = cursor; nodeIndex < nodes.length; nodeIndex += 1) {\n        const node = nodes[nodeIndex];\n        buffer.push(node);\n        const newValue = createValueNode(buffer);\n        if (lexer.match(syntax, newValue).matched) {\n          value = newValue;\n          newCursor = nodeIndex + 1;\n        }\n      }\n      if (value) {\n        matched.set(syntaxIndex, value);\n        break;\n      }\n    }\n    // last pass is the same as previous one\n    // which means infinite loop detected\n    if (cursor === newCursor) {\n      break;\n    }\n    cursor = newCursor;\n  }\n  return [\n    ...syntaxes.map((_syntax, index) => matched.get(index)),\n    createValueNode(nodes.slice(cursor)),\n  ];\n};\n\n/**\n *\n * border = <line-width> || <line-style> || <color>\n *\n * should also reset border-image but would bloat\n * resulting data for very rare usecase\n *\n */\nconst expandBorder = function* (property: string, value: CssNode) {\n  switch (property) {\n    case \"border\":\n    case \"border-inline\":\n    case \"border-inline-start\":\n    case \"border-inline-end\":\n    case \"border-block\":\n    case \"border-block-start\":\n    case \"border-block-end\":\n    case \"border-top\":\n    case \"border-right\":\n    case \"border-bottom\":\n    case \"border-left\": {\n      const [width, style, color] = parseUnordered(\n        [\"<line-width>\", \"<line-style>\", \"<color>\"],\n        value\n      );\n      yield [`${property}-width`, width ?? createIdentifier(\"medium\")] as const;\n      yield [`${property}-style`, style ?? createIdentifier(\"none\")] as const;\n      yield [\n        `${property}-color`,\n        color ?? createIdentifier(\"currentcolor\"),\n      ] as const;\n      break;\n    }\n    default:\n      yield [property, value] as const;\n  }\n};\n\ntype GetProperty = (edge: string) => string;\n\nconst expandBox = function* (getProperty: GetProperty, value: CssNode) {\n  const [top, right, bottom, left] = getValueList(value);\n  yield [getProperty(\"top\"), top] as const;\n  yield [getProperty(\"right\"), right ?? top] as const;\n  yield [getProperty(\"bottom\"), bottom ?? top] as const;\n  yield [getProperty(\"left\"), left ?? right ?? top] as const;\n};\n\nconst expandLogical = function* (getProperty: GetProperty, value: CssNode) {\n  const [start, end] = getValueList(value);\n  yield [getProperty(\"start\"), start] as const;\n  yield [getProperty(\"end\"), end ?? start] as const;\n};\n\n/**\n *\n * border-radius = <length-percentage [0,∞]>{1,4} [ / <length-percentage [0,∞]>{1,4} ]?\n *\n */\nconst expandBorderRadius = function* (value: CssNode) {\n  const firstRadius = [];\n  const secondRadius = [];\n  let hasSecondRadius = false;\n  for (const node of getValueList(value)) {\n    if (node.type === \"Operator\" && node.value === \"/\") {\n      hasSecondRadius = true;\n    } else if (hasSecondRadius) {\n      secondRadius.push(node);\n    } else {\n      firstRadius.push(node);\n    }\n  }\n  const topLeft = createValueNode();\n  const topRight = createValueNode();\n  const bottomRight = createValueNode();\n  const bottomLeft = createValueNode();\n  // add first radius\n  const [firstTopLeft, firstTopRight, firstBottomRight, firstBottomLeft] =\n    firstRadius;\n  topLeft.children.appendData(firstTopLeft);\n  topRight.children.appendData(firstTopRight ?? firstTopLeft);\n  bottomRight.children.appendData(firstBottomRight ?? firstTopLeft);\n  bottomLeft.children.appendData(\n    firstBottomLeft ?? firstTopRight ?? firstTopLeft\n  );\n  // add second radius if specified\n  const [secondTopLeft, secondTopRight, secondBottomRight, secondBottomLeft] =\n    secondRadius;\n  if (hasSecondRadius) {\n    topLeft.children.appendData(secondTopLeft);\n    topRight.children.appendData(secondTopRight ?? secondTopLeft);\n    bottomRight.children.appendData(secondBottomRight ?? secondTopLeft);\n    bottomLeft.children.appendData(\n      secondBottomLeft ?? secondTopRight ?? secondTopLeft\n    );\n  }\n  yield [\"border-top-left-radius\", topLeft] as const;\n  yield [\"border-top-right-radius\", topRight] as const;\n  yield [\"border-bottom-right-radius\", bottomRight] as const;\n  yield [\"border-bottom-left-radius\", bottomLeft] as const;\n};\n\n/**\n *\n * border-image =\n *   <'border-image-source'>\n *   || <'border-image-slice'> [ / <'border-image-width'> | / <'border-image-width'>? / <'border-image-outset'> ]?\n *   || <'border-image-repeat'>\n * <border-image-source> = none | <image>\n * <border-image-slice> = [ <number [0,∞]> | <percentage [0,∞]> ]{1,4} && fill?\n * <border-image-width> = [ <length-percentage [0,∞]> | <number [0,∞]> | auto ]{1,4}\n * <border-image-outset> = [ <length [0,∞]> | <number [0,∞]> ]{1,4}\n * <border-image-repeat> = [ stretch | repeat | round | space ]{1,2}\n *\n */\nconst expandBorderImage = function* (value: CssNode) {\n  const [source, config, repeat] = parseUnordered(\n    [\n      \"<'border-image-source'>\",\n      \"<'border-image-slice'> [ / <'border-image-width'> | / <'border-image-width'>? / <'border-image-outset'> ]?\",\n      \"<'border-image-repeat'>\",\n    ],\n    value\n  );\n  let slice: undefined | CssNode;\n  let width: undefined | CssNode;\n  let outset: undefined | CssNode;\n  if (config) {\n    [slice, width, outset] = splitByOperator(config, \"/\");\n  }\n  yield [\"border-image-source\", source ?? createInitialNode()] as const;\n  yield [\"border-image-slice\", slice ?? createInitialNode()] as const;\n  yield [\"border-image-width\", width ?? createInitialNode()] as const;\n  yield [\"border-image-outset\", outset ?? createInitialNode()] as const;\n  yield [\"border-image-repeat\", repeat ?? createInitialNode()] as const;\n};\n\n/**\n *\n * font =\n *   [ <'font-style'> || <font-variant-css21> || <'font-weight'> || <'font-stretch'> ]?\n *   <'font-size'> [ / <'line-height'> ]? <'font-family'>\n *\n */\nconst expandFont = function* (value: CssNode) {\n  const [fontStyle, fontVariant, fontWeight, fontWidth, config] =\n    parseUnordered(\n      [\n        \"<'font-style'>\",\n        \"<font-variant-css21>\",\n        \"<'font-weight'>\",\n        \"<'font-stretch'>\",\n      ],\n      value\n    );\n  let fontSize: CssNode = createInitialNode();\n  let lineHeight: CssNode = createInitialNode();\n  let fontFamily: CssNode = createInitialNode();\n  if (config) {\n    if (\n      lexer.match(\"<'font-size'> / <'line-height'> <'font-family'>#\", config)\n        .matched\n    ) {\n      const [fontSizeNode, _slashNode, lineHeightNode, ...fontFamilyNodes] =\n        getValueList(config);\n      fontSize = fontSizeNode;\n      lineHeight = lineHeightNode;\n      fontFamily = createValueNode(fontFamilyNodes);\n    } else {\n      const [fontSizeNode, ...fontFamilyNodes] = getValueList(config);\n      fontSize = fontSizeNode;\n      fontFamily = createValueNode(fontFamilyNodes);\n    }\n  }\n  yield [\"font-style\", fontStyle ?? createInitialNode()] as const;\n  yield [\"font-variant-caps\", fontVariant ?? createInitialNode()] as const;\n  yield [\"font-weight\", fontWeight ?? createInitialNode()] as const;\n  yield [\"font-stretch\", fontWidth ?? createInitialNode()] as const;\n  yield [\"font-size\", fontSize] as const;\n  yield [\"line-height\", lineHeight] as const;\n  yield [\"font-family\", fontFamily] as const;\n};\n\n/**\n *\n * font-synthesis = none | [ weight || style || small-caps || position ]\n *\n */\nconst expandFontSynthesis = function* (value: CssNode) {\n  const [weight, style, smallCaps, position] = parseUnordered(\n    [\"weight\", \"style\", \"small-caps\", \"position\"],\n    value\n  );\n  const auto = createIdentifier(\"auto\");\n  const none = createIdentifier(\"none\");\n  yield [\"font-synthesis-weight\", weight ? auto : none] as const;\n  yield [\"font-synthesis-style\", style ? auto : none] as const;\n  yield [\"font-synthesis-small-caps\", smallCaps ? auto : none] as const;\n  yield [\"font-synthesis-position\", position ? auto : none] as const;\n};\n\n/**\n *\n * font-variant =\n *   normal |\n *   none |\n *   [\n *     [ <common-lig-values> || <discretionary-lig-values> || <historical-lig-values> || <contextual-alt-values> ] ||\n *     [ small-caps | all-small-caps | petite-caps | all-petite-caps | unicase | titling-caps ] ||\n *     [ <numeric-figure-values> || <numeric-spacing-values> || <numeric-fraction-values> || ordinal || slashed-zero ] ||\n *     [ <east-asian-variant-values> || <east-asian-width-values> || ruby ] ||\n *     [ sub | super ] ||\n *     [ text | emoji | unicode ]\n *   ]\n *\n */\nconst expandFontVariant = function* (value: CssNode) {\n  const [ligatures, caps, alternates, numeric, eastAsian, position, emoji] =\n    parseUnordered(\n      [\n        \"[ normal | none | <common-lig-values> || <discretionary-lig-values> || <historical-lig-values> || <contextual-alt-values> ]\",\n        \"[ small-caps | all-small-caps | petite-caps | all-petite-caps | unicase | titling-caps ]\",\n        \"[ stylistic( <feature-value-name> ) || historical-forms || styleset( <feature-value-name># ) || character-variant( <feature-value-name># ) || swash( <feature-value-name> ) || ornaments( <feature-value-name> ) || annotation( <feature-value-name> ) ]\",\n        \"[ <numeric-figure-values> || <numeric-spacing-values> || <numeric-fraction-values> || ordinal || slashed-zero ]\",\n        \"[ <east-asian-variant-values> || <east-asian-width-values> || ruby ]\",\n        \"[ sub | super ]\",\n        \"[ text | emoji | unicode ]\",\n      ],\n      value\n    );\n  const normal = createIdentifier(\"normal\");\n  yield [\"font-variant-ligatures\", ligatures ?? normal] as const;\n  yield [\"font-variant-caps\", caps ?? normal] as const;\n  yield [\"font-variant-alternates\", alternates ?? normal] as const;\n  yield [\"font-variant-numeric\", numeric ?? normal] as const;\n  yield [\"font-variant-east-asian\", eastAsian ?? normal] as const;\n  yield [\"font-variant-position\", position ?? normal] as const;\n  yield [\"font-variant-emoji\", emoji ?? normal] as const;\n};\n\nconst expandFlex = function* (value: CssNode) {\n  const zero = createNumber(\"0\");\n  const one = createNumber(\"1\");\n  const auto = createIdentifier(\"auto\");\n  let grow: undefined | Value;\n  let shrink: undefined | Value;\n  let basis: undefined | Value;\n  if (lexer.match(\"initial\", value).matched) {\n    [grow, shrink, basis] = [zero, one, auto];\n  } else if (lexer.match(\"auto\", value).matched) {\n    [grow, shrink, basis] = [one, one, auto];\n  } else if (lexer.match(\"none\", value).matched) {\n    [grow, shrink, basis] = [zero, zero, auto];\n  } else {\n    [grow, shrink, basis] = parseUnordered(\n      [\"<'flex-grow'>\", \"<'flex-shrink'>\", \"<'flex-basis'>\"],\n      value\n    );\n  }\n  yield [\"flex-grow\", grow ?? one] as const;\n  yield [\"flex-shrink\", shrink ?? one] as const;\n  yield [\"flex-basis\", basis ?? zero] as const;\n};\n\n/**\n *\n * animation = <single-animation>#\n *\n * <single-animation> =\n *   <time [0s,∞]> ||\n *   <easing-function> ||\n *   <time> ||\n *   <single-animation-iteration-count> ||\n *   <single-animation-direction> ||\n *   <single-animation-fill-mode> ||\n *   <single-animation-play-state> ||\n *   [ none | <keyframes-name> ]\n *\n */\nconst expandAnimation = function* (value: CssNode) {\n  const [\n    duration,\n    timingFunction,\n    delay,\n    iterationCount,\n    direction,\n    fillMode,\n    playState,\n    name,\n  ] = parseRepeated(value, (single) => {\n    const [\n      duration,\n      easing,\n      delay,\n      iterationCount,\n      direction,\n      fillMode,\n      playState,\n      name,\n    ] = parseUnordered(\n      [\n        \"<time [0s,∞]>\",\n        \"<easing-function>\",\n        \"<time>\",\n        \"<single-animation-iteration-count>\",\n        \"<single-animation-direction>\",\n        \"<single-animation-fill-mode>\",\n        \"<single-animation-play-state>\",\n        \"[ none | <keyframes-name> ]\",\n      ],\n      single\n    );\n    return [\n      duration ?? createDimension(\"0\", \"s\"),\n      easing ?? createIdentifier(\"ease\"),\n      delay ?? createDimension(\"0\", \"s\"),\n      iterationCount ?? createNumber(\"1\"),\n      direction ?? createIdentifier(\"normal\"),\n      fillMode ?? createIdentifier(\"none\"),\n      playState ?? createIdentifier(\"running\"),\n      name ?? createIdentifier(\"none\"),\n    ];\n  });\n  yield [\"animation-duration\", duration] as const;\n  yield [\"animation-timing-function\", timingFunction] as const;\n  yield [\"animation-delay\", delay] as const;\n  yield [\"animation-iteration-count\", iterationCount] as const;\n  yield [\"animation-direction\", direction] as const;\n  yield [\"animation-fill-mode\", fillMode] as const;\n  yield [\"animation-play-state\", playState] as const;\n  yield [\"animation-name\", name] as const;\n  // reset with animation shorthand but cannot be set with it\n  yield [\"animation-timeline\", createIdentifier(\"auto\")] as const;\n  yield [\"animation-range-start\", createIdentifier(\"normal\")] as const;\n  yield [\"animation-range-end\", createIdentifier(\"normal\")] as const;\n};\n\n/**\n *\n * animation-range = [ <'animation-range-start'> <'animation-range-end'>? ]#\n *\n * <animation-range-start> =\n *   [ normal | <length-percentage> | <timeline-range-name> <length-percentage>? ]#\n *\n * <animation-range-end> =\n *   [ normal | <length-percentage> | <timeline-range-name> <length-percentage>? ]#\n *\n */\nconst expandAnimationRange = function* (value: CssNode) {\n  const [start, end] = parseRepeated(value, (single) => {\n    const [start, end] = parseUnordered(\n      [\n        `normal | <length-percentage> | <ident> <length-percentage>?`,\n        `normal | <length-percentage> | <ident> <length-percentage>?`,\n      ],\n      single\n    );\n    return [\n      start ?? createIdentifier(\"normal\"),\n      end ?? createIdentifier(\"normal\"),\n    ];\n  });\n  yield [\"animation-range-start\", start] as const;\n  yield [\"animation-range-end\", end] as const;\n};\n\n/**\n *\n * transition = <single-transition>#\n *\n * <single-transition> =\n *   [ none | <single-transition-property> ] ||\n *   <time> ||\n *   <easing-function> ||\n *   <time>\n *\n */\nconst expandTransition = function* (value: CssNode) {\n  const [property, duration, timingFunction, delay, behavior] = parseRepeated(\n    value,\n    (single) => {\n      const [property, duration, easing, delay, behavior] = parseUnordered(\n        [\n          \"[ none | <single-transition-property> ]\",\n          \"<time>\",\n          \"<easing-function>\",\n          \"<time>\",\n          \"<transition-behavior-value>\",\n        ],\n        single\n      );\n      return [\n        property ?? createIdentifier(\"all\"),\n        duration ?? createDimension(\"0\", \"s\"),\n        easing ?? createIdentifier(\"ease\"),\n        delay ?? createDimension(\"0\", \"s\"),\n        behavior ?? createIdentifier(\"normal\"),\n      ];\n    }\n  );\n  yield [\"transition-property\", property] as const;\n  yield [\"transition-duration\", duration] as const;\n  yield [\"transition-timing-function\", timingFunction] as const;\n  yield [\"transition-delay\", delay] as const;\n  yield [\"transition-behavior\", behavior] as const;\n};\n\n/**\n *\n * mask = <mask-layer>#\n *\n * <mask-layer> =\n *   <mask-reference> ||\n *   <position> [ / <bg-size> ]? ||\n *   <repeat-style> ||\n *   <geometry-box> ||\n *   [ <geometry-box> | no-clip ] ||\n *   <compositing-operator> ||\n *   <masking-mode>\n *\n * should also reset mask-border but would bloat\n * resulting data for very rare usecase\n *\n */\nconst expandMask = function* (value: CssNode) {\n  const [image, position, size, repeat, origin, clip, composite, mode] =\n    parseRepeated(value, (single) => {\n      const [\n        reference,\n        positionAndSize,\n        repeatStyle,\n        origin,\n        clip,\n        compositingOperator,\n        mode,\n      ] = parseUnordered(\n        [\n          \"<mask-reference>\",\n          \"<position> [ / <bg-size> ]?\",\n          \"<repeat-style>\",\n          \"<geometry-box>\",\n          \"[ <geometry-box> | no-clip ]\",\n          \"<compositing-operator>\",\n          \"<masking-mode>\",\n        ],\n        single\n      );\n      let position: undefined | Value;\n      let bgSize: undefined | Value;\n      if (positionAndSize) {\n        [position, bgSize] = splitByOperator(positionAndSize, \"/\");\n      }\n      return [\n        reference ?? createIdentifier(\"none\"),\n        position ??\n          createValueNode([\n            { type: \"Dimension\", value: \"0\", unit: \"%\", loc: null },\n            { type: \"Dimension\", value: \"0\", unit: \"%\", loc: null },\n          ]),\n        bgSize ?? createIdentifier(\"auto\"),\n        repeatStyle ?? createIdentifier(\"repeat\"),\n        origin ?? createIdentifier(\"border-box\"),\n        clip ?? origin ?? createIdentifier(\"border-box\"),\n        compositingOperator ?? createIdentifier(\"add\"),\n        mode ?? createIdentifier(\"match-source\"),\n      ];\n    });\n  yield [\"mask-image\", image] as const;\n  yield [\"mask-position\", position] as const;\n  yield [\"mask-size\", size] as const;\n  yield [\"mask-repeat\", repeat] as const;\n  yield [\"mask-origin\", origin] as const;\n  yield [\"mask-clip\", clip] as const;\n  yield [\"mask-composite\", composite] as const;\n  yield [\"mask-mode\", mode] as const;\n};\n\n/**\n *\n * mask-border =\n *   <'mask-border-source'> ||\n *   <'mask-border-slice'> [ / <'mask-border-width'>? [ / <'mask-border-outset'> ]? ]? ||\n *   <'mask-border-repeat'> ||\n *   <'mask-border-mode'>\n *\n */\nconst expandMaskBorder = function* (value: CssNode) {\n  const [source, config, repeat, mode] = parseUnordered(\n    [\n      \"<'mask-border-source'>\",\n      \"<'mask-border-slice'> [ / <'mask-border-width'>? [ / <'mask-border-outset'> ]? ]?\",\n      \"<'mask-border-repeat'>\",\n      \"<'mask-border-mode'>\",\n    ],\n    value\n  );\n  let slice: undefined | CssNode;\n  let width: undefined | CssNode;\n  let outset: undefined | CssNode;\n  if (config) {\n    [slice, width, outset] = splitByOperator(config, \"/\");\n  }\n  yield [\"mask-border-source\", source ?? createInitialNode()] as const;\n  yield [\"mask-border-slice\", slice ?? createInitialNode()] as const;\n  yield [\"mask-border-width\", width ?? createInitialNode()] as const;\n  yield [\"mask-border-outset\", outset ?? createInitialNode()] as const;\n  yield [\"mask-border-repeat\", repeat ?? createInitialNode()] as const;\n  yield [\"mask-border-mode\", mode ?? createInitialNode()] as const;\n};\n\n/**\n *\n * offset = [\n *   <'offset-position'>?\n *   [ <'offset-path'> [ <'offset-distance'> || <'offset-rotate'> ]? ]?\n * ]!\n * [ / <'offset-anchor'> ]?\n *\n */\nconst expandOffset = function* (value: CssNode) {\n  const [config, anchor] = splitByOperator(value, \"/\");\n  const [position, config2] = parseUnordered(\n    [\n      `<'offset-position'>`,\n      `[ <'offset-path'> [ <'offset-distance'> || <'offset-rotate'> ]? ]`,\n    ],\n    config ?? createIdentifier(\"none\")\n  );\n  let path;\n  let distance;\n  let rotate;\n  if (config2) {\n    [path, distance, rotate] = parseUnordered(\n      [`<'offset-path'>`, `<'offset-distance'>`, `<'offset-rotate'>`],\n      config2\n    );\n  }\n  yield [\"offset-position\", position ?? createIdentifier(\"normal\")] as const;\n  yield [\"offset-path\", path ?? createIdentifier(\"none\")] as const;\n  yield [\"offset-distance\", distance ?? createNumber(\"0\")] as const;\n  yield [\"offset-rotate\", rotate ?? createIdentifier(\"auto\")] as const;\n  yield [\"offset-anchor\", anchor ?? createIdentifier(\"auto\")] as const;\n};\n\n/**\n *\n * scroll-timeline = [ <'scroll-timeline-name'> <'scroll-timeline-axis'>? ]#\n *\n */\nconst expandScrollTimeline = function* (value: CssNode) {\n  const [name, axis] = parseRepeated(value, (single) => {\n    const [name, axis] = parseUnordered(\n      [`none | <custom-ident>`, `block | inline | x | y`],\n      single\n    );\n    return [\n      name ?? createIdentifier(\"none\"),\n      axis ?? createIdentifier(\"block\"),\n    ];\n  });\n  yield [\"scroll-timeline-name\", name] as const;\n  yield [\"scroll-timeline-axis\", axis] as const;\n};\n\n/**\n *\n * view-timeline =\n *   [ <'view-timeline-name'> [ <'view-timeline-axis'> || <'view-timeline-inset'> ]? ]#\n *\n * <view-timeline-name> = [ none | <dashed-ident> ]#\n *\n * <view-timeline-axis> = [ block | inline | x | y ]#\n *\n * <view-timeline-inset> = [ [ auto | <length-percentage> ]{1,2} ]#\n *\n */\nconst expandViewTimeline = function* (value: CssNode) {\n  const [name, axis, inset] = parseRepeated(value, (single) => {\n    const [name, axis, inset] = parseUnordered(\n      [\n        `none | <custom-ident>`,\n        `block | inline | x | y`,\n        `[ auto | <length-percentage> ]{1,2}`,\n      ],\n      single\n    );\n    return [\n      name ?? createIdentifier(\"none\"),\n      axis ?? createIdentifier(\"block\"),\n      inset ?? createIdentifier(\"auto\"),\n    ];\n  });\n  yield [\"view-timeline-name\", name] as const;\n  yield [\"view-timeline-axis\", axis] as const;\n  yield [\"view-timeline-inset\", inset] as const;\n};\n\n/**\n *\n * grid-template =\n *   none |\n *   [ <'grid-template-rows'> / <'grid-template-columns'> ] |\n *   [ <line-names>? <string> <track-size>? <line-names>? ]+ [ / <explicit-track-list> ]?\n *\n */\nconst expandGridTemplate = function* (value: CssNode) {\n  let rows = createIdentifier(\"none\");\n  let columns = createIdentifier(\"none\");\n  let areas = createIdentifier(\"none\");\n  [rows = createIdentifier(\"none\"), columns = createIdentifier(\"none\")] =\n    splitByOperator(value, \"/\");\n  const rowsNodes: CssNode[] = [];\n  const areasNodes: CssNode[] = [];\n  for (const node of getValueList(rows)) {\n    if (node.type === \"String\") {\n      areasNodes.push(node);\n    } else {\n      rowsNodes.push(node);\n    }\n  }\n  if (areasNodes.length > 0) {\n    areas = createValueNode(areasNodes);\n    rows = createValueNode(rowsNodes);\n  }\n  yield [\"grid-template-areas\", areas] as const;\n  yield [\"grid-template-rows\", rows] as const;\n  yield [\"grid-template-columns\", columns] as const;\n};\n\n/**\n *\n * grid =\n *   <'grid-template'> |\n *   <'grid-template-rows'> / [ auto-flow && dense? ] <'grid-auto-columns'>? |\n *   [ auto-flow && dense? ] <'grid-auto-rows'>? / <'grid-template-columns'>\n *\n */\nconst expandGrid = function* (value: CssNode) {\n  const areas = createIdentifier(\"none\");\n  let templateRows = createIdentifier(\"none\");\n  let templateColumns = createIdentifier(\"none\");\n  let autoFlow = createIdentifier(\"row\");\n  let autoRows = createIdentifier(\"auto\");\n  let autoColumns = createIdentifier(\"auto\");\n  const [rows = createIdentifier(\"none\"), columns = createIdentifier(\"none\")] =\n    splitByOperator(value, \"/\");\n  if (lexer.match(`<'grid-template'>`, value).matched) {\n    yield* expandGridTemplate(value);\n    yield [\"grid-auto-flow.\", autoFlow] as const;\n    yield [\"grid-auto-rows\", autoRows] as const;\n    yield [\"grid-auto-columns\", autoColumns] as const;\n    return;\n  }\n  if (\n    lexer.match(`[ auto-flow && dense? ] <'grid-auto-rows'>?`, rows).matched\n  ) {\n    const [autoFlowKeyword, denseKeyword, config] = parseUnordered(\n      [\"auto-flow\", \"dense\", `<'grid-auto-rows'>?`],\n      rows\n    );\n    if (autoFlowKeyword) {\n      autoFlow = createIdentifier(\"row\");\n      if (denseKeyword) {\n        denseKeyword.children.forEach((item) => {\n          autoFlow.children.appendData(item);\n        });\n      }\n    }\n    autoRows = config ?? createIdentifier(\"auto\");\n    templateColumns = columns;\n  }\n  if (\n    lexer.match(`[ auto-flow && dense? ] <'grid-auto-columns'>?`, columns)\n      .matched\n  ) {\n    const [autoFlowKeyword, denseKeyword, config] = parseUnordered(\n      [\"auto-flow\", \"dense\", `<'grid-auto-columns'>?`],\n      columns\n    );\n    if (autoFlowKeyword) {\n      autoFlow = createIdentifier(\"column\");\n      if (denseKeyword) {\n        denseKeyword.children.forEach((item) => {\n          autoFlow.children.appendData(item);\n        });\n      }\n    }\n    autoColumns = config ?? createIdentifier(\"auto\");\n    templateRows = rows;\n  }\n  yield [\"grid-template-areas\", areas] as const;\n  yield [\"grid-template-rows\", templateRows] as const;\n  yield [\"grid-template-columns\", templateColumns] as const;\n  yield [\"grid-auto-flow.\", autoFlow] as const;\n  yield [\"grid-auto-rows\", autoRows] as const;\n  yield [\"grid-auto-columns\", autoColumns] as const;\n};\n\nconst expandWhiteSpace = function* (value: CssNode) {\n  const collapseKeyword = createIdentifier(\"collapse\");\n  const preserveKeyword = createIdentifier(\"preserve\");\n  const wrapKeyword = createIdentifier(\"wrap\");\n  const nowrapKeyword = createIdentifier(\"nowrap\");\n  let collapse = collapseKeyword;\n  let wrapMode = wrapKeyword;\n  [collapse = collapseKeyword, wrapMode = wrapKeyword] = parseUnordered(\n    [\"<'white-space-collapse'>\", \"<'text-wrap-mode'>\"],\n    value\n  );\n  if (lexer.match(\"normal\", value).matched) {\n    [collapse, wrapMode] = [collapseKeyword, wrapKeyword];\n  }\n  if (lexer.match(\"pre\", value).matched) {\n    [collapse, wrapMode] = [preserveKeyword, nowrapKeyword];\n  }\n  if (lexer.match(\"pre-wrap\", value).matched) {\n    [collapse, wrapMode] = [preserveKeyword, wrapKeyword];\n  }\n  if (lexer.match(\"pre-line\", value).matched) {\n    [collapse, wrapMode] = [createIdentifier(\"preserve-breaks\"), wrapKeyword];\n  }\n  yield [\"white-space-collapse\", collapse] as const;\n  yield [\"text-wrap-mode\", wrapMode] as const;\n};\n\n/**\n *\n * background-position = <bg-position>#\n * <bg-position> =\n *   [ left | center | right | top | bottom | <length-percentage> ] |\n *   [ left | center | right | <length-percentage> ] [ top | center | bottom | <length-percentage> ] |\n *   [ center | [ left | right ] <length-percentage>? ] && [ center | [ top | bottom ] <length-percentage>? ]\n *\n */\nconst expandBackgroundPosition = function* (value: CssNode) {\n  const center = createIdentifier(\"center\");\n  const [x, y] = parseRepeated(value, (single) => {\n    const list = getValueList(single);\n    if (list.length === 1) {\n      const [first] = list;\n      if (lexer.match(`center | <length-percentage>`, first).matched) {\n        return [createValueNode([first]), center];\n      }\n      if (lexer.match(`left | right`, first).matched) {\n        return [createValueNode([first]), center];\n      }\n      if (lexer.match(`top | bottom`, first).matched) {\n        return [center, createValueNode([first])];\n      }\n      return [single, single];\n    }\n    if (list.length === 2) {\n      const [first, second] = list;\n      if (\n        lexer.match(`top | bottom`, first).matched ||\n        lexer.match(`left | right`, second).matched\n      ) {\n        return [createValueNode([second]), createValueNode([first])];\n      }\n      return [createValueNode([first]), createValueNode([second])];\n    }\n    const [_center, x, y] = parseUnordered(\n      [\n        `center`,\n        `[ left | right ] <length-percentage>?`,\n        `[ top | bottom ] <length-percentage>?`,\n      ],\n      single\n    );\n    return [x ?? center, y ?? center];\n  });\n  yield [\"background-position-x\", x] as const;\n  yield [\"background-position-y\", y] as const;\n};\n\n/**\n *\n * background = <bg-layer>#? , <final-bg-layer>\n *\n * <bg-layer> =\n *   <bg-image> ||\n *   <bg-position> [ / <bg-size> ]? ||\n *   <repeat-style> ||\n *   <attachment> ||\n *   <visual-box> ||j\n *   <visual-box>\n *\n * <final-bg-layer> =\n *   <bg-image> ||\n *   <bg-position> [ / <bg-size> ]? ||\n *   <repeat-style> ||\n *   <attachment> ||\n *   <visual-box> ||\n *   <visual-box> ||\n *   <'background-color'>\n *\n */\nconst expandBackground = function* (value: CssNode) {\n  let backgroundColor: Value = createIdentifier(\"transparent\");\n  const [image, position, size, repeat, attachment, origin, clip] =\n    parseRepeated(value, (single) => {\n      const [\n        image,\n        positionAndSize,\n        repeatStyle,\n        attachment,\n        origin,\n        clip,\n        color,\n      ] = parseUnordered(\n        [\n          `<bg-image>`,\n          `<bg-position> [ / <bg-size> ]?`,\n          `<repeat-style>`,\n          `<attachment>`,\n          `<visual-box>`,\n          `<visual-box>`,\n          `<'background-color'>`,\n        ],\n        single\n      );\n      let position: undefined | Value;\n      let size: undefined | Value;\n      if (positionAndSize) {\n        [position, size] = splitByOperator(positionAndSize, \"/\");\n      }\n      if (color) {\n        backgroundColor = color;\n      }\n      return [\n        image ?? createIdentifier(\"none\"),\n        position ??\n          createValueNode([\n            { type: \"Dimension\", value: \"0\", unit: \"%\", loc: null },\n            { type: \"Dimension\", value: \"0\", unit: \"%\", loc: null },\n          ]),\n        size ??\n          createValueNode([\n            { type: \"Identifier\", name: \"auto\", loc: null },\n            { type: \"Identifier\", name: \"auto\", loc: null },\n          ]),\n        repeatStyle ?? createIdentifier(\"repeat\"),\n        attachment ?? createIdentifier(\"scroll\"),\n        origin ?? createIdentifier(\"padding-box\"),\n        clip ?? origin ?? createIdentifier(\"border-box\"),\n      ];\n    });\n  yield [\"background-image\", image] as const;\n  yield* expandBackgroundPosition(position);\n  yield [\"background-size\", size] as const;\n  yield [\"background-repeat\", repeat] as const;\n  yield [\"background-attachment\", attachment] as const;\n  yield [\"background-origin\", origin] as const;\n  yield [\"background-clip\", clip] as const;\n  yield [\"background-color\", backgroundColor] as const;\n};\n\nconst expandShorthand = function* (property: string, value: CssNode) {\n  switch (property) {\n    // ignore \"all\" to avoid bloating styles with huge amount of longhand properties\n    case \"all\":\n      break;\n\n    case \"font\":\n      yield* expandFont(value);\n      break;\n\n    case \"font-synthesis\":\n      yield* expandFontSynthesis(value);\n      break;\n\n    case \"font-variant\":\n      yield* expandFontVariant(value);\n      break;\n\n    case \"text-decoration\": {\n      const [line, style, color] = parseUnordered(\n        [\n          \"<'text-decoration-line'>\",\n          \"<'text-decoration-style'>\",\n          \"<'text-decoration-color'>\",\n        ],\n        value\n      );\n      yield [\"text-decoration-line\", line ?? createIdentifier(\"none\")] as const;\n      yield [\n        \"text-decoration-style\",\n        style ?? createIdentifier(\"solid\"),\n      ] as const;\n      yield [\n        \"text-decoration-color\",\n        color ?? createIdentifier(\"currentcolor\"),\n      ] as const;\n      break;\n    }\n\n    case \"text-emphasis\": {\n      const [style, color] = parseUnordered(\n        [\"<'text-emphasis-style'>\", \"<'text-emphasis-color'>\"],\n        value\n      );\n      yield [\"text-emphasis-style\", style ?? createInitialNode()] as const;\n      yield [\"text-emphasis-color\", color ?? createInitialNode()] as const;\n      break;\n    }\n\n    case \"border-width\":\n    case \"border-style\":\n    case \"border-color\": {\n      const type = property.split(\"-\").pop() ?? \"\"; // width, style or color\n      yield* expandBox((edge) => `border-${edge}-${type}`, value);\n      break;\n    }\n\n    case \"border-inline-width\":\n    case \"border-inline-style\":\n    case \"border-inline-color\": {\n      const type = property.split(\"-\").pop() ?? \"\"; // width, style or color\n      yield* expandLogical((edge) => `border-inline-${edge}-${type}`, value);\n      break;\n    }\n\n    case \"border-block-width\":\n    case \"border-block-style\":\n    case \"border-block-color\": {\n      const type = property.split(\"-\").pop() ?? \"\"; // width, style or color\n      yield* expandLogical((edge) => `border-block-${edge}-${type}`, value);\n      break;\n    }\n\n    case \"border-radius\":\n      yield* expandBorderRadius(value);\n      break;\n\n    case \"border-image\":\n      yield* expandBorderImage(value);\n      break;\n\n    case \"outline\": {\n      const [color, style, width] = parseUnordered(\n        [`<'outline-color'>`, `<'outline-style'>`, `<'outline-width'>`],\n        value\n      );\n      yield [`${property}-width`, width ?? createIdentifier(\"medium\")] as const;\n      yield [`${property}-style`, style ?? createIdentifier(\"none\")] as const;\n      yield [\n        `${property}-color`,\n        // auto is not actually supported but is described in the spec draft\n        color ?? createIdentifier(\"currentcolor\"),\n      ] as const;\n      break;\n    }\n\n    case \"mask\":\n      yield* expandMask(value);\n      break;\n\n    case \"mask-border\":\n      yield* expandMaskBorder(value);\n      break;\n\n    case \"margin\":\n    case \"padding\":\n      yield* expandBox((edge) => `${property}-${edge}`, value);\n      break;\n\n    case \"margin-inline\":\n    case \"margin-block\":\n    case \"padding-inline\":\n    case \"padding-block\":\n      yield* expandLogical((edge) => `${property}-${edge}`, value);\n      break;\n\n    case \"inset\":\n      yield* expandBox((edge) => edge, value);\n      break;\n\n    case \"inset-inline\":\n    case \"inset-block\":\n      yield* expandLogical((edge) => `${property}-${edge}`, value);\n      break;\n\n    case \"gap\":\n    case \"grid-gap\": {\n      const [rowGap, columnGap] = getValueList(value);\n      yield [\"row-gap\", rowGap] as const;\n      yield [\"column-gap\", columnGap ?? rowGap] as const;\n      break;\n    }\n\n    case \"grid-row-gap\":\n      yield [\"row-gap\", value] as const;\n      break;\n\n    case \"grid-column-gap\":\n      yield [\"column-gap\", value] as const;\n      break;\n\n    case \"grid-area\": {\n      const [rowStart, columnStart, rowEnd, columnEnd] = splitByOperator(\n        value,\n        \"/\"\n      );\n      yield [\"grid-row-start\", rowStart ?? createIdentifier(\"auto\")] as const;\n      yield [\n        \"grid-column-start\",\n        columnStart ?? createIdentifier(\"auto\"),\n      ] as const;\n      yield [\"grid-row-end\", rowEnd ?? createIdentifier(\"auto\")] as const;\n      yield [\"grid-column-end\", columnEnd ?? createIdentifier(\"auto\")] as const;\n      break;\n    }\n\n    case \"grid-row\": {\n      const [start, end] = splitByOperator(value, \"/\");\n      yield [\"grid-row-start\", start ?? createIdentifier(\"auto\")] as const;\n      yield [\"grid-row-end\", end ?? createIdentifier(\"auto\")] as const;\n      break;\n    }\n\n    case \"grid-column\": {\n      const [start, end] = splitByOperator(value, \"/\");\n      yield [\"grid-column-start\", start ?? createIdentifier(\"auto\")] as const;\n      yield [\"grid-column-end\", end ?? createIdentifier(\"auto\")] as const;\n      break;\n    }\n\n    case \"grid-template\":\n      yield* expandGridTemplate(value);\n      break;\n\n    case \"grid\":\n      yield* expandGrid(value);\n      break;\n\n    case \"flex\":\n      yield* expandFlex(value);\n      break;\n\n    case \"flex-flow\": {\n      const [direction, wrap] = parseUnordered(\n        [\"<'flex-direction'>\", \"<'flex-wrap'>\"],\n        value\n      );\n      yield [\"flex-direction\", direction ?? createInitialNode()] as const;\n      yield [\"flex-wrap\", wrap ?? createInitialNode()] as const;\n      break;\n    }\n\n    case \"place-content\": {\n      const [align, justify] = getValueList(value);\n      yield [\"align-content\", align] as const;\n      yield [\"justify-content\", justify ?? align] as const;\n      break;\n    }\n\n    case \"place-items\": {\n      const [align, justify] = getValueList(value);\n      yield [\"align-items\", align] as const;\n      yield [\"justify-items\", justify ?? align] as const;\n      break;\n    }\n\n    case \"place-self\": {\n      const [align, justify] = getValueList(value);\n      yield [\"align-self\", align] as const;\n      yield [\"justify-self\", justify ?? align] as const;\n      break;\n    }\n\n    case \"columns\": {\n      const [width, count] = parseUnordered(\n        [\"<'column-width'>\", \"<'column-count'>\"],\n        value\n      );\n      yield [\"column-width\", width ?? createInitialNode()] as const;\n      yield [\"column-count\", count ?? createInitialNode()] as const;\n      break;\n    }\n\n    case \"column-rule\": {\n      const [width, style, color] = parseUnordered(\n        [\n          \"<'column-rule-width'>\",\n          \"<'column-rule-style'>\",\n          \"<'column-rule-color'>\",\n        ],\n        value\n      );\n      yield [\"column-rule-width\", width ?? createInitialNode()] as const;\n      yield [\"column-rule-style\", style ?? createInitialNode()] as const;\n      yield [\"column-rule-color\", color ?? createInitialNode()] as const;\n      break;\n    }\n\n    case \"list-style\": {\n      const [position, image, type] = parseUnordered(\n        [\n          \"<'list-style-position'>\",\n          \"<'list-style-image'>\",\n          \"<'list-style-type'>\",\n        ],\n        value\n      );\n      // Using a value of none in the shorthand is potentially ambiguous,\n      // as none is a valid value for both list-style-image and list-style-type.\n      // To resolve this ambiguity, a value of none in the shorthand must be applied\n      // to whichever of the two properties aren’t otherwise set by the shorthand.\n      let imageValue = image ? generate(image) : undefined;\n      let typeValue = type ? generate(type) : undefined;\n      if (\n        (imageValue ?? typeValue) === \"none\" &&\n        (typeValue ?? imageValue) === \"none\"\n      ) {\n        imageValue = imageValue ?? typeValue;\n        typeValue = imageValue ?? typeValue;\n      }\n      yield [\n        \"list-style-position\",\n        position ?? createIdentifier(\"outside\"),\n      ] as const;\n      yield [\n        \"list-style-image\",\n        image ?? createIdentifier(imageValue ?? \"none\"),\n      ] as const;\n      yield [\n        \"list-style-type\",\n        type ?? createIdentifier(typeValue ?? \"disc\"),\n      ] as const;\n      break;\n    }\n\n    case \"animation\":\n      yield* expandAnimation(value);\n      break;\n\n    case \"animation-range\":\n      yield* expandAnimationRange(value);\n      break;\n\n    case \"transition\":\n      yield* expandTransition(value);\n      break;\n\n    case \"offset\":\n      yield* expandOffset(value);\n      break;\n\n    case \"scroll-timeline\":\n      yield* expandScrollTimeline(value);\n      break;\n\n    case \"view-timeline\":\n      yield* expandViewTimeline(value);\n      break;\n\n    case \"scroll-margin\":\n    case \"scroll-padding\":\n      yield* expandBox((edge) => `${property}-${edge}`, value);\n      break;\n\n    case \"scroll-margin-inline\":\n    case \"scroll-margin-block\":\n    case \"scroll-padding-inline\":\n    case \"scroll-padding-block\":\n      yield* expandLogical((edge) => `${property}-${edge}`, value);\n      break;\n\n    case \"overflow\": {\n      const [x, y] = getValueList(value);\n      yield [\"overflow-x\", x] as const;\n      yield [\"overflow-y\", y ?? x] as const;\n      break;\n    }\n\n    case \"container\": {\n      const [name, type] = splitByOperator(value, \"/\");\n      yield [\"container-name\", name ?? createIdentifier(\"none\")] as const;\n      yield [\"container-type\", type ?? createIdentifier(\"normal\")] as const;\n      break;\n    }\n\n    case \"contain-intrinsic-size\": {\n      const [width, height] = parseUnordered(\n        [`<'contain-intrinsic-width'>`, `<'contain-intrinsic-height'>`],\n        value\n      );\n      yield [\n        \"contain-intrinsic-width\",\n        width ?? createIdentifier(\"none\"),\n      ] as const;\n      yield [\n        \"contain-intrinsic-height\",\n        height ?? width ?? createIdentifier(\"none\"),\n      ] as const;\n      break;\n    }\n\n    case \"white-space\":\n      yield* expandWhiteSpace(value);\n      break;\n\n    case \"text-wrap\": {\n      const [\n        mode = createIdentifier(\"wrap\"),\n        style = createIdentifier(\"auto\"),\n      ] = parseUnordered([\"<'text-wrap-mode'>\", \"<'text-wrap-style'>\"], value);\n      yield [\"text-wrap-mode\", mode] as const;\n      yield [\"text-wrap-style\", style] as const;\n      break;\n    }\n\n    case \"caret\": {\n      const [color, shape] = parseUnordered(\n        [`<'caret-color'>`, `<'caret-shape'>`],\n        value\n      );\n      yield [\"caret-color\", color ?? createIdentifier(\"auto\")] as const;\n      yield [\"caret-shape\", shape ?? createIdentifier(\"auto\")] as const;\n      break;\n    }\n\n    case \"background-position\":\n      yield* expandBackgroundPosition(value);\n      break;\n\n    case \"background\":\n      yield* expandBackground(value);\n      break;\n\n    case \"overscroll-behavior\": {\n      const [x, y] = getValueList(value);\n      yield [\"overscroll-behavior-x\", x] as const;\n      yield [\"overscroll-behavior-y\", y ?? x] as const;\n      break;\n    }\n\n    case \"position-try\": {\n      const [order, options] = parseUnordered(\n        [\n          `normal | most-width | most-height | most-block-size | most-inline-size`,\n          `none | [ [<custom-ident> || flip-block || flip-inline || flip-start] | inset-area( <'inset-area'> ) ]#`,\n        ],\n        value\n      );\n      yield [\n        \"position-try-order\",\n        order ?? createIdentifier(\"normal\"),\n      ] as const;\n      yield [\n        \"position-try-options\",\n        options ?? createIdentifier(\"none\"),\n      ] as const;\n      break;\n    }\n\n    default:\n      yield [property, value] as const;\n  }\n};\n\nconst parseValue = function* (property: string, value: string) {\n  try {\n    const ast = parse(value, { context: \"value\" });\n    if (\n      // custom properties can be empty\n      !property.startsWith(\"--\") &&\n      ast.type === \"Value\" &&\n      ast.children.isEmpty\n    ) {\n      ast.children.appendData({ type: \"Identifier\", name: \"unset\", loc: null });\n    }\n    yield [property, ast] as const;\n  } catch {\n    // empty block\n  }\n};\n\nexport const expandShorthands = (\n  shorthands: [property: string, value: string][]\n): [property: CssProperty, value: string][] => {\n  const longhands: [property: string, value: string][] = [];\n  for (const [property, value] of shorthands) {\n    const generator = parseValue(property, value);\n\n    for (const [property, value] of generator) {\n      // set all longhand properties to the same css-wide keyword\n      // specified in shorthand\n      let cssWideKeyword: undefined | CssNode;\n      if (lexer.match(cssWideKeywordsSyntax, value).matched) {\n        cssWideKeyword = value;\n      }\n\n      const generator = expandBorder(property, value);\n\n      for (const [property, value] of generator) {\n        const generator = expandShorthand(property, value);\n\n        for (const [property, value] of generator) {\n          try {\n            longhands.push([property, generate(cssWideKeyword ?? value)]);\n          } catch {\n            warnOnce(\n              true,\n              `Failed to generate longhands for shorthand ${shorthands.map((shorthand) => shorthand.join(\"=\")).join(\", \")}`\n            );\n          }\n        }\n      }\n    }\n  }\n  return longhands as [property: CssProperty, value: string][];\n};\n"
  },
  {
    "path": "packages/css-data/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"],\n  \"compilerOptions\": {\n    \"isolatedDeclarations\": true\n  }\n}\n"
  },
  {
    "path": "packages/css-engine/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/css-engine/README.md",
    "content": "## CSS Engine\n\nThis package is designed to render effectively CSS in the format produced by\nWebstudio Builder. It is using CSS variables for all dynamic parts and can\nrender at runtime as for usage on Live Canvas in the Builder as well as for\nextracting a fully static style sheet for production.\n"
  },
  {
    "path": "packages/css-engine/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/css-engine\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"CSS Renderer for Webstudio\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"build\": \"rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external && esbuild src/runtime.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@emotion/hash\": \"^0.9.2\",\n    \"@webstudio-is/fonts\": \"workspace:*\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/design-system\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vitest\": \"^3.1.2\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\",\n      \"types\": \"./lib/types/index.d.ts\",\n      \"import\": \"./lib/index.js\"\n    },\n    \"./runtime\": {\n      \"webstudio\": \"./src/runtime.ts\",\n      \"types\": \"./lib/types/runtime.d.ts\",\n      \"import\": \"./lib/runtime.js\"\n    }\n  },\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/css-engine/src/__generated__/types.ts",
    "content": "// This file was generated by pnpm mdn-data\nexport type CamelCasedProperty =\n  | \"WebkitFontSmoothing\"\n  | \"MozOsxFontSmoothing\"\n  | \"-webkit-box-orient\"\n  | \"viewTimelineName\"\n  | \"scrollTimelineName\"\n  | \"viewTimelineInset\"\n  | \"-webkit-line-clamp\"\n  | \"-webkit-overflow-scrolling\"\n  | \"-webkit-tap-highlight-color\"\n  | \"accentColor\"\n  | \"alignContent\"\n  | \"alignItems\"\n  | \"alignSelf\"\n  | \"alignmentBaseline\"\n  | \"animationComposition\"\n  | \"animationDelay\"\n  | \"animationDirection\"\n  | \"animationDuration\"\n  | \"animationFillMode\"\n  | \"animationIterationCount\"\n  | \"animationName\"\n  | \"animationPlayState\"\n  | \"animationTimingFunction\"\n  | \"appearance\"\n  | \"aspectRatio\"\n  | \"backdropFilter\"\n  | \"backfaceVisibility\"\n  | \"backgroundAttachment\"\n  | \"backgroundBlendMode\"\n  | \"backgroundClip\"\n  | \"backgroundColor\"\n  | \"backgroundImage\"\n  | \"backgroundOrigin\"\n  | \"backgroundPositionX\"\n  | \"backgroundPositionY\"\n  | \"backgroundRepeat\"\n  | \"backgroundSize\"\n  | \"blockSize\"\n  | \"borderBlockEndColor\"\n  | \"borderBlockEndStyle\"\n  | \"borderBlockEndWidth\"\n  | \"borderBlockStartColor\"\n  | \"borderBlockStartStyle\"\n  | \"borderBlockStartWidth\"\n  | \"borderBottomColor\"\n  | \"borderBottomLeftRadius\"\n  | \"borderBottomRightRadius\"\n  | \"borderBottomStyle\"\n  | \"borderBottomWidth\"\n  | \"borderCollapse\"\n  | \"borderEndEndRadius\"\n  | \"borderEndStartRadius\"\n  | \"borderImageOutset\"\n  | \"borderImageRepeat\"\n  | \"borderImageSlice\"\n  | \"borderImageSource\"\n  | \"borderImageWidth\"\n  | \"borderInlineEndColor\"\n  | \"borderInlineEndStyle\"\n  | \"borderInlineEndWidth\"\n  | \"borderInlineStartColor\"\n  | \"borderInlineStartStyle\"\n  | \"borderInlineStartWidth\"\n  | \"borderLeftColor\"\n  | \"borderLeftStyle\"\n  | \"borderLeftWidth\"\n  | \"borderRightColor\"\n  | \"borderRightStyle\"\n  | \"borderRightWidth\"\n  | \"borderSpacing\"\n  | \"borderStartEndRadius\"\n  | \"borderStartStartRadius\"\n  | \"borderTopColor\"\n  | \"borderTopLeftRadius\"\n  | \"borderTopRightRadius\"\n  | \"borderTopStyle\"\n  | \"borderTopWidth\"\n  | \"bottom\"\n  | \"boxDecorationBreak\"\n  | \"boxShadow\"\n  | \"boxSizing\"\n  | \"breakAfter\"\n  | \"breakBefore\"\n  | \"breakInside\"\n  | \"captionSide\"\n  | \"caretColor\"\n  | \"clear\"\n  | \"clipPath\"\n  | \"clipRule\"\n  | \"color\"\n  | \"colorInterpolationFilters\"\n  | \"colorScheme\"\n  | \"columnCount\"\n  | \"columnFill\"\n  | \"columnGap\"\n  | \"columnRuleColor\"\n  | \"columnRuleStyle\"\n  | \"columnRuleWidth\"\n  | \"columnSpan\"\n  | \"columnWidth\"\n  | \"contain\"\n  | \"containIntrinsicBlockSize\"\n  | \"containIntrinsicHeight\"\n  | \"containIntrinsicInlineSize\"\n  | \"containIntrinsicWidth\"\n  | \"containerName\"\n  | \"containerType\"\n  | \"content\"\n  | \"contentVisibility\"\n  | \"counterIncrement\"\n  | \"counterReset\"\n  | \"counterSet\"\n  | \"cursor\"\n  | \"cx\"\n  | \"cy\"\n  | \"d\"\n  | \"direction\"\n  | \"display\"\n  | \"dominantBaseline\"\n  | \"emptyCells\"\n  | \"fieldSizing\"\n  | \"fill\"\n  | \"fillOpacity\"\n  | \"fillRule\"\n  | \"filter\"\n  | \"flexBasis\"\n  | \"flexDirection\"\n  | \"flexGrow\"\n  | \"flexShrink\"\n  | \"flexWrap\"\n  | \"float\"\n  | \"floodColor\"\n  | \"floodOpacity\"\n  | \"fontFamily\"\n  | \"fontFeatureSettings\"\n  | \"fontKerning\"\n  | \"fontLanguageOverride\"\n  | \"fontOpticalSizing\"\n  | \"fontPalette\"\n  | \"fontSize\"\n  | \"fontSizeAdjust\"\n  | \"fontStyle\"\n  | \"fontSynthesisSmallCaps\"\n  | \"fontSynthesisStyle\"\n  | \"fontSynthesisWeight\"\n  | \"fontVariantAlternates\"\n  | \"fontVariantCaps\"\n  | \"fontVariantEastAsian\"\n  | \"fontVariantEmoji\"\n  | \"fontVariantLigatures\"\n  | \"fontVariantNumeric\"\n  | \"fontVariantPosition\"\n  | \"fontVariationSettings\"\n  | \"fontWeight\"\n  | \"forcedColorAdjust\"\n  | \"gridAutoColumns\"\n  | \"gridAutoFlow\"\n  | \"gridAutoRows\"\n  | \"gridColumnEnd\"\n  | \"gridColumnStart\"\n  | \"gridRowEnd\"\n  | \"gridRowStart\"\n  | \"gridTemplateAreas\"\n  | \"gridTemplateColumns\"\n  | \"gridTemplateRows\"\n  | \"hangingPunctuation\"\n  | \"height\"\n  | \"hyphenateCharacter\"\n  | \"hyphenateLimitChars\"\n  | \"hyphens\"\n  | \"imageOrientation\"\n  | \"imageRendering\"\n  | \"initialLetter\"\n  | \"inlineSize\"\n  | \"insetBlockEnd\"\n  | \"insetBlockStart\"\n  | \"insetInlineEnd\"\n  | \"insetInlineStart\"\n  | \"isolation\"\n  | \"justifyContent\"\n  | \"justifyItems\"\n  | \"justifySelf\"\n  | \"left\"\n  | \"letterSpacing\"\n  | \"lightingColor\"\n  | \"lineBreak\"\n  | \"lineClamp\"\n  | \"lineHeight\"\n  | \"listStyleImage\"\n  | \"listStylePosition\"\n  | \"listStyleType\"\n  | \"marginBlockEnd\"\n  | \"marginBlockStart\"\n  | \"marginBottom\"\n  | \"marginInlineEnd\"\n  | \"marginInlineStart\"\n  | \"marginLeft\"\n  | \"marginRight\"\n  | \"marginTop\"\n  | \"markerEnd\"\n  | \"markerMid\"\n  | \"markerStart\"\n  | \"maskBorderMode\"\n  | \"maskBorderOutset\"\n  | \"maskBorderRepeat\"\n  | \"maskBorderSlice\"\n  | \"maskBorderSource\"\n  | \"maskBorderWidth\"\n  | \"maskClip\"\n  | \"maskComposite\"\n  | \"maskImage\"\n  | \"maskMode\"\n  | \"maskOrigin\"\n  | \"maskPosition\"\n  | \"maskRepeat\"\n  | \"maskSize\"\n  | \"maskType\"\n  | \"mathDepth\"\n  | \"mathStyle\"\n  | \"maxBlockSize\"\n  | \"maxHeight\"\n  | \"maxInlineSize\"\n  | \"maxWidth\"\n  | \"minBlockSize\"\n  | \"minHeight\"\n  | \"minInlineSize\"\n  | \"minWidth\"\n  | \"mixBlendMode\"\n  | \"objectFit\"\n  | \"objectPosition\"\n  | \"offsetAnchor\"\n  | \"offsetDistance\"\n  | \"offsetPath\"\n  | \"offsetPosition\"\n  | \"offsetRotate\"\n  | \"opacity\"\n  | \"order\"\n  | \"orphans\"\n  | \"outlineColor\"\n  | \"outlineOffset\"\n  | \"outlineStyle\"\n  | \"outlineWidth\"\n  | \"overflowAnchor\"\n  | \"overflowBlock\"\n  | \"overflowClipMargin\"\n  | \"overflowInline\"\n  | \"overflowWrap\"\n  | \"overflowX\"\n  | \"overflowY\"\n  | \"overscrollBehavior\"\n  | \"overscrollBehaviorBlock\"\n  | \"overscrollBehaviorInline\"\n  | \"overscrollBehaviorX\"\n  | \"overscrollBehaviorY\"\n  | \"paddingBlockEnd\"\n  | \"paddingBlockStart\"\n  | \"paddingBottom\"\n  | \"paddingInlineEnd\"\n  | \"paddingInlineStart\"\n  | \"paddingLeft\"\n  | \"paddingRight\"\n  | \"paddingTop\"\n  | \"page\"\n  | \"paintOrder\"\n  | \"perspective\"\n  | \"perspectiveOrigin\"\n  | \"pointerEvents\"\n  | \"position\"\n  | \"printColorAdjust\"\n  | \"quotes\"\n  | \"r\"\n  | \"resize\"\n  | \"right\"\n  | \"rotate\"\n  | \"rowGap\"\n  | \"rubyAlign\"\n  | \"rubyPosition\"\n  | \"rx\"\n  | \"ry\"\n  | \"scale\"\n  | \"scrollBehavior\"\n  | \"scrollMarginBlockEnd\"\n  | \"scrollMarginBlockStart\"\n  | \"scrollMarginBottom\"\n  | \"scrollMarginInlineEnd\"\n  | \"scrollMarginInlineStart\"\n  | \"scrollMarginLeft\"\n  | \"scrollMarginRight\"\n  | \"scrollMarginTop\"\n  | \"scrollPaddingBlockEnd\"\n  | \"scrollPaddingBlockStart\"\n  | \"scrollPaddingBottom\"\n  | \"scrollPaddingInlineEnd\"\n  | \"scrollPaddingInlineStart\"\n  | \"scrollPaddingLeft\"\n  | \"scrollPaddingRight\"\n  | \"scrollPaddingTop\"\n  | \"scrollSnapAlign\"\n  | \"scrollSnapStop\"\n  | \"scrollSnapType\"\n  | \"scrollbarColor\"\n  | \"scrollbarGutter\"\n  | \"scrollbarWidth\"\n  | \"shapeImageThreshold\"\n  | \"shapeMargin\"\n  | \"shapeOutside\"\n  | \"shapeRendering\"\n  | \"stopColor\"\n  | \"stopOpacity\"\n  | \"strokeDasharray\"\n  | \"strokeDashoffset\"\n  | \"strokeLinecap\"\n  | \"strokeLinejoin\"\n  | \"strokeMiterlimit\"\n  | \"strokeOpacity\"\n  | \"strokeWidth\"\n  | \"tabSize\"\n  | \"tableLayout\"\n  | \"textAlign\"\n  | \"textAlignLast\"\n  | \"textAnchor\"\n  | \"textCombineUpright\"\n  | \"textDecorationColor\"\n  | \"textDecorationLine\"\n  | \"textDecorationSkipInk\"\n  | \"textDecorationStyle\"\n  | \"textDecorationThickness\"\n  | \"textEmphasisColor\"\n  | \"textEmphasisPosition\"\n  | \"textEmphasisStyle\"\n  | \"textIndent\"\n  | \"textJustify\"\n  | \"textOrientation\"\n  | \"textOverflow\"\n  | \"textRendering\"\n  | \"textShadow\"\n  | \"textSizeAdjust\"\n  | \"textTransform\"\n  | \"textUnderlineOffset\"\n  | \"textUnderlinePosition\"\n  | \"textWrapMode\"\n  | \"textWrapStyle\"\n  | \"top\"\n  | \"touchAction\"\n  | \"transform\"\n  | \"transformBox\"\n  | \"transformOrigin\"\n  | \"transformStyle\"\n  | \"transitionBehavior\"\n  | \"transitionDelay\"\n  | \"transitionDuration\"\n  | \"transitionProperty\"\n  | \"transitionTimingFunction\"\n  | \"translate\"\n  | \"unicodeBidi\"\n  | \"userSelect\"\n  | \"vectorEffect\"\n  | \"verticalAlign\"\n  | \"viewTransitionName\"\n  | \"visibility\"\n  | \"whiteSpaceCollapse\"\n  | \"widows\"\n  | \"width\"\n  | \"willChange\"\n  | \"wordBreak\"\n  | \"wordSpacing\"\n  | \"wordWrap\"\n  | \"writingMode\"\n  | \"x\"\n  | \"y\"\n  | \"zIndex\"\n  | \"zoom\";\n\nexport type HyphenatedProperty =\n  | \"-webkit-font-smoothing\"\n  | \"-moz-osx-font-smoothing\"\n  | \"-webkit-box-orient\"\n  | \"view-timeline-name\"\n  | \"scroll-timeline-name\"\n  | \"view-timeline-inset\"\n  | \"-webkit-line-clamp\"\n  | \"-webkit-overflow-scrolling\"\n  | \"-webkit-tap-highlight-color\"\n  | \"accent-color\"\n  | \"align-content\"\n  | \"align-items\"\n  | \"align-self\"\n  | \"alignment-baseline\"\n  | \"animation-composition\"\n  | \"animation-delay\"\n  | \"animation-direction\"\n  | \"animation-duration\"\n  | \"animation-fill-mode\"\n  | \"animation-iteration-count\"\n  | \"animation-name\"\n  | \"animation-play-state\"\n  | \"animation-timing-function\"\n  | \"appearance\"\n  | \"aspect-ratio\"\n  | \"backdrop-filter\"\n  | \"backface-visibility\"\n  | \"background-attachment\"\n  | \"background-blend-mode\"\n  | \"background-clip\"\n  | \"background-color\"\n  | \"background-image\"\n  | \"background-origin\"\n  | \"background-position-x\"\n  | \"background-position-y\"\n  | \"background-repeat\"\n  | \"background-size\"\n  | \"block-size\"\n  | \"border-block-end-color\"\n  | \"border-block-end-style\"\n  | \"border-block-end-width\"\n  | \"border-block-start-color\"\n  | \"border-block-start-style\"\n  | \"border-block-start-width\"\n  | \"border-bottom-color\"\n  | \"border-bottom-left-radius\"\n  | \"border-bottom-right-radius\"\n  | \"border-bottom-style\"\n  | \"border-bottom-width\"\n  | \"border-collapse\"\n  | \"border-end-end-radius\"\n  | \"border-end-start-radius\"\n  | \"border-image-outset\"\n  | \"border-image-repeat\"\n  | \"border-image-slice\"\n  | \"border-image-source\"\n  | \"border-image-width\"\n  | \"border-inline-end-color\"\n  | \"border-inline-end-style\"\n  | \"border-inline-end-width\"\n  | \"border-inline-start-color\"\n  | \"border-inline-start-style\"\n  | \"border-inline-start-width\"\n  | \"border-left-color\"\n  | \"border-left-style\"\n  | \"border-left-width\"\n  | \"border-right-color\"\n  | \"border-right-style\"\n  | \"border-right-width\"\n  | \"border-spacing\"\n  | \"border-start-end-radius\"\n  | \"border-start-start-radius\"\n  | \"border-top-color\"\n  | \"border-top-left-radius\"\n  | \"border-top-right-radius\"\n  | \"border-top-style\"\n  | \"border-top-width\"\n  | \"bottom\"\n  | \"box-decoration-break\"\n  | \"box-shadow\"\n  | \"box-sizing\"\n  | \"break-after\"\n  | \"break-before\"\n  | \"break-inside\"\n  | \"caption-side\"\n  | \"caret-color\"\n  | \"clear\"\n  | \"clip-path\"\n  | \"clip-rule\"\n  | \"color\"\n  | \"color-interpolation-filters\"\n  | \"color-scheme\"\n  | \"column-count\"\n  | \"column-fill\"\n  | \"column-gap\"\n  | \"column-rule-color\"\n  | \"column-rule-style\"\n  | \"column-rule-width\"\n  | \"column-span\"\n  | \"column-width\"\n  | \"contain\"\n  | \"contain-intrinsic-block-size\"\n  | \"contain-intrinsic-height\"\n  | \"contain-intrinsic-inline-size\"\n  | \"contain-intrinsic-width\"\n  | \"container-name\"\n  | \"container-type\"\n  | \"content\"\n  | \"content-visibility\"\n  | \"counter-increment\"\n  | \"counter-reset\"\n  | \"counter-set\"\n  | \"cursor\"\n  | \"cx\"\n  | \"cy\"\n  | \"d\"\n  | \"direction\"\n  | \"display\"\n  | \"dominant-baseline\"\n  | \"empty-cells\"\n  | \"field-sizing\"\n  | \"fill\"\n  | \"fill-opacity\"\n  | \"fill-rule\"\n  | \"filter\"\n  | \"flex-basis\"\n  | \"flex-direction\"\n  | \"flex-grow\"\n  | \"flex-shrink\"\n  | \"flex-wrap\"\n  | \"float\"\n  | \"flood-color\"\n  | \"flood-opacity\"\n  | \"font-family\"\n  | \"font-feature-settings\"\n  | \"font-kerning\"\n  | \"font-language-override\"\n  | \"font-optical-sizing\"\n  | \"font-palette\"\n  | \"font-size\"\n  | \"font-size-adjust\"\n  | \"font-style\"\n  | \"font-synthesis-small-caps\"\n  | \"font-synthesis-style\"\n  | \"font-synthesis-weight\"\n  | \"font-variant-alternates\"\n  | \"font-variant-caps\"\n  | \"font-variant-east-asian\"\n  | \"font-variant-emoji\"\n  | \"font-variant-ligatures\"\n  | \"font-variant-numeric\"\n  | \"font-variant-position\"\n  | \"font-variation-settings\"\n  | \"font-weight\"\n  | \"forced-color-adjust\"\n  | \"grid-auto-columns\"\n  | \"grid-auto-flow\"\n  | \"grid-auto-rows\"\n  | \"grid-column-end\"\n  | \"grid-column-start\"\n  | \"grid-row-end\"\n  | \"grid-row-start\"\n  | \"grid-template-areas\"\n  | \"grid-template-columns\"\n  | \"grid-template-rows\"\n  | \"hanging-punctuation\"\n  | \"height\"\n  | \"hyphenate-character\"\n  | \"hyphenate-limit-chars\"\n  | \"hyphens\"\n  | \"image-orientation\"\n  | \"image-rendering\"\n  | \"initial-letter\"\n  | \"inline-size\"\n  | \"inset-block-end\"\n  | \"inset-block-start\"\n  | \"inset-inline-end\"\n  | \"inset-inline-start\"\n  | \"isolation\"\n  | \"justify-content\"\n  | \"justify-items\"\n  | \"justify-self\"\n  | \"left\"\n  | \"letter-spacing\"\n  | \"lighting-color\"\n  | \"line-break\"\n  | \"line-clamp\"\n  | \"line-height\"\n  | \"list-style-image\"\n  | \"list-style-position\"\n  | \"list-style-type\"\n  | \"margin-block-end\"\n  | \"margin-block-start\"\n  | \"margin-bottom\"\n  | \"margin-inline-end\"\n  | \"margin-inline-start\"\n  | \"margin-left\"\n  | \"margin-right\"\n  | \"margin-top\"\n  | \"marker-end\"\n  | \"marker-mid\"\n  | \"marker-start\"\n  | \"mask-border-mode\"\n  | \"mask-border-outset\"\n  | \"mask-border-repeat\"\n  | \"mask-border-slice\"\n  | \"mask-border-source\"\n  | \"mask-border-width\"\n  | \"mask-clip\"\n  | \"mask-composite\"\n  | \"mask-image\"\n  | \"mask-mode\"\n  | \"mask-origin\"\n  | \"mask-position\"\n  | \"mask-repeat\"\n  | \"mask-size\"\n  | \"mask-type\"\n  | \"math-depth\"\n  | \"math-style\"\n  | \"max-block-size\"\n  | \"max-height\"\n  | \"max-inline-size\"\n  | \"max-width\"\n  | \"min-block-size\"\n  | \"min-height\"\n  | \"min-inline-size\"\n  | \"min-width\"\n  | \"mix-blend-mode\"\n  | \"object-fit\"\n  | \"object-position\"\n  | \"offset-anchor\"\n  | \"offset-distance\"\n  | \"offset-path\"\n  | \"offset-position\"\n  | \"offset-rotate\"\n  | \"opacity\"\n  | \"order\"\n  | \"orphans\"\n  | \"outline-color\"\n  | \"outline-offset\"\n  | \"outline-style\"\n  | \"outline-width\"\n  | \"overflow-anchor\"\n  | \"overflow-block\"\n  | \"overflow-clip-margin\"\n  | \"overflow-inline\"\n  | \"overflow-wrap\"\n  | \"overflow-x\"\n  | \"overflow-y\"\n  | \"overscroll-behavior\"\n  | \"overscroll-behavior-block\"\n  | \"overscroll-behavior-inline\"\n  | \"overscroll-behavior-x\"\n  | \"overscroll-behavior-y\"\n  | \"padding-block-end\"\n  | \"padding-block-start\"\n  | \"padding-bottom\"\n  | \"padding-inline-end\"\n  | \"padding-inline-start\"\n  | \"padding-left\"\n  | \"padding-right\"\n  | \"padding-top\"\n  | \"page\"\n  | \"paint-order\"\n  | \"perspective\"\n  | \"perspective-origin\"\n  | \"pointer-events\"\n  | \"position\"\n  | \"print-color-adjust\"\n  | \"quotes\"\n  | \"r\"\n  | \"resize\"\n  | \"right\"\n  | \"rotate\"\n  | \"row-gap\"\n  | \"ruby-align\"\n  | \"ruby-position\"\n  | \"rx\"\n  | \"ry\"\n  | \"scale\"\n  | \"scroll-behavior\"\n  | \"scroll-margin-block-end\"\n  | \"scroll-margin-block-start\"\n  | \"scroll-margin-bottom\"\n  | \"scroll-margin-inline-end\"\n  | \"scroll-margin-inline-start\"\n  | \"scroll-margin-left\"\n  | \"scroll-margin-right\"\n  | \"scroll-margin-top\"\n  | \"scroll-padding-block-end\"\n  | \"scroll-padding-block-start\"\n  | \"scroll-padding-bottom\"\n  | \"scroll-padding-inline-end\"\n  | \"scroll-padding-inline-start\"\n  | \"scroll-padding-left\"\n  | \"scroll-padding-right\"\n  | \"scroll-padding-top\"\n  | \"scroll-snap-align\"\n  | \"scroll-snap-stop\"\n  | \"scroll-snap-type\"\n  | \"scrollbar-color\"\n  | \"scrollbar-gutter\"\n  | \"scrollbar-width\"\n  | \"shape-image-threshold\"\n  | \"shape-margin\"\n  | \"shape-outside\"\n  | \"shape-rendering\"\n  | \"stop-color\"\n  | \"stop-opacity\"\n  | \"stroke-dasharray\"\n  | \"stroke-dashoffset\"\n  | \"stroke-linecap\"\n  | \"stroke-linejoin\"\n  | \"stroke-miterlimit\"\n  | \"stroke-opacity\"\n  | \"stroke-width\"\n  | \"tab-size\"\n  | \"table-layout\"\n  | \"text-align\"\n  | \"text-align-last\"\n  | \"text-anchor\"\n  | \"text-combine-upright\"\n  | \"text-decoration-color\"\n  | \"text-decoration-line\"\n  | \"text-decoration-skip-ink\"\n  | \"text-decoration-style\"\n  | \"text-decoration-thickness\"\n  | \"text-emphasis-color\"\n  | \"text-emphasis-position\"\n  | \"text-emphasis-style\"\n  | \"text-indent\"\n  | \"text-justify\"\n  | \"text-orientation\"\n  | \"text-overflow\"\n  | \"text-rendering\"\n  | \"text-shadow\"\n  | \"text-size-adjust\"\n  | \"text-transform\"\n  | \"text-underline-offset\"\n  | \"text-underline-position\"\n  | \"text-wrap-mode\"\n  | \"text-wrap-style\"\n  | \"top\"\n  | \"touch-action\"\n  | \"transform\"\n  | \"transform-box\"\n  | \"transform-origin\"\n  | \"transform-style\"\n  | \"transition-behavior\"\n  | \"transition-delay\"\n  | \"transition-duration\"\n  | \"transition-property\"\n  | \"transition-timing-function\"\n  | \"translate\"\n  | \"unicode-bidi\"\n  | \"user-select\"\n  | \"vector-effect\"\n  | \"vertical-align\"\n  | \"view-transition-name\"\n  | \"visibility\"\n  | \"white-space-collapse\"\n  | \"widows\"\n  | \"width\"\n  | \"will-change\"\n  | \"word-break\"\n  | \"word-spacing\"\n  | \"word-wrap\"\n  | \"writing-mode\"\n  | \"x\"\n  | \"y\"\n  | \"z-index\"\n  | \"zoom\";\n\nexport type Unit =\n  | \"%\"\n  | \"deg\"\n  | \"grad\"\n  | \"rad\"\n  | \"turn\"\n  | \"db\"\n  | \"fr\"\n  | \"hz\"\n  | \"khz\"\n  | \"cm\"\n  | \"mm\"\n  | \"q\"\n  | \"in\"\n  | \"pt\"\n  | \"pc\"\n  | \"px\"\n  | \"em\"\n  | \"rem\"\n  | \"ex\"\n  | \"rex\"\n  | \"cap\"\n  | \"rcap\"\n  | \"ch\"\n  | \"rch\"\n  | \"ic\"\n  | \"ric\"\n  | \"lh\"\n  | \"rlh\"\n  | \"vw\"\n  | \"svw\"\n  | \"lvw\"\n  | \"dvw\"\n  | \"vh\"\n  | \"svh\"\n  | \"lvh\"\n  | \"dvh\"\n  | \"vi\"\n  | \"svi\"\n  | \"lvi\"\n  | \"dvi\"\n  | \"vb\"\n  | \"svb\"\n  | \"lvb\"\n  | \"dvb\"\n  | \"vmin\"\n  | \"svmin\"\n  | \"lvmin\"\n  | \"dvmin\"\n  | \"vmax\"\n  | \"svmax\"\n  | \"lvmax\"\n  | \"dvmax\"\n  | \"cqw\"\n  | \"cqh\"\n  | \"cqi\"\n  | \"cqb\"\n  | \"cqmin\"\n  | \"cqmax\"\n  | \"dpi\"\n  | \"dpcm\"\n  | \"dppx\"\n  | \"x\"\n  | \"st\"\n  | \"s\"\n  | \"ms\";\n"
  },
  {
    "path": "packages/css-engine/src/core/atomic.test.ts",
    "content": "import { test, expect } from \"vitest\";\nimport { createRegularStyleSheet } from \"./create-style-sheet\";\nimport { generateAtomic } from \"./atomic\";\nimport type { NestingRule } from \"./rules\";\nimport type { StyleValue } from \"../schema\";\n\nconst mediaRuleOptions0 = { minWidth: 0 } as const;\nconst mediaId0 = \"0\";\n\ntest(\"use matching media rule\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const rule = sheet.addNestingRule(\"\");\n  rule.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"marginTop\",\n    value: { type: \"keyword\", value: \"auto\" },\n  });\n  rule.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"red\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all {\n  .chcgnqf {\n    margin-top: auto\n  }\n  .cen0ymu {\n    color: red\n  }\n}\"\n`);\n});\n\ntest(\"use nested selector\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const rule = sheet.addNestingRule(\"\");\n  rule.setDeclaration({\n    breakpoint: \"x\",\n    selector: \":hover\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all {\n  .c143pt9k:hover {\n    display: block\n  }\n}\"\n`);\n});\n\ntest(\"added classes\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const rule = sheet.addNestingRule(\".instance\");\n  rule.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  const instances = new Map([[rule, \"instanceId\"]]);\n  const { classes } = generateAtomic(sheet, {\n    getKey: (rule) => instances.get(rule) ?? \"\",\n  });\n  expect(classes.get(\"instanceId\")).toEqual([\"ccqp4le\"]);\n});\n\ntest(\"rule with multiple properties\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(mediaId0, mediaRuleOptions0);\n  const rule = sheet.addNestingRule(\".instance\");\n  rule.setDeclaration({\n    breakpoint: mediaId0,\n    selector: \"\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  rule.setDeclaration({\n    breakpoint: mediaId0,\n    selector: \"\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"red\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all and (min-width: 0px) {\n  .c1qg54vh {\n    display: block\n  }\n  .ckgcokb {\n    color: red\n  }\n}\"\n`);\n});\n\ntest(\"share atomic rules\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const rule1 = sheet.addNestingRule(\"1\");\n  rule1.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  rule1.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"red\" },\n  });\n  const rule2 = sheet.addNestingRule(\"2\");\n  rule2.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  const instances = new Map([\n    [rule1, \"1\"],\n    [rule2, \"2\"],\n  ]);\n  const { cssText, classes } = generateAtomic(sheet, {\n    getKey: (rule) => instances.get(rule) ?? \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"@media all {\n  .ccqp4le {\n    display: block\n  }\n  .cen0ymu {\n    color: red\n  }\n}\"\n`);\n  expect(classes.get(\"1\")).toEqual([\"ccqp4le\", \"cen0ymu\"]);\n  expect(classes.get(\"2\")).toEqual([\"ccqp4le\"]);\n});\n\ntest(\"distinct similar declarations from different breakpoints\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"a\");\n  sheet.addMediaRule(\"b\");\n  const rule1 = sheet.addNestingRule(\"1\");\n  rule1.setDeclaration({\n    breakpoint: \"a\",\n    selector: \"\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  rule1.setDeclaration({\n    breakpoint: \"b\",\n    selector: \"\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all {\n  .cumb2su {\n    display: block\n  }\n}\n@media all {\n  .c1u3btle {\n    display: block\n  }\n}\"\n`);\n});\n\ntest(\"support descendant suffix\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const rule1 = sheet.addNestingRule(\"instance\");\n  rule1.setDeclaration({\n    breakpoint: \"x\",\n    selector: \":hover\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  const rule2 = sheet.addNestingRule(\"instance\", \" img\");\n  rule2.setDeclaration({\n    breakpoint: \"x\",\n    selector: \":hover\",\n    property: \"display\",\n    value: { type: \"keyword\", value: \"block\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all {\n  .c143pt9k:hover {\n    display: block\n  }\n  .cpdl2lp img:hover {\n    display: block\n  }\n}\"\n`);\n});\n\ntest(\"generate prefixed and unprefixed in the same rule\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const rule = sheet.addNestingRule(\"instance\");\n  rule.setDeclaration({\n    breakpoint: \"x\",\n    selector: \"\",\n    property: \"textSizeAdjust\",\n    value: { type: \"keyword\", value: \"auto\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all {\n  .c1h1gugw {\n    -webkit-text-size-adjust: auto;\n    text-size-adjust: auto\n  }\n}\"\n`);\n});\n\ntest(\"generate merged properties as single rule\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const setMargins = (rule: NestingRule, value: StyleValue) => {\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginTop\",\n      value,\n    });\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginRight\",\n      value,\n    });\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginBottom\",\n      value,\n    });\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginLeft\",\n      value,\n    });\n  };\n  setMargins(sheet.addNestingRule(\"instance\"), {\n    type: \"keyword\",\n    value: \"auto\",\n  });\n  setMargins(sheet.addNestingRule(\"instance\", \" img\"), {\n    type: \"unit\",\n    value: 10,\n    unit: \"px\",\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n\"@media all {\n  .cdj9gv4 {\n    margin: auto\n  }\n  .c340vfr img {\n    margin: 10px\n  }\n}\"\n`);\n});\n\ntest(\"convert :local-link to [aria-current=page] selector\", () => {\n  const sheet = createRegularStyleSheet();\n  const rule = sheet.addNestingRule(\".instance\");\n  sheet.addMediaRule(\"x\");\n  rule.setDeclaration({\n    breakpoint: \"x\",\n    selector: \":local-link\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"green\" },\n  });\n  expect(generateAtomic(sheet, { getKey: () => \"\" }).cssText)\n    .toMatchInlineSnapshot(`\n    \"@media all {\n      .c3mubaz[aria-current=page] {\n        color: green\n      }\n    }\"\n  `);\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/atomic.ts",
    "content": "import hash from \"@emotion/hash\";\nimport type { StyleSheet } from \"./style-sheet\";\nimport { NestingRule } from \"./rules\";\nimport { toValue, type TransformValue } from \"./to-value\";\n\ntype Options = {\n  /** in case of undefined the rule will not be split into atomics */\n  getKey: (rule: NestingRule) => string | undefined;\n  transformValue?: TransformValue;\n  classes?: Map<string, string[]>;\n};\n\nexport const generateAtomic = (sheet: StyleSheet, options: Options) => {\n  const { getKey, transformValue } = options;\n  const atomicRules = new Map<string, NestingRule>();\n  const classes = options.classes ?? new Map<string, string[]>();\n  for (const rule of sheet.nestingRules.values()) {\n    const descendantSuffix = rule.getDescendantSuffix();\n    const groupKey = getKey(rule);\n    if (groupKey === undefined) {\n      atomicRules.set(rule.getSelector(), rule);\n      continue;\n    }\n    // a few rules can be in the same group\n    // when rule have descendant suffix\n    let classList = classes.get(groupKey);\n    if (classList === undefined) {\n      classList = [];\n      classes.set(groupKey, classList);\n    }\n    // convert each declaration into separate rule\n    for (const declaration of rule.getMergedDeclarations()) {\n      const atomicHash = hash(\n        descendantSuffix +\n          declaration.breakpoint +\n          declaration.selector +\n          declaration.property +\n          toValue(declaration.value, transformValue)\n      );\n      // \"c\" makes sure hash always starts with a letter.\n      const className = `c${atomicHash}`;\n      // reuse atomic rules\n      let atomicRule = atomicRules.get(atomicHash);\n      if (atomicRule === undefined) {\n        atomicRule = new NestingRule(\n          new Map(),\n          `.${className}`,\n          descendantSuffix\n        );\n        atomicRule.setDeclaration(declaration);\n        atomicRules.set(atomicHash, atomicRule);\n      }\n      classList.push(className);\n    }\n  }\n  const cssText = sheet.generateWith({\n    nestingRules: Array.from(atomicRules.values()),\n    transformValue,\n  });\n  return { cssText, classes };\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/compare-media.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { compareMedia } from \"./compare-media\";\n\ndescribe(\"Compare media\", () => {\n  test(\"min-width\", () => {\n    const initial = [\n      {},\n      { minWidth: 1280 },\n      { minWidth: 0 },\n      { minWidth: 1024 },\n      { minWidth: 768 },\n    ];\n    const expected = [\n      {},\n      { minWidth: 0 },\n      { minWidth: 768 },\n      { minWidth: 1024 },\n      { minWidth: 1280 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"max-width\", () => {\n    const initial = [\n      {},\n      { maxWidth: 1280 },\n      { maxWidth: 0 },\n      { maxWidth: 1024 },\n      { maxWidth: 768 },\n    ];\n    const expected = [\n      {},\n      { maxWidth: 1280 },\n      { maxWidth: 1024 },\n      { maxWidth: 768 },\n      { maxWidth: 0 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"mixed max and min\", () => {\n    const initial = [\n      {},\n      { maxWidth: 991 },\n      { maxWidth: 479 },\n      { maxWidth: 767 },\n      { minWidth: 1440 },\n      { minWidth: 1280 },\n      { minWidth: 1920 },\n    ];\n    const expected = [\n      {},\n      { maxWidth: 991 },\n      { maxWidth: 767 },\n      { maxWidth: 479 },\n      { minWidth: 1280 },\n      { minWidth: 1440 },\n      { minWidth: 1920 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"custom conditions sorted alphabetically after base\", () => {\n    const initial = [\n      {},\n      { condition: \"orientation:portrait\" },\n      { condition: \"hover:hover\" },\n      { condition: \"prefers-color-scheme:dark\" },\n    ];\n    const expected = [\n      {},\n      { condition: \"hover:hover\" },\n      { condition: \"orientation:portrait\" },\n      { condition: \"prefers-color-scheme:dark\" },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"custom conditions before width-based\", () => {\n    const initial = [\n      {},\n      { minWidth: 1024 },\n      { condition: \"orientation:portrait\" },\n      { maxWidth: 768 },\n    ];\n    const expected = [\n      {},\n      { condition: \"orientation:portrait\" },\n      { maxWidth: 768 },\n      { minWidth: 1024 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"mixed custom conditions and width-based\", () => {\n    const initial = [\n      {},\n      { maxWidth: 991 },\n      { condition: \"hover:hover\" },\n      { minWidth: 1280 },\n      { condition: \"orientation:landscape\" },\n      { maxWidth: 767 },\n      { condition: \"prefers-color-scheme:dark\" },\n    ];\n    const expected = [\n      {},\n      { condition: \"hover:hover\" },\n      { condition: \"orientation:landscape\" },\n      { condition: \"prefers-color-scheme:dark\" },\n      { maxWidth: 991 },\n      { maxWidth: 767 },\n      { minWidth: 1280 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"simulated conditions (mediaType only) sorted after base\", () => {\n    const initial = [\n      {},\n      { mediaType: \"all\" as const },\n      { mediaType: \"not all\" as const },\n    ];\n    const expected = [\n      {},\n      { mediaType: \"all\" as const },\n      { mediaType: \"not all\" as const },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"simulated conditions sorted before width-based\", () => {\n    const initial = [\n      {},\n      { minWidth: 1024 },\n      { mediaType: \"all\" as const },\n      { maxWidth: 768 },\n    ];\n    const expected = [\n      {},\n      { mediaType: \"all\" as const },\n      { maxWidth: 768 },\n      { minWidth: 1024 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"simulated conditions mixed with real conditions\", () => {\n    const initial = [\n      {},\n      { condition: \"prefers-color-scheme:dark\" },\n      { mediaType: \"all\" as const },\n      { mediaType: \"not all\" as const },\n      { condition: \"hover:hover\" },\n    ];\n    // Both real conditions and simulated conditions sort together\n    // Real conditions have their condition string, simulated have \"\"\n    const expected = [\n      {},\n      { mediaType: \"all\" as const },\n      { mediaType: \"not all\" as const },\n      { condition: \"hover:hover\" },\n      { condition: \"prefers-color-scheme:dark\" },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n\n  test(\"full simulation scenario: base, simulated dark, hidden light, width\", () => {\n    const initial = [\n      {},\n      { maxWidth: 991 },\n      { mediaType: \"all\" as const }, // simulated dark (always applies)\n      { mediaType: \"not all\" as const }, // simulated light (hidden)\n      { minWidth: 1280 },\n    ];\n    const expected = [\n      {},\n      { mediaType: \"all\" as const },\n      { mediaType: \"not all\" as const },\n      { maxWidth: 991 },\n      { minWidth: 1280 },\n    ];\n    const sorted = initial.sort(compareMedia);\n    expect(sorted).toStrictEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/compare-media.ts",
    "content": "import type { MediaRuleOptions } from \"./rules\";\n\n/**\n * Check if a media rule is a simulated condition breakpoint.\n * Simulated conditions have an explicit mediaType (\"all\" or \"not all\")\n * but no condition, minWidth, or maxWidth.\n *\n * Note: This assumes that options with ONLY mediaType set are always simulated\n * conditions, not legitimate \"screen-only\" or \"print-only\" queries.\n * This is safe because the codebase doesn't use standalone mediaType options\n * for any other purpose.\n */\nconst isSimulatedCondition = (options: MediaRuleOptions) =>\n  options.mediaType !== undefined &&\n  options.condition === undefined &&\n  options.minWidth === undefined &&\n  options.maxWidth === undefined;\n\n/**\n * Check if a media rule is a condition-based breakpoint\n * (either real or simulated).\n */\nconst isCondition = (options: MediaRuleOptions) =>\n  options.condition !== undefined || isSimulatedCondition(options);\n\n/**\n * Sort media queries for CSS cascade order.\n * Width-based: minWidth descending, then maxWidth ascending\n * Custom conditions: sorted alphabetically, placed between base and width-based\n *\n * Note: minWidth/maxWidth and condition are mutually exclusive in MediaRuleOptions.\n */\nexport const compareMedia = (\n  optionA: MediaRuleOptions,\n  optionB: MediaRuleOptions\n) => {\n  const aIsCondition = isCondition(optionA);\n  const bIsCondition = isCondition(optionB);\n\n  // If both are conditions (real or simulated), sort alphabetically by condition\n  if (aIsCondition && bIsCondition) {\n    return (optionA.condition ?? \"\").localeCompare(optionB.condition ?? \"\");\n  }\n\n  // Condition comes after base but before width-based\n  if (aIsCondition) {\n    if (optionB.minWidth === undefined && optionB.maxWidth === undefined) {\n      return 1; // optionA (condition) after optionB (base)\n    }\n    return -1; // optionA (condition) before optionB (width)\n  }\n  if (bIsCondition) {\n    if (optionA.minWidth === undefined && optionA.maxWidth === undefined) {\n      return -1; // optionA (base) before optionB (condition)\n    }\n    return 1; // optionA (width) after optionB (condition)\n  }\n\n  // Ensures a media with no min/max is always first\n  if (optionA.minWidth === undefined && optionA.maxWidth === undefined) {\n    return -1;\n  }\n  if (optionB.minWidth === undefined && optionB.maxWidth === undefined) {\n    return 1;\n  }\n\n  // Both are defined by minWidth, put the bigger one first\n  if (optionA.minWidth !== undefined && optionB.minWidth !== undefined) {\n    return optionA.minWidth - optionB.minWidth;\n  }\n  // Both are defined by maxWidth, put the smaller one first\n  if (optionA.maxWidth !== undefined && optionB.maxWidth !== undefined) {\n    return optionB.maxWidth - optionA.maxWidth;\n  }\n\n  // Media with maxWith should render before minWith just to have the same sorting visually in the UI as in CSSOM.\n  return \"minWidth\" in optionA ? 1 : -1;\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/create-style-sheet.ts",
    "content": "import { StyleSheetRegular } from \"./style-sheet-regular\";\nimport { StyleElement, FakeStyleElement } from \"./style-element\";\n\nexport const createRegularStyleSheet = (options?: {\n  name?: string;\n  element?: StyleElement | FakeStyleElement;\n}) => {\n  const element = options?.element ?? new StyleElement(options?.name);\n  return new StyleSheetRegular(element);\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/css-engine.stories.tsx",
    "content": "import { createRegularStyleSheet } from \"./create-style-sheet\";\nimport { StorySection } from \"@webstudio-is/design-system\";\n\nexport default {\n  title: \"CSS Engine\",\n  component: \"CssEngine\",\n};\n\nconst mediaRuleOptions0 = { minWidth: 0 } as const;\nconst mediaId = \"0\";\n\nexport const CSSEngine = () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(mediaId, mediaRuleOptions0);\n  const rule = sheet.addNestingRule(\".test\");\n  rule.setDeclaration({\n    breakpoint: \"0\",\n    selector: \"\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"red\" },\n  });\n  sheet.render();\n  return (\n    <StorySection title=\"CSS Engine\">\n      <div className=\"test\">Should be red</div>\n      <button\n        onClick={() => {\n          rule.setDeclaration({\n            breakpoint: \"0\",\n            selector: \"\",\n            property: \"color\",\n            value: { type: \"keyword\", value: \"green\" },\n          });\n          sheet.render();\n        }}\n      >\n        Make it green\n      </button>\n      <button\n        onClick={() => {\n          const rule = sheet.addNestingRule(\".test\");\n          rule.setDeclaration({\n            breakpoint: \"0\",\n            selector: \"\",\n            property: \"backgroundColor\",\n            value: { type: \"keyword\", value: \"yellow\" },\n          });\n          sheet.render();\n        }}\n      >\n        Add rule with yellow background\n      </button>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/equal-media.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { equalMedia } from \"./equal-media\";\n\ndescribe(\"equalMedia\", () => {\n  test(\"minWidth\", () => {\n    expect(equalMedia({ minWidth: 100 }, { minWidth: 10 })).toBe(false);\n    expect(equalMedia({ minWidth: 100 }, { minWidth: 100 })).toBe(true);\n    expect(equalMedia({ minWidth: 100 }, { minWidth: 101 })).toBe(false);\n  });\n\n  test(\"maxWidth\", () => {\n    expect(equalMedia({ maxWidth: 100 }, { maxWidth: 101 })).toBe(false);\n    expect(equalMedia({ maxWidth: 100 }, { maxWidth: 100 })).toBe(true);\n    expect(equalMedia({ maxWidth: 100 }, { maxWidth: 10 })).toBe(false);\n  });\n\n  test(\"minWidth and maxWidth\", () => {\n    expect(equalMedia({ maxWidth: 100, minWidth: 10 }, { maxWidth: 100 })).toBe(\n      false\n    );\n    expect(equalMedia({ maxWidth: 100, minWidth: 10 }, { minWidth: 10 })).toBe(\n      false\n    );\n    expect(\n      equalMedia(\n        { maxWidth: 100, minWidth: 10 },\n        { maxWidth: 100, minWidth: 10 }\n      )\n    ).toBe(true);\n  });\n\n  test(\"custom condition\", () => {\n    expect(\n      equalMedia(\n        { condition: \"orientation:portrait\" },\n        { condition: \"orientation:portrait\" }\n      )\n    ).toBe(true);\n    expect(\n      equalMedia(\n        { condition: \"orientation:portrait\" },\n        { condition: \"orientation:landscape\" }\n      )\n    ).toBe(false);\n    expect(equalMedia({ condition: \"orientation:portrait\" }, {})).toBe(false);\n    expect(equalMedia({}, { condition: \"orientation:portrait\" })).toBe(false);\n  });\n\n  test(\"condition with width should not be equal\", () => {\n    // Note: In practice, condition and minWidth/maxWidth are mutually exclusive\n    // (enforced by schema validation). This test verifies the comparison logic only.\n    expect(\n      equalMedia(\n        { condition: \"hover:hover\", minWidth: 100 },\n        { condition: \"hover:hover\", minWidth: 100 }\n      )\n    ).toBe(true);\n    expect(equalMedia({ condition: \"hover:hover\" }, { minWidth: 100 })).toBe(\n      false\n    );\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/equal-media.ts",
    "content": "import type { MediaRuleOptions } from \"./rules\";\n\n/**\n * Compare two media query options for equality.\n * Note: minWidth/maxWidth and condition are mutually exclusive.\n */\nexport const equalMedia = (left: MediaRuleOptions, right: MediaRuleOptions) => {\n  return (\n    left.minWidth === right.minWidth &&\n    left.maxWidth === right.maxWidth &&\n    left.condition === right.condition\n  );\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/find-applicable-media.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { findApplicableMedia } from \"./find-applicable-media\";\n\nconst media = [\n  {},\n  { maxWidth: 991 },\n  { maxWidth: 767 },\n  { maxWidth: 479 },\n  { minWidth: 1280 },\n  { minWidth: 1440 },\n  { minWidth: 1920 },\n];\n\ndescribe(\"Find applicable media\", () => {\n  test(\"200\", () => {\n    expect(findApplicableMedia([...media], 200)).toStrictEqual({\n      maxWidth: 479,\n    });\n  });\n  test(\"479\", () => {\n    expect(findApplicableMedia([...media], 479)).toStrictEqual({\n      maxWidth: 479,\n    });\n  });\n  test(\"480\", () => {\n    expect(findApplicableMedia([...media], 480)).toStrictEqual({\n      maxWidth: 767,\n    });\n  });\n  test(\"1279\", () => {\n    expect(findApplicableMedia([...media], 1279)).toStrictEqual({});\n  });\n  test(\"1280\", () => {\n    expect(findApplicableMedia([...media], 1280)).toStrictEqual({\n      minWidth: 1280,\n    });\n  });\n  test(\"1440\", () => {\n    expect(findApplicableMedia([...media], 1440)).toStrictEqual({\n      minWidth: 1440,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/find-applicable-media.ts",
    "content": "import { compareMedia } from \"./compare-media\";\nimport { matchMedia } from \"./match-media\";\nimport type { MediaRuleOptions } from \"./rules\";\n\n/**\n * Find the applicable media rule that matches the given width.\n * Only matches width-based breakpoints (minWidth/maxWidth).\n * Custom condition breakpoints are not matched by this function.\n */\nexport const findApplicableMedia = <Media extends MediaRuleOptions>(\n  media: Array<Media>,\n  width: number\n) => {\n  const sortedMedia = [...media]\n    .sort(compareMedia)\n    // Reverse order is needed because the last rule in CSSOM has higher source order specificity.\n    .reverse();\n\n  for (const options of sortedMedia) {\n    if (matchMedia(options, width)) {\n      return options;\n    }\n  }\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/index.ts",
    "content": "export type {\n  StyleMap,\n  NestingRule,\n  MediaRule,\n  MediaRuleOptions,\n  PlaintextRule,\n  FontFaceRule,\n} from \"./rules\";\nexport { prefixStyles } from \"./prefixer\";\nexport { mergeStyles } from \"./merger\";\nexport { generateStyleMap } from \"./rules\";\nexport type { StyleSheetRegular } from \"./style-sheet-regular\";\nexport * from \"./create-style-sheet\";\nexport { FakeStyleElement } from \"./style-element\";\nexport * from \"./to-value\";\nexport { hyphenateProperty } from \"./to-property\";\nexport * from \"./match-media\";\nexport * from \"./equal-media\";\nexport * from \"./compare-media\";\nexport * from \"./find-applicable-media\";\nexport * from \"./atomic\";\n"
  },
  {
    "path": "packages/css-engine/src/core/match-media.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { matchMedia } from \"./match-media\";\n\ndescribe(\"matchMedia\", () => {\n  test(\"minWidth\", () => {\n    expect(matchMedia({ minWidth: 100 }, 10)).toBe(false);\n    expect(matchMedia({ minWidth: 100 }, 100)).toBe(true);\n    expect(matchMedia({ minWidth: 100 }, 101)).toBe(true);\n  });\n\n  test(\"maxWidth\", () => {\n    expect(matchMedia({ maxWidth: 100 }, 101)).toBe(false);\n    expect(matchMedia({ maxWidth: 100 }, 100)).toBe(true);\n    expect(matchMedia({ maxWidth: 100 }, 10)).toBe(true);\n  });\n\n  test(\"minWidth and maxWidth\", () => {\n    expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 9)).toBe(false);\n    expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 101)).toBe(false);\n    expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 100)).toBe(true);\n    expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 10)).toBe(true);\n    expect(matchMedia({ maxWidth: 100, minWidth: 10 }, 11)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/match-media.ts",
    "content": "import type { MediaRuleOptions } from \"./rules\";\n\n/**\n * Check if a media query matches a given width.\n * Only applies to width-based breakpoints (minWidth/maxWidth).\n * Custom condition breakpoints are not matched by this function.\n */\nexport const matchMedia = (options: MediaRuleOptions, width: number) => {\n  const minWidth = options.minWidth ?? Number.MIN_SAFE_INTEGER;\n  const maxWidth = options.maxWidth ?? Number.MAX_SAFE_INTEGER;\n  return width >= minWidth && width <= maxWidth;\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/merger.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport type { StyleValue } from \"../schema\";\nimport { mergeStyles } from \"./merger\";\nimport type { StyleMap } from \"./rules\";\nimport { toValue } from \"./to-value\";\n\nconst toStringMap = (style: StyleMap) =>\n  Array.from(\n    style.entries(),\n    ([property, value]) => [property, toValue(value)] as const\n  );\n\ntest(\"merge border when all parts are set\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"border-top-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-top-style\", { type: \"keyword\", value: \"solid\" }],\n          [\"border-top-color\", { type: \"keyword\", value: \"red\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"border-top\", \"1px solid red\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"border-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-style\", { type: \"keyword\", value: \"solid\" }],\n          [\"border-color\", { type: \"keyword\", value: \"red\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"border\", \"1px solid red\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"border-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-style\", { type: \"keyword\", value: \"solid\" }],\n        ])\n      )\n    )\n  ).toEqual([\n    [\"border-width\", \"1px\"],\n    [\"border-style\", \"solid\"],\n  ]);\n});\n\ntest(\"merge border with vars\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"border-width\", { type: \"var\", value: \"width\" }],\n          [\"border-style\", { type: \"var\", value: \"style\" }],\n          [\"border-color\", { type: \"var\", value: \"color\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"border\", \"var(--width) var(--style) var(--color)\"]]);\n});\n\ntest(\"should not merge border with initial, inherit or unset\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\n            \"border-width\",\n            {\n              type: \"var\",\n              value: \"width\",\n              fallback: { type: \"keyword\", value: \"unset\" },\n            },\n          ],\n          [\"border-style\", { type: \"var\", value: \"style\" }],\n          [\"border-color\", { type: \"var\", value: \"color\" }],\n        ])\n      )\n    )\n  ).toEqual([\n    [\"border-width\", \"var(--width, unset)\"],\n    [\"border-style\", \"var(--style)\"],\n    [\"border-color\", \"var(--color)\"],\n  ]);\n});\n\ntest(\"merge margin/padding when the same value is set\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"margin-top\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"margin-right\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"margin-bottom\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"margin-left\", { type: \"unit\", value: 10, unit: \"px\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"margin\", \"10px\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"padding-top\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"padding-right\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"padding-bottom\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"padding-left\", { type: \"unit\", value: 10, unit: \"px\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"padding\", \"10px\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"padding-top\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"padding-right\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"padding-bottom\", { type: \"unit\", value: 10, unit: \"px\" }],\n        ])\n      )\n    )\n  ).toEqual([\n    [\"padding-top\", \"10px\"],\n    [\"padding-right\", \"10px\"],\n    [\"padding-bottom\", \"10px\"],\n  ]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"padding-top\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"padding-right\", { type: \"unit\", value: 2, unit: \"px\" }],\n          [\"padding-bottom\", { type: \"unit\", value: 10, unit: \"px\" }],\n          [\"padding-left\", { type: \"unit\", value: 10, unit: \"px\" }],\n        ])\n      )\n    )\n  ).toEqual([\n    [\"padding-top\", \"1px\"],\n    [\"padding-right\", \"2px\"],\n    [\"padding-bottom\", \"10px\"],\n    [\"padding-left\", \"10px\"],\n  ]);\n});\n\ntest(\"merge border longhands\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"border-top-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-top-style\", { type: \"keyword\", value: \"solid\" }],\n          [\"border-top-color\", { type: \"keyword\", value: \"red\" }],\n          [\"border-right-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-right-style\", { type: \"keyword\", value: \"solid\" }],\n          [\"border-right-color\", { type: \"keyword\", value: \"red\" }],\n          [\"border-bottom-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-bottom-style\", { type: \"keyword\", value: \"solid\" }],\n          [\"border-bottom-color\", { type: \"keyword\", value: \"red\" }],\n          [\"border-left-width\", { type: \"unit\", value: 1, unit: \"px\" }],\n          [\"border-left-style\", { type: \"keyword\", value: \"solid\" }],\n          [\"border-left-color\", { type: \"keyword\", value: \"red\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"border\", \"1px solid red\"]]);\n});\n\nconst mergeKeywords = (list: [property: string, keyword: string][]) => {\n  const styleMap: StyleMap = new Map();\n  for (const [property, value] of list) {\n    styleMap.set(property, { type: \"keyword\", value });\n  }\n  return toStringMap(mergeStyles(styleMap));\n};\n\ntest(\"merge white-space-collapse and text-wrap-mode into white-space\", () => {\n  expect(\n    mergeKeywords([\n      [\"white-space-collapse\", \"collapse\"],\n      [\"text-wrap-mode\", \"wrap\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"normal\"],\n    [\"white-space-collapse\", \"collapse\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"white-space-collapse\", \"collapse\"],\n      [\"text-wrap-mode\", \"nowrap\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"nowrap\"],\n    [\"white-space-collapse\", \"collapse\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"white-space-collapse\", \"preserve\"],\n      [\"text-wrap-mode\", \"nowrap\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"pre\"],\n    [\"white-space-collapse\", \"preserve\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"white-space-collapse\", \"preserve\"],\n      [\"text-wrap-mode\", \"wrap\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"pre-wrap\"],\n    [\"white-space-collapse\", \"preserve\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"white-space-collapse\", \"preserve-breaks\"],\n      [\"text-wrap-mode\", \"wrap\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"pre-line\"],\n    [\"white-space-collapse\", \"preserve-breaks\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"white-space-collapse\", \"preserve-spaces\"],\n      [\"text-wrap-mode\", \"wrap\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"normal\"],\n    [\"white-space-collapse\", \"preserve-spaces\"],\n  ]);\n});\n\ntest(\"merge white-space with vars\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([[\"white-space-collapse\", { type: \"var\", value: \"collapse\" }]])\n      )\n    )\n  ).toEqual([[\"white-space-collapse\", \"var(--collapse)\"]]);\n});\n\ntest(\"merge text-wrap-mode and text-wrap-style into text-wrap\", () => {\n  expect(\n    mergeKeywords([\n      [\"text-wrap-mode\", \"wrap\"],\n      [\"text-wrap-style\", \"balance\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"normal\"],\n    [\"text-wrap\", \"balance\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"text-wrap-mode\", \"wrap\"],\n      [\"text-wrap-style\", \"stable\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"normal\"],\n    [\"text-wrap\", \"stable\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"text-wrap-mode\", \"wrap\"],\n      [\"text-wrap-style\", \"pretty\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"normal\"],\n    [\"text-wrap\", \"pretty\"],\n  ]);\n  expect(\n    mergeKeywords([\n      [\"text-wrap-mode\", \"wrap\"],\n      [\"text-wrap-style\", \"auto\"],\n    ])\n  ).toEqual([\n    [\"white-space\", \"normal\"],\n    [\"text-wrap\", \"wrap\"],\n  ]);\n  expect(mergeKeywords([[\"text-wrap-style\", \"auto\"]])).toEqual([\n    [\"text-wrap\", \"wrap\"],\n  ]);\n});\n\ntest(\"merge text-wrap with vars\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"text-wrap-mode\", { type: \"var\", value: \"mode\" }],\n          [\"text-wrap-style\", { type: \"var\", value: \"style\" }],\n        ])\n      )\n    )\n  ).toEqual([[\"text-wrap\", \"var(--style)\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([[\"text-wrap-style\", { type: \"var\", value: \"style\" }]])\n      )\n    )\n  ).toEqual([[\"text-wrap\", \"var(--style)\"]]);\n});\n\nconst layers = (...keywords: string[]): StyleValue => ({\n  type: \"layers\",\n  value: keywords.map((value) => ({ type: \"keyword\", value })),\n});\n\ntest(\"merge background-position-{x,y}\", () => {\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"background-position-x\", layers(\"right\")],\n          [\"background-position-y\", layers(\"bottom\")],\n        ])\n      )\n    )\n  ).toEqual([[\"background-position\", \"right bottom\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(new Map([[\"background-position-x\", layers(\"right\")]]))\n    )\n  ).toEqual([[\"background-position-x\", \"right\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(new Map([[\"background-position-y\", layers(\"bottom\")]]))\n    )\n  ).toEqual([[\"background-position-y\", \"bottom\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"background-position-x\", layers(\"right\", \"left\")],\n          [\"background-position-y\", layers(\"bottom\", \"top\")],\n        ])\n      )\n    )\n  ).toEqual([[\"background-position\", \"right bottom, left top\"]]);\n  expect(\n    toStringMap(\n      mergeStyles(\n        new Map([\n          [\"background-position-x\", layers(\"right\", \"left\")],\n          [\"background-position-y\", layers(\"bottom\")],\n        ])\n      )\n    )\n  ).toEqual([\n    [\"background-position-x\", \"right, left\"],\n    [\"background-position-y\", \"bottom\"],\n  ]);\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/merger.ts",
    "content": "import { StyleValue, TupleValue, TupleValueItem } from \"../schema\";\nimport { cssWideKeywords } from \"../css\";\nimport type { StyleMap } from \"./rules\";\nimport { toValue } from \"./to-value\";\n\n/**\n * Css wide keywords cannot be used in shorthand parts\n */\nconst isLonghandValue = (value?: StyleValue): value is StyleValue => {\n  if (value === undefined) {\n    return false;\n  }\n  if (value.type === \"keyword\" && cssWideKeywords.has(value.value)) {\n    return false;\n  }\n  if (value.type === \"var\") {\n    const fallback = value.fallback;\n    if (fallback?.type === \"keyword\" && cssWideKeywords.has(fallback.value)) {\n      return false;\n    }\n  }\n  return true;\n};\n\nconst mergeBorder = (styleMap: StyleMap, base: string) => {\n  // support any type in tuple, adding only\n  // var would cause circular dependency issue in zod schema\n  const width = styleMap.get(`${base}-width`) as undefined | TupleValueItem;\n  const style = styleMap.get(`${base}-style`) as undefined | TupleValueItem;\n  const color = styleMap.get(`${base}-color`) as undefined | TupleValueItem;\n  if (\n    isLonghandValue(width) &&\n    isLonghandValue(style) &&\n    isLonghandValue(color)\n  ) {\n    styleMap.delete(`${base}-width`);\n    styleMap.delete(`${base}-style`);\n    styleMap.delete(`${base}-color`);\n    styleMap.set(base, { type: \"tuple\", value: [width, style, color] });\n  }\n};\n\nconst mergeBox = (styleMap: StyleMap, base: string) => {\n  const topValue = styleMap.get(`${base}-top`);\n  const top = toValue(topValue);\n  const right = toValue(styleMap.get(`${base}-right`));\n  const bottom = toValue(styleMap.get(`${base}-bottom`));\n  const left = toValue(styleMap.get(`${base}-left`));\n  if (\n    isLonghandValue(topValue) &&\n    top === right &&\n    top === bottom &&\n    top === left\n  ) {\n    styleMap.delete(`${base}-top`);\n    styleMap.delete(`${base}-right`);\n    styleMap.delete(`${base}-bottom`);\n    styleMap.delete(`${base}-left`);\n    styleMap.set(base, topValue);\n  }\n};\n\nconst mergeWhiteSpaceAndTextWrap = (styleMap: StyleMap) => {\n  const collapseValue = styleMap.get(\"white-space-collapse\");\n  const collapse = toValue(collapseValue);\n  const modeValue = styleMap.get(\"text-wrap-mode\");\n  const mode = toValue(modeValue);\n  const styleValue = styleMap.get(\"text-wrap-style\");\n  const style = toValue(styleValue);\n  // completely unsupported anywhere\n  styleMap.delete(\"text-wrap-mode\");\n  styleMap.delete(\"text-wrap-style\");\n  if (\n    collapse === \"collapse\" ||\n    collapse === \"initial\" ||\n    mode === \"wrap\" ||\n    mode === \"initial\"\n  ) {\n    styleMap.set(\"white-space\", { type: \"keyword\", value: \"normal\" });\n  }\n  if (mode === \"nowrap\") {\n    styleMap.set(\"white-space\", { type: \"keyword\", value: \"nowrap\" });\n  }\n  if (collapse === \"preserve\") {\n    if (mode === \"nowrap\") {\n      styleMap.set(\"white-space\", { type: \"keyword\", value: \"pre\" });\n    } else {\n      styleMap.set(\"white-space\", { type: \"keyword\", value: \"pre-wrap\" });\n    }\n  }\n  if (collapse === \"preserve-breaks\") {\n    styleMap.set(\"white-space\", { type: \"keyword\", value: \"pre-line\" });\n  }\n  if (collapse === \"break-spaces\") {\n    styleMap.set(\"white-space\", { type: \"keyword\", value: \"break-spaces\" });\n  }\n  if (style === \"auto\") {\n    styleMap.set(\"text-wrap\", modeValue ?? { type: \"keyword\", value: \"wrap\" });\n  }\n  if (style === \"balance\" || style === \"stable\" || style === \"pretty\") {\n    styleMap.set(\"text-wrap\", { type: \"keyword\", value: style });\n  }\n  // fallback non keyword types as is to text-wrap\n  const textWrap =\n    (styleValue?.type !== \"keyword\" ? styleValue : undefined) ??\n    (modeValue?.type !== \"keyword\" ? modeValue : undefined);\n  if (textWrap) {\n    styleMap.set(\"text-wrap\", textWrap);\n  }\n  // supported in most browsers so use as fallback in the end\n  if (collapseValue) {\n    styleMap.delete(\"white-space-collapse\");\n    styleMap.set(\"white-space-collapse\", collapseValue);\n  }\n};\n\nconst mergeBackgroundPosition = (styleMap: StyleMap) => {\n  const x = styleMap.get(\"background-position-x\");\n  const y = styleMap.get(\"background-position-y\");\n  if (\n    x?.type === \"layers\" &&\n    y?.type === \"layers\" &&\n    x.value.length === y.value.length\n  ) {\n    const position = x.value.map((xValue, index): TupleValue => {\n      const yValue = y.value[index];\n      return {\n        type: \"tuple\",\n        value: [xValue as TupleValueItem, yValue as TupleValueItem],\n      };\n    });\n    styleMap.delete(\"background-position-x\");\n    styleMap.delete(\"background-position-y\");\n    styleMap.set(\"background-position\", {\n      type: \"layers\",\n      value: position,\n    });\n  }\n};\n\nexport const mergeStyles = (styleMap: StyleMap) => {\n  const newStyle = new Map(styleMap);\n  mergeBorder(newStyle, \"border-top\");\n  mergeBorder(newStyle, \"border-right\");\n  mergeBorder(newStyle, \"border-bottom\");\n  mergeBorder(newStyle, \"border-left\");\n  mergeBorder(newStyle, \"border\");\n  mergeBorder(newStyle, \"outline\");\n  mergeBox(newStyle, \"border\");\n  mergeBox(newStyle, \"margin\");\n  mergeBox(newStyle, \"padding\");\n  mergeWhiteSpaceAndTextWrap(newStyle);\n  mergeBackgroundPosition(newStyle);\n  return newStyle;\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/prefixer.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { prefixStyles } from \"./prefixer\";\n\ntest(\"prefix background-clip\", () => {\n  expect(\n    prefixStyles(\n      new Map([[\"background-clip\", { type: \"keyword\", value: \"text\" }]])\n    )\n  ).toEqual(\n    new Map([\n      [\"-webkit-background-clip\", { type: \"keyword\", value: \"text\" }],\n      [\"background-clip\", { type: \"keyword\", value: \"text\" }],\n    ])\n  );\n});\n\ntest(\"prefix user-select\", () => {\n  expect(\n    prefixStyles(new Map([[\"user-select\", { type: \"keyword\", value: \"none\" }]]))\n  ).toEqual(\n    new Map([\n      [\"-webkit-user-select\", { type: \"keyword\", value: \"none\" }],\n      [\"user-select\", { type: \"keyword\", value: \"none\" }],\n    ])\n  );\n});\n\ntest(\"prefix text-size-adjust\", () => {\n  expect(\n    prefixStyles(\n      new Map([[\"text-size-adjust\", { type: \"keyword\", value: \"auto\" }]])\n    )\n  ).toEqual(\n    new Map([\n      [\"-webkit-text-size-adjust\", { type: \"keyword\", value: \"auto\" }],\n      [\"text-size-adjust\", { type: \"keyword\", value: \"auto\" }],\n    ])\n  );\n});\n\ntest(\"prefix backdrop-filter\", () => {\n  expect(\n    prefixStyles(\n      new Map([[\"backdrop-filter\", { type: \"unparsed\", value: \"blur(4px)\" }]])\n    )\n  ).toEqual(\n    new Map([\n      [\"-webkit-backdrop-filter\", { type: \"unparsed\", value: \"blur(4px)\" }],\n      [\"backdrop-filter\", { type: \"unparsed\", value: \"blur(4px)\" }],\n    ])\n  );\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/prefixer.ts",
    "content": "import type { StyleMap } from \"./rules\";\n\nexport const prefixStyles = (styleMap: StyleMap) => {\n  const newStyleMap: StyleMap = new Map();\n  for (const [property, value] of styleMap) {\n    // chrome started to support unprefixed background-clip in December 2023\n    // https://caniuse.com/background-clip-text\n    // @todo stop prerfixed maybe one year later\n    if (property === \"background-clip\") {\n      newStyleMap.set(\"-webkit-background-clip\", value);\n    }\n    // safari still supports only prefixed version\n    // https://caniuse.com/?search=user-select\n    if (property === \"user-select\") {\n      newStyleMap.set(\"-webkit-user-select\", value);\n    }\n    // ios safari and firefox android supports only -webkit- prefix\n    // https://caniuse.com/text-size-adjust\n    if (property === \"text-size-adjust\") {\n      newStyleMap.set(\"-webkit-text-size-adjust\", value);\n    }\n    // safari supports with -webkit- prefix in stable version\n    // and without prefix in technology preview\n    // https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter\n    if (property === \"backdrop-filter\") {\n      newStyleMap.set(\"-webkit-backdrop-filter\", value);\n    }\n\n    // Safari and FF do not support this property and strip it from the CSS\n    // For polyfill to work we need to set it as a CSS property\n    // https://developer.mozilla.org/en-US/docs/Web/CSS/view-timeline-name\n    if (\n      property === \"view-timeline-name\" ||\n      property === \"scroll-timeline-name\" ||\n      property === \"view-timeline-inset\"\n    ) {\n      newStyleMap.set(`--${property}`, value);\n    }\n\n    newStyleMap.set(property, value);\n  }\n  return newStyleMap;\n};\n"
  },
  {
    "path": "packages/css-engine/src/core/rules.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { MediaRule, PlaintextRule } from \"./rules\";\n\ndescribe(\"MediaRule with custom conditions\", () => {\n  test(\"generates media query with custom condition only\", () => {\n    const mediaRule = new MediaRule(\"breakpoint1\", {\n      condition: \"orientation:portrait\",\n    });\n    const plainRule = new PlaintextRule(\"  .test { color: red; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all and (orientation:portrait)\");\n    expect(css).toContain(\"color: red\");\n  });\n\n  test(\"generates media query with multiple conditions\", () => {\n    const mediaRule = new MediaRule(\"breakpoint2\", {\n      condition: \"orientation:portrait and hover:hover\",\n    });\n    const plainRule = new PlaintextRule(\"  .test { color: blue; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\n      \"@media all and (orientation:portrait and hover:hover)\"\n    );\n    expect(css).toContain(\"color: blue\");\n  });\n\n  test(\"generates media query with hover condition\", () => {\n    const mediaRule = new MediaRule(\"breakpoint3\", {\n      condition: \"hover:hover\",\n    });\n    const plainRule = new PlaintextRule(\"  .test { cursor: pointer; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all and (hover:hover)\");\n    expect(css).toContain(\"cursor: pointer\");\n  });\n\n  test(\"generates media query with prefers-color-scheme condition\", () => {\n    const mediaRule = new MediaRule(\"breakpoint4\", {\n      condition: \"prefers-color-scheme:dark\",\n    });\n    const plainRule = new PlaintextRule(\"  .test { background: black; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all and (prefers-color-scheme:dark)\");\n    expect(css).toContain(\"background: black\");\n  });\n\n  test(\"prefers condition over width when both present\", () => {\n    // Note: In practice, condition and minWidth/maxWidth are mutually exclusive\n    // (enforced by schema validation). This test verifies the precedence logic only.\n    const mediaRule = new MediaRule(\"breakpoint5\", {\n      condition: \"orientation:portrait\",\n      minWidth: 768,\n    });\n    const plainRule = new PlaintextRule(\"  .test { color: red; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all and (orientation:portrait)\");\n    expect(css).not.toContain(\"min-width\");\n  });\n\n  test(\"generates media query with minWidth when no condition\", () => {\n    const mediaRule = new MediaRule(\"breakpoint6\", {\n      minWidth: 768,\n    });\n    const plainRule = new PlaintextRule(\"  .test { color: red; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all and (min-width: 768px)\");\n    expect(css).not.toContain(\"orientation\");\n  });\n\n  test(\"generates media query with maxWidth when no condition\", () => {\n    const mediaRule = new MediaRule(\"breakpoint7\", {\n      maxWidth: 1024,\n    });\n    const plainRule = new PlaintextRule(\"  .test { color: green; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all and (max-width: 1024px)\");\n  });\n\n  test(\"generates base media query when no condition or width\", () => {\n    const mediaRule = new MediaRule(\"breakpoint8\", {});\n    const plainRule = new PlaintextRule(\"  .test { color: black; }\");\n    mediaRule.insertRule(plainRule);\n\n    const css = mediaRule.cssText;\n    expect(css).toContain(\"@media all\");\n    expect(css).not.toContain(\"min-width\");\n    expect(css).not.toContain(\"max-width\");\n    expect(css).not.toContain(\"orientation\");\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/rules.ts",
    "content": "import type { StyleValue } from \"../schema\";\nimport { toValue, type TransformValue } from \"./to-value\";\nimport { hyphenateProperty } from \"./to-property\";\nimport { prefixStyles } from \"./prefixer\";\nimport { mergeStyles } from \"./merger\";\n\nconst mapGroupBy = <Item, Key>(\n  array: Item[] | Iterable<Item>,\n  getKey: (item: Item) => Key\n) => {\n  const groups = new Map<Key, Item[]>();\n  for (const item of array) {\n    const key = getKey(item);\n    let group = groups.get(key);\n    if (group === undefined) {\n      group = [];\n      groups.set(key, group);\n    }\n    group.push(item);\n  }\n  return groups;\n};\n\n/**\n * Merge styles on every group by breakpoint and selector\n * and convert back to declarations list\n */\nconst mergeDeclarations = (declarations: Iterable<Declaration>) => {\n  const newDeclarations: Declaration[] = [];\n  const groups = mapGroupBy(\n    declarations,\n    (declaration) => declaration.breakpoint + declaration.selector\n  );\n  for (const groupDeclarations of groups.values()) {\n    const { breakpoint, selector } = groupDeclarations[0];\n    const merged = mergeStyles(\n      new Map(\n        groupDeclarations.map((item) => [item.property, item.value] as const)\n      )\n    );\n    for (const [property, value] of merged) {\n      newDeclarations.push({\n        breakpoint,\n        selector,\n        property,\n        value,\n      });\n    }\n  }\n  return newDeclarations;\n};\n\n/**\n * Use CssStyleMap instead to enforce hyphenated properties.\n * @deprecated\n */\nexport type StyleMap = Map<string, StyleValue>;\n\nexport const generateStyleMap = (\n  style: StyleMap,\n  {\n    indent = 0,\n    transformValue,\n  }: {\n    indent?: number;\n    transformValue?: TransformValue;\n  } = {}\n) => {\n  const spaces = \" \".repeat(indent);\n  let lines = \"\";\n  for (const [property, value] of style) {\n    const propertyString = hyphenateProperty(property);\n    const valueString = toValue(value, transformValue);\n    const line = `${spaces}${propertyString}: ${valueString}`;\n    lines += lines === \"\" ? line : `;\\n${line}`;\n  }\n  return lines;\n};\n\nexport type Declaration = {\n  breakpoint: string;\n  selector: string;\n  property: string;\n  value: StyleValue;\n};\n\nconst normalizeDeclaration = <Type extends DeclarationKey>(\n  declaration: Type\n): Type => ({\n  ...declaration,\n  property: hyphenateProperty(declaration.property),\n});\n\ntype DeclarationKey = Omit<Declaration, \"value\">;\n\nconst getDeclarationKey = (declaraionKey: DeclarationKey) => {\n  const { breakpoint, selector, property } = declaraionKey;\n  return `${breakpoint}:${selector}:${property}`;\n};\n\n/**\n * Reusable style rule in any nesting rule\n *\n * @mixin name {\n *   \\@media breakpoint {\n *     &selector {\n *       property: value\n *     }\n *   }\n * }\n */\nexport class MixinRule {\n  // use map to avoid duplicated properties\n  #declarations = new Map<string, Declaration>();\n  #dirtyBreakpoints = new Set<string>();\n  /*\n   * check if breakpoint was updated\n   */\n  isDirtyBreakpoint(breakpoint: string) {\n    return this.#dirtyBreakpoints.has(breakpoint);\n  }\n  /**\n   * reset breakpoints invalidation\n   */\n  clearBreakpoints() {\n    this.#dirtyBreakpoints.clear();\n  }\n  setDeclaration(declaration: Declaration) {\n    // @todo temporary solution until styles are migrated to hyphenated format\n    declaration = normalizeDeclaration(declaration);\n    this.#declarations.set(getDeclarationKey(declaration), declaration);\n    this.#dirtyBreakpoints.add(declaration.breakpoint);\n  }\n  deleteDeclaration(declaration: DeclarationKey) {\n    // @todo temporary solution until styles are migrated to hyphenated format\n    declaration = normalizeDeclaration(declaration);\n    this.#declarations.delete(getDeclarationKey(declaration));\n    this.#dirtyBreakpoints.add(declaration.breakpoint);\n  }\n  getDeclarations() {\n    return this.#declarations.values();\n  }\n}\n\n/**\n * Universal style rule with nested selectors and media queries support\n * Rules are generated by each media query\n * and heavily cached to avoid complex computation\n *\n * selector {\n *   \\@media breakpoint {\n *     &selector {\n *       property: value\n *     }\n *   }\n * }\n */\nexport class NestingRule {\n  #selector: string;\n  #descendantSuffix: string;\n  #mixinRules = new Map<string, MixinRule>();\n  #mixins = new Set<string>();\n  // use map to avoid duplicated properties\n  #declarations = new Map<string, Declaration>();\n  // cached generated rule by breakpoint\n  #cache = new Map<\n    string,\n    { generated: string; indent: number; transformValue?: TransformValue }\n  >();\n  constructor(\n    mixinRules: Map<string, MixinRule>,\n    selector: string,\n    descendantSuffix: string\n  ) {\n    this.#selector = selector;\n    this.#descendantSuffix = descendantSuffix;\n    this.#mixinRules = mixinRules;\n  }\n  getSelector() {\n    return this.#selector;\n  }\n  setSelector(selector: string) {\n    this.#selector = selector;\n    this.#cache.clear();\n  }\n  getDescendantSuffix() {\n    return this.#descendantSuffix;\n  }\n  addMixin(mixin: string) {\n    this.#mixins.add(mixin);\n    this.#cache.clear();\n  }\n  applyMixins(mixins: string[]) {\n    this.#mixins = new Set(mixins);\n    this.#cache.clear();\n  }\n  setDeclaration(declaration: Declaration) {\n    // @todo temporary solution until styles are migrated to hyphenated format\n    declaration = normalizeDeclaration(declaration);\n    this.#declarations.set(getDeclarationKey(declaration), declaration);\n    this.#cache.delete(declaration.breakpoint);\n  }\n  deleteDeclaration(declaration: DeclarationKey) {\n    // @todo temporary solution until styles are migrated to hyphenated format\n    declaration = normalizeDeclaration(declaration);\n    this.#declarations.delete(getDeclarationKey(declaration));\n    this.#cache.delete(declaration.breakpoint);\n  }\n  #getDeclarations() {\n    // apply mixins first and then merge added declarations\n    const declarations = new Map<string, Declaration>();\n    for (const mixin of this.#mixins) {\n      const rule = this.#mixinRules.get(mixin);\n      if (rule === undefined) {\n        continue;\n      }\n      for (const declaration of rule.getDeclarations()) {\n        declarations.set(getDeclarationKey(declaration), declaration);\n      }\n    }\n    for (const declaration of this.#declarations.values()) {\n      declarations.set(getDeclarationKey(declaration), declaration);\n    }\n    return declarations.values();\n  }\n  getMergedDeclarations() {\n    return mergeDeclarations(this.#getDeclarations());\n  }\n  toString({\n    breakpoint,\n    indent = 0,\n    transformValue,\n  }: {\n    breakpoint: string;\n    indent?: number;\n    transformValue?: TransformValue;\n  }) {\n    for (const mixin of this.#mixins) {\n      const rule = this.#mixinRules.get(mixin);\n      // invalidate cache when mixin is changed\n      if (rule?.isDirtyBreakpoint(breakpoint)) {\n        this.#cache.delete(breakpoint);\n      }\n    }\n\n    const cached = this.#cache.get(breakpoint);\n    // invalidate cache when indent and value transformer are changed\n    if (\n      cached &&\n      cached.indent === indent &&\n      cached.transformValue === transformValue\n    ) {\n      return cached.generated;\n    }\n    const styleBySelector = new Map<string, StyleMap>();\n    for (const declaration of this.getMergedDeclarations()) {\n      // generate declarations only for specified breakpoint\n      if (declaration.breakpoint !== breakpoint) {\n        continue;\n      }\n      let nestedSelector = declaration.selector;\n      // polyfill :local-link with framework specific logic\n      if (nestedSelector === \":local-link\") {\n        nestedSelector = \"[aria-current=page]\";\n      }\n      const selector = this.#selector + this.#descendantSuffix + nestedSelector;\n      let style = styleBySelector.get(selector);\n      if (style === undefined) {\n        style = new Map();\n        styleBySelector.set(selector, style);\n      }\n      style.set(declaration.property, declaration.value);\n    }\n    const spaces = \" \".repeat(indent);\n    // sort by selector to put values without nested selector first\n    const generated = Array.from(styleBySelector)\n      .sort(([leftSelector], [rightSelector]) =>\n        leftSelector.localeCompare(rightSelector)\n      )\n      .map(([selector, style]) => {\n        const content = generateStyleMap(prefixStyles(style), {\n          indent: indent + 2,\n          transformValue,\n        });\n        return `${spaces}${selector} {\\n${content}\\n${spaces}}\\n`;\n      })\n      .join(\"\")\n      .trimEnd();\n    this.#cache.set(breakpoint, { generated, indent, transformValue });\n    return generated;\n  }\n}\n\n/**\n * Options for media queries.\n * Note: minWidth/maxWidth and condition are mutually exclusive.\n * If condition is set, minWidth and maxWidth must be undefined.\n * If minWidth or maxWidth is set, condition must be undefined.\n */\nexport type MediaRuleOptions = {\n  minWidth?: number;\n  maxWidth?: number;\n  condition?: string;\n  mediaType?: \"all\" | \"not all\" | \"screen\" | \"print\";\n};\n\nexport class MediaRule {\n  #name: string;\n  options: MediaRuleOptions;\n  rules: Map<string, PlaintextRule>;\n  constructor(name: string, options: MediaRuleOptions = {}) {\n    this.#name = name;\n    this.options = options;\n    this.rules = new Map();\n  }\n  insertRule(rule: PlaintextRule) {\n    this.rules.set(rule.cssText, rule);\n    return rule;\n  }\n  get cssText() {\n    return this.toString();\n  }\n  toString() {\n    return this.generateRule({ nestingRules: [] });\n  }\n  generateRule({\n    nestingRules,\n    transformValue,\n  }: {\n    nestingRules: NestingRule[];\n    transformValue?: TransformValue;\n  }) {\n    if (this.rules.size === 0 && nestingRules.length === 0) {\n      return \"\";\n    }\n    const rules = [];\n    for (const rule of this.rules.values()) {\n      rules.push(rule.toString());\n    }\n    for (const rule of nestingRules) {\n      const generatedRule = rule.toString({\n        breakpoint: this.#name,\n        indent: 2,\n        transformValue,\n      });\n      if (generatedRule !== \"\") {\n        rules.push(generatedRule);\n      }\n    }\n    // avoid rendering empty media queries\n    if (rules.length === 0) {\n      return \"\";\n    }\n    let conditionText = \"\";\n    const { minWidth, maxWidth, condition, mediaType = \"all\" } = this.options;\n\n    // condition and minWidth/maxWidth are mutually exclusive\n    // If condition is set, use it exclusively (minWidth/maxWidth must be undefined)\n    if (condition !== undefined) {\n      conditionText = ` and (${condition})`;\n    } else {\n      // Otherwise use width-based conditions (condition must be undefined)\n      if (minWidth !== undefined) {\n        conditionText = ` and (min-width: ${minWidth}px)`;\n      }\n      if (maxWidth !== undefined) {\n        conditionText += ` and (max-width: ${maxWidth}px)`;\n      }\n    }\n\n    return `@media ${mediaType}${conditionText} {\\n${rules.join(\"\\n\")}\\n}`;\n  }\n}\n\nexport class PlaintextRule {\n  cssText;\n  constructor(cssText: string) {\n    this.cssText = cssText;\n  }\n  toString() {\n    return this.cssText;\n  }\n}\n\nexport type FontFaceOptions = {\n  fontFamily: string;\n  fontStyle?: \"normal\" | \"italic\" | \"oblique\";\n  fontWeight?: number | string;\n  fontDisplay: \"swap\" | \"auto\" | \"block\" | \"fallback\" | \"optional\";\n  src: string;\n};\n\nexport class FontFaceRule {\n  #cached: undefined | string;\n  #options: FontFaceOptions;\n  constructor(options: FontFaceOptions) {\n    this.#options = options;\n  }\n  get cssText() {\n    return this.toString();\n  }\n  toString() {\n    if (this.#cached) {\n      return this.#cached;\n    }\n    const decls = [];\n    const { fontFamily, fontStyle, fontWeight, fontDisplay, src } =\n      this.#options;\n    const value = toValue(\n      { type: \"fontFamily\", value: [fontFamily] },\n      // Avoids adding a fallback automatically which needs to happen for font family in general but not for font face.\n      (value) => value\n    );\n    decls.push(`font-family: ${value}`);\n    decls.push(`font-style: ${fontStyle}`);\n    decls.push(`font-weight: ${fontWeight}`);\n    decls.push(`font-display: ${fontDisplay}`);\n    decls.push(`src: ${src}`);\n    this.#cached = `@font-face {\\n  ${decls.join(\"; \")};\\n}`;\n    return this.#cached;\n  }\n}\n"
  },
  {
    "path": "packages/css-engine/src/core/style-element.ts",
    "content": "export class StyleElement {\n  #element?: HTMLStyleElement;\n  #name: string;\n  constructor(name = \"\") {\n    this.#name = name;\n  }\n  get isMounted() {\n    return this.#element?.parentElement != null;\n  }\n  mount() {\n    if (this.isMounted === false) {\n      this.#element = document.createElement(\"style\");\n      this.#element.setAttribute(\"data-webstudio\", this.#name);\n      document.head.appendChild(this.#element);\n    }\n  }\n  unmount() {\n    if (this.isMounted) {\n      this.#element?.parentElement?.removeChild(this.#element);\n      this.#element = undefined;\n    }\n  }\n  render(cssText: string) {\n    if (this.#element) {\n      this.#element.textContent = cssText;\n    }\n  }\n  setAttribute(name: string, value: string) {\n    if (this.#element) {\n      this.#element.setAttribute(name, value);\n    }\n  }\n  getAttribute(name: string) {\n    if (this.#element) {\n      return this.#element.getAttribute(name);\n    }\n  }\n}\n\n/**\n * A fake style element that does nothing.\n * Useful for testing stylesheets without DOM.\n */\nexport class FakeStyleElement {\n  get isMounted() {\n    return false;\n  }\n  mount() {}\n  unmount() {}\n  render(_cssText: string) {}\n  setAttribute(_name: string, _value: string) {}\n  getAttribute(_name: string) {\n    return;\n  }\n}\n"
  },
  {
    "path": "packages/css-engine/src/core/style-sheet-regular.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { createRegularStyleSheet } from \"./create-style-sheet\";\nimport type { TransformValue } from \"./to-value\";\nimport type { NestingRule } from \"./rules\";\nimport type { StyleValue } from \"../schema\";\n\nconst mediaRuleOptions0 = { minWidth: 0 } as const;\nconst mediaId0 = \"0\";\n\nconst mediaRuleOptions1 = { minWidth: 300 } as const;\nconst mediaId1 = \"1\";\n\ndescribe(\"nesting rule\", () => {\n  const transformValue: TransformValue = (styleValue) => {\n    if (styleValue.type === \"keyword\") {\n      return {\n        type: \"keyword\",\n        value: styleValue.value.toUpperCase(),\n      };\n    }\n  };\n\n  test(\"generate rules for breakpoint\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"height\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    rule.setDeclaration({\n      breakpoint: \"small\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"transparent\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto;\n  height: auto\n}\"\n`);\n    expect(rule.toString({ breakpoint: \"small\" })).toMatchInlineSnapshot(`\n\".instance {\n  color: transparent\n}\"\n`);\n  });\n\n  test(\"generated nested rules\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"height\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"transparent\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto;\n  height: auto\n}\n.instance:hover {\n  color: transparent\n}\"\n`);\n  });\n\n  test(\"sort nested rules without state first\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"transparent\" },\n    });\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto\n}\n.instance:hover {\n  color: transparent\n}\"\n`);\n  });\n\n  test(\"customize indentation\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\", indent: 4 }))\n      .toMatchInlineSnapshot(`\n\"    .instance {\n      width: auto\n    }\"\n`);\n  });\n\n  test(\"customize value transformer\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\", transformValue }))\n      .toMatchInlineSnapshot(`\n\".instance {\n  width: AUTO\n}\"\n`);\n  });\n\n  test(\"invalidate cache\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto\n}\"\n`);\n    // invalidate by set declaration\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"height\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto;\n  height: auto\n}\"\n`);\n    // invalidate by delete declaration\n    rule.deleteDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"height\",\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto\n}\"\n`);\n    // invalidate by indent\n    expect(rule.toString({ breakpoint: \"base\", indent: 2 }))\n      .toMatchInlineSnapshot(`\n\"  .instance {\n    width: auto\n  }\"\n`);\n    // invalidate by transform value\n    expect(rule.toString({ breakpoint: \"base\", indent: 2, transformValue }))\n      .toMatchInlineSnapshot(`\n\"  .instance {\n    width: AUTO\n  }\"\n`);\n  });\n\n  test(\"generate breakpoint without declarations\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    expect(rule.toString({ breakpoint: \"base\" })).toEqual(\"\");\n  });\n});\n\ndescribe(\"mixin rule\", () => {\n  test(\"compose rules from multiple mixins\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    const localMixin = sheet.addMixinRule(\"local\");\n    const tokenMixin = sheet.addMixinRule(\"token\");\n    rule.applyMixins([\"token\", \"local\"]);\n    localMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"fit-content\" },\n    });\n    tokenMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    tokenMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"height\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: fit-content;\n  height: auto\n}\"\n`);\n  });\n\n  test(\"generate nested selector\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    const localMixin = sheet.addMixinRule(\"local\");\n    const tokenMixin = sheet.addMixinRule(\"token\");\n    rule.applyMixins([\"token\", \"local\"]);\n    localMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \":hover\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"fit-content\" },\n    });\n    tokenMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \":hover\",\n      property: \"height\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance:hover {\n  height: auto;\n  width: fit-content\n}\"\n`);\n  });\n\n  test(\"invalidate cache after applying mixins\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    const localMixin = sheet.addMixinRule(\"local\");\n    localMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\"\"`);\n    rule.applyMixins([\"local\"]);\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto\n}\"\n`);\n  });\n\n  test(\"invalidate cache after updating mixin declaration\", () => {\n    const sheet = createRegularStyleSheet();\n    const rule = sheet.addNestingRule(\".instance\");\n    const localMixin = sheet.addMixinRule(\"local\");\n    rule.applyMixins([\"local\"]);\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\"\"`);\n    localMixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n\".instance {\n  width: auto\n}\"\n`);\n  });\n});\n\ndescribe(\"Style Sheet Regular\", () => {\n  test(\"minWidth media rule\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"0\", { minWidth: 0 });\n    sheet.addNestingRule(\".c1\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 0px) {\n        .c1 {\n          color: red\n        }\n      }\"\n    `);\n  });\n\n  test(\"maxWidth media rule\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"0\", { maxWidth: 1000 });\n    sheet.addNestingRule(\".c1\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (max-width: 1000px) {\n        .c1 {\n          color: red\n        }\n      }\"\n    `);\n  });\n\n  test(\"maxWidth and maxWith media rule\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"0\", { maxWidth: 1000, minWidth: 360 });\n    sheet.addNestingRule(\".c1\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 360px) and (max-width: 1000px) {\n        .c1 {\n          color: red\n        }\n      }\"\n    `);\n  });\n\n  test(\"sort media queries based on lower min-width\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(mediaId1, mediaRuleOptions1);\n    sheet.addNestingRule(\".c2\").setDeclaration({\n      breakpoint: mediaId1,\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    });\n\n    sheet.addMediaRule(mediaId0, mediaRuleOptions0);\n    sheet.addNestingRule(\".c1\").setDeclaration({\n      breakpoint: mediaId0,\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    });\n\n    sheet.addMediaRule(\"x\");\n    sheet.addNestingRule(\".c3\").setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    });\n\n    // Default media query should allways be the first to have the lowest source order specificity\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all {\n        .c3 {\n          display: block\n        }\n      }\n      @media all and (min-width: 0px) {\n        .c1 {\n          display: block\n        }\n      }\n      @media all and (min-width: 300px) {\n        .c2 {\n          display: flex\n        }\n      }\"\n    `);\n  });\n\n  test(\"keep the sort order when minWidth is not defined\", () => {\n    let sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"x\");\n    sheet.addNestingRule(\".c0\").setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    });\n    sheet.addNestingRule(\".c1\").setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all {\n        .c0 {\n          display: block\n        }\n        .c1 {\n          display: flex\n        }\n      }\"\n    `);\n\n    sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"x\");\n    sheet.addNestingRule(\".c1\").setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"flex\" },\n    });\n    sheet.addNestingRule(\".c0\").setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all {\n        .c1 {\n          display: flex\n        }\n        .c0 {\n          display: block\n        }\n      }\"\n    `);\n  });\n\n  test(\"hyphenate property\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(mediaId0, mediaRuleOptions0);\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"backgroundColor\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 0px) {\n        .c {\n          background-color: red\n        }\n      }\"\n    `);\n  });\n\n  test(\"update media rule options\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(mediaId0, mediaRuleOptions0);\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 0px) {\n        .c {\n          color: red\n        }\n      }\"\n    `);\n    sheet.addMediaRule(mediaId0, { minWidth: 10 });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 10px) {\n        .c {\n          color: red\n        }\n      }\"\n    `);\n  });\n\n  test(\"update condition media rule to simulated mediaType\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"base\", {});\n    sheet.addMediaRule(\"dark\", { condition: \"prefers-color-scheme:dark\" });\n    sheet.addMediaRule(\"light\", { condition: \"prefers-color-scheme:light\" });\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"dark\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"light\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"green\" },\n    });\n    // simulate selecting \"dark\" breakpoint\n    sheet.addMediaRule(\"dark\", { mediaType: \"all\" });\n    sheet.addMediaRule(\"light\", { mediaType: \"not all\" });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all {\n        .c {\n          color: red\n        }\n      }\n      @media all {\n        .c {\n          color: blue\n        }\n      }\n      @media not all {\n        .c {\n          color: green\n        }\n      }\"\n    `);\n  });\n\n  test(\"don't override media queries\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(mediaId0, mediaRuleOptions0);\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 0px) {\n        .c {\n          display: block\n        }\n      }\"\n    `);\n    sheet.addMediaRule(mediaId0, mediaRuleOptions0);\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all and (min-width: 0px) {\n        .c {\n          display: block\n        }\n      }\"\n    `);\n  });\n\n  test(\"plaintext rule\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addPlaintextRule(\".c { color: red }\");\n    expect(sheet.cssText).toMatchInlineSnapshot(`\".c { color: red }\"`);\n  });\n\n  test(\"plaintext - no duplicates\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addPlaintextRule(\".c { color: red }\");\n    sheet.addPlaintextRule(\".c { color: red }\");\n    sheet.addPlaintextRule(\".c { color: green }\");\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \".c { color: red }\n      .c { color: green }\"\n    `);\n  });\n\n  test(\"font family rule with space in the name\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addFontFaceRule({\n      fontFamily: \"Some Font\",\n      fontStyle: \"normal\",\n      fontWeight: 400,\n      fontDisplay: \"swap\",\n      src: \"url(/src)\",\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n\"@font-face {\n  font-family: \"Some Font\"; font-style: normal; font-weight: 400; font-display: swap; src: url(/src);\n}\"\n`);\n  });\n\n  test(\"render images with injected asset url\", () => {\n    const sheet = createRegularStyleSheet();\n    const assets = new Map<string, { path: string }>([\n      [\"1234\", { path: \"foo.png\" }],\n    ]);\n    sheet.setTransformer((styleValue) => {\n      if (styleValue.type === \"image\" && styleValue.value.type === \"asset\") {\n        const asset = assets.get(styleValue.value.value);\n        if (asset === undefined) {\n          return { type: \"keyword\", value: \"none\" };\n        }\n        return {\n          type: \"image\",\n          value: {\n            type: \"url\",\n            url: asset.path,\n          },\n        };\n      }\n    });\n    sheet.addMediaRule(\"0\");\n    sheet.addNestingRule(\".c\").setDeclaration({\n      breakpoint: \"0\",\n      selector: \"\",\n      property: \"backgroundImage\",\n      value: {\n        type: \"image\",\n        value: {\n          type: \"asset\",\n          value: \"1234\",\n        },\n      },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n      \"@media all {\n        .c {\n          background-image: url(\"foo.png\")\n        }\n      }\"\n    `);\n  });\n\n  test(\"render nesting rules\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"base\", {});\n    sheet.addMediaRule(\"small\", { minWidth: 768 });\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"small\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n\"@media all {\n  .instance {\n    color: red\n  }\n}\n@media all and (min-width: 768px) {\n  .instance {\n    color: blue\n  }\n}\"\n`);\n  });\n\n  test(\"render mixin rules\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"base\", {});\n    sheet.addMediaRule(\"small\", { minWidth: 768 });\n    const mixin = sheet.addMixinRule(\"local\");\n    mixin.setDeclaration({\n      breakpoint: \"small\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    mixin.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    });\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.applyMixins([\"local\"]);\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n\"@media all {\n  .instance {\n    color: red\n  }\n}\n@media all and (min-width: 768px) {\n  .instance {\n    color: blue\n  }\n}\"\n`);\n  });\n\n  test(\"avoid rendering empty media queries\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"base\", {});\n    sheet.addMediaRule(\"small\", { minWidth: 768 });\n    const rule = sheet.addNestingRule(\".instance\");\n    rule.setDeclaration({\n      breakpoint: \"base\",\n      selector: \"\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n\"@media all {\n  .instance {\n    color: blue\n  }\n}\"\n`);\n  });\n\n  test(\"support descendant suffix\", () => {\n    const sheet = createRegularStyleSheet();\n    sheet.addMediaRule(\"base\", {});\n    const rule1 = sheet.addNestingRule(\".instance\");\n    rule1.setDeclaration({\n      breakpoint: \"base\",\n      selector: \":hover\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    const rule2 = sheet.addNestingRule(\".instance\", \" img\");\n    rule2.setDeclaration({\n      breakpoint: \"base\",\n      selector: \":hover\",\n      property: \"width\",\n      value: { type: \"keyword\", value: \"auto\" },\n    });\n    expect(sheet.cssText).toMatchInlineSnapshot(`\n\"@media all {\n  .instance:hover {\n    width: auto\n  }\n  .instance img:hover {\n    width: auto\n  }\n}\"\n`);\n  });\n});\n\ntest(\"generate merged properties as single rule\", () => {\n  const sheet = createRegularStyleSheet();\n  sheet.addMediaRule(\"x\");\n  const setMargins = (rule: NestingRule, value: StyleValue) => {\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginTop\",\n      value,\n    });\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginRight\",\n      value,\n    });\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginBottom\",\n      value,\n    });\n    rule.setDeclaration({\n      breakpoint: \"x\",\n      selector: \"\",\n      property: \"marginLeft\",\n      value,\n    });\n  };\n  setMargins(sheet.addNestingRule(\"instance\"), {\n    type: \"keyword\",\n    value: \"auto\",\n  });\n  setMargins(sheet.addNestingRule(\"instance\", \" img\"), {\n    type: \"unit\",\n    value: 10,\n    unit: \"px\",\n  });\n  expect(sheet.cssText).toMatchInlineSnapshot(`\n\"@media all {\n  instance {\n    margin: auto\n  }\n  instance img {\n    margin: 10px\n  }\n}\"\n`);\n});\n\ntest(\"convert :local-link to [aria-current=page] selector\", () => {\n  const sheet = createRegularStyleSheet();\n  const rule = sheet.addNestingRule(\".instance\");\n  rule.setDeclaration({\n    breakpoint: \"base\",\n    selector: \":local-link\",\n    property: \"color\",\n    value: { type: \"keyword\", value: \"green\" },\n  });\n  expect(rule.toString({ breakpoint: \"base\" })).toMatchInlineSnapshot(`\n    \".instance[aria-current=page] {\n      color: green\n    }\"\n  `);\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/style-sheet-regular.ts",
    "content": "import { StyleSheet } from \"./style-sheet\";\n\nexport class StyleSheetRegular extends StyleSheet {}\n"
  },
  {
    "path": "packages/css-engine/src/core/style-sheet.ts",
    "content": "import {\n  FontFaceRule,\n  MediaRule,\n  MixinRule,\n  NestingRule,\n  PlaintextRule,\n  type FontFaceOptions,\n  type MediaRuleOptions,\n} from \"./rules\";\nimport { compareMedia } from \"./compare-media\";\nimport { StyleElement, FakeStyleElement } from \"./style-element\";\nimport type { TransformValue } from \"./to-value\";\n\nexport class StyleSheet {\n  #cssText = \"\";\n  #mediaRules: Map<string, MediaRule> = new Map();\n  #plainRules: Map<string, PlaintextRule> = new Map();\n  #mixinRules: Map<string, MixinRule> = new Map();\n  nestingRules: Map<string, NestingRule> = new Map();\n  #fontFaceRules: Array<FontFaceRule> = [];\n  #transformValue?: TransformValue;\n  #element: StyleElement | FakeStyleElement;\n  constructor(element: StyleElement | FakeStyleElement) {\n    this.#element = element;\n  }\n  setTransformer(transformValue: TransformValue) {\n    this.#transformValue = transformValue;\n  }\n  addMediaRule(id: string, options?: MediaRuleOptions) {\n    let mediaRule = this.#mediaRules.get(id);\n    if (mediaRule === undefined) {\n      mediaRule = new MediaRule(id, options);\n      this.#mediaRules.set(id, mediaRule);\n      return mediaRule;\n    }\n\n    if (options) {\n      mediaRule.options = options;\n    }\n\n    if (mediaRule === undefined) {\n      // Should be impossible to reach.\n      throw new Error(\"No media rule found\");\n    }\n\n    return mediaRule;\n  }\n  addPlaintextRule(cssText: string) {\n    const rule = this.#plainRules.get(cssText);\n    if (rule !== undefined) {\n      return rule;\n    }\n    return this.#plainRules.set(cssText, new PlaintextRule(cssText));\n  }\n  addMixinRule(name: string) {\n    let rule = this.#mixinRules.get(name);\n    if (rule === undefined) {\n      rule = new MixinRule();\n      this.#mixinRules.set(name, rule);\n    }\n    return rule;\n  }\n  addNestingRule(selector: string, descendantSuffix: string = \"\") {\n    const key = selector + descendantSuffix;\n    let rule = this.nestingRules.get(key);\n    if (rule === undefined) {\n      rule = new NestingRule(this.#mixinRules, selector, descendantSuffix);\n      this.nestingRules.set(key, rule);\n    }\n    return rule;\n  }\n  addFontFaceRule(options: FontFaceOptions) {\n    return this.#fontFaceRules.push(new FontFaceRule(options));\n  }\n  generateWith({\n    nestingRules,\n    transformValue,\n  }: {\n    nestingRules: NestingRule[];\n    transformValue?: TransformValue;\n  }) {\n    const css: Array<string> = [];\n\n    css.push(...this.#fontFaceRules.map((rule) => rule.cssText));\n    for (const plaintextRule of this.#plainRules.values()) {\n      css.push(plaintextRule.cssText);\n    }\n\n    const sortedMediaRules = Array.from(this.#mediaRules.values()).sort(\n      (ruleA, ruleB) => compareMedia(ruleA.options, ruleB.options)\n    );\n    for (const mediaRule of sortedMediaRules) {\n      const cssText = mediaRule.generateRule({\n        nestingRules,\n        transformValue,\n      });\n      if (cssText !== \"\") {\n        css.push(cssText);\n      }\n    }\n    // reset invalidation from mixins after rendering\n    for (const rule of this.#mixinRules.values()) {\n      rule.clearBreakpoints();\n    }\n    this.#cssText = css.join(\"\\n\");\n    return this.#cssText;\n  }\n  get cssText() {\n    return this.generateWith({\n      nestingRules: Array.from(this.nestingRules.values()),\n      transformValue: this.#transformValue,\n    });\n  }\n  clear() {\n    this.#mediaRules.clear();\n    this.#mixinRules.clear();\n    this.nestingRules.clear();\n    this.#plainRules.clear();\n    this.#fontFaceRules = [];\n  }\n  render() {\n    this.#element.mount();\n    this.#element.render(this.cssText);\n  }\n  unmount() {\n    this.#element.unmount();\n  }\n  setAttribute(name: string, value: string) {\n    this.#element.setAttribute(name, value);\n  }\n  getAttribute(name: string) {\n    return this.#element.getAttribute(name);\n  }\n}\n"
  },
  {
    "path": "packages/css-engine/src/core/to-property.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { hyphenateProperty } from \"./to-property\";\n\ndescribe(\"hyphenateProperty\", () => {\n  test(\"hyphenates regular css\", () => {\n    expect(hyphenateProperty(\"backgroundColor\")).toEqual(\"background-color\");\n    expect(hyphenateProperty(\"fontSize\")).toEqual(\"font-size\");\n    expect(hyphenateProperty(\"color\")).toEqual(\"color\");\n    expect(hyphenateProperty(\"borderTopLeftRadius\")).toEqual(\n      \"border-top-left-radius\"\n    );\n  });\n\n  test(\"hyphenates vendor prefixes correctly\", () => {\n    expect(hyphenateProperty(\"MozTransition\")).toEqual(\"-moz-transition\");\n    expect(hyphenateProperty(\"WebkitTransition\")).toEqual(\"-webkit-transition\");\n  });\n\n  test(\"hyphenating is idempotent\", () => {\n    expect(hyphenateProperty(hyphenateProperty(\"-moz-transition\"))).toEqual(\n      \"-moz-transition\"\n    );\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/to-property.ts",
    "content": "import type { CssProperty } from \"../schema\";\n\n/**\n * Hyphenates a camelcased CSS property name\n */\nexport const hyphenateProperty = (property: string): CssProperty =>\n  property.replace(\n    /[A-Z]/g,\n    (match) => \"-\" + match.toLowerCase()\n  ) as CssProperty;\n"
  },
  {
    "path": "packages/css-engine/src/core/to-value.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { toValue } from \"./to-value\";\n\ndescribe(\"Convert WS CSS Values to native CSS strings\", () => {\n  test(\"keyword\", () => {\n    const value = toValue({ type: \"keyword\", value: \"red\" });\n    expect(value).toBe(\"red\");\n  });\n\n  test(\"unit\", () => {\n    const value = toValue({ type: \"unit\", value: 10, unit: \"px\" });\n    expect(value).toBe(\"10px\");\n  });\n\n  test(\"invalid\", () => {\n    const value = toValue({ type: \"invalid\", value: \"bad\" });\n    expect(value).toBe(\"bad\");\n  });\n\n  test(\"unset\", () => {\n    const value = toValue({ type: \"unset\", value: \"\" });\n    expect(value).toBe(\"\");\n  });\n\n  test(\"var\", () => {\n    const value = toValue({ type: \"var\", value: \"namespace\" });\n    expect(value).toBe(\"var(--namespace)\");\n  });\n\n  test(\"var with fallbacks\", () => {\n    const value = toValue({\n      type: \"var\",\n      value: \"namespace\",\n      fallback: {\n        type: \"unparsed\",\n        value: \"normal, 10px\",\n      },\n    });\n    expect(value).toBe(\"var(--namespace, normal, 10px)\");\n  });\n\n  test(\"fontFamily is known stack name\", () => {\n    expect(\n      toValue({\n        type: \"fontFamily\",\n        value: [\"Humanist\"],\n      })\n    ).toBe(\n      'Seravek, \"Gill Sans Nova\", Ubuntu, Calibri, \"DejaVu Sans\", source-sans-pro, sans-serif'\n    );\n  });\n\n  test(\"fontFamily is a custom stack\", () => {\n    expect(\n      toValue({\n        type: \"fontFamily\",\n        value: [\"DejaVu Sans Mono\", \"monospace\"],\n      })\n    ).toBe('\"DejaVu Sans Mono\", monospace');\n  });\n\n  test(\"fontFamily is unknown family name\", () => {\n    expect(\n      toValue({\n        type: \"fontFamily\",\n        value: [\"something-random\"],\n      })\n    ).toBe(\"something-random, sans-serif\");\n  });\n\n  test(\"fontFamily is empty\", () => {\n    expect(\n      toValue({\n        type: \"fontFamily\",\n        value: [],\n      })\n    ).toBe(\"sans-serif\");\n  });\n\n  test(\"fontFamily has duplicates\", () => {\n    expect(\n      toValue({\n        type: \"fontFamily\",\n        value: [\"a\", \"a\", \"b\"],\n      })\n    ).toBe(\"a, b\");\n  });\n\n  test(\"Transform font family value to override default fallback\", () => {\n    const value = toValue(\n      {\n        type: \"fontFamily\",\n        value: [\"Courier New\"],\n      },\n      (styleValue) => {\n        if (styleValue.type === \"fontFamily\") {\n          return {\n            type: \"fontFamily\",\n            value: [\"A B\"],\n          };\n        }\n      }\n    );\n    expect(value).toBe('\"A B\"');\n  });\n\n  test(\"array\", () => {\n    const assets = new Map<string, { path: string }>([\n      [\"1234567890\", { path: \"foo.png\" }],\n    ]);\n\n    const value = toValue(\n      {\n        type: \"layers\",\n        value: [\n          {\n            type: \"keyword\",\n            value: \"auto\",\n          },\n          { type: \"unit\", value: 10, unit: \"px\" },\n          { type: \"unparsed\", value: \"calc(10px)\" },\n          {\n            type: \"image\",\n            value: {\n              type: \"asset\",\n              value: \"1234567890\",\n            },\n          },\n        ],\n      },\n      (styleValue) => {\n        if (styleValue.type === \"image\" && styleValue.value.type === \"asset\") {\n          const asset = assets.get(styleValue.value.value);\n          if (asset === undefined) {\n            return {\n              type: \"keyword\",\n              value: \"none\",\n            };\n          }\n          return {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: asset.path,\n            },\n          };\n        }\n      }\n    );\n\n    expect(value).toBe(`auto, 10px, calc(10px), url(\"foo.png\")`);\n  });\n\n  test(\"tuple\", () => {\n    const value = toValue({\n      type: \"tuple\",\n      value: [\n        { type: \"unit\", value: 10, unit: \"px\" },\n        { type: \"unit\", value: 20, unit: \"px\" },\n        { type: \"unit\", value: 30, unit: \"px\" },\n        { type: \"unit\", value: 40, unit: \"px\" },\n      ],\n    });\n    expect(value).toBe(\"10px 20px 30px 40px\");\n  });\n\n  test(\"function\", () => {\n    const translate3D = toValue({\n      type: \"function\",\n      name: \"translate3d\",\n      args: {\n        type: \"keyword\",\n        value: \"42px, -62px, -135px\",\n      },\n    });\n\n    const dropShadowValue = toValue({\n      type: \"function\",\n      name: \"drop-shadow\",\n      args: {\n        type: \"shadow\",\n        position: \"outset\",\n        offsetX: { type: \"unit\", value: 10, unit: \"px\" },\n        offsetY: { type: \"unit\", value: 10, unit: \"px\" },\n        blur: { type: \"unit\", value: 10, unit: \"px\" },\n        color: { type: \"keyword\", value: \"red\" },\n      },\n    });\n\n    expect(translate3D).toBe(\"translate3d(42px, -62px, -135px)\");\n    expect(dropShadowValue).toBe(\"drop-shadow(10px 10px 10px red)\");\n  });\n\n  test(\"sanitize url\", () => {\n    const assets = new Map<string, { path: string }>([\n      [\"1234567890\", { path: `fo\"o\\\\o.png` }],\n    ]);\n\n    const value = toValue(\n      {\n        type: \"image\",\n        value: {\n          type: \"asset\",\n          value: \"1234567890\",\n        },\n      },\n      (styleValue) => {\n        if (styleValue.type === \"image\" && styleValue.value.type === \"asset\") {\n          const asset = assets.get(styleValue.value.value);\n          if (asset === undefined) {\n            return {\n              type: \"keyword\",\n              value: \"none\",\n            };\n          }\n          return {\n            type: \"image\",\n            value: {\n              type: \"url\",\n              url: asset.path,\n            },\n          };\n        }\n      }\n    );\n\n    expect(value).toMatchInlineSnapshot(`\"url(\"fo\\\\\"o\\\\\\\\o.png\")\"`);\n  });\n\n  test(\"guaranteed-invalid\", () => {\n    const value = toValue({\n      type: \"guaranteedInvalid\",\n    });\n    expect(value).toBe(\"\");\n  });\n\n  test(\"color with srgb color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [1, 0.5, 0.2],\n      alpha: 1,\n    });\n    expect(value).toBe(\"rgb(255 128 51 / 1)\");\n  });\n\n  test(\"color with srgb and alpha channel\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [1, 0, 0],\n      alpha: 0.5,\n    });\n    expect(value).toBe(\"rgb(255 0 0 / 0.5)\");\n  });\n\n  test(\"color with srgb and zero alpha\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"srgb\",\n      components: [0.5, 0.5, 0.5],\n      alpha: 0,\n    });\n    expect(value).toBe(\"rgb(128 128 128 / 0)\");\n  });\n\n  test(\"color with hsl color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"hsl\",\n      components: [120, 100, 50],\n      alpha: 1,\n    });\n    expect(value).toBe(\"hsl(120 100% 50% / 1)\");\n  });\n\n  test(\"color with hwb color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"hwb\",\n      components: [45, 10, 20],\n      alpha: 1,\n    });\n    expect(value).toBe(\"hwb(45 10% 20% / 1)\");\n  });\n\n  test(\"color with lab color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"lab\",\n      components: [50, 20, 30],\n      alpha: 1,\n    });\n    expect(value).toBe(\"lab(50% 20 30 / 1)\");\n  });\n\n  test(\"color with lch color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"lch\",\n      components: [50, 20, 120],\n      alpha: 1,\n    });\n    expect(value).toBe(\"lch(50% 20 120 / 1)\");\n  });\n\n  test(\"color with oklab color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"oklab\",\n      components: [0.6, 0.1, -0.1],\n      alpha: 1,\n    });\n    expect(value).toBe(\"oklab(0.6 0.1 -0.1 / 1)\");\n  });\n\n  test(\"color with oklch color space\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"oklch\",\n      components: [0.5, 0.1, 180],\n      alpha: 1,\n    });\n    expect(value).toBe(\"oklch(0.5 0.1 180 / 1)\");\n  });\n\n  test(\"color with p3 color space uses color() function\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"p3\",\n      components: [0.8, 0.4, 0.6],\n      alpha: 0.8,\n    });\n    expect(value).toBe(\"color(p3 0.8 0.4 0.6 / 0.8)\");\n  });\n\n  test(\"color with xyz-d65 uses color() function\", () => {\n    const value = toValue({\n      type: \"color\",\n      colorSpace: \"xyz-d65\",\n      components: [0.5, 0.3, 0.2],\n      alpha: 1,\n    });\n    expect(value).toBe(\"color(xyz-d65 0.5 0.3 0.2 / 1)\");\n  });\n\n  test(\"color in tuple\", () => {\n    const value = toValue({\n      type: \"tuple\",\n      value: [\n        {\n          type: \"color\",\n          colorSpace: \"oklch\",\n          components: [0.5, 0.1, 180],\n          alpha: 1,\n        },\n        { type: \"unit\", value: 10, unit: \"px\" },\n      ],\n    });\n    expect(value).toBe(\"oklch(0.5 0.1 180 / 1) 10px\");\n  });\n\n  test(\"color in shadow\", () => {\n    const value = toValue({\n      type: \"shadow\",\n      position: \"outset\",\n      offsetX: { type: \"unit\", value: 1, unit: \"px\" },\n      offsetY: { type: \"unit\", value: 2, unit: \"px\" },\n      color: {\n        type: \"color\",\n        colorSpace: \"hsl\",\n        components: [240, 100, 50],\n        alpha: 0.7,\n      },\n    });\n    expect(value).toBe(\"1px 2px hsl(240 100% 50% / 0.7)\");\n  });\n});\n\ndescribe(\"serialize shadow value\", () => {\n  test(\"minimal value\", () => {\n    expect(\n      toValue({\n        type: \"layers\",\n        value: [\n          {\n            type: \"shadow\",\n            position: \"outset\",\n            offsetX: { type: \"unit\", value: 1, unit: \"px\" },\n            offsetY: { type: \"unit\", value: 2, unit: \"px\" },\n          },\n        ],\n      })\n    ).toEqual(\"1px 2px\");\n  });\n\n  test(\"full value\", () => {\n    expect(\n      toValue({\n        type: \"layers\",\n        value: [\n          {\n            type: \"shadow\",\n            position: \"inset\",\n            offsetX: { type: \"unit\", value: 1, unit: \"px\" },\n            offsetY: { type: \"unit\", value: 2, unit: \"px\" },\n            blur: { type: \"unit\", value: 3, unit: \"px\" },\n            spread: { type: \"unit\", value: 4, unit: \"px\" },\n            color: { type: \"rgb\", r: 0, g: 0, b: 0, alpha: 1 },\n          },\n        ],\n      })\n    ).toEqual(\"1px 2px 3px 4px rgb(0 0 0 / 1) inset\");\n  });\n\n  test(\"hidden value\", () => {\n    expect(\n      toValue({\n        type: \"layers\",\n        value: [\n          {\n            type: \"shadow\",\n            hidden: true,\n            position: \"outset\",\n            offsetX: { type: \"unit\", value: 1, unit: \"px\" },\n            offsetY: { type: \"unit\", value: 2, unit: \"px\" },\n          },\n        ],\n      })\n    ).toEqual(\"none\");\n  });\n\n  test(\"multiple values\", () => {\n    expect(\n      toValue({\n        type: \"layers\",\n        value: [\n          {\n            type: \"shadow\",\n            position: \"outset\",\n            offsetX: { type: \"unit\", value: 1, unit: \"px\" },\n            offsetY: { type: \"unit\", value: 2, unit: \"px\" },\n          },\n          {\n            type: \"shadow\",\n            position: \"outset\",\n            offsetX: { type: \"unit\", value: 3, unit: \"px\" },\n            offsetY: { type: \"unit\", value: 4, unit: \"px\" },\n          },\n        ],\n      })\n    ).toEqual(\"1px 2px, 3px 4px\");\n  });\n});\n"
  },
  {
    "path": "packages/css-engine/src/core/to-value.ts",
    "content": "import { DEFAULT_FONT_FALLBACK, SYSTEM_FONTS } from \"@webstudio-is/fonts\";\nimport type { StyleValue } from \"../schema\";\n\nexport type TransformValue = (styleValue: StyleValue) => undefined | StyleValue;\n\nconst fallbackTransform: TransformValue = (styleValue) => {\n  if (styleValue.type !== \"fontFamily\") {\n    return;\n  }\n\n  // By default we assume its a custom font stack.\n  let { value } = styleValue;\n\n  // Shouldn't be possible, but just in case.\n  if (value.length === 0) {\n    value = [DEFAULT_FONT_FALLBACK];\n  }\n\n  // User provided a single name. It could be a specific font name or a stack name.\n  if (value.length === 1) {\n    const stack = SYSTEM_FONTS.get(value[0])?.stack;\n    value = stack ?? [value[0], DEFAULT_FONT_FALLBACK];\n  }\n\n  return {\n    type: \"fontFamily\",\n    value: Array.from(new Set(value)),\n  };\n};\n\n// Use JSON.stringify to escape double quotes and backslashes in strings as it automatically replaces \" with \\\" and \\ with \\\\.\nconst sanitizeCssUrl = (str: string) => JSON.stringify(str);\n\nexport const toValue = (\n  styleValue: undefined | StyleValue,\n  transformValue?: TransformValue\n): string => {\n  if (styleValue === undefined) {\n    return \"\";\n  }\n  const transformedValue =\n    transformValue?.(styleValue) ?? fallbackTransform(styleValue);\n  const value = transformedValue ?? styleValue;\n  if (value.type === \"unit\") {\n    return value.value + (value.unit === \"number\" ? \"\" : value.unit);\n  }\n  if (value.type === \"fontFamily\") {\n    const families = [];\n    for (const family of value.value) {\n      families.push(family.includes(\" \") ? `\"${family}\"` : family);\n    }\n    return families.join(\", \");\n  }\n  if (value.type === \"var\") {\n    if (value.hidden) {\n      return \"\";\n    }\n    let fallbacksString = \"\";\n    if (value.fallback) {\n      fallbacksString = `, ${toValue(value.fallback, transformValue)}`;\n    }\n    return `var(--${value.value}${fallbacksString})`;\n  }\n\n  if (value.type === \"keyword\") {\n    // The hidden property is used to hide values in the builder\n    // But we can't use none here like its done for image.\n    // As none is not valid in all cases.\n    // Eg: backface-visibility\n    // https://developer.mozilla.org/en-US/docs/Web/CSS/backface-visibility#syntax\n    if (value.hidden === true) {\n      return \"\";\n    }\n\n    return value.value;\n  }\n\n  if (value.type === \"invalid\") {\n    return value.value;\n  }\n\n  if (value.type === \"unset\") {\n    return value.value;\n  }\n\n  if (value.type === \"rgb\") {\n    return `rgb(${value.r} ${value.g} ${value.b} / ${value.alpha})`;\n  }\n\n  if (value.type === \"color\") {\n    let [c1, c2, c3] = value.components;\n    const alpha = value.alpha;\n\n    // Use specific CSS functions when available\n    switch (value.colorSpace) {\n      case \"srgb\": {\n        c1 = Math.round(c1 * 255);\n        c2 = Math.round(c2 * 255);\n        c3 = Math.round(c3 * 255);\n        return `rgb(${c1} ${c2} ${c3} / ${alpha})`;\n      }\n      case \"hsl\":\n        return `hsl(${c1} ${c2}% ${c3}% / ${alpha})`;\n      case \"hwb\":\n        return `hwb(${c1} ${c2}% ${c3}% / ${alpha})`;\n      case \"lab\":\n        return `lab(${c1}% ${c2} ${c3} / ${alpha})`;\n      case \"lch\":\n        return `lch(${c1}% ${c2} ${c3} / ${alpha})`;\n      case \"oklab\":\n        return `oklab(${c1} ${c2} ${c3} / ${alpha})`;\n      case \"oklch\":\n        return `oklch(${c1} ${c2} ${c3} / ${alpha})`;\n      // Fall back to color() function for less common color spaces\n      case \"p3\":\n      case \"srgb-linear\":\n      case \"a98rgb\":\n      case \"prophoto\":\n      case \"rec2020\":\n      case \"xyz-d65\":\n      case \"xyz-d50\":\n      default:\n        return `color(${value.colorSpace} ${c1} ${c2} ${c3} / ${alpha})`;\n    }\n  }\n\n  if (value.type === \"image\") {\n    if (value.hidden || value.value.type !== \"url\") {\n      // We assume that property is background-image and use this to hide background layers\n      // In the future we might want to have a more generic way to hide values\n      // i.e. have knowledge about property-name, as none is property specific\n      return \"none\";\n    }\n\n    // @todo image-set\n    return `url(${sanitizeCssUrl(value.value.url)})`;\n  }\n\n  if (value.type === \"unparsed\") {\n    if (value.hidden === true) {\n      // We assume that property is background-image and use this to hide background layers\n      // In the future we might want to have a more generic way to hide values\n      // i.e. have knowledge about property-name, as none is property specific\n      return \"none\";\n    }\n\n    return value.value;\n  }\n\n  if (value.type === \"layers\") {\n    const valueString = value.value\n      .filter((layer) => layer.hidden !== true)\n      .map((layer) => toValue(layer, transformValue))\n      .join(\", \");\n    return valueString === \"\" ? \"none\" : valueString;\n  }\n\n  if (value.type === \"tuple\") {\n    // Properties ike translate and scale are handled as tuples directly.\n    // When the layer is hidden, the value goes as none.\n    if (value.hidden === true) {\n      return \"none\";\n    }\n\n    return value.value\n      .filter((value) => value.hidden !== true)\n      .map((value) => toValue(value, transformValue))\n      .join(\" \");\n  }\n\n  if (value.type === \"shadow\") {\n    let shadow = `${toValue(value.offsetX)} ${toValue(value.offsetY)}`;\n    if (value.blur) {\n      shadow += ` ${toValue(value.blur)}`;\n    }\n    if (value.spread) {\n      shadow += ` ${toValue(value.spread)}`;\n    }\n    if (value.color) {\n      shadow += ` ${toValue(value.color)}`;\n    }\n    if (value.position === \"inset\") {\n      shadow += ` inset`;\n    }\n    return shadow;\n  }\n\n  if (value.type === \"function\") {\n    // Right now, we are using function-value only for filter and backdrop-filter functions\n    if (value.hidden === true) {\n      return \"\";\n    }\n\n    return `${value.name}(${toValue(value.args, transformValue)})`;\n  }\n\n  // https://www.w3.org/TR/css-variables-1/#guaranteed-invalid\n  if (value.type === \"guaranteedInvalid\") {\n    return \"\";\n  }\n\n  value satisfies never;\n  return \"\";\n};\n"
  },
  {
    "path": "packages/css-engine/src/css.ts",
    "content": "export const cssWideKeywords = new Set([\n  \"initial\",\n  \"inherit\",\n  \"unset\",\n  \"revert\",\n  \"revert-layer\",\n]);\n"
  },
  {
    "path": "packages/css-engine/src/index.ts",
    "content": "export * from \"./core/index\";\nexport * from \"./schema\";\nexport * from \"./css\";\n\n// necessary for sdk dts generation\nexport type { Unit as __Unit } from \"./__generated__/types\";\nexport type { HyphenatedProperty as __HyphenatedProperty } from \"./__generated__/types\";\n"
  },
  {
    "path": "packages/css-engine/src/runtime.ts",
    "content": "export { toValue } from \"./core/to-value\";\n"
  },
  {
    "path": "packages/css-engine/src/schema.ts",
    "content": "import { z } from \"zod\";\nimport type {\n  CamelCasedProperty,\n  HyphenatedProperty,\n  Unit as GeneratedUnit,\n} from \"./__generated__/types\";\nimport { toValue, type TransformValue } from \"./core/to-value\";\n\nexport type CustomProperty = `--${string}`;\n\nexport type StyleProperty = CamelCasedProperty | CustomProperty;\n\nexport type CssProperty = HyphenatedProperty | CustomProperty;\n\nexport type CssStyleMap = Map<CssProperty, StyleValue>;\n\nconst Unit = z.string() as z.ZodType<GeneratedUnit | \"number\">;\n\nexport type Unit = z.infer<typeof Unit>;\n\nexport const UnitValue = z.object({\n  type: z.literal(\"unit\"),\n  unit: Unit,\n  value: z.number(),\n  hidden: z.boolean().optional(),\n});\n\nexport type UnitValue = z.infer<typeof UnitValue>;\n\nexport const KeywordValue = z.object({\n  type: z.literal(\"keyword\"),\n  // @todo use exact type\n  value: z.string(),\n  hidden: z.boolean().optional(),\n});\nexport type KeywordValue = z.infer<typeof KeywordValue>;\n\n/**\n * Valid unparsed css value\n **/\nexport const UnparsedValue = z.object({\n  type: z.literal(\"unparsed\"),\n  value: z.string(),\n  // For the builder we want to be able to hide background-image\n  hidden: z.boolean().optional(),\n});\n\nexport type UnparsedValue = z.infer<typeof UnparsedValue>;\n\nconst FontFamilyValue = z.object({\n  type: z.literal(\"fontFamily\"),\n  value: z.array(z.string()),\n  hidden: z.boolean().optional(),\n});\nexport type FontFamilyValue = z.infer<typeof FontFamilyValue>;\n\nconst RgbValue = z.object({\n  type: z.literal(\"rgb\"),\n  r: z.number(),\n  g: z.number(),\n  b: z.number(),\n  alpha: z.number(),\n  hidden: z.boolean().optional(),\n});\nexport type RgbValue = z.infer<typeof RgbValue>;\n\nexport const ColorValue = z.object({\n  type: z.literal(\"color\"),\n  // all these color spaces are defined by design tokens specification\n  colorSpace: z.union([\n    z.literal(\"srgb\"),\n    z.literal(\"p3\"),\n    z.literal(\"srgb-linear\"),\n    z.literal(\"hsl\"),\n    z.literal(\"hwb\"),\n    z.literal(\"lab\"),\n    z.literal(\"lch\"),\n    z.literal(\"oklab\"),\n    z.literal(\"oklch\"),\n    z.literal(\"a98rgb\"),\n    z.literal(\"prophoto\"),\n    z.literal(\"rec2020\"),\n    z.literal(\"xyz-d65\"),\n    z.literal(\"xyz-d50\"),\n  ]),\n  components: z.tuple([z.number(), z.number(), z.number()]),\n  alpha: z.number(),\n  hidden: z.boolean().optional(),\n});\nexport type ColorValue = z.infer<typeof ColorValue>;\n\nexport type FunctionValue = z.infer<typeof FunctionValue>;\n\nexport const FunctionValue: z.ZodType<{\n  type: \"function\";\n  name: string;\n  args: StyleValue;\n  hidden?: boolean;\n}> = z.object({\n  type: z.literal(\"function\"),\n  name: z.string(),\n  args: z.lazy(() => StyleValue),\n  hidden: z.boolean().optional(),\n});\n\nexport const ImageValue = z.object({\n  type: z.literal(\"image\"),\n  value: z.union([\n    z.object({ type: z.literal(\"asset\"), value: z.string() }),\n    // url is not stored in db and only used by css-engine transformValue\n    // to prepare image value for rendering\n    z.object({ type: z.literal(\"url\"), url: z.string() }),\n  ]),\n  // For the builder we want to be able to hide images\n  hidden: z.boolean().optional(),\n});\n\nexport type ImageValue = z.infer<typeof ImageValue>;\n\n// initial value of custom properties\n// https://www.w3.org/TR/css-variables-1/#guaranteed-invalid\nexport const GuaranteedInvalidValue = z.object({\n  type: z.literal(\"guaranteedInvalid\"),\n  hidden: z.boolean().optional(),\n});\nexport type GuaranteedInvalidValue = z.infer<typeof GuaranteedInvalidValue>;\n\n// We want to be able to render the invalid value\n// and show it is invalid visually, without saving it to the db\nexport const InvalidValue = z.object({\n  type: z.literal(\"invalid\"),\n  value: z.string(),\n  hidden: z.boolean().optional(),\n});\nexport type InvalidValue = z.infer<typeof InvalidValue>;\n\n/**\n * Use GuaranteedInvalidValue if you need a temp placeholder before user enters a value\n * @deprecated\n */\nconst UnsetValue = z.object({\n  type: z.literal(\"unset\"),\n  value: z.literal(\"\"),\n  hidden: z.boolean().optional(),\n});\nexport type UnsetValue = z.infer<typeof UnsetValue>;\n\nexport const VarFallback = z.union([\n  UnparsedValue,\n  KeywordValue,\n  UnitValue,\n  ColorValue,\n  RgbValue,\n]);\nexport type VarFallback = z.infer<typeof VarFallback>;\n\nexport const toVarFallback = (\n  styleValue: StyleValue,\n  transformValue?: TransformValue\n): VarFallback => {\n  if (\n    styleValue.type === \"unparsed\" ||\n    styleValue.type === \"keyword\" ||\n    styleValue.type === \"unit\" ||\n    styleValue.type === \"color\" ||\n    styleValue.type === \"rgb\"\n  ) {\n    return styleValue;\n  }\n  styleValue satisfies Exclude<StyleValue, VarFallback>;\n  return { type: \"unparsed\", value: toValue(styleValue, transformValue) };\n};\n\nconst VarValue = z.object({\n  type: z.literal(\"var\"),\n  value: z.string(),\n  fallback: VarFallback.optional(),\n  hidden: z.boolean().optional(),\n});\nexport type VarValue = z.infer<typeof VarValue>;\n\nexport const TupleValueItem = z.union([\n  UnitValue,\n  KeywordValue,\n  UnparsedValue,\n  ImageValue,\n  ColorValue,\n  RgbValue,\n  FunctionValue,\n  VarValue,\n]);\nexport type TupleValueItem = z.infer<typeof TupleValueItem>;\n\nexport const TupleValue = z.object({\n  type: z.literal(\"tuple\"),\n  value: z.array(TupleValueItem),\n  hidden: z.boolean().optional(),\n});\n\nexport type TupleValue = z.infer<typeof TupleValue>;\n\nexport const ShadowValue = z.object({\n  type: z.literal(\"shadow\"),\n  hidden: z.boolean().optional(),\n  position: z.union([z.literal(\"inset\"), z.literal(\"outset\")]),\n  offsetX: z.union([UnitValue, VarValue]),\n  offsetY: z.union([UnitValue, VarValue]),\n  blur: z.union([UnitValue, VarValue]).optional(),\n  spread: z.union([UnitValue, VarValue]).optional(),\n  color: z.union([ColorValue, RgbValue, KeywordValue, VarValue]).optional(),\n});\n\nexport type ShadowValue = z.infer<typeof ShadowValue>;\n\nconst LayerValueItem = z.union([\n  UnitValue,\n  KeywordValue,\n  UnparsedValue,\n  ImageValue,\n  TupleValue,\n  ShadowValue,\n  ColorValue,\n  RgbValue,\n  InvalidValue,\n  FunctionValue,\n  VarValue,\n]);\n\nexport type LayerValueItem = z.infer<typeof LayerValueItem>;\n// To support background layers https://developer.mozilla.org/en-US/docs/Web/CSS/background\n// and similar comma separated css properties\n// InvalidValue used in case of asset not found\nexport const LayersValue = z.object({\n  type: z.literal(\"layers\"),\n  value: z.array(LayerValueItem),\n  hidden: z.boolean().optional(),\n});\n\nexport type LayersValue = z.infer<typeof LayersValue>;\n\nexport const StyleValue = z.union([\n  ImageValue,\n  LayersValue,\n  UnitValue,\n  KeywordValue,\n  FontFamilyValue,\n  ColorValue,\n  RgbValue,\n  UnparsedValue,\n  TupleValue,\n  FunctionValue,\n  GuaranteedInvalidValue,\n  InvalidValue,\n  UnsetValue,\n  VarValue,\n  ShadowValue,\n]);\n\nexport type StyleValue = z.infer<typeof StyleValue>;\n"
  },
  {
    "path": "packages/css-engine/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/css-engine/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/dashboard/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/dashboard/README.md",
    "content": "# Webstudio Project\n\nProject related functionalities. This was temporarily placed here to reuse between other packages, but we need to split this into separate packages and probably remove this package entirely or keep it only for \"project\" specific functionality.\n"
  },
  {
    "path": "packages/dashboard/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/dashboard\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Dashboard\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/project\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"type-fest\": \"^4.37.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\"\n    },\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/dashboard/src/db/index.ts",
    "content": "export * as db from \"./projects\";\n"
  },
  {
    "path": "packages/dashboard/src/db/projects.ts",
    "content": "import {\n  AuthorizationError,\n  type AppContext,\n} from \"@webstudio-is/trpc-interface/index.server\";\n\ntype DomainVirtual = {\n  domain: string;\n  status: string;\n  verified: boolean;\n};\n\nconst fetchAndMapDomains = async <\n  T extends {\n    id: string;\n    title: string;\n    domain: string;\n    createdAt: string;\n    [key: string]: unknown;\n  },\n>(\n  projects: T[],\n  context: AppContext\n) => {\n  const projectIds = projects.map((project) => project.id);\n\n  type ProjectWithDomains = T & {\n    domainsVirtual: DomainVirtual[];\n  };\n\n  if (projectIds.length === 0) {\n    return projects.map((project) => ({\n      ...project,\n      domainsVirtual: [],\n    })) as ProjectWithDomains[];\n  }\n\n  // Query ProjectDomain and Domain tables\n  const domainsData = await context.postgrest.client\n    .from(\"ProjectDomain\")\n    .select(\"projectId, Domain!inner(domain, status, txtRecord), txtRecord\")\n    .in(\"projectId\", projectIds);\n\n  if (domainsData.error) {\n    console.error(\"Error fetching domains:\", domainsData.error);\n    // Continue without domains rather than failing\n  }\n\n  // Map domains to projects\n  const domainsByProject = new Map<string, DomainVirtual[]>();\n  if (domainsData.data) {\n    for (const projectDomain of domainsData.data) {\n      if (!domainsByProject.has(projectDomain.projectId)) {\n        domainsByProject.set(projectDomain.projectId, []);\n      }\n      // Type assertion needed for joined data\n      const domainData = projectDomain.Domain as unknown as {\n        domain: string;\n        status: string;\n        txtRecord: string;\n      };\n      const verified = domainData.txtRecord === projectDomain.txtRecord;\n      domainsByProject.get(projectDomain.projectId)?.push({\n        domain: domainData.domain,\n        status: domainData.status,\n        verified,\n      });\n    }\n  }\n\n  // Add domains to projects\n  return projects.map((project) => ({\n    ...project,\n    domainsVirtual: project.id ? domainsByProject.get(project.id) || [] : [],\n  })) as ProjectWithDomains[];\n};\n\nexport type DashboardProject = Awaited<ReturnType<typeof findMany>>[number];\n\nexport const findMany = async (userId: string, context: AppContext) => {\n  if (context.authorization.type !== \"user\") {\n    throw new AuthorizationError(\n      \"Only logged in users can view the project list\"\n    );\n  }\n\n  if (userId !== context.authorization.userId) {\n    throw new AuthorizationError(\n      \"Only the project owner can view the project list\"\n    );\n  }\n\n  const data = await context.postgrest.client\n    .from(\"DashboardProject\")\n    .select(\"*, previewImageAsset:Asset (*), latestBuildVirtual (*)\")\n    .eq(\"userId\", userId)\n    .eq(\"isDeleted\", false)\n    .order(\"createdAt\", { ascending: false })\n    .order(\"id\", { ascending: false });\n  if (data.error) {\n    throw data.error;\n  }\n\n  // Type assertion: These fields are never null in practice (come from Project table which has them as required)\n  return await fetchAndMapDomains(\n    data.data as Array<\n      (typeof data.data)[number] & {\n        id: string;\n        title: string;\n        domain: string;\n        createdAt: string;\n      }\n    >,\n    context\n  );\n};\n\nexport const findManyByIds = async (\n  projectIds: string[],\n  context: AppContext\n) => {\n  if (projectIds.length === 0) {\n    return [];\n  }\n\n  // Get the user ID for ownership filtering\n  // Allow service context (no authorization) to access any projects (for templates)\n  const userId =\n    context.authorization.type === \"user\"\n      ? context.authorization.userId\n      : undefined;\n\n  let query = context.postgrest.client\n    .from(\"DashboardProject\")\n    .select(\"*, previewImageAsset:Asset (*), latestBuildVirtual (*)\")\n    .in(\"id\", projectIds)\n    .eq(\"isDeleted\", false);\n\n  // If user context, also filter by userId OR isMarketplaceApproved (public templates)\n  if (userId !== undefined) {\n    query = query.or(\n      `userId.eq.${userId},marketplaceApprovalStatus.eq.APPROVED`\n    );\n  }\n\n  const data = await query\n    .order(\"createdAt\", { ascending: false })\n    .order(\"id\", { ascending: false });\n  if (data.error) {\n    throw data.error;\n  }\n\n  // Type assertion: These fields are never null in practice (come from Project table which has them as required)\n  return await fetchAndMapDomains(\n    data.data as Array<\n      (typeof data.data)[number] & {\n        id: string;\n        title: string;\n        domain: string;\n        createdAt: string;\n      }\n    >,\n    context\n  );\n};\n"
  },
  {
    "path": "packages/dashboard/src/index.server.ts",
    "content": "export * as db from \"./db\";\nexport * from \"./trpc\";\n"
  },
  {
    "path": "packages/dashboard/src/index.ts",
    "content": "export type { DashboardProjectRouter } from \"./trpc\";\nexport type { DashboardProject } from \"./db/projects\";\n"
  },
  {
    "path": "packages/dashboard/src/trpc/index.ts",
    "content": "export * from \"./project-router\";\n"
  },
  {
    "path": "packages/dashboard/src/trpc/project-router.ts",
    "content": "import { z } from \"zod\";\nimport {\n  router,\n  procedure,\n  mergeRouters,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { projectRouter as baseProjectRouter } from \"@webstudio-is/project/index.server\";\n\nimport { db } from \"../db\";\n\nconst projectRouter = router({\n  findMany: procedure\n    .input(z.object({ userId: z.string() }))\n    .query(async ({ input, ctx }) => {\n      return await db.findMany(input.userId, ctx);\n    }),\n\n  findManyByIds: procedure\n    .input(z.object({ projectIds: z.array(z.string()) }))\n    .query(async ({ input, ctx }) => {\n      return await db.findManyByIds(input.projectIds, ctx);\n    }),\n});\n\nexport const dashboardProjectRouter = mergeRouters(\n  baseProjectRouter,\n  projectRouter\n);\n\nexport type DashboardProjectRouter = typeof dashboardProjectRouter;\n"
  },
  {
    "path": "packages/dashboard/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/design-system/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/design-system/bin/transform-figma-tokens.ts",
    "content": "import { readFileSync, writeFileSync, existsSync, rmSync } from \"node:fs\";\nimport { execSync } from \"node:child_process\";\nimport { camelCase } from \"change-case\";\nimport { z, type ZodType, type ZodTypeDef } from \"zod\";\n\nconst SOURCE_FILE = \"./src/__generated__/figma-design-tokens.json\";\nconst TMP_OUTPUT_FILE = \"./src/__generated__/figma-design-tokens.tmp\";\nconst OUTPUT_FILE = \"./src/__generated__/figma-design-tokens.ts\";\n\n// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping\n// (hopefully the fonts we use, Figma, Tokens plugin — all follow this convention)\nconst fontWeightMapping = {\n  thin: 100,\n  hairline: 100,\n  extralight: 200,\n  ultralight: 200,\n  light: 300,\n  normal: 400,\n  regular: 400,\n  medium: 500,\n  semibold: 600,\n  demibold: 600,\n  bold: 700,\n  extrabold: 800,\n  ultrabold: 800,\n  black: 900,\n  heavy: 900,\n  extrablack: 950,\n  ultrablack: 950,\n} as const;\n\nconst fontFamilies = {\n  Inter:\n    \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n  Manrope: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n  Roboto: \"Roboto Mono, RobotoMono, menlo, monospace\",\n} as const;\nconst fontFamilyMapping = {\n  ...fontFamilies,\n  InterVariable: fontFamilies.Inter,\n  \"Inter Variable\": fontFamilies.Inter,\n  ManropeVariable: fontFamilies.Manrope,\n  \"Manrope Variable\": fontFamilies.Manrope,\n  \"Roboto Mono\": fontFamilies.Roboto,\n} as const;\n\nconst TreeLeaf = z.object({\n  type: z.string(),\n  value: z.unknown(),\n});\n\nconst FontWeight = z.preprocess(\n  (x) => (typeof x === \"string\" ? x.toLowerCase().replace(/\\s+/g, \"\") : x),\n  z.enum(Object.keys(fontWeightMapping) as [keyof typeof fontWeightMapping])\n);\n\nconst FontFamily = z.string();\n\nconst LineHeight = z.union([z.string(), z.number()]);\n\nconst FontSize = z.number();\n\nconst LetterSpacing = z.union([z.string(), z.number()]);\n\nconst TextCase = z.enum([\"uppercase\", \"lowercase\", \"capitalize\", \"none\"]);\n\nconst TextDecoration = z.enum([\n  \"none\",\n  \"underline\",\n  \"overline\",\n  \"line-through\",\n]);\n\nconst Dimention = z.union([z.string(), z.number()]);\n\nconst Typography = z.object({\n  fontFamily: z.unknown(),\n  fontWeight: z.unknown(),\n  lineHeight: z.unknown(),\n  fontSize: z.unknown(),\n  letterSpacing: z.unknown(),\n  textCase: z.unknown(),\n  textDecoration: z.unknown(),\n  paragraphIndent: z.unknown(),\n});\n\nconst SingleShadow = z.object({\n  color: z.string(),\n  type: z.enum([\"dropShadow\", \"innerShadow\"]),\n  x: z.number(),\n  y: z.number(),\n  blur: z.number(),\n  spread: z.number(),\n});\n\nconst Shadow = z.union([SingleShadow, z.array(SingleShadow)]);\n\nconst parse = <Output, Def extends ZodTypeDef, Input>(\n  path: string[],\n  value: unknown,\n  schema: ZodType<Output, Def, Input>\n) => {\n  const result = schema.safeParse(value);\n  if (result.success === false) {\n    throw new Error(\n      `Could not parse ${path.join(\" > \")}. Got a error: ${\n        result.error.message\n      }`\n    );\n  }\n  return result.data;\n};\n\nconst printShadow = (path: string[], unparsedValue: unknown) => {\n  const shadow = parse(path, unparsedValue, Shadow);\n  const printSingle = (shadow: z.infer<typeof SingleShadow>) => {\n    return [\n      shadow.type === \"innerShadow\" ? \"inset\" : \"\",\n      `${shadow.x}px`,\n      `${shadow.y}px`,\n      `${shadow.blur}px`,\n      `${shadow.spread}px`,\n      `${shadow.color}`,\n    ]\n      .join(\" \")\n      .trim();\n  };\n  return Array.isArray(shadow)\n    ? shadow.map(printSingle).join(\", \")\n    : printSingle(shadow);\n};\n\nconst printLineHeight = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, LineHeight);\n\n  if (typeof value === \"number\") {\n    return `${value}px`;\n  }\n  // @todo: figure out how to convert AUTO to pixels or something\n  // https://discord.com/channels/955905230107738152/1065939291479478343\n  if (value === \"AUTO\") {\n    return;\n  }\n  if (value.endsWith(\"%\")) {\n    return value;\n  }\n  throw new Error(\n    `Could not parse \"${path.join(\" > \")} > lineHeight\": ${value}`\n  );\n};\n\nconst printLetterSpacing = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, LetterSpacing);\n\n  if (typeof value === \"number\") {\n    return `${value}px`;\n  }\n  if (/^-?[0-9]+(.[0-9]+)?%$/.test(value)) {\n    const fraction = Number.parseFloat(value) / 100;\n    return `${fraction}em`;\n  }\n  throw new Error(\n    `Could not parse \"${path.join(\" > \")} > letterSpacing\": ${value}`\n  );\n};\n\nconst printFontWeight = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, FontWeight);\n  return fontWeightMapping[value];\n};\n\nconst printFontFamily = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, FontFamily);\n  return fontFamilyMapping[value as keyof typeof fontFamilyMapping] || value;\n};\n\nconst printFontSize = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, FontSize);\n  return `${value}px`;\n};\n\nconst printTextCase = (path: string[], unparsedValue: unknown) => {\n  return parse(path, unparsedValue, TextCase);\n};\n\nconst printTextDecoration = (path: string[], unparsedValue: unknown) => {\n  return parse(path, unparsedValue, TextDecoration);\n};\n\nconst printDimension = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, Dimention);\n  if (typeof value === \"number\") {\n    return `${value}px`;\n  }\n  return value;\n};\n\nconst printTypography = (path: string[], unparsedValue: unknown) => {\n  const value = parse(path, unparsedValue, Typography);\n  return {\n    fontFamily: printFontFamily(path, value.fontFamily),\n    fontWeight: printFontWeight(path, value.fontWeight),\n    fontSize: printFontSize(path, value.fontSize),\n    lineHeight: printLineHeight(path, value.lineHeight),\n    letterSpacing: printLetterSpacing(path, value.letterSpacing),\n    textTransform: printTextCase(path, value.textCase),\n    textDecoration: printTextDecoration(path, value.textDecoration),\n    textIndent: printDimension(path, value.paragraphIndent),\n  };\n};\n\nconst printerByType = {\n  boxShadow: printShadow,\n  typography: printTypography,\n  letterSpacing: printLetterSpacing,\n  lineHeights: printLineHeight,\n  fontWeights: printFontWeight,\n  fontSizes: printFontSize,\n  fontFamilies: printFontFamily,\n  textCase: printTextCase,\n  textDecoration: printTextDecoration,\n  dimension: printDimension,\n} as const;\n\nconst traverse = (\n  node: unknown,\n  nodePath: string[],\n  fn: (path: string[], type: string, value: unknown) => void\n) => {\n  if (typeof node !== \"object\" || node === null) {\n    return;\n  }\n\n  const asLeaf = TreeLeaf.safeParse(node);\n  if (asLeaf.success && asLeaf.data.value !== undefined) {\n    fn(nodePath, asLeaf.data.type, asLeaf.data.value);\n    return;\n  }\n\n  for (const [key, value] of Object.entries(node)) {\n    traverse(value, [...nodePath, key], fn);\n  }\n};\n\nconst pathToName = (path: string[], type: string) => {\n  const cleanedUp = camelCase(path.join(\" \").replace(/[^a-z0-9]+/gi, \" \"), {\n    locale: false,\n  });\n\n  const withoutType = cleanedUp\n    .toLocaleLowerCase()\n    .startsWith(type.toLocaleLowerCase())\n    ? cleanedUp.slice(type.length)\n    : cleanedUp;\n\n  // apply camelCase again to make sure\n  // the first letter is lowercase after removing the type\n  return camelCase(withoutType, { locale: false });\n};\n\nconst detectBrokenLinks = (data: unknown) => {\n  const brokenLinks: string[] = [];\n  const testForBrokenLink = (path: string[], type: string, value: unknown) => {\n    if (typeof value === \"string\" && /^{.+}$/.test(value)) {\n      brokenLinks.push([type, ...path].join(\" > \"));\n    }\n  };\n  traverse(data, [], (path, type, value) => {\n    if (typeof value === \"object\" && value !== null) {\n      for (const [key, val] of Object.entries(value)) {\n        testForBrokenLink([...path, key], type, val);\n      }\n    } else {\n      testForBrokenLink(path, type, value);\n    }\n  });\n  if (brokenLinks.length > 0) {\n    throw new Error(`Broken links found:\\n${brokenLinks.join(\"\\n\")}`);\n  }\n};\n\nconst main = () => {\n  execSync(`token-transformer ${SOURCE_FILE} ${TMP_OUTPUT_FILE}`, {\n    stdio: \"inherit\",\n  });\n\n  const data = JSON.parse(readFileSync(TMP_OUTPUT_FILE, \"utf-8\"));\n\n  detectBrokenLinks(data);\n\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const byType = new Map<string, Record<string, any>>();\n\n  traverse(data, [], (path, type, value) => {\n    const record = byType.get(type) ?? {};\n    byType.set(type, record);\n\n    // no need to check for __proto__ (prototype polution)\n    // because we know pathToName returns a string without \"_\"\n    record[pathToName(path, type)] =\n      type in printerByType\n        ? printerByType[type as keyof typeof printerByType](path, value)\n        : value;\n  });\n\n  writeFileSync(\n    OUTPUT_FILE,\n    `// Generated by transform-figma-tokens.ts from ${SOURCE_FILE}\\n\\n` +\n      [...byType.entries()]\n        .map(\n          ([type, values]) =>\n            `export const ${type} = ${JSON.stringify(values)} as const`\n        )\n        .join(\";\\n\\n\")\n  );\n\n  execSync(`prettier --write ${OUTPUT_FILE}`, { stdio: \"inherit\" });\n};\n\nconst cleanup = () => {\n  if (existsSync(TMP_OUTPUT_FILE)) {\n    rmSync(TMP_OUTPUT_FILE);\n  }\n};\n\ntry {\n  main();\n} catch (error) {\n  try {\n    cleanup();\n  } catch (cleanupError) {\n    console.error(\"Cleanup failed:\", cleanupError);\n  }\n  throw error;\n}\ncleanup();\n"
  },
  {
    "path": "packages/design-system/documentation/figma-design-tokens.md",
    "content": "# Figma Design Tokens\n\nWe use [Tokens Studio for Figma](https://docs.tokens.studio) plugin to sync design tokens between Figma and our code.\n\n- [`__generated__/figma-design-tokens.json`](../src/__generated__/figma-design-tokens.json) — this file is synced with Figma by the plugin.\n- [`__generated__/figma-design-tokens.ts`](../src/__generated__/figma-design-tokens.ts) — this file is generated from `figma-design-tokens.json` by [`transform-figma-tokens.ts`](../bin/transform-figma-tokens.ts) and contains data in format ready to be used in code.\n\n### Generating Access Token for sync\n\n1. Create a GitHub account if you don't have one.\n2. Ask Oleg to add you to the `@webstudio-is/core` team if you are not already in it.\n3. Go to https://github.com/settings/personal-access-tokens/new\n   - Under \"Resource owner\" choose \"webstudio-is\" <br /><img src=\"./assets/1.png\" width=\"349\" />\n   - Under \"Repository access\" choose `webstudio-is/webstudio` <br /><img src=\"./assets/2.png\" width=\"514\" />\n   - Set \"Repository permissions\" / \"Contents\" to \"Read and write\", and leave other permissions as is <br /><img src=\"./assets/3.png\" width=\"806\" />\n   - Press \"Generate token and request access\"\n   - COPY THE TOKEN NOW AND SAVE IT SOMEWHERE SAFE (you won't be able to see it again)\n4. Ask Oleg to approve your token using [this instruction](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/managing-requests-for-personal-access-tokens-in-your-organization).\n\n### Syncing via plugin UI\n\n1. Open a Figma file with design tokens, such as: https://www.figma.com/file/xCBegXEWxROLqA1Y31z2Xo/%F0%9F%93%96-Webstudio-Design-Docs\n2. Click \"Resources\" in the topbar, then \"Plugins\" > \"Tokens Studio for Figma\" > Run <br/><img src=\"./assets/4.png\" width=\"611\" />\n3. You might be asked for an access token. You can generate one by following the instructions above, or ask someone to share theirs (not recommended).\n4. You should be able to sync with GitHub using ↧ and ↥ buttons at the bottom of the plugin window. <br/><img src=\"./assets/5.png\" width=\"420\" />\n\nIn case sync provider is not configured in the plugin, you can add one using these settings:\n\n- Name: up to you\n- Personal Access Token: token generated using the instructions above\n- Repository: `webstudio-is/webstudio`\n- Branch: `figma-tokens`\n- File Path: `packages/design-system/src/__generated__/figma-design-tokens.json`\n- baseUrl: leave empty\n\n### Creating a Pull Request\n\nAfter you've synced the tokens, the updates will be applied in the `figma-tokens` branch but not in the `main` branch yet. To merge the changes into `main`, you need to create a Pull Request.\n\nYou don't have to create the Pull Request after every sync, you can do a bunch of syncs and then create a single PR with all the changes.\n\nOnly one PR can be open at a time. If a PR is already open, you can continue to do syncs and the changes will be added to the existing PR.\n\nThe process:\n\n1. At the last step of the sync you should see a \"Create Pull Request\" button. Click it. <br/><img src=\"./assets/11.png\" width=\"418\" />\n2. You'll be redirected to GitHub. Enter any title and description. You can describe the changes if they are not obvious.\n3. Assign some reviewers. Feel free to always assign me (@rpominov). A Pull Request can't be merged without at least one review. Also you might need a developer's help if you've renamed or removed a token that was used in code. <br/><img src=\"./assets/10.png\" width=\"329\" />\n4. Click \"Create pull request\" button. (It might be hidden behind \"Draft pull request\") <br/><img src=\"./assets/12.png\" width=\"372\" />\n5. In the PR you can already see the changes in the deployed app <br/><img src=\"./assets/8.png\" width=\"926\" />\n6. Also, you can see the cnahges in a Storybook using this link https://figma-tokens--638affb61acca1e593c6e558.chromatic.com/ or by going to the bottom of PR page > \"Show all checks\" > \"Storybook Publish: !webstudio-is/webstudio\" > \"Details\" <br/><img src=\"./assets/9.png\" width=\"924\" />\n7. If you see \"This branch is out-of-date with the base branch\" message at the bottom, this means that when you look at the changes with the links above, you see all your changes applied, but some of the changes made by developers might not be applied. To fix that, click \"Update branch\" button, but make sure to choose \"Update with merge commit\" <br/><img src=\"./assets/7.png\" width=\"1029\" />\n8. Sometimes a visual regression test may fail in the PR. Next sections describes how to fix it.\n\n### Reviewing failed visual regression tests\n\n1. At the bottom of the PR you may see a failed \"Lost Pixel\" check. Click on \"Details\" <br/><img src=\"./assets/13.png\" width=\"922\" />\n2. You'll be redirected to lost-pixel.com and it should ask you to login with your GitHub account.\n3. After you login you should see a page like this, if not, repeat step 1 <br/><img src=\"./assets/14.png\" width=\"1222\" />\n4. Click on \"Start review\". You'll be presented with series of before/after screenshots of our demos <br/><img src=\"./assets/15.png\" width=\"1222\" />\n5. Approve each screenshot unless there's something wrong\n6. If everything is fine, click \"Update baselines\" <br/><img src=\"./assets/16.png\" width=\"1222\" />\n7. An \"update lost pixel baseline\" commit should appear in the PR and the failed check should pass <br/><img src=\"./assets/17.png\" width=\"867\" />\n\n### Links\n\n- https://docs.tokens.studio/sync/github — documentation on sync with GitHub by plugin authors\n- https://github.com/tokens-studio/figma-plugin/issues/1285 — why we use fine-grained tokens unlike what the documentation above suggests\n"
  },
  {
    "path": "packages/design-system/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/design-system\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Design System\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build-figma-tokens\": \"tsx ./bin/transform-figma-tokens.ts\",\n    \"typecheck\": \"tsgo --noEmit -p tsconfig.typecheck.json\",\n    \"test\": \"vitest run\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vitest\": \"^3.1.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"dependencies\": {\n    \"@atlaskit/pragmatic-drag-and-drop\": \"^1.7.4\",\n    \"@atlaskit/pragmatic-drag-and-drop-auto-scroll\": \"^2.1.0\",\n    \"@atlaskit/pragmatic-drag-and-drop-hitbox\": \"^1.0.3\",\n    \"@floating-ui/dom\": \"^1.6.13\",\n    \"@radix-ui/colors\": \"^3.0.0\",\n    \"@radix-ui/react-accessible-icon\": \"^1.1.4\",\n    \"@radix-ui/react-avatar\": \"^1.1.7\",\n    \"@radix-ui/react-checkbox\": \"^1.2.3\",\n    \"@radix-ui/react-collapsible\": \"^1.1.8\",\n    \"@radix-ui/react-context-menu\": \"^2.2.12\",\n    \"@radix-ui/react-dialog\": \"^1.1.11\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.12\",\n    \"@radix-ui/react-label\": \"^2.1.4\",\n    \"@radix-ui/react-popover\": \"^1.1.11\",\n    \"@radix-ui/react-progress\": \"^1.1.4\",\n    \"@radix-ui/react-radio-group\": \"^1.3.4\",\n    \"@radix-ui/react-scroll-area\": \"^1.0.5\",\n    \"@radix-ui/react-select\": \"^2.2.2\",\n    \"@radix-ui/react-separator\": \"^1.1.4\",\n    \"@radix-ui/react-slot\": \"^1.2.0\",\n    \"@radix-ui/react-switch\": \"^1.2.2\",\n    \"@radix-ui/react-tabs\": \"^1.1.9\",\n    \"@radix-ui/react-toast\": \"^1.2.11\",\n    \"@radix-ui/react-toggle\": \"^1.1.6\",\n    \"@radix-ui/react-toggle-group\": \"^1.1.7\",\n    \"@radix-ui/react-toolbar\": \"^1.1.7\",\n    \"@radix-ui/react-tooltip\": \"^1.2.4\",\n    \"@radix-ui/react-use-controllable-state\": \"^1.2.2\",\n    \"@radix-ui/react-visually-hidden\": \"^1.2.0\",\n    \"@react-aria/focus\": \"^3.19.1\",\n    \"@react-aria/interactions\": \"^3.23.0\",\n    \"@react-aria/utils\": \"^3.27.0\",\n    \"@stitches/react\": \"1.3.1-1\",\n    \"@webstudio-is/css-data\": \"workspace:*\",\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/icons\": \"workspace:*\",\n    \"change-case\": \"^5.4.4\",\n    \"cmdk\": \"^1.1.1\",\n    \"colorjs.io\": \"^0.6.1\",\n    \"downshift\": \"^6.1.7\",\n    \"match-sorter\": \"^8.0.0\",\n    \"react-colorful\": \"^5.6.1\",\n    \"react-hot-toast\": \"^2.5.1\",\n    \"token-transformer\": \"^0.0.28\",\n    \"type-fest\": \"^4.37.0\",\n    \"use-debounce\": \"^10.0.4\",\n    \"warn-once\": \"^0.1.1\"\n  },\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/design-system/src/__generated__/figma-design-tokens.json",
    "content": "{\n  \"global\": {\n    \"menu drop shadow\": {\n      \"value\": [\n        {\n          \"color\": \"#0000001a\",\n          \"type\": \"dropShadow\",\n          \"x\": \"0\",\n          \"y\": \"2\",\n          \"blur\": \"7\",\n          \"spread\": \"0\"\n        },\n        {\n          \"color\": \"#0000004d\",\n          \"type\": \"dropShadow\",\n          \"x\": \"0\",\n          \"y\": \"5\",\n          \"blur\": \"17\",\n          \"spread\": \"0\"\n        }\n      ],\n      \"type\": \"boxShadow\"\n    },\n    \"fontFamilies\": {\n      \"inter\": {\n        \"value\": \"Inter\",\n        \"type\": \"fontFamilies\"\n      },\n      \"manrope\": {\n        \"value\": \"Manrope\",\n        \"type\": \"fontFamilies\"\n      },\n      \"roboto-mono\": {\n        \"value\": \"Roboto Mono\",\n        \"type\": \"fontFamilies\"\n      }\n    },\n    \"lineHeights\": {\n      \"0\": {\n        \"value\": \"16\",\n        \"type\": \"lineHeights\"\n      },\n      \"1\": {\n        \"value\": \"8\",\n        \"type\": \"lineHeights\"\n      },\n      \"2\": {\n        \"value\": \"12\",\n        \"type\": \"lineHeights\"\n      },\n      \"3\": {\n        \"value\": \"10\",\n        \"type\": \"lineHeights\"\n      },\n      \"4\": {\n        \"value\": \"39\",\n        \"type\": \"lineHeights\"\n      },\n      \"5\": {\n        \"value\": \"11\",\n        \"type\": \"lineHeights\"\n      },\n      \"6\": {\n        \"value\": \"27\",\n        \"type\": \"lineHeights\"\n      },\n      \"7\": {\n        \"value\": \"58\",\n        \"type\": \"lineHeights\"\n      },\n      \"8\": {\n        \"value\": \"22\",\n        \"type\": \"lineHeights\"\n      },\n      \"9\": {\n        \"value\": \"38\",\n        \"type\": \"lineHeights\"\n      },\n      \"10\": {\n        \"value\": \"260\",\n        \"type\": \"lineHeights\"\n      }\n    },\n    \"fontWeights\": {\n      \"inter-0\": {\n        \"value\": \"Regular\",\n        \"type\": \"fontWeights\"\n      },\n      \"inter-1\": {\n        \"value\": \"Bold\",\n        \"type\": \"fontWeights\"\n      },\n      \"inter-2\": {\n        \"value\": \"Medium\",\n        \"type\": \"fontWeights\"\n      },\n      \"inter-3\": {\n        \"value\": \"Bold\",\n        \"type\": \"fontWeights\"\n      },\n      \"inter-4\": {\n        \"value\": \"Semi Bold\",\n        \"type\": \"fontWeights\"\n      },\n      \"roboto-mono-4\": {\n        \"value\": \"Bold\",\n        \"type\": \"fontWeights\"\n      },\n      \"manrope-5\": {\n        \"value\": \"Bold\",\n        \"type\": \"fontWeights\"\n      },\n      \"manrope-6\": {\n        \"value\": \"Regular\",\n        \"type\": \"fontWeights\"\n      },\n      \"manrope-7\": {\n        \"value\": \"SemiBold\",\n        \"type\": \"fontWeights\"\n      },\n      \"manrope-8\": {\n        \"value\": \"ExtraLight\",\n        \"type\": \"fontWeights\"\n      },\n      \"manrope-9\": {\n        \"value\": \"ExtraBold\",\n        \"type\": \"fontWeights\"\n      },\n      \"roboto-mono-3\": {\n        \"value\": \"Medium\",\n        \"type\": \"fontWeights\"\n      },\n      \"inter-5\": {\n        \"value\": \"Semi Bold\",\n        \"type\": \"fontWeights\"\n      },\n      \"manrope-10\": {\n        \"value\": \"ExtraBold\",\n        \"type\": \"fontWeights\"\n      }\n    },\n    \"letterSpacing\": {\n      \"0\": {\n        \"value\": \"0.5%\",\n        \"type\": \"letterSpacing\"\n      },\n      \"1\": {\n        \"value\": \"1%\",\n        \"type\": \"letterSpacing\"\n      },\n      \"2\": {\n        \"value\": \"0%\",\n        \"type\": \"letterSpacing\"\n      },\n      \"3\": {\n        \"value\": \"-2%\",\n        \"type\": \"letterSpacing\"\n      },\n      \"4\": {\n        \"value\": \"2%\",\n        \"type\": \"letterSpacing\"\n      },\n      \"5\": {\n        \"value\": \"-5%\",\n        \"type\": \"letterSpacing\"\n      }\n    },\n    \"paragraphSpacing\": {\n      \"0\": {\n        \"value\": \"0\",\n        \"type\": \"paragraphSpacing\"\n      }\n    },\n    \"Regular\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-0}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Labels - Title Case\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.capitalize}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Labels - Sentence case\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Titles\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-1}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.1}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.capitalize}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Small\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-0}\",\n        \"lineHeight\": \"{lineHeights.5}\",\n        \"fontSize\": \"{fontSize.1}\",\n        \"letterSpacing\": \"{letterSpacing.1}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"When Regular is too big and Tiny is too tiny.\"\n    },\n    \"Tiny\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.1}\",\n        \"fontSize\": \"{fontSize.0}\",\n        \"letterSpacing\": \"{letterSpacing.1}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Unit\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.2}\",\n        \"fontSize\": \"{fontSize.1}\",\n        \"letterSpacing\": \"{letterSpacing.2}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.uppercase}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Mono\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.roboto-mono}\",\n        \"fontWeight\": \"{fontWeights.roboto-mono-3}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.2}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Where code is displayed, it should be Mono.\"\n    },\n    \"Big Title\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.4}\",\n        \"fontSize\": \"{fontSize.5}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Space Section Unit Text\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.1}\",\n        \"fontSize\": \"{fontSize.0}\",\n        \"letterSpacing\": \"{letterSpacing.1}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.uppercase}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"Space Section Value Text\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.3}\",\n        \"fontSize\": \"{fontSize.1}\",\n        \"letterSpacing\": \"{letterSpacing.1}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\"\n    },\n    \"textCase\": {\n      \"none\": {\n        \"value\": \"none\",\n        \"type\": \"textCase\"\n      },\n      \"uppercase\": {\n        \"value\": \"uppercase\",\n        \"type\": \"textCase\"\n      },\n      \"capitalize\": {\n        \"value\": \"capitalize\",\n        \"type\": \"textCase\"\n      }\n    },\n    \"textDecoration\": {\n      \"none\": {\n        \"value\": \"none\",\n        \"type\": \"textDecoration\"\n      },\n      \"underline\": {\n        \"value\": \"underline\",\n        \"type\": \"textDecoration\"\n      }\n    },\n    \"borderRadius\": {\n      \"0\": {\n        \"value\": \"1px\",\n        \"type\": \"borderRadius\"\n      },\n      \"1\": {\n        \"value\": \"2px\",\n        \"type\": \"borderRadius\"\n      },\n      \"2\": {\n        \"value\": \"3px\",\n        \"type\": \"borderRadius\"\n      },\n      \"3\": {\n        \"value\": \"4px\",\n        \"type\": \"borderRadius\"\n      },\n      \"4\": {\n        \"value\": \"5px\",\n        \"type\": \"borderRadius\"\n      },\n      \"5\": {\n        \"value\": \"6px\",\n        \"type\": \"borderRadius\"\n      }\n    },\n    \"white\": {\n      \"value\": \"#ffffff\",\n      \"type\": \"color\"\n    },\n    \"black\": {\n      \"value\": \"#000000\",\n      \"type\": \"color\"\n    },\n    \"background\": {\n      \"panel\": {\n        \"value\": \"#fff\",\n        \"type\": \"color\",\n        \"description\": \"background color for all panels, titles, columns and rows.\"\n      },\n      \"primary\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\",\n        \"description\": \"primary color, used in button\"\n      },\n      \"hover\": {\n        \"value\": \"#efefef\",\n        \"type\": \"color\",\n        \"description\": \"hover color for toggle group button, icon button, toggle button, small icon button, small toggle button, nested select button, menu item large\"\n      },\n      \"active\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\",\n        \"description\": \"color for active elements\"\n      },\n      \"menu\": {\n        \"value\": \"#fff\",\n        \"type\": \"color\",\n        \"description\": \"background color for menu and select menu\"\n      },\n      \"controls\": {\n        \"value\": \"#f5f5f5\",\n        \"type\": \"color\",\n        \"description\": \"background color for text input, toggle group, input field, color input, select button, panel button, text-area, search field, checkbox, spacing, flex control\"\n      },\n      \"assetcard-hover\": {\n        \"value\": \"#e6e8eb\",\n        \"type\": \"color\",\n        \"description\": \"used only for the hover state for the asset card component\"\n      },\n      \"neutral\": {\n        \"main\": {\n          \"value\": \"#d7dbdf\",\n          \"type\": \"color\",\n          \"description\": \"used for neutral button\"\n        },\n        \"accent\": {\n          \"value\": \"#11181c\",\n          \"type\": \"color\",\n          \"description\": \"accent color on neutral toast\"\n        },\n        \"notification\": {\n          \"value\": \"#ffffff\",\n          \"type\": \"color\",\n          \"description\": \"background color on neutral toast\"\n        },\n        \"dark\": {\n          \"value\": \"#b2b2b2\",\n          \"type\": \"color\"\n        }\n      },\n      \"destructive\": {\n        \"main\": {\n          \"value\": \"#dc2929\",\n          \"type\": \"color\",\n          \"description\": \"used for destructive button and accent color on destructive toast\"\n        },\n        \"notification\": {\n          \"value\": \"#ffe9e9\",\n          \"type\": \"color\",\n          \"description\": \"background for destructive toast\"\n        }\n      },\n      \"success\": {\n        \"main\": {\n          \"value\": \"#00894a\",\n          \"type\": \"color\",\n          \"description\": \"used for success button and accent color on success toast\"\n        },\n        \"notification\": {\n          \"value\": \"#e9f9ee\",\n          \"type\": \"color\",\n          \"description\": \"background for success banner & toast\"\n        }\n      },\n      \"alert\": {\n        \"main\": {\n          \"value\": \"#f5d90a\",\n          \"type\": \"color\",\n          \"description\": \"used for alert button and accent color on alert toast\"\n        },\n        \"notification\": {\n          \"value\": \"#fffbd1\",\n          \"type\": \"color\",\n          \"description\": \"background for alert banner & toast\"\n        }\n      },\n      \"info\": {\n        \"main\": {\n          \"value\": \"#0175dc\",\n          \"type\": \"color\"\n        },\n        \"notification\": {\n          \"value\": \"#e0f0ff\",\n          \"type\": \"color\",\n          \"description\": \"background for info banner & toast\"\n        }\n      },\n      \"preset\": {\n        \"main\": {\n          \"value\": \"#e6e6e6\",\n          \"type\": \"color\"\n        },\n        \"hover\": {\n          \"value\": \"#dfe3e6\",\n          \"type\": \"color\"\n        }\n      },\n      \"local\": {\n        \"main\": {\n          \"value\": \"#e1f0ff\",\n          \"type\": \"color\"\n        },\n        \"hover\": {\n          \"value\": \"#cee7fe\",\n          \"type\": \"color\"\n        }\n      },\n      \"remote\": {\n        \"main\": {\n          \"value\": \"#ffe8d7\",\n          \"type\": \"color\"\n        },\n        \"hover\": {\n          \"value\": \"#ffdcc3\",\n          \"type\": \"color\"\n        }\n      },\n      \"input\": {\n        \"selected\": {\n          \"value\": \"#b7d9f8\",\n          \"type\": \"color\",\n          \"description\": \"background color for selected text\"\n        },\n        \"disabled\": {\n          \"value\": \"#f8f8f8\",\n          \"type\": \"color\",\n          \"description\": \"background for disabled text fields\"\n        },\n        \"highlight\": {\n          \"value\": \"#b7d9f8\",\n          \"type\": \"color\",\n          \"description\": \"background color for highlighted/selected text\"\n        }\n      },\n      \"button\": {\n        \"hover\": {\n          \"value\": \"#00000010\",\n          \"type\": \"color\",\n          \"description\": \"transparent color to create the hover state for button\"\n        },\n        \"pressed\": {\n          \"value\": \"#0000001c\",\n          \"type\": \"color\",\n          \"description\": \"transparent color to create the pressed state for button\"\n        },\n        \"disabled\": {\n          \"value\": \"#e9ebed\",\n          \"type\": \"color\",\n          \"description\": \"disabled state for button\"\n        },\n        \"disabled-dark\": {\n          \"value\": \"#646464\",\n          \"type\": \"color\",\n          \"description\": \"For disabled controls that are against a dark background.\"\n        }\n      },\n      \"item\": {\n        \"current\": {\n          \"value\": \"#e1f0ff\",\n          \"type\": \"color\",\n          \"description\": \"color for current item in the navigator item, menu item, pages panel item components\"\n        },\n        \"current-child\": {\n          \"value\": \"{background.panel}\",\n          \"type\": \"color\",\n          \"description\": \"color for child of current item in the navigator item, pages panel item components\"\n        },\n        \"current-hidden\": {\n          \"value\": \"#7e868c\",\n          \"type\": \"color\",\n          \"description\": \"color for the hidden current item in the navigator item component\"\n        },\n        \"menu-item-hover\": {\n          \"value\": \"#efefef\",\n          \"type\": \"color\",\n          \"description\": \"The hover state on menu item components.\"\n        }\n      },\n      \"tooltip\": {\n        \"main\": {\n          \"value\": \"#11181c\",\n          \"type\": \"color\"\n        },\n        \"builder\": {\n          \"value\": \"#ffffff\",\n          \"type\": \"color\"\n        },\n        \"designer\": {\n          \"value\": \"#ffffff\",\n          \"type\": \"color\"\n        }\n      },\n      \"spacing\": {\n        \"top-bottom\": {\n          \"value\": \"#f8f8f8\",\n          \"type\": \"color\"\n        },\n        \"left-right\": {\n          \"value\": \"#f1f3f5\",\n          \"type\": \"color\",\n          \"description\": \"Left and right padding and margin background colors for the Position section UI.\"\n        },\n        \"hover\": {\n          \"value\": \"#e3e3e3\",\n          \"type\": \"color\"\n        }\n      },\n      \"style-source\": {\n        \"token\": {\n          \"value\": \"{foreground.reusable}\",\n          \"type\": \"color\",\n          \"description\": \"Style sources that are tokens\"\n        },\n        \"tag\": {\n          \"value\": \"#d54113\",\n          \"type\": \"color\",\n          \"description\": \"For the HTML tag variant of the Style Source component.\"\n        },\n        \"state\": {\n          \"value\": \"#00894a\",\n          \"type\": \"color\",\n          \"description\": \"For the state variant of the Style Source component.\"\n        },\n        \"neutral\": {\n          \"value\": \"#687076\",\n          \"type\": \"color\",\n          \"description\": \"For the inactive variant of the Style Source component.\"\n        },\n        \"disabled\": {\n          \"value\": \"#9da2a6\",\n          \"type\": \"color\",\n          \"description\": \"For the disabled state of the token variant of the Style Source component.\"\n        },\n        \"gradient\": {\n          \"token\": {\n            \"value\": \"linear-gradient(90deg, #834df400 0%, #834df4 31.87%)\",\n            \"type\": \"color\"\n          },\n          \"tag\": {\n            \"value\": \"linear-gradient(90deg, #d5411300 0%, #d54113 31.87%)\",\n            \"type\": \"color\"\n          },\n          \"unselected\": {\n            \"value\": \"linear-gradient(90deg, #68707600 0%, #687076 31.87%)\",\n            \"type\": \"color\"\n          },\n          \"local\": {\n            \"value\": \"linear-gradient(90deg, #096cff00 0%, #096cff 31.87%)\",\n            \"type\": \"color\"\n          }\n        },\n        \"breakpoint\": {\n          \"value\": \"#bd2fdb\",\n          \"type\": \"color\",\n          \"description\": \"The color of the style source badge that represents a breakpoint. Used in tooltips.\"\n        },\n        \"local\": {\n          \"value\": \"#096cff\",\n          \"type\": \"color\",\n          \"description\": \"For the local variant of the Style Source component.\"\n        }\n      },\n      \"canvas\": {\n        \"value\": \"#eee\",\n        \"type\": \"color\",\n        \"description\": \"The color of the Builder UI canvas area background.\"\n      },\n      \"topbar\": {\n        \"value\": \"#2d2d2d\",\n        \"type\": \"color\",\n        \"description\": \"For the Builder UI top bar background color.\"\n      },\n      \"gradient\": {\n        \"primary\": {\n          \"value\": \"linear-gradient(135deg, #1774ff 0%, #bd2fdb 100%)\",\n          \"type\": \"color\",\n          \"description\": \"For buttons and other larger UI elements that use a brand gradient. (experimental)\"\n        },\n        \"vertical\": {\n          \"value\": \"linear-gradient(180deg, #096cff 0%, #096cff 0.01%, #bd2fdb 100%)\",\n          \"type\": \"color\",\n          \"description\": \"For vertically-oriented elements in the Builder UI that use a brand gradient background.\"\n        },\n        \"horizontal\": {\n          \"value\": \"linear-gradient(90deg, #4a4efa 0%, #bd2fdb 100%)\",\n          \"type\": \"color\",\n          \"description\": \"For horizontally-oriented elements in the Builder UI that use a brand gradient background.\"\n        },\n        \"horizontal-reverse\": {\n          \"value\": \"linear-gradient(90deg, #bd2fdb 0%, #4a4efa 100%)\",\n          \"type\": \"color\",\n          \"description\": \"Like horizontal brand gradient, but opposite direction.\"\n        }\n      },\n      \"menu-hint\": {\n        \"value\": \"#efefef\",\n        \"type\": \"color\",\n        \"description\": \"For the background of the hint menu item.\"\n      },\n      \"topbar-hover\": {\n        \"value\": \"#383838\",\n        \"type\": \"color\",\n        \"description\": \"For tabs and button backgrounds that are in the top toolbar\"\n      },\n      \"workspace\": {\n        \"value\": \"#a9a9a9\",\n        \"type\": \"color\",\n        \"description\": \"The color of the Builder UI canvas area background.\"\n      },\n      \"icon-subtle\": {\n        \"value\": \"#3e3e3e\",\n        \"type\": \"color\"\n      },\n      \"primary-light\": {\n        \"value\": \"#2e82ff\",\n        \"type\": \"color\",\n        \"description\": \"A lighter version of primary blue. Not to be used with or as  text. Used for drag handles.\"\n      },\n      \"overwritten\": {\n        \"main\": {\n          \"value\": \"#ffd9d9\",\n          \"type\": \"color\",\n          \"description\": \"Background color for labels on properties whose values are overwritten.\"\n        },\n        \"hover\": {\n          \"value\": \"#fec4c4\",\n          \"type\": \"color\",\n          \"description\": \"Hover background for overwritten labels.\"\n        }\n      },\n      \"disabled-dark\": {\n        \"value\": \"#323232\",\n        \"type\": \"color\",\n        \"description\": \"For disabled controls that are against a dark background.\"\n      }\n    },\n    \"brand\": {\n      \"background\": {\n        \"project-card\": {\n          \"front\": {\n            \"value\": \"linear-gradient(0deg, #fbf8ff 0%, #e2e2e2 100%)\",\n            \"type\": \"color\",\n            \"description\": \"big text color in the project card component\"\n          },\n          \"back\": {\n            \"value\": \"linear-gradient(0deg, #fbf8ff 0%, #c7c7c7 100%)\",\n            \"type\": \"color\"\n          },\n          \"text-area\": {\n            \"value\": \"#ffffff\",\n            \"type\": \"color\"\n          }\n        },\n        \"published-main\": {\n          \"value\": \"#39fbbb\",\n          \"type\": \"color\"\n        },\n        \"gradient\": {\n          \"value\": \"linear-gradient(180deg, #e63cfe 0%, #ffae3c 100%)\",\n          \"type\": \"color\"\n        },\n        \"published-contrast\": {\n          \"value\": \"#ebfffc\",\n          \"type\": \"color\"\n        },\n        \"dashboard\": {\n          \"value\": \"radial-gradient(65.88% 47.48% at 50% 50%, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%),linear-gradient(0deg, rgba(255, 255, 255, 0) 49.46%, rgba(255, 255, 255, 0.33) 100%), linear-gradient(180deg, rgba(255, 174, 60, 0) 0%, rgba(230, 60, 254, 0.33) 100%),radial-gradient(211.58% 161.63% at 3.13% 100%, rgba(255, 174, 60, 0.3) 0%, rgba(227, 53, 255, 0) 100%), radial-gradient(107.1% 32.15% at 92.96% 5.04%, rgba(53, 255, 182, 0.2) 0%, rgba(74, 78, 250, 0.2) 100%), #EBFFFC;\",\n          \"type\": \"color\"\n        },\n        \"regular-button-selected\": {\n          \"value\": \"linear-gradient(180deg, #bffeec 0%, #fbfff6 100%)\",\n          \"type\": \"color\"\n        },\n        \"cta-button\": {\n          \"value\": \"linear-gradient(135deg, #4a4efa 0%, #bd2fdb 100%)\",\n          \"type\": \"color\"\n        }\n      },\n      \"foreground\": {\n        \"published\": {\n          \"value\": \"#00894a\",\n          \"type\": \"color\"\n        }\n      },\n      \"border\": {\n        \"published\": {\n          \"value\": \"#ebfffc\",\n          \"type\": \"color\"\n        },\n        \"full-gradient\": {\n          \"value\": \"linear-gradient(135deg, #92fddc 0%, #7d7ffb 31.94%, #ed72fe 64.24%, #fdd791 100%)\",\n          \"type\": \"color\"\n        },\n        \"navbar\": {\n          \"value\": \"linear-gradient(90deg, #39fbbb00 0%, #39fbbb 20%, #4a4efa 40.03%, #e63cfe 60.02%, #ffae3c 80.04%, #ffae3c00 100%, #ffae3c00 100%)\",\n          \"type\": \"color\"\n        }\n      },\n      \"spinner-turquoise\": {\n        \"value\": \"#39fbbb\",\n        \"type\": \"color\"\n      },\n      \"spinner-blue\": {\n        \"value\": \"#4a4efa\",\n        \"type\": \"color\"\n      },\n      \"spinner-purple\": {\n        \"value\": \"#e63cfe\",\n        \"type\": \"color\"\n      },\n      \"spinner-orange\": {\n        \"value\": \"#ffae3c\",\n        \"type\": \"color\"\n      }\n    },\n    \"border\": {\n      \"main\": {\n        \"value\": \"#e6e6e6\",\n        \"type\": \"color\",\n        \"description\": \"color for separators, panel borders, preset borders, input fields, select buttons, panel buttons, text area, search field, menu, select menu, spacing controls,\"\n      },\n      \"focus\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\",\n        \"description\": \"border color for all focus elements\"\n      },\n      \"menu-inner\": {\n        \"value\": \"#fcfcfc\",\n        \"type\": \"color\",\n        \"description\": \"inner border color for menu and select menu\"\n      },\n      \"color-swatch\": {\n        \"value\": \"#687076\",\n        \"type\": \"color\",\n        \"description\": \"border color for color swatch\"\n      },\n      \"neutral\": {\n        \"value\": \"#e8e8e8\",\n        \"type\": \"color\",\n        \"description\": \"border color for neutral toast\"\n      },\n      \"success\": {\n        \"value\": \"#00894a\",\n        \"type\": \"color\",\n        \"description\": \"border color for success banner & toast\"\n      },\n      \"alert\": {\n        \"value\": \"#e2c802\",\n        \"type\": \"color\",\n        \"description\": \"border color for alert banner & toast\"\n      },\n      \"info\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\",\n        \"description\": \"border color for info banner & toast\"\n      },\n      \"contrast\": {\n        \"value\": \"#ffffff\",\n        \"type\": \"color\",\n        \"description\": \"border color to create contrast on dark/colored elements. Used in button, new position indicator for navigator\"\n      },\n      \"item\": {\n        \"child-line\": {\n          \"value\": \"#889096\",\n          \"type\": \"color\",\n          \"description\": \"color of child lines within the navigator item & pages panel item\"\n        },\n        \"child-line-current\": {\n          \"value\": \"#96c7f2\",\n          \"type\": \"color\",\n          \"description\": \"color of child lines within the current item of the navigator item & pages panel item\"\n        }\n      },\n      \"local\": {\n        \"main\": {\n          \"value\": \"#b7d9f8\",\n          \"type\": \"color\",\n          \"description\": \"border color of set elements\"\n        },\n        \"flex-ui\": {\n          \"value\": \"#096cff\",\n          \"type\": \"color\",\n          \"description\": \"border color of set elements within the flex controls ui\"\n        }\n      },\n      \"remote\": {\n        \"main\": {\n          \"value\": \"#fbc69f\",\n          \"type\": \"color\",\n          \"description\": \"border color of inherited elements\"\n        },\n        \"flex-ui\": {\n          \"value\": \"#fa934e\",\n          \"type\": \"color\",\n          \"description\": \"border color of inherited elements within the flex controls ui\"\n        }\n      },\n      \"destructive\": {\n        \"main\": {\n          \"value\": \"#d13a3a\",\n          \"type\": \"color\",\n          \"description\": \"border color for destructive input field, text area and search field\"\n        },\n        \"notification\": {\n          \"value\": \"#f9c6c6\",\n          \"type\": \"color\",\n          \"description\": \"border color for destructive toast\"\n        }\n      },\n      \"dark\": {\n        \"value\": \"#595c5d\",\n        \"type\": \"color\",\n        \"description\": \"For borders that use dark mode colors in light mode like the Builder UI top bar.\"\n      },\n      \"overwritten\": {\n        \"main\": {\n          \"value\": \"#fdaaab\",\n          \"type\": \"color\",\n          \"description\": \"Border color for labels on properties whose values are overwritten.\"\n        },\n        \"flex-ui\": {\n          \"value\": \"#dc3d43\",\n          \"type\": \"color\",\n          \"description\": \"For the flex control grid border when the value is overwritten.\"\n        }\n      },\n      \"primary\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\"\n      }\n    },\n    \"foreground\": {\n      \"main\": {\n        \"value\": \"#11181c\",\n        \"type\": \"color\",\n        \"description\": \"default color for all text and icons\"\n      },\n      \"subtle\": {\n        \"value\": \"#656869\",\n        \"type\": \"color\",\n        \"description\": \"color for less prominent elements. Used in small icon button, nested select button, input field, color input, select button, panel button, text area, search field & toast\"\n      },\n      \"category-label\": {\n        \"value\": \"#889096\",\n        \"type\": \"color\",\n        \"description\": \"foreground color for the category label element within the font item component\"\n      },\n      \"text-subtle\": {\n        \"value\": \"#687076\",\n        \"type\": \"color\",\n        \"description\": \"foreground color for spacing controls text\"\n      },\n      \"destructive\": {\n        \"value\": \"#d13a3a\",\n        \"type\": \"color\",\n        \"description\": \"destructive foreground color. Used in input field, text field, search area, & toast\"\n      },\n      \"success\": {\n        \"value\": \"#00b661\",\n        \"type\": \"color\",\n        \"description\": \"A brighter success color for use with icons and text against a dark bg. Not for use with text against light bg.\"\n      },\n      \"info\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\",\n        \"description\": \"info foreground color. Used in toast\"\n      },\n      \"disabled\": {\n        \"value\": \"#c1c8cd\",\n        \"type\": \"color\",\n        \"description\": \"color for disabled foreground elements\"\n      },\n      \"hidden-item\": {\n        \"value\": \"#7e868c\",\n        \"type\": \"color\",\n        \"description\": \"color for hidden item element within navigator item component\"\n      },\n      \"flex-ui\": {\n        \"main\": {\n          \"value\": \"#c7c7c7\",\n          \"type\": \"color\",\n          \"description\": \"color for flex control ui elements\"\n        },\n        \"hover\": {\n          \"value\": \"#e3e3e3\",\n          \"type\": \"color\",\n          \"description\": \"color for hovered state of flex control ui elements\"\n        }\n      },\n      \"contrast\": {\n        \"main\": {\n          \"value\": \"#ffffff\",\n          \"type\": \"color\",\n          \"description\": \"color to create contrast over colored and dark backgrounds\"\n        },\n        \"subtle\": {\n          \"value\": \"#c1c8cd\",\n          \"type\": \"color\",\n          \"description\": \"color to create a more subtle contrast over colored and dark backgrounds\"\n        }\n      },\n      \"local\": {\n        \"main\": {\n          \"value\": \"#016ccc\",\n          \"type\": \"color\",\n          \"description\": \"foreground color for set elements\"\n        },\n        \"flex-ui\": {\n          \"value\": \"#237cff\",\n          \"type\": \"color\",\n          \"description\": \"foreground color for flex-ui set elements\"\n        }\n      },\n      \"remote\": {\n        \"main\": {\n          \"value\": \"#b74900\",\n          \"type\": \"color\",\n          \"description\": \"foreground color for inherited elements\"\n        },\n        \"flex-ui\": {\n          \"value\": \"#fa934e\",\n          \"type\": \"color\",\n          \"description\": \"foreground color for flex-ui inherited elements\"\n        }\n      },\n      \"text-more-subtle\": {\n        \"value\": \"#8d949a\",\n        \"type\": \"color\",\n        \"description\": \"for subtle text that needs to be even more subtle \\nthan \\\"text-subtle\\\"\\n\\nused as the color for text on title tab triggers and the category label of the font item component\"\n      },\n      \"primary\": {\n        \"value\": \"#096cff\",\n        \"type\": \"color\"\n      },\n      \"success-text\": {\n        \"value\": \"#008447\",\n        \"type\": \"color\",\n        \"description\": \"Success color that is dark enough for text. Same color as background/success\"\n      },\n      \"grid-controls\": {\n        \"dot\": {\n          \"value\": \"#c7c7c7\",\n          \"type\": \"color\",\n          \"description\": \"color for flex control ui elements\"\n        },\n        \"dot-hover\": {\n          \"value\": \"#707375\",\n          \"type\": \"color\"\n        },\n        \"flex-hover\": {\n          \"value\": \"#e3e3e3\",\n          \"type\": \"color\",\n          \"description\": \"color for hovered state of flex control ui elements\"\n        }\n      },\n      \"icon\": {\n        \"secondary\": {\n          \"value\": \"#c1c8cd\",\n          \"type\": \"color\"\n        },\n        \"main\": {\n          \"value\": \"#11181c\",\n          \"type\": \"color\"\n        }\n      },\n      \"more-subtle\": {\n        \"value\": \"#adb1b4\",\n        \"type\": \"color\",\n        \"description\": \"The scroll bar, and anything that needs to be a bit lighter than foreground/subtle\"\n      },\n      \"scroll-bar\": {\n        \"value\": \"#a7acaf99\",\n        \"type\": \"color\",\n        \"description\": \"For the scroll bar thumb only\"\n      },\n      \"overwritten\": {\n        \"main\": {\n          \"value\": \"#bf0007\",\n          \"type\": \"color\",\n          \"description\": \"Foreground (text & icon) color for labels on properties whose values are overwritten.\"\n        },\n        \"flex-ui\": {\n          \"value\": \"#dc3d43\",\n          \"type\": \"color\",\n          \"description\": \"For the flex control grid when the value is overwritten.\"\n        }\n      },\n      \"inverse-primary\": {\n        \"value\": \"#ff22ae\",\n        \"type\": \"color\",\n        \"description\": \"The inverse color of our primary blue, when we want a contrasting highlight.\"\n      },\n      \"reusable\": {\n        \"value\": \"#834df4\",\n        \"type\": \"color\"\n      }\n    },\n    \"Brand\": {\n      \"elevation-small\": {\n        \"value\": {\n          \"color\": \"#1717171a\",\n          \"type\": \"dropShadow\",\n          \"x\": \"0\",\n          \"y\": \"4\",\n          \"blur\": \"4\",\n          \"spread\": \"0\"\n        },\n        \"type\": \"boxShadow\",\n        \"description\": \"elevation for small things within the dashboard\"\n      },\n      \"elevation-big\": {\n        \"value\": {\n          \"color\": \"#1717171a\",\n          \"type\": \"dropShadow\",\n          \"x\": \"0\",\n          \"y\": \"8\",\n          \"blur\": \"16\",\n          \"spread\": \"0\"\n        },\n        \"type\": \"boxShadow\",\n        \"description\": \"elevation for big things within the dashboard\"\n      },\n      \"Large Title\": {\n        \"value\": {\n          \"fontFamily\": \"{fontFamilies.manrope}\",\n          \"fontWeight\": \"{fontWeights.manrope-5}\",\n          \"lineHeight\": \"{lineHeights.7}\",\n          \"fontSize\": \"{fontSize.6}\",\n          \"letterSpacing\": \"{letterSpacing.3}\",\n          \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n          \"paragraphIndent\": \"{paragraphIndent.0}\",\n          \"textCase\": \"{textCase.none}\",\n          \"textDecoration\": \"{textDecoration.none}\"\n        },\n        \"type\": \"typography\"\n      },\n      \"Medium Title\": {\n        \"value\": {\n          \"fontFamily\": \"{fontFamilies.manrope}\",\n          \"fontWeight\": \"{fontWeights.manrope-5}\",\n          \"lineHeight\": \"{lineHeights.9}\",\n          \"fontSize\": \"{fontSize.5}\",\n          \"letterSpacing\": \"{letterSpacing.3}\",\n          \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n          \"paragraphIndent\": \"{paragraphIndent.0}\",\n          \"textCase\": \"{textCase.none}\",\n          \"textDecoration\": \"{textDecoration.none}\"\n        },\n        \"type\": \"typography\"\n      },\n      \"Section Title\": {\n        \"value\": {\n          \"fontFamily\": \"{fontFamilies.manrope}\",\n          \"fontWeight\": \"{fontWeights.manrope-5}\",\n          \"lineHeight\": \"{lineHeights.6}\",\n          \"fontSize\": \"{fontSize.4}\",\n          \"letterSpacing\": \"{letterSpacing.2}\",\n          \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n          \"paragraphIndent\": \"{paragraphIndent.0}\",\n          \"textCase\": \"{textCase.none}\",\n          \"textDecoration\": \"{textDecoration.none}\"\n        },\n        \"type\": \"typography\"\n      },\n      \"regular\": {\n        \"value\": {\n          \"fontFamily\": \"{fontFamilies.manrope}\",\n          \"fontWeight\": \"{fontWeights.manrope-6}\",\n          \"lineHeight\": \"{lineHeights.8}\",\n          \"fontSize\": \"{fontSize.3}\",\n          \"letterSpacing\": \"{letterSpacing.2}\",\n          \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n          \"paragraphIndent\": \"{paragraphIndent.0}\",\n          \"textCase\": \"{textCase.none}\",\n          \"textDecoration\": \"{textDecoration.none}\"\n        },\n        \"type\": \"typography\"\n      },\n      \"small\": {\n        \"value\": {\n          \"fontFamily\": \"{fontFamilies.manrope}\",\n          \"fontWeight\": \"{fontWeights.manrope-7}\",\n          \"lineHeight\": \"{lineHeights.0}\",\n          \"fontSize\": \"{fontSize.2}\",\n          \"letterSpacing\": \"{letterSpacing.4}\",\n          \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n          \"paragraphIndent\": \"{paragraphIndent.0}\",\n          \"textCase\": \"{textCase.none}\",\n          \"textDecoration\": \"{textDecoration.none}\"\n        },\n        \"type\": \"typography\"\n      },\n      \"Thumbnail Large\": {\n        \"Default\": {\n          \"value\": {\n            \"fontFamily\": \"{fontFamilies.manrope}\",\n            \"fontWeight\": \"{fontWeights.manrope-8}\",\n            \"lineHeight\": \"{lineHeights.10}\",\n            \"fontSize\": \"{fontSize.7}\",\n            \"letterSpacing\": \"{letterSpacing.5}\",\n            \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n            \"paragraphIndent\": \"{paragraphIndent.0}\",\n            \"textCase\": \"{textCase.none}\",\n            \"textDecoration\": \"{textDecoration.none}\"\n          },\n          \"type\": \"typography\"\n        },\n        \"Hover\": {\n          \"value\": {\n            \"fontFamily\": \"{fontFamilies.manrope}\",\n            \"fontWeight\": \"{fontWeights.manrope-9}\",\n            \"lineHeight\": \"{lineHeights.10}\",\n            \"fontSize\": \"{fontSize.7}\",\n            \"letterSpacing\": \"{letterSpacing.5}\",\n            \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n            \"paragraphIndent\": \"{paragraphIndent.0}\",\n            \"textCase\": \"{textCase.none}\",\n            \"textDecoration\": \"{textDecoration.none}\"\n          },\n          \"type\": \"typography\"\n        }\n      },\n      \"Thumbnail Small\": {\n        \"Default\": {\n          \"value\": {\n            \"fontFamily\": \"{fontFamilies.manrope}\",\n            \"fontWeight\": \"{fontWeights.manrope-6}\",\n            \"lineHeight\": \"{lineHeights.7}\",\n            \"fontSize\": \"{fontSize.6}\",\n            \"letterSpacing\": \"{letterSpacing.3}\",\n            \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n            \"paragraphIndent\": \"{paragraphIndent.0}\",\n            \"textCase\": \"{textCase.none}\",\n            \"textDecoration\": \"{textDecoration.none}\"\n          },\n          \"type\": \"typography\"\n        },\n        \"Hover\": {\n          \"value\": {\n            \"fontFamily\": \"{fontFamilies.manrope}\",\n            \"fontWeight\": \"{fontWeights.manrope-9}\",\n            \"lineHeight\": \"{lineHeights.7}\",\n            \"fontSize\": \"{fontSize.6}\",\n            \"letterSpacing\": \"{letterSpacing.3}\",\n            \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n            \"paragraphIndent\": \"{paragraphIndent.0}\",\n            \"textCase\": \"{textCase.none}\",\n            \"textDecoration\": \"{textDecoration.none}\"\n          },\n          \"type\": \"typography\"\n        }\n      },\n      \"button\": {\n        \"regular\": {\n          \"value\": {\n            \"fontFamily\": \"{fontFamilies.manrope}\",\n            \"fontWeight\": \"{fontWeights.manrope-7}\",\n            \"lineHeight\": \"{lineHeights.8}\",\n            \"fontSize\": \"{fontSize.3}\",\n            \"letterSpacing\": \"{letterSpacing.2}\",\n            \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n            \"paragraphIndent\": \"{paragraphIndent.0}\",\n            \"textCase\": \"{textCase.none}\",\n            \"textDecoration\": \"{textDecoration.none}\"\n          },\n          \"type\": \"typography\"\n        },\n        \"cta\": {\n          \"value\": {\n            \"fontFamily\": \"{fontFamilies.manrope}\",\n            \"fontWeight\": \"{fontWeights.manrope-5}\",\n            \"lineHeight\": \"{lineHeights.4}\",\n            \"fontSize\": \"{fontSize.5}\",\n            \"letterSpacing\": \"{letterSpacing.3}\",\n            \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n            \"paragraphIndent\": \"{paragraphIndent.0}\",\n            \"textCase\": \"{textCase.none}\",\n            \"textDecoration\": \"{textDecoration.none}\"\n          },\n          \"type\": \"typography\"\n        }\n      }\n    },\n    \"maintenance\": {\n      \"light\": {\n        \"value\": \"#ededed\",\n        \"type\": \"color\"\n      },\n      \"medium\": {\n        \"value\": \"#c7c7c7\",\n        \"type\": \"color\"\n      },\n      \"dark\": {\n        \"value\": \"#858585\",\n        \"type\": \"color\"\n      },\n      \"spacerViz\": {\n        \"value\": \"#f9c6c6\",\n        \"type\": \"color\"\n      }\n    },\n    \"border-width\": {\n      \"0\": {\n        \"value\": \"1\",\n        \"type\": \"borderWidth\",\n        \"description\": \"sets border width value to 1\"\n      },\n      \"1\": {\n        \"value\": \"2\",\n        \"type\": \"borderWidth\",\n        \"description\": \"sets border width value to 2. Used for focus borders (among other things)\"\n      }\n    },\n    \"paragraphIndent\": {\n      \"0\": {\n        \"value\": \"0px\",\n        \"type\": \"dimension\"\n      }\n    },\n    \"fontSize\": {\n      \"0\": {\n        \"value\": \"8\",\n        \"type\": \"fontSizes\"\n      },\n      \"1\": {\n        \"value\": \"10\",\n        \"type\": \"fontSizes\"\n      },\n      \"2\": {\n        \"value\": \"12\",\n        \"type\": \"fontSizes\"\n      },\n      \"3\": {\n        \"value\": \"16\",\n        \"type\": \"fontSizes\"\n      },\n      \"4\": {\n        \"value\": \"20\",\n        \"type\": \"fontSizes\"\n      },\n      \"5\": {\n        \"value\": \"32\",\n        \"type\": \"fontSizes\"\n      },\n      \"6\": {\n        \"value\": \"48\",\n        \"type\": \"fontSizes\"\n      },\n      \"7\": {\n        \"value\": \"260\",\n        \"type\": \"fontSizes\"\n      }\n    },\n    \"panel section drop shadow\": {\n      \"value\": [\n        {\n          \"color\": \"#00000014\",\n          \"type\": \"dropShadow\",\n          \"x\": \"0\",\n          \"y\": \"4\",\n          \"blur\": \"15\",\n          \"spread\": \"0\"\n        },\n        {\n          \"color\": \"#00000014\",\n          \"type\": \"dropShadow\",\n          \"x\": \"0\",\n          \"y\": \"1\",\n          \"blur\": \"7\",\n          \"spread\": \"0\"\n        }\n      ],\n      \"type\": \"boxShadow\",\n      \"description\": \"Shadow for sections that overlap other sections within the same panel. More subtle than the menu drop shadow.\"\n    },\n    \"Regular Bold\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-1}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Regular text, except it's bold.\"\n    },\n    \"Mono Bold\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.roboto-mono}\",\n        \"fontWeight\": \"{fontWeights.roboto-mono-4}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.2}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.none}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Labels with code syntax, such as CSS property labels within tooltips\"\n    },\n    \"darkBlue-Fade\": {\n      \"value\": \"linear-gradient(180deg, #11273f 0%, #11273f00 100%)\",\n      \"type\": \"color\"\n    },\n    \"Stroke-Fade\": {\n      \"value\": \"linear-gradient(180deg, #9697fc 0%, #4a4efa00 100%)\",\n      \"type\": \"color\"\n    },\n    \"Link\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-0}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.underline}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"link text\"\n    },\n    \"Regular Link\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-0}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.underline}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Regular text underlined to represent links within a body of Regular text\"\n    },\n    \"Label Link\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.inter}\",\n        \"fontWeight\": \"{fontWeights.inter-2}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.0}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.underline}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Label text with underline to represent links in Label text. Sentence case.\"\n    },\n    \"Mono Bold Link\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.roboto-mono}\",\n        \"fontWeight\": \"{fontWeights.roboto-mono-4}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.2}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.underline}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Mono Bold text style with underline to indicate a text link\"\n    },\n    \"Mono Link\": {\n      \"value\": {\n        \"fontFamily\": \"{fontFamilies.roboto-mono}\",\n        \"fontWeight\": \"{fontWeights.roboto-mono-3}\",\n        \"lineHeight\": \"{lineHeights.0}\",\n        \"fontSize\": \"{fontSize.2}\",\n        \"letterSpacing\": \"{letterSpacing.2}\",\n        \"paragraphSpacing\": \"{paragraphSpacing.0}\",\n        \"paragraphIndent\": \"{paragraphIndent.0}\",\n        \"textCase\": \"{textCase.none}\",\n        \"textDecoration\": \"{textDecoration.underline}\"\n      },\n      \"type\": \"typography\",\n      \"description\": \"Mono text style with underline to indicate a text link\"\n    }\n  },\n  \"$themes\": [],\n  \"$metadata\": {\n    \"tokenSetOrder\": [\"global\"]\n  }\n}\n"
  },
  {
    "path": "packages/design-system/src/__generated__/figma-design-tokens.ts",
    "content": "// Generated by transform-figma-tokens.ts from ./src/__generated__/figma-design-tokens.json\n\nexport const boxShadow = {\n  menuDropShadow: \"0px 2px 7px 0px #0000001a, 0px 5px 17px 0px #0000004d\",\n  brandElevationSmall: \"0px 4px 4px 0px #1717171a\",\n  brandElevationBig: \"0px 8px 16px 0px #1717171a\",\n  panelSectionDropShadow:\n    \"0px 4px 15px 0px #00000014, 0px 1px 7px 0px #00000014\",\n} as const;\n\nexport const fontFamilies = {\n  inter:\n    \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n  manrope: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n  robotoMono: \"Roboto Mono, RobotoMono, menlo, monospace\",\n} as const;\n\nexport const lineHeights = {\n  \"0\": \"16px\",\n  \"1\": \"8px\",\n  \"2\": \"12px\",\n  \"3\": \"10px\",\n  \"4\": \"39px\",\n  \"5\": \"11px\",\n  \"6\": \"27px\",\n  \"7\": \"58px\",\n  \"8\": \"22px\",\n  \"9\": \"38px\",\n  \"10\": \"260px\",\n} as const;\n\nexport const fontWeights = {\n  inter_0: 400,\n  inter_1: 700,\n  inter_2: 500,\n  inter_3: 700,\n  inter_4: 600,\n  robotoMono_4: 700,\n  manrope_5: 700,\n  manrope_6: 400,\n  manrope_7: 600,\n  manrope_8: 200,\n  manrope_9: 800,\n  robotoMono_3: 500,\n  inter_5: 600,\n  manrope_10: 800,\n} as const;\n\nexport const letterSpacing = {\n  \"0\": \"0.005em\",\n  \"1\": \"0.01em\",\n  \"2\": \"0em\",\n  \"3\": \"-0.02em\",\n  \"4\": \"0.02em\",\n  \"5\": \"-0.05em\",\n} as const;\n\nexport const paragraphSpacing = { \"0\": 0 } as const;\n\nexport const typography = {\n  regular: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 400,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.005em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  labels: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.005em\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  titles: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 700,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.01em\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  small: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 400,\n    fontSize: \"10px\",\n    lineHeight: \"11px\",\n    letterSpacing: \"0.01em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  tiny: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"8px\",\n    lineHeight: \"8px\",\n    letterSpacing: \"0.01em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  unit: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"10px\",\n    lineHeight: \"12px\",\n    letterSpacing: \"0em\",\n    textTransform: \"uppercase\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  mono: {\n    fontFamily: \"Roboto Mono, RobotoMono, menlo, monospace\",\n    fontWeight: 500,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  bigTitle: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"32px\",\n    lineHeight: \"39px\",\n    letterSpacing: \"0.005em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  spaceSectionUnitText: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"8px\",\n    lineHeight: \"8px\",\n    letterSpacing: \"0.01em\",\n    textTransform: \"uppercase\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  spaceSectionValueText: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"10px\",\n    lineHeight: \"10px\",\n    letterSpacing: \"0.01em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandLargeTitle: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 700,\n    fontSize: \"48px\",\n    lineHeight: \"58px\",\n    letterSpacing: \"-0.02em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandMediumTitle: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 700,\n    fontSize: \"32px\",\n    lineHeight: \"38px\",\n    letterSpacing: \"-0.02em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandSectionTitle: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 700,\n    fontSize: \"20px\",\n    lineHeight: \"27px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandRegular: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 400,\n    fontSize: \"16px\",\n    lineHeight: \"22px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandSmall: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 600,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.02em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandThumbnailLargeDefault: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 200,\n    fontSize: \"260px\",\n    lineHeight: \"260px\",\n    letterSpacing: \"-0.05em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandThumbnailLargeHover: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 800,\n    fontSize: \"260px\",\n    lineHeight: \"260px\",\n    letterSpacing: \"-0.05em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandThumbnailSmallDefault: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 400,\n    fontSize: \"48px\",\n    lineHeight: \"58px\",\n    letterSpacing: \"-0.02em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandThumbnailSmallHover: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 800,\n    fontSize: \"48px\",\n    lineHeight: \"58px\",\n    letterSpacing: \"-0.02em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandButtonRegular: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 600,\n    fontSize: \"16px\",\n    lineHeight: \"22px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  brandButtonCta: {\n    fontFamily: \"Manrope Variable, ManropeVariable, Manrope, sans-serif\",\n    fontWeight: 700,\n    fontSize: \"32px\",\n    lineHeight: \"39px\",\n    letterSpacing: \"-0.02em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  regularBold: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 700,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.005em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  monoBold: {\n    fontFamily: \"Roboto Mono, RobotoMono, menlo, monospace\",\n    fontWeight: 700,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"none\",\n    textIndent: \"0px\",\n  },\n  link: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 400,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.005em\",\n    textTransform: \"none\",\n    textDecoration: \"underline\",\n    textIndent: \"0px\",\n  },\n  regularLink: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 400,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.005em\",\n    textTransform: \"none\",\n    textDecoration: \"underline\",\n    textIndent: \"0px\",\n  },\n  labelLink: {\n    fontFamily:\n      \"Inter Variable, InterVariable, Inter, -apple-system, system-ui, sans-serif\",\n    fontWeight: 500,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0.005em\",\n    textTransform: \"none\",\n    textDecoration: \"underline\",\n    textIndent: \"0px\",\n  },\n  monoBoldLink: {\n    fontFamily: \"Roboto Mono, RobotoMono, menlo, monospace\",\n    fontWeight: 700,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"underline\",\n    textIndent: \"0px\",\n  },\n  monoLink: {\n    fontFamily: \"Roboto Mono, RobotoMono, menlo, monospace\",\n    fontWeight: 500,\n    fontSize: \"12px\",\n    lineHeight: \"16px\",\n    letterSpacing: \"0em\",\n    textTransform: \"none\",\n    textDecoration: \"underline\",\n    textIndent: \"0px\",\n  },\n} as const;\n\nexport const textCase = {\n  none: \"none\",\n  uppercase: \"uppercase\",\n  capitalize: \"capitalize\",\n} as const;\n\nexport const textDecoration = { none: \"none\", underline: \"underline\" } as const;\n\nexport const borderRadius = {\n  \"0\": \"1px\",\n  \"1\": \"2px\",\n  \"2\": \"3px\",\n  \"3\": \"4px\",\n  \"4\": \"5px\",\n  \"5\": \"6px\",\n} as const;\n\nexport const color = {\n  white: \"#ffffff\",\n  black: \"#000000\",\n  backgroundPanel: \"#fff\",\n  backgroundPrimary: \"#096cff\",\n  backgroundHover: \"#efefef\",\n  backgroundActive: \"#096cff\",\n  backgroundMenu: \"#fff\",\n  backgroundControls: \"#f5f5f5\",\n  backgroundAssetcardHover: \"#e6e8eb\",\n  backgroundNeutralMain: \"#d7dbdf\",\n  backgroundNeutralAccent: \"#11181c\",\n  backgroundNeutralNotification: \"#ffffff\",\n  backgroundNeutralDark: \"#b2b2b2\",\n  backgroundDestructiveMain: \"#dc2929\",\n  backgroundDestructiveNotification: \"#ffe9e9\",\n  backgroundSuccessMain: \"#00894a\",\n  backgroundSuccessNotification: \"#e9f9ee\",\n  backgroundAlertMain: \"#f5d90a\",\n  backgroundAlertNotification: \"#fffbd1\",\n  backgroundInfoMain: \"#0175dc\",\n  backgroundInfoNotification: \"#e0f0ff\",\n  backgroundPresetMain: \"#e6e6e6\",\n  backgroundPresetHover: \"#dfe3e6\",\n  backgroundLocalMain: \"#e1f0ff\",\n  backgroundLocalHover: \"#cee7fe\",\n  backgroundRemoteMain: \"#ffe8d7\",\n  backgroundRemoteHover: \"#ffdcc3\",\n  backgroundInputSelected: \"#b7d9f8\",\n  backgroundInputDisabled: \"#f8f8f8\",\n  backgroundInputHighlight: \"#b7d9f8\",\n  backgroundButtonHover: \"#00000010\",\n  backgroundButtonPressed: \"#0000001c\",\n  backgroundButtonDisabled: \"#e9ebed\",\n  backgroundButtonDisabledDark: \"#646464\",\n  backgroundItemCurrent: \"#e1f0ff\",\n  backgroundItemCurrentChild: \"#fff\",\n  backgroundItemCurrentHidden: \"#7e868c\",\n  backgroundItemMenuItemHover: \"#efefef\",\n  backgroundTooltipMain: \"#11181c\",\n  backgroundTooltipBuilder: \"#ffffff\",\n  backgroundTooltipDesigner: \"#ffffff\",\n  backgroundSpacingTopBottom: \"#f8f8f8\",\n  backgroundSpacingLeftRight: \"#f1f3f5\",\n  backgroundSpacingHover: \"#e3e3e3\",\n  backgroundStyleSourceToken: \"#834df4\",\n  backgroundStyleSourceTag: \"#d54113\",\n  backgroundStyleSourceState: \"#00894a\",\n  backgroundStyleSourceNeutral: \"#687076\",\n  backgroundStyleSourceDisabled: \"#9da2a6\",\n  backgroundStyleSourceGradientToken:\n    \"linear-gradient(90deg, #834df400 0%, #834df4 31.87%)\",\n  backgroundStyleSourceGradientTag:\n    \"linear-gradient(90deg, #d5411300 0%, #d54113 31.87%)\",\n  backgroundStyleSourceGradientUnselected:\n    \"linear-gradient(90deg, #68707600 0%, #687076 31.87%)\",\n  backgroundStyleSourceGradientLocal:\n    \"linear-gradient(90deg, #096cff00 0%, #096cff 31.87%)\",\n  backgroundStyleSourceBreakpoint: \"#bd2fdb\",\n  backgroundStyleSourceLocal: \"#096cff\",\n  backgroundCanvas: \"#eee\",\n  backgroundTopbar: \"#2d2d2d\",\n  backgroundGradientPrimary:\n    \"linear-gradient(135deg, #1774ff 0%, #bd2fdb 100%)\",\n  backgroundGradientVertical:\n    \"linear-gradient(180deg, #096cff 0%, #096cff 0.01%, #bd2fdb 100%)\",\n  backgroundGradientHorizontal:\n    \"linear-gradient(90deg, #4a4efa 0%, #bd2fdb 100%)\",\n  backgroundGradientHorizontalReverse:\n    \"linear-gradient(90deg, #bd2fdb 0%, #4a4efa 100%)\",\n  backgroundMenuHint: \"#efefef\",\n  backgroundTopbarHover: \"#383838\",\n  backgroundWorkspace: \"#a9a9a9\",\n  backgroundIconSubtle: \"#3e3e3e\",\n  backgroundPrimaryLight: \"#2e82ff\",\n  backgroundOverwrittenMain: \"#ffd9d9\",\n  backgroundOverwrittenHover: \"#fec4c4\",\n  backgroundDisabledDark: \"#323232\",\n  brandBackgroundProjectCardFront:\n    \"linear-gradient(0deg, #fbf8ff 0%, #e2e2e2 100%)\",\n  brandBackgroundProjectCardBack:\n    \"linear-gradient(0deg, #fbf8ff 0%, #c7c7c7 100%)\",\n  brandBackgroundProjectCardTextArea: \"#ffffff\",\n  brandBackgroundPublishedMain: \"#39fbbb\",\n  brandBackgroundGradient: \"linear-gradient(180deg, #e63cfe 0%, #ffae3c 100%)\",\n  brandBackgroundPublishedContrast: \"#ebfffc\",\n  brandBackgroundDashboard:\n    \"radial-gradient(65.88% 47.48% at 50% 50%, #FFFFFF 0%, #ffffff00 100%),linear-gradient(0deg, #ffffff00 49.46%, #ffffff54 100%), linear-gradient(180deg, #ffae3c00 0%, #e63cfe54 100%),radial-gradient(211.58% 161.63% at 3.13% 100%, #ffae3c4d 0%, #e335ff00 100%), radial-gradient(107.1% 32.15% at 92.96% 5.04%, #35ffb633 0%, #4a4efa33 100%), #EBFFFC;\",\n  brandBackgroundRegularButtonSelected:\n    \"linear-gradient(180deg, #bffeec 0%, #fbfff6 100%)\",\n  brandBackgroundCtaButton: \"linear-gradient(135deg, #4a4efa 0%, #bd2fdb 100%)\",\n  brandForegroundPublished: \"#00894a\",\n  brandBorderPublished: \"#ebfffc\",\n  brandBorderFullGradient:\n    \"linear-gradient(135deg, #92fddc 0%, #7d7ffb 31.94%, #ed72fe 64.24%, #fdd791 100%)\",\n  brandBorderNavbar:\n    \"linear-gradient(90deg, #39fbbb00 0%, #39fbbb 20%, #4a4efa 40.03%, #e63cfe 60.02%, #ffae3c 80.04%, #ffae3c00 100%, #ffae3c00 100%)\",\n  brandSpinnerTurquoise: \"#39fbbb\",\n  brandSpinnerBlue: \"#4a4efa\",\n  brandSpinnerPurple: \"#e63cfe\",\n  brandSpinnerOrange: \"#ffae3c\",\n  borderMain: \"#e6e6e6\",\n  borderFocus: \"#096cff\",\n  borderMenuInner: \"#fcfcfc\",\n  borderColorSwatch: \"#687076\",\n  borderNeutral: \"#e8e8e8\",\n  borderSuccess: \"#00894a\",\n  borderAlert: \"#e2c802\",\n  borderInfo: \"#096cff\",\n  borderContrast: \"#ffffff\",\n  borderItemChildLine: \"#889096\",\n  borderItemChildLineCurrent: \"#96c7f2\",\n  borderLocalMain: \"#b7d9f8\",\n  borderLocalFlexUi: \"#096cff\",\n  borderRemoteMain: \"#fbc69f\",\n  borderRemoteFlexUi: \"#fa934e\",\n  borderDestructiveMain: \"#d13a3a\",\n  borderDestructiveNotification: \"#f9c6c6\",\n  borderDark: \"#595c5d\",\n  borderOverwrittenMain: \"#fdaaab\",\n  borderOverwrittenFlexUi: \"#dc3d43\",\n  borderPrimary: \"#096cff\",\n  foregroundMain: \"#11181c\",\n  foregroundSubtle: \"#656869\",\n  foregroundCategoryLabel: \"#889096\",\n  foregroundTextSubtle: \"#687076\",\n  foregroundDestructive: \"#d13a3a\",\n  foregroundSuccess: \"#00b661\",\n  foregroundInfo: \"#096cff\",\n  foregroundDisabled: \"#c1c8cd\",\n  foregroundHiddenItem: \"#7e868c\",\n  foregroundFlexUiMain: \"#c7c7c7\",\n  foregroundFlexUiHover: \"#e3e3e3\",\n  foregroundContrastMain: \"#ffffff\",\n  foregroundContrastSubtle: \"#c1c8cd\",\n  foregroundLocalMain: \"#016ccc\",\n  foregroundLocalFlexUi: \"#237cff\",\n  foregroundRemoteMain: \"#b74900\",\n  foregroundRemoteFlexUi: \"#fa934e\",\n  foregroundTextMoreSubtle: \"#8d949a\",\n  foregroundPrimary: \"#096cff\",\n  foregroundSuccessText: \"#008447\",\n  foregroundGridControlsDot: \"#c7c7c7\",\n  foregroundGridControlsDotHover: \"#707375\",\n  foregroundGridControlsFlexHover: \"#e3e3e3\",\n  foregroundIconSecondary: \"#c1c8cd\",\n  foregroundIconMain: \"#11181c\",\n  foregroundMoreSubtle: \"#adb1b4\",\n  foregroundScrollBar: \"#a7acaf99\",\n  foregroundOverwrittenMain: \"#bf0007\",\n  foregroundOverwrittenFlexUi: \"#dc3d43\",\n  foregroundInversePrimary: \"#ff22ae\",\n  foregroundReusable: \"#834df4\",\n  maintenanceLight: \"#ededed\",\n  maintenanceMedium: \"#c7c7c7\",\n  maintenanceDark: \"#858585\",\n  maintenanceSpacerViz: \"#f9c6c6\",\n  darkBlueFade: \"linear-gradient(180deg, #11273f 0%, #11273f00 100%)\",\n  strokeFade: \"linear-gradient(180deg, #9697fc 0%, #4a4efa00 100%)\",\n} as const;\n\nexport const borderWidth = { \"0\": 1, \"1\": 2 } as const;\n\nexport const dimension = { paragraphIndent_0: \"0px\" } as const;\n\nexport const fontSizes = {\n  fontSize_0: \"8px\",\n  fontSize_1: \"10px\",\n  fontSize_2: \"12px\",\n  fontSize_3: \"16px\",\n  fontSize_4: \"20px\",\n  fontSize_5: \"32px\",\n  fontSize_6: \"48px\",\n  fontSize_7: \"260px\",\n} as const;\n\nexport const other = { tokenSetOrder_0: \"global\" } as const;\n"
  },
  {
    "path": "packages/design-system/src/components/__DEPRECATED__/list.stories.tsx",
    "content": "import { CheckMarkIcon, EllipsesIcon } from \"@webstudio-is/icons\";\nimport { useState } from \"react\";\nimport { DeprecatedList, DeprecatedListItem, useDeprecatedList } from \"./list\";\nimport { StorySection } from \"../storybook\";\n\nexport default {\n  title: \"Deprecated/List\",\n  component: DeprecatedList,\n};\n\nexport const List = () => {\n  const items = [\"Banana\", \"Orange\", \"Apple\"];\n  const [selectedIndex, setSelectedIndex] = useState(-1);\n  const [currentIndex, setCurrentIndex] = useState(-1);\n  const { getListProps, getItemProps } = useDeprecatedList({\n    items,\n    selectedIndex,\n    currentIndex,\n    onSelect: setSelectedIndex,\n    onChangeCurrent: setCurrentIndex,\n  });\n\n  return (\n    <>\n      <StorySection title=\"Declarative\">\n        <DeprecatedList>\n          <DeprecatedListItem>Apple</DeprecatedListItem>\n          <DeprecatedListItem state=\"disabled\">Banana</DeprecatedListItem>\n          <DeprecatedListItem state=\"selected\">Orange</DeprecatedListItem>\n          <DeprecatedListItem\n            prefix={<CheckMarkIcon />}\n            suffix={<EllipsesIcon />}\n          >\n            Strawberry\n          </DeprecatedListItem>\n          <DeprecatedListItem\n            prefix={<CheckMarkIcon />}\n            suffix={<EllipsesIcon />}\n            current\n            state=\"selected\"\n          >\n            Watermelon\n          </DeprecatedListItem>\n        </DeprecatedList>\n      </StorySection>\n\n      <StorySection title=\"With hook\">\n        <DeprecatedList {...getListProps()}>\n          {items.map((item, index) => {\n            const itemProps = getItemProps({ index });\n            return (\n              <DeprecatedListItem\n                {...itemProps}\n                key={index}\n                prefix={itemProps.current ? <CheckMarkIcon /> : undefined}\n              >\n                {item}\n              </DeprecatedListItem>\n            );\n          })}\n        </DeprecatedList>\n      </StorySection>\n\n      <StorySection title=\"Current without selected\">\n        <DeprecatedList>\n          <DeprecatedListItem>Regular item</DeprecatedListItem>\n          <DeprecatedListItem current>\n            Current but not selected\n          </DeprecatedListItem>\n          <DeprecatedListItem prefix={<CheckMarkIcon />}>\n            With prefix only\n          </DeprecatedListItem>\n        </DeprecatedList>\n      </StorySection>\n\n      <StorySection title=\"Suffix only\">\n        <DeprecatedList>\n          <DeprecatedListItem suffix={<EllipsesIcon />}>\n            Item with suffix\n          </DeprecatedListItem>\n          <DeprecatedListItem suffix={<EllipsesIcon />} state=\"selected\">\n            Selected with suffix\n          </DeprecatedListItem>\n          <DeprecatedListItem suffix={<EllipsesIcon />} state=\"disabled\">\n            Disabled with suffix\n          </DeprecatedListItem>\n        </DeprecatedList>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/__DEPRECATED__/list.tsx",
    "content": "import {\n  type ComponentProps,\n  type FocusEvent,\n  forwardRef,\n  type JSX,\n  type KeyboardEvent,\n} from \"react\";\nimport { Text } from \"../text\";\nimport { Flex } from \"../flex\";\nimport { styled, theme } from \"../../stitches.config\";\nimport { findNextListItemIndex } from \"../primitives/list\";\n\nconst ListBase = styled(\"ul\", {\n  display: \"flex\",\n  flexDirection: \"column\",\n  margin: 0,\n  padding: 0,\n});\n\nconst ListItemBase = styled(\"li\", {\n  display: \"grid\",\n  gridTemplateColumns: `${theme.spacing[10]} 1fr`,\n  alignItems: \"center\",\n  justifyContent: \"space-between\",\n  height: theme.spacing[11],\n  paddingLeft: theme.spacing[5],\n  paddingRight: theme.spacing[8],\n  listStyle: \"none\",\n  outline: 0,\n  position: \"relative\",\n  \"&[aria-selected]::before\": {\n    content: \"''\",\n    position: \"absolute\",\n    pointerEvents: \"none\",\n    inset: `0 ${theme.spacing[3]}`,\n    borderRadius: theme.borderRadius[4],\n    border: `1px solid ${theme.colors.borderFocus}`,\n  },\n});\n\nexport const DeprecatedList = forwardRef<\n  HTMLUListElement,\n  ComponentProps<typeof ListBase>\n>((props, ref) => {\n  return <ListBase role=\"listbox\" ref={ref} {...props} />;\n});\nDeprecatedList.displayName = \"DeprecatedList\";\n\nexport const DeprecatedListItem = forwardRef<\n  HTMLLIElement,\n  Omit<ComponentProps<typeof ListItemBase>, \"prefix\" | \"suffix\" | \"current\"> & {\n    state?: \"disabled\" | \"selected\";\n    prefix?: JSX.Element;\n    suffix?: JSX.Element;\n    current?: boolean;\n  }\n>(({ children, prefix, suffix, state, current, ...props }, ref) => {\n  return (\n    <ListItemBase\n      ref={ref}\n      tabIndex={state === \"disabled\" ? -1 : 0}\n      role=\"option\"\n      {...(state === \"disabled\" ? { \"aria-disabled\": true } : undefined)}\n      {...(state === \"selected\" ? { \"aria-selected\": true } : undefined)}\n      {...(current ? { \"aria-current\": true } : undefined)}\n      {...props}\n    >\n      {prefix}\n      <Flex\n        css={{ gridColumn: 2, cursor: \"default\" }}\n        align=\"center\"\n        justify=\"between\"\n      >\n        <Text\n          variant=\"labels\"\n          truncate\n          color={state === \"disabled\" ? \"subtle\" : \"main\"}\n        >\n          {children}\n        </Text>\n        {suffix}\n      </Flex>\n    </ListItemBase>\n  );\n});\nDeprecatedListItem.displayName = \"DeprecatedListItem\";\n\ntype UseList<Item = unknown> = {\n  items: Array<Item>;\n  selectedIndex: number;\n  currentIndex: number;\n  onSelect: (index: number) => void;\n  onChangeCurrent: (index: number) => void;\n};\n\nexport const useDeprecatedList = ({\n  items,\n  selectedIndex,\n  currentIndex,\n  onSelect,\n  onChangeCurrent,\n}: UseList) => {\n  const getItemProps = ({ index }: { index: number }) => {\n    return {\n      state: selectedIndex === index ? (\"selected\" as const) : undefined,\n      key: index,\n      current: currentIndex === index,\n      onFocus(event: FocusEvent) {\n        const isItem = event.target === event.currentTarget;\n        // We need to ignore focus on anything inside\n        if (isItem) {\n          onSelect(index);\n        }\n      },\n      onMouseEnter() {\n        onSelect(index);\n      },\n      onMouseLeave() {\n        onSelect(-1);\n      },\n      onClick() {\n        onChangeCurrent(index);\n      },\n    };\n  };\n\n  const getListProps = () => {\n    return {\n      onKeyDown(event: KeyboardEvent) {\n        switch (event.code) {\n          case \"ArrowUp\":\n          case \"ArrowDown\": {\n            const nextIndex = findNextListItemIndex(\n              selectedIndex,\n              items.length,\n              event.code === \"ArrowUp\" ? \"previous\" : \"next\"\n            );\n            onSelect(nextIndex);\n            break;\n          }\n          case \"Enter\":\n          case \"Space\": {\n            onChangeCurrent(selectedIndex);\n          }\n        }\n      },\n      onBlur(event: FocusEvent) {\n        const isFocusWithin =\n          event.relatedTarget instanceof Node &&\n          event.currentTarget.contains(event.relatedTarget);\n        if (isFocusWithin === false) {\n          onSelect(-1);\n        }\n      },\n    };\n  };\n\n  return { getItemProps, getListProps };\n};\n"
  },
  {
    "path": "packages/design-system/src/components/avatar.stories.tsx",
    "content": "import { Flex } from \"./flex\";\nimport { Avatar as AvatarComponent } from \"./avatar\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Avatar\",\n  component: AvatarComponent,\n};\n\nconst avatarImage = `data:image/svg+xml,${encodeURIComponent(\n  `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"150\" height=\"150\">\n    <rect width=\"150\" height=\"150\" fill=\"#7c8a99\"/>\n    <text x=\"75\" y=\"75\" text-anchor=\"middle\" dominant-baseline=\"central\"\n      font-family=\"sans-serif\" font-size=\"48\" fill=\"#fff\">JD</text>\n  </svg>`\n)}`;\n\nexport const Avatar = () => (\n  <>\n    <StorySection title=\"With image and fallback\">\n      <Flex gap=\"3\" align=\"center\">\n        <AvatarComponent src={avatarImage} alt=\"User avatar\" fallback=\"JD\" />\n        <AvatarComponent fallback=\"JD\" />\n        <AvatarComponent fallback=\"AB\" />\n        <AvatarComponent fallback=\"WS\" />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Fallback variants\">\n      <Flex gap=\"3\" align=\"center\">\n        <AvatarComponent fallback=\"A\" />\n        <AvatarComponent fallback=\"JD\" />\n        <AvatarComponent fallback=\"WEB\" />\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Broken image\">\n      <Flex gap=\"3\" align=\"center\">\n        <AvatarComponent\n          src=\"https://broken-url.invalid/avatar.png\"\n          fallback=\"FB\"\n          alt=\"Broken image avatar\"\n        />\n        <AvatarComponent fallback=\"OK\" alt=\"No image avatar\" />\n      </Flex>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/avatar.tsx",
    "content": "import {\n  type ComponentProps,\n  type ElementRef,\n  type ReactNode,\n  forwardRef,\n} from \"react\";\nimport { theme, styled, type VariantProps, type CSS } from \"../stitches.config\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\nimport { Box } from \"./box\";\n\nconst StyledAvatar = styled(AvatarPrimitive.Root, {\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  verticalAlign: \"middle\",\n  overflow: \"hidden\",\n  userSelect: \"none\",\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  flexShrink: 0,\n  position: \"relative\",\n  border: \"none\",\n  fontFamily: \"inherit\",\n  lineHeight: \"1\",\n  margin: \"0\",\n  outline: \"none\",\n  padding: \"0\",\n  fontWeight: \"500\",\n  width: 24,\n  height: 24,\n  backgroundColor: theme.colors.foregroundSubtle,\n  borderRadius: \"50%\",\n});\n\nconst StyledAvatarImage = styled(AvatarPrimitive.Image, {\n  display: \"flex\",\n  objectFit: \"cover\",\n  boxSizing: \"border-box\",\n  height: \"100%\",\n  verticalAlign: \"middle\",\n  width: \"100%\",\n});\n\nconst StyledAvatarFallback = styled(AvatarPrimitive.Fallback, {\n  textTransform: \"uppercase\",\n  borderRadius: \"50%\",\n  fontSize: theme.deprecatedFontSize[4],\n  color: theme.colors.white,\n});\n\ntype AvatarVariants = VariantProps<typeof StyledAvatar>;\ntype AvatarPrimitiveProps = ComponentProps<typeof AvatarPrimitive.Root>;\ntype AvatarOwnProps = AvatarPrimitiveProps &\n  AvatarVariants & {\n    css?: CSS;\n    alt?: string;\n    src?: string;\n    fallback?: ReactNode;\n  };\n\nexport const Avatar = forwardRef<\n  ElementRef<typeof StyledAvatar>,\n  AvatarOwnProps\n>(({ alt, src, fallback, css, ...props }, forwardedRef) => {\n  return (\n    <Box\n      css={{\n        ...css,\n        position: \"relative\",\n        height: \"fit-content\",\n        width: \"fit-content\",\n      }}\n    >\n      <StyledAvatar {...props} ref={forwardedRef}>\n        <StyledAvatarImage alt={alt} src={src} />\n        <StyledAvatarFallback>{fallback}</StyledAvatarFallback>\n      </StyledAvatar>\n    </Box>\n  );\n});\n\nAvatar.displayName = \"Avatar\";\n"
  },
  {
    "path": "packages/design-system/src/components/box.tsx",
    "content": "import { styled } from \"../stitches.config\";\n\nexport const Box = styled(\"div\", {\n  // Reset\n  boxSizing: \"border-box\",\n  minWidth: 0,\n});\n"
  },
  {
    "path": "packages/design-system/src/components/button.stories.tsx",
    "content": "import type { ComponentProps } from \"react\";\nimport { TrashIcon } from \"@webstudio-is/icons\";\nimport { Button as ButtonComponent } from \"./button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\nimport { Text } from \"./text\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { theme } from \"../stitches.config\";\n\nexport default {\n  title: \"Button\",\n};\n\nconst colors: ReadonlyArray<ComponentProps<typeof ButtonComponent>[\"color\"]> = [\n  \"primary\",\n  \"neutral\",\n  \"destructive\",\n  \"neutral-destructive\",\n  \"positive\",\n  \"ghost\",\n  \"dark\",\n  \"gradient\",\n  \"dark-ghost\",\n];\n\nconst states: ReadonlyArray<ComponentProps<typeof ButtonComponent>[\"state\"]> = [\n  \"auto\",\n  \"hover\",\n  \"focus\",\n  \"pressed\",\n  \"pending\",\n];\n\nexport const Button = () => (\n  <>\n    <StorySection title=\"Colors & States\">\n      <StoryGrid>\n        {colors.map((color) => (\n          <StoryGrid\n            horizontal\n            key={color}\n            css={\n              color === \"dark-ghost\"\n                ? { backgroundColor: \"#1E1E1E\", padding: 8 }\n                : undefined\n            }\n          >\n            {states.map((state) => (\n              <ButtonComponent\n                prefix={<TrashIcon />}\n                state={state}\n                color={color}\n                key={state}\n              >\n                {color} {state}\n              </ButtonComponent>\n            ))}\n            <ButtonComponent prefix={<TrashIcon />} color={color} disabled>\n              {color} disabled\n            </ButtonComponent>\n            <fieldset style={{ display: \"contents\" }} disabled>\n              <ButtonComponent prefix={<TrashIcon />} color={color}>\n                {color} disabled by fieldset\n              </ButtonComponent>\n            </fieldset>\n          </StoryGrid>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Icon\">\n      <StoryGrid horizontal>\n        <ButtonComponent prefix={<TrashIcon />}>Button</ButtonComponent>\n        <ButtonComponent suffix={<TrashIcon />}>Button</ButtonComponent>\n        <ButtonComponent prefix={<TrashIcon />} />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Preserves size when pending\">\n      <StoryGrid\n        css={{\n          alignItems: \"flex-start\",\n        }}\n      >\n        <ButtonComponent>Any content to preserve size</ButtonComponent>\n        <ButtonComponent state=\"pending\">\n          Any content to preserve size\n        </ButtonComponent>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Used as a Trigger for something that opens\">\n      <Popover defaultOpen>\n        <PopoverTrigger asChild>\n          <ButtonComponent prefix={<TrashIcon />}>Open</ButtonComponent>\n        </PopoverTrigger>\n        <PopoverContent css={{ padding: theme.panel.padding }}>\n          <Text>Some content</Text>\n        </PopoverContent>\n      </Popover>\n    </StorySection>\n\n    <StorySection title=\"Text only\">\n      <StoryGrid horizontal>\n        {colors.map((color) => (\n          <ButtonComponent key={color} color={color}>\n            {color}\n          </ButtonComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Icon only\">\n      <StoryGrid horizontal>\n        {colors.map((color) => (\n          <ButtonComponent key={color} prefix={<TrashIcon />} color={color} />\n        ))}\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/button.tsx",
    "content": "/**\n * Implementation of the \"Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A2709\n */\n\nimport {\n  forwardRef,\n  type Ref,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { textVariants } from \"./text\";\nimport { css, styled, theme, type CSS } from \"../stitches.config\";\nimport { LoadingDotsIcon } from \"@webstudio-is/icons\";\nimport { Flex } from \"./flex\";\n\nconst colors = [\n  \"primary\",\n  \"destructive\",\n  \"positive\",\n  \"neutral\",\n  \"ghost\",\n  \"dark\",\n  \"gradient\",\n  \"neutral-destructive\",\n  \"dark-ghost\",\n] as const;\n\ntype ButtonColor = (typeof colors)[number];\n\ntype ButtonState = \"auto\" | \"hover\" | \"focus\" | \"pressed\" | \"pending\";\n\nconst backgrounds: Record<ButtonColor, string> = {\n  primary: theme.colors.backgroundPrimary,\n  neutral: theme.colors.backgroundNeutralMain,\n  \"neutral-destructive\": theme.colors.backgroundNeutralMain,\n  destructive: theme.colors.backgroundDestructiveMain,\n  positive: theme.colors.backgroundSuccessMain,\n  ghost: theme.colors.backgroundHover,\n  dark: theme.colors.backgroundTopbar,\n  gradient: theme.colors.backgroundGradientPrimary,\n  \"dark-ghost\": theme.colors.backgroundTopbar,\n};\n\nconst foregrounds: Record<ButtonColor, string> = {\n  primary: theme.colors.foregroundContrastMain,\n  destructive: theme.colors.foregroundContrastMain,\n  \"neutral-destructive\": theme.colors.foregroundDestructive,\n  positive: theme.colors.foregroundContrastMain,\n  neutral: theme.colors.foregroundMain,\n  ghost: theme.colors.foregroundMain,\n  dark: theme.colors.foregroundContrastMain,\n  gradient: theme.colors.foregroundContrastMain,\n  \"dark-ghost\": theme.colors.foregroundContrastMain,\n};\n\nconst perColorStyle = (variant: ButtonColor) => ({\n  background:\n    variant === \"ghost\" || variant === \"dark-ghost\"\n      ? \"transparent\"\n      : backgrounds[variant],\n  color:\n    variant === \"dark-ghost\"\n      ? theme.colors.foregroundSubtle\n      : foregrounds[variant],\n\n  \"&[data-state=auto]:hover, &[data-state=hover]\": {\n    color: foregrounds[variant],\n    background:\n      variant === \"gradient\"\n        ? `linear-gradient(${theme.colors.backgroundButtonHover}, ${theme.colors.backgroundButtonHover}), ${backgrounds[variant]}`\n        : `oklch(from ${backgrounds[variant]} l c h / 0.8)`,\n  },\n\n  \"&[data-state=auto]:focus-visible, &[data-state=focus]\": {\n    color: foregrounds[variant],\n    outline: `1px solid ${theme.colors.borderFocus}`,\n    outlineOffset: \"1px\",\n  },\n\n  \"&[data-state=auto]:active, &[data-state=pressed]\": {\n    color: foregrounds[variant],\n    background:\n      variant === \"gradient\"\n        ? `linear-gradient(${theme.colors.backgroundButtonPressed}, ${theme.colors.backgroundButtonPressed}), ${backgrounds[variant]}`\n        : `oklch(from ${backgrounds[variant]} l c h / 0.8)`,\n  },\n\n  \"&:disabled:not([data-state=pending]), &[data-state=disabled], &[aria-disabled=true], &[aria-disabled=true]:hover, &[aria-disabled=true]:visited\":\n    {\n      background: theme.colors.backgroundButtonDisabled,\n      color: theme.colors.foregroundDisabled,\n    },\n\n  \"&[data-state=pending]\": {\n    cursor: \"wait\",\n  },\n});\n\nexport const buttonStyle = css({\n  all: \"unset\",\n  boxSizing: \"border-box\",\n  minWidth: 0,\n  display: \"inline-grid\",\n  gridAutoFlow: \"column\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  gap: theme.spacing[2],\n  padding: `0 ${theme.spacing[3]}`,\n  height: theme.sizes.controlHeight,\n  borderRadius: theme.borderRadius[4],\n  whiteSpace: \"nowrap\",\n\n  variants: {\n    color: {\n      primary: perColorStyle(\"primary\"),\n      destructive: perColorStyle(\"destructive\"),\n      \"neutral-destructive\": perColorStyle(\"neutral-destructive\"),\n      positive: perColorStyle(\"positive\"),\n      neutral: perColorStyle(\"neutral\"),\n      ghost: perColorStyle(\"ghost\"),\n      dark: perColorStyle(\"dark\"),\n      gradient: perColorStyle(\"gradient\"),\n      \"dark-ghost\": perColorStyle(\"dark-ghost\"),\n    },\n  },\n\n  defaultVariants: {\n    color: \"primary\",\n  },\n});\n\nconst TextContainer = styled(\"span\", textVariants.labels, {\n  padding: `0 ${theme.spacing[2]}`,\n  overflow: \"hidden\",\n  textOverflow: \"ellipsis\",\n  position: \"relative\",\n  variants: {\n    // \"hidden\" is used to hide the text when the button is in a pending state but preserving the button size\n    hidden: {\n      true: {\n        visibility: \"hidden\",\n      },\n    },\n  },\n});\n\ntype ButtonProps = {\n  state?: ButtonState;\n  color?: ButtonColor;\n\n  // We don't want all the noise from StyledButton,\n  // so we're cherry-picking just the props we need\n  css?: CSS;\n\n  // prefix/suffix are primarily for Icons\n  // this is a replacement for icon/icon-left/icon-right in Figma\n  prefix?: ReactNode;\n  suffix?: ReactNode;\n\n  // might be set when <Button> is asChild\n  \"data-state\"?: string;\n} & Omit<ComponentProps<\"button\">, \"prefix\">;\n\nexport const Button = forwardRef(\n  (\n    {\n      disabled,\n      state,\n      prefix,\n      suffix,\n      children,\n      \"data-state\": dataState,\n      className,\n      css,\n      color,\n      ...restProps\n    }: ButtonProps,\n    ref: Ref<HTMLButtonElement>\n  ) => {\n    // when button is used as a trigger for something that opens\n    // <SomeTrigger asChild><Button /></SomeTrigger>\n    let finalState = dataState === \"open\" ? \"pressed\" : undefined;\n\n    // \"state\" wins over \"data-state\"\n    if (state !== undefined) {\n      finalState = state;\n    }\n\n    // \"disabled\" wins over everything\n    if (disabled) {\n      finalState = \"disabled\";\n    }\n\n    return (\n      <button\n        {...restProps}\n        disabled={disabled || state === \"pending\"}\n        data-state={finalState ?? \"auto\"}\n        ref={ref}\n        className={buttonStyle({ color, className, css })}\n      >\n        {prefix}\n        {children && (\n          <TextContainer hidden={state === \"pending\"}>\n            {children}\n            {state === \"pending\" && (\n              <Flex\n                css={{\n                  position: \"absolute\",\n                  inset: 0,\n                  visibility: \"visible\",\n                  pointerEvents: \"none\",\n                }}\n                justify={\"center\"}\n                align={\"center\"}\n              >\n                <LoadingDotsIcon size={28} fill=\"currentColor\" />\n              </Flex>\n            )}\n          </TextContainer>\n        )}\n\n        {suffix}\n      </button>\n    );\n  }\n);\nButton.displayName = \"Button\";\n"
  },
  {
    "path": "packages/design-system/src/components/card.stories.tsx",
    "content": "import { Text } from \"./text\";\nimport { Card as CardComponent } from \"./card\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Card\",\n  component: CardComponent,\n};\n\nexport const Card = () => (\n  <>\n    <StorySection title=\"Sizes\">\n      <StoryGrid horizontal>\n        <CardComponent>\n          <Text>Default</Text>\n        </CardComponent>\n        <CardComponent size=\"1\">\n          <Text>Size 1</Text>\n        </CardComponent>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Multiple\">\n      <StoryGrid horizontal>\n        <CardComponent size=\"1\">\n          <Text>First card</Text>\n        </CardComponent>\n        <CardComponent size=\"1\">\n          <Text>Second card</Text>\n        </CardComponent>\n        <CardComponent size=\"1\">\n          <Text>Third card</Text>\n        </CardComponent>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/card.tsx",
    "content": "import { styled } from \"../stitches.config\";\nimport { theme } from \"../stitches.config\";\n\nexport const Card = styled(\"div\", {\n  appearance: \"none\",\n  border: \"none\",\n  boxSizing: \"border-box\",\n  font: \"inherit\",\n  lineHeight: \"1\",\n  outline: \"none\",\n  textAlign: \"inherit\",\n  verticalAlign: \"middle\",\n  WebkitTapHighlightColor: \"rgba(0, 0, 0, 0)\",\n  backgroundColor: \"white\",\n  display: \"block\",\n  textDecoration: \"none\",\n  color: \"inherit\",\n  flexShrink: 0,\n  borderRadius: theme.borderRadius[5],\n  position: \"relative\",\n\n  \"&::before\": {\n    boxSizing: \"border-box\",\n    content: '\"\"',\n    position: \"absolute\",\n    top: 0,\n    right: 0,\n    bottom: 0,\n    left: 0,\n    boxShadow: \"inset 0 0 0 1px rgba(0,0,0,.1)\",\n    borderRadius: theme.borderRadius[7],\n    pointerEvents: \"none\",\n  },\n\n  variants: {\n    size: {\n      1: {\n        width: theme.spacing[30],\n        padding: theme.spacing[11],\n      },\n    },\n  },\n  defaultVariants: {\n    size: \"1\",\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/checkbox.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { Label } from \"./label\";\nimport { Checkbox as CheckboxComponent, CheckboxAndLabel } from \"./checkbox\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { Tooltip } from \"./tooltip\";\n\nexport default {\n  title: \"Checkbox\",\n  parameters: {\n    // to make the white background in the control visible\n    backgrounds: { default: \"Panel\" },\n  },\n};\n\nconst ControlledCheckbox = () => {\n  const [checked, setChecked] = useState<boolean | \"indeterminate\">(false);\n  return (\n    <StoryGrid horizontal>\n      <CheckboxAndLabel>\n        <CheckboxComponent\n          checked={checked}\n          onCheckedChange={setChecked}\n          id=\"controlled\"\n        />\n        <Label htmlFor=\"controlled\">State: {String(checked)}</Label>\n      </CheckboxAndLabel>\n    </StoryGrid>\n  );\n};\n\nexport const Checkbox = () => {\n  return (\n    <>\n      <StorySection title=\"Enabled\">\n        <StoryGrid horizontal>\n          <CheckboxComponent defaultChecked />\n          <CheckboxComponent defaultChecked=\"indeterminate\" />\n          <CheckboxComponent />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Disabled\">\n        <StoryGrid horizontal>\n          <CheckboxComponent defaultChecked disabled />\n          <CheckboxComponent defaultChecked=\"indeterminate\" disabled />\n          <CheckboxComponent disabled />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Focussed (initially)\">\n        <StoryGrid horizontal>\n          <CheckboxComponent defaultChecked autoFocus />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"With lables\">\n        <CheckboxAndLabel>\n          <CheckboxComponent defaultChecked id=\"A\" />\n          <Label htmlFor=\"A\">Label A</Label>\n        </CheckboxAndLabel>\n        <CheckboxAndLabel>\n          <CheckboxComponent id=\"B\" />\n          <Label htmlFor=\"B\">Label B</Label>\n        </CheckboxAndLabel>\n      </StorySection>\n\n      <StorySection title=\"With Tooltip\">\n        <Tooltip content=\"Tooltip content\">\n          <CheckboxComponent defaultChecked />\n        </Tooltip>\n        <Tooltip content=\"Tooltip content\">\n          <CheckboxComponent disabled />\n        </Tooltip>\n      </StorySection>\n\n      <StorySection title=\"Controlled\">\n        <ControlledCheckbox />\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/checkbox.tsx",
    "content": "/**\n * Implementation of the \"Checkbox\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A3352\n */\n\nimport { forwardRef, type ComponentProps, type Ref } from \"react\";\nimport * as Primitive from \"@radix-ui/react-checkbox\";\nimport { CheckMarkIcon, MinusIcon } from \"@webstudio-is/icons\";\nimport { type CSS, css, theme, styled } from \"../stitches.config\";\n\nconst checkboxStyle = css({\n  all: \"unset\", // reset <button>\n\n  width: theme.spacing[7],\n  height: theme.spacing[7],\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  position: \"relative\",\n  borderRadius: theme.borderRadius[3],\n  color: theme.colors.foregroundMain,\n  background: theme.colors.backgroundControls,\n  border: `1px solid ${theme.colors.borderMain}`,\n\n  \"&:focus-visible\": {\n    borderColor: theme.colors.borderFocus,\n  },\n\n  // [data-state] is needed to make selector specificity higher\n  \"&[data-state]:disabled\": {\n    color: theme.colors.foregroundDisabled,\n  },\n});\n\nconst iconStyle = css({ position: \"relative\" });\n\nconst iconByState = {\n  checked: CheckMarkIcon,\n  unchecked: undefined,\n  indeterminate: MinusIcon,\n};\n\ntype CheckboxProps = ComponentProps<\"button\"> &\n  Pick<ComponentProps<typeof Primitive.Checkbox>, \"aria-checked\">;\n\ntype AriaChecked = ComponentProps<typeof Primitive.Checkbox>[\"aria-checked\"];\n\nconst ariaCheckedToDataState = (\n  ariaChecked: AriaChecked\n): keyof typeof iconByState => {\n  if (ariaChecked === \"true\" || ariaChecked === true) {\n    return \"checked\";\n  }\n  if (ariaChecked === \"false\" || ariaChecked === false) {\n    return \"unchecked\";\n  }\n\n  if (ariaChecked === \"mixed\" || ariaChecked === undefined) {\n    return \"indeterminate\";\n  }\n\n  ariaChecked satisfies never;\n  return \"indeterminate\";\n};\n\n// We need this component basicslly just to get access to \"data-state\".\n// We could render all icons and hide one using CSS,\n// but that probably will be less performant.\nconst Button = forwardRef(\n  (props: CheckboxProps, ref: Ref<HTMLButtonElement>) => {\n    // Using aria-checked instead of data-state ensures compatibility with Tooltip,\n    // as Tooltip overrides the Checkbox's data-state attribute.\n    const dataState = ariaCheckedToDataState(props[\"aria-checked\"]);\n    const Icon = iconByState[dataState];\n\n    return (\n      <button\n        {...props}\n        data-state={dataState}\n        type={props.type ?? \"button\"}\n        ref={ref}\n      >\n        {Icon ? <Icon className={iconStyle()} size={12} /> : undefined}\n      </button>\n    );\n  }\n);\nButton.displayName = \"Button\";\n\nexport const Checkbox = forwardRef(\n  (\n    {\n      className,\n      css,\n      ...props\n    }: ComponentProps<typeof Primitive.Root> & { css?: CSS },\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <Primitive.Root\n      className={checkboxStyle({ className, css })}\n      {...props}\n      ref={ref}\n      asChild\n    >\n      <Button />\n    </Primitive.Root>\n  )\n);\nCheckbox.displayName = \"Checkbox\";\n\nexport const CheckboxAndLabel = styled(\"div\", {\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  alignItems: \"center\",\n  gap: theme.spacing[3],\n});\n"
  },
  {
    "path": "packages/design-system/src/components/color-picker.stories.tsx",
    "content": "import { useState } from \"react\";\nimport type { RgbValue, StyleValue } from \"@webstudio-is/css-engine\";\nimport {\n  ColorPicker as ColorPickerComponent,\n  ColorPickerPopover,\n  ColorThumb,\n} from \"./color-picker\";\nimport { Flex } from \"./flex\";\nimport { Grid } from \"./grid\";\nimport { Text } from \"./text\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Color Picker\",\n};\n\nconst initialColor: RgbValue = {\n  type: \"rgb\",\n  r: 90,\n  g: 155,\n  b: 255,\n  alpha: 1,\n};\n\nexport const ColorPicker = () => {\n  const [inlineValue, setInlineValue] = useState<StyleValue>(initialColor);\n  const [popoverValue, setPopoverValue] = useState<StyleValue>(initialColor);\n\n  return (\n    <>\n      <StorySection title=\"Inline\">\n        <Flex direction=\"column\" gap=\"2\">\n          <ColorPickerComponent\n            value={inlineValue}\n            onChange={(value) => {\n              if (value !== undefined) {\n                setInlineValue(value);\n              }\n            }}\n            onChangeComplete={setInlineValue}\n          />\n          <Text>{JSON.stringify(inlineValue)}</Text>\n        </Flex>\n      </StorySection>\n\n      <StorySection title=\"Popover\">\n        <Flex direction=\"column\" gap=\"2\">\n          <ColorPickerPopover\n            value={popoverValue}\n            onChange={(value) => {\n              if (value !== undefined) {\n                setPopoverValue(value);\n              }\n            }}\n            onChangeComplete={setPopoverValue}\n          />\n          <Text>{JSON.stringify(popoverValue)}</Text>\n        </Flex>\n      </StorySection>\n\n      <StorySection title=\"Thumb\">\n        <Flex gap=\"3\" align=\"center\">\n          <ColorThumb color=\"rgb(255, 0, 0)\" />\n          <ColorThumb color=\"rgba(0, 128, 255, 0.5)\" />\n          <ColorThumb color=\"transparent\" />\n          <ColorThumb color=\"#00FF00\" interactive />\n        </Flex>\n      </StorySection>\n    </>\n  );\n};\n\nexport const PopoverPositioning = () => {\n  const [value, setValue] = useState<StyleValue>(initialColor);\n  const handleChange = (val: StyleValue | undefined) => {\n    if (val !== undefined) {\n      setValue(val);\n    }\n  };\n  return (\n    <Grid\n      columns={2}\n      gap=\"9\"\n      align=\"center\"\n      justify=\"center\"\n      style={{ padding: 100, minHeight: \"100vh\" }}\n    >\n      <Flex direction=\"column\" gap=\"2\" align=\"center\">\n        <Text variant=\"labels\">Side top</Text>\n        <ColorPickerPopover\n          value={value}\n          onChange={handleChange}\n          onChangeComplete={setValue}\n          side=\"top\"\n          open={true}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"2\" align=\"center\">\n        <Text variant=\"labels\">Side right</Text>\n        <ColorPickerPopover\n          value={value}\n          onChange={handleChange}\n          onChangeComplete={setValue}\n          side=\"right\"\n          open={true}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"2\" align=\"center\">\n        <Text variant=\"labels\">Align start</Text>\n        <ColorPickerPopover\n          value={value}\n          onChange={handleChange}\n          onChangeComplete={setValue}\n          side=\"bottom\"\n          align=\"start\"\n          open={true}\n        />\n      </Flex>\n      <Flex direction=\"column\" gap=\"2\" align=\"center\">\n        <Text variant=\"labels\">Side offset 16</Text>\n        <ColorPickerPopover\n          value={value}\n          onChange={handleChange}\n          onChangeComplete={setValue}\n          sideOffset={16}\n          open={true}\n        />\n      </Flex>\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/color-picker.tsx",
    "content": "import * as colorjs from \"colorjs.io/fn\";\nimport {\n  forwardRef,\n  type ComponentProps,\n  type ElementRef,\n  useEffect,\n  useState,\n} from \"react\";\nimport { clamp } from \"@react-aria/utils\";\nimport { useDebouncedCallback } from \"use-debounce\";\nimport { RgbaColorPicker } from \"react-colorful\";\nimport { EyedropperIcon } from \"@webstudio-is/icons\";\nimport {\n  toValue,\n  type StyleValue,\n  type Unit,\n  type RgbValue,\n} from \"@webstudio-is/css-engine\";\nimport { css, rawTheme, theme, type CSS } from \"../stitches.config\";\nimport { useDisableCanvasPointerEvents } from \"../utilities\";\nimport { Grid } from \"./grid\";\nimport { IconButton } from \"./icon-button\";\nimport { InputField } from \"./input-field\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\n\ncolorjs.ColorSpace.register(colorjs.sRGB);\ncolorjs.ColorSpace.register(colorjs.sRGB_Linear);\ncolorjs.ColorSpace.register(colorjs.HSL);\ncolorjs.ColorSpace.register(colorjs.HWB);\ncolorjs.ColorSpace.register(colorjs.Lab);\ncolorjs.ColorSpace.register(colorjs.LCH);\ncolorjs.ColorSpace.register(colorjs.OKLab);\ncolorjs.ColorSpace.register(colorjs.OKLCH);\ncolorjs.ColorSpace.register(colorjs.P3);\ncolorjs.ColorSpace.register(colorjs.A98RGB);\ncolorjs.ColorSpace.register(colorjs.ProPhoto);\ncolorjs.ColorSpace.register(colorjs.REC_2020);\ncolorjs.ColorSpace.register(colorjs.XYZ_D65);\ncolorjs.ColorSpace.register(colorjs.XYZ_D50);\n\ntype RgbaColor = {\n  r: number;\n  g: number;\n  b: number;\n  a: number;\n};\n\n// Helper to create RgbaColor from colorjs.io Color\nconst colorToRgba = (color: colorjs.PlainColorObject): RgbaColor => {\n  const [r, g, b] = color.coords;\n  return {\n    r: (r ?? 0) * 255,\n    g: (g ?? 0) * 255,\n    b: (b ?? 0) * 255,\n    a: color.alpha ?? 1,\n  };\n};\n\nconst transparentColor: RgbaColor = { r: 0, g: 0, b: 0, a: 0 };\n\n// Helper to parse color string to RgbaColor\nexport const parseColorString = (colorString: string): RgbaColor => {\n  try {\n    return colorToRgba(colorjs.to(colorString, \"srgb\"));\n  } catch {\n    return transparentColor;\n  }\n};\n\n// Helper to convert RgbaColor to RGB string\nconst rgbaToRgbString = (color: RgbaColor): string => {\n  const { r, g, b, a } = color;\n  if (a < 1) {\n    return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${a})`;\n  }\n  return `rgb(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)})`;\n};\n\n// Helper to convert RgbaColor to hex\nconst rgbaToHex = (color: RgbaColor): string => {\n  const { r, g, b, a } = color;\n  const toHex = (n: number) => {\n    const hex = Math.round(n).toString(16);\n    return hex.length === 1 ? \"0\" + hex : hex;\n  };\n  return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(a * 255)}`.toUpperCase();\n};\n\nconst colorfulStyles = css({\n  \".react-colorful__pointer\": {\n    width: theme.spacing[10],\n    height: theme.spacing[10],\n  },\n});\n\nconst whiteColor: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };\nconst borderColorSwatch = colorToRgba(\n  colorjs.to(rawTheme.colors.borderColorSwatch, \"srgb\")\n);\n\nconst distance = (a: RgbaColor, b: RgbaColor) =>\n  Math.sqrt(\n    Math.pow(a.r / 255 - b.r / 255, 2) +\n      Math.pow(a.g / 255 - b.g / 255, 2) +\n      Math.pow(a.b / 255 - b.b / 255, 2) +\n      Math.pow(a.a - b.a, 2)\n  );\n\nconst calcBorderColor = (color: RgbaColor) => {\n  const distanceToStartDrawBorder = 0.15;\n  const alpha = clamp(\n    (distanceToStartDrawBorder - distance(whiteColor, color)) /\n      distanceToStartDrawBorder,\n    0,\n    1\n  );\n  return lerpColor(transparentColor, borderColorSwatch, alpha);\n};\n\nconst lerp = (a: number, b: number, t: number) => a * (1 - t) + b * t;\n\nconst lerpColor = (a: RgbaColor, b: RgbaColor, t: number) => ({\n  r: lerp(a.r, b.r, t),\n  g: lerp(a.g, b.g, t),\n  b: lerp(a.b, b.b, t),\n  a: lerp(a.a, b.a, t),\n});\n\nconst thumbStyle = css({\n  display: \"block\",\n  width: theme.spacing[9],\n  height: theme.spacing[9],\n  backgroundBlendMode: \"difference\",\n  borderRadius: theme.borderRadius[2],\n  borderWidth: 0,\n  borderStyle: \"solid\",\n  \"&:focus-visible\": {\n    outline: `1px solid ${theme.colors.borderFocus}`,\n    outlineOffset: 1,\n  },\n});\n\nexport type ColorThumbProps = Omit<\n  ComponentProps<\"button\" | \"span\">,\n  \"color\"\n> & {\n  interactive?: boolean;\n  color?: string;\n  css?: CSS;\n};\n\nexport const ColorThumb = forwardRef<ElementRef<\"button\">, ColorThumbProps>(\n  ({ interactive, color = \"transparent\", css, ...rest }, ref) => {\n    const rgba = parseColorString(color);\n    const background =\n      rgba.a < 1\n        ? `repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${color}`\n        : color;\n    const borderColor = calcBorderColor(rgba);\n\n    const Component = interactive ? \"button\" : \"span\";\n\n    return (\n      <Component\n        style={{\n          background,\n          borderColor: rgbaToRgbString(borderColor),\n          borderWidth: borderColor.a === 0 ? 0 : 1,\n        }}\n        className={thumbStyle({ css })}\n        tabIndex={-1}\n        {...rest}\n        ref={ref}\n      />\n    );\n  }\n);\n\nColorThumb.displayName = \"ColorThumb\";\n\nconst colorResultToRgbValue = (rgb: RgbaColor): RgbValue => ({\n  type: \"rgb\",\n  r: rgb.r,\n  g: rgb.g,\n  b: rgb.b,\n  alpha: rgb.a ?? 1,\n});\n\nconst normalizeHex = (value: string) => {\n  const trimmed = value.trim();\n  const hex = trimmed.startsWith(\"#\") ? trimmed : `#${trimmed}`;\n  return hex;\n};\n\nconst getEyeDropper = () => {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const Constructor = (window as any).EyeDropper;\n  if (Constructor === undefined) {\n    return;\n  }\n  const eyeDropper = new Constructor();\n  return (callback: (rgb: string) => void) => {\n    eyeDropper.open().then((result: { sRGBHex: string }) => {\n      callback(result.sRGBHex);\n    });\n  };\n};\n\ntype IntermediateColorValue = {\n  type: \"intermediate\";\n  value: string;\n  unit?: Unit;\n};\n\ntype ColorPickerValue = StyleValue | IntermediateColorValue;\n\ntype ColorPickerProps = {\n  value: ColorPickerValue;\n  onChange: (value: StyleValue | undefined) => void;\n  onChangeComplete: (value: StyleValue) => void;\n};\n\ntype ColorPickerPopoverProps = {\n  value: StyleValue;\n  onChange: (value: StyleValue | undefined) => void;\n  onChangeComplete: (value: StyleValue) => void;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  thumb?: React.ReactElement;\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  align?: \"start\" | \"center\" | \"end\";\n  sideOffset?: number;\n};\n\nconst fixColor = (value: ColorPickerValue, color: RgbaColor) => {\n  if (value.type === \"keyword\" && value.value === \"transparent\") {\n    return { ...color, a: 1 };\n  }\n  return color;\n};\n\nconst EyeDropper = ({ onChange }: { onChange: (rgb: string) => void }) => {\n  const open = getEyeDropper();\n  return (\n    <IconButton\n      disabled={open === undefined}\n      onClick={() => {\n        open?.(onChange);\n      }}\n    >\n      <EyedropperIcon />\n    </IconButton>\n  );\n};\n\nexport const ColorPicker = ({\n  value,\n  onChange,\n  onChangeComplete,\n}: ColorPickerProps) => {\n  const [hex, setHex] = useState(() => {\n    const colorString =\n      value.type === \"intermediate\" ? value.value : toValue(value);\n    return rgbaToHex(parseColorString(colorString));\n  });\n  const normalizedHex = normalizeHex(hex);\n  const handleCompleteDebounced = useDebouncedCallback(\n    (newValue: RgbValue) => onChangeComplete(newValue),\n    500\n  );\n\n  return (\n    <>\n      <RgbaColorPicker\n        className={colorfulStyles.toString()}\n        color={parseColorString(normalizedHex)}\n        onChange={(newRgb) => {\n          const fixedRgb = fixColor(value, newRgb);\n          setHex(rgbaToHex(fixedRgb));\n          const newValue = colorResultToRgbValue(fixedRgb);\n          onChange(newValue);\n          handleCompleteDebounced(newValue);\n        }}\n      />\n      <Grid css={{ gridTemplateColumns: \"auto 1fr\" }} gap=\"1\">\n        <EyeDropper\n          onChange={(newHex) => {\n            setHex(newHex);\n            const newValue = colorResultToRgbValue(parseColorString(newHex));\n            onChangeComplete(newValue);\n          }}\n        />\n        <InputField\n          value={hex}\n          onChange={(event) => {\n            setHex(event.target.value);\n            try {\n              const color = colorjs.to(\n                normalizeHex(event.target.value),\n                \"srgb\"\n              );\n              const rgba = colorToRgba(color);\n              const newValue = colorResultToRgbValue(rgba);\n              onChange(newValue);\n              handleCompleteDebounced(newValue);\n            } catch {\n              // Invalid color, don't update\n            }\n          }}\n        />\n      </Grid>\n    </>\n  );\n};\n\nexport const ColorPickerPopover = ({\n  value,\n  onChange,\n  onChangeComplete,\n  open,\n  onOpenChange,\n  thumb,\n  side = \"bottom\",\n  align = \"center\",\n  sideOffset,\n}: ColorPickerPopoverProps) => {\n  const [displayColorPicker, setDisplayColorPicker] = useState(false);\n  const { enableCanvasPointerEvents, disableCanvasPointerEvents } =\n    useDisableCanvasPointerEvents();\n\n  const isControlled = open !== undefined;\n  const isOpen = isControlled ? open : displayColorPicker;\n\n  useEffect(() => {\n    if (isOpen) {\n      disableCanvasPointerEvents();\n      document.body.style.userSelect = \"none\";\n    } else {\n      document.body.style.removeProperty(\"user-select\");\n      enableCanvasPointerEvents();\n    }\n\n    return () => {\n      document.body.style.removeProperty(\"user-select\");\n      enableCanvasPointerEvents();\n    };\n  }, [isOpen, disableCanvasPointerEvents, enableCanvasPointerEvents]);\n\n  const handleOpenChange = (nextOpen: boolean) => {\n    if (!isControlled) {\n      setDisplayColorPicker(nextOpen);\n    }\n    onOpenChange?.(nextOpen);\n  };\n\n  return (\n    <Popover modal open={isOpen} onOpenChange={handleOpenChange}>\n      <PopoverTrigger\n        asChild\n        aria-label=\"Open color picker\"\n        onClick={() => handleOpenChange(!isOpen)}\n      >\n        {thumb ?? <ColorThumb color={toValue(value)} interactive={true} />}\n      </PopoverTrigger>\n      <PopoverContent\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        css={{\n          display: \"grid\",\n          padding: theme.spacing[5],\n          gap: theme.spacing[5],\n        }}\n      >\n        <ColorPicker\n          value={value}\n          onChange={onChange}\n          onChangeComplete={onChangeComplete}\n        />\n      </PopoverContent>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/combobox.stories.tsx",
    "content": "import { SearchIcon } from \"@webstudio-is/icons\";\nimport { useCallback, useState } from \"react\";\nimport type {\n  UseComboboxState,\n  UseComboboxStateChangeOptions,\n} from \"downshift\";\nimport {\n  ComboboxListboxItem,\n  useCombobox,\n  ComboboxContent,\n  ComboboxRoot,\n  ComboboxListbox,\n  ComboboxAnchor,\n  ComboboxItemDescription,\n  Combobox,\n} from \"./combobox\";\nimport { Flex } from \"./flex\";\nimport { InputField } from \"./input-field\";\nimport { StorySection } from \"./storybook\";\n\nconst BasicDemo = () => {\n  const [value, setValue] = useState(\"\");\n  return (\n    <Combobox<string>\n      value={value}\n      itemToString={(item) => item ?? \"\"}\n      getItems={() => [\"Apple\", \"Banana\", \"Orange\"]}\n      onItemSelect={setValue}\n      onChange={(value) => {\n        setValue(value ?? \"\");\n      }}\n    />\n  );\n};\n\nconst ComplexDemo = () => {\n  const [value, setValue] = useState(\"\");\n\n  const stateReducer = useCallback(\n    (\n      _state: UseComboboxState<string>,\n      actionAndChanges: UseComboboxStateChangeOptions<string>\n    ) => {\n      const { type, changes } = actionAndChanges;\n\n      switch (type) {\n        default:\n          return changes; // otherwise business as usual.\n      }\n    },\n    []\n  );\n\n  const {\n    items,\n    getComboboxProps,\n    getMenuProps,\n    getItemProps,\n    getInputProps,\n    isOpen,\n  } = useCombobox<string>({\n    getItems: () => [\"Apple\", \"Banana\", \"Orange\"],\n    value,\n    selectedItem: value,\n    itemToString: (item) => item ?? \"\",\n    stateReducer,\n    onItemSelect: setValue,\n    onChange: (value) => {\n      setValue(value ?? \"\");\n    },\n  });\n\n  return (\n    <ComboboxRoot open={isOpen}>\n      <Flex {...getComboboxProps()} direction=\"column\" gap=\"3\">\n        <ComboboxAnchor asChild>\n          <InputField\n            prefix={\n              <Flex align=\"center\">\n                <SearchIcon />\n              </Flex>\n            }\n            {...getInputProps({ value })}\n          />\n        </ComboboxAnchor>\n        <ComboboxContent>\n          <ComboboxListbox {...getMenuProps()}>\n            {items.map((item, index) => {\n              return (\n                <ComboboxListboxItem\n                  {...getItemProps({ item, index })}\n                  key={index}\n                >\n                  {item}\n                </ComboboxListboxItem>\n              );\n            })}\n            <ComboboxItemDescription\n              descriptions={[\"Hello\", \"World\", \"Description\"]}\n            >\n              Description\n            </ComboboxItemDescription>\n          </ComboboxListbox>\n        </ComboboxContent>\n      </Flex>\n    </ComboboxRoot>\n  );\n};\n\nconst WithPlaceholderDemo = () => {\n  const [value, setValue] = useState(\"\");\n  return (\n    <Combobox<string>\n      value={value}\n      itemToString={(item) => item ?? \"\"}\n      getItems={() => [\"Apple\", \"Banana\", \"Orange\"]}\n      onItemSelect={setValue}\n      onChange={(value) => setValue(value ?? \"\")}\n      placeholder=\"Search fruits…\"\n    />\n  );\n};\n\nconst WithAutoFocusDemo = () => {\n  const [value, setValue] = useState(\"\");\n  return (\n    <Combobox<string>\n      value={value}\n      itemToString={(item) => item ?? \"\"}\n      getItems={() => [\"Apple\", \"Banana\", \"Orange\"]}\n      onItemSelect={setValue}\n      onChange={(value) => setValue(value ?? \"\")}\n      autoFocus\n      placeholder=\"Focused on mount\"\n    />\n  );\n};\n\nconst WithErrorColorDemo = () => {\n  const [value, setValue] = useState(\"\");\n  return (\n    <Combobox<string>\n      value={value}\n      itemToString={(item) => item ?? \"\"}\n      getItems={() => [\"Apple\", \"Banana\", \"Orange\"]}\n      onItemSelect={setValue}\n      onChange={(value) => setValue(value ?? \"\")}\n      color=\"error\"\n      placeholder=\"Error state\"\n    />\n  );\n};\n\nconst fruitDescriptions: Record<string, string> = {\n  Apple: \"A red or green fruit\",\n  Banana: \"A yellow tropical fruit\",\n  Orange: \"A citrus fruit\",\n} as const;\n\nconst WithDescriptionDemo = () => {\n  const [value, setValue] = useState(\"\");\n  return (\n    <Combobox<string>\n      value={value}\n      itemToString={(item) => item ?? \"\"}\n      getItems={() => [\"Apple\", \"Banana\", \"Orange\"]}\n      getDescription={(item) => (item ? fruitDescriptions[item] : undefined)}\n      onItemSelect={setValue}\n      onChange={(value) => setValue(value ?? \"\")}\n      placeholder=\"Pick a fruit…\"\n    />\n  );\n};\n\nexport const ComboboxStory = () => (\n  <>\n    <StorySection title=\"Basic\">\n      <BasicDemo />\n    </StorySection>\n    <StorySection title=\"Complex\">\n      <ComplexDemo />\n    </StorySection>\n    <StorySection title=\"With placeholder\">\n      <WithPlaceholderDemo />\n    </StorySection>\n    <StorySection title=\"With auto focus\">\n      <WithAutoFocusDemo />\n    </StorySection>\n    <StorySection title=\"With error color\">\n      <WithErrorColorDemo />\n    </StorySection>\n    <StorySection title=\"With description\">\n      <WithDescriptionDemo />\n    </StorySection>\n  </>\n);\n\nexport default {\n  title: \"Combobox\",\n  component: ComboboxStory,\n};\n"
  },
  {
    "path": "packages/design-system/src/components/combobox.tsx",
    "content": "import {\n  type ComponentProps,\n  type ChangeEvent,\n  type ReactNode,\n  type Ref,\n  type ForwardRefRenderFunction,\n  useState,\n  forwardRef,\n  useCallback,\n  useEffect,\n  useRef,\n} from \"react\";\nimport {\n  Portal,\n  Popover,\n  PopoverContent,\n  PopoverAnchor,\n} from \"@radix-ui/react-popover\";\nimport {\n  type UseComboboxState,\n  type UseComboboxStateChangeOptions,\n  type UseComboboxProps as UseDownshiftComboboxProps,\n  type UseComboboxGetInputPropsOptions,\n  useCombobox as useDownshiftCombobox,\n  type UseComboboxGetItemPropsOptions,\n} from \"downshift\";\nimport { matchSorter } from \"match-sorter\";\nimport { styled, theme } from \"../stitches.config\";\nimport { focusFirstCollectionItem } from \"../utilities\";\nimport {\n  menuItemCss,\n  menuCss,\n  menuItemIndicatorCss,\n  labelCss,\n  separatorCss,\n  MenuCheckedIcon,\n} from \"./menu\";\nimport { ScrollArea } from \"./scroll-area\";\nimport { Box } from \"./box\";\nimport { Flex } from \"./flex\";\nimport { NestedInputButton } from \"./nested-input-button\";\nimport { InputField } from \"./input-field\";\nimport { Grid } from \"./grid\";\n\nexport const ComboboxListbox = styled(\n  \"ul\",\n  {\n    display: \"flex\",\n    flexDirection: \"column\",\n    margin: \"unset\", // reset <ul>\n    listStyle: \"none\",\n    variants: {\n      state: { closed: { display: \"none\" } },\n      empty: { true: { display: \"none\" } },\n    },\n  },\n  menuCss\n);\n\nexport const ComboboxScrollArea = forwardRef(\n  (\n    { children, ...props }: ComponentProps<typeof ScrollArea>,\n    forwardRef: Ref<HTMLDivElement>\n  ) => {\n    return (\n      <ScrollArea css={{ order: 1 }} {...props}>\n        <Grid ref={forwardRef} css={{ maxHeight: theme.spacing[34] }}>\n          {children}\n        </Grid>\n      </ScrollArea>\n    );\n  }\n);\nComboboxScrollArea.displayName = \"ComboboxScrollArea\";\n\nconst ListboxItem = styled(\"li\", menuItemCss);\n\nconst Indicator = styled(\"span\", menuItemIndicatorCss);\n\nexport const ComboboxLabel = styled(\"li\", labelCss);\n\nexport const ComboboxSeparator = styled(\"li\", separatorCss);\n\nconst ListboxItemBase: ForwardRefRenderFunction<\n  HTMLLIElement,\n  ComponentProps<typeof ListboxItem> & {\n    disabled?: boolean;\n    selected?: boolean;\n    selectable?: boolean;\n    highlighted?: boolean;\n    icon?: ReactNode;\n  }\n> = (props, ref) => {\n  const {\n    disabled,\n    selected,\n    selectable = true,\n    highlighted,\n    children,\n    icon = <MenuCheckedIcon />,\n    ...rest\n  } = props;\n\n  return (\n    <ListboxItem\n      {...(disabled ? { \"aria-disabled\": true, disabled: true } : {})}\n      {...(selected ? { \"aria-current\": true } : {})}\n      {...(disabled ? {} : rest)}\n      withIndicator={selectable}\n      text={rest.text ?? \"sentence\"}\n      ref={ref}\n    >\n      {selectable && selected && <Indicator>{icon}</Indicator>}\n      {children}\n    </ListboxItem>\n  );\n};\n\nexport const ComboboxListboxItem = forwardRef(ListboxItemBase);\n\nexport const ComboboxItemDescription = ({\n  children,\n  descriptions,\n}: {\n  children: ReactNode;\n  descriptions: ReactNode[];\n}) => {\n  return (\n    <>\n      <ComboboxSeparator\n        style={{\n          display: `var(--ws-combobox-description-display-bottom, none)`,\n          order: \"var(--ws-combobox-description-order)\",\n        }}\n      />\n      <ListboxItem\n        css={{\n          display: \"grid\",\n        }}\n        hint\n        style={{\n          order: \"var(--ws-combobox-description-order)\",\n        }}\n      >\n        {descriptions.map((description, index) => (\n          <Box\n            css={{\n              gridColumn: \"1\",\n              gridRow: \"1\",\n              visibility: \"hidden\",\n            }}\n            key={index}\n          >\n            {description}\n          </Box>\n        ))}\n        <Box\n          css={{\n            gridColumn: \"1\",\n            gridRow: \"1\",\n          }}\n        >\n          {children}\n        </Box>\n      </ListboxItem>\n      <ComboboxSeparator\n        style={{\n          display: `var(--ws-combobox-description-display-top, none)`,\n          order: \"var(--ws-combobox-description-order)\",\n        }}\n      />\n    </>\n  );\n};\n\nexport const ComboboxRoot = (props: ComponentProps<typeof Popover>) => {\n  return <Popover {...props} modal />;\n};\n\nconst StyledPopoverContent = styled(PopoverContent, {\n  minWidth: \"var(--radix-popper-anchor-width)\",\n  \"&[data-side=top]\": {\n    \"--ws-combobox-description-display-top\": \"block\",\n    \"--ws-combobox-description-order\": 0,\n  },\n  \"&[data-side=bottom]\": {\n    \"--ws-combobox-description-display-bottom\": \"block\",\n    \"--ws-combobox-description-order\": 2,\n  },\n});\n\nexport const ComboboxContent = forwardRef(\n  (\n    { style, ...props }: ComponentProps<typeof PopoverContent>,\n    forwardRef: Ref<HTMLDivElement>\n  ) => {\n    return (\n      <Portal>\n        <StyledPopoverContent\n          onOpenAutoFocus={(event) => {\n            event.preventDefault();\n          }}\n          {...props}\n          ref={forwardRef}\n        />\n      </Portal>\n    );\n  }\n);\nComboboxContent.displayName = \"ComboboxContent\";\n\nexport const ComboboxAnchor = PopoverAnchor;\n\ntype Match<Item> = (\n  search: string,\n  items: Item[],\n  itemToString: (item: Item | null) => string\n) => Item[];\n\nconst defaultMatch = <Item,>(\n  search: string,\n  items: Array<Item>,\n  itemToString: (item: Item | null) => string\n) =>\n  matchSorter(items, search, {\n    keys: [itemToString],\n    baseSort: (left, right) =>\n      left.rankedValue.localeCompare(right.rankedValue, undefined, {\n        numeric: true,\n      }),\n  });\n\ntype ItemToString<Item> = (item: Item | null) => string;\n\ntype UseComboboxProps<Item> = Omit<UseDownshiftComboboxProps<Item>, \"items\"> & {\n  getItems: () => Array<Item>;\n  itemToString: ItemToString<Item>;\n  getDescription?: (item: Item | null) => ReactNode;\n  getItemProps?: (\n    options: UseComboboxGetItemPropsOptions<Item>\n  ) => ComponentProps<typeof ComboboxListboxItem>;\n  value: Item | null; // This is to prevent: \"downshift: A component has changed the uncontrolled prop \"selectedItem\" to be controlled.\"\n  selectedItem?: Item;\n  onChange?: (value: string | undefined) => void;\n  onItemSelect?: (value: Item) => void;\n  onItemHighlight?: (value: Item | null) => void;\n  stateReducer?: (\n    state: UseComboboxState<Item>,\n    changes: UseComboboxStateChangeOptions<Item>\n  ) => Partial<UseComboboxStateChangeOptions<Item>>;\n  match?: Match<Item>;\n  defaultHighlightedIndex?: number;\n};\n\nexport const comboboxStateChangeTypes = useDownshiftCombobox.stateChangeTypes;\n\nconst isNumericString = (input: string) =>\n  String(input).trim().length !== 0 && Number.isNaN(Number(input)) === false;\n\nexport const useCombobox = <Item,>({\n  getItems,\n  value,\n  selectedItem,\n  getItemProps,\n  itemToString,\n  onChange,\n  onItemSelect,\n  onItemHighlight,\n  stateReducer = (_state, { changes }) => changes,\n  match = defaultMatch,\n  defaultHighlightedIndex = -1,\n  ...rest\n}: UseComboboxProps<Item>) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const selectedItemRef = useRef<undefined | Item>(undefined);\n  const itemsCache = useRef<Item[]>([]);\n  const [matchedItems, setMatchedItems] = useState<Item[]>([]);\n\n  const downshiftProps = useDownshiftCombobox({\n    ...rest,\n    items: matchedItems,\n    defaultHighlightedIndex,\n    selectedItem: selectedItem ?? null, // Prevent downshift warning about switching controlled mode\n    isOpen,\n    onIsOpenChange(state) {\n      const { type, isOpen, inputValue } = state;\n\n      // Tab from the input with menu opened should reset the input value if nothing is selected\n      if (type === comboboxStateChangeTypes.InputBlur) {\n        onChange?.(undefined);\n        // If the input is blurred, we want to close the menu and reset the value to the selected item.\n        setIsOpen(false);\n        setMatchedItems([]);\n        return;\n      }\n\n      // Don't open the combobox if the input is a number and the user is using the arrow keys.\n      // This prevents the combobox from opening when the user is trying to increment or decrement a number.\n      if (\n        (type === comboboxStateChangeTypes.InputKeyDownArrowDown ||\n          type === comboboxStateChangeTypes.InputKeyDownArrowUp) &&\n        inputValue !== undefined &&\n        isNumericString(inputValue)\n      ) {\n        return;\n      }\n\n      // If the menu is opened using the up or down arrows, we want to display all items without applying any filters.\n      if (\n        isOpen &&\n        (type === comboboxStateChangeTypes.InputKeyDownArrowDown ||\n          type === comboboxStateChangeTypes.InputKeyDownArrowUp)\n      ) {\n        const matchedItems = getItems();\n        setMatchedItems(matchedItems);\n        setIsOpen(matchedItems.length > 0);\n\n        return;\n      }\n\n      if (isOpen) {\n        itemsCache.current = getItems();\n        // Don't set isOpen to true if there are no items to show\n        // because otherwise first ESC press will try to close it and only next ESC\n        // will reset the value. When list is empty, first ESC should reset the value.\n        setMatchedItems(itemsCache.current);\n        setIsOpen(itemsCache.current.length > 0);\n      } else {\n        setMatchedItems([]);\n        setIsOpen(false);\n      }\n    },\n\n    stateReducer: (state, actionAndChanges) => {\n      // Apply user's state reducer first\n      const userChanges = stateReducer(state, actionAndChanges);\n      // When menu opens, set highlighted index to defaultHighlightedIndex\n      if (\n        userChanges.isOpen === true &&\n        state.isOpen === false &&\n        defaultHighlightedIndex >= 0\n      ) {\n        return { ...userChanges, highlightedIndex: defaultHighlightedIndex };\n      }\n      return userChanges;\n    },\n    itemToString,\n    inputValue: value ? itemToString(value) : \"\",\n    onInputValueChange(state) {\n      const { inputValue, type } = state;\n      if (type === comboboxStateChangeTypes.InputChange) {\n        const matchedItems = match(\n          inputValue ?? \"\",\n          itemsCache.current,\n          itemToString\n        );\n        setIsOpen(matchedItems.length > 0);\n        setMatchedItems(matchedItems);\n      }\n    },\n    onSelectedItemChange({ selectedItem, type }) {\n      // Don't call onItemSelect when ESC is pressed\n      if (type === comboboxStateChangeTypes.InputKeyDownEscape) {\n        // Reset intermediate value when ESC is pressed\n        onChange?.(undefined);\n        return;\n      }\n\n      if (selectedItem != null) {\n        // We are using a ref because we need to call onItemSelect after the component is closed\n        // otherwise popover will take focus from on click and you can't focus something else after onItemSelect imediately\n        selectedItemRef.current = selectedItem;\n      }\n    },\n    onHighlightedIndexChange({ highlightedIndex }) {\n      if (highlightedIndex !== undefined) {\n        onItemHighlight?.(matchedItems[highlightedIndex] ?? null);\n      }\n    },\n  });\n\n  useEffect(() => {\n    // Selecting the item when the popover is closed.\n    if (isOpen === false && selectedItemRef.current) {\n      onItemSelect?.(selectedItemRef.current);\n      selectedItemRef.current = undefined;\n    }\n  }, [isOpen, onItemSelect]);\n\n  const downshiftGetInputProps = downshiftProps.getInputProps;\n  const enhancedGetInputProps = useCallback(\n    (options?: UseComboboxGetInputPropsOptions) => {\n      const inputProps = downshiftGetInputProps(options);\n      return {\n        ...inputProps,\n        onChange: (event: ChangeEvent<HTMLInputElement>) => {\n          inputProps.onChange(event);\n          // If we want controllable input we need to call onChange here\n          // see https://github.com/downshift-js/downshift/issues/1108\n          onChange?.(event.target.value);\n        },\n      };\n    },\n    [downshiftGetInputProps, onChange]\n  );\n\n  const downshiftHighlightedIndex = downshiftProps.highlightedIndex;\n  const downshiftGetItemProps = downshiftProps.getItemProps;\n  const enhancedGetItemProps = useCallback(\n    (options: UseComboboxGetItemPropsOptions<Item>) => {\n      const itemOptions = {\n        // We need to either deep compare objects here or use itemToString to get primitive types\n        selected:\n          selectedItem !== undefined &&\n          itemToString(selectedItem) === itemToString(options.item),\n        key: options.id,\n        ...options,\n      };\n\n      return {\n        highlighted: downshiftHighlightedIndex === options.index,\n        ...downshiftGetItemProps(itemOptions),\n        ...getItemProps?.(itemOptions),\n      };\n    },\n    [\n      downshiftHighlightedIndex,\n      downshiftGetItemProps,\n      itemToString,\n      selectedItem,\n      getItemProps,\n    ]\n  );\n\n  const downshiftGetMenuProps = downshiftProps.getMenuProps;\n  const enhancedGetMenuProps = useCallback(\n    (options?: Parameters<typeof downshiftGetMenuProps>[0]) => {\n      return {\n        ...downshiftGetMenuProps(options, { suppressRefError: true }),\n        state: isOpen ? \"open\" : \"closed\",\n        empty: matchedItems.length === 0,\n      };\n    },\n    [downshiftGetMenuProps, isOpen, matchedItems.length]\n  );\n\n  return {\n    ...downshiftProps,\n    items: matchedItems,\n    getItemProps: enhancedGetItemProps,\n    getMenuProps: enhancedGetMenuProps,\n    getInputProps: enhancedGetInputProps,\n  };\n};\n\ntype ComboboxProps<Item> = UseComboboxProps<Item> &\n  Pick<\n    ComponentProps<typeof InputField>,\n    | \"inputRef\"\n    | \"autoFocus\"\n    | \"placeholder\"\n    | \"name\"\n    | \"color\"\n    | \"suffix\"\n    | \"onBlur\"\n    | \"onInvalid\"\n  >;\n\nexport const Combobox = <Item,>({\n  getDescription,\n  // input props\n  inputRef,\n  autoFocus,\n  placeholder,\n  name,\n  color,\n  suffix,\n  onBlur,\n  onInvalid,\n  ...props\n}: ComboboxProps<Item>) => {\n  const combobox = useCombobox<Item>(props);\n\n  const descriptionItem =\n    combobox.highlightedIndex === -1\n      ? combobox.selectedItem\n      : combobox.items[combobox.highlightedIndex];\n\n  const description = getDescription?.(descriptionItem);\n  const descriptions = combobox.items.map((item) => getDescription?.(item));\n\n  return (\n    <ComboboxRoot open={combobox.isOpen}>\n      <Box {...combobox.getComboboxProps()}>\n        <ComboboxAnchor>\n          <InputField\n            {...combobox.getInputProps()}\n            inputRef={inputRef}\n            autoFocus={autoFocus}\n            placeholder={placeholder}\n            name={name}\n            color={color}\n            suffix={\n              suffix ??\n              (props.getItems().length > 0 && (\n                <Flex>\n                  <NestedInputButton {...combobox.getToggleButtonProps()} />\n                </Flex>\n              ))\n            }\n            onBlur={onBlur}\n            onInvalid={onInvalid}\n          />\n        </ComboboxAnchor>\n        <ComboboxContent\n          onOpenAutoFocus={(event) => {\n            event.preventDefault();\n            if ((props.defaultHighlightedIndex ?? 0) < 0) {\n              return;\n            }\n            if (event.currentTarget instanceof HTMLElement) {\n              focusFirstCollectionItem(event.currentTarget);\n            }\n          }}\n        >\n          <ComboboxListbox {...combobox.getMenuProps()}>\n            <ComboboxScrollArea>\n              {combobox.isOpen &&\n                combobox.items.map((item, index) => {\n                  return (\n                    <ComboboxListboxItem\n                      selectable={false}\n                      {...combobox.getItemProps({ item, index })}\n                      key={index}\n                    >\n                      {props.itemToString(item)}\n                    </ComboboxListboxItem>\n                  );\n                })}\n            </ComboboxScrollArea>\n            {descriptions.some(Boolean) && (\n              <ComboboxItemDescription descriptions={descriptions}>\n                {description ?? \" \"}\n              </ComboboxItemDescription>\n            )}\n          </ComboboxListbox>\n        </ComboboxContent>\n      </Box>\n    </ComboboxRoot>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/command.stories.tsx",
    "content": "import { useState } from \"react\";\nimport type { Meta, StoryFn } from \"@storybook/react\";\nimport { StorySection } from \"./storybook\";\nimport {\n  Command as CommandComponent,\n  CommandDialog,\n  CommandBackButton,\n  CommandFooter,\n  CommandGroup,\n  CommandGroupHeading,\n  CommandIcon,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"./command\";\nimport { Text } from \"./text\";\nimport { InfoCircleIcon } from \"@webstudio-is/icons\";\nimport { Kbd } from \"./kbd\";\nimport { Flex } from \"./flex\";\nimport { Separator } from \"./separator\";\n\nconst meta: Meta = {\n  title: \"Command\",\n};\nexport default meta;\n\nconst CommandContent = () => {\n  return (\n    <>\n      <CommandInput />\n      <CommandList>\n        <CommandGroup\n          heading={<CommandGroupHeading>Suggestions</CommandGroupHeading>}\n          name=\"suggestions\"\n          actions={[\n            { name: \"select\", label: \"Select\" },\n            { name: \"edit\", label: \"Edit\" },\n            { name: \"delete\", label: \"Delete\" },\n          ]}\n        >\n          <CommandItem>\n            <Flex gap={2}>\n              <CommandIcon>\n                <InfoCircleIcon />\n              </CommandIcon>\n              <Text variant=\"labels\">Calendar</Text>\n            </Flex>\n          </CommandItem>\n          <CommandItem>\n            <Flex gap={2}>\n              <CommandIcon>\n                <InfoCircleIcon />\n              </CommandIcon>\n              <Text variant=\"labels\">Search Emoji</Text>\n            </Flex>\n          </CommandItem>\n          <CommandItem>\n            <Flex gap={2}>\n              <CommandIcon>\n                <InfoCircleIcon />\n              </CommandIcon>\n              <Text variant=\"labels\">Calculator</Text>\n            </Flex>\n          </CommandItem>\n        </CommandGroup>\n        <CommandGroup\n          heading={<CommandGroupHeading>Settings</CommandGroupHeading>}\n          name=\"settings\"\n          actions={[{ name: \"open\", label: \"Open\" }]}\n        >\n          <CommandItem>\n            <Flex gap={2}>\n              <CommandIcon>\n                <InfoCircleIcon />\n              </CommandIcon>\n              <Text variant=\"labels\">Profile</Text>\n            </Flex>\n            <Kbd value={[\"meta\", \"p\"]} />\n          </CommandItem>\n          <CommandItem>\n            <Flex gap={2}>\n              <CommandIcon>\n                <InfoCircleIcon />\n              </CommandIcon>\n              <Text variant=\"labels\">Billing</Text>\n            </Flex>\n            <Kbd value={[\"meta\", \"b\"]} />\n          </CommandItem>\n          <CommandItem>\n            <Flex gap={2}>\n              <CommandIcon>\n                <InfoCircleIcon />\n              </CommandIcon>\n              <Text variant=\"labels\">Settings</Text>\n            </Flex>\n            <Kbd value={[\"meta\", \"s\"]} />\n          </CommandItem>\n        </CommandGroup>\n      </CommandList>\n      <Separator />\n      <CommandFooter />\n    </>\n  );\n};\n\nexport const Command: StoryFn = () => {\n  return (\n    <StorySection title=\"Command\">\n      <CommandComponent>\n        <CommandContent />\n      </CommandComponent>\n    </StorySection>\n  );\n};\n\nexport const InDialog: StoryFn = () => {\n  const [open, setOpen] = useState(true);\n  return (\n    <StorySection title=\"In dialog\">\n      <CommandDialog open={open} onOpenChange={setOpen}>\n        <CommandComponent>\n          <CommandContent />\n        </CommandComponent>\n      </CommandDialog>\n    </StorySection>\n  );\n};\n\nexport const WithBackNavigation: StoryFn = () => {\n  const [value, setValue] = useState(\"\");\n  return (\n    <StorySection title=\"With back navigation\">\n      <CommandComponent>\n        <CommandInput\n          value={value}\n          onValueChange={setValue}\n          prefix={<CommandBackButton onClick={() => setValue(\"\")} />}\n          onBack={() => setValue(\"\")}\n          placeholder=\"Search with back button...\"\n        />\n        <CommandList>\n          <CommandGroup\n            heading={<CommandGroupHeading>Results</CommandGroupHeading>}\n            name=\"results\"\n            actions={[{ name: \"select\", label: \"Select\" }]}\n          >\n            <CommandItem>\n              <Flex gap={2}>\n                <CommandIcon>\n                  <InfoCircleIcon />\n                </CommandIcon>\n                <Text variant=\"labels\">Result one</Text>\n              </Flex>\n            </CommandItem>\n            <CommandItem>\n              <Flex gap={2}>\n                <CommandIcon>\n                  <InfoCircleIcon />\n                </CommandIcon>\n                <Text variant=\"labels\">Result two</Text>\n              </Flex>\n            </CommandItem>\n          </CommandGroup>\n        </CommandList>\n        <Separator />\n        <CommandFooter />\n      </CommandComponent>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/command.tsx",
    "content": "import {\n  createContext,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  useCallback,\n  type ComponentPropsWithoutRef,\n  type Dispatch,\n  type SetStateAction,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport {\n  Command as CommandPrimitive,\n  defaultFilter,\n  useCommandState,\n} from \"cmdk\";\nimport { VisuallyHidden } from \"@radix-ui/react-visually-hidden\";\nimport {\n  DialogTitle,\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogContent,\n} from \"@radix-ui/react-dialog\";\nimport { SearchIcon, ChevronLeftIcon } from \"@webstudio-is/icons\";\nimport { styled, theme } from \"../stitches.config\";\nimport { Text, textVariants } from \"./text\";\nimport { Button } from \"./button\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"./popover\";\nimport { Kbd } from \"./kbd\";\nimport { useDebounceEffect } from \"../utilities\";\nimport { Flex } from \"./flex\";\nimport { InputField } from \"./input-field\";\nimport { SmallIconButton } from \"./small-icon-button\";\n\nconst panelWidth = \"500px\";\nconst itemHeight = \"32px\";\nconst inputBorderBottomSize = \"--command-input-border-bottom-width\";\n\nconst StyledCommand = styled(CommandPrimitive, {\n  boxSizing: \"border-box\",\n  width: panelWidth,\n  boxShadow: theme.shadows.menuDropShadow,\n  backgroundColor: theme.colors.backgroundControls,\n  border: `1px solid ${theme.colors.borderMain}`,\n  borderRadius: theme.borderRadius[7],\n  // clip selected item background\n  overflow: \"clip\",\n  // remove input border bottom when no command matches\n  [inputBorderBottomSize]: \"0px\",\n  \"&:has([cmdk-group]:not([hidden]))\": {\n    [inputBorderBottomSize]: \"1px\",\n  },\n});\n\ntype CommandProps = ComponentPropsWithoutRef<typeof CommandPrimitive>;\n\n// this will match \"Box\" when entered \"box\"\nconst lowerCasedFilter: CommandProps[\"filter\"] = (\n  string,\n  abbreviation,\n  aliases\n) =>\n  defaultFilter!(\n    string.toLocaleLowerCase(),\n    abbreviation.toLocaleLowerCase(),\n    aliases\n  );\n\nexport type CommandAction = {\n  name: string;\n  label: string;\n};\n\ntype CommandState = {\n  highlightedGroup: string;\n  actions: CommandAction[];\n  actionIndex: number;\n  footerContent: ReactNode;\n};\n\nconst CommandContext = createContext<\n  [CommandState, Dispatch<SetStateAction<CommandState>>]\n>([\n  {\n    highlightedGroup: \"\",\n    actions: [],\n    actionIndex: 0,\n    footerContent: undefined,\n  },\n  () => {},\n]);\n\nexport const useSelectedAction = () => {\n  const [state] = useContext(CommandContext);\n  return state.actions[state.actionIndex];\n};\n\nexport const useResetActionIndex = () => {\n  const [, setState] = useContext(CommandContext);\n  return () => {\n    setState((prev) => ({ ...prev, actionIndex: 0 }));\n  };\n};\n\nconst useSetFooterContent = () => {\n  const [, setState] = useContext(CommandContext);\n  return useCallback(\n    (content: ReactNode) => {\n      setState((prev) => ({ ...prev, footerContent: content }));\n    },\n    [setState]\n  );\n};\n\nexport const Command = (props: CommandProps) => {\n  const state = useState<CommandState>({\n    highlightedGroup: \"\",\n    actions: [],\n    actionIndex: 0,\n    footerContent: undefined,\n  });\n  return (\n    <CommandContext.Provider value={state}>\n      <StyledCommand\n        disablePointerSelection\n        loop={true}\n        filter={lowerCasedFilter}\n        {...props}\n      />\n    </CommandContext.Provider>\n  );\n};\n\nconst CommandDialogContent = styled(DialogContent, {\n  position: \"absolute\",\n  top: \"20%\",\n  left: `calc(50% - ${panelWidth} / 2)`,\n  width: panelWidth,\n});\n\nexport const CommandDialog = ({\n  children,\n  ...props\n}: ComponentPropsWithoutRef<typeof Dialog>) => {\n  return (\n    <Dialog {...props}>\n      <DialogPortal>\n        <DialogOverlay />\n        <CommandDialogContent aria-describedby={undefined}>\n          {/* title is required by radix dialog */}\n          <VisuallyHidden asChild>\n            <DialogTitle>Command Panel</DialogTitle>\n          </VisuallyHidden>\n          {children}\n        </CommandDialogContent>\n      </DialogPortal>\n    </Dialog>\n  );\n};\n\nconst CommandInputContainer = styled(\"div\", {\n  borderBottom: `var(${inputBorderBottomSize}) solid ${theme.colors.borderMain}`,\n});\n\nexport const CommandSearchIcon = styled(SearchIcon, {\n  display: \"flex\",\n  width: theme.spacing[11],\n  color: theme.colors.foregroundSubtle,\n});\n\nexport const CommandBackIcon = styled(ChevronLeftIcon, {\n  display: \"flex\",\n  width: theme.spacing[11],\n  color: theme.colors.foregroundSubtle,\n});\n\nconst CommandInputField = styled(InputField, {\n  \"--sizes-controlHeight\": theme.spacing[15],\n  border: \"none\",\n  paddingInline: theme.spacing[4],\n});\n\nexport const CommandInput = (\n  props: ComponentProps<typeof InputField> & {\n    action?: CommandAction;\n    onBack?: () => void;\n    onValueChange?: (value: string) => void;\n  }\n) => {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const contextAction = useSelectedAction();\n  const {\n    action = contextAction,\n    placeholder = \"Type a command or search...\",\n    prefix,\n    onBack,\n    value,\n    onValueChange,\n    ref,\n    ...inputProps\n  } = props;\n  return (\n    <CommandInputContainer>\n      <CommandInputField\n        prefix={prefix ?? <CommandSearchIcon />}\n        suffix={\n          action && (\n            <Text\n              color=\"moreSubtle\"\n              css={{ alignSelf: \"center\", paddingInline: theme.spacing[5] }}\n            >\n              {action.label} <Kbd value={[\"enter\"]} color=\"moreSubtle\" />\n            </Text>\n          )\n        }\n        inputRef={inputRef}\n        autoFocus={true}\n        placeholder={placeholder}\n        value={value}\n        {...inputProps}\n        onKeyDown={(event) => {\n          if (onBack && event.key === \"Backspace\" && value === \"\") {\n            event.preventDefault();\n            onBack();\n          }\n          inputProps.onKeyDown?.(event);\n        }}\n        onChange={(event) => {\n          // reset scroll whenever search is changed\n          requestAnimationFrame(() => {\n            inputRef.current\n              ?.closest(\"[cmdk-root]\")\n              ?.querySelector(\"[data-radix-scroll-area-viewport]\")\n              ?.scrollTo(0, 0);\n          });\n          onValueChange?.(event.target.value);\n        }}\n      />\n    </CommandInputContainer>\n  );\n};\n\nconst ActionsCommand = styled(CommandPrimitive, {});\n\nexport const CommandFooter = ({ children }: { children?: ReactNode }) => {\n  const [isActionOpen, setIsActionOpen] = useState(false);\n  const scheduleEffect = useDebounceEffect();\n\n  const actionsRef = useRef<HTMLDivElement>(null);\n\n  // store group actions whenever highlighted item is changed\n  const [state, setState] = useContext(CommandContext);\n  const highlightedValue = useCommandState((state) => state.value);\n  useEffect(() => {\n    const actionsElement = actionsRef.current?.closest(\"[cmdk-root]\");\n    const selectedGroup = actionsElement?.querySelector(\n      \"[cmdk-group]:has([aria-selected=true])\"\n    );\n    const highlightedGroup = selectedGroup?.getAttribute(\"data-value\") ?? \"\";\n    const actionsJson = selectedGroup?.getAttribute(\"data-actions\") ?? \"[]\";\n    let actions: CommandAction[] = [];\n    try {\n      actions = JSON.parse(actionsJson);\n    } catch {\n      // fallback to empty array if parsing fails\n    }\n    setState((prev) => {\n      // reset index only when group is changed\n      if (prev.highlightedGroup === highlightedGroup) {\n        return prev;\n      }\n      return { ...prev, highlightedGroup, actions, actionIndex: 0 };\n    });\n  }, [highlightedValue, setState]);\n\n  // open action popover with Tab\n  useEffect(() => {\n    const controller = new AbortController();\n    const actionsElement = actionsRef.current?.closest(\"[cmdk-root]\");\n    if (actionsElement instanceof HTMLElement) {\n      actionsElement.addEventListener(\n        \"keydown\",\n        (event) => {\n          if (event.key === \"Tab\") {\n            event.preventDefault();\n            setIsActionOpen(true);\n          }\n        },\n        { signal: controller.signal }\n      );\n    }\n    return () => controller.abort();\n  }, [setState]);\n\n  return (\n    <CommandGroupFooter ref={actionsRef}>\n      {children || state.footerContent}\n      <Popover open={isActionOpen} onOpenChange={setIsActionOpen}>\n        <PopoverTrigger asChild>\n          <Button tabIndex={-1} color=\"ghost\" data-action-trigger>\n            Actions <Kbd value={[\"tab\"]} />\n          </Button>\n        </PopoverTrigger>\n        <PopoverContent\n          side=\"top\"\n          onCloseAutoFocus={(event) => {\n            event.preventDefault();\n            // restore focus to the input instead of button\n            const root = actionsRef.current?.closest(\"[cmdk-root]\");\n            const input = root?.querySelector(\"[cmdk-input]\");\n            if (input instanceof HTMLElement) {\n              input.focus();\n            }\n          }}\n        >\n          <ActionsCommand disablePointerSelection loop>\n            <CommandInputContainer>\n              <CommandInputField placeholder=\"Choose action...\" />\n            </CommandInputContainer>\n            <CommandList data-action-list>\n              {state.actions.map((action, actionIndex) => (\n                <CommandItem\n                  key={action.name}\n                  allowSingleClick\n                  onSelect={() => {\n                    setState((prev) => ({ ...prev, actionIndex }));\n                    setIsActionOpen(false);\n                    const root = actionsRef.current?.closest(\"[cmdk-root]\");\n                    const item = root?.querySelector(\n                      \"[cmdk-group] [aria-selected=true]\"\n                    );\n                    // execute after action state is applied\n                    scheduleEffect(() => {\n                      if (item instanceof HTMLElement) {\n                        item.click();\n                      }\n                    });\n                  }}\n                >\n                  <Text>{action.label}</Text>\n                </CommandItem>\n              ))}\n            </CommandList>\n          </ActionsCommand>\n        </PopoverContent>\n      </Popover>\n    </CommandGroupFooter>\n  );\n};\n\nexport const CommandList = styled(CommandPrimitive.List, {\n  \"& [cmdk-group-heading]\": {\n    position: \"sticky\",\n    top: 0,\n  },\n});\n\ntype CommandGroupProps = Omit<\n  ComponentPropsWithoutRef<typeof CommandPrimitive.Group>,\n  \"value\"\n> & {\n  name: string;\n  actions: CommandAction[];\n  hideAfterItemsAmount?: number;\n};\n\nexport const CommandGroup = ({\n  name,\n  actions,\n  children,\n  hideAfterItemsAmount = 50,\n  ...props\n}: CommandGroupProps) => {\n  const [visibleCount, setVisibleCount] = useState(hideAfterItemsAmount);\n  const groupRef = useRef<HTMLDivElement>(null);\n  const itemCount = Array.isArray(children) ? children.length : 0;\n  const hasMoreItems = itemCount > visibleCount;\n\n  const handleShowMore = () => {\n    setVisibleCount((prev) => prev + 100);\n  };\n\n  return (\n    <div ref={groupRef}>\n      <CommandPrimitive.Group\n        {...props}\n        value={name}\n        data-actions={JSON.stringify(actions)}\n      >\n        {\n          // Show items up to visibleCount\n          Array.isArray(children) && hasMoreItems\n            ? children.slice(0, visibleCount)\n            : children\n        }\n      </CommandPrimitive.Group>\n      {hasMoreItems && (\n        <Flex justify=\"center\" css={{ padding: theme.spacing[2] }}>\n          <Button color=\"ghost\" onClick={handleShowMore} type=\"button\">\n            Show more ({itemCount - visibleCount} hidden)\n          </Button>\n        </Flex>\n      )}\n    </div>\n  );\n};\n\nexport const CommandItem = ({\n  onSelect,\n  allowSingleClick,\n  ...props\n}: ComponentPropsWithoutRef<typeof CommandItemStyled> & {\n  onSelect?: () => void;\n  allowSingleClick?: boolean;\n}) => {\n  const doubleClickedRef = useRef(false);\n  const selectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();\n  const pointerDownRef = useRef(false);\n\n  const handleSelect = () => {\n    // Actions menu mode - execute immediately\n    if (allowSingleClick) {\n      onSelect?.();\n      return;\n    }\n\n    // Default mode\n    if (selectTimeoutRef.current) {\n      clearTimeout(selectTimeoutRef.current);\n    }\n\n    // If double-click already happened, skip (it already executed)\n    if (doubleClickedRef.current) {\n      doubleClickedRef.current = false;\n      return;\n    }\n\n    // If triggered by Enter key (no pointer down), execute immediately\n    if (!pointerDownRef.current) {\n      onSelect?.();\n      return;\n    }\n\n    // For mouse clicks, delay to detect double-click\n    selectTimeoutRef.current = setTimeout(() => {\n      // Reset pointer flag after delay\n      pointerDownRef.current = false;\n    }, 300);\n  };\n\n  return (\n    <CommandItemStyled\n      {...props}\n      onPointerDown={\n        allowSingleClick\n          ? undefined\n          : () => {\n              pointerDownRef.current = true;\n            }\n      }\n      onDoubleClick={\n        allowSingleClick\n          ? undefined\n          : () => {\n              // Mark that double-click happened and execute immediately\n              doubleClickedRef.current = true;\n              if (selectTimeoutRef.current) {\n                clearTimeout(selectTimeoutRef.current);\n              }\n              onSelect?.();\n            }\n      }\n      onSelect={handleSelect}\n    />\n  );\n};\n\nexport const CommandGroupHeading = styled(\"div\", {\n  ...textVariants.labels,\n  color: theme.colors.foregroundMoreSubtle,\n  display: \"flex\",\n  backgroundColor: theme.colors.backgroundControls,\n  gap: theme.spacing[5],\n  alignItems: \"center\",\n  paddingInline: theme.spacing[5],\n  height: itemHeight,\n});\n\nexport const CommandGroupFooter = styled(\"div\", {\n  ...textVariants.labels,\n  color: theme.colors.foregroundMoreSubtle,\n  display: \"flex\",\n  gap: theme.spacing[5],\n  alignItems: \"center\",\n  paddingInline: theme.spacing[5],\n  height: itemHeight,\n  justifyContent: \"end\",\n  borderTop: `1px solid ${theme.colors.borderMain}`,\n});\n\nexport const CommandBackButton = ({ onClick }: { onClick?: () => void }) => {\n  return (\n    <SmallIconButton\n      icon={<CommandBackIcon />}\n      tabIndex={-1}\n      onClick={onClick}\n      aria-label=\"Go back\"\n      css={{ display: \"flex\" }}\n    />\n  );\n};\n\nconst CommandItemStyled = styled(CommandPrimitive.Item, {\n  display: \"grid\",\n  gridTemplateColumns: `1fr max-content`,\n  alignItems: \"center\",\n  minHeight: itemHeight,\n  paddingInline: theme.spacing[9],\n  \"&:hover\": {\n    backgroundColor: theme.colors.backgroundItemMenuItemHover,\n  },\n  \"&[aria-selected=true]\": {\n    backgroundColor: theme.colors.backgroundItemCurrent,\n  },\n});\n\nexport const CommandIcon = styled(\"div\", {\n  width: theme.spacing[9],\n  height: theme.spacing[9],\n  placeSelf: \"center\",\n});\n\nexport { useCommandState, useSetFooterContent };\n"
  },
  {
    "path": "packages/design-system/src/components/component-card.stories.tsx",
    "content": "import { BoxIcon } from \"@webstudio-is/icons\";\nimport { StorySection } from \"./storybook\";\nimport { ComponentCard as ComponentCardComponent } from \"./component-card\";\nimport { TooltipProvider } from \"./tooltip\";\nimport { Grid } from \"./grid\";\nimport { Text } from \"./text\";\n\nexport default {\n  title: \"Component Card\",\n};\n\nexport const ComponentCard = () => (\n  <TooltipProvider>\n    <StorySection title=\"States\">\n      <Grid css={{ gridTemplateColumns: \"repeat(4, 70px)\" }} gap=\"2\">\n        <Text variant=\"labels\">default</Text>\n        <Text variant=\"labels\">hover</Text>\n        <Text variant=\"labels\">selected</Text>\n        <Text variant=\"labels\">disabled</Text>\n        <ComponentCardComponent icon={<BoxIcon />} label=\"Box\" tabIndex={0} />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Box\"\n          state=\"hover\"\n          tabIndex={0}\n        />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Box\"\n          state=\"selected\"\n          tabIndex={0}\n        />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Box\"\n          state=\"disabled\"\n          tabIndex={0}\n        />\n      </Grid>\n    </StorySection>\n    <StorySection title=\"Labels\">\n      <Grid gap=\"2\" css={{ gridTemplateColumns: \"repeat(4, 70px)\" }}>\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Single\"\n          tabIndex={0}\n        />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Too many words too many\"\n          tabIndex={0}\n        />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Manylongwordstotruncate Manylongwordstotruncate\"\n          tabIndex={0}\n        />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Truncatedlongword\"\n          tabIndex={0}\n        />\n      </Grid>\n    </StorySection>\n    <StorySection title=\"With description tooltip\">\n      <Grid gap=\"2\" css={{ gridTemplateColumns: \"repeat(2, 70px)\" }}>\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Box\"\n          description=\"A generic container element\"\n          tabIndex={0}\n        />\n        <ComponentCardComponent\n          icon={<BoxIcon />}\n          label=\"Box\"\n          description=\"Tooltip disabled\"\n          disableTooltip\n          tabIndex={0}\n        />\n      </Grid>\n    </StorySection>\n  </TooltipProvider>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/component-card.tsx",
    "content": "/**\n * Implementation of the \"Component Card\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=2608-8921\n */\n\nimport {\n  forwardRef,\n  type ComponentProps,\n  type JSX,\n  type ReactNode,\n} from \"react\";\nimport { css, theme, type CSS } from \"../stitches.config\";\nimport { textVariants } from \"./text\";\nimport { Tooltip } from \"./tooltip\";\n\nconst cardStyle = css({\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  flexDirection: \"column\",\n  alignItems: \"center\",\n  textAlign: \"center\",\n  padding: theme.spacing[3],\n  aspectRatio: \"1\",\n  border: `1px solid`,\n  borderColor: theme.colors.borderMain,\n  borderRadius: theme.borderRadius[2],\n  outline: \"none\",\n  userSelect: \"none\",\n  color: theme.colors.foregroundIconMain,\n  cursor: \"grab\",\n  background: theme.colors.backgroundPanel,\n  \"&:hover, &[data-state=hover]\": {\n    background: theme.colors.backgroundHover,\n  },\n  \"&[data-state=disabled]\": {\n    background: theme.colors.backgroundPanel,\n    color: theme.colors.foregroundDisabled,\n  },\n  \"&:focus-visible, &[data-state=selected]\": {\n    borderColor: theme.colors.borderFocus,\n  },\n  \"& svg\": {\n    flexGrow: 0,\n    marginTop: theme.spacing[7],\n    width: 22,\n    height: 22,\n  },\n});\n\nconst textContainerStyle = css({\n  display: \"flex\",\n  flexDirection: \"column\",\n  justifyContent: \"center\",\n  flexGrow: 1,\n  width: \"100%\",\n});\n\nconst textStyle = css(textVariants.small, {\n  display: \"-webkit-box\",\n  WebkitLineClamp: 2,\n  WebkitBoxOrient: \"vertical\",\n  overflow: \"hidden\",\n  textOverflow: \"ellipsis\",\n});\n\ntype ComponentCardProps = {\n  label: ReactNode;\n  description?: string;\n  icon: JSX.Element;\n  state?: \"hover\" | \"disabled\" | \"selected\";\n  disableTooltip?: boolean;\n  css?: CSS;\n} & ComponentProps<\"div\">;\n\nexport const ComponentCard = forwardRef<HTMLDivElement, ComponentCardProps>(\n  (\n    {\n      icon,\n      label,\n      className,\n      css,\n      state,\n      description,\n      disableTooltip,\n      ...props\n    },\n    ref\n  ) => {\n    return (\n      <Tooltip\n        disableHoverableContent\n        open={disableTooltip ? false : undefined}\n        content={description ?? label}\n        css={{ maxWidth: theme.spacing[28] }}\n      >\n        <div\n          className={cardStyle({ className, css })}\n          ref={ref}\n          data-state={state}\n          {...props}\n        >\n          {icon}\n\n          <div className={textContainerStyle()}>\n            <div className={textStyle()}>{label}</div>\n          </div>\n        </div>\n      </Tooltip>\n    );\n  }\n);\n\nComponentCard.displayName = \"ComponentCard\";\n"
  },
  {
    "path": "packages/design-system/src/components/context-menu.stories.tsx",
    "content": "import {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuLabel,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuArrow,\n} from \"./dropdown-menu\";\nimport {\n  CopyIcon,\n  TrashIcon,\n  PlusIcon,\n  EllipsesIcon,\n} from \"@webstudio-is/icons\";\nimport { IconButton } from \"./icon-button\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Components/Context Menu\",\n};\n\nexport const ContextMenu = () => (\n  <StorySection title=\"Context menu\">\n    <DropdownMenu defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <IconButton>\n          <EllipsesIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuLabel>Actions</DropdownMenuLabel>\n        <DropdownMenuItem icon={<CopyIcon />}>Copy</DropdownMenuItem>\n        <DropdownMenuItem icon={<PlusIcon />}>Add item</DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuCheckboxItem checked>Bookmarked</DropdownMenuCheckboxItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuRadioGroup value=\"pedro\">\n          <DropdownMenuLabel>People</DropdownMenuLabel>\n          <DropdownMenuRadioItem value=\"pedro\">Pedro</DropdownMenuRadioItem>\n          <DropdownMenuRadioItem value=\"colm\">Colm</DropdownMenuRadioItem>\n        </DropdownMenuRadioGroup>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem icon={<TrashIcon />}>Delete</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  </StorySection>\n);\n\nexport const WithSubMenu = () => (\n  <StorySection title=\"With sub menu\">\n    <DropdownMenu defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <IconButton>\n          <EllipsesIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem>Regular item</DropdownMenuItem>\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger>More options</DropdownMenuSubTrigger>\n          <DropdownMenuSubContent>\n            <DropdownMenuItem>Sub item one</DropdownMenuItem>\n            <DropdownMenuItem>Sub item two</DropdownMenuItem>\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem>Another item</DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  </StorySection>\n);\n\nexport const DestructiveAndDisabled = () => (\n  <StorySection title=\"Destructive and disabled\">\n    <DropdownMenu defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <IconButton>\n          <EllipsesIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem>Normal item</DropdownMenuItem>\n        <DropdownMenuItem disabled>Disabled item</DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem icon={<TrashIcon />} destructive>\n          Delete\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  </StorySection>\n);\n\nexport const WithHintAndArrow = () => (\n  <StorySection title=\"With hint and arrow\">\n    <DropdownMenu defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <IconButton>\n          <EllipsesIcon />\n        </IconButton>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem>Regular item</DropdownMenuItem>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem hint>Press ⌘K to search</DropdownMenuItem>\n        <DropdownMenuArrow />\n      </DropdownMenuContent>\n    </DropdownMenu>\n  </StorySection>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/context-menu.tsx",
    "content": "import {\n  forwardRef,\n  type ComponentProps,\n  type ElementRef,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { ChevronRightIcon } from \"@webstudio-is/icons\";\nimport { styled } from \"../stitches.config\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimport {\n  menuCss,\n  subMenuCss,\n  separatorCss,\n  menuItemCss,\n  labelCss,\n  menuItemIndicatorCss,\n  subContentProps,\n  MenuCheckedIcon,\n} from \"./menu\";\nexport { DropdownMenuArrow } from \"./menu\";\nimport type { Simplify } from \"type-fest\";\n\nexport const ContextMenu = ContextMenuPrimitive.Root;\n\nexport const ContextMenuTrigger = ContextMenuPrimitive.Trigger;\n\nexport const ContextMenuSub = ContextMenuPrimitive.Sub;\n\nconst ContextMenuContentStyled = styled(ContextMenuPrimitive.Content, menuCss);\nexport const ContextMenuContent = forwardRef<\n  ElementRef<typeof ContextMenuContentStyled>,\n  ComponentProps<typeof ContextMenuContentStyled>\n>((props, ref) => {\n  return (\n    <ContextMenuPrimitive.Portal>\n      <ContextMenuContentStyled {...props} ref={ref} />\n    </ContextMenuPrimitive.Portal>\n  );\n});\n\nconst SubContentStyled = styled(ContextMenuPrimitive.SubContent, subMenuCss);\nexport const ContextMenuSubContent = forwardRef<\n  ElementRef<typeof SubContentStyled>,\n  ComponentProps<typeof SubContentStyled>\n>((props, forwardedRef) => (\n  <SubContentStyled {...subContentProps} {...props} ref={forwardedRef} />\n));\nContextMenuSubContent.displayName = \"ContextMenuSubContent\";\n\nexport const ContextMenuSeparator = styled(\n  ContextMenuPrimitive.Separator,\n  separatorCss\n);\n\nexport const ContextMenuLabel = styled(ContextMenuPrimitive.Label, labelCss);\n\nconst StyledMenuItem = styled(ContextMenuPrimitive.Item, menuItemCss, {\n  defaultVariants: { withIndicator: true },\n});\n\nexport const ContextMenuItem = forwardRef<\n  ElementRef<typeof StyledMenuItem>,\n  Simplify<ComponentProps<typeof StyledMenuItem> & { icon?: ReactNode }>\n>(({ icon, children, withIndicator, ...props }, forwardedRef) =>\n  icon ? (\n    <StyledMenuItem\n      withIndicator={withIndicator || Boolean(icon)}\n      {...props}\n      ref={forwardedRef}\n    >\n      <div className={menuItemIndicatorCss()}>{icon}</div>\n      {children}\n    </StyledMenuItem>\n  ) : (\n    <StyledMenuItem\n      withIndicator={withIndicator || Boolean(icon)}\n      {...props}\n      ref={forwardedRef}\n    >\n      {children}\n    </StyledMenuItem>\n  )\n);\nContextMenuItem.displayName = \"ContextMenuItem\";\n\nexport const ContextMenuItemRightSlot = styled(\"span\", {\n  marginLeft: \"auto\",\n  display: \"flex\",\n});\n\nconst SubTriggerStyled = styled(ContextMenuPrimitive.SubTrigger, menuItemCss, {\n  defaultVariants: { withIndicator: true },\n});\nexport const ContextMenuSubTrigger = forwardRef<\n  ElementRef<typeof SubTriggerStyled>,\n  ComponentProps<typeof SubTriggerStyled> & { icon?: ReactNode }\n>(({ children, withIndicator, icon, ...props }, forwardedRef) => (\n  <SubTriggerStyled\n    withIndicator={withIndicator || Boolean(icon)}\n    {...props}\n    ref={forwardedRef}\n  >\n    {icon && <div className={menuItemIndicatorCss()}>{icon}</div>}\n    {children}\n    <ContextMenuItemRightSlot>\n      <ChevronRightIcon />\n    </ContextMenuItemRightSlot>\n  </SubTriggerStyled>\n));\nContextMenuSubTrigger.displayName = \"ContextMenuSubTrigger\";\n\nconst Indicator = styled(\n  ContextMenuPrimitive.ItemIndicator,\n  menuItemIndicatorCss\n);\n\nconst StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, menuItemCss);\nexport const ContextMenuRadioItem = forwardRef<\n  ElementRef<typeof StyledRadioItem>,\n  ComponentProps<typeof StyledRadioItem> & { icon?: ReactElement }\n>(({ children, icon, ...props }, forwardedRef) => (\n  <StyledRadioItem\n    withIndicator={icon !== undefined}\n    {...props}\n    ref={forwardedRef}\n  >\n    {icon !== undefined && <Indicator>{icon}</Indicator>}\n    {children}\n  </StyledRadioItem>\n));\nContextMenuRadioItem.displayName = \"ContextMenuRadioItem\";\n\nconst StyledCheckboxItem = styled(\n  ContextMenuPrimitive.CheckboxItem,\n  menuItemCss\n);\nexport const ContextMenuCheckboxItem = forwardRef<\n  ElementRef<typeof StyledCheckboxItem>,\n  ComponentProps<typeof StyledCheckboxItem> & { icon?: ReactNode }\n>(({ children, icon = <MenuCheckedIcon />, ...props }, forwardedRef) => (\n  <StyledCheckboxItem withIndicator {...props} ref={forwardedRef}>\n    <Indicator>{icon}</Indicator>\n    {children}\n  </StyledCheckboxItem>\n));\nContextMenuCheckboxItem.displayName = \"ContextMenuCheckboxItem\";\n\nexport const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;\n\nexport const ContextMenuGroup = ContextMenuPrimitive.Group;\n"
  },
  {
    "path": "packages/design-system/src/components/css-value-list-item.stories.tsx",
    "content": "import { useState, type ComponentProps, type ReactNode } from \"react\";\nimport { styled, theme } from \"../stitches.config\";\nimport {\n  CssValueListArrowFocus,\n  CssValueListItem,\n  __testing__,\n} from \"./css-value-list-item\";\nimport { Label, labelColors } from \"./label\";\nimport { SmallToggleButton } from \"./small-toggle-button\";\nimport { EyeOpenIcon, EyeClosedIcon, MinusIcon } from \"@webstudio-is/icons\";\nimport { SmallIconButton } from \"./small-icon-button\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  component: CssValueListItem,\n  title: \"CSS Value List Item\",\n};\n\nconst Thumbnail = styled(\"div\", {\n  width: theme.spacing[10],\n  height: theme.spacing[10],\n  backgroundImage: \"linear-gradient(yellow, red)\",\n});\n\nconst Panel = styled(\"div\", {\n  width: theme.spacing[30],\n});\n\nconst ListItem = (props: {\n  hidden: boolean;\n  labelColor: ComponentProps<typeof Label>[\"color\"];\n  state: undefined | \"open\";\n  active: boolean;\n  focused: undefined | boolean;\n  label?: ReactNode;\n  index: number;\n}) => {\n  const [pressed, onPressedChange] = useState(false);\n\n  return (\n    <CssValueListItem\n      label={\n        <Label disabled={props.hidden} color={props.labelColor} truncate>\n          {props.label ?? \"Image\"}\n        </Label>\n      }\n      thumbnail={<Thumbnail />}\n      hidden={props.hidden}\n      draggable\n      state={props.state}\n      focused={props.focused}\n      active={props.active}\n      index={props.index}\n      id={String(props.index)}\n      buttons={\n        <>\n          <SmallToggleButton\n            pressed={pressed}\n            onPressedChange={onPressedChange}\n            variant=\"normal\"\n            tabIndex={-1}\n            icon={pressed ? <EyeClosedIcon /> : <EyeOpenIcon />}\n          />\n\n          <SmallIconButton\n            variant=\"destructive\"\n            tabIndex={-1}\n            icon={<MinusIcon />}\n          />\n        </>\n      }\n      {...__testing__.listItemAttributes}\n    />\n  );\n};\n\nexport const CSSValueListItem = () => {\n  return (\n    <Panel>\n      <StorySection title=\"Overflows\">\n        <StoryGrid>\n          <>\n            {labelColors.map((labelColor, index) => (\n              <ListItem\n                key={labelColor}\n                index={index}\n                hidden={false}\n                active={false}\n                labelColor={labelColor}\n                state={undefined}\n                focused={false}\n                label=\"Very long text, very long text\"\n              />\n            ))}\n          </>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Variants\">\n        <StoryGrid>\n          <CssValueListArrowFocus>\n            {labelColors.map((labelColor, index) => (\n              <ListItem\n                key={labelColor}\n                index={index}\n                hidden={false}\n                active={false}\n                labelColor={labelColor}\n                state={undefined}\n                focused={false}\n              />\n            ))}\n\n            {labelColors.map((labelColor) => (\n              <ListItem\n                key={labelColor}\n                index={-1}\n                hidden={true}\n                active={false}\n                labelColor={labelColor}\n                state={undefined}\n                focused={false}\n              />\n            ))}\n\n            {labelColors.map((labelColor, index) => (\n              <ListItem\n                key={labelColor}\n                index={index}\n                hidden={false}\n                active={false}\n                labelColor={labelColor}\n                state={\"open\"}\n                focused={false}\n              />\n            ))}\n          </CssValueListArrowFocus>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Active\">\n        <StoryGrid>\n          <CssValueListArrowFocus>\n            {labelColors.map((labelColor, index) => (\n              <ListItem\n                key={labelColor}\n                index={index}\n                hidden={false}\n                active={true}\n                labelColor={labelColor}\n                state={undefined}\n                focused={false}\n              />\n            ))}\n\n            {labelColors.map((labelColor, index) => (\n              <ListItem\n                key={labelColor}\n                index={index}\n                hidden={true}\n                active={true}\n                labelColor={labelColor}\n                state={undefined}\n                focused={false}\n              />\n            ))}\n\n            {labelColors.map((labelColor, index) => (\n              <ListItem\n                key={labelColor}\n                index={index}\n                hidden={false}\n                active={true}\n                labelColor={labelColor}\n                state={\"open\"}\n                focused={false}\n              />\n            ))}\n          </CssValueListArrowFocus>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"No thumbnail\">\n        <CssValueListArrowFocus>\n          <CssValueListItem\n            id=\"no-thumb-0\"\n            index={0}\n            label={<Label>Text only item</Label>}\n            hidden={false}\n            buttons={\n              <SmallIconButton\n                variant=\"destructive\"\n                tabIndex={-1}\n                icon={<MinusIcon />}\n              />\n            }\n            {...__testing__.listItemAttributes}\n          />\n          <CssValueListItem\n            id=\"no-thumb-1\"\n            index={1}\n            label={<Label>Another text item</Label>}\n            hidden={false}\n            buttons={\n              <SmallIconButton\n                variant=\"destructive\"\n                tabIndex={-1}\n                icon={<MinusIcon />}\n              />\n            }\n            {...__testing__.listItemAttributes}\n          />\n        </CssValueListArrowFocus>\n      </StorySection>\n\n      <StorySection title=\"Disabled state\">\n        <CssValueListArrowFocus>\n          <CssValueListItem\n            id=\"disabled-0\"\n            index={0}\n            label={<Label>Disabled item</Label>}\n            thumbnail={<Thumbnail />}\n            hidden={false}\n            disabled\n            {...__testing__.listItemAttributes}\n          />\n          <CssValueListItem\n            id=\"disabled-1\"\n            index={1}\n            label={<Label>Enabled item</Label>}\n            thumbnail={<Thumbnail />}\n            hidden={false}\n            {...__testing__.listItemAttributes}\n          />\n        </CssValueListArrowFocus>\n      </StorySection>\n\n      <StorySection title=\"Arrow focus with drag item\">\n        <CssValueListArrowFocus dragItemId=\"drag-1\">\n          {labelColors.map((labelColor, index) => (\n            <ListItem\n              key={labelColor}\n              index={index}\n              hidden={false}\n              active={index === 1}\n              labelColor={labelColor}\n              state={undefined}\n              focused={false}\n            />\n          ))}\n        </CssValueListArrowFocus>\n      </StorySection>\n    </Panel>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/css-value-list-item.tsx",
    "content": "import {\n  type ComponentProps,\n  type Ref,\n  forwardRef,\n  Children,\n  useMemo,\n  type ReactNode,\n} from \"react\";\nimport { styled } from \"../stitches.config\";\nimport { Flex } from \"./flex\";\nimport { Box } from \"./box\";\nimport { theme } from \"../stitches.config\";\nimport { DragHandleIcon } from \"@webstudio-is/icons\";\nimport { ArrowFocus } from \"./primitives/arrow-focus\";\n\nconst listItemAttribute = \"data-list-item\";\nconst listItemAttributes = { [listItemAttribute]: true };\n\nconst DragHandleIconStyled = styled(DragHandleIcon, {\n  width: theme.spacing[7],\n  visibility: \"hidden\",\n  cursor: \"grab\",\n  color: theme.colors.foregroundSubtle,\n  flexShrink: 0,\n});\n\nconst ThumbHolder = styled(\"div\", {\n  flexShrink: 0,\n});\n\n/**\n * We draw button above rela button positions, therefore we need to have same padding\n */\nconst sharedPaddingRight = theme.spacing[7];\n\nconst IconButtonsWrapper = styled(Flex, {\n  position: \"absolute\",\n  right: 0,\n  top: 0,\n  bottom: 0,\n  paddingRight: sharedPaddingRight,\n  display: \"none\",\n});\n\nconst FakeIconButtonsWrapper = styled(Flex, {\n  paddingLeft: theme.spacing[5],\n  display: \"none\",\n});\n\n/**\n * Should be a button as otherwise radix trigger doesn't work with keyboard interactions\n */\nconst ItemButton = styled(\"button\", {\n  appearance: \"none\",\n  width: \"100%\",\n  border: \"none\",\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"start\",\n  userSelect: \"none\",\n  backgroundColor: \"inherit\",\n  padding: 0,\n\n  paddingRight: sharedPaddingRight,\n\n  height: theme.spacing[11],\n  position: \"relative\",\n\n  \"&:focus-visible, &[data-focused=true], &[data-state=open]\": {\n    [`& ${FakeIconButtonsWrapper}`]: {\n      display: \"flex\",\n    },\n    [`~ ${IconButtonsWrapper}`]: {\n      display: \"flex\",\n    },\n    outline: \"none\",\n    backgroundColor: theme.colors.backgroundHover,\n  },\n  variants: {\n    hidden: {\n      true: {\n        opacity: 0.2,\n        [`& ${ThumbHolder}`]: {\n          opacity: 0.2,\n        },\n      },\n    },\n  },\n});\n\ntype Props = ComponentProps<typeof ItemButton> & {\n  id: string;\n  index: number;\n  hidden?: boolean;\n  draggable?: boolean;\n  label: React.ReactElement;\n  thumbnail?: React.ReactElement;\n  buttons?: React.ReactElement;\n  // to support Radix trigger asChild\n  \"data-state\"?: \"open\";\n  // for Storybook purposes\n  focused?: boolean;\n  state?: \"open\";\n  active?: boolean;\n};\n\nconst ItemWrapper = styled(\"div\", {\n  position: \"relative\",\n  width: \"100%\",\n  \"&:hover, &:focus-within, &[data-active=true]\": {\n    [`& ${ItemButton}`]: {\n      backgroundColor: theme.colors.backgroundHover,\n      [`&[data-draggable=true] ${DragHandleIconStyled}`]: {\n        visibility: \"visible\",\n      },\n    },\n    [`& ${IconButtonsWrapper}`]: {\n      display: \"flex\",\n    },\n    [`& ${FakeIconButtonsWrapper}`]: {\n      display: \"flex\",\n    },\n  },\n});\n\nconst FakeSmallButton = styled(\"div\", {\n  width: theme.spacing[9],\n  height: theme.spacing[9],\n});\n\nexport const CssValueListItem = forwardRef(\n  (\n    {\n      label,\n      thumbnail,\n      buttons,\n      focused,\n      state,\n      active,\n      index,\n      id,\n      hidden,\n      draggable,\n      \"data-state\": dataState,\n      ...rest\n    }: Props,\n    ref: Ref<HTMLButtonElement>\n  ) => {\n    const buttonsCount = buttons\n      ? // When children is a single element, count results in 0\n        Children.count(buttons.props.children) || 1\n      : 0;\n\n    const fakeButtons = useMemo(\n      () => (\n        <>\n          {Array.from(new Array(buttonsCount), (_v, index) => (\n            <FakeSmallButton key={index} />\n          ))}\n        </>\n      ),\n      [buttonsCount]\n    );\n\n    return (\n      <ArrowFocus\n        render={({ handleKeyDown }) => (\n          <ItemWrapper\n            onKeyDown={(event) => {\n              if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n                handleKeyDown(event);\n              }\n            }}\n            data-active={active}\n          >\n            <ItemButton\n              ref={ref}\n              data-id={id}\n              data-draggable={draggable}\n              data-focused={focused}\n              data-state={state ?? dataState}\n              data-active={active}\n              tabIndex={index === 0 ? 0 : -1}\n              {...listItemAttributes}\n              {...rest}\n              hidden={hidden}\n              disabled={hidden === true || rest.disabled}\n            >\n              <DragHandleIconStyled />\n\n              <Flex shrink align=\"center\" css={{ overflow: \"hidden\" }}>\n                {thumbnail ? <ThumbHolder>{thumbnail}</ThumbHolder> : null}\n                {label}\n              </Flex>\n\n              <Flex grow={true} />\n\n              {\n                // We place fake divs with same dimensions as small buttons here to avoid following warning:\n                // Warning: validateDOMNesting(...): <button> cannot appear as a descendant of <button>\n                // Real buttons will be placed on top of fake buttons\n              }\n              <FakeIconButtonsWrapper shrink={false} gap={2}>\n                {fakeButtons}\n              </FakeIconButtonsWrapper>\n            </ItemButton>\n\n            {\n              // Real buttons are placed above ItemButton to avoid <button> cannot appear as a descendant of <button> warning\n            }\n            <IconButtonsWrapper gap={2} align=\"center\">\n              {buttons}\n            </IconButtonsWrapper>\n          </ItemWrapper>\n        )}\n      />\n    );\n  }\n);\n\nCssValueListItem.displayName = \"CssValueListItem\";\n\nexport const CssValueListArrowFocus = ({\n  children,\n  dragItemId,\n}: {\n  children: ReactNode;\n  dragItemId?: string;\n}) => {\n  return (\n    <ArrowFocus\n      render={({ handleKeyDown }) => (\n        <Box\n          css={{\n            display: \"contents\",\n            pointerEvents: dragItemId ? \"none\" : \"auto\",\n            // to make DnD work we have to disable scrolling using touch\n            touchAction: \"none\",\n          }}\n          onKeyDown={(event) => {\n            if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n              handleKeyDown(event, {\n                accept: (element) =>\n                  element.getAttribute(listItemAttribute) === \"true\",\n              });\n            }\n          }}\n        >\n          {children}\n        </Box>\n      )}\n    />\n  );\n};\n\nexport const __testing__ = {\n  listItemAttributes,\n};\n"
  },
  {
    "path": "packages/design-system/src/components/dialog.stories.tsx",
    "content": "import { Button } from \"./button\";\nimport { css, theme } from \"../stitches.config\";\nimport { textVariants } from \"./text\";\nimport { StorySection } from \"./storybook\";\nimport {\n  Dialog,\n  DialogActions,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogMaximize,\n  DialogTitle,\n  DialogTitleActions,\n  DialogTrigger,\n} from \"./dialog\";\n\nexport default {\n  title: \"Floating panel/Dialog\",\n};\n\nconst bodyStyle = css({\n  padding: theme.spacing[9],\n});\n\nconst descriptionStyle = css(textVariants.regular, {\n  marginTop: 0,\n  marginBottom: theme.spacing[9],\n});\n\nconst buttonsStyle = css({\n  display: \"flex\",\n  gap: theme.spacing[5],\n  justifyContent: \"flex-end\",\n});\n\nconst DialogDemo = () => (\n  <StorySection title=\"Dialog\">\n    <Dialog defaultOpen>\n      <DialogTrigger asChild>\n        <Button>Open</Button>\n      </DialogTrigger>\n      <DialogContent>\n        <div className={bodyStyle()}>\n          <DialogDescription asChild>\n            <p className={descriptionStyle()}>This is a description</p>\n          </DialogDescription>\n          <div className={buttonsStyle()}>\n            <DialogClose>\n              <Button color=\"ghost\">Cancel</Button>\n            </DialogClose>\n            <DialogClose>\n              <Button color=\"positive\">Save</Button>\n            </DialogClose>\n          </div>\n        </div>\n\n        {/* Title is at the end intentionally,\n         * to make the close button last in the tab order\n         */}\n        <DialogTitle>Title</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  </StorySection>\n);\nexport { DialogDemo as Dialog };\n\nexport const ResizableDialog = () => (\n  <StorySection title=\"Resizable dialog\">\n    <Dialog defaultOpen resize=\"both\">\n      <DialogTrigger asChild>\n        <Button>Open resizable</Button>\n      </DialogTrigger>\n      <DialogContent>\n        <div className={bodyStyle()}>\n          <DialogDescription asChild>\n            <p className={descriptionStyle()}>\n              Resize this dialog by dragging the edges\n            </p>\n          </DialogDescription>\n        </div>\n        <DialogTitle>Resizable</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  </StorySection>\n);\n\nexport const WithMaximize = () => (\n  <StorySection title=\"With maximize\">\n    <Dialog defaultOpen resize=\"both\">\n      <DialogTrigger asChild>\n        <Button>Open maximizable</Button>\n      </DialogTrigger>\n      <DialogContent>\n        <div className={bodyStyle()}>\n          <DialogDescription asChild>\n            <p className={descriptionStyle()}>\n              Click the maximize button to expand\n            </p>\n          </DialogDescription>\n        </div>\n        <DialogTitle\n          suffix={\n            <DialogTitleActions>\n              <DialogMaximize />\n              <DialogClose />\n            </DialogTitleActions>\n          }\n        >\n          Maximizable\n        </DialogTitle>\n      </DialogContent>\n    </Dialog>\n  </StorySection>\n);\n\nexport const WithActions = () => (\n  <StorySection title=\"With actions\">\n    <Dialog defaultOpen>\n      <DialogTrigger asChild>\n        <Button>Open with actions</Button>\n      </DialogTrigger>\n      <DialogContent>\n        <div className={bodyStyle()}>\n          <DialogDescription asChild>\n            <p className={descriptionStyle()}>\n              Dialog with action buttons at the bottom\n            </p>\n          </DialogDescription>\n        </div>\n        <DialogActions>\n          <DialogClose>\n            <Button color=\"positive\">Save</Button>\n          </DialogClose>\n          <DialogClose>\n            <Button color=\"ghost\">Cancel</Button>\n          </DialogClose>\n        </DialogActions>\n        <DialogTitle>With actions</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  </StorySection>\n);\n\nexport const NonDraggable = () => (\n  <StorySection title=\"Non-draggable\">\n    <Dialog defaultOpen draggable={false}>\n      <DialogTrigger asChild>\n        <Button>Open non-draggable</Button>\n      </DialogTrigger>\n      <DialogContent>\n        <div className={bodyStyle()}>\n          <DialogDescription asChild>\n            <p className={descriptionStyle()}>This dialog cannot be dragged</p>\n          </DialogDescription>\n        </div>\n        <DialogTitle>Non-draggable</DialogTitle>\n      </DialogContent>\n    </Dialog>\n  </StorySection>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/dialog.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { __testing__ } from \"./dialog\";\n\nconst {\n  calculateInset,\n  calculateCenteredPosition,\n  calculateDialogStyle,\n  applyBoundaries,\n} = __testing__;\n\ndescribe(\"calculateInset\", () => {\n  test(\"calculates inset for centered boundary\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const result = calculateInset(bounds, 1920, 1080);\n    expect(result).toBe(\"50px 1020px 430px 100px\");\n  });\n\n  test(\"calculates inset for full viewport\", () => {\n    const bounds = { x: 0, y: 0, width: 1920, height: 1080 };\n    const result = calculateInset(bounds, 1920, 1080);\n    expect(result).toBe(\"0px 0px 0px 0px\");\n  });\n\n  test(\"calculates inset for small boundary\", () => {\n    const bounds = { x: 500, y: 300, width: 400, height: 300 };\n    const result = calculateInset(bounds, 1920, 1080);\n    expect(result).toBe(\"300px 1020px 480px 500px\");\n  });\n});\n\ndescribe(\"calculateCenteredPosition\", () => {\n  test(\"centers dialog in boundary\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const result = calculateCenteredPosition(bounds, 400, 300);\n    expect(result).toEqual({ top: 250, left: 300 });\n  });\n\n  test(\"centers dialog with offset boundary\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const result = calculateCenteredPosition(bounds, 400, 300);\n    expect(result).toEqual({ top: 200, left: 300 });\n  });\n\n  test(\"handles dialog larger than boundary\", () => {\n    const bounds = { x: 0, y: 0, width: 200, height: 150 };\n    const result = calculateCenteredPosition(bounds, 400, 300);\n    // Should clamp to bounds.y/x (0, 0) when dialog is too large\n    expect(result).toEqual({ top: 0, left: 0 });\n  });\n\n  test(\"handles undefined dimensions\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const result = calculateCenteredPosition(bounds);\n    expect(result).toEqual({ top: 350, left: 500 });\n  });\n});\n\ndescribe(\"calculateDialogStyle\", () => {\n  test(\"maximized dialog fills boundary\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const style = calculateDialogStyle(bounds, {\n      isMaximized: true,\n      windowWidth: 1920,\n      windowHeight: 1080,\n    });\n\n    expect(style).toEqual({\n      top: 50,\n      left: 100,\n      width: 800,\n      height: 600,\n    });\n  });\n\n  test(\"dialog with both width and height uses centered positioning\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const style = calculateDialogStyle(bounds, {\n      isMaximized: false,\n      width: 400,\n      height: 300,\n      windowWidth: 1920,\n      windowHeight: 1080,\n    });\n\n    expect(style).toMatchObject({\n      top: 250,\n      left: 300,\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"dialog with only width uses inset centering\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const style = calculateDialogStyle(bounds, {\n      isMaximized: false,\n      width: 400,\n      windowWidth: 1920,\n      windowHeight: 1080,\n    });\n\n    expect(style).toMatchObject({\n      inset: \"50px 1020px 430px 100px\",\n      margin: \"auto\",\n      width: 400,\n      maxWidth: 800,\n      maxHeight: 600,\n    });\n  });\n\n  test(\"dialog with no dimensions uses inset centering\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const style = calculateDialogStyle(bounds, {\n      isMaximized: false,\n      windowWidth: 1920,\n      windowHeight: 1080,\n    });\n\n    expect(style).toMatchObject({\n      inset: \"50px 1020px 430px 100px\",\n      margin: \"auto\",\n      maxWidth: 800,\n      maxHeight: 600,\n    });\n  });\n\n  test(\"applies minWidth and minHeight\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const style = calculateDialogStyle(bounds, {\n      isMaximized: false,\n      width: 400,\n      height: 300,\n      minWidth: 200,\n      minHeight: 150,\n      windowWidth: 1920,\n      windowHeight: 1080,\n    });\n\n    expect(style.minWidth).toBe(200);\n    expect(style.minHeight).toBe(150);\n  });\n\n  test(\"handles edge case: zero-sized boundary\", () => {\n    const bounds = { x: 0, y: 0, width: 0, height: 0 };\n    const style = calculateDialogStyle(bounds, {\n      isMaximized: false,\n      width: 400,\n      height: 300,\n      windowWidth: 1920,\n      windowHeight: 1080,\n    });\n\n    expect(style).toMatchObject({\n      top: 0,\n      left: 0,\n      width: 400,\n      height: 300,\n    });\n  });\n});\n\ndescribe(\"applyBoundaries\", () => {\n  test(\"constrains dialog within bounds\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const result = applyBoundaries(100, 50, 400, 300, bounds);\n\n    expect(result).toEqual({\n      x: 100,\n      y: 50,\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"constrains dialog that exceeds right boundary\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const result = applyBoundaries(800, 50, 400, 300, bounds);\n\n    expect(result).toEqual({\n      x: 600, // 1000 - 400\n      y: 50,\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"constrains dialog that exceeds bottom boundary\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const result = applyBoundaries(100, 700, 400, 300, bounds);\n\n    expect(result).toEqual({\n      x: 100,\n      y: 500, // 800 - 300\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"constrains dialog with negative position\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const result = applyBoundaries(-50, -20, 400, 300, bounds);\n\n    expect(result).toEqual({\n      x: 0,\n      y: 0,\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"constrains dialog larger than bounds\", () => {\n    const bounds = { x: 0, y: 0, width: 500, height: 400 };\n    const result = applyBoundaries(0, 0, 800, 600, bounds);\n\n    expect(result).toEqual({\n      x: 0,\n      y: 0,\n      width: 500, // Constrained to bounds width\n      height: 400, // Constrained to bounds height\n    });\n  });\n\n  test(\"applies horizontal tolerance\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const tolerance = { horizontal: 50 };\n    const result = applyBoundaries(-60, 50, 400, 300, bounds, tolerance);\n\n    expect(result).toEqual({\n      x: -50, // Allowed to go 50px outside on left\n      y: 50,\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"applies vertical tolerance\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const tolerance = { vertical: 30 };\n    const result = applyBoundaries(100, -40, 400, 300, bounds, tolerance);\n\n    expect(result).toEqual({\n      x: 100,\n      y: -30, // Allowed to go 30px outside on top\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"applies both horizontal and vertical tolerance\", () => {\n    const bounds = { x: 0, y: 0, width: 1000, height: 800 };\n    const tolerance = { horizontal: 50, vertical: 30 };\n    const result = applyBoundaries(1100, 900, 400, 300, bounds, tolerance);\n\n    expect(result).toEqual({\n      x: 650, // 1000 + 50 - 400\n      y: 530, // 800 + 30 - 300\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"handles offset boundary with tolerance\", () => {\n    const bounds = { x: 100, y: 50, width: 800, height: 600 };\n    const tolerance = { horizontal: 20, vertical: 10 };\n    const result = applyBoundaries(90, 45, 400, 300, bounds, tolerance);\n\n    expect(result).toEqual({\n      x: 90, // Within tolerance (100 - 20 = 80)\n      y: 45, // Within tolerance (50 - 10 = 40)\n      width: 400,\n      height: 300,\n    });\n  });\n\n  test(\"increases allowed size with tolerance\", () => {\n    const bounds = { x: 0, y: 0, width: 500, height: 400 };\n    const tolerance = { horizontal: 100, vertical: 50 };\n    const result = applyBoundaries(0, 0, 800, 600, bounds, tolerance);\n\n    expect(result).toEqual({\n      x: -100, // Can go 100px outside on left\n      y: -50, // Can go 50px outside on top\n      width: 700, // 500 + 100*2\n      height: 500, // 400 + 50*2\n    });\n  });\n});\n"
  },
  {
    "path": "packages/design-system/src/components/dialog.tsx",
    "content": "import {\n  type ReactNode,\n  type ComponentProps,\n  type Ref,\n  forwardRef,\n  useRef,\n  type DragEventHandler,\n  createContext,\n  useContext,\n  useState,\n  useEffect,\n  useCallback,\n  useMemo,\n  type RefObject,\n} from \"react\";\nimport * as Primitive from \"@radix-ui/react-dialog\";\nimport { css, theme, type CSS } from \"../stitches.config\";\nimport { PanelTitle } from \"./panel-title\";\nimport { Flex } from \"./flex\";\nimport { useDisableCanvasPointerEvents, useResize } from \"../utilities\";\nimport type { CSSProperties } from \"@stitches/react\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { Button } from \"./button\";\nimport { XIcon, MaximizeIcon, MinimizeIcon } from \"@webstudio-is/icons\";\nimport { Separator } from \"./separator\";\nimport { Text } from \"./text\";\n\nconst DIALOG_TITLE_HEIGHT = 40;\n\nexport const DialogTrigger = Primitive.Trigger;\n\n// An optional accessible description to be announced when the dialog is opened\n// https://www.radix-ui.com/docs/primitives/components/dialog#description\nexport const DialogDescription = Primitive.Description;\n\nconst placeholderImage =\n  typeof Image !== \"undefined\" ? new Image(0, 0) : undefined;\n// It's important to set the src early, because it has to be loaded by the time drag starts.\nif (placeholderImage) {\n  placeholderImage.src =\n    \"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==\";\n}\n\nconst panelStyle = css({\n  boxShadow: theme.shadows.panelSectionDropShadow,\n  background: theme.colors.backgroundPanel,\n  borderRadius: theme.borderRadius[7],\n  display: \"flex\",\n  flexDirection: \"column\",\n\n  \"&:focus\": {\n    // override browser default\n    outline: \"none\",\n  },\n});\n\nconst DialogContext = createContext<{\n  isMaximized: boolean;\n  setIsMaximized: (isMaximized: boolean) => void;\n  resize?: \"both\" | \"none\";\n  draggable?: boolean;\n}>({\n  isMaximized: false,\n  setIsMaximized: () => {},\n  draggable: true,\n  resize: \"none\",\n});\n\nexport const Dialog = ({\n  resize,\n  draggable,\n  onOpenChange,\n  ...props\n}: ComponentProps<typeof Primitive.Dialog> & {\n  resize?: \"both\" | \"none\";\n  draggable?: boolean;\n}) => {\n  const [isMaximized, setIsMaximized] = useState(false);\n  return (\n    <DialogContext.Provider\n      value={{ isMaximized, setIsMaximized, resize, draggable }}\n    >\n      <Primitive.Dialog\n        {...props}\n        onOpenChange={(open) => {\n          // When dialog closes, there can be state changes in the content that haven't rendered yet.\n          // In that case we might close the dialog without saving the form changes.\n          // Currently known example is binding popover that opens from variable popover. Second popover gets\n          // closed by unmounting if we click outside and first popover doesn't get the changes because it is trying to render\n          // the value in a form and serialize the form using FormData.\n          // With this we are giving React's render cycle time to render the state before we close the dialog.\n          requestAnimationFrame(() => {\n            onOpenChange?.(open);\n          });\n        }}\n      />\n    </DialogContext.Provider>\n  );\n};\n\nexport const DialogClose = forwardRef(\n  (\n    { children, ...props }: ComponentProps<typeof Button>,\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <Primitive.Close asChild>\n      {children ?? (\n        <Button\n          color=\"ghost\"\n          prefix={<XIcon />}\n          aria-label=\"Close\"\n          {...props}\n          ref={ref}\n        />\n      )}\n    </Primitive.Close>\n  )\n);\nDialogClose.displayName = \"DialogClose\";\n\nexport const DialogMaximize = forwardRef(\n  (props: ComponentProps<typeof Button>, ref: Ref<HTMLButtonElement>) => {\n    const { isMaximized, setIsMaximized } = useContext(DialogContext);\n    return (\n      <Button\n        color=\"ghost\"\n        prefix={isMaximized ? <MinimizeIcon /> : <MaximizeIcon />}\n        aria-label=\"Expand\"\n        onClick={() => setIsMaximized(isMaximized ? false : true)}\n        {...props}\n        ref={ref}\n      />\n    );\n  }\n);\nDialogMaximize.displayName = \"DialogMaximize\";\n\ntype Point = { x: number; y: number };\ntype Size = { width: number; height: number };\ntype Rect = Point & Size;\n\ntype Tolerance = {\n  horizontal?: number;\n  vertical?: number;\n};\n\n// Utility: Calculate inset CSS string from boundary rect\nconst calculateInset = (\n  bounds: Rect,\n  windowWidth: number,\n  windowHeight: number\n): string => {\n  return `${bounds.y}px ${windowWidth - bounds.x - bounds.width}px ${windowHeight - bounds.y - bounds.height}px ${bounds.x}px`;\n};\n\n// Utility: Calculate centered dialog position\nconst calculateCenteredPosition = (\n  bounds: Rect,\n  width?: number,\n  height?: number\n): { top: number; left: number } => {\n  return {\n    top: Math.max(bounds.y, bounds.y + bounds.height / 2 - (height ?? 0) / 2),\n    left: Math.max(bounds.x, bounds.x + bounds.width / 2 - (width ?? 0) / 2),\n  };\n};\n\n// Utility: Calculate dialog style based on positioning mode\nconst calculateDialogStyle = (\n  bounds: Rect,\n  options: {\n    isMaximized: boolean;\n    width?: number;\n    height?: number;\n    minWidth?: number;\n    minHeight?: number;\n    windowWidth: number;\n    windowHeight: number;\n  }\n): CSSProperties => {\n  const {\n    isMaximized,\n    width,\n    height,\n    minWidth,\n    minHeight,\n    windowWidth,\n    windowHeight,\n  } = options;\n\n  if (isMaximized) {\n    return {\n      top: bounds.y,\n      left: bounds.x,\n      width: bounds.width,\n      height: bounds.height,\n    };\n  }\n\n  // If both width and height are specified, use centered positioning\n  if (width && height) {\n    const centered = calculateCenteredPosition(bounds, width, height);\n    return {\n      top: centered.top,\n      left: centered.left,\n      width,\n      height,\n      ...(minWidth && { minWidth }),\n      ...(minHeight && { minHeight }),\n    };\n  }\n\n  // Otherwise use inset-based centering with margin: auto\n  const style: CSSProperties = {\n    inset: calculateInset(bounds, windowWidth, windowHeight),\n    margin: \"auto\",\n    maxWidth: bounds.width,\n    maxHeight: bounds.height,\n  };\n\n  if (width !== undefined) {\n    style.width = width;\n  }\n  if (height !== undefined) {\n    style.height = height;\n  }\n  if (minWidth !== undefined) {\n    style.minWidth = minWidth;\n  }\n  if (minHeight !== undefined) {\n    style.minHeight = minHeight;\n  }\n\n  return style;\n};\n\n// Utility: Apply boundary constraints to dialog position and size\nconst applyBoundaries = (\n  x: number,\n  y: number,\n  width: number,\n  height: number,\n  bounds: Rect,\n  tolerance?: Tolerance\n): Rect => {\n  const horizontalTolerance = tolerance?.horizontal ?? 0;\n  const verticalTolerance = tolerance?.vertical ?? 0;\n\n  const minX = bounds.x - horizontalTolerance;\n  const minY = bounds.y - verticalTolerance;\n  const maxWidth = bounds.width + horizontalTolerance * 2;\n  const maxHeight = bounds.height + verticalTolerance * 2;\n\n  const constrainedWidth = Math.min(width, maxWidth);\n  const constrainedHeight = Math.min(height, maxHeight);\n\n  const maxX = bounds.x + bounds.width + horizontalTolerance - constrainedWidth;\n  const maxY = bounds.y + bounds.height + verticalTolerance - constrainedHeight;\n\n  return {\n    x: Math.max(minX, Math.min(x, maxX)),\n    y: Math.max(minY, Math.min(y, maxY)),\n    width: constrainedWidth,\n    height: constrainedHeight,\n  };\n};\n\nconst useBoundary = () => {\n  const [boundaryRect, setBoundaryRect] = useState<Rect | undefined>(undefined);\n\n  useEffect(() => {\n    const boundaryElement = document.querySelector(\n      \"[data-dialog-boundary]\"\n    ) as HTMLElement | null;\n\n    if (!boundaryElement) {\n      setBoundaryRect(undefined);\n      return;\n    }\n\n    const updateBoundary = () => {\n      const rect = boundaryElement.getBoundingClientRect();\n      setBoundaryRect({\n        x: rect.x,\n        y: rect.y,\n        width: rect.width,\n        height: rect.height,\n      });\n    };\n\n    // Initial measurement\n    updateBoundary();\n\n    const resizeObserver = new ResizeObserver(updateBoundary);\n    resizeObserver.observe(boundaryElement);\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, []);\n\n  return useMemo(\n    () =>\n      boundaryRect ?? {\n        x: 0,\n        y: 0,\n        width: window.innerWidth,\n        height: window.innerHeight,\n      },\n    [boundaryRect]\n  );\n};\n\ntype UseDraggableProps = {\n  isMaximized: boolean;\n  minWidth?: number;\n  minHeight?: number;\n  boundaryTolerance?: { horizontal?: number; vertical?: number };\n} & Partial<Rect>;\n\nconst useDraggable = ({\n  width,\n  height,\n  minHeight,\n  minWidth,\n  isMaximized,\n  boundaryTolerance,\n  ...props\n}: UseDraggableProps) => {\n  const [x, setX] = useState(props.x);\n  const [y, setY] = useState(props.y);\n  const bounds = useBoundary();\n\n  const lastDragDataRef = useRef<\n    | undefined\n    | {\n        point: Point;\n        rect: Rect;\n      }\n  >(undefined);\n\n  const ref = useRef<HTMLDivElement | null>(null);\n\n  const calcStyle = useCallback(() => {\n    const style = calculateDialogStyle(bounds, {\n      isMaximized,\n      width,\n      height,\n      minWidth,\n      minHeight,\n      windowWidth: window.innerWidth,\n      windowHeight: window.innerHeight,\n    });\n\n    if (isMaximized === false) {\n      if (x !== undefined && y !== undefined) {\n        // Get actual rendered dimensions if width/height not specified\n        const actualWidth = width ?? ref.current?.offsetWidth ?? bounds.width;\n        const actualHeight =\n          height ?? ref.current?.offsetHeight ?? bounds.height;\n\n        const constrained = applyBoundaries(\n          x,\n          y,\n          actualWidth,\n          actualHeight,\n          bounds,\n          boundaryTolerance\n        );\n\n        style.left = constrained.x;\n        style.top = constrained.y;\n        if (width !== undefined) {\n          style.width = constrained.width;\n        }\n        if (height !== undefined) {\n          style.height = constrained.height;\n        }\n        style.right = \"auto\";\n        style.bottom = \"auto\";\n        style.margin = 0;\n      } else if (x !== undefined) {\n        const actualWidth = width ?? ref.current?.offsetWidth ?? bounds.width;\n        const actualHeight =\n          height ?? ref.current?.offsetHeight ?? bounds.height;\n\n        const constrained = applyBoundaries(\n          x,\n          0,\n          actualWidth,\n          actualHeight,\n          bounds,\n          boundaryTolerance\n        );\n\n        style.left = constrained.x;\n        if (width !== undefined) {\n          style.width = constrained.width;\n        }\n        if (height !== undefined) {\n          style.height = constrained.height;\n        }\n        style.right = \"auto\";\n        style.margin = 0;\n      } else if (y !== undefined) {\n        const actualWidth = width ?? ref.current?.offsetWidth ?? bounds.width;\n        const actualHeight =\n          height ?? ref.current?.offsetHeight ?? bounds.height;\n\n        const constrained = applyBoundaries(\n          0,\n          y,\n          actualWidth,\n          actualHeight,\n          bounds,\n          boundaryTolerance\n        );\n\n        style.top = constrained.y;\n        if (width !== undefined) {\n          style.width = constrained.width;\n        }\n        if (height !== undefined) {\n          style.height = constrained.height;\n        }\n        style.bottom = \"auto\";\n        style.margin = 0;\n      } else {\n        // Only apply max constraints when not positioned\n        style.maxWidth = bounds.width;\n        style.maxHeight = bounds.height;\n      }\n    }\n\n    return style;\n  }, [\n    x,\n    y,\n    width,\n    height,\n    isMaximized,\n    minWidth,\n    minHeight,\n    bounds,\n    boundaryTolerance,\n  ]);\n\n  const [style, setStyle] = useState(calcStyle());\n\n  useEffect(() => {\n    setStyle(calcStyle());\n  }, [calcStyle]);\n\n  useEffect(() => {\n    if (lastDragDataRef.current) {\n      // Until user draggs, we need component props to define the position, because floating panel needs to adjust it after rendering.\n      // We don't want to use the props x/y value after user has dragged manually. At this point position is defined\n      // by drag interaction and props can't override it, otherwise position will jump for unpredictable reasons, e.g. when parent decides to update.\n      return;\n    }\n    setX(props.x);\n    setY(props.y);\n  }, [props.x, props.y]);\n\n  const handleDragStart: DragEventHandler = (event) => {\n    event.stopPropagation();\n    const target = ref.current;\n    if (target === null) {\n      return;\n    }\n    if (placeholderImage) {\n      event.dataTransfer.setDragImage(placeholderImage, 0, 0);\n    }\n\n    const rect = target.getBoundingClientRect();\n    target.style.left = `${rect.x}px`;\n    target.style.top = `${rect.y}px`;\n    lastDragDataRef.current = {\n      point: { x: event.pageX, y: event.pageY },\n      rect,\n    };\n  };\n\n  const handleDrag: DragEventHandler = (event) => {\n    event.stopPropagation();\n    const target = ref.current;\n\n    if (\n      event.pageX <= 0 ||\n      event.pageY <= 0 ||\n      lastDragDataRef.current === undefined ||\n      target === null\n    ) {\n      return;\n    }\n\n    const { rect, point } = lastDragDataRef.current;\n    const movementX = point.x - event.pageX;\n    const movementY = point.y - event.pageY;\n    // Allow dragging anywhere without constraints\n    const left = rect.x - movementX;\n    const top = rect.y - movementY;\n    target.style.left = `${left}px`;\n    target.style.top = `${top}px`;\n  };\n\n  const handleDragEnd: DragEventHandler = (event) => {\n    event.stopPropagation();\n    const target = ref.current;\n    if (target === null) {\n      return;\n    }\n    const rect = target.getBoundingClientRect();\n\n    // Apply constraints to snap to the closest valid position\n    let constrainedX = Math.max(rect.x, bounds.x);\n    constrainedX = Math.min(constrainedX, bounds.x + bounds.width - rect.width);\n    let constrainedY = Math.max(rect.y, bounds.y);\n    // Keep at least title visible at the bottom\n    constrainedY = Math.min(\n      constrainedY,\n      bounds.y + bounds.height - DIALOG_TITLE_HEIGHT\n    );\n\n    setX(constrainedX);\n    setY(constrainedY);\n\n    // Apply the constrained position immediately\n    target.style.left = `${constrainedX}px`;\n    target.style.top = `${constrainedY}px`;\n  };\n\n  return {\n    onDragStart: handleDragStart,\n    onDrag: handleDrag,\n    onDragEnd: handleDragEnd,\n    style,\n    ref,\n  };\n};\n\n// This is needed to prevent pointer events on the iframe from interfering with dragging and resizing.\nconst useSetPointerEvents = (elementRef: RefObject<HTMLElement | null>) => {\n  const { enableCanvasPointerEvents, disableCanvasPointerEvents } =\n    useDisableCanvasPointerEvents();\n\n  return useCallback(\n    (value: string) => {\n      return () => {\n        value === \"none\"\n          ? disableCanvasPointerEvents()\n          : enableCanvasPointerEvents();\n        // RAF is needed otherwise dragstart event won't fire because of pointer-events: none\n        requestAnimationFrame(() => {\n          if (elementRef.current) {\n            elementRef.current.style.pointerEvents = value;\n          }\n        });\n      };\n    },\n    [elementRef, enableCanvasPointerEvents, disableCanvasPointerEvents]\n  );\n};\n\nconst ContentContainer = forwardRef(\n  (\n    {\n      children,\n      className,\n      css,\n      width,\n      height,\n      x,\n      y,\n      minWidth,\n      minHeight,\n      boundaryTolerance,\n      ...props\n    }: ComponentProps<typeof Primitive.Content> &\n      Partial<UseDraggableProps> & {\n        css?: CSS;\n      },\n    forwardedRef: Ref<HTMLDivElement>\n  ) => {\n    const { resize, isMaximized } = useContext(DialogContext);\n    const { ref, ...draggableProps } = useDraggable({\n      width,\n      height,\n      x,\n      y,\n      minWidth,\n      minHeight,\n      isMaximized,\n      boundaryTolerance,\n    });\n    const setPointerEvents = useSetPointerEvents(ref);\n\n    const [_, setElement] = useResize({\n      onResizeStart: setPointerEvents?.(\"none\"),\n      onResizeEnd: setPointerEvents?.(\"auto\"),\n    });\n\n    return (\n      <Primitive.Content\n        className={contentStyle({ className, css, resize })}\n        onDragStartCapture={setPointerEvents(\"none\")}\n        onDragEndCapture={setPointerEvents(\"auto\")}\n        {...draggableProps}\n        {...props}\n        ref={mergeRefs(forwardedRef, ref, setElement)}\n      >\n        {children}\n      </Primitive.Content>\n    );\n  }\n);\nContentContainer.displayName = \"ContentContainer\";\n\nexport const DialogContent = forwardRef(\n  (\n    props: ComponentProps<typeof ContentContainer>,\n    forwardedRef: Ref<HTMLDivElement>\n  ) => {\n    return (\n      <Primitive.Portal>\n        <Primitive.Overlay className={overlayStyle()} />\n        <ContentContainer {...props} ref={forwardedRef} />\n      </Primitive.Portal>\n    );\n  }\n);\nDialogContent.displayName = \"DialogContent\";\n\nconst titleSlotStyle = css({\n  // We put title at the bottom in DOM to make the close button last in the TAB order\n  // But visually we want it to be first\n  order: -1,\n});\n\nconst titleStyle = css({\n  display: \"flex\",\n  flexGrow: 1,\n  height: \"100%\",\n  alignItems: \"center\",\n});\n\nexport const DialogTitle = ({\n  children,\n  suffix,\n  ...rest\n}: ComponentProps<typeof PanelTitle> & {\n  suffix?: ReactNode;\n  closeLabel?: string;\n}) => {\n  const { draggable } = useContext(DialogContext);\n\n  return (\n    <div className={titleSlotStyle()}>\n      <PanelTitle {...rest} suffix={suffix ?? <DialogClose />}>\n        <Primitive.Title asChild>\n          <Text\n            draggable={draggable}\n            className={titleStyle()}\n            variant=\"titles\"\n            truncate\n          >\n            {children}\n          </Text>\n        </Primitive.Title>\n      </PanelTitle>\n      <Separator />\n    </div>\n  );\n};\n\nexport const DialogTitleActions = ({ children }: { children: ReactNode }) => {\n  return (\n    <Flex gap=\"1\" align=\"center\">\n      {children}\n    </Flex>\n  );\n};\n\nexport const DialogActions = ({ children }: { children: ReactNode }) => {\n  return (\n    <Flex\n      gap=\"2\"\n      css={{\n        padding: theme.panel.padding,\n        // Making sure the tab order is the last item first.\n        flexFlow: \"row-reverse\",\n      }}\n    >\n      {children}\n    </Flex>\n  );\n};\n\n// Styles specific to dialog\n// (as opposed to be common for all floating panels)\nconst overlayStyle = css({\n  backgroundColor: \"rgba(17, 24, 28, 0.66)\",\n  position: \"fixed\",\n  inset: 0,\n});\n\nconst contentStyle = css(panelStyle, {\n  position: \"fixed\",\n  width: \"min-content\",\n  height: \"min-content\",\n  minWidth: theme.sizes.sidebarWidth,\n  maxWidth: `calc(100vw - ${theme.spacing[15]})`,\n  maxHeight: `calc(100vh - ${theme.spacing[15]})`,\n  userSelect: \"none\",\n\n  overflow: \"hidden\",\n  variants: {\n    resize: {\n      both: {\n        resize: \"both\",\n      },\n      none: {\n        resize: \"none\",\n      },\n    },\n  },\n});\n\n// Export utilities for testing\nexport const __testing__ = {\n  calculateInset,\n  calculateCenteredPosition,\n  calculateDialogStyle,\n  applyBoundaries,\n};\n"
  },
  {
    "path": "packages/design-system/src/components/dropdown-menu.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { CopyIcon, TrashIcon } from \"@webstudio-is/icons\";\nimport { Flex } from \"./flex\";\nimport { Button } from \"./button\";\nimport { StorySection } from \"./storybook\";\nimport {\n  DropdownMenu as DropdownMenuComponent,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuCheckboxItem,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuArrow,\n} from \"./dropdown-menu\";\n\nexport default {\n  title: \"Dropdown Menu\",\n};\n\nexport const DropdownMenu = () => {\n  const [bold, setBold] = useState(true);\n  const [italic, setItalic] = useState(false);\n  return (\n    <StorySection title=\"Dropdown menu\">\n      <Flex gap=\"9\" css={{ padding: 100 }}>\n        <DropdownMenuComponent open>\n          <DropdownMenuTrigger asChild>\n            <Button>Items</Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent>\n            <DropdownMenuItem>New file</DropdownMenuItem>\n            <DropdownMenuItem>Open project</DropdownMenuItem>\n            <DropdownMenuSeparator />\n            <DropdownMenuItem>Settings</DropdownMenuItem>\n            <DropdownMenuItem disabled>Disabled item</DropdownMenuItem>\n          </DropdownMenuContent>\n        </DropdownMenuComponent>\n\n        <DropdownMenuComponent open>\n          <DropdownMenuTrigger asChild>\n            <Button>Checkboxes</Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent>\n            <DropdownMenuCheckboxItem checked={bold} onCheckedChange={setBold}>\n              Bold\n            </DropdownMenuCheckboxItem>\n            <DropdownMenuCheckboxItem\n              checked={italic}\n              onCheckedChange={setItalic}\n            >\n              Italic\n            </DropdownMenuCheckboxItem>\n          </DropdownMenuContent>\n        </DropdownMenuComponent>\n      </Flex>\n    </StorySection>\n  );\n};\n\nexport const WithIcons = () => (\n  <StorySection title=\"With icons\">\n    <DropdownMenuComponent defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <Button>With icons</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem icon={<CopyIcon />}>Copy</DropdownMenuItem>\n        <DropdownMenuItem icon={<TrashIcon />} destructive>\n          Delete\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenuComponent>\n  </StorySection>\n);\n\nexport const WithRadioGroup = () => {\n  const [value, setValue] = useState(\"one\");\n  return (\n    <StorySection title=\"With radio group\">\n      <DropdownMenuComponent defaultOpen>\n        <DropdownMenuTrigger asChild>\n          <Button>Radio group</Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent>\n          <DropdownMenuLabel>Choose one</DropdownMenuLabel>\n          <DropdownMenuRadioGroup value={value} onValueChange={setValue}>\n            <DropdownMenuRadioItem value=\"one\">\n              Option one\n            </DropdownMenuRadioItem>\n            <DropdownMenuRadioItem value=\"two\">\n              Option two\n            </DropdownMenuRadioItem>\n            <DropdownMenuRadioItem value=\"three\">\n              Option three\n            </DropdownMenuRadioItem>\n          </DropdownMenuRadioGroup>\n        </DropdownMenuContent>\n      </DropdownMenuComponent>\n    </StorySection>\n  );\n};\n\nexport const WithSubMenu = () => (\n  <StorySection title=\"With sub menu\">\n    <DropdownMenuComponent defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <Button>Sub menu</Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem>Item one</DropdownMenuItem>\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger>More options</DropdownMenuSubTrigger>\n          <DropdownMenuSubContent>\n            <DropdownMenuItem>Sub item one</DropdownMenuItem>\n            <DropdownMenuItem>Sub item two</DropdownMenuItem>\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n        <DropdownMenuSeparator />\n        <DropdownMenuItem>Item two</DropdownMenuItem>\n        <DropdownMenuArrow />\n      </DropdownMenuContent>\n    </DropdownMenuComponent>\n  </StorySection>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/dropdown-menu.tsx",
    "content": "import {\n  forwardRef,\n  useState,\n  useEffect,\n  type ComponentProps,\n  type ElementRef,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { ChevronRightIcon } from \"@webstudio-is/icons\";\nimport { styled } from \"../stitches.config\";\nimport { focusFirstCollectionItem } from \"../utilities\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport {\n  menuCss,\n  subMenuCss,\n  separatorCss,\n  menuItemCss,\n  labelCss,\n  menuItemIndicatorCss,\n  subContentProps,\n  MenuCheckedIcon,\n} from \"./menu\";\nexport { DropdownMenuArrow } from \"./menu\";\n\nexport const DropdownMenu = DropdownMenuPrimitive.Root;\n\nexport const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nexport const DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuContentStyled = styled(\n  DropdownMenuPrimitive.Content,\n  menuCss\n);\nexport const DropdownMenuContent = forwardRef<\n  ElementRef<typeof DropdownMenuContentStyled>,\n  ComponentProps<typeof DropdownMenuContentStyled> & { autoFocus?: boolean }\n>(({ autoFocus, ...props }, forwardedRef) => {\n  const [node, setNode] = useState<HTMLDivElement | null>(null);\n\n  useEffect(() => {\n    if (autoFocus && node) {\n      focusFirstCollectionItem(node);\n    }\n  }, [autoFocus, node]);\n\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuContentStyled\n        {...props}\n        ref={mergeRefs(forwardedRef, setNode)}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n});\nDropdownMenuContent.displayName = \"DropdownMenuContent\";\n\nconst SubContentStyled = styled(DropdownMenuPrimitive.SubContent, subMenuCss);\nexport const DropdownMenuSubContent = forwardRef<\n  ElementRef<typeof SubContentStyled>,\n  ComponentProps<typeof SubContentStyled>\n>((props, forwardedRef) => (\n  <SubContentStyled {...subContentProps} {...props} ref={forwardedRef} />\n));\nDropdownMenuSubContent.displayName = \"DropdownMenuSubContent\";\n\nexport const DropdownMenuSeparator = styled(\n  DropdownMenuPrimitive.Separator,\n  separatorCss\n);\n\nexport const DropdownMenuLabel = styled(DropdownMenuPrimitive.Label, labelCss);\n\nexport const StyledMenuItem = styled(DropdownMenuPrimitive.Item, menuItemCss, {\n  defaultVariants: { withIndicator: true },\n});\nexport const DropdownMenuItem = forwardRef<\n  ElementRef<typeof StyledMenuItem>,\n  ComponentProps<typeof StyledMenuItem> & { icon?: ReactNode }\n>(({ icon, children, withIndicator, ...props }, forwardedRef) =>\n  icon ? (\n    <StyledMenuItem\n      withIndicator={withIndicator || Boolean(icon)}\n      {...props}\n      ref={forwardedRef}\n    >\n      <div className={menuItemIndicatorCss()}>{icon}</div>\n      {children}\n    </StyledMenuItem>\n  ) : (\n    <StyledMenuItem\n      withIndicator={withIndicator || Boolean(icon)}\n      {...props}\n      ref={forwardedRef}\n    >\n      {children}\n    </StyledMenuItem>\n  )\n);\nDropdownMenuItem.displayName = \"DropdownMenuItem\";\n\nexport const DropdownMenuItemRightSlot = styled(\"span\", {\n  marginLeft: \"auto\",\n  display: \"flex\",\n});\n\nconst SubTriggerStyled = styled(DropdownMenuPrimitive.SubTrigger, menuItemCss, {\n  defaultVariants: { withIndicator: true },\n});\nexport const DropdownMenuSubTrigger = forwardRef<\n  ElementRef<typeof SubTriggerStyled>,\n  ComponentProps<typeof SubTriggerStyled> & { icon?: ReactNode }\n>(({ children, withIndicator, icon, ...props }, forwardedRef) => (\n  <SubTriggerStyled\n    withIndicator={withIndicator || Boolean(icon)}\n    {...props}\n    ref={forwardedRef}\n  >\n    {icon && <div className={menuItemIndicatorCss()}>{icon}</div>}\n    {children}\n    <DropdownMenuItemRightSlot>\n      <ChevronRightIcon />\n    </DropdownMenuItemRightSlot>\n  </SubTriggerStyled>\n));\nDropdownMenuSubTrigger.displayName = \"DropdownMenuSubTrigger\";\n\nconst Indicator = styled(\n  DropdownMenuPrimitive.ItemIndicator,\n  menuItemIndicatorCss\n);\n\nconst StyledRadioItem = styled(DropdownMenuPrimitive.RadioItem, menuItemCss);\nexport const DropdownMenuRadioItem = forwardRef<\n  ElementRef<typeof StyledRadioItem>,\n  ComponentProps<typeof StyledRadioItem> & { icon?: ReactElement }\n>(({ children, icon, ...props }, forwardedRef) => (\n  <StyledRadioItem\n    withIndicator={icon !== undefined}\n    {...props}\n    ref={forwardedRef}\n  >\n    {icon !== undefined && <Indicator>{icon}</Indicator>}\n    {children}\n  </StyledRadioItem>\n));\nDropdownMenuRadioItem.displayName = \"DropdownMenuRadioItem\";\n\nconst StyledCheckboxItem = styled(\n  DropdownMenuPrimitive.CheckboxItem,\n  menuItemCss\n);\nexport const DropdownMenuCheckboxItem = forwardRef<\n  ElementRef<typeof StyledCheckboxItem>,\n  ComponentProps<typeof StyledCheckboxItem> & { icon?: ReactNode }\n>(({ children, icon = <MenuCheckedIcon />, ...props }, forwardedRef) => (\n  <StyledCheckboxItem withIndicator {...props} ref={forwardedRef}>\n    <Indicator>{icon}</Indicator>\n    {children}\n  </StyledCheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName = \"DropdownMenuCheckboxItem\";\n\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\n"
  },
  {
    "path": "packages/design-system/src/components/enhanced-tooltip.stories.tsx",
    "content": "import {\n  EnhancedTooltip as EnhancedTooltipComponent,\n  EnhancedTooltipProvider,\n} from \"./enhanced-tooltip\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\nimport { InputField } from \"./input-field\";\nimport { StorySection } from \"./storybook\";\nimport { Button } from \"./button\";\n\nexport default {\n  title: \"Enhanced Tooltip\",\n  component: EnhancedTooltipComponent,\n};\n\nexport const EnhancedTooltip = () => (\n  <EnhancedTooltipProvider>\n    <StorySection title=\"Default (open)\">\n      <Flex css={{ padding: 40 }}>\n        <EnhancedTooltipComponent content=\"Hello world\" defaultOpen>\n          <InputField />\n        </EnhancedTooltipComponent>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"With JSX content\">\n      <Flex css={{ padding: 40 }}>\n        <EnhancedTooltipComponent\n          content={\n            <Flex direction=\"column\" gap=\"1\">\n              <Text variant=\"labels\">Tooltip title</Text>\n              <Text variant=\"small\" color=\"moreSubtle\">\n                Additional description text\n              </Text>\n            </Flex>\n          }\n          defaultOpen\n        >\n          <InputField placeholder=\"Hover me\" />\n        </EnhancedTooltipComponent>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Side placements\">\n      <Flex gap=\"6\" css={{ padding: 60 }}>\n        <EnhancedTooltipComponent content=\"Top\" side=\"top\" defaultOpen>\n          <Button>Top</Button>\n        </EnhancedTooltipComponent>\n        <EnhancedTooltipComponent content=\"Right\" side=\"right\" defaultOpen>\n          <Button>Right</Button>\n        </EnhancedTooltipComponent>\n        <EnhancedTooltipComponent content=\"Bottom\" side=\"bottom\" defaultOpen>\n          <Button>Bottom</Button>\n        </EnhancedTooltipComponent>\n        <EnhancedTooltipComponent content=\"Left\" side=\"left\" defaultOpen>\n          <Button>Left</Button>\n        </EnhancedTooltipComponent>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Custom delay\">\n      <Flex css={{ padding: 40 }}>\n        <EnhancedTooltipComponent\n          content=\"Shows after 500ms\"\n          delayDuration={500}\n        >\n          <InputField placeholder=\"Short delay (500ms)\" />\n        </EnhancedTooltipComponent>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Alignment options\">\n      <Flex gap=\"6\" css={{ padding: 60 }}>\n        <EnhancedTooltipComponent\n          content=\"Aligned start\"\n          align=\"start\"\n          defaultOpen\n        >\n          <Button>Start</Button>\n        </EnhancedTooltipComponent>\n        <EnhancedTooltipComponent\n          content=\"Aligned center\"\n          align=\"center\"\n          defaultOpen\n        >\n          <Button>Center</Button>\n        </EnhancedTooltipComponent>\n        <EnhancedTooltipComponent content=\"Aligned end\" align=\"end\" defaultOpen>\n          <Button>End</Button>\n        </EnhancedTooltipComponent>\n      </Flex>\n    </StorySection>\n  </EnhancedTooltipProvider>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/enhanced-tooltip.tsx",
    "content": "import {\n  type Ref,\n  type ComponentProps,\n  type FocusEvent,\n  forwardRef,\n  useRef,\n  useState,\n  createContext,\n  useContext,\n  useMemo,\n} from \"react\";\nimport { Tooltip, type TooltipProps } from \"./tooltip\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { useDebouncedCallback } from \"use-debounce\";\n\nconst EnhancedTooltipContext = createContext<\n  Omit<ComponentProps<typeof TooltipPrimitive.TooltipProvider>, \"children\">\n>({});\n\nconst DEFAULT_DELAY_DURATION = 1600;\n\n/**\n * To have the ability to access properties from TooltipPrimitive.TooltipProvider at EnhancedTooltip\n **/\nexport const EnhancedTooltipProvider = ({\n  delayDuration = DEFAULT_DELAY_DURATION,\n  skipDelayDuration = 0,\n  disableHoverableContent = false,\n  children,\n}: ComponentProps<typeof TooltipPrimitive.TooltipProvider>) => {\n  const contextValue = useMemo(\n    () => ({\n      delayDuration,\n      skipDelayDuration,\n      disableHoverableContent,\n    }),\n    [delayDuration, disableHoverableContent, skipDelayDuration]\n  );\n\n  return (\n    <EnhancedTooltipContext.Provider value={contextValue}>\n      <TooltipPrimitive.TooltipProvider {...contextValue}>\n        {children}\n      </TooltipPrimitive.TooltipProvider>\n    </EnhancedTooltipContext.Provider>\n  );\n};\n\nexport const useEnhancedTooltipProps = () => useContext(EnhancedTooltipContext);\n\n/**\n * EnhancedTooltip has the following differences from the radix-ui\n * 1. Don't show the tooltip if any click or key-down was made inside Tooltip.Trigger\n * 2. Show Tooltip on focus after the delay\n **/\nexport const EnhancedTooltip = forwardRef(\n  (\n    props: Omit<TooltipProps, \"open\" | \"onOpenChange\">,\n    _ref: Ref<HTMLDivElement>\n  ) => {\n    const [open, setOpen] = useState(props.defaultOpen ?? false);\n    const context = useContext(EnhancedTooltipContext);\n\n    /**\n     * Used to skip multiple focus/blur calls\n     */\n    const isFocusedRef = useRef(false);\n\n    /**\n     * Disable opening if any interaction with control occurred\n     */\n    const allowOpenRef = useRef(true);\n\n    const handleDisableOpen = () => {\n      allowOpenRef.current = false;\n      setOpen(false);\n      showTooltipDelayed.cancel();\n    };\n\n    const showTooltipDelayed = useDebouncedCallback(\n      () => {\n        if (allowOpenRef.current) {\n          setOpen(true);\n        }\n      },\n      props.delayDuration ?? context.delayDuration ?? DEFAULT_DELAY_DURATION,\n      { leading: false }\n    );\n\n    // We debounce blur/focus events in a single function, to skip fast `blur/focus` which can occur during select menu hovers\n    const handleFocusEventsDebounced = useDebouncedCallback(\n      (eventType: \"focus\" | \"blur\" | \"mouseLeave\") => {\n        if (eventType === \"blur\") {\n          isFocusedRef.current = false;\n          allowOpenRef.current = true;\n          setOpen(false);\n          showTooltipDelayed.cancel();\n          return;\n        }\n\n        if (eventType === \"mouseLeave\") {\n          allowOpenRef.current = true;\n          isFocusedRef.current = true;\n          return;\n        }\n\n        if (isFocusedRef.current === false) {\n          showTooltipDelayed();\n        }\n\n        isFocusedRef.current = true;\n      },\n      0,\n      { leading: false }\n    );\n\n    const triggerProps = {\n      onFocus: (event: FocusEvent<HTMLElement>) => {\n        handleFocusEventsDebounced(\"focus\");\n        event.preventDefault();\n      },\n      onBlur: (event: FocusEvent<HTMLElement>) => {\n        handleFocusEventsDebounced(\"blur\");\n        event.preventDefault();\n      },\n      onPointerDown: () => {\n        handleDisableOpen();\n      },\n      onKeyDown: () => {\n        handleDisableOpen();\n      },\n      /*\n        When onKeyDown or onPointerDown events occur, the tooltips are disabled.\n        Inorder not to show them when the drag operation is under progress.\n        However, there is a scenario in which users press keyDown,\n        drag the mouse, and leaves the icon and then leaves the drag on the input. (e.g., columnGap and rowGap handlers).\n        In this case, it is necessary to reset and enable the tooltips to appear again when\n        users hover over the icon after completing the drag operation. Without this reset,\n        the tooltips may not appear if the icon is revisited following a keyDown and drag operation.\n        Because showTooltipDelayed.cancel() is called in handleDisableOpen, or onKeyDown and onPointerDown events.\n      */\n      onMouseLeave: (event: React.MouseEvent<HTMLElement>) => {\n        handleFocusEventsDebounced(\"mouseLeave\");\n        event.preventDefault();\n      },\n    };\n\n    const onOpenChange = (isOpen: boolean) => {\n      if (allowOpenRef.current) {\n        setOpen(isOpen);\n      }\n    };\n\n    // Wrap in provider so each control will use own hover delay\n    return (\n      <Tooltip\n        {...props}\n        open={open}\n        onOpenChange={onOpenChange}\n        triggerProps={triggerProps}\n      />\n    );\n  }\n);\n\nEnhancedTooltip.displayName = \"EnhancedTooltip\";\n"
  },
  {
    "path": "packages/design-system/src/components/flex.tsx",
    "content": "import { styled } from \"../stitches.config\";\nimport { theme } from \"../stitches.config\";\n\nexport const Flex = styled(\"div\", {\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  // Fixes a bug where container doesn't want to grow.\n  minHeight: 0,\n  minWidth: 0,\n  variants: {\n    direction: {\n      row: {\n        flexDirection: \"row\",\n      },\n      column: {\n        flexDirection: \"column\",\n      },\n      rowReverse: {\n        flexDirection: \"row-reverse\",\n      },\n      columnReverse: {\n        flexDirection: \"column-reverse\",\n      },\n    },\n    align: {\n      start: {\n        alignItems: \"flex-start\",\n      },\n      center: {\n        alignItems: \"center\",\n      },\n      end: {\n        alignItems: \"flex-end\",\n      },\n      stretch: {\n        alignItems: \"stretch\",\n      },\n      baseline: {\n        alignItems: \"baseline\",\n      },\n    },\n    alignSelf: {\n      start: {\n        alignSelf: \"flex-start\",\n      },\n      center: {\n        alignSelf: \"center\",\n      },\n      end: {\n        alignSelf: \"flex-end\",\n      },\n      stretch: {\n        alignSelf: \"stretch\",\n      },\n      baseline: {\n        alignSelf: \"baseline\",\n      },\n    },\n    justify: {\n      start: {\n        justifyContent: \"flex-start\",\n      },\n      center: {\n        justifyContent: \"center\",\n      },\n      end: {\n        justifyContent: \"flex-end\",\n      },\n      between: {\n        justifyContent: \"space-between\",\n      },\n      around: {\n        justifyContent: \"space-around\",\n      },\n    },\n    wrap: {\n      noWrap: {\n        flexWrap: \"nowrap\",\n      },\n      wrap: {\n        flexWrap: \"wrap\",\n      },\n      wrapReverse: {\n        flexWrap: \"wrap-reverse\",\n      },\n    },\n    gap: {\n      1: {\n        gap: theme.spacing[3],\n      },\n      2: {\n        gap: theme.spacing[5],\n      },\n      3: {\n        gap: theme.spacing[9],\n      },\n      4: {\n        gap: theme.spacing[10],\n      },\n      5: {\n        gap: theme.spacing[11],\n      },\n      6: {\n        gap: theme.spacing[13],\n      },\n      7: {\n        gap: theme.spacing[17],\n      },\n      8: {\n        gap: theme.spacing[19],\n      },\n      9: {\n        gap: theme.spacing[20],\n      },\n    },\n    shrink: {\n      true: {\n        flexShrink: 1,\n      },\n      false: {\n        flexShrink: 0,\n      },\n    },\n    grow: {\n      true: {\n        flexGrow: 1,\n      },\n      false: {\n        flexGrow: 0,\n      },\n    },\n  },\n  defaultVariants: {\n    direction: \"row\",\n    align: \"stretch\",\n    justify: \"start\",\n    wrap: \"noWrap\",\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/floating-panel.stories.tsx",
    "content": "import { useRef, type ReactNode } from \"react\";\nimport { FloatingPanel as FloatingPanelComponent } from \"./floating-panel\";\nimport { Box } from \"./box\";\nimport { StorySection } from \"./storybook\";\nimport { Button } from \"./button\";\nimport { Text } from \"./text\";\nimport { CopyIcon } from \"@webstudio-is/icons\";\n\nexport default {\n  title: \"Floating Panel\",\n  component: FloatingPanelComponent,\n};\n\nconst Container = ({ children }: { children: ReactNode }) => {\n  const ref = useRef(null);\n  return (\n    <Box\n      ref={ref}\n      data-floating-panel-container\n      css={{\n        display: \"inline-block\",\n        marginLeft: 300,\n        padding: 100,\n        border: `1px solid black`,\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nexport const LeftDefault = () => (\n  <StorySection title=\"Left (default)\">\n    <Container>\n      <FloatingPanelComponent\n        open\n        title=\"Left (default)\"\n        content={<Text>Content</Text>}\n      >\n        <Button>Open on the left</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n\nexport const RightStart = () => (\n  <StorySection title=\"Right start\">\n    <Container>\n      <FloatingPanelComponent\n        placement=\"right-start\"\n        open\n        title=\"Right\"\n        content={<Text>Content</Text>}\n      >\n        <Button>Open on the right</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n\nexport const BottomWithin = () => (\n  <StorySection title=\"Bottom within\">\n    <Container>\n      <FloatingPanelComponent\n        placement=\"bottom-within\"\n        open\n        title=\"Bottom\"\n        content={<Text>Content</Text>}\n      >\n        <Button>Open below</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n\nexport const Center = () => (\n  <StorySection title=\"Center\">\n    <Container>\n      <FloatingPanelComponent\n        placement=\"center\"\n        open\n        maximizable\n        resize=\"both\"\n        title=\"Center\"\n        content={<Text>Content</Text>}\n      >\n        <Button>Open screen-centered</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n\nexport const CustomOffsetAndSize = () => (\n  <StorySection title=\"Custom offset and size\">\n    <Container>\n      <FloatingPanelComponent\n        offset={{ mainAxis: 20, alignmentAxis: -100 }}\n        width={200}\n        height={300}\n        open\n        title=\"Custom Offset & Size\"\n        content={<Text>Content</Text>}\n      >\n        <Button>Open with custom offsets</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n\nexport const WithTitleSuffix = () => (\n  <StorySection title=\"With title suffix\">\n    <Container>\n      <FloatingPanelComponent\n        open\n        title=\"With suffix\"\n        titleSuffix={<Button color=\"ghost\" prefix={<CopyIcon />} />}\n        content={<Text>Panel with a custom title suffix button</Text>}\n      >\n        <Button>Open with title suffix</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n\nexport const PersistentPanel = () => (\n  <StorySection title=\"Persistent panel\">\n    <Container>\n      <FloatingPanelComponent\n        open\n        title=\"Persistent\"\n        closeOnInteractOutside={false}\n        content={<Text>This panel stays open when clicking outside</Text>}\n      >\n        <Button>Open persistent</Button>\n      </FloatingPanelComponent>\n    </Container>\n  </StorySection>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/floating-panel.tsx",
    "content": "import {\n  type ReactNode,\n  type ComponentProps,\n  useState,\n  useRef,\n  useLayoutEffect,\n} from \"react\";\nimport { css, theme } from \"../stitches.config\";\nimport {\n  Dialog,\n  DialogTitleActions,\n  DialogClose,\n  DialogContent,\n  DialogTitle,\n  DialogTrigger,\n  DialogMaximize,\n} from \"./dialog\";\n\ntype OffsetOptions =\n  | number\n  | { mainAxis?: number; crossAxis?: number; alignmentAxis?: number | null };\n\nconst computeFloatingPosition = (\n  trigger: HTMLElement,\n  floating: HTMLElement,\n  container: HTMLElement,\n  placement: \"left-start\" | \"right-start\" | \"bottom-within\",\n  offsetOptions: OffsetOptions\n): { x: number; y: number } => {\n  const triggerRect = trigger.getBoundingClientRect();\n  const floatingRect = floating.getBoundingClientRect();\n  const containerRect = container.getBoundingClientRect();\n\n  const mainAxis =\n    typeof offsetOptions === \"number\"\n      ? offsetOptions\n      : (offsetOptions.mainAxis ?? 0);\n  const crossAxis =\n    typeof offsetOptions === \"number\" ? 0 : (offsetOptions.crossAxis ?? 0);\n  const alignmentAxis =\n    typeof offsetOptions === \"number\"\n      ? null\n      : (offsetOptions.alignmentAxis ?? null);\n\n  let x = 0;\n  let y = 0;\n\n  if (placement === \"left-start\") {\n    // Position to the left of the container, aligned with the top of trigger\n    x = containerRect.left - floatingRect.width + mainAxis;\n    // Align panel top with trigger top\n    y = triggerRect.top + (alignmentAxis ?? 0);\n    // Apply crossAxis offset (moves vertically)\n    y += crossAxis;\n  } else if (placement === \"right-start\") {\n    // Position to the right of the container, aligned with the top of trigger\n    x = containerRect.right + mainAxis;\n    // Align panel top with trigger top, using trigger's relative position within container\n    y = triggerRect.top + (alignmentAxis ?? 0);\n    // Apply crossAxis offset (moves vertically)\n    y += crossAxis;\n  } else if (placement === \"bottom-within\") {\n    // Position below the trigger, centered horizontally within the container\n    // Center the panel horizontally within the container\n    x =\n      containerRect.left +\n      (containerRect.width - floatingRect.width) / 2 +\n      (alignmentAxis ?? 0);\n    // Apply crossAxis offset (moves horizontally)\n    x += crossAxis;\n    // Y: below the trigger with 5px default offset\n    y = triggerRect.bottom + (mainAxis === 0 ? 5 : mainAxis);\n  }\n\n  // Keep within viewport bounds (simple shift)\n  const viewportWidth = window.innerWidth;\n  const viewportHeight = window.innerHeight;\n\n  // Adjust horizontal position if needed\n  if (x < 0) {\n    x = 0;\n  } else if (x + floatingRect.width > viewportWidth) {\n    x = viewportWidth - floatingRect.width;\n  }\n\n  // Adjust vertical position if needed\n  if (y < 0) {\n    y = 0;\n  } else if (y + floatingRect.height > viewportHeight) {\n    y = viewportHeight - floatingRect.height;\n  }\n\n  return { x, y };\n};\n\nconst contentFitsAtPosition = (\n  contentElement: HTMLElement,\n  position: { x: number; y: number }\n): boolean => {\n  const rect = contentElement.getBoundingClientRect();\n  const { x, y } = position;\n  return (\n    y >= 0 &&\n    y + rect.height <= window.innerHeight &&\n    x >= 0 &&\n    x + rect.width <= window.innerWidth\n  );\n};\n\ntype FloatingPanelProps = {\n  title?: ReactNode;\n  content: ReactNode;\n  children: ReactNode;\n  titleSuffix?: ReactNode;\n  maximizable?: boolean;\n  resize?: ComponentProps<typeof Dialog>[\"resize\"];\n  width?: number;\n  height?: number;\n  // - bottom-within - below the trigger button, within container bounds\n  // - left-start - on the left side relative to the container, aligned with the top of the trigger button\n  // - center - center of the screen\n  placement?: \"left-start\" | \"right-start\" | \"center\" | \"bottom-within\";\n  offset?: OffsetOptions;\n  open?: boolean;\n  onOpenChange?: (isOpen: boolean) => void;\n  /** When false, the panel won't close when clicking outside it. */\n  closeOnInteractOutside?: boolean;\n};\n\nconst contentStyle = css({\n  width: theme.sizes.sidebarWidth,\n  overflow: \"auto\",\n});\n\nconst defaultOffset: OffsetOptions = { mainAxis: 0, crossAxis: 0 };\n\nexport const FloatingPanel = ({\n  title,\n  content,\n  children,\n  titleSuffix,\n  resize,\n  maximizable,\n  width,\n  height,\n  placement = \"left-start\",\n  offset: offsetProp = defaultOffset,\n  open: openProp,\n  onOpenChange,\n  closeOnInteractOutside = true,\n}: FloatingPanelProps) => {\n  // Support both controlled and uncontrolled modes\n  const [internalOpen, setInternalOpen] = useState(false);\n  const open = openProp ?? internalOpen;\n\n  const [contentElement, setContentElement] = useState<HTMLDivElement | null>(\n    null\n  );\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const [position, setPosition] = useState<{ x: number; y: number }>();\n  const currentPositionRef = useRef<{ x: number; y: number }>();\n  const maxHeightRef = useRef<number | undefined>();\n  const containerRef = useRef<HTMLElement | null>(null);\n\n  // Wrap onOpenChange to reset position when panel closes\n  const handleOpenChange = (isOpen: boolean) => {\n    if (isOpen === false) {\n      currentPositionRef.current = undefined;\n      setPosition(undefined);\n      maxHeightRef.current = undefined;\n      containerRef.current = null;\n    }\n    // Update internal state if uncontrolled\n    if (openProp === undefined) {\n      setInternalOpen(isOpen);\n    }\n    onOpenChange?.(isOpen);\n  };\n\n  // Reset position tracking when panel closes via open prop\n  useLayoutEffect(() => {\n    if (open === false) {\n      currentPositionRef.current = undefined;\n      setPosition(undefined);\n      maxHeightRef.current = undefined;\n      containerRef.current = null;\n    }\n  }, [open]);\n\n  useLayoutEffect(() => {\n    // Find container when trigger is available and panel is opening\n    if (triggerRef.current && open && !containerRef.current) {\n      const container = triggerRef.current.closest(\n        \"[data-floating-panel-container]\"\n      ) as HTMLElement | null;\n      containerRef.current = container;\n    }\n\n    if (\n      triggerRef.current === null ||\n      containerRef.current === null ||\n      contentElement === null ||\n      // When centering the dialog, we don't need to calculate the position\n      placement === \"center\" ||\n      // Don't recalculate position when panel is closed\n      open === false\n    ) {\n      return;\n    }\n\n    const updatePosition = () => {\n      if (\n        triggerRef.current === null ||\n        containerRef.current === null ||\n        contentElement === null\n      ) {\n        return;\n      }\n\n      // Set initial position once when panel opens\n      if (!currentPositionRef.current) {\n        const { x, y } = computeFloatingPosition(\n          triggerRef.current,\n          contentElement,\n          containerRef.current,\n          placement,\n          offsetProp\n        );\n        currentPositionRef.current = { x, y };\n\n        // Calculate max height based on container bounds\n        const containerRect = containerRef.current.getBoundingClientRect();\n        const availableHeight = containerRect.bottom - y - 10; // 10px padding\n        if (availableHeight > 0) {\n          maxHeightRef.current = availableHeight;\n          contentElement.style.maxHeight = `${availableHeight}px`;\n        }\n\n        setPosition({ x, y });\n        return;\n      }\n\n      // Only recalculate if content doesn't fit at current position\n      const fits = contentFitsAtPosition(\n        contentElement,\n        currentPositionRef.current\n      );\n\n      if (fits) {\n        return;\n      }\n      const { x, y } = computeFloatingPosition(\n        triggerRef.current,\n        contentElement,\n        containerRef.current,\n        placement,\n        offsetProp\n      );\n      currentPositionRef.current = { x, y };\n\n      // Only update state if position actually changed\n      setPosition((current) => {\n        if (current && current.x === x && current.y === y) {\n          return current;\n        }\n        return { x, y };\n      });\n    };\n\n    // Calculate initial position or check if it still fits\n    updatePosition();\n\n    // Observe content size changes and update position\n    const resizeObserver = new ResizeObserver(() => {\n      updatePosition();\n    });\n\n    resizeObserver.observe(contentElement);\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [contentElement, containerRef, placement, offsetProp, open]);\n\n  return (\n    <Dialog\n      draggable\n      resize={resize}\n      open={open}\n      modal={false}\n      onOpenChange={handleOpenChange}\n    >\n      <DialogTrigger asChild ref={triggerRef}>\n        {children}\n      </DialogTrigger>\n      <DialogContent\n        className={contentStyle()}\n        width={width}\n        height={height}\n        {...position}\n        boundaryTolerance={\n          placement === \"bottom-within\" ? { horizontal: Infinity } : undefined\n        }\n        aria-describedby={undefined}\n        ref={setContentElement}\n        onInteractOutside={(event) => {\n          // When a dialog is centered, we don't want to close it when clicking outside\n          // This allows having inline and left positioned dialogs open at the same time as a centered dialog,\n          // while not allowing having multiple non-center positioned dialogs open at the same time.\n          if (placement === \"center\" || closeOnInteractOutside === false) {\n            event.preventDefault();\n          }\n        }}\n        onEscapeKeyDown={(event) => {\n          if (event.target instanceof HTMLInputElement) {\n            event.preventDefault();\n\n            return;\n          }\n        }}\n      >\n        {content}\n        {typeof title === \"string\" ? (\n          <DialogTitle\n            suffix={\n              <DialogTitleActions>\n                {titleSuffix}\n                {maximizable && <DialogMaximize />}\n                <DialogClose />\n              </DialogTitleActions>\n            }\n          >\n            {title}\n          </DialogTitle>\n        ) : (\n          title\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/focus-ring.ts",
    "content": "import { theme, type CSS } from \"../stitches.config\";\n\nexport const focusRingStyle = (props?: CSS) => ({\n  \"&::after\": {\n    content: '\"\"',\n    position: \"absolute\",\n    inset: theme.spacing[3],\n    outlineWidth: 1,\n    outlineStyle: \"solid\",\n    outlineColor: theme.colors.borderFocus,\n    borderRadius: theme.borderRadius[3],\n    pointerEvents: \"none\",\n    ...props,\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/gradient-picker.stories.tsx",
    "content": "import {\n  parseLinearGradient,\n  formatLinearGradient,\n  type ParsedLinearGradient,\n} from \"@webstudio-is/css-data\";\nimport { useState } from \"react\";\nimport { GradientPicker as GradientPickerComponent } from \"./gradient-picker\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Gradient Picker\",\n};\n\nconst parseLinearGradientOrThrow = (\n  gradientString: string\n): ParsedLinearGradient => {\n  const parsed = parseLinearGradient(gradientString);\n  if (parsed === undefined) {\n    throw new Error(`Invalid gradient: ${gradientString}`);\n  }\n  return parsed;\n};\n\nconst GradientVariant = ({\n  label,\n  initial,\n}: {\n  label: string;\n  initial: string;\n}) => {\n  const [gradient, setGradient] = useState<ParsedLinearGradient>(() =>\n    parseLinearGradientOrThrow(initial)\n  );\n  return (\n    <Flex direction=\"column\" gap=\"2\">\n      <Text variant=\"labels\">{label}</Text>\n      <GradientPickerComponent\n        gradient={gradient}\n        backgroundImage={formatLinearGradient(gradient)}\n        onChange={setGradient}\n        onChangeComplete={setGradient}\n        onThumbSelect={() => {}}\n      />\n      <Text>{formatLinearGradient(gradient)}</Text>\n    </Flex>\n  );\n};\n\nexport const GradientPicker = () => {\n  const [gradient, setGradient] = useState<ParsedLinearGradient>(() =>\n    parseLinearGradientOrThrow(\n      \"linear-gradient(90deg, red 0%, green 50%, blue 100%)\"\n    )\n  );\n  return (\n    <>\n      <StorySection title=\"Variants\">\n        <Flex direction=\"column\" gap=\"6\">\n          <GradientVariant\n            label=\"Simple (90deg)\"\n            initial=\"linear-gradient(90deg, black 0%, white 100%)\"\n          />\n          <GradientVariant\n            label=\"Angle + Hints\"\n            initial=\"linear-gradient(145deg, #ff00fa 0%, #00f497 34% 34%, #ffa800 56% 56%, #00eaff 100%)\"\n          />\n          <GradientVariant\n            label=\"Side or Corner\"\n            initial=\"linear-gradient(to left top, blue 0%, red 100%)\"\n          />\n        </Flex>\n      </StorySection>\n\n      <StorySection title=\"With selected stop\">\n        <Flex direction=\"column\" gap=\"2\">\n          <Text variant=\"labels\">Pre-selected middle stop (index 1)</Text>\n          <GradientPickerComponent\n            gradient={gradient}\n            backgroundImage={formatLinearGradient(gradient)}\n            onChange={setGradient}\n            onChangeComplete={setGradient}\n            onThumbSelect={() => {}}\n            selectedStopIndex={1}\n          />\n          <Text>{formatLinearGradient(gradient)}</Text>\n        </Flex>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/gradient-picker.tsx",
    "content": "import { clamp } from \"@react-aria/utils\";\nimport {\n  toValue,\n  type RgbValue,\n  type StyleValue,\n} from \"@webstudio-is/css-engine\";\nimport {\n  useState,\n  useCallback,\n  useRef,\n  useEffect,\n  useMemo,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PointerEvent as ReactPointerEvent,\n} from \"react\";\nimport type {\n  GradientColorValue,\n  GradientStop,\n  ParsedGradient,\n} from \"@webstudio-is/css-data\";\nimport * as colorjs from \"colorjs.io/fn\";\nimport { ChevronFilledUpIcon } from \"@webstudio-is/icons\";\nimport { styled, theme } from \"../stitches.config\";\nimport { Flex } from \"./flex\";\nimport { Box } from \"./box\";\nimport { ColorPickerPopover, ColorThumb } from \"./color-picker\";\n\n// Helper to mix two RGB colors\nconst mixColors = (\n  color1: RgbValue,\n  color2: RgbValue,\n  ratio: number\n): RgbValue => {\n  const c1: colorjs.ColorConstructor = {\n    spaceId: \"srgb\",\n    coords: [\n      (color1.r ?? 0) / 255,\n      (color1.g ?? 0) / 255,\n      (color1.b ?? 0) / 255,\n    ],\n    alpha: undefined,\n  };\n  const c2: colorjs.ColorConstructor = {\n    spaceId: \"srgb\",\n    coords: [\n      (color2.r ?? 0) / 255,\n      (color2.g ?? 0) / 255,\n      (color2.b ?? 0) / 255,\n    ],\n    alpha: undefined,\n  };\n  const mixed = colorjs.mix(c1, c2, ratio);\n  const [r, g, b] = mixed.coords;\n  return {\n    type: \"rgb\",\n    r: (r ?? 0) * 255,\n    g: (g ?? 0) * 255,\n    b: (b ?? 0) * 255,\n    alpha: color1.alpha ?? 1,\n  };\n};\n\nexport type GradientPickerProps<T extends ParsedGradient = ParsedGradient> = {\n  gradient: T;\n  backgroundImage: string;\n  onChange: (value: T) => void;\n  onChangeComplete: (value: T) => void;\n  onThumbSelect: (index: number, stop: GradientStop) => void;\n  selectedStopIndex?: number;\n  type?: T[\"type\"];\n};\n\nconst THUMB_INTERACTION_DISTANCE = 12;\nconst DRAG_THRESHOLD = 3;\nconst SLIDER_HEIGHT = 16;\nconst THUMB_HEIGHT = 14;\n\nconst defaultStopColor: RgbValue = {\n  type: \"rgb\",\n  r: 0,\n  g: 0,\n  b: 0,\n  alpha: 1,\n};\n\nconst toRgbColor = (\n  color: GradientStop[\"color\"] | undefined\n): RgbValue | undefined => {\n  if (color === undefined) {\n    return;\n  }\n\n  if (color.type === \"rgb\") {\n    return color;\n  }\n\n  try {\n    const parsed = colorjs.parse(toValue(color));\n    const [r, g, b] = parsed.coords;\n    const alpha = parsed.alpha;\n    return {\n      type: \"rgb\",\n      r: (r ?? 0) * 255,\n      g: (g ?? 0) * 255,\n      b: (b ?? 0) * 255,\n      alpha: alpha ?? 1,\n    };\n  } catch {\n    return;\n  }\n};\n\nconst computePositionFromClientX = (\n  sliderElement: HTMLElement,\n  clientX: number\n) => {\n  const rect = sliderElement.getBoundingClientRect();\n  if (rect.width === 0) {\n    return 0;\n  }\n  const relativePosition = clientX - rect.left;\n  return clamp(Math.round((relativePosition / rect.width) * 100), 0, 100);\n};\n\nconst getStopPositionValue = (stop: GradientStop): number => {\n  return stop.position?.type === \"unit\" ? stop.position.value : 0;\n};\n\nconst createPercentUnit = (value: number) => ({\n  type: \"unit\" as const,\n  unit: \"%\" as const,\n  value,\n});\n\nconst createDragHandler = (threshold: number = DRAG_THRESHOLD) => {\n  let pointerAbortController: AbortController | undefined;\n  let clickAbortController: AbortController | undefined;\n\n  const handlePointerDown = (options: {\n    event: ReactPointerEvent<HTMLDivElement>;\n    onDragMove: (position: number) => void;\n    onDragStart?: () => void;\n    onDragEnd?: () => void;\n  }) => {\n    const { event } = options;\n    const pointerId = event.pointerId;\n    const target = event.currentTarget as HTMLDivElement;\n    const startX = event.clientX;\n    let hasDragged = false;\n\n    // Find the slider element using closest with data attribute\n    const sliderElement = target.closest(\"[data-gradient-slider]\") as\n      | HTMLElement\n      | undefined;\n    if (sliderElement === undefined) {\n      return;\n    }\n\n    // Abort any previous drag session that might still be active\n    cleanup();\n\n    // Create a new abort controller for this drag session\n    pointerAbortController = new AbortController();\n    const pointerSignal = pointerAbortController.signal;\n\n    // Create a separate abort controller for the click handler\n    clickAbortController = new AbortController();\n    const clickSignal = clickAbortController.signal;\n\n    // Capture pointer immediately if threshold is 0 (e.g., for hints)\n    if (threshold === 0) {\n      event.preventDefault();\n      target.setPointerCapture(pointerId);\n    }\n\n    const handlePointerMove = (moveEvent: Event) => {\n      if (!(moveEvent instanceof PointerEvent)) {\n        return;\n      }\n      if (!hasDragged) {\n        if (Math.abs(moveEvent.clientX - startX) <= threshold) {\n          return;\n        }\n        hasDragged = true;\n        if (threshold !== 0) {\n          target.setPointerCapture(pointerId);\n        }\n        options.onDragStart?.();\n      }\n      const newPosition = computePositionFromClientX(\n        sliderElement,\n        moveEvent.clientX\n      );\n      options.onDragMove(newPosition);\n    };\n\n    // Prevent click event from firing if drag occurred\n    // Use document-level listener to catch clicks even if they happen outside the target\n    const handleClick = (event: Event) => {\n      if (hasDragged) {\n        event.preventDefault();\n        event.stopPropagation();\n      }\n      // Cleanup after click fires\n      clickAbortController?.abort();\n      clickAbortController = undefined;\n    };\n    document.addEventListener(\"click\", handleClick, {\n      capture: true,\n      signal: clickSignal,\n    });\n\n    const handlePointerUp = () => {\n      if (target.hasPointerCapture(pointerId)) {\n        target.releasePointerCapture(pointerId);\n      }\n      if (hasDragged) {\n        options.onDragEnd?.();\n      }\n      // Don't abort immediately - click event needs to fire first\n      // Defer cleanup to allow click event to be processed\n      setTimeout(() => {\n        pointerAbortController?.abort();\n        pointerAbortController = undefined;\n      }, 0);\n    };\n\n    document.addEventListener(\"pointermove\", handlePointerMove, {\n      signal: pointerSignal,\n    });\n    document.addEventListener(\"pointerup\", handlePointerUp, {\n      signal: pointerSignal,\n    });\n    document.addEventListener(\"pointercancel\", handlePointerUp, {\n      signal: pointerSignal,\n    });\n  };\n\n  const cleanup = () => {\n    pointerAbortController?.abort();\n    pointerAbortController = undefined;\n    clickAbortController?.abort();\n    clickAbortController = undefined;\n  };\n\n  return { handlePointerDown, cleanup };\n};\n\nexport const GradientPicker = <T extends ParsedGradient>({\n  gradient,\n  backgroundImage,\n  onChange,\n  onChangeComplete,\n  onThumbSelect,\n  selectedStopIndex,\n}: GradientPickerProps<T>) => {\n  const [stops, setStops] = useState<Array<GradientStop>>(gradient.stops);\n  const [selectedStop, setSelectedStop] = useState<number | undefined>();\n  const [isHoveredOnStop, setIsHoveredOnStop] = useState<boolean>(false);\n  const [draggingStop, setDraggingStop] = useState<number | undefined>();\n  const [colorPickerOpenStop, setColorPickerOpenStop] = useState<\n    number | undefined\n  >();\n  const sliderRef = useRef<HTMLDivElement | undefined>(undefined);\n  const thumbRefs = useRef<Array<HTMLDivElement | undefined>>([]);\n  const stopsRef = useRef(stops);\n\n  const handleSliderRef = useCallback((element: HTMLDivElement | null) => {\n    sliderRef.current = element ?? undefined;\n  }, []);\n\n  useEffect(() => {\n    stopsRef.current = stops;\n  }, [stops]);\n\n  const thumbDragHandler = useMemo(() => createDragHandler(), []);\n\n  const hintDragHandler = useMemo(() => createDragHandler(0), []);\n\n  useEffect(() => {\n    return () => {\n      thumbDragHandler.cleanup();\n      hintDragHandler.cleanup();\n    };\n  }, [thumbDragHandler, hintDragHandler]);\n\n  useEffect(() => {\n    if (selectedStopIndex !== undefined) {\n      setSelectedStop(selectedStopIndex);\n    }\n  }, [selectedStopIndex]);\n\n  const buildGradient = useCallback(\n    (stopsValue: GradientStop[]): T => {\n      return {\n        ...gradient,\n        stops: stopsValue,\n      } as T;\n    },\n    [gradient]\n  );\n\n  useEffect(() => {\n    setStops(gradient.stops);\n    stopsRef.current = gradient.stops;\n\n    setSelectedStop((currentSelected) => {\n      if (gradient.stops.length === 0) {\n        return;\n      }\n\n      // If no stop is selected, select the first one\n      if (currentSelected === undefined) {\n        const firstStop = gradient.stops[0];\n        if (firstStop !== undefined) {\n          onThumbSelect(0, firstStop);\n        }\n        return 0;\n      }\n\n      const nextIndex = Math.min(currentSelected, gradient.stops.length - 1);\n      const nextStop = gradient.stops[nextIndex];\n\n      if (nextStop !== undefined && nextIndex !== currentSelected) {\n        onThumbSelect(nextIndex, nextStop);\n      }\n\n      return nextIndex;\n    });\n  }, [gradient.stops, onThumbSelect]);\n\n  const positions = stops\n    .map((stop) =>\n      stop.position?.type === \"unit\" && stop.position.unit === \"%\"\n        ? stop.position.value\n        : undefined\n    )\n    .filter((item): item is number => item !== undefined);\n  const hints = gradient.stops\n    .map((stop, index): { value: number; stopIndex: number } | undefined =>\n      stop.hint?.type === \"unit\" && stop.hint.unit === \"%\"\n        ? { value: stop.hint.value, stopIndex: index }\n        : undefined\n    )\n    .filter(\n      (item): item is { value: number; stopIndex: number } => item !== undefined\n    );\n\n  const updateStops = useCallback(\n    (\n      updater: (currentStops: GradientStop[]) => GradientStop[],\n      type: \"change\" | \"complete\"\n    ) => {\n      let nextStops: GradientStop[] | undefined;\n      let hasChanged = false;\n      setStops((currentStops) => {\n        nextStops = updater(currentStops);\n        if (nextStops !== currentStops) {\n          hasChanged = true;\n          stopsRef.current = nextStops;\n          return nextStops;\n        }\n        nextStops = currentStops;\n        return currentStops;\n      });\n\n      if (hasChanged === false || nextStops === undefined) {\n        return;\n      }\n\n      const nextGradient = buildGradient(nextStops);\n\n      if (type === \"change\") {\n        onChange(nextGradient);\n      } else {\n        onChangeComplete(nextGradient);\n      }\n    },\n    [buildGradient, onChange, onChangeComplete]\n  );\n\n  const updateStopPosition = useCallback(\n    (index: number, value: number, type: \"change\" | \"complete\") => {\n      const nextValue = clamp(value, 0, 100);\n      updateStops((currentStops) => {\n        if (index < 0 || index >= currentStops.length) {\n          return currentStops;\n        }\n\n        return currentStops.map((stop, stopIndex) => {\n          if (stopIndex !== index) {\n            return stop;\n          }\n          return {\n            ...stop,\n            position: createPercentUnit(nextValue),\n          };\n        });\n      }, type);\n    },\n    [updateStops]\n  );\n\n  const updateStopHint = useCallback(\n    (index: number, value: number, type: \"change\" | \"complete\") => {\n      const nextValue = clamp(value, 0, 100);\n      updateStops((currentStops) => {\n        if (index < 0 || index >= currentStops.length) {\n          return currentStops;\n        }\n\n        return currentStops.map((stop, stopIndex) => {\n          if (stopIndex !== index) {\n            return stop;\n          }\n          return {\n            ...stop,\n            hint: createPercentUnit(nextValue),\n          };\n        });\n      }, type);\n    },\n    [updateStops]\n  );\n\n  const handleColorPickerOpenChange = useCallback(\n    (index: number, open: boolean) => {\n      setColorPickerOpenStop(open ? index : undefined);\n    },\n    []\n  );\n\n  const isGradientColorValue = (\n    value: StyleValue\n  ): value is GradientColorValue =>\n    value.type === \"rgb\" || value.type === \"keyword\" || value.type === \"var\";\n\n  const handleStopColorChange = useCallback(\n    (\n      index: number,\n      value: StyleValue | undefined,\n      changeType: \"change\" | \"complete\"\n    ) => {\n      if (value === undefined || !isGradientColorValue(value)) {\n        return;\n      }\n\n      updateStops((currentStops) => {\n        if (index < 0 || index >= currentStops.length) {\n          return currentStops;\n        }\n\n        return currentStops.map((stop, stopIndex) => {\n          if (stopIndex !== index) {\n            return stop;\n          }\n          return {\n            ...stop,\n            color: value,\n          };\n        });\n      }, changeType);\n    },\n    [updateStops]\n  );\n\n  useEffect(() => {\n    if (\n      colorPickerOpenStop !== undefined &&\n      colorPickerOpenStop >= stops.length\n    ) {\n      setColorPickerOpenStop(undefined);\n    }\n  }, [colorPickerOpenStop, stops.length]);\n\n  const checkIfStopExistsAtPosition = useCallback(\n    (\n      clientX: number\n    ): {\n      isStopExistingAtPosition: boolean;\n      newPosition: number;\n    } => {\n      const sliderElement = sliderRef.current;\n      if (sliderElement === undefined) {\n        return { isStopExistingAtPosition: false, newPosition: 0 };\n      }\n      const rect = sliderElement.getBoundingClientRect();\n      const newPosition = computePositionFromClientX(sliderElement, clientX);\n\n      if (rect.width === 0) {\n        return { isStopExistingAtPosition: false, newPosition };\n      }\n\n      const relativeX = clamp(clientX - rect.left, 0, rect.width);\n      const isStopExistingAtPosition = positions.some((position) => {\n        const positionPx = (position / 100) * rect.width;\n        return Math.abs(positionPx - relativeX) <= THUMB_INTERACTION_DISTANCE;\n      });\n\n      return { isStopExistingAtPosition, newPosition };\n    },\n    [positions]\n  );\n\n  const handleStopSelected = useCallback(\n    (index: number, stop: GradientStop) => {\n      setSelectedStop(index);\n      onThumbSelect(index, stop);\n    },\n    [onThumbSelect]\n  );\n\n  useEffect(() => {\n    if (selectedStop === undefined) {\n      return;\n    }\n\n    const thumb = thumbRefs.current[selectedStop];\n    thumb?.focus({ preventScroll: true });\n  }, [selectedStop]);\n\n  const handleThumbPointerDown = useCallback(\n    (index: number, stop: GradientStop) =>\n      (event: ReactPointerEvent<HTMLDivElement>) => {\n        // Don't start drag if the color picker is open for this thumb\n        if (colorPickerOpenStop === index) {\n          event.stopPropagation();\n          return;\n        }\n\n        handleStopSelected(index, stop);\n        setIsHoveredOnStop(true);\n\n        // Track the currently dragged stop index\n        let currentIndex = index;\n\n        // Calculate hint offset relative to stop position\n        const originalPosition = getStopPositionValue(stop);\n        const originalHint =\n          stop.hint?.type === \"unit\" ? stop.hint.value : undefined;\n        const hintOffset =\n          originalHint !== undefined ? originalHint - originalPosition : 0;\n\n        thumbDragHandler.handlePointerDown({\n          event,\n          onDragMove: (newPosition) => {\n            updateStops((currentStops) => {\n              if (currentIndex < 0 || currentIndex >= currentStops.length) {\n                return currentStops;\n              }\n\n              const clampedPosition = clamp(newPosition, 0, 100);\n\n              // Update the position of the dragged stop and maintain hint offset\n              const updatedStops = currentStops.map((stop, stopIndex) => {\n                if (stopIndex !== currentIndex) {\n                  return stop;\n                }\n\n                const updatedStop: GradientStop = {\n                  ...stop,\n                  position: createPercentUnit(clampedPosition),\n                };\n\n                // Update hint to maintain the same offset relative to the stop\n                if (stop.hint?.type === \"unit\") {\n                  const newHintPosition = clamp(\n                    clampedPosition + hintOffset,\n                    0,\n                    100\n                  );\n                  updatedStop.hint = createPercentUnit(newHintPosition);\n                }\n\n                return updatedStop;\n              });\n\n              // Sort stops by position and track where our dragged stop ends up\n              const draggedStop = updatedStops[currentIndex];\n              const sortedStops = [...updatedStops].sort((stopA, stopB) => {\n                return (\n                  getStopPositionValue(stopA) - getStopPositionValue(stopB)\n                );\n              });\n\n              // Find the new index of the dragged stop\n              const newIndex = sortedStops.findIndex(\n                (stop) => stop === draggedStop\n              );\n              if (newIndex !== -1 && newIndex !== currentIndex) {\n                currentIndex = newIndex;\n                setSelectedStop(newIndex);\n                setDraggingStop(newIndex);\n              }\n\n              return sortedStops;\n            }, \"change\");\n          },\n          onDragStart: () => {\n            setColorPickerOpenStop(undefined);\n            setDraggingStop(currentIndex);\n          },\n          onDragEnd: () => {\n            setIsHoveredOnStop(false);\n            setDraggingStop(undefined);\n            onChangeComplete(buildGradient(stopsRef.current));\n          },\n        });\n      },\n    [\n      colorPickerOpenStop,\n      handleStopSelected,\n      thumbDragHandler,\n      updateStops,\n      onChangeComplete,\n      buildGradient,\n    ]\n  );\n\n  const handleHintPointerDown = useCallback(\n    (index: number) => (event: ReactPointerEvent<HTMLDivElement>) => {\n      event.stopPropagation();\n      hintDragHandler.handlePointerDown({\n        event,\n        onDragMove: (newPosition) => {\n          updateStopHint(index, newPosition, \"change\");\n        },\n        onDragEnd: () => {\n          onChangeComplete(buildGradient(stopsRef.current));\n        },\n      });\n    },\n    [hintDragHandler, updateStopHint, onChangeComplete, buildGradient]\n  );\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      if (selectedStop === undefined) {\n        return;\n      }\n\n      if (event.key === \"Enter\") {\n        event.preventDefault();\n        if (colorPickerOpenStop === selectedStop) {\n          setColorPickerOpenStop(undefined);\n        } else {\n          setColorPickerOpenStop(selectedStop);\n        }\n        return;\n      }\n\n      if (event.key === \"Backspace\" || event.key === \"Delete\") {\n        event.preventDefault();\n        let nextSelection:\n          | { index: number | undefined; stop?: GradientStop }\n          | undefined;\n\n        updateStops((currentStops) => {\n          if (selectedStop < 0 || selectedStop >= currentStops.length) {\n            return currentStops;\n          }\n\n          const nextStops = currentStops.filter(\n            (_, index) => index !== selectedStop\n          );\n\n          if (nextStops.length > 0) {\n            const candidateIndex = Math.min(selectedStop, nextStops.length - 1);\n            nextSelection = {\n              index: candidateIndex,\n              stop: nextStops[candidateIndex],\n            };\n          } else {\n            nextSelection = { index: undefined };\n          }\n\n          return nextStops;\n        }, \"complete\");\n\n        if (nextSelection?.index !== undefined && nextSelection.stop) {\n          setSelectedStop(nextSelection.index);\n          onThumbSelect(nextSelection.index, nextSelection.stop);\n        } else {\n          setSelectedStop(undefined);\n        }\n\n        return;\n      }\n\n      if (event.key === \"ArrowUp\" || event.key === \"ArrowDown\") {\n        event.preventDefault();\n        const stopCount = stops.length;\n        if (stopCount === 0) {\n          return;\n        }\n\n        const normalizedCurrent =\n          ((selectedStop % stopCount) + stopCount) % stopCount;\n        const delta = event.key === \"ArrowUp\" ? -1 : 1;\n        const nextIndex = (normalizedCurrent + delta + stopCount) % stopCount;\n\n        if (nextIndex !== normalizedCurrent) {\n          const nextStop = stops[nextIndex];\n          if (nextStop !== undefined) {\n            handleStopSelected(nextIndex, nextStop);\n          }\n        }\n\n        return;\n      }\n\n      if (event.key === \"ArrowLeft\" || event.key === \"ArrowRight\") {\n        event.preventDefault();\n        const step = event.shiftKey ? 10 : 1;\n        const delta = event.key === \"ArrowLeft\" ? -step : step;\n        const currentPosition =\n          stops[selectedStop]?.position?.type === \"unit\"\n            ? stops[selectedStop]?.position.value\n            : 0;\n        updateStopPosition(selectedStop, currentPosition + delta, \"complete\");\n      }\n    },\n    [\n      handleStopSelected,\n      onThumbSelect,\n      selectedStop,\n      stops,\n      updateStopPosition,\n      updateStops,\n      colorPickerOpenStop,\n    ]\n  );\n\n  const handlePointerDown = useCallback(\n    (event: ReactPointerEvent<HTMLDivElement>) => {\n      if (\n        event.target instanceof HTMLElement &&\n        event.target.closest(\"[data-gradient-thumb='true']\")\n      ) {\n        return;\n      }\n\n      setColorPickerOpenStop(undefined);\n\n      const { isStopExistingAtPosition, newPosition } =\n        checkIfStopExistsAtPosition(event.clientX);\n\n      if (isStopExistingAtPosition === true) {\n        return;\n      }\n\n      event.preventDefault();\n\n      let nextSelection: { index: number; stop: GradientStop } | undefined;\n\n      updateStops((currentStops) => {\n        if (currentStops.length === 0) {\n          return currentStops;\n        }\n\n        const currentPositions = currentStops\n          .map((stop) =>\n            stop.position?.type === \"unit\" ? stop.position.value : undefined\n          )\n          .filter((value): value is number => value !== undefined);\n\n        const newStopIndex = currentPositions.findIndex(\n          (position) => position > newPosition\n        );\n        const insertionIndex =\n          newStopIndex === -1 ? currentStops.length : newStopIndex;\n\n        const prevIndex = insertionIndex === 0 ? 0 : insertionIndex - 1;\n        const nextIndex =\n          insertionIndex === currentStops.length\n            ? currentStops.length - 1\n            : insertionIndex;\n\n        const prevColor = currentStops[prevIndex]?.color;\n        const nextColor = currentStops[nextIndex]?.color ?? prevColor;\n\n        const prevRgb = toRgbColor(prevColor);\n        const nextRgb = toRgbColor(nextColor);\n\n        let newColor: GradientStop[\"color\"] | undefined;\n        if (prevRgb !== undefined && nextRgb !== undefined) {\n          const interpolationColor = mixColors(\n            prevRgb,\n            nextRgb,\n            newPosition / 100\n          );\n          newColor = {\n            type: \"rgb\",\n            alpha: interpolationColor.alpha,\n            r: interpolationColor.r,\n            g: interpolationColor.g,\n            b: interpolationColor.b,\n          };\n        } else if (prevColor !== undefined) {\n          newColor = { ...prevColor };\n        } else if (nextColor !== undefined) {\n          newColor = { ...nextColor };\n        } else {\n          newColor = { ...defaultStopColor };\n        }\n\n        const newStop: GradientStop = {\n          color: newColor,\n          position: { type: \"unit\", value: newPosition, unit: \"%\" },\n        };\n\n        const nextStops: GradientStop[] = [\n          ...currentStops.slice(0, insertionIndex),\n          newStop,\n          ...currentStops.slice(insertionIndex),\n        ];\n\n        nextSelection = { index: insertionIndex, stop: newStop };\n\n        return nextStops;\n      }, \"complete\");\n\n      if (nextSelection !== undefined) {\n        setSelectedStop(nextSelection.index);\n        onThumbSelect(nextSelection.index, nextSelection.stop);\n      }\n\n      setIsHoveredOnStop(true);\n    },\n    [checkIfStopExistsAtPosition, onThumbSelect, updateStops]\n  );\n\n  const handleMouseIndicator = useCallback(\n    (event: MouseEvent<HTMLDivElement>) => {\n      const { isStopExistingAtPosition } = checkIfStopExistsAtPosition(\n        event.clientX\n      );\n      setIsHoveredOnStop(isStopExistingAtPosition);\n    },\n    [checkIfStopExistsAtPosition]\n  );\n\n  const handleSliderFocus = useCallback(() => {\n    if (stops.length === 0) {\n      return;\n    }\n\n    // Always ensure a stop is selected when focusing\n    if (selectedStop === undefined) {\n      const [firstStop] = stops;\n      if (firstStop !== undefined) {\n        handleStopSelected(0, firstStop);\n      }\n    }\n  }, [handleStopSelected, selectedStop, stops]);\n\n  if (\n    stops.some(\n      (stop) =>\n        stop.color === undefined ||\n        stop.position?.type !== \"unit\" ||\n        stop.position.unit !== \"%\"\n    )\n  ) {\n    return;\n  }\n\n  return (\n    <Flex\n      align=\"end\"\n      css={{ height: theme.spacing[16] }}\n      onKeyDown={handleKeyDown}\n    >\n      <SliderRoot\n        ref={handleSliderRef}\n        data-gradient-slider\n        css={{ backgroundImage }}\n        isHoveredOnStop={isHoveredOnStop}\n        tabIndex={0}\n        role=\"group\"\n        aria-label=\"Gradient stops\"\n        onPointerDown={handlePointerDown}\n        onFocus={handleSliderFocus}\n        onMouseEnter={handleMouseIndicator}\n        onMouseMove={handleMouseIndicator}\n        onMouseLeave={() => setIsHoveredOnStop(false)}\n      >\n        <SliderTrack />\n        {stops.map((stop, index) => {\n          const isSelected = selectedStop === index;\n          const isDragging = draggingStop === index;\n          if (\n            stop.color === undefined ||\n            stop.position?.type !== \"unit\" ||\n            stop.position.unit !== \"%\"\n          ) {\n            return;\n          }\n\n          const stopColor = stop.color as StyleValue;\n\n          return (\n            <SliderThumb\n              key={index}\n              data-gradient-thumb=\"true\"\n              style={{\n                left: `${stop.position.value}%`,\n                zIndex: isDragging ? 2 : 1,\n              }}\n              role=\"slider\"\n              aria-orientation=\"horizontal\"\n              aria-valuemin={0}\n              aria-valuemax={100}\n              aria-valuenow={stop.position.value}\n              aria-label={`Gradient stop ${index + 1}`}\n              aria-selected={isSelected}\n              tabIndex={-1}\n              ref={(element) => {\n                thumbRefs.current[index] = element ?? undefined;\n              }}\n              onPointerDown={handleThumbPointerDown(index, stop)}\n              onClick={() => {\n                handleStopSelected(index, stop);\n              }}\n            >\n              <ColorPickerPopover\n                value={stopColor}\n                onChange={(value) =>\n                  handleStopColorChange(index, value, \"change\")\n                }\n                onChangeComplete={(value) =>\n                  handleStopColorChange(index, value, \"complete\")\n                }\n                open={colorPickerOpenStop === index}\n                onOpenChange={(open) =>\n                  handleColorPickerOpenChange(index, open)\n                }\n                sideOffset={SLIDER_HEIGHT + THUMB_HEIGHT}\n                thumb={\n                  <ColorThumb\n                    color={stop.color ? toValue(stop.color) : \"transparent\"}\n                    interactive={true}\n                    css={{\n                      margin: 1,\n                      width: THUMB_HEIGHT,\n                      height: THUMB_HEIGHT,\n                    }}\n                    data-gradient-thumb=\"true\"\n                  />\n                }\n              />\n              <SliderThumbPointer aria-hidden />\n            </SliderThumb>\n          );\n        })}\n\n        {hints.map((hint) => {\n          return (\n            <SliderHint\n              key={`${hint.stopIndex}-${hint.value}`}\n              style={{ left: `${hint.value}%` }}\n              onPointerDown={handleHintPointerDown(hint.stopIndex)}\n            >\n              <ChevronFilledUpIcon size=\"100%\" />\n            </SliderHint>\n          );\n        })}\n      </SliderRoot>\n    </Flex>\n  );\n};\n\nconst SliderRoot = styled(\"div\", {\n  position: \"relative\",\n  width: \"100%\",\n  height: SLIDER_HEIGHT,\n  border: `1px solid ${theme.colors.borderMain}`,\n  borderRadius: theme.borderRadius[3],\n  touchAction: \"none\",\n  userSelect: \"none\",\n  outline: \"none\",\n  \"&::before\": {\n    content: '\"\"',\n    position: \"absolute\",\n    inset: 0,\n    background: `repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%`,\n    pointerEvents: \"none\",\n    backgroundSize: \"10px 10px\",\n    zIndex: -1,\n  },\n  variants: {\n    isHoveredOnStop: {\n      true: {\n        cursor: \"default\",\n      },\n      false: {\n        cursor: \"copy\",\n      },\n    },\n  },\n  \"&:focus-visible\": {\n    boxShadow: `0 0 0 1px ${theme.colors.borderFocus}`,\n  },\n});\n\nconst SliderTrack = styled(\"div\", {\n  position: \"absolute\",\n  inset: 0,\n  borderRadius: theme.borderRadius[3],\n  pointerEvents: \"none\",\n  isolation: \"isolate\",\n});\n\nconst SliderThumb = styled(Box, {\n  \"--thumb-border-color\": theme.colors.borderMain,\n  position: \"absolute\",\n  display: \"grid\",\n  placeItems: \"center\",\n  bottom: `calc(100% + ${theme.spacing[6]})`,\n  transform: \"translateX(-50%)\",\n  borderRadius: theme.borderRadius[2],\n  boxShadow: `0 0 0 1px var(--thumb-border-color)`,\n  outline: \"none\",\n  zIndex: 1,\n  cursor: \"grab\",\n  \"&:active\": {\n    cursor: \"grabbing\",\n  },\n  \"&:focus-visible, &[aria-selected=true]\": {\n    \"--thumb-border-color\": theme.colors.borderFocus,\n    boxShadow: `0 0 0 1px ${theme.colors.borderFocus}`,\n  },\n});\n\nconst SliderThumbPointer = styled(\"div\", {\n  position: \"absolute\",\n  width: theme.spacing[3],\n  height: theme.spacing[3],\n  left: \"50%\",\n  bottom: 0,\n  background: \"white\",\n  zIndex: -1,\n  border: `1px solid var(--thumb-border-color)`,\n  borderTopColor: \"transparent\",\n  borderLeftColor: \"transparent\",\n  borderRadius: theme.borderRadius[1],\n  transform: \"translate(-50%, 50%) rotate(45deg)\",\n  pointerEvents: \"none\",\n});\n\nconst SliderHint = styled(Flex, {\n  position: \"absolute\",\n  top: `100%`,\n  width: theme.spacing[7],\n  height: theme.spacing[7],\n  pointerEvents: \"auto\",\n  transform: \"translateX(-50%)\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  cursor: \"grab\",\n  touchAction: \"none\",\n  userSelect: \"none\",\n  color: \"black\",\n  \"&:active\": {\n    cursor: \"grabbing\",\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/grid.tsx",
    "content": "import { styled } from \"../stitches.config\";\nimport { theme } from \"../stitches.config\";\n\nexport const Grid = styled(\"div\", {\n  boxSizing: \"border-box\",\n  display: \"grid\",\n  minHeight: 0,\n\n  variants: {\n    align: {\n      start: {\n        alignItems: \"start\",\n      },\n      center: {\n        alignItems: \"center\",\n      },\n      end: {\n        alignItems: \"end\",\n      },\n      stretch: {\n        alignItems: \"stretch\",\n      },\n      baseline: {\n        alignItems: \"baseline\",\n      },\n    },\n    justify: {\n      start: {\n        justifyContent: \"start\",\n      },\n      center: {\n        justifyContent: \"center\",\n      },\n      end: {\n        justifyContent: \"end\",\n      },\n      between: {\n        justifyContent: \"space-between\",\n      },\n    },\n    flow: {\n      row: {\n        gridAutoFlow: \"row\",\n      },\n      column: {\n        gridAutoFlow: \"column\",\n      },\n      dense: {\n        gridAutoFlow: \"dense\",\n      },\n      rowDense: {\n        gridAutoFlow: \"row dense\",\n      },\n      columnDense: {\n        gridAutoFlow: \"column dense\",\n      },\n    },\n    columns: {\n      1: {\n        gridTemplateColumns: \"repeat(1, 1fr)\",\n      },\n      2: {\n        gridTemplateColumns: \"repeat(2, 1fr)\",\n      },\n      3: {\n        gridTemplateColumns: \"repeat(3, 1fr)\",\n      },\n      4: {\n        gridTemplateColumns: \"repeat(4, 1fr)\",\n      },\n    },\n    gap: {\n      1: {\n        gap: theme.spacing[3],\n      },\n      2: {\n        gap: theme.spacing[5],\n      },\n      3: {\n        gap: theme.spacing[9],\n      },\n      4: {\n        gap: theme.spacing[10],\n      },\n      5: {\n        gap: theme.spacing[11],\n      },\n      6: {\n        gap: theme.spacing[13],\n      },\n      7: {\n        gap: theme.spacing[17],\n      },\n      8: {\n        gap: theme.spacing[19],\n      },\n      9: {\n        gap: theme.spacing[20],\n      },\n    },\n    gapX: {\n      1: {\n        columnGap: theme.spacing[3],\n      },\n      2: {\n        columnGap: theme.spacing[5],\n      },\n      3: {\n        columnGap: theme.spacing[9],\n      },\n      4: {\n        columnGap: theme.spacing[10],\n      },\n      5: {\n        columnGap: theme.spacing[11],\n      },\n      6: {\n        columnGap: theme.spacing[13],\n      },\n      7: {\n        columnGap: theme.spacing[17],\n      },\n      8: {\n        columnGap: theme.spacing[19],\n      },\n      9: {\n        columnGap: theme.spacing[20],\n      },\n    },\n    gapY: {\n      1: {\n        rowGap: theme.spacing[3],\n      },\n      2: {\n        rowGap: theme.spacing[5],\n      },\n      3: {\n        rowGap: theme.spacing[9],\n      },\n      4: {\n        rowGap: theme.spacing[10],\n      },\n      5: {\n        rowGap: theme.spacing[11],\n      },\n      6: {\n        rowGap: theme.spacing[13],\n      },\n      7: {\n        rowGap: theme.spacing[17],\n      },\n      8: {\n        rowGap: theme.spacing[19],\n      },\n      9: {\n        rowGap: theme.spacing[20],\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/icon-button.stories.tsx",
    "content": "import { XIcon } from \"@webstudio-is/icons\";\nimport { IconButton as IconButtonComponent } from \"./icon-button\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Icon Button\",\n  component: IconButtonComponent,\n};\n\nexport const IconButton = () => (\n  <>\n    <StorySection title=\"Variants\">\n      <StoryGrid horizontal>\n        <IconButtonComponent>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"preset\">\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"local\">\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"overwritten\">\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"remote\">\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Disabled\">\n      <StoryGrid horizontal>\n        <IconButtonComponent disabled={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"preset\" disabled={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"local\" disabled={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"overwritten\" disabled={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"remote\" disabled={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Open state\">\n      <StoryGrid horizontal>\n        <IconButtonComponent data-state={\"open\"}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"preset\" data-state={\"open\"}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"local\" data-state={\"open\"}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"overwritten\" data-state={\"open\"}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"remote\" data-state={\"open\"}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Focused\">\n      <StoryGrid horizontal>\n        <IconButtonComponent data-focused={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"preset\" data-focused={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"local\" data-focused={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"overwritten\" data-focused={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"remote\" data-focused={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Hovered\">\n      <StoryGrid horizontal>\n        <IconButtonComponent data-hovered={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"preset\" data-hovered={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"local\" data-hovered={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"overwritten\" data-hovered={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n        <IconButtonComponent variant=\"remote\" data-hovered={true}>\n          <XIcon fill=\"currentColor\" />\n        </IconButtonComponent>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/icon-button.tsx",
    "content": "/**\n * Implementation of the \"Icon Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4-3139\n *\n * Also used as \"Toggle Button\" (see toggle-button.tsx)\n */\nimport { styled } from \"../stitches.config\";\nimport { theme } from \"../stitches.config\";\n\nconst openOrHoverStateStyle = {\n  backgroundColor: theme.colors.backgroundHover,\n};\n\nconst disabledVariantStyles = {\n  \"&:disabled, &[aria-disabled=true]\": {\n    color: theme.colors.foregroundDisabled,\n    \"&:hover\": {\n      backgroundColor: theme.colors.backgroundHover,\n    },\n  },\n};\n\nexport const IconButton = styled(\"button\", {\n  // reset styles\n  boxSizing: \"border-box\",\n  padding: 0,\n  appearance: \"none\",\n  backgroundColor: \"transparent\",\n  border: \"1px solid transparent\",\n  // center icon\n  display: \"flex\",\n  justifyContent: \"center\",\n  alignItems: \"center\",\n  // prevent shrinking inside flex box\n  flexShrink: 0,\n  minWidth: theme.sizes.controlHeight,\n  width: \"max-content\",\n  height: theme.sizes.controlHeight,\n  borderRadius: theme.borderRadius[3],\n  outline: \"none\",\n\n  \"&[data-focused=true], &:focus-visible\": {\n    borderColor: theme.colors.borderFocus,\n  },\n\n  \"&:disabled, &[aria-disabled=true]\": {\n    borderColor: \"transparent\",\n    backgroundColor: \"transparent\",\n  },\n\n  // https://www.radix-ui.com/docs/primitives/components/popover#trigger\n  \"&[data-state=open]\": openOrHoverStateStyle,\n\n  variants: {\n    variant: {\n      default: {\n        color: theme.colors.foregroundMain,\n        \"&:hover, &[data-hovered=true]\": openOrHoverStateStyle,\n        // According to the design https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4-3199&t=lpT9jFuaiUnz1Foa-0\n        // only the default variant has different toggle state\n        // https://www.radix-ui.com/docs/primitives/components/toggle#root\n        \"&[data-state=on]\": {\n          backgroundColor: theme.colors.backgroundPresetMain,\n          borderColor: theme.colors.borderMain,\n\n          \"&:hover, &[data-hovered=true]\": openOrHoverStateStyle,\n        },\n        \"&[data-focused=true], &:focus-visible\": {\n          borderColor: theme.colors.borderFocus,\n        },\n        ...disabledVariantStyles,\n      },\n\n      preset: {\n        backgroundColor: theme.colors.backgroundPresetMain,\n        borderColor: theme.colors.borderMain,\n        color: theme.colors.foregroundMain,\n        \"&:hover, &[data-hovered=true]\": {\n          backgroundColor: theme.colors.backgroundPresetHover,\n        },\n        ...disabledVariantStyles,\n      },\n\n      local: {\n        backgroundColor: theme.colors.backgroundLocalMain,\n        borderColor: theme.colors.borderLocalMain,\n        color: theme.colors.foregroundLocalMain,\n        \"&:hover, &[data-hovered=true]\": {\n          backgroundColor: theme.colors.backgroundLocalHover,\n        },\n        ...disabledVariantStyles,\n      },\n\n      overwritten: {\n        backgroundColor: theme.colors.backgroundOverwrittenMain,\n        borderColor: theme.colors.borderOverwrittenMain,\n        color: theme.colors.foregroundOverwrittenMain,\n        \"&:hover, &[data-hovered=true]\": {\n          backgroundColor: theme.colors.backgroundOverwrittenHover,\n        },\n        ...disabledVariantStyles,\n      },\n\n      remote: {\n        backgroundColor: theme.colors.backgroundRemoteMain,\n        borderColor: theme.colors.borderRemoteMain,\n        color: theme.colors.foregroundRemoteMain,\n        \"&:hover, &[data-hovered=true]\": {\n          backgroundColor: theme.colors.backgroundRemoteHover,\n        },\n        ...disabledVariantStyles,\n      },\n    },\n    state: {\n      open: openOrHoverStateStyle,\n    },\n  },\n\n  defaultVariants: {\n    variant: \"default\",\n  },\n});\n\nIconButton.displayName = \"IconButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/input-field.stories.tsx",
    "content": "import { GapVerticalIcon } from \"@webstudio-is/icons\";\nimport { Flex } from \"./flex\";\nimport {\n  InputField as InputFieldComponent,\n  inputFieldColors,\n} from \"./input-field\";\nimport { NestedIconLabel } from \"./nested-icon-label\";\nimport { NestedInputButton } from \"./nested-input-button\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Input Field\",\n};\n\nconst prefix = (\n  <NestedIconLabel color=\"local\" tabIndex={-1}>\n    <GapVerticalIcon />\n  </NestedIconLabel>\n);\n\nconst suffix = <NestedInputButton tabIndex={-1} />;\n\nexport const InputField = () => (\n  <>\n    <StorySection title=\"Basic\">\n      <StoryGrid horizontal>\n        {inputFieldColors.map((color) => (\n          <InputFieldComponent key={color} defaultValue={color} color={color} />\n        ))}\n        <InputFieldComponent defaultValue=\"disabled\" disabled />\n        <InputFieldComponent placeholder=\"Actual placeholder\" />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Mono\">\n      <StoryGrid horizontal>\n        <InputFieldComponent defaultValue=\"mono text\" text=\"mono\" />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Nested Controls\">\n      <StoryGrid horizontal>\n        {inputFieldColors.map((color) => (\n          <InputFieldComponent\n            key={color}\n            defaultValue={color}\n            color={color}\n            prefix={prefix}\n            suffix={suffix}\n          />\n        ))}\n        <InputFieldComponent\n          defaultValue=\"disabled\"\n          prefix={\n            <NestedIconLabel disabled color=\"local\">\n              <GapVerticalIcon />\n            </NestedIconLabel>\n          }\n          suffix={<NestedInputButton disabled />}\n          disabled\n        />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Focused (initially)\">\n      <StoryGrid horizontal>\n        <InputFieldComponent\n          defaultValue=\"Some value\"\n          prefix={prefix}\n          suffix={suffix}\n          autoFocus\n        />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Width text\">\n      <StoryGrid>\n        {[300, 100].map((width) => (\n          <>\n            <Flex css={{ width, background: \"black\", height: 4 }} />\n            <Flex\n              css={{ width, justifyItems: \"stretch\", flexDirection: \"column\" }}\n            >\n              <InputFieldComponent prefix={prefix} suffix={suffix} />\n            </Flex>\n            <InputFieldComponent\n              prefix={prefix}\n              suffix={suffix}\n              css={{ width }}\n            />\n          </>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Chromeless variant\">\n      <StoryGrid horizontal>\n        <InputFieldComponent defaultValue=\"Chromeless\" variant=\"chromeless\" />\n        <InputFieldComponent\n          defaultValue=\"With prefix\"\n          variant=\"chromeless\"\n          prefix={prefix}\n          suffix={suffix}\n        />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Small size (size 1)\">\n      <StoryGrid horizontal>\n        <InputFieldComponent defaultValue=\"Size 1\" size=\"1\" />\n        <InputFieldComponent defaultValue=\"Size 2\" size=\"2\" />\n        <InputFieldComponent\n          defaultValue=\"Size 1 with prefix\"\n          size=\"1\"\n          prefix={prefix}\n          suffix={suffix}\n        />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Input types\">\n      <StoryGrid horizontal>\n        <InputFieldComponent type=\"number\" defaultValue=\"42\" />\n        <InputFieldComponent type=\"password\" defaultValue=\"secret\" />\n        <InputFieldComponent type=\"email\" placeholder=\"email@example.com\" />\n        <InputFieldComponent type=\"url\" placeholder=\"https://…\" />\n        <InputFieldComponent type=\"tel\" placeholder=\"+1 555 0100\" />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Field sizing content\">\n      <StoryGrid horizontal>\n        <InputFieldComponent defaultValue=\"Content\" fieldSizing=\"content\" />\n        <InputFieldComponent defaultValue=\"Fixed\" fieldSizing=\"fixed\" />\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/input-field.tsx",
    "content": "/**\n * Implementation of the \"Input Field\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4-3304\n */\n\nimport {\n  forwardRef,\n  type ReactNode,\n  type ComponentProps,\n  type Ref,\n  type FocusEventHandler,\n  useRef,\n  type KeyboardEventHandler,\n} from \"react\";\nimport { textVariants } from \"./text\";\nimport { css, theme, type CSS } from \"../stitches.config\";\nimport { ArrowFocus } from \"./primitives/arrow-focus\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { useFocusWithin } from \"@react-aria/interactions\";\n\n// we only support types that behave more or less like a regular text input\nexport const inputFieldTypes = [\n  \"email\",\n  \"password\",\n  \"tel\",\n  \"text\",\n  \"url\",\n  \"number\",\n  \"search\",\n] as const;\n\nexport const inputFieldColors = [\"placeholder\", \"set\", \"error\"] as const;\n\nconst inputStyle = css({\n  all: \"unset\",\n  color: theme.colors.foregroundMain,\n  flexGrow: 1,\n  flexShrink: 1,\n  minWidth: 0,\n  height: \"100%\",\n  paddingRight: theme.spacing[2],\n  paddingLeft: theme.spacing[3],\n  \"&[data-color=placeholder]:not(:hover, :disabled, [aria-disabled=true], :focus), &::placeholder\":\n    {\n      color: theme.colors.foregroundSubtle,\n    },\n  \"&[data-color=error]\": { color: theme.colors.foregroundDestructive },\n  \"&:disabled, &[aria-disabled=true]\": {\n    \"&, &::placeholder\": {\n      color: theme.colors.foregroundDisabled,\n    },\n  },\n  '&[type=\"number\"]': {\n    MozAppearance: \"textfield\",\n    \"&::-webkit-outer-spin-button, &::-webkit-inner-spin-button\": {\n      WebkitAppearance: \"none\",\n      margin: 0,\n    },\n  },\n  variants: {\n    text: {\n      regular: textVariants.regular,\n      mono: textVariants.mono,\n    },\n    fieldSizing: {\n      content: {\n        fieldSizing: \"content\",\n      },\n      fixed: {\n        fieldSizing: \"fixed\",\n      },\n    },\n  },\n  defaultVariants: {\n    text: \"regular\",\n  },\n});\n\nconst containerStyle = css({\n  display: \"flex\",\n  boxSizing: \"border-box\",\n  minWidth: 0,\n  alignItems: \"center\",\n  borderRadius: theme.borderRadius[4],\n  border: `solid 1px transparent`,\n  backgroundColor: theme.colors.backgroundControls,\n  \"&:hover\": {\n    borderColor: theme.colors.borderMain,\n  },\n  \"&:focus-within\": {\n    borderColor: theme.colors.borderFocus,\n    outline: \"none\",\n  },\n  \"&:has([data-input-field-input][data-color=error])\": {\n    borderColor: theme.colors.borderDestructiveMain,\n  },\n  \"&:focus-within:has([data-color=error])\": {\n    borderColor: theme.colors.borderDestructiveMain,\n  },\n  \"&:has([data-input-field-input]:is(:disabled, [aria-disabled=true]))\": {\n    backgroundColor: theme.colors.backgroundInputDisabled,\n  },\n\n  variants: {\n    variant: {\n      chromeless: {\n        \"&:not(:hover, :focus-within)\": {\n          borderColor: \"transparent\",\n          backgroundColor: \"transparent\",\n        },\n      },\n    },\n    size: {\n      1: {\n        height: theme.spacing[9],\n      },\n      2: {\n        height: theme.sizes.controlHeight,\n      },\n    },\n  },\n  defaultVariants: {\n    size: 2,\n  },\n});\n\nconst suffixSlotStyle = css({\n  marginRight: theme.spacing[1],\n});\n\nconst prefixSlotStyle = css({\n  marginLeft: theme.spacing[1],\n});\n\nconst Container = forwardRef(\n  (\n    {\n      children,\n      className,\n      css,\n      prefix,\n      suffix,\n      variant,\n      size,\n      ...props\n    }: {\n      children: ReactNode;\n      prefix?: ReactNode;\n      suffix?: ReactNode;\n      css?: CSS;\n      variant: InputFieldProps[\"variant\"];\n      size: InputFieldProps[\"size\"];\n    } & Omit<ComponentProps<\"div\">, \"prefix\">,\n    ref: Ref<HTMLDivElement>\n  ) => {\n    // no reason to use ArrowFocus if there's no prefix or suffix\n    if (!prefix && !suffix) {\n      return (\n        <div\n          className={containerStyle({ className, css, variant, size })}\n          {...props}\n          ref={ref}\n        >\n          {children}\n        </div>\n      );\n    }\n\n    return (\n      <ArrowFocus\n        render={({ handleKeyDown }) => (\n          <div\n            className={containerStyle({ className, css, variant, size })}\n            {...props}\n            onKeyDown={(event) => {\n              props.onKeyDown?.(event);\n\n              // ignore up/down,\n              // because they're likely used for dropdowns or increment/decrement\n              if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n                return;\n              }\n\n              handleKeyDown(event);\n            }}\n            ref={ref}\n          >\n            {prefix && <div className={prefixSlotStyle()}>{prefix}</div>}\n            {children}\n            {suffix && <div className={suffixSlotStyle()}>{suffix}</div>}\n          </div>\n        )}\n      />\n    );\n  }\n);\nContainer.displayName = \"Container\";\n\ntype InputProps = Omit<\n  ComponentProps<\"input\">,\n  \"onFocus\" | \"onBlur\" | \"prefix\" | \"size\"\n> & {\n  onFocus?: FocusEventHandler;\n  onBlur?: FocusEventHandler;\n};\n\ntype InputFieldProps = {\n  prefix?: ReactNode;\n  suffix?: ReactNode;\n  containerRef?: Ref<HTMLDivElement>;\n  inputRef?: Ref<HTMLInputElement>;\n  variant?: \"chromeless\";\n  size?: \"1\" | \"2\";\n  type?: (typeof inputFieldTypes)[number];\n  color?: (typeof inputFieldColors)[number];\n  css?: CSS;\n  text?: \"regular\" | \"mono\";\n  fieldSizing?: \"content\" | \"fixed\";\n};\n\nexport const InputField = forwardRef(\n  (\n    {\n      css,\n      className,\n      prefix,\n      suffix,\n      containerRef,\n      inputRef,\n      onFocus,\n      onBlur,\n      variant,\n      size,\n      color,\n      text,\n      fieldSizing,\n      onKeyDown,\n      ...inputProps\n    }: InputProps & InputFieldProps,\n    ref: Ref<HTMLDivElement>\n  ) => {\n    // Our input field can contain multiple focused elements,\n    // so we need to use useFocusWithin to track focus within the container.\n    const { focusWithinProps } = useFocusWithin({\n      onFocusWithin: onFocus,\n      onBlurWithin: onBlur,\n    });\n    const unfocusContainerRef = useRef<HTMLDivElement>(null);\n    const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {\n      // If Radix is preventing the Escape key from closing the dialog,\n      // it intercepts the key event at the document level.\n      // However, we still want to allow the user to unfocus the input field.\n      // This means we should not check `defaultPrevented`, but only verify\n      // if our event handler explicitly prevented it.\n      const isPreventedBefore = event.defaultPrevented;\n      onKeyDown?.(event);\n      const isPreventedAfter = event.defaultPrevented;\n      const isEventPrevented = !isPreventedBefore && isPreventedAfter;\n\n      if (event.key === \"Escape\" && !isEventPrevented) {\n        event.preventDefault();\n        unfocusContainerRef.current?.focus();\n      }\n    };\n\n    return (\n      <Container\n        css={css}\n        className={className}\n        prefix={prefix}\n        suffix={suffix}\n        variant={variant}\n        size={size}\n        {...focusWithinProps}\n        ref={mergeRefs(ref, containerRef ?? null)}\n      >\n        <div\n          // This element is used to move focus to it when user hits Escape.\n          // This way user can unfocus the input and then use any single-key shortcut.\n          tabIndex={-1}\n          ref={unfocusContainerRef}\n          // When managing focus with ArrowFocus, we don't want to focus this element.\n          data-no-arrow-focus\n        />\n        <input\n          {...inputProps}\n          ref={inputRef}\n          spellCheck={false}\n          data-input-field-input // to distinguish from potential other inputs in prefix/suffix\n          data-color={color}\n          className={inputStyle({ className, css, text, fieldSizing })}\n          onKeyDown={handleKeyDown}\n        />\n      </Container>\n    );\n  }\n);\nInputField.displayName = \"InputField\";\n"
  },
  {
    "path": "packages/design-system/src/components/kbd.stories.tsx",
    "content": "import { Kbd as KbdComponent } from \"./kbd\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Kbd\",\n};\n\nexport const Kbd = () => (\n  <>\n    <StorySection title=\"Colors\">\n      <StoryGrid>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">subtle (default)</Text>\n          <KbdComponent value={[\"meta\", \"z\"]} />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">moreSubtle</Text>\n          <KbdComponent value={[\"meta\", \"z\"]} color=\"moreSubtle\" />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">contrast</Text>\n          <KbdComponent value={[\"meta\", \"z\"]} color=\"contrast\" />\n        </Flex>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Variant\">\n      <StoryGrid>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">default</Text>\n          <KbdComponent value={[\"meta\", \"c\"]} />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">regular</Text>\n          <KbdComponent value={[\"meta\", \"c\"]} variant=\"regular\" />\n        </Flex>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Key combinations\">\n      <Flex direction=\"column\" gap=\"2\">\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Undo\n          </Text>\n          <KbdComponent value={[\"meta\", \"z\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Redo\n          </Text>\n          <KbdComponent value={[\"meta\", \"shift\", \"z\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Delete\n          </Text>\n          <KbdComponent value={[\"backspace\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Enter\n          </Text>\n          <KbdComponent value={[\"enter\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Tab\n          </Text>\n          <KbdComponent value={[\"tab\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Alt + click\n          </Text>\n          <KbdComponent value={[\"alt\", \"click\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Click\n          </Text>\n          <KbdComponent value={[\"click\"]} />\n        </Flex>\n        <Flex gap=\"3\" align=\"center\">\n          <Text variant=\"labels\" css={{ width: 120 }}>\n            Single key\n          </Text>\n          <KbdComponent value={[\"d\"]} />\n        </Flex>\n      </Flex>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/kbd.tsx",
    "content": "import { Text } from \"./text\";\n\nconst isMac =\n  typeof navigator === \"object\" ? /mac/i.test(navigator.platform) : false;\n\nconst shortcutSymbolMap: Record<string, string> = {\n  meta: isMac ? \"⌘\" : \"Ctrl\",\n  shift: \"⇧\",\n  alt: isMac ? \"⌥\" : \"Alt\",\n  backspace: \"⌫\",\n  enter: \"↵\",\n  tab: isMac ? \"tab\" : \"Tab\",\n  click: isMac ? \"+click\" : \"+ Click\",\n  \"click[0]\": isMac ? \"click\" : \"Click\",\n};\n\ntype ShortcutDefinition = ReadonlyArray<string>;\n\nconst format = (value: ShortcutDefinition) => {\n  return value.map(\n    (shortcut, index) =>\n      shortcutSymbolMap[`${shortcut}[${index}]`] ??\n      shortcutSymbolMap[shortcut] ??\n      shortcut.toUpperCase()\n  );\n};\n\n/**\n * Filter hotkeys array to show only the appropriate one for current OS.\n * Prioritizes meta (Mac) or ctrl (Windows/Linux) hotkeys.\n */\nexport const filterHotkeyByOs = (hotkeys: readonly string[]): string => {\n  if (hotkeys.length === 1) {\n    return hotkeys[0];\n  }\n\n  const macHotkey = hotkeys.find((key) => key.includes(\"meta\"));\n  const winHotkey = hotkeys.find((key) => key.includes(\"ctrl\"));\n\n  if (isMac && macHotkey) {\n    return macHotkey;\n  }\n  if (!isMac && winHotkey) {\n    return winHotkey;\n  }\n\n  return hotkeys[0];\n};\n\nexport const Kbd = ({\n  value,\n  color = \"subtle\",\n  variant,\n}: {\n  value: ShortcutDefinition;\n  color?: \"subtle\" | \"moreSubtle\" | \"contrast\";\n  variant?: \"regular\";\n}) => {\n  return (\n    <Text color={color} variant={variant} as=\"kbd\">\n      {format(value).join(isMac ? \"\" : \" \")}\n    </Text>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/label.stories.tsx",
    "content": "import { StorySection, StoryGrid } from \"./storybook\";\nimport { Label } from \"./label\";\nimport { Flex } from \"./flex\";\n\nexport default {\n  title: \"Label\",\n};\n\nconst colors = [\"default\", \"preset\", \"local\", \"overwritten\", \"remote\"] as const;\n\nconst LabelStory = () => (\n  <>\n    <StorySection title=\"Colors\">\n      <StoryGrid horizontal>\n        {colors.map((color) => (\n          <Label key={color} color={color}>\n            {color}\n          </Label>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Section title\">\n      <StoryGrid horizontal>\n        {colors.map((color) => (\n          <Label key={color} color={color} text=\"title\">\n            {color}\n          </Label>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Focused (initially)\">\n      <StoryGrid horizontal>\n        <Label color=\"local\" ref={(element) => element?.focus()}>\n          Local\n        </Label>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"With checkbox\">\n      <input id=\"checkbox1\" type=\"checkbox\"></input>\n      <Label htmlFor=\"checkbox1\">Label text</Label>\n    </StorySection>\n\n    <StorySection title=\"Disabled\">\n      <StoryGrid horizontal>\n        <Label disabled={true}>Label text</Label>\n        <div>\n          <input id=\"checkbox2\" type=\"checkbox\" disabled={true}></input>\n          <Label htmlFor=\"checkbox2\" disabled={true}>\n            Label text\n          </Label>\n        </div>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Truncated\">\n      <Flex direction=\"column\" gap=\"2\" css={{ width: 150 }}>\n        <Label truncate>\n          This is a very long label text that should be truncated at the edge\n        </Label>\n        <Label color=\"local\" truncate>\n          This is a very long local label text that should be truncated\n        </Label>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Mono text variant\">\n      <StoryGrid horizontal>\n        <Label text=\"mono\">Mono text</Label>\n        <Label text=\"sentence\">Sentence text</Label>\n        <Label text=\"title\">Title text</Label>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Inactive color\">\n      <Label color=\"inactive\">Inactive label</Label>\n    </StorySection>\n\n    <StorySection title=\"Tag variants\">\n      <StoryGrid horizontal>\n        <Label tag=\"button\">Explicit button tag</Label>\n        <Label tag=\"label\" color=\"local\">\n          Explicit label tag (with color)\n        </Label>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n\nexport { LabelStory as Label };\n"
  },
  {
    "path": "packages/design-system/src/components/label.tsx",
    "content": "/**\n * Implementation of the \"Label\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A3274\n */\n\nimport type { ComponentProps, ReactNode, Ref } from \"react\";\nimport { forwardRef } from \"react\";\nimport { textVariants } from \"./text\";\nimport { styled, theme } from \"../stitches.config\";\nimport { Label as RadixLabel } from \"@radix-ui/react-label\";\n\nexport const labelColors = [\n  \"default\",\n  \"preset\",\n  \"local\",\n  \"overwritten\",\n  \"remote\",\n  \"inactive\",\n] as const;\n\nconst StyledLabel = styled(RadixLabel, {\n  all: \"unset\", // reset <button>\n  margin: 0,\n  WebkitAppearance: \"none\",\n  WebkitFontSmoothing: \"antialiased\",\n  display: \"block\",\n  cursor: \"default\",\n  userSelect: \"none\",\n\n  boxSizing: \"border-box\",\n  flexShrink: 0,\n  py: theme.spacing[1],\n  px: theme.spacing[2],\n  border: \"1px solid transparent\",\n  borderRadius: theme.borderRadius[3],\n  transition: \"150ms color, 150ms background-color\",\n  color: theme.colors.foregroundMain,\n\n  // https://github.com/webstudio-is/webstudio/issues/1271#issuecomment-1478436340\n  \"&:focus-visible\": {\n    outline: `2px solid ${theme.colors.borderFocus}`,\n    outlineOffset: 1,\n  },\n\n  \"&[aria-disabled=true]\": {\n    color: theme.colors.foregroundDisabled,\n  },\n\n  variants: {\n    // The \"display: inline\" property can cause sizing issues with the label in certain scenarios.\n    // However, in our case, the label is being used as a button.\n    // To ensure compatibility with form labels, we only set the \"inline\" property if the \"htmlFor\" attribute is present.\n    hasHtmlFor: {\n      true: {\n        display: \"inline\",\n      },\n    },\n    color: {\n      default: {\n        color: theme.colors.foregroundMain,\n        \"&:hover\": {\n          backgroundColor: theme.colors.backgroundHover,\n        },\n      },\n      preset: {\n        backgroundColor: theme.colors.backgroundPresetMain,\n        color: theme.colors.foregroundTextSubtle,\n        \"&:hover\": {\n          backgroundColor: theme.colors.backgroundPresetHover,\n        },\n      },\n      local: {\n        backgroundColor: theme.colors.backgroundLocalMain,\n        color: theme.colors.foregroundLocalMain,\n        \"&:hover\": {\n          backgroundColor: theme.colors.backgroundLocalHover,\n        },\n      },\n      overwritten: {\n        backgroundColor: theme.colors.backgroundOverwrittenMain,\n        color: theme.colors.foregroundOverwrittenMain,\n        \"&:hover\": {\n          backgroundColor: theme.colors.backgroundOverwrittenHover,\n        },\n      },\n      remote: {\n        backgroundColor: theme.colors.backgroundRemoteMain,\n        color: theme.colors.foregroundRemoteMain,\n        \"&:hover\": {\n          backgroundColor: theme.colors.backgroundRemoteHover,\n        },\n      },\n      // Example is collapsible section title label when section has no content.\n      inactive: {\n        color: theme.colors.foregroundTextSubtle,\n        \"&:hover\": {\n          color: theme.colors.foregroundMain,\n        },\n      },\n    },\n    truncate: {\n      true: {\n        whiteSpace: \"nowrap\",\n        overflow: \"hidden\",\n        textOverflow: \"ellipsis\",\n        flexBasis: 0,\n        flexGrow: 1,\n      },\n    },\n    text: {\n      title: textVariants.titles,\n      sentence: textVariants.labels,\n      mono: textVariants.mono,\n    },\n  },\n\n  defaultVariants: {\n    text: \"sentence\",\n  },\n});\n\ntype Props = {\n  tag?: \"button\" | \"label\";\n  color?: (typeof labelColors)[number];\n  text?: \"title\" | \"sentence\" | \"mono\";\n  disabled?: boolean;\n  truncate?: boolean;\n  children: ReactNode;\n} & ComponentProps<typeof StyledLabel>;\n\nexport const isLabelButton = (color: Props[\"color\"]) => color !== undefined;\n\nexport const Label = forwardRef((props: Props, ref: Ref<HTMLLabelElement>) => {\n  const { tag, disabled, children, ...rest } = props;\n\n  // To enable keyboard accessibility for users who rely on the spacebar to activate the radix\n  // when using a preset, locala, overwritten or remote color, we need to wrap the label with\n  // a button that has a \"label\" role.\n  // (Radix adds role=\"button\" to the label)\n  let isButton = isLabelButton(props.color) || tag === \"button\";\n  // when explicit label\n  if (tag === \"label\") {\n    isButton = false;\n  }\n\n  return (\n    <StyledLabel\n      ref={ref}\n      asChild={isButton}\n      // Label is exluded from tab order by default\n      tabIndex={props.tabIndex ?? (isButton ? -1 : undefined)}\n      hasHtmlFor={props.htmlFor !== undefined}\n      aria-disabled={disabled}\n      {...rest}\n    >\n      {isButton ? <button>{children}</button> : children}\n    </StyledLabel>\n  );\n});\n\nLabel.displayName = \"Label\";\n"
  },
  {
    "path": "packages/design-system/src/components/link.stories.tsx",
    "content": "import { Box } from \"./box\";\nimport { Link } from \"./link\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Link\",\n};\n\nconst LinkStory = () => (\n  <>\n    <StorySection title=\"Variant\">\n      <StoryGrid\n        css={{\n          alignItems: \"flex-start\",\n        }}\n      >\n        <Link href=\"\">Regular</Link>\n        <Link variant=\"label\" href=\"\">\n          Label\n        </Link>\n        <Link variant=\"mono\" href=\"\">\n          Mono\n        </Link>\n        <Link variant=\"monoBold\" href=\"\">\n          Mono Bold\n        </Link>\n        <div style={{ fontSize: 20 }}>\n          <Link variant=\"inherit\" href=\"\">\n            Inherit\n          </Link>\n        </div>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Underline\">\n      <StoryGrid\n        css={{\n          alignItems: \"flex-start\",\n        }}\n      >\n        <Link href=\"\" underline=\"none\">\n          None\n        </Link>\n        <Link href=\"\" underline=\"hover\">\n          Hover\n        </Link>\n        <Link href=\"\" underline=\"always\">\n          Always\n        </Link>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Colors\">\n      <StoryGrid\n        css={{\n          alignItems: \"flex-start\",\n        }}\n      >\n        <Link color=\"main\" href=\"\">\n          Main\n        </Link>\n        <Box css={{ backgroundColor: \"black\" }}>\n          <Link color=\"contrast\" href=\"\">\n            Contrast\n          </Link>\n        </Box>\n        <Link color=\"subtle\" href=\"\">\n          Subtle\n        </Link>\n        <Link color=\"moreSubtle\" href=\"\">\n          More Subtle\n        </Link>\n        <div style={{ color: \"blue\" }}>\n          <Link color=\"inherit\" href=\"\">\n            Inherit\n          </Link>\n        </div>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Stretched link\">\n      <Box css={{ position: \"relative\" }}>\n        <Link href=\"\" stretched>\n          This link stretches to cover the parent container\n        </Link>\n      </Box>\n    </StorySection>\n    <StorySection title=\"Disabled link\">\n      <StoryGrid css={{ alignItems: \"flex-start\" }}>\n        <Link href=\"\" aria-disabled=\"true\">\n          Regular disabled\n        </Link>\n        <Link href=\"\" variant=\"label\" aria-disabled=\"true\">\n          Label disabled\n        </Link>\n        <Link href=\"\" variant=\"mono\" aria-disabled=\"true\">\n          Mono disabled\n        </Link>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n\nexport { LinkStory as Link };\n"
  },
  {
    "path": "packages/design-system/src/components/link.tsx",
    "content": "import { forwardRef } from \"react\";\nimport { styled, theme } from \"../stitches.config\";\nimport { textVariants } from \"./text\";\nimport { ExternalLinkIcon } from \"@webstudio-is/icons\";\n\nexport const IconLink = forwardRef<\n  HTMLAnchorElement,\n  React.ComponentProps<typeof Link> & { size?: number | string }\n>(({ size, ...props }, ref) => (\n  <Link {...props} ref={ref}>\n    <ExternalLinkIcon size={size} />\n  </Link>\n));\n\nexport const Link = styled(\"a\", {\n  cursor: \"pointer\",\n  \"&[aria-disabled=true]\": {\n    cursor: \"default\",\n    color: theme.colors.foregroundDisabled,\n    \"&:hover, &:visited\": {\n      color: theme.colors.foregroundDisabled,\n    },\n  },\n  \"&:focus-visible\": {\n    outline: `1px solid ${theme.colors.borderFocus}`,\n  },\n  variants: {\n    variant: {\n      inherit: {\n        fontFamily: \"inherit\",\n        fontWeight: \"inherit\",\n        fontSize: \"inherit\",\n        lineHeight: \"inherit\",\n        letterSpacing: \"inherit\",\n        textTransform: \"inherit\",\n        textIndent: \"inherit\",\n        textDecoration: \"underline\",\n      },\n      regular: textVariants.regularLink,\n      label: textVariants.labelLink,\n      mono: textVariants.monoLink,\n      monoBold: textVariants.monoBoldLink,\n    },\n    color: {\n      main: {\n        color: theme.colors.foregroundMain,\n        \"&:hover, &:visited\": { color: theme.colors.foregroundMain },\n      },\n      contrast: {\n        color: theme.colors.foregroundContrastMain,\n        \"&:hover, &:visited\": { color: theme.colors.foregroundContrastMain },\n      },\n      subtle: {\n        color: theme.colors.foregroundTextSubtle,\n        \"&:hover, &:visited\": { color: theme.colors.foregroundTextSubtle },\n      },\n      moreSubtle: {\n        color: theme.colors.foregroundTextMoreSubtle,\n        \"&:hover, &:visited\": { color: theme.colors.foregroundTextMoreSubtle },\n      },\n      inherit: {\n        color: \"inherit\",\n        \"&:hover, &:visited\": { color: \"inherit\" },\n      },\n    },\n    underline: {\n      none: {\n        textDecoration: \"none\",\n        \"&:hover\": { textDecoration: \"none\" },\n      },\n      hover: {\n        textDecoration: \"none\",\n        \"&:hover\": { textDecoration: \"underline\" },\n      },\n      always: {\n        textDecoration: \"underline\",\n        \"&:hover\": { textDecoration: \"underline\" },\n      },\n    },\n    stretched: {\n      true: {\n        \"&::after\": {\n          content: '\"\"',\n          position: \"absolute\",\n          inset: 0,\n        },\n      },\n    },\n  },\n  defaultVariants: {\n    variant: \"regular\",\n    color: \"main\",\n    underline: \"always\",\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/list-position-indicator.stories.tsx",
    "content": "import {\n  ListPositionIndicator as ListPositionIndicatorComponent,\n  TreePositionIndicator,\n} from \"./list-position-indicator\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"List Position Indicator\",\n  parameters: {\n    // to make white outline visible\n    backgrounds: { default: \"Maintenance Medium\" },\n  },\n};\n\nexport const ListPositionIndicator = () => (\n  <>\n    <StorySection title=\"Coordinates check\">\n      <StoryGrid>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 250,\n            height: 30,\n          }}\n        >\n          <ListPositionIndicatorComponent x={0} y={0} length={200} />\n        </div>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 250,\n            height: 30,\n          }}\n        >\n          <TreePositionIndicator x={0} y={0} length={200} />\n        </div>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Length check\">\n      <StoryGrid>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 200,\n            height: 30,\n          }}\n        >\n          <ListPositionIndicatorComponent x={0} y={0} length={200} />\n        </div>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 200,\n            height: 30,\n          }}\n        >\n          <TreePositionIndicator x={0} y={0} length={200} />\n        </div>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Offset positions\">\n      <StoryGrid>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 250,\n            height: 50,\n          }}\n        >\n          <ListPositionIndicatorComponent x={20} y={15} length={180} />\n        </div>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 250,\n            height: 50,\n          }}\n        >\n          <TreePositionIndicator x={20} y={25} length={180} />\n        </div>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"String position values\">\n      <StoryGrid>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 250,\n            height: 30,\n          }}\n        >\n          <ListPositionIndicatorComponent x=\"10%\" y=\"50%\" length=\"80%\" />\n        </div>\n        <div\n          style={{\n            background: \"black\",\n            position: \"relative\",\n            width: 250,\n            height: 30,\n          }}\n        >\n          <TreePositionIndicator x=\"10%\" y=\"50%\" length=\"80%\" />\n        </div>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/list-position-indicator.tsx",
    "content": "/**\n * Implementation of the \"List Position Indicator\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=8%3A1883\n */\n\nimport { Box } from \"./box\";\nimport { styled, theme } from \"../stitches.config\";\n\nconst CIRCLE_SIZE = 8;\nconst LINE_THICKNESS = 2;\nconst OUTLINE_WIDTH = 1;\n\n// overlap the line with the circle by this much\n// to make sure they are connected\nconst OVERLAP = 2;\n\nconst Container = styled(Box, {\n  position: \"absolute\",\n  zIndex: 1,\n});\n\nconst CircleOutline = styled(Box, {\n  width: CIRCLE_SIZE + OUTLINE_WIDTH * 2,\n  height: CIRCLE_SIZE + OUTLINE_WIDTH * 2,\n  position: \"absolute\",\n  top: -CIRCLE_SIZE / 2 - OUTLINE_WIDTH,\n  left: -CIRCLE_SIZE / 2 - OUTLINE_WIDTH,\n  borderRadius: \"50%\",\n  pointerEvents: \"none\",\n  backgroundColor: theme.colors.borderContrast,\n});\n\nconst Circle = styled(Box, {\n  width: CIRCLE_SIZE,\n  height: CIRCLE_SIZE,\n  position: \"absolute\",\n  top: -CIRCLE_SIZE / 2,\n  left: -CIRCLE_SIZE / 2,\n  border: `2px solid ${theme.colors.backgroundPrimary}`,\n  borderRadius: \"50%\",\n  pointerEvents: \"none\",\n});\n\nconst Line = styled(Box, {\n  boxSizing: \"content-box\",\n  position: \"absolute\",\n  top: -LINE_THICKNESS / 2,\n  left: 0,\n  width: \"100%\",\n  height: LINE_THICKNESS,\n  backgroundColor: theme.colors.backgroundPrimary,\n  pointerEvents: \"none\",\n  outline: `solid ${theme.colors.borderContrast}`,\n  outlineWidth: OUTLINE_WIDTH,\n});\n\nconst LineWithNub = styled(Line, {\n  left: CIRCLE_SIZE / 2 - OVERLAP,\n  width: `calc(100% - ${CIRCLE_SIZE / 2 - OVERLAP}px)`,\n});\n\nexport const ListPositionIndicator = ({\n  x,\n  y,\n  length,\n}: {\n  x: number | string;\n  y: number | string;\n  length: number | string;\n}) => {\n  return (\n    <Container style={{ top: y, left: x, width: length }}>\n      <Line />\n    </Container>\n  );\n};\n\nexport const TreePositionIndicator = ({\n  x,\n  y,\n  length,\n}: {\n  x: number | string;\n  y: number | string;\n  length: number | string;\n}) => {\n  return (\n    <Container style={{ top: y, left: x, width: length }}>\n      <CircleOutline />\n      <LineWithNub />\n      <Circle />\n    </Container>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/menu.stories.tsx",
    "content": "// We demonstrate dropdown-menu/combobox/etc instead of menu.tsx,\n// because what menu.tsx exports is not intended to be used directly\n\nimport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuArrow,\n  DropdownMenuSeparator,\n  DropdownMenuCheckboxItem,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n  DropdownMenuLabel,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n} from \"./dropdown-menu\";\nimport {\n  useCombobox,\n  ComboboxRoot,\n  ComboboxContent,\n  ComboboxAnchor,\n  ComboboxListbox,\n  ComboboxListboxItem,\n  ComboboxLabel,\n  ComboboxSeparator,\n} from \"./combobox\";\nimport {\n  Select,\n  SelectItem,\n  SelectLabel,\n  SelectSeparator,\n  SelectGroup,\n} from \"./select\";\nimport {\n  MenuCheckedAndSetIcon,\n  MenuCheckedIcon,\n  MenuSetDotIcon,\n  MenuList,\n  MenuItemButton,\n} from \"./menu\";\nimport { Button } from \"./button\";\nimport {\n  ChevronDownIcon,\n  TrashIcon,\n  EllipsesIcon,\n  DotIcon,\n} from \"@webstudio-is/icons\";\nimport { useState } from \"react\";\nimport { StorySection } from \"./storybook\";\nimport { InputField } from \"./input-field\";\nimport { NestedInputButton } from \"./nested-input-button\";\n\nconst DropdownDemo = ({ withIndicator }: { withIndicator: boolean }) => {\n  const [isApple, setIsApple] = useState(true);\n  const [isOrange, setIsOrange] = useState(true);\n  const [isPeach, setIsPeach] = useState(true);\n  const [radioValue, setRadioValue] = useState(\"apple\");\n\n  return (\n    <DropdownMenu defaultOpen>\n      <DropdownMenuTrigger asChild>\n        <Button prefix={<EllipsesIcon />} />\n      </DropdownMenuTrigger>\n      <DropdownMenuContent width=\"regular\">\n        <DropdownMenuLabel>Not choosable</DropdownMenuLabel>\n\n        <DropdownMenuItem withIndicator={withIndicator}>\n          Create\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          withIndicator={withIndicator}\n          icon={withIndicator ? <TrashIcon /> : null}\n          destructive\n        >\n          Delete\n        </DropdownMenuItem>\n        <DropdownMenuItem withIndicator={withIndicator} disabled>\n          Disabled\n        </DropdownMenuItem>\n\n        <DropdownMenuSeparator />\n        <DropdownMenuLabel>Sub-menu</DropdownMenuLabel>\n\n        <DropdownMenuSub>\n          <DropdownMenuSubTrigger withIndicator={withIndicator}>\n            Open sub-menu\n          </DropdownMenuSubTrigger>\n          <DropdownMenuSubContent width=\"regular\">\n            <DropdownMenuItem withIndicator={withIndicator}>\n              Regular\n            </DropdownMenuItem>\n            <DropdownMenuItem withIndicator={withIndicator} destructive>\n              Destructive\n            </DropdownMenuItem>\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n\n        {withIndicator && (\n          <>\n            <DropdownMenuSeparator />\n            <DropdownMenuLabel>Choose many</DropdownMenuLabel>\n\n            <DropdownMenuCheckboxItem\n              checked={isApple}\n              onSelect={() => setIsApple(isApple === false)}\n            >\n              Apple\n            </DropdownMenuCheckboxItem>\n            <DropdownMenuCheckboxItem\n              checked={isOrange}\n              icon={<MenuCheckedAndSetIcon />}\n              onSelect={() => setIsOrange(isOrange === false)}\n            >\n              Orange\n            </DropdownMenuCheckboxItem>\n            <DropdownMenuCheckboxItem\n              checked={isPeach}\n              icon={<MenuSetDotIcon />}\n              onSelect={() => setIsPeach(isPeach === false)}\n            >\n              Peach\n            </DropdownMenuCheckboxItem>\n\n            <DropdownMenuSeparator />\n            <DropdownMenuLabel>Choose one</DropdownMenuLabel>\n\n            <DropdownMenuRadioGroup\n              value={radioValue}\n              onValueChange={setRadioValue}\n            >\n              <DropdownMenuRadioItem icon={<MenuCheckedIcon />} value=\"apple\">\n                Apple\n              </DropdownMenuRadioItem>\n              <DropdownMenuRadioItem icon={<MenuCheckedIcon />} value=\"orange\">\n                Orange\n              </DropdownMenuRadioItem>\n            </DropdownMenuRadioGroup>\n          </>\n        )}\n        <DropdownMenuSeparator />\n        <DropdownMenuItem hint>Hint</DropdownMenuItem>\n        <DropdownMenuArrow />\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n};\n\ntype Fruit = \"Apple\" | \"Banana\" | \"Orange\" | \"Peach\";\nconst fruits: Fruit[] = [\"Orange\", \"Apple\", \"Peach\", \"Banana\"];\n\nconst ComboboxDemo = () => {\n  const [selectedItem, onItemSelect] = useState<Fruit>();\n\n  const {\n    isOpen,\n    items,\n    getInputProps,\n    getComboboxProps,\n    getToggleButtonProps,\n    getMenuProps,\n    getItemProps,\n  } = useCombobox<Fruit>({\n    getItems: () => fruits,\n    itemToString: (item) => item ?? \"\",\n    value: null,\n    selectedItem,\n    onItemSelect,\n  });\n\n  const renderItem = (item: Fruit) => (\n    <ComboboxListboxItem\n      {...getItemProps({ item, index: items.indexOf(item) })}\n      key={item}\n      destructive={item === \"Orange\"}\n      disabled={item === \"Peach\"}\n      icon={item === \"Apple\" ? <DotIcon /> : undefined}\n    >\n      {item}\n    </ComboboxListboxItem>\n  );\n\n  const roundItems = items.filter((item) => item !== \"Banana\");\n  const longItems = items.filter((item) => item === \"Banana\");\n\n  return (\n    <ComboboxRoot open={isOpen}>\n      <div {...getComboboxProps()}>\n        <ComboboxAnchor>\n          <InputField\n            {...getInputProps()}\n            placeholder=\"Enter: Apple\"\n            suffix={\n              <NestedInputButton {...getToggleButtonProps()}>\n                <ChevronDownIcon />\n              </NestedInputButton>\n            }\n          />\n        </ComboboxAnchor>\n        <ComboboxContent align=\"end\" sideOffset={5}>\n          <ComboboxListbox {...getMenuProps()}>\n            {roundItems.length > 0 && (\n              <>\n                <ComboboxLabel>Round</ComboboxLabel>\n                {roundItems.map(renderItem)}\n              </>\n            )}\n            {roundItems.length > 0 && longItems.length > 0 && (\n              <ComboboxSeparator />\n            )}\n            {longItems.length > 0 && (\n              <>\n                <ComboboxLabel>Long</ComboboxLabel>\n                {longItems.map(renderItem)}\n              </>\n            )}\n          </ComboboxListbox>\n        </ComboboxContent>\n      </div>\n    </ComboboxRoot>\n  );\n};\n\nconst BasicSelectDemo = () => {\n  const [value, setValue] = useState<Fruit>(\"Apple\");\n  return (\n    <Select\n      options={fruits}\n      value={value}\n      onChange={(value) => {\n        setValue(value as Fruit);\n      }}\n    />\n  );\n};\n\nconst ComplexSelectDemo = () => {\n  const [value, setValue] = useState<Fruit>(\"Orange\");\n  return (\n    <Select\n      options={fruits}\n      value={value}\n      onChange={(value) => {\n        setValue(value as Fruit);\n      }}\n    >\n      <SelectGroup>\n        <SelectLabel>Round</SelectLabel>\n        <SelectItem destructive value=\"Orange\" textValue=\"Orange\">\n          Orange (destructive)\n        </SelectItem>\n        <SelectItem icon={<DotIcon />} value=\"Apple\" textValue=\"Apple\">\n          Apple (custom icon)\n        </SelectItem>\n        <SelectItem disabled value=\"Peach\" textValue=\"Peach\">\n          Peach (disabled)\n        </SelectItem>\n      </SelectGroup>\n      <SelectSeparator />\n      <SelectGroup>\n        <SelectLabel>Long</SelectLabel>\n        <SelectItem value=\"Banana\" textValue=\"Banana\">\n          Banana\n        </SelectItem>\n      </SelectGroup>\n    </Select>\n  );\n};\n\nexport const MenuMenuItem = () => (\n  <>\n    <StorySection title=\"Dropdown menu\">\n      <div style={{ display: \"flex\", paddingBottom: 360 }}>\n        <div style={{ paddingLeft: 100, paddingRight: 100 }}>\n          <DropdownDemo withIndicator={true} />\n        </div>\n        <div style={{ paddingLeft: 100, paddingRight: 100 }}>\n          <DropdownDemo withIndicator={false} />\n        </div>\n      </div>\n    </StorySection>\n    <StorySection title=\"Select menu (Combobox component)\">\n      <div style={{ width: 200 }}>\n        <ComboboxDemo />\n      </div>\n    </StorySection>\n    <StorySection title=\"Basic select menu (Select component)\">\n      <BasicSelectDemo />\n    </StorySection>\n    <StorySection title=\"Complex select menu (Select component)\">\n      <ComplexSelectDemo />\n    </StorySection>\n  </>\n);\n\nexport const StandaloneMenuItems = () => (\n  <StorySection title=\"Standalone menu list\">\n    <MenuList width=\"regular\">\n      <MenuItemButton>Create new</MenuItemButton>\n      <MenuItemButton destructive>Delete item</MenuItemButton>\n      <MenuItemButton disabled>Disabled item</MenuItemButton>\n      <MenuItemButton hint>Hint item</MenuItemButton>\n    </MenuList>\n  </StorySection>\n);\n\nexport default {\n  title: \"Menu, Menu Item\",\n};\n"
  },
  {
    "path": "packages/design-system/src/components/menu.tsx",
    "content": "/**\n * Implementation of the \"Menu\" and \"Menu Item\" components from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=6%3A2104&t=xHSB8rNf2VXrwLAU-0\n *\n * Only CSS is implemented here, and intended to be used with:\n *  - @radix-ui/react-dropdown-menu\n *  - @radix-ui/react-select\n *  - @radix-ui/react-popper & downshift\n *  - @radix-ui/react-context-menu (@todo, not implemented yet)\n *\n * @todo not implemented yet Figma features:\n *  - Component: \"Menu Item Large\"\n *  - Type of \"Menu\" component: \"Dropdown w/large items\"\n *  - Type of \"Menu\" component: \"Context menu\"\n *\n * @todo: group everything under a folder same as floating-panel?\n */\n\nimport { css, styled, theme } from \"../stitches.config\";\nimport { textVariants } from \"./text\";\nimport {\n  Arrow as BaseDropdownMenuArrow,\n  SubContent,\n} from \"@radix-ui/react-dropdown-menu\";\nimport { CheckMarkIcon, DotIcon } from \"@webstudio-is/icons\";\nimport type { ComponentProps } from \"react\";\nimport { truncate } from \"../utilities\";\n\nexport const labelCss = css(textVariants.titles, {\n  color: theme.colors.foregroundMain,\n  mx: theme.spacing[3],\n  padding: theme.spacing[3],\n  order: 1,\n  ...truncate(),\n});\n\nconst indicatorSize = theme.spacing[9];\nexport const menuItemIndicatorCss = css({\n  position: \"absolute\",\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  left: theme.spacing[3],\n  width: indicatorSize,\n  height: indicatorSize,\n});\n\nexport const MenuItemIndicator = styled(\"span\", menuItemIndicatorCss);\n\nconst itemMargin = theme.spacing[3];\nexport const menuItemCss = css({\n  outline: \"none\",\n  cursor: \"default\",\n  position: \"relative\",\n  display: \"flex\",\n  order: 1,\n  alignItems: \"center\",\n  color: theme.colors.foregroundMain,\n  mx: itemMargin,\n  padding: theme.spacing[3],\n  borderRadius: theme.borderRadius[3],\n  // override button default styles\n  backgroundColor: \"transparent\",\n  \"&:focus, &[data-found], &[aria-selected=true], &[data-state=open], &[data-state=checked]:is(:hover,:focus)\":\n    {\n      backgroundColor: theme.colors.backgroundItemMenuItemHover,\n    },\n  \"&[data-disabled], &[aria-disabled], &[disabled]\": {\n    color: theme.colors.foregroundDisabled,\n  },\n  variants: {\n    text: {\n      title: textVariants.labels,\n      sentence: textVariants.labels,\n    },\n    withIndicator: {\n      true: {\n        paddingLeft: `calc(${theme.spacing[3]} + ${indicatorSize} + ${theme.spacing[3]})`,\n      },\n    },\n    destructive: {\n      true: {\n        color: theme.colors.foregroundDestructive,\n      },\n    },\n    hint: {\n      true: {\n        ...textVariants.labels,\n        px: theme.spacing[5],\n        background: theme.colors.backgroundMenuHint,\n        borderRadius: theme.borderRadius[2],\n        overflow: \"hidden\",\n        \"&::before\": {\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          content: '\"\"',\n          width: 2,\n          height: \"100%\",\n          background: theme.colors.backgroundGradientVertical,\n        },\n      },\n    },\n  },\n  defaultVariants: { text: \"title\" },\n});\n\n// To use outside of any menu context, e.g. in a Popover\nexport const MenuItemButton = styled(\"button\", menuItemCss, {\n  border: \"none\",\n  boxSizing: \"border-box\",\n  width: `calc(100% - ${itemMargin} * 2)`,\n  \"&:focus:not(:focus-visible)\": { backgroundColor: \"unset\" },\n  \"&:hover:not([diabled])\": {\n    backgroundColor: theme.colors.backgroundItemMenuItemHover,\n  },\n});\n\nexport const separatorCss = css({\n  height: 1,\n  minHeight: 1,\n  my: theme.spacing[3],\n  backgroundColor: theme.colors.borderMain,\n  order: 1,\n});\n\nconst menuPadding = theme.spacing[3];\nconst menuBorderWidth = \"1px\";\n\nexport const menuCss = css({\n  boxSizing: \"border-box\",\n  borderRadius: theme.borderRadius[6],\n  backgroundColor: theme.colors.backgroundMenu,\n  border: `1px solid ${theme.colors.borderMain}`,\n  boxShadow: `${theme.shadows.menuDropShadow}, inset 0 0 0 1px ${theme.colors.borderMenuInner}`,\n  padding: `${menuPadding} 0`,\n  variants: {\n    width: {\n      regular: { width: theme.spacing[26] },\n    },\n  },\n});\n\nexport const MenuList = styled(\"div\", menuCss);\n\nexport const subMenuCss = css(menuCss, {\n  // the goal is to align the top menu item in a sub menu\n  // with the menu item in the parent menu that opened it\n  marginTop: `calc((${menuPadding} + ${menuBorderWidth}) * -1)`,\n});\n\nexport const subContentProps: Partial<ComponentProps<typeof SubContent>> = {\n  // this depends on menuItemCss.margin and menuCss.padding,\n  // the goal is to make sub-menu overlap the parent menu by exactly 2px\n  sideOffset: 3,\n};\n\n// Arrow is hard to implement with just CSS,\n// so we implement it as a component\nconst ArrowBackground = styled(\"path\", { fill: theme.colors.backgroundMenu });\nconst ArrowInnerBorder = styled(\"path\", { fill: theme.colors.borderMenuInner });\nconst ArrowOuterBorder = styled(\"path\", { fill: theme.colors.borderMain });\nconst ArrowSgv = styled(\"svg\", { transform: \"translateY(-3px)\" });\nexport const DropdownMenuArrow = () => (\n  <BaseDropdownMenuArrow width={16} height={11} asChild>\n    <ArrowSgv xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 11\">\n      <ArrowOuterBorder d=\"M8.73 9.76a1 1 0 0 1-1.46 0L.5 2.54h15L8.73 9.76Z\" />\n      <ArrowInnerBorder d=\"M8.146 8.909a.2.2 0 0 1-.292 0L.5 1.065h15L8.146 8.909Z\" />\n      <ArrowBackground d=\"M8.073 7.52a.1.1 0 0 1-.146 0L.877 0h14.246l-7.05 7.52Z\" />\n    </ArrowSgv>\n  </BaseDropdownMenuArrow>\n);\n\nconst setIconStyle = css({\n  color: theme.colors.foregroundPrimary,\n});\n\n// Icon for the \"checked\" state from Figma\nexport const MenuCheckedIcon = () => <CheckMarkIcon size={12} />;\n\n// Icon for the \"checked and set\" state from Figma\nexport const MenuCheckedAndSetIcon = () => (\n  <CheckMarkIcon className={setIconStyle()} />\n);\n\n// Icon for the \"set dot\" state from Figma\nexport const MenuSetDotIcon = () => <DotIcon className={setIconStyle()} />;\n"
  },
  {
    "path": "packages/design-system/src/components/nested-icon-label.stories.tsx",
    "content": "import { NestedIconLabel as NestedIconLabelComponent } from \"./nested-icon-label\";\nimport { labelColors } from \"./label\";\nimport { GapVerticalIcon } from \"@webstudio-is/icons\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Nested Icon Label\",\n};\n\nexport const NestedIconLabel = () => {\n  return (\n    <>\n      <StorySection title=\"Default\">\n        <StoryGrid horizontal>\n          {labelColors.map((color) => (\n            <NestedIconLabelComponent key={color} color={color}>\n              <GapVerticalIcon />\n            </NestedIconLabelComponent>\n          ))}\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Hover\">\n        <StoryGrid horizontal>\n          {labelColors.map((color) => (\n            <NestedIconLabelComponent key={color} color={color} hover>\n              <GapVerticalIcon />\n            </NestedIconLabelComponent>\n          ))}\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Disabled\">\n        <StoryGrid horizontal>\n          {labelColors.map((color) => (\n            <NestedIconLabelComponent key={color} color={color} disabled>\n              <GapVerticalIcon />\n            </NestedIconLabelComponent>\n          ))}\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/nested-icon-label.tsx",
    "content": "/**\n * Implementation of the \"Nested Icon Label\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=148-3161\n */\n\nimport { forwardRef, type ComponentProps, type Ref } from \"react\";\nimport { type CSS, css, theme } from \"../stitches.config\";\nimport type { labelColors } from \"./label\";\n\nconst colors = {\n  default: {\n    border: \"transparent\",\n    background: \"transparent\",\n    backgroundHover: theme.colors.backgroundHover,\n    icon: theme.colors.foregroundIconMain,\n  },\n  preset: {\n    border: theme.colors.borderMain,\n    background: theme.colors.backgroundPresetMain,\n    backgroundHover: theme.colors.backgroundPresetHover,\n    icon: theme.colors.foregroundIconMain,\n  },\n  local: {\n    border: theme.colors.borderLocalMain,\n    background: theme.colors.backgroundLocalMain,\n    backgroundHover: theme.colors.backgroundLocalHover,\n    icon: theme.colors.foregroundLocalMain,\n  },\n  overwritten: {\n    border: theme.colors.borderOverwrittenMain,\n    background: theme.colors.backgroundOverwrittenMain,\n    backgroundHover: theme.colors.backgroundOverwrittenHover,\n    icon: theme.colors.foregroundOverwrittenMain,\n  },\n  remote: {\n    border: theme.colors.borderRemoteMain,\n    background: theme.colors.backgroundRemoteMain,\n    backgroundHover: theme.colors.backgroundRemoteHover,\n    icon: theme.colors.foregroundRemoteMain,\n  },\n  inactive: {\n    border: \"transparent\",\n    background: \"transparent\",\n    backgroundHover: \"transparent\",\n    icon: theme.colors.foregroundSubtle,\n  },\n} as const;\n\nconst perColorStyle = (color: (typeof labelColors)[number]) => ({\n  \"&:not([data-state=disabled])\": {\n    color: colors[color].icon,\n    borderColor: colors[color].border,\n    background: colors[color].background,\n  },\n  \"&:not([data-state=disabled]):hover, &[data-state=hover]\": {\n    background: colors[color].backgroundHover,\n  },\n  \"&[data-state=disabled]\": {\n    color: theme.colors.foregroundDisabled,\n  },\n});\n\nconst style = css({\n  display: \"flex\",\n  width: theme.spacing[10],\n  height: theme.spacing[10],\n  boxSizing: \"border-box\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  borderRadius: theme.borderRadius[2],\n  border: \"1px solid transparent\",\n  variants: {\n    color: {\n      default: perColorStyle(\"default\"),\n      preset: perColorStyle(\"preset\"),\n      local: perColorStyle(\"local\"),\n      overwritten: perColorStyle(\"overwritten\"),\n      remote: perColorStyle(\"remote\"),\n      inactive: perColorStyle(\"inactive\"),\n    },\n  },\n  defaultVariants: { color: \"default\" },\n});\n\ntype Props = ComponentProps<\"label\"> & {\n  css?: CSS;\n  color?: (typeof labelColors)[number];\n  disabled?: boolean;\n  hover?: boolean;\n};\n\nexport const NestedIconLabel = forwardRef(\n  (\n    { css, className, color, disabled, hover, ...props }: Props,\n    ref: Ref<HTMLLabelElement>\n  ) => (\n    <label\n      {...props}\n      className={style({ css, className, color })}\n      data-state={disabled ? \"disabled\" : hover ? \"hover\" : undefined}\n      ref={ref}\n    />\n  )\n);\nNestedIconLabel.displayName = \"NestedIconLabel\";\n"
  },
  {
    "path": "packages/design-system/src/components/nested-input-button.stories.tsx",
    "content": "import { CopyIcon } from \"@webstudio-is/icons\";\nimport {\n  NestedInputButton,\n  nestedSelectButtonUnitless,\n} from \"./nested-input-button\";\nimport { StoryGrid, StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Nested Select Button\",\n};\n\nexport const NestedSelectButton = () => {\n  return (\n    <>\n      <StorySection title=\"Text\">\n        <StoryGrid horizontal>\n          <NestedInputButton>px</NestedInputButton>\n          <NestedInputButton data-state=\"hover\">px</NestedInputButton>\n          <NestedInputButton data-state=\"open\">px</NestedInputButton>\n          <NestedInputButton disabled>px</NestedInputButton>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Chevron down icon\">\n        <StoryGrid horizontal>\n          <NestedInputButton />\n          <NestedInputButton data-state=\"hover\" />\n          <NestedInputButton data-state=\"open\" />\n          <NestedInputButton disabled />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Any Icon\">\n        <StoryGrid horizontal>\n          <NestedInputButton>\n            <CopyIcon />\n          </NestedInputButton>\n          <NestedInputButton data-state=\"hover\">\n            <CopyIcon />\n          </NestedInputButton>\n          <NestedInputButton data-state=\"open\">\n            <CopyIcon />\n          </NestedInputButton>\n          <NestedInputButton disabled>\n            <CopyIcon />\n          </NestedInputButton>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Unitless value\">\n        <StoryGrid horizontal>\n          <NestedInputButton>{nestedSelectButtonUnitless}</NestedInputButton>\n          <NestedInputButton data-state=\"hover\">\n            {nestedSelectButtonUnitless}\n          </NestedInputButton>\n          <NestedInputButton data-state=\"open\">\n            {nestedSelectButtonUnitless}\n          </NestedInputButton>\n          <NestedInputButton disabled>\n            {nestedSelectButtonUnitless}\n          </NestedInputButton>\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/nested-input-button.tsx",
    "content": "/**\n * Implementation of the \"Nested Input Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=148-3113\n */\n\nimport { ChevronDownIcon } from \"@webstudio-is/icons\";\nimport { forwardRef, type ComponentProps, type Ref } from \"react\";\nimport { textVariants } from \"./text\";\nimport { type CSS, css, theme } from \"../stitches.config\";\n\n// From Figma:\n// In production the unitless unit should be an en dash with a space before and after\nexport const nestedSelectButtonUnitless = \" – \";\n\nconst style = css({\n  all: \"unset\",\n  ...textVariants.unit,\n  color: theme.colors.foregroundSubtle,\n  borderRadius: theme.borderRadius[2],\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  whiteSpace: \"pre\", // to make nestedSelectButtonUnitless work as expected\n  height: theme.spacing[10],\n  \"&:not(:has(svg))\": {\n    paddingLeft: theme.spacing[2],\n    paddingRight: theme.spacing[2],\n  },\n  \"&[data-state=hover], &:not([data-state=open], :disabled, :focus-visible):hover\":\n    {\n      color: theme.colors.foregroundMain,\n      backgroundColor: theme.colors.backgroundHover,\n    },\n  \"&[data-state=open], &:focus-visible\": {\n    color: theme.colors.foregroundContrastMain,\n    backgroundColor: theme.colors.backgroundActive,\n  },\n  \"&:disabled\": {\n    color: theme.colors.foregroundDisabled,\n  },\n  variants: {\n    /**\n     * ChevronDownIcon is the only case when we have svg inside the button and width is not equal to height\n     */\n    hasChildren: {\n      true: {\n        \"&:where(:has(svg))\": {\n          paddingInline: theme.spacing[2],\n        },\n      },\n    },\n  },\n});\n\nexport const NestedInputButton = forwardRef(\n  (\n    {\n      css,\n      className,\n      children,\n      ...props\n    }: ComponentProps<\"button\"> & { css?: CSS },\n    ref: Ref<HTMLButtonElement>\n  ) => {\n    return (\n      <button\n        className={style({\n          css,\n          className,\n          hasChildren: children !== undefined,\n        })}\n        {...props}\n        ref={ref}\n      >\n        {children ?? <ChevronDownIcon />}\n      </button>\n    );\n  }\n);\nNestedInputButton.displayName = \"NestedInputButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/panel-banner.stories.tsx",
    "content": "import {\n  PanelBanner as PanelBannerComponent,\n  panelBannerIconColor,\n} from \"./panel-banner\";\nimport { StoryGrid, StorySection } from \"./storybook\";\nimport { Text } from \"./text\";\nimport { Link } from \"./link\";\nimport { Flex } from \"./flex\";\nimport { buttonStyle } from \"./button\";\nimport {\n  AlertIcon,\n  InfoCircleIcon,\n  CheckMarkIcon,\n  AlertCircleIcon,\n} from \"@webstudio-is/icons\";\n\nexport default {\n  title: \"Panel Banner\",\n};\n\nexport const PanelBanner = () => {\n  return (\n    <>\n      <StorySection title=\"Panel Banner - Info (default)\">\n        <StoryGrid horizontal>\n          <PanelBannerComponent css={{ width: 300 }}>\n            <Text variant=\"regularBold\">Free domains limit reached</Text>\n            <Text variant=\"regular\">\n              You have reached the limit of 5 custom domains on your account.{\" \"}\n              <Text variant=\"regularBold\" inline>\n                Upgrade to a Pro account\n              </Text>{\" \"}\n              to add unlimited domains.\n            </Text>\n            <Link\n              className={buttonStyle({ color: \"gradient\" })}\n              color=\"contrast\"\n              underline=\"none\"\n              href=\"https://webstudio.is/pricing\"\n              target=\"_blank\"\n            >\n              Upgrade\n            </Link>\n          </PanelBannerComponent>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Panel Banner - Variants with Icons\">\n        <StoryGrid horizontal>\n          <PanelBannerComponent variant=\"info\" css={{ width: 280 }}>\n            <Flex align=\"center\" gap={1}>\n              <InfoCircleIcon color={panelBannerIconColor} />\n              <Text variant=\"regularBold\">Info</Text>\n            </Flex>\n            <Text variant=\"regular\">This is an informational message.</Text>\n          </PanelBannerComponent>\n          <PanelBannerComponent variant=\"warning\" css={{ width: 280 }}>\n            <Flex align=\"center\" gap={1}>\n              <AlertIcon color={panelBannerIconColor} />\n              <Text variant=\"regularBold\">Warning</Text>\n            </Flex>\n            <Text variant=\"regular\">This is a warning message.</Text>\n          </PanelBannerComponent>\n          <PanelBannerComponent variant=\"error\" css={{ width: 280 }}>\n            <Flex align=\"center\" gap={1}>\n              <AlertCircleIcon color={panelBannerIconColor} />\n              <Text variant=\"regularBold\">Error</Text>\n            </Flex>\n            <Text variant=\"regular\">This is an error message.</Text>\n          </PanelBannerComponent>\n          <PanelBannerComponent variant=\"success\" css={{ width: 280 }}>\n            <Flex align=\"center\" gap={1}>\n              <CheckMarkIcon color={panelBannerIconColor} />\n              <Text variant=\"regularBold\">Success</Text>\n            </Flex>\n            <Text variant=\"regular\">This is a success message.</Text>\n          </PanelBannerComponent>\n          <PanelBannerComponent variant=\"neutral\" css={{ width: 280 }}>\n            <Flex align=\"center\" gap={1}>\n              <InfoCircleIcon color={panelBannerIconColor} />\n              <Text variant=\"regularBold\">Neutral</Text>\n            </Flex>\n            <Text variant=\"regular\">This is a neutral message.</Text>\n          </PanelBannerComponent>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Compact banners\">\n        <StoryGrid horizontal>\n          <PanelBannerComponent style={{ width: 280 }}>\n            <Text variant=\"regular\">\n              Simple informational message without a heading.\n            </Text>\n          </PanelBannerComponent>\n          <PanelBannerComponent variant=\"warning\" style={{ width: 280 }}>\n            <Text variant=\"regular\">\n              A brief warning message without icons or headings.\n            </Text>\n          </PanelBannerComponent>\n          <PanelBannerComponent variant=\"error\" style={{ width: 280 }}>\n            <Text variant=\"regular\">\n              Something went wrong. Please try again.\n            </Text>\n          </PanelBannerComponent>\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/panel-banner.tsx",
    "content": "import { styled, theme } from \"../stitches.config\";\nimport { Box } from \"./box\";\n\nconst iconColor = \"--ws-panel-banner-icon-color\";\n\nexport const panelBannerIconColor = `var(${iconColor})`;\n\nexport const PanelBanner = styled(Box, {\n  display: \"flex\",\n  gap: theme.spacing[7],\n  flexDirection: \"column\",\n  backgroundColor: theme.colors.backgroundInfoNotification,\n  padding: theme.panel.padding,\n  [iconColor]: theme.colors.foregroundInfo,\n\n  variants: {\n    variant: {\n      info: {},\n      warning: {\n        backgroundColor: theme.colors.backgroundAlertNotification,\n        [iconColor]: theme.colors.backgroundAlertMain,\n      },\n      error: {\n        backgroundColor: theme.colors.backgroundDestructiveNotification,\n        [iconColor]: theme.colors.foregroundDestructive,\n      },\n      success: {\n        backgroundColor: theme.colors.backgroundSuccessNotification,\n        [iconColor]: theme.colors.foregroundSuccess,\n      },\n      neutral: {\n        backgroundColor: theme.colors.backgroundNeutralNotification,\n        [iconColor]: theme.colors.foregroundMain,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/panel-tabs.stories.tsx",
    "content": "import { StoryGrid, StorySection } from \"./storybook\";\nimport {\n  PanelTabs as PanelTabsComponent,\n  PanelTabsList,\n  PanelTabsTrigger,\n  PanelTabsContent,\n} from \"./panel-tabs\";\nimport { Box } from \"./box\";\nimport { Text } from \"./text\";\nimport { theme } from \"../stitches.config\";\n\nexport default {\n  title: \"Panel Tabs\",\n};\n\nconst Wrap = ({ children }: { children: React.ReactNode }) => (\n  <div style={{ width: 240, border: \"dashed 3px #e3e3e3\" }}>{children}</div>\n);\n\nconst Content = ({ children }: { children: React.ReactNode }) => (\n  <Box css={{ padding: theme.spacing[5] }}>\n    <Text>{children}</Text>\n  </Box>\n);\n\nexport const PanelTabs = () => (\n  <>\n    <StorySection title=\"Three tabs\">\n      <StoryGrid>\n        <Wrap>\n          <PanelTabsComponent defaultValue=\"1\">\n            <PanelTabsList>\n              <PanelTabsTrigger value=\"1\">Tab 1</PanelTabsTrigger>\n              <PanelTabsTrigger value=\"2\">Tab 2</PanelTabsTrigger>\n              <PanelTabsTrigger value=\"3\">Tab 3</PanelTabsTrigger>\n            </PanelTabsList>\n            <PanelTabsContent value=\"1\">\n              <Content>Content 1</Content>\n            </PanelTabsContent>\n            <PanelTabsContent value=\"2\">\n              <Content>Content 2</Content>\n            </PanelTabsContent>\n            <PanelTabsContent value=\"3\">\n              <Content>Content 3</Content>\n            </PanelTabsContent>\n          </PanelTabsComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Disabled tab\">\n      <StoryGrid>\n        <Wrap>\n          <PanelTabsComponent defaultValue=\"1\">\n            <PanelTabsList>\n              <PanelTabsTrigger value=\"1\">Active</PanelTabsTrigger>\n              <PanelTabsTrigger value=\"2\" disabled>\n                Disabled\n              </PanelTabsTrigger>\n              <PanelTabsTrigger value=\"3\">Enabled</PanelTabsTrigger>\n            </PanelTabsList>\n            <PanelTabsContent value=\"1\">\n              <Content>Active tab content</Content>\n            </PanelTabsContent>\n            <PanelTabsContent value=\"3\">\n              <Content>Third tab content</Content>\n            </PanelTabsContent>\n          </PanelTabsComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Two tabs\">\n      <StoryGrid>\n        <Wrap>\n          <PanelTabsComponent defaultValue=\"a\">\n            <PanelTabsList>\n              <PanelTabsTrigger value=\"a\">Design</PanelTabsTrigger>\n              <PanelTabsTrigger value=\"b\">Settings</PanelTabsTrigger>\n            </PanelTabsList>\n            <PanelTabsContent value=\"a\">\n              <Content>Design content</Content>\n            </PanelTabsContent>\n            <PanelTabsContent value=\"b\">\n              <Content>Settings content</Content>\n            </PanelTabsContent>\n          </PanelTabsComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Many tabs\">\n      <StoryGrid>\n        <Wrap>\n          <PanelTabsComponent defaultValue=\"1\">\n            <PanelTabsList>\n              {Array.from({ length: 6 }, (_, i) => (\n                <PanelTabsTrigger key={i} value={String(i + 1)}>\n                  Tab {i + 1}\n                </PanelTabsTrigger>\n              ))}\n            </PanelTabsList>\n            {Array.from({ length: 6 }, (_, i) => (\n              <PanelTabsContent key={i} value={String(i + 1)}>\n                <Content>Content {i + 1}</Content>\n              </PanelTabsContent>\n            ))}\n          </PanelTabsComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/panel-tabs.tsx",
    "content": "/**\n * Implementation of \"Panel Tabs List\" and \"Panel Tab Trigger\" components from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=2647-9488\n */\n\nimport * as Primitive from \"@radix-ui/react-tabs\";\nimport { textVariants } from \"./text\";\nimport { styled, theme } from \"../stitches.config\";\n\nexport const PanelTabs = styled(Primitive.Root, {\n  display: \"flex\",\n  flexDirection: \"column\",\n  \"&[hidden]\": { display: \"none\" },\n});\n\nexport const PanelTabsList = styled(Primitive.List, {\n  display: \"flex\",\n  padding: theme.spacing[5],\n});\n\nexport const PanelTabsTrigger = styled(Primitive.Trigger, {\n  all: \"unset\", // reset <button>\n  ...textVariants.titles,\n  color: theme.colors.foregroundTextMoreSubtle,\n  padding: theme.spacing[3],\n  borderRadius: theme.borderRadius[4],\n\n  \"&:hover\": {\n    backgroundColor: theme.colors.backgroundHover,\n    color: theme.colors.foregroundMain,\n  },\n\n  \"&:focus-visible\": {\n    outline: `1px solid ${theme.colors.borderFocus}`,\n    outlineOffset: \"-1px\",\n  },\n\n  \"&[data-state=active]\": { color: theme.colors.foregroundMain },\n});\n\nexport const PanelTabsContent = styled(Primitive.Content, {\n  display: \"grid\",\n  minHeight: 0,\n  \"&:focus\": { outline: \"none\" },\n  \"&[data-state=inactive]\": { display: \"none\" },\n});\n"
  },
  {
    "path": "packages/design-system/src/components/panel-title.stories.tsx",
    "content": "import {\n  PanelTitle as PanelTitleComponent,\n  TitleSuffixSpacer,\n} from \"./panel-title\";\nimport { Button } from \"./button\";\nimport { XIcon, CopyIcon } from \"@webstudio-is/icons\";\nimport { StoryGrid, StorySection } from \"./storybook\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\n\nexport default {\n  title: \"Panel Title\",\n};\n\nexport const PanelTitle = () => (\n  <>\n    <StorySection title=\"Basic variants\">\n      <StoryGrid>\n        <PanelTitleComponent>Without buttons</PanelTitleComponent>\n\n        <PanelTitleComponent\n          suffix={<Button prefix={<XIcon />} color=\"ghost\" />}\n        >\n          One icon button\n        </PanelTitleComponent>\n\n        <PanelTitleComponent\n          suffix={\n            <>\n              <Button prefix={<XIcon />} color=\"ghost\" />\n              <Button prefix={<CopyIcon />} color=\"ghost\" />\n            </>\n          }\n        >\n          Many icon buttons\n        </PanelTitleComponent>\n\n        <PanelTitleComponent\n          suffix={\n            <>\n              <Button prefix={<XIcon />} color=\"ghost\" />\n              <Button prefix={<CopyIcon />} color=\"ghost\" />\n              <TitleSuffixSpacer />\n              <Button>Button</Button>\n            </>\n          }\n        >\n          Icon and regular buttons\n        </PanelTitleComponent>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Long title\">\n      <StoryGrid>\n        <div style={{ width: 240 }}>\n          <PanelTitleComponent\n            suffix={<Button prefix={<XIcon />} color=\"ghost\" />}\n          >\n            A very long panel title that should be truncated when it overflows\n          </PanelTitleComponent>\n        </div>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Custom children\">\n      <StoryGrid>\n        <PanelTitleComponent\n          suffix={<Button prefix={<XIcon />} color=\"ghost\" />}\n        >\n          <Flex align=\"center\" gap=\"1\">\n            <CopyIcon />\n            <Text variant=\"titles\">With icon</Text>\n          </Flex>\n        </PanelTitleComponent>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/panel-title.tsx",
    "content": "/**\n * Implementation of the \"Panel Title\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=2647-10046\n */\n\nimport {\n  forwardRef,\n  type ComponentProps,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { theme, styled, type CSS } from \"../stitches.config\";\nimport { Text } from \"./text\";\n\ntype TitleProps = ComponentProps<\"div\"> & {\n  suffix?: ReactNode;\n  css?: CSS;\n};\n\nconst Container = styled(\"div\", {\n  display: \"flex\",\n  alignItems: \"center\",\n  flexShrink: 0,\n  justifyContent: \"space-between\",\n  height: theme.spacing[15],\n  paddingInline: theme.panel.paddingInline,\n  paddingRight: theme.spacing[5],\n});\n\nconst SuffixSlot = styled(\"div\", {\n  display: \"flex\",\n  alignItems: \"center\",\n  marginLeft: theme.spacing[5],\n});\n\n// we can't just set a gap on SuffixSlot,\n// because we want no gap between icon-buttons\nexport const TitleSuffixSpacer = styled(\"div\", {\n  width: theme.spacing[5],\n});\n\nexport const PanelTitle = forwardRef(\n  (\n    { children, suffix, className, css, ...rest }: TitleProps,\n    ref: Ref<HTMLDivElement>\n  ) => (\n    <Container className={className} css={css} {...rest} ref={ref}>\n      {typeof children === \"string\" ? (\n        <Text variant=\"titles\" truncate>\n          {children}\n        </Text>\n      ) : (\n        children\n      )}\n      {suffix && <SuffixSlot>{suffix}</SuffixSlot>}\n    </Container>\n  )\n);\nPanelTitle.displayName = \"PanelTitle\";\n"
  },
  {
    "path": "packages/design-system/src/components/popover.stories.tsx",
    "content": "import { Button } from \"./button\";\nimport { Text } from \"./text\";\nimport { StorySection } from \"./storybook\";\nimport {\n  Popover,\n  PopoverClose,\n  PopoverContent,\n  PopoverTitle,\n  PopoverTitleActions,\n  PopoverTrigger,\n  PopoverSeparator,\n} from \"./popover\";\nimport { MenuItemButton } from \"./menu\";\nimport { theme } from \"../stitches.config\";\nimport { Flex } from \"./flex\";\n\nexport default {\n  title: \"Popover\",\n};\n\nconst PopoverDemo = () => (\n  <StorySection title=\"Popover\">\n    <Popover defaultOpen>\n      <PopoverTrigger asChild>\n        <Button>Open</Button>\n      </PopoverTrigger>\n      <PopoverContent>\n        <Flex css={{ padding: theme.spacing[7] }}>\n          <Text>Some content</Text>\n        </Flex>\n        <PopoverSeparator />\n        <Flex css={{ padding: theme.spacing[3] }}>\n          <PopoverClose>\n            <MenuItemButton>Close</MenuItemButton>\n          </PopoverClose>\n        </Flex>\n      </PopoverContent>\n    </Popover>\n  </StorySection>\n);\nexport { PopoverDemo as Popover };\n\nexport const WithTitle = () => (\n  <StorySection title=\"With title\">\n    <Popover defaultOpen>\n      <PopoverTrigger asChild>\n        <Button>Open</Button>\n      </PopoverTrigger>\n      <PopoverContent>\n        <Flex direction=\"column\" gap=\"2\" style={{ padding: 12 }}>\n          <Text>Content with a title bar</Text>\n        </Flex>\n        <PopoverTitle>Panel title</PopoverTitle>\n      </PopoverContent>\n    </Popover>\n  </StorySection>\n);\n\nexport const WithTitleActions = () => (\n  <StorySection title=\"With title actions\">\n    <Popover defaultOpen>\n      <PopoverTrigger asChild>\n        <Button>Open</Button>\n      </PopoverTrigger>\n      <PopoverContent>\n        <Flex direction=\"column\" gap=\"2\" style={{ padding: 12 }}>\n          <Text>Content with title actions</Text>\n        </Flex>\n        <PopoverTitle\n          suffix={\n            <PopoverTitleActions>\n              <PopoverClose />\n            </PopoverTitleActions>\n          }\n        >\n          Settings\n        </PopoverTitle>\n      </PopoverContent>\n    </Popover>\n  </StorySection>\n);\n\nexport const SideRight = () => (\n  <StorySection title=\"Side right\">\n    <Flex style={{ padding: 100 }}>\n      <Popover defaultOpen>\n        <PopoverTrigger asChild>\n          <Button>Right</Button>\n        </PopoverTrigger>\n        <PopoverContent side=\"right\">\n          <Flex direction=\"column\" gap=\"2\" style={{ padding: 12 }}>\n            <Text>Right side</Text>\n          </Flex>\n        </PopoverContent>\n      </Popover>\n    </Flex>\n  </StorySection>\n);\n\nexport const SideTop = () => (\n  <StorySection title=\"Side top\">\n    <Flex style={{ padding: 100 }}>\n      <Popover defaultOpen>\n        <PopoverTrigger asChild>\n          <Button>Top</Button>\n        </PopoverTrigger>\n        <PopoverContent side=\"top\">\n          <Flex direction=\"column\" gap=\"2\" style={{ padding: 12 }}>\n            <Text>Top side</Text>\n          </Flex>\n        </PopoverContent>\n      </Popover>\n    </Flex>\n  </StorySection>\n);\n\nexport const SideBottomWithOffset = () => (\n  <StorySection title=\"Side bottom with offset\">\n    <Flex style={{ padding: 100 }}>\n      <Popover defaultOpen>\n        <PopoverTrigger asChild>\n          <Button>Bottom</Button>\n        </PopoverTrigger>\n        <PopoverContent side=\"bottom\" sideOffset={16}>\n          <Flex direction=\"column\" gap=\"2\" style={{ padding: 12 }}>\n            <Text>Bottom with custom offset</Text>\n          </Flex>\n        </PopoverContent>\n      </Popover>\n    </Flex>\n  </StorySection>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/popover.tsx",
    "content": "import {\n  type ComponentProps,\n  type ReactNode,\n  type Ref,\n  forwardRef,\n} from \"react\";\nimport * as Primitive from \"@radix-ui/react-popover\";\nimport { css, theme, styled, type CSS } from \"../stitches.config\";\nimport { Separator } from \"./separator\";\nimport { PanelTitle } from \"./panel-title\";\nimport { Flex } from \"./flex\";\nimport { Button } from \"./button\";\nimport { XIcon } from \"@webstudio-is/icons\";\n\nexport const Popover = Primitive.Root;\n\nexport const PopoverPortal = Primitive.Portal;\n\nconst contentStyle = css({\n  border: `1px solid ${theme.colors.borderMain}`,\n  boxShadow: `${theme.shadows.menuDropShadow}, inset 0 0 0 1px ${theme.colors.borderMenuInner}`,\n  background: theme.colors.backgroundMenu,\n  borderRadius: theme.borderRadius[6],\n  display: \"flex\",\n  flexDirection: \"column\",\n  maxWidth: \"max-content\",\n  overflow: \"clip\",\n  \"&:focus\": {\n    // override browser default\n    outline: \"none\",\n  },\n});\n\nconst titleSlotStyle = css({\n  // We put title at the bottom in DOM to make the close button last in the TAB order\n  // But visually we want it to be first\n  order: -1,\n});\n\nexport const PopoverTitle = ({\n  children,\n  suffix,\n  ...rest\n}: ComponentProps<typeof PanelTitle> & {\n  suffix?: ReactNode;\n  closeLabel?: string;\n}) => (\n  <div className={titleSlotStyle()}>\n    <PanelTitle {...rest} suffix={suffix ?? <PopoverClose />}>\n      {children}\n    </PanelTitle>\n    <Separator />\n  </div>\n);\n\nexport const PopoverTitleActions = ({ children }: { children: ReactNode }) => {\n  return <Flex gap=\"1\">{children}</Flex>;\n};\n\nexport const PopoverContent = forwardRef(\n  (\n    {\n      children,\n      className,\n      css,\n      sideOffset,\n      ...props\n    }: ComponentProps<typeof Primitive.Content> & {\n      css?: CSS;\n    },\n    ref: Ref<HTMLDivElement>\n  ) => (\n    <Primitive.Portal>\n      <Primitive.Content\n        sideOffset={sideOffset ?? 4}\n        collisionPadding={4}\n        className={contentStyle({ className, css })}\n        {...props}\n        ref={ref}\n      >\n        {children}\n      </Primitive.Content>\n    </Primitive.Portal>\n  )\n);\nPopoverContent.displayName = \"PopoverContent\";\n\nexport const PopoverTrigger = Primitive.Trigger;\n\nexport const PopoverClose = forwardRef(\n  (\n    { children, ...props }: ComponentProps<typeof Button>,\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <Primitive.Close asChild>\n      {children ?? (\n        <Button\n          color=\"ghost\"\n          prefix={<XIcon />}\n          aria-label=\"Close\"\n          {...props}\n          ref={ref}\n        />\n      )}\n    </Primitive.Close>\n  )\n);\nPopoverClose.displayName = \"PopoverClose\";\n\nexport const PopoverMenuItemRightSlot = styled(\"span\", {\n  marginLeft: \"auto\",\n  display: \"flex\",\n});\n\nexport const PopoverSeparator = styled(Separator);\n"
  },
  {
    "path": "packages/design-system/src/components/position-grid.stories.tsx",
    "content": "import { action } from \"@storybook/addon-actions\";\nimport { PositionGrid as PositionGridComponent } from \"./position-grid\";\nimport { useState } from \"react\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Position Grid\",\n};\n\nexport const PositionGrid = () => {\n  const [selectedPosition, setSelectedPosition] = useState({\n    y: 50,\n    x: 50,\n  });\n  return (\n    <>\n      <StorySection title=\"States\">\n        <StoryGrid horizontal>\n          <PositionGridComponent\n            onSelect={(position) => {\n              setSelectedPosition(position);\n              action(\"onSelect\")(position);\n            }}\n          />\n          <PositionGridComponent\n            focused\n            onSelect={(position) => {\n              setSelectedPosition(position);\n              action(\"onSelect\")(position);\n            }}\n          />\n          <PositionGridComponent\n            focused\n            selectedPosition={selectedPosition}\n            onSelect={(position) => {\n              setSelectedPosition(position);\n              action(\"onSelect\")(position);\n            }}\n          />\n          <PositionGridComponent\n            focused\n            selectedPosition={selectedPosition}\n            onSelect={(position) => {\n              setSelectedPosition(position);\n              action(\"onSelect\")(position);\n            }}\n            focusedPosition={{ y: 0, x: 0 }}\n          />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"With keywords\">\n        <StoryGrid horizontal>\n          <PositionGridComponent\n            focused\n            selectedPosition={{ x: \"center\", y: \"bottom\" }}\n            onSelect={action(\"onSelect\")}\n          />\n          <PositionGridComponent\n            focused\n            selectedPosition={{ x: \"right\", y: \"center\" }}\n            onSelect={action(\"onSelect\")}\n          />\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/position-grid.tsx",
    "content": "import { type KeyboardEventHandler, useState } from \"react\";\nimport { css, theme } from \"../stitches.config\";\nimport { Box } from \"./box\";\nimport { Grid } from \"./grid\";\n\n// prettier-ignore\nconst positions = [\n  [0, 0],   [50, 0],   [100, 0],\n  [0, 50],  [50, 50], [100, 50],\n  [0, 100], [50, 100], [100, 100],\n];\n\ntype Position = { y: number; x: number };\ntype MixedPosition = { y: number | string; x: number | string };\n\nconst keywordNumberMap: Record<string, number> = {\n  x: 0,\n  y: 0,\n  center: 50,\n  right: 100,\n  bottom: 100,\n};\n\nconst toNumericPosition = (position?: MixedPosition) => {\n  if (position === undefined) {\n    return;\n  }\n  return {\n    x: keywordNumberMap[position.x] ?? position.x,\n    y: keywordNumberMap[position.y] ?? position.y,\n  };\n};\n\nconst containerStyle = css({\n  background: theme.colors.backgroundControls,\n  padding: theme.spacing[3],\n  width: \"fit-content\",\n  borderRadius: theme.borderRadius[4],\n  border: `1px solid transparent`,\n  outline: \"none\",\n  gridTemplateColumns: \"repeat(3, 1fr)\",\n  gridTemplateAreas: `\n    \"x x x\"\n    \"x x x\"\n    \"x x x\"\n  `,\n  \"&[data-focused=true], &:focus-visible\": {\n    borderColor: theme.colors.borderFocus,\n  },\n});\n\nconst dotStyle = css({\n  padding: theme.spacing[4],\n  margin: 1,\n  background: theme.colors.backgroundControls,\n  border: `1px solid transparent`,\n  borderRadius: theme.borderRadius[4],\n  outline: \"none\",\n  minWidth: \"auto\",\n  \"&::before\": {\n    content: '\"\"',\n    display: \"block\",\n    width: theme.spacing[3],\n    height: theme.spacing[3],\n    background: theme.colors.foregroundGridControlsDot,\n    borderRadius: \"50%\",\n  },\n  \"&[data-selected=true], &:hover\": {\n    background: theme.colors.foregroundGridControlsFlexHover,\n    \"&::before\": {\n      background: theme.colors.foregroundGridControlsDotHover,\n    },\n  },\n  \"&[data-focused=true]\": {\n    borderColor: theme.colors.borderFocus,\n  },\n});\n\nconst useKeyboard = ({\n  onSelect,\n  focusedPosition,\n}: {\n  focusedPosition?: Position;\n  onSelect: (position: Position) => void;\n}) => {\n  // -50 is to prevent the focus to be on the first item when the grid is not focused\n  const [y, setTop] = useState(focusedPosition?.y ?? -50);\n  const [x, setLeft] = useState(focusedPosition?.x ?? 0);\n\n  const handleKeyDown: KeyboardEventHandler = (event) => {\n    switch (event.key) {\n      case \"ArrowUp\": {\n        setTop(y <= 0 ? 100 : y - 50);\n        break;\n      }\n      case \"ArrowDown\": {\n        setTop(y >= 100 ? 0 : y + 50);\n        break;\n      }\n      case \"ArrowLeft\": {\n        setLeft(x <= 0 ? 100 : x - 50);\n        break;\n      }\n      case \"ArrowRight\": {\n        setLeft(x >= 100 ? 0 : x + 50);\n        break;\n      }\n      case \"Enter\": {\n        if (y >= 0 && x >= 0) {\n          onSelect({ y, x });\n        }\n        break;\n      }\n    }\n  };\n\n  return { handleKeyDown, focusedKey: `${x}-${y}` };\n};\n\ntype PositionGridProps = {\n  focusedPosition?: Position;\n  selectedPosition?: MixedPosition;\n  focused?: boolean;\n  onSelect: (position: Position) => void;\n};\n\n/**\n * It will render selected item when the y/x values in `selectedPosition` are 0, 50 or 100.\n * All other values will be ignored.\n * Props `focused` and `focusedPosition` are for testing only, because they shold be set by interactions.\n */\nexport const PositionGrid = ({\n  selectedPosition,\n  focusedPosition,\n  focused = false,\n  onSelect,\n}: PositionGridProps) => {\n  const { handleKeyDown, focusedKey } = useKeyboard({\n    onSelect,\n    focusedPosition,\n  });\n  const numericSelectedPosition = toNumericPosition(selectedPosition);\n\n  return (\n    <Grid\n      tabIndex={0}\n      onKeyDown={handleKeyDown}\n      data-focused={focused}\n      className={containerStyle()}\n    >\n      {positions.map(([x, y]) => {\n        const selectedKey = `${numericSelectedPosition?.x}-${numericSelectedPosition?.y}`;\n        const positionKey = `${x}-${y}`;\n        return (\n          <Box\n            key={positionKey}\n            data-selected={selectedKey === positionKey}\n            data-focused={focusedKey === positionKey}\n            className={dotStyle()}\n            onClick={() => {\n              onSelect({ x, y });\n            }}\n          />\n        );\n      })}\n    </Grid>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/arrow-focus.tsx",
    "content": "import {\n  FocusScope,\n  useFocusManager,\n  type FocusManagerOptions,\n} from \"@react-aria/focus\";\nimport type { KeyboardEvent, JSX } from \"react\";\n\ntype Render = (props: {\n  handleKeyDown: (\n    event: KeyboardEvent,\n    focusManagerOptions?: FocusManagerOptions\n  ) => void;\n  focusManager: ReturnType<typeof useFocusManager>;\n}) => JSX.Element;\n\nconst willEventMoveCaret = (event: KeyboardEvent) => {\n  const { activeElement } = document;\n\n  if (!(activeElement instanceof HTMLInputElement)) {\n    return false;\n  }\n\n  const { selectionStart, selectionEnd, value } = activeElement;\n\n  // probably never the case, just for TypeScript\n  if (selectionStart === null || selectionEnd === null) {\n    return true;\n  }\n\n  // if some text is selected, arrow keys remove selection\n  if (selectionStart !== selectionEnd) {\n    return true;\n  }\n\n  // if caret at the end, right and down will not move it\n  if (\n    selectionEnd === value.length &&\n    (event.key === \"ArrowRight\" || event.key === \"ArrowDown\")\n  ) {\n    return false;\n  }\n\n  // if caret at the start, left and up will not move it\n  if (\n    selectionStart === 0 &&\n    (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\")\n  ) {\n    return false;\n  }\n\n  return true;\n};\n\nconst accept = (element: Element) => {\n  // In some cases we want to have an element that is tabbable, but it should not be ignored for arrow focus management.\n  // One use case is in input field, which is using ESC to focus an div to unfocus the input\n  return element.hasAttribute(\"data-no-arrow-focus\") === false;\n};\n\n// Need this wrapper becuase we can't call useFocusManager\n// in the same component that renders FocusScope\nconst ContextHelper = ({ render }: { render: Render }) => {\n  const focusManager = useFocusManager();\n\n  return render({\n    handleKeyDown: (\n      event: KeyboardEvent,\n      focusManagerOptions?: FocusManagerOptions\n    ) => {\n      if (event.defaultPrevented) {\n        return;\n      }\n\n      if (\n        event.target instanceof Element &&\n        false === event.currentTarget.contains(event.target)\n      ) {\n        // Event occurs inside a portal, typically within a popover or dialog, but the handler is outside the popover/dialog.\n        // Ignore these events as they do not affect focus outside the dialog.\n        return;\n      }\n\n      if (willEventMoveCaret(event)) {\n        return;\n      }\n\n      if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n        focusManager?.focusNext({ wrap: true, accept, ...focusManagerOptions });\n        event.preventDefault(); // Prevents the page from scrolling\n      }\n      if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n        focusManager?.focusPrevious({\n          wrap: true,\n          accept,\n          ...focusManagerOptions,\n        });\n        event.preventDefault(); // Prevents the page from scrolling\n      }\n    },\n    focusManager,\n  });\n};\n\nexport const ArrowFocus = ({ render }: { render: Render }) => (\n  <FocusScope>\n    <ContextHelper render={render} />\n  </FocusScope>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/create-content-controller.stories.tsx",
    "content": "import { useState, useRef, useEffect, type RefObject } from \"react\";\nimport { createContentController } from \"./create-content-controller\";\nimport { StorySection } from \"../storybook\";\n\nconst useContentController = ({\n  ref,\n  read,\n  write,\n  callback,\n}: {\n  ref: RefObject<HTMLInputElement | null>;\n  read: (value: HTMLInputElement) => string;\n  write: (target: HTMLInputElement, value: string) => void;\n  callback: (value: { name: string; value: string }) => void;\n}) => {\n  useEffect(() => {\n    if (ref.current == null) {\n      return;\n    }\n    const { cleanup } = createContentController(ref.current, {\n      contents: [\n        {\n          name: \"unit\",\n          match: (value: string) => [\"px\", \"em\", \"rem\", \"%\"].includes(value),\n        },\n        {\n          name: \"number\",\n          match: (value: string) =>\n            Number.isNaN(Number.parseFloat(value)) === false,\n        },\n        { name: \"unknown\", match: (value: string) => Boolean(value) },\n      ],\n      read,\n      write,\n      onMouseMove: callback,\n      onCaretMove: callback,\n    });\n    return cleanup;\n  }, [ref, read, write, callback]);\n};\n\nconst ExampleContentController = () => {\n  const [content, setContent] = useState({ name: \"none\", value: \"\" });\n  const ref = useRef<HTMLInputElement | null>(null);\n  useContentController({\n    ref,\n    callback: (payload) => setContent(payload),\n    read: (target) => target.value,\n    write: (target, value) => (target.value = value),\n  });\n  return (\n    <StorySection title=\"Create content controller\">\n      <form style={{ fontFamily: \"sans-serif\" }}>\n        <fieldset>\n          <input\n            defaultValue=\"100px 20% fit-content calc(0.2px, -99em)\"\n            ref={ref}\n            style={{ width: \"50%\" }}\n          />\n        </fieldset>\n        <fieldset>\n          <h1>Currently on: {content.name}</h1>\n          <h1>With a value of: {content.value}</h1>\n        </fieldset>\n      </form>\n    </StorySection>\n  );\n};\n\nexport const CreateContentController = Object.assign(\n  ExampleContentController.bind({})\n);\n\nexport default {\n  title: \"Primitives/Create Content Controller\",\n  component: ExampleContentController,\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/create-content-controller.ts",
    "content": "/**\n * @description\n * - detects whether the pointer/keyboard is on any given token as specified by the {contents: []} contract.\n * - pointermove/keyup in turn dispatch onMouseMove/onCaretMove with the current state if and when within a configured content token.\n * - returns two methods {update, cleanup} the former of which dispatches a content update(while preserving the caret position if any)\n * - read/write methods are called when reading from the passed element or when updating/writing to the same element.\n * @example\n * const target = document.querySelector('input');\n * const {update, cleanup} = createContentController(target, {\n *   contents: [\n *     { name: 'unit', match: (value) => value === 'px' },\n *     { name: 'number', match: (value) => Number.isNaN(Number.parseFloat(value)) === false },\n *     { name: 'unknown', match: (value) => true },\n *   ],\n *   onMouseMove: ({name, value, position}) => { },\n *   onCaretMove: ({name, value, position}) => { },\n *   read: (target) => target.value,\n *   write: (target, value) => target.value = value,\n * });\n */\nconst canvasContext = globalThis.document\n  .createElement(\"canvas\")\n  .getContext(\"2d\") as CanvasRenderingContext2D & { letterSpacing: string };\nconst measureTextWidth = (value: string): number =>\n  canvasContext.measureText(value).width;\nconst measureTextPadding = (node: HTMLElement, value: string) => {\n  const {\n    fontSize,\n    fontFamily,\n    fontWeight,\n    textAlign,\n    letterSpacing,\n    paddingLeft,\n    paddingRight,\n  } = getComputedStyle(node);\n  if (letterSpacing !== \"normal\") {\n    canvasContext.letterSpacing = letterSpacing;\n  }\n  canvasContext.font = `${fontWeight} ${fontSize} ${fontFamily}`;\n  return [\"left\", \"start\"].includes(textAlign)\n    ? Number.parseFloat(paddingLeft)\n    : node.clientWidth -\n        canvasContext.measureText(value).width -\n        Number.parseFloat(paddingRight);\n};\nconst measureTextMetrics = (value: string, index: number, offset: number) => {\n  let length = 0;\n  const DECIMAL_OR_WHITESPACE_OR_NON_WORD = /([-+]?\\d*\\.?\\d+)|([\\s\\W])/;\n  return value\n    .split(DECIMAL_OR_WHITESPACE_OR_NON_WORD)\n    .filter((value) => value)\n    .map((value) => {\n      const width = measureTextWidth(value);\n      const head = index - offset;\n      const tail = index - (offset + width);\n      const props = { head, tail, value, width, length, offset };\n      length += value.length;\n      offset += width;\n      return props;\n    });\n};\nexport const createContentController = (\n  targetNode: HTMLInputElement,\n  {\n    read = (node: HTMLInputElement) => node.value,\n    write = (node: HTMLInputElement, value: string) => (node.value = value),\n    contents = [],\n    onMouseMove = (value: { name: string; value: string; position: string }) =>\n      value,\n    onCaretMove = (value: { name: string; value: string; position: string }) =>\n      value,\n  }: {\n    read: (node: HTMLInputElement) => string;\n    write: (node: HTMLInputElement, value: string) => void;\n    contents: {\n      name: string;\n      match: (value: string) => boolean;\n    }[];\n    onMouseMove: (value: {\n      name: string;\n      value: string;\n      position: string;\n    }) => void;\n    onCaretMove: (value: {\n      name: string;\n      value: string;\n      position: string;\n    }) => void;\n  }\n): {\n  update: (value: string) => void;\n  cleanup: () => void;\n} => {\n  const eventNames = [\"keyup\", \"pointermove\"] as const;\n  const handleEvent = (event: KeyboardEvent | PointerEvent): void => {\n    let { offsetX } = event as PointerEvent;\n    const { type } = event;\n    const targetValue = read(targetNode);\n    const targetPadding = measureTextPadding(targetNode, targetValue);\n    if (type === eventNames[0]) {\n      offsetX =\n        targetPadding +\n        measureTextWidth(\n          targetValue.slice(0, targetNode.selectionStart || undefined)\n        );\n    }\n    const targetMetrics = measureTextMetrics(\n      targetValue,\n      offsetX,\n      targetPadding\n    );\n    for (const targetMetricsData of targetMetrics) {\n      const { head, tail, value } = targetMetricsData;\n      // zero for exact boundary match, added buffer for precision forgiveness, more so from the head\n      if (head >= -2 && tail <= 0) {\n        const current = contents.find(({ match }) => match(value));\n        if (current) {\n          const { name } = current;\n          const position = `${head},${tail}`;\n          if (type === eventNames[0]) {\n            onCaretMove({ name, value, position });\n          } else if (type === eventNames[1]) {\n            onMouseMove({ name, value, position });\n          }\n        }\n      }\n    }\n  };\n\n  eventNames.forEach((eventName) =>\n    targetNode.addEventListener(eventName, handleEvent, false)\n  );\n  return {\n    update: (value) => {\n      const { selectionStart, selectionEnd } = targetNode;\n      write(targetNode, value);\n      targetNode.setSelectionRange(selectionStart, selectionEnd);\n    },\n    cleanup: () => {\n      eventNames.forEach((eventName) =>\n        targetNode.removeEventListener(eventName, handleEvent, false)\n      );\n    },\n  };\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/README.md",
    "content": "## Terminology\n\n- drop target - item accepting draggable element for drop\n- drag item - item that has originally received a pointerdown event\n- drop index - specific index at which item is going to drop relative to children\n- drop target outline - frame that outlines a drop target\n- placement indicator - a line that shows where the item is going to be placed\n- edge detection - when pointer is near (~5px) an edge of a drop target\n\n## Principles\n\n- You can drop anywhere. At any point in time, there is a drop target and an index that would define the best possible position based on the current pointer position. There will never be an error state. If the best possible position is the original position, so be it.\n- At any point in time, we can calculate the drop target outline, rect and placement indicator rect.\n\n## Architecture\n\nWe provide two main React hooks: `useDrag` and `useDrop`, and secondary helpers:\n\n- `useAutoScroll` scrolls a scroll container when the pointer nears its edge,\n- `useDragCursor` sets `cursor:grabbing` globally during the drag,\n- `useHold` detects when a piece of data stays the same for a given amount of time.\n\n### `useDrag`\n\nThis hook tracks a “drag” pointer interaction. It tells you:\n\n- when a drag starts,\n- on which element,\n- when pointer is moved during the drag,\n- when the drag has ended.\n\n### `useDrop`\n\nThis hook chooses the drop target. You give it the pointer coordinates received from `useDrag`, and it does its best to find the best position among potential drop targets. As a result, you get:\n\n- drop target element,\n- coordinates of its bounding rect (drop target outline),\n- index within children of the drop target (drop index),\n- coordinates of the placement indicator line.\n\nThis division of responsibilities between the two hooks allows to put them in different windows. For example, `useDrag` can be instantiated in the main browser window, while `useDrop` can live in an iframe. The information you’ll need to pass between them can be transferred via `postMessage()`.\n\n| Needs access to: | Drop targets DOM | Drag items DOM | Scroll container |\n| ---------------- | ---------------- | -------------- | ---------------- |\n| `useDrag`        |                  | V              |                  |\n| `useDrop`        | V                |                | V                |\n| `useAutoScroll`  |                  |                | V                |\n| `useHold`        |                  |                |                  |\n| `useDragCursor`  |                  |                |                  |\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/__snapshots__/geometry-utils.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`getPlacementInside > childrenOrientation={ type: 'horizontal', reverse: false } 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 90,\n  \"parentRect\": {\n    \"height\": 100,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 100,\n  },\n  \"type\": \"inside-parent\",\n  \"x\": 5,\n  \"y\": 5,\n}\n`;\n\nexports[`getPlacementInside > childrenOrientation={ type: 'horizontal', reverse: true } 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 90,\n  \"parentRect\": {\n    \"height\": 100,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 100,\n  },\n  \"type\": \"inside-parent\",\n  \"x\": 5,\n  \"y\": 5,\n}\n`;\n\nexports[`getPlacementInside > childrenOrientation={ type: 'mixed' } 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 90,\n  \"parentRect\": {\n    \"height\": 100,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 100,\n  },\n  \"type\": \"inside-parent\",\n  \"x\": 5,\n  \"y\": 5,\n}\n`;\n\nexports[`getPlacementInside > childrenOrientation={ type: 'vertical', reverse: false } 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 90,\n  \"parentRect\": {\n    \"height\": 100,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 100,\n  },\n  \"type\": \"inside-parent\",\n  \"x\": 5,\n  \"y\": 5,\n}\n`;\n\nexports[`getPlacementInside > childrenOrientation={ type: 'vertical', reverse: true } 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 90,\n  \"parentRect\": {\n    \"height\": 100,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 100,\n  },\n  \"type\": \"inside-parent\",\n  \"x\": 5,\n  \"y\": 5,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=backward > child almost same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 2,\n  \"y\": 4,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=backward > child bigger than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=backward > child same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=backward > child smaller than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 95,\n  \"y\": 100,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=forward > child almost same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 298,\n  \"y\": 4,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=forward > child bigger than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 300,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=forward > child same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 300,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: false } > direction=forward > child smaller than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 205,\n  \"y\": 100,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=backward > child almost same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 298,\n  \"y\": 4,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=backward > child bigger than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 300,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=backward > child same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 300,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=backward > child smaller than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 205,\n  \"y\": 100,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=forward > child almost same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 2,\n  \"y\": 4,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=forward > child bigger than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=forward > child same as parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'horizontal', reverse: true } > direction=forward > child smaller than parent 1`] = `\n{\n  \"direction\": \"vertical\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 95,\n  \"y\": 100,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=backward > child almost same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 4,\n  \"y\": 2,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=backward > child bigger than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=backward > child same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=backward > child smaller than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 100,\n  \"y\": 95,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=forward > child almost same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 4,\n  \"y\": 298,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=forward > child bigger than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": 300,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=forward > child same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 300,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'mixed' } > direction=forward > child smaller than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 100,\n  \"y\": 205,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=backward > child almost same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 4,\n  \"y\": 2,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=backward > child bigger than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=backward > child same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=backward > child smaller than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 100,\n  \"y\": 95,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=forward > child almost same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 4,\n  \"y\": 298,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=forward > child bigger than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": 300,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=forward > child same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 300,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: false } > direction=forward > child smaller than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 100,\n  \"y\": 205,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=backward > child almost same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 4,\n  \"y\": 298,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=backward > child bigger than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": 300,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=backward > child same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 300,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=backward > child smaller than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 100,\n  \"y\": 205,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=forward > child almost same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 292,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 4,\n  \"y\": 2,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=forward > child bigger than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 310,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": -10,\n  \"y\": -10,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=forward > child same as parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 300,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 0,\n  \"y\": 0,\n}\n`;\n\nexports[`getPlacementNextTo > childrenOrientation={ type: 'vertical', reverse: true } > direction=forward > child smaller than parent 1`] = `\n{\n  \"direction\": \"horizontal\",\n  \"length\": 100,\n  \"parentRect\": {\n    \"height\": 300,\n    \"left\": 0,\n    \"top\": 0,\n    \"width\": 300,\n  },\n  \"type\": \"next-to-child\",\n  \"x\": 100,\n  \"y\": 95,\n}\n`;\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/canvas.stories.tsx",
    "content": "import type { Meta } from \"@storybook/react\";\nimport { type CSSProperties, useState, useRef } from \"react\";\nimport { StorySection } from \"../../storybook\";\nimport { Box } from \"../../box\";\nimport { useDrop, type DropTarget } from \"./use-drop\";\nimport { useDrag } from \"./use-drag\";\nimport {\n  computeIndicatorPlacement,\n  PlacementIndicator,\n} from \"./placement-indicator\";\nimport { useAutoScroll } from \"./use-auto-scroll\";\nimport { theme } from \"../../../stitches.config\";\nimport type { Placement } from \"./geometry-utils\";\n\nconst ROOT_ID = \"root\";\n\ntype ItemData = {\n  id: string;\n  style: CSSProperties;\n  acceptsChildren: boolean;\n  children: ItemData[];\n};\n\nconst Item = ({\n  data,\n  dragItemId,\n}: {\n  data: ItemData;\n  dragItemId: string | undefined;\n}) => {\n  return (\n    <Box\n      css={{\n        opacity: dragItemId === data.id ? 0.3 : 1,\n        minWidth: 100,\n        minHeight: 100,\n        margin: 10,\n        padding: 10,\n        background: theme.colors.backgroundCanvas,\n        border: `1px solid ${theme.colors.borderContrast}`,\n      }}\n      style={data.style}\n      data-id={data.id}\n      title={JSON.stringify({ id: data.id, style: data.style }, null, 2)}\n    >\n      <Items data={data.children} dragItemId={dragItemId} />\n    </Box>\n  );\n};\n\nconst Items = ({\n  data,\n  dragItemId,\n}: {\n  data: ItemData[];\n  dragItemId: string | undefined;\n}) => {\n  return (\n    <>\n      {data.map((child) => (\n        <Item data={child} dragItemId={dragItemId} key={child.id} />\n      ))}\n    </>\n  );\n};\n\nconst elementToId = (element: Element) =>\n  element instanceof HTMLElement && element.dataset.id;\n\nconst idToElement = (\n  root: HTMLElement,\n  id: string\n): HTMLElement | undefined => {\n  if (elementToId(root) === id) {\n    return root;\n  }\n  for (const child of root.children) {\n    if (child instanceof HTMLElement) {\n      const found = idToElement(child, id);\n      if (found) {\n        return found;\n      }\n    }\n  }\n  return;\n};\n\nconst mapItems = (\n  data: ItemData[],\n  fn: (item: ItemData) => ItemData\n): ItemData[] => {\n  const recur = (\n    data: ItemData[],\n    fn: (item: ItemData) => ItemData\n  ): ItemData[] =>\n    data.map((item) => fn({ ...item, children: recur(item.children, fn) }));\n  return fn({\n    id: ROOT_ID,\n    style: {},\n    acceptsChildren: true,\n    children: recur(data, fn),\n  }).children;\n};\n\nconst findItem = (data: ItemData[], id: string): ItemData | undefined => {\n  for (const item of data) {\n    if (item.id === id) {\n      return item;\n    }\n    const found = findItem(item.children, id);\n    if (found) {\n      return found;\n    }\n  }\n  return;\n};\n\nconst findItemPath = (data: ItemData[], id: string): ItemData[] | undefined => {\n  for (const item of data) {\n    if (item.id === id) {\n      return [item];\n    }\n    const found = findItemPath(item.children, id);\n    if (found) {\n      return [...found, item];\n    }\n  }\n  return;\n};\n\nexport const Canvas = () => {\n  const [data, setData] = useState<ItemData[]>([\n    {\n      id: \"0\",\n      style: {},\n      children: [\n        {\n          id: \"1\",\n          style: { margin: 0, background: \"#ff7878\" },\n          children: [],\n          acceptsChildren: true,\n        },\n        {\n          id: \"2\",\n          style: { margin: 0, background: \"#a8d1ff\" },\n          children: [],\n          acceptsChildren: true,\n        },\n        {\n          id: \"3\",\n          style: { margin: 0, background: \"#94ef94\" },\n          children: [],\n          acceptsChildren: true,\n        },\n      ],\n      acceptsChildren: true,\n    },\n    { id: \"4\", style: {}, children: [], acceptsChildren: true },\n    { id: \"5\", style: {}, children: [], acceptsChildren: true },\n    {\n      id: \"6\",\n      style: { background: \"whitesmoke\" },\n      children: [],\n      acceptsChildren: false,\n    },\n    {\n      id: \"7\",\n      style: { display: \"flex\" },\n      children: [\n        {\n          id: \"8\",\n          style: { margin: 0, background: \"#ff7878\" },\n          children: [],\n          acceptsChildren: true,\n        },\n        {\n          id: \"9\",\n          style: { margin: 0, background: \"#a8d1ff\" },\n          children: [],\n          acceptsChildren: true,\n        },\n        {\n          id: \"10\",\n          style: { margin: 0, background: \"#94ef94\" },\n          children: [],\n          acceptsChildren: true,\n        },\n      ],\n      acceptsChildren: true,\n    },\n  ]);\n\n  const [currentDropTarget, setCurrentDropTarget] = useState<\n    DropTarget<string> | undefined\n  >();\n  const [placementIndicator, setPlacementIndicator] = useState<\n    undefined | Placement\n  >();\n  const [dragItemId, setDragItemId] = useState<string>();\n\n  const rootRef = useRef<HTMLElement | null>(null);\n\n  const dropHandlers = useDrop<string>({\n    edgeDistanceThreshold: 10,\n\n    elementToData(element) {\n      return elementToId(element) ?? false;\n    },\n\n    swapDropTarget(dropTarget) {\n      const rootElement = rootRef.current;\n      if (dropTarget === undefined || rootElement === null) {\n        return;\n      }\n\n      const { data: id, area } = dropTarget;\n\n      const path = findItemPath(data, id) ?? [];\n\n      if (area !== \"center\") {\n        path.shift();\n      }\n\n      // Don't allow to drop inside drag item or any of its children\n      const dragItemIndex = path.findIndex((item) => item.id === dragItemId);\n      if (dragItemIndex !== -1) {\n        path.splice(0, dragItemIndex + 1);\n      }\n\n      const newItem = path.find((item) => item.acceptsChildren);\n\n      if (newItem === undefined) {\n        return;\n      }\n\n      const element = idToElement(rootElement, newItem.id);\n\n      if (element === undefined) {\n        return;\n      }\n\n      return { data: newItem.id, element };\n    },\n\n    onDropTargetChange(dropTarget) {\n      setCurrentDropTarget(dropTarget);\n      if (dropTarget === undefined) {\n        setPlacementIndicator(undefined);\n      } else {\n        setPlacementIndicator(\n          computeIndicatorPlacement({\n            placement: dropTarget.placement,\n            element: dropTarget.element,\n          })\n        );\n      }\n    },\n  });\n\n  const autoScrollHandlers = useAutoScroll();\n\n  const useDragHandlers = useDrag<string>({\n    elementToData(element) {\n      const id = element instanceof HTMLElement && element.dataset.id;\n      if (id) {\n        return id;\n      }\n      return false;\n    },\n    onStart({ data: id }) {\n      setDragItemId(id);\n      autoScrollHandlers.setEnabled(true);\n      dropHandlers.handleStart();\n    },\n    onMove: (point) => {\n      dropHandlers.handleMove(point);\n      autoScrollHandlers.handleMove(point);\n    },\n    onEnd({ isCanceled }) {\n      if (dragItemId !== undefined && currentDropTarget !== undefined) {\n        setData((current) => {\n          const dragItem = findItem(current, dragItemId);\n\n          // shouldn't happen, for TypeScript\n          if (dragItem === undefined) {\n            return current;\n          }\n\n          return mapItems(current, (item) => {\n            let children = item.children;\n\n            const oldIndex = children.findIndex(\n              (child) => child.id === dragItemId\n            );\n\n            if (oldIndex !== -1) {\n              children = children.slice();\n              children.splice(oldIndex, 1);\n            }\n\n            if (item.id === currentDropTarget.data) {\n              // placement.index does not take into account the fact that the drag item will be removed.\n              // we need to do this to account for it.\n              const newIndex =\n                oldIndex !== -1 &&\n                oldIndex < currentDropTarget.indexWithinChildren\n                  ? currentDropTarget.indexWithinChildren - 1\n                  : currentDropTarget.indexWithinChildren;\n\n              children = children.slice();\n              children.splice(newIndex, 0, dragItem);\n            }\n\n            return children === item.children ? item : { ...item, children };\n          });\n        });\n      }\n\n      dropHandlers.handleEnd({ isCanceled });\n      autoScrollHandlers.setEnabled(false);\n      setDragItemId(undefined);\n      setCurrentDropTarget(undefined);\n      setPlacementIndicator(undefined);\n    },\n  });\n\n  return (\n    <StorySection title=\"Canvas\">\n      <Box\n        css={{\n          background: \"white\",\n          padding: 10,\n          width: 500,\n          height: 500,\n          overflow: \"auto\",\n          // to make DnD work we have to disable scrolling using touch\n          touchAction: \"none\",\n          \"[data-id]\": {\n            cursor: dragItemId === undefined ? \"grab\" : \"default\",\n          },\n        }}\n        ref={autoScrollHandlers.targetRef}\n        onScroll={dropHandlers.handleScroll}\n      >\n        <Box\n          ref={(element) => {\n            dropHandlers.rootRef(element);\n            useDragHandlers.rootRef(element);\n            rootRef.current = element;\n          }}\n          data-id={ROOT_ID}\n        >\n          <Items data={data} dragItemId={dragItemId} />\n        </Box>\n      </Box>\n      {placementIndicator && (\n        <PlacementIndicator placement={placementIndicator} />\n      )}\n    </StorySection>\n  );\n};\n\nexport default {\n  title: \"Primitives/DND/Canvas\",\n  component: Canvas,\n} satisfies Meta<typeof Canvas>;\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/dom-utils.ts",
    "content": "import {\n  type Rect,\n  type ChildrenOrientation,\n  getRectsOrientation,\n  getTwoRectsOrientation,\n} from \"./geometry-utils\";\n\nconst getOrientaionUsingProbe = (\n  parent: Element,\n  relativeToChild?: Element\n): ChildrenOrientation => {\n  const probe = document.createElement(\"div\");\n  if (relativeToChild) {\n    parent.insertBefore(probe, relativeToChild);\n  } else {\n    parent.appendChild(probe);\n  }\n  const probeRect = probe.getBoundingClientRect();\n  parent.removeChild(probe);\n\n  // If there's no child, see if one of the dimensions collapsed.\n  // If both or neither collapsed, fallback to vertical as the best guess.\n  if (relativeToChild === undefined) {\n    return {\n      type:\n        probeRect.width === 0 && probeRect.height > 0\n          ? \"horizontal\"\n          : \"vertical\",\n      reverse: false,\n    };\n  }\n\n  return getTwoRectsOrientation(\n    probeRect,\n    relativeToChild.getBoundingClientRect()\n  );\n};\n\n// By looking at a specific child and it's neighbours,\n// determines their orientation relative to each other\nexport const getLocalChildrenOrientation = (\n  parent: Element,\n  getChildren: (parent: Element) => Element[] | HTMLCollection,\n  childrentRects: Rect[],\n  childIndex: number\n): ChildrenOrientation => {\n  const previous = childrentRects[childIndex - 1] as Rect | undefined;\n  const current = childrentRects[childIndex] as Rect | undefined;\n  const next = childrentRects[childIndex + 1] as Rect | undefined;\n\n  // If there are no two rects to compare, use a probe\n  if (current === undefined || (next === undefined && previous === undefined)) {\n    const children = getChildren(parent);\n    return getOrientaionUsingProbe(\n      parent,\n      childIndex < children.length ? children[childIndex] : undefined\n    );\n  }\n\n  return getRectsOrientation(previous, current, next);\n};\n\nexport const getChildrenRects = (\n  parent: Element,\n  children: Element[] | HTMLCollection\n) => {\n  const parentRect = parent.getBoundingClientRect();\n\n  // We convert to relative coordinates to be able to store the result in cache.\n  // Otherwise we would have to clear cache on scroll etc.\n  const toRelativeCoordinates = (rect: Rect) => ({\n    left: rect.left - parentRect.left,\n    top: rect.top - parentRect.top,\n    width: rect.width,\n    height: rect.height,\n  });\n\n  return Array.from(children).map((child) =>\n    toRelativeCoordinates(child.getBoundingClientRect())\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/geometry-utils.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  getPlacementBetween,\n  getDistanceToRect,\n  getArea,\n  getPlacementNextTo,\n  getPlacementInside,\n  getTwoRectsOrientation,\n  getRectsOrientation,\n  getIndexAdjustment,\n} from \"./geometry-utils\";\n\ndescribe(\"getDistanceToRect\", () => {\n  const rect = {\n    top: 100,\n    left: 100,\n    width: 100,\n    height: 100,\n  };\n\n  test(\"inside\", () => {\n    expect(getDistanceToRect(rect, { x: rect.left + 1, y: rect.top + 1 })).toBe(\n      0\n    );\n  });\n\n  test(\"near top edge\", () => {\n    expect(getDistanceToRect(rect, { x: rect.left + 1, y: rect.top - 2 })).toBe(\n      2\n    );\n  });\n\n  test(\"near bottom edge\", () => {\n    expect(\n      getDistanceToRect(rect, {\n        x: rect.left + 1,\n        y: rect.top + rect.height + 2,\n      })\n    ).toBe(2);\n  });\n\n  test(\"near left edge\", () => {\n    expect(getDistanceToRect(rect, { x: rect.left - 2, y: rect.top + 1 })).toBe(\n      2\n    );\n  });\n\n  test(\"near right edge\", () => {\n    expect(\n      getDistanceToRect(rect, {\n        x: rect.left + rect.width + 2,\n        y: rect.top + 1,\n      })\n    ).toBe(2);\n  });\n\n  test(\"near top left corner\", () => {\n    expect(\n      getDistanceToRect(rect, {\n        x: rect.left - 2,\n        y: rect.top - 2,\n      })\n    ).toBe(Math.sqrt(8));\n  });\n\n  test(\"near top right corner\", () => {\n    expect(\n      getDistanceToRect(rect, {\n        x: rect.left + rect.width + 2,\n        y: rect.top - 2,\n      })\n    ).toBe(Math.sqrt(8));\n  });\n\n  test(\"near bottom left corner\", () => {\n    expect(\n      getDistanceToRect(rect, {\n        x: rect.left - 2,\n        y: rect.top + rect.height + 2,\n      })\n    ).toBe(Math.sqrt(8));\n  });\n\n  test(\"near bottom right corner\", () => {\n    expect(\n      getDistanceToRect(rect, {\n        x: rect.left + rect.width + 2,\n        y: rect.top + rect.height + 2,\n      })\n    ).toBe(Math.sqrt(8));\n  });\n});\n\ndescribe(\"getArea\", () => {\n  const rect = {\n    top: 100,\n    left: 100,\n    width: 100,\n    height: 100,\n  };\n  const threshold = 10;\n\n  test(\"top\", () => {\n    expect(\n      getArea(\n        { x: rect.left + rect.width / 2, y: rect.top + 1 },\n        threshold,\n        rect\n      )\n    ).toBe(\"top\");\n  });\n\n  test(\"bottom\", () => {\n    expect(\n      getArea(\n        { x: rect.left + rect.width / 2, y: rect.top + rect.height - 1 },\n        threshold,\n        rect\n      )\n    ).toBe(\"bottom\");\n  });\n\n  test(\"left\", () => {\n    expect(\n      getArea(\n        { x: rect.left + 1, y: rect.top + rect.height / 2 },\n        threshold,\n        rect\n      )\n    ).toBe(\"left\");\n  });\n\n  test(\"right\", () => {\n    expect(\n      getArea(\n        { x: rect.left + rect.width - 1, y: rect.top + rect.height / 2 },\n        threshold,\n        rect\n      )\n    ).toBe(\"right\");\n  });\n\n  test(\"center\", () => {\n    expect(\n      getArea(\n        { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },\n        threshold,\n        rect\n      )\n    ).toBe(\"center\");\n  });\n});\n\ndescribe(\"getPlacementBetween\", () => {\n  const size = { width: 100, height: 100 };\n  const parentRect = { top: 0, left: 0, width: 0, height: 0 };\n\n  test.each([\n    [\n      // [a] | [b]\n      parentRect,\n      { top: 0, left: 0 },\n      { top: 0, left: 200 },\n      {\n        parentRect,\n        type: \"between-children\",\n        direction: \"vertical\",\n        x: 150,\n        y: 0,\n        length: 100,\n      },\n    ],\n\n    [\n      // [a]\n      // ---\n      // [b]\n      parentRect,\n      { top: 0, left: 0 },\n      { top: 200, left: 0 },\n      {\n        parentRect,\n        type: \"between-children\",\n        direction: \"horizontal\",\n        x: 0,\n        y: 150,\n        length: 100,\n      },\n    ],\n\n    [\n      //       [a]\n      // [b]\n      parentRect,\n      { top: 0, left: 600 },\n      { top: 200, left: 0 },\n      undefined,\n    ],\n\n    [\n      // [a]\n      //\n      //     [b]\n      parentRect,\n      { top: 0, left: 0 },\n      { top: 600, left: 200 },\n      undefined,\n    ],\n\n    // overlaps ...\n    [parentRect, { top: 0, left: 0 }, { top: 0, left: 0 }, undefined],\n    [parentRect, { top: 0, left: 0 }, { top: 50, left: 50 }, undefined],\n    [parentRect, { top: 0, left: 50 }, { top: 50, left: 0 }, undefined],\n    [\n      parentRect,\n      { top: 0, left: 0, width: 200, height: 200 },\n      { top: 50, left: 50 },\n      undefined,\n    ],\n  ])(\"%o + %o\", (parentRect, a, b, placement) => {\n    expect(\n      getPlacementBetween(parentRect, { ...size, ...a }, { ...size, ...b })\n    ).toEqual(placement);\n    expect(\n      getPlacementBetween(parentRect, { ...size, ...b }, { ...size, ...a })\n    ).toEqual(placement);\n  });\n});\n\ndescribe(\"getPlacementNextTo\", () => {\n  describe.each([\n    { type: \"horizontal\", reverse: false },\n    { type: \"horizontal\", reverse: true },\n    { type: \"vertical\", reverse: false },\n    { type: \"vertical\", reverse: true },\n    { type: \"mixed\" },\n  ] as const)(\"childrenOrientation=%o\", (childrenOrientation) => {\n    describe.each([\"forward\", \"backward\"] as const)(\n      \"direction=%s\",\n      (direction) => {\n        test.each([\n          [\n            \"child smaller than parent\",\n            { top: 100, left: 100, width: 100, height: 100 },\n          ],\n          [\n            \"child almost same as parent\",\n            { top: 4, left: 4, width: 292, height: 292 },\n          ],\n          [\n            \"child same as parent\",\n            { top: 0, left: 0, width: 300, height: 300 },\n          ],\n          [\n            \"child bigger than parent\",\n            { top: -10, left: -10, width: 310, height: 310 },\n          ],\n        ])(\"%s\", (_, child) => {\n          expect(\n            getPlacementNextTo(\n              { top: 0, left: 0, width: 300, height: 300 },\n              child,\n              childrenOrientation,\n              direction\n            )\n          ).toMatchSnapshot();\n        });\n      }\n    );\n  });\n});\n\ndescribe(\"getPlacementInside\", () => {\n  test.each([\n    { type: \"horizontal\", reverse: false },\n    { type: \"horizontal\", reverse: true },\n    { type: \"vertical\", reverse: false },\n    { type: \"vertical\", reverse: true },\n    { type: \"mixed\" },\n  ] as const)(\"childrenOrientation=%o\", (childrenOrientation) => {\n    expect(\n      getPlacementInside(\n        { top: 0, left: 0, width: 100, height: 100 },\n        childrenOrientation\n      )\n    ).toMatchSnapshot();\n  });\n});\n\ndescribe(\"getTwoRectsOrientation\", () => {\n  test.each([\n    [\n      \"horizontal\",\n      { top: 0, left: 0, width: 100, height: 100 },\n      { top: 0, left: 100, width: 100, height: 100 },\n    ],\n    [\n      \"horizontal\",\n      { top: 10, left: 0, width: 80, height: 80 },\n      { top: 0, left: 100, width: 100, height: 100 },\n    ],\n    [\n      \"vertical\",\n      { top: 0, left: 0, width: 100, height: 100 },\n      { top: 100, left: 0, width: 100, height: 100 },\n    ],\n    [\n      \"vertical\",\n      { top: 0, left: 10, width: 80, height: 80 },\n      { top: 100, left: 0, width: 100, height: 100 },\n    ],\n  ])(\"%s %o %o\", (type, rectA, rectB) => {\n    expect(getTwoRectsOrientation(rectA, rectB)).toEqual({\n      reverse: false,\n      type,\n    });\n    expect(getTwoRectsOrientation(rectB, rectA)).toEqual({\n      reverse: true,\n      type,\n    });\n  });\n\n  test.each([\n    [\n      { top: 0, left: 0, width: 100, height: 100 },\n      { top: 0, left: 0, width: 100, height: 100 },\n    ],\n    [\n      { top: 10, left: 10, width: 80, height: 80 },\n      { top: 0, left: 0, width: 100, height: 100 },\n    ],\n    [\n      { top: 0, left: 0, width: 100, height: 100 },\n      { top: 50, left: 50, width: 100, height: 100 },\n    ],\n    [\n      { top: 0, left: 0, width: 100, height: 100 },\n      { top: 100, left: 100, width: 100, height: 100 },\n    ],\n  ])(\"mixed %o %o\", (rectA, rectB) => {\n    expect(getTwoRectsOrientation(rectA, rectB)).toEqual({\n      type: \"mixed\",\n    });\n    expect(getTwoRectsOrientation(rectB, rectA)).toEqual({\n      type: \"mixed\",\n    });\n  });\n});\n\ndescribe(\"getRectsOrientation\", () => {\n  const vertical = [\n    { top: 0, left: 100, width: 100, height: 100 },\n    { top: 100, left: 100, width: 100, height: 100 },\n    { top: 200, left: 100, width: 100, height: 100 },\n  ] as const;\n\n  const horizontal = [\n    { top: 100, left: 0, width: 100, height: 100 },\n    { top: 100, left: 100, width: 100, height: 100 },\n    { top: 100, left: 200, width: 100, height: 100 },\n  ] as const;\n\n  test(\"if all reversed, result is reversed\", () => {\n    expect(\n      getRectsOrientation(vertical[2], vertical[1], vertical[0]).reverse\n    ).toBe(true);\n  });\n\n  test(\"if only one reversed, result is not reversed\", () => {\n    expect(\n      getRectsOrientation(vertical[0], vertical[1], vertical[0]).reverse\n    ).toBe(false);\n  });\n\n  test(\"if all vertical result is vertical\", () => {\n    expect(\n      getRectsOrientation(vertical[0], vertical[1], vertical[2]).type\n    ).toBe(\"vertical\");\n    expect(getRectsOrientation(undefined, vertical[1], vertical[2]).type).toBe(\n      \"vertical\"\n    );\n    expect(getRectsOrientation(vertical[0], vertical[1], undefined).type).toBe(\n      \"vertical\"\n    );\n  });\n\n  test(\"if all horizontal result is horizontal\", () => {\n    expect(\n      getRectsOrientation(horizontal[0], horizontal[1], horizontal[2]).type\n    ).toBe(\"horizontal\");\n    expect(\n      getRectsOrientation(undefined, horizontal[1], horizontal[2]).type\n    ).toBe(\"horizontal\");\n    expect(\n      getRectsOrientation(horizontal[0], horizontal[1], undefined).type\n    ).toBe(\"horizontal\");\n  });\n\n  test(\"if mixed result is mixed\", () => {\n    expect(\n      getRectsOrientation(horizontal[0], vertical[1], vertical[2]).type\n    ).toBe(\"mixed\");\n  });\n});\n\ndescribe(\"getIndexAdjustment\", () => {\n  const rect = { top: 100, left: 100, width: 200, height: 200 };\n\n  describe.each([true, false])(\"orientation.reverse=%s\", (reverse) => {\n    describe(\"orientation.type=vertical\", () => {\n      test(\"above\", () => {\n        expect(\n          getIndexAdjustment(\n            { x: rect.left + rect.width / 2, y: rect.top - 10 },\n            rect,\n            { type: \"vertical\", reverse }\n          )\n        ).toBe(reverse ? 1 : 0);\n      });\n      test(\"below\", () => {\n        expect(\n          getIndexAdjustment(\n            {\n              x: rect.left + rect.width / 2,\n              y: rect.top + rect.width + 10,\n            },\n            rect,\n            { type: \"vertical\", reverse }\n          )\n        ).toBe(reverse ? 0 : 1);\n      });\n      test(\"inside, above middle\", () => {\n        expect(\n          getIndexAdjustment(\n            {\n              x: rect.left + rect.width / 2,\n              y: rect.top + rect.height / 3,\n            },\n            rect,\n            { type: \"vertical\", reverse }\n          )\n        ).toBe(reverse ? 1 : 0);\n      });\n      test(\"inside, below middle\", () => {\n        expect(\n          getIndexAdjustment(\n            {\n              x: rect.left + rect.width / 2,\n              y: rect.top + rect.height * (2 / 3),\n            },\n            rect,\n            { type: \"vertical\", reverse }\n          )\n        ).toBe(reverse ? 0 : 1);\n      });\n    });\n    describe(\"orientation.type=horizontal\", () => {\n      test(\"to the left\", () => {\n        expect(\n          getIndexAdjustment(\n            { x: rect.left - 10, y: rect.top + rect.height / 2 },\n            rect,\n            { type: \"horizontal\", reverse }\n          )\n        ).toBe(reverse ? 1 : 0);\n      });\n      test(\"to the right\", () => {\n        expect(\n          getIndexAdjustment(\n            {\n              x: rect.left + rect.width + 10,\n              y: rect.top + rect.height / 2,\n            },\n            rect,\n            { type: \"horizontal\", reverse }\n          )\n        ).toBe(reverse ? 0 : 1);\n      });\n      test(\"inside, to the left of middle\", () => {\n        expect(\n          getIndexAdjustment(\n            {\n              x: rect.left + rect.width / 3,\n              y: rect.top + rect.height / 2,\n            },\n            rect,\n            { type: \"horizontal\", reverse }\n          )\n        ).toBe(reverse ? 1 : 0);\n      });\n      test(\"inside, to the right of middle\", () => {\n        expect(\n          getIndexAdjustment(\n            {\n              x: rect.left + rect.width * (2 / 3),\n              y: rect.top + rect.height / 2,\n            },\n            rect,\n            { type: \"horizontal\", reverse }\n          )\n        ).toBe(reverse ? 0 : 1);\n      });\n    });\n  });\n  describe(\"orientation.type=mixed\", () => {\n    test.each([\n      [\"above\", rect.left + rect.width / 2, rect.top - 10, 0],\n      [\"below\", rect.left + rect.width / 2, rect.top + rect.height + 10, 1],\n      [\"to the left\", rect.left - 10, rect.top + rect.height / 2, 0],\n      [\n        \"to the right\",\n        rect.left + rect.width + 10,\n        rect.top + rect.height / 2,\n        1,\n      ],\n      [\n        \"inside, above middle\",\n        rect.left + rect.width / 2,\n        rect.top + rect.height / 3,\n        0,\n      ],\n      [\n        \"inside, below middle\",\n        rect.left + rect.width / 2,\n        rect.top + rect.height * (2 / 3),\n        1,\n      ],\n      [\n        \"inside, to the left of middle\",\n        rect.left + rect.width / 3,\n        rect.top + rect.height / 2,\n        0,\n      ],\n      [\n        \"inside, to the right of middle\",\n        rect.left + rect.width * (2 / 3),\n        rect.top + rect.height / 2,\n        1,\n      ],\n    ])(\"%s\", (_, x, y, expected) => {\n      expect(getIndexAdjustment({ x, y }, rect, { type: \"mixed\" })).toBe(\n        expected\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/geometry-utils.ts",
    "content": "export type Rect = {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n};\n\nexport type Point = { x: number; y: number };\n\nexport type ChildrenOrientation =\n  | { type: \"horizontal\" | \"vertical\"; reverse: boolean }\n  | { type: \"mixed\"; reverse?: boolean };\n\nexport type Placement = {\n  parentRect: Rect;\n  type: \"between-children\" | \"next-to-child\" | \"inside-parent\";\n  x: number;\n  y: number;\n  length: number;\n  direction: \"horizontal\" | \"vertical\";\n};\n\nexport type Area = \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\";\n\n// https://stackoverflow.com/a/18157551/478603\nexport const getDistanceToRect = (rect: Rect, { x, y }: Point) => {\n  const dx = Math.max(rect.left - x, 0, x - (rect.left + rect.width));\n  const dy = Math.max(rect.top - y, 0, y - (rect.top + rect.height));\n  return Math.sqrt(dx * dx + dy * dy);\n};\n\nexport const getClosestRectIndex = (rects: Rect[], point: Point) => {\n  if (rects.length === 0) {\n    return -1;\n  }\n  const sorted = rects\n    .map((rect, index) => ({\n      index,\n      distance: getDistanceToRect(rect, point),\n    }))\n    .sort((a, b) => a.distance - b.distance);\n  return sorted[0].index;\n};\n\nexport const isEqualRect = (a: Rect | undefined, b: Rect) =>\n  a !== undefined &&\n  a.top === b.top &&\n  a.left === b.left &&\n  a.width === b.width &&\n  a.height === b.height;\n\nexport const getArea = (\n  { x, y }: Point,\n  edgeDistanceThreshold: number,\n  rect: Rect\n): Area => {\n  if (y - rect.top <= edgeDistanceThreshold) {\n    return \"top\";\n  }\n  if (rect.top + rect.height - y <= edgeDistanceThreshold) {\n    return \"bottom\";\n  }\n  if (x - rect.left <= edgeDistanceThreshold) {\n    return \"left\";\n  }\n  if (rect.left + rect.width - x <= edgeDistanceThreshold) {\n    return \"right\";\n  }\n  return \"center\";\n};\n\nexport const getPlacementBetween = (\n  parentRect: Rect,\n  a: Rect | undefined,\n  b: Rect | undefined\n): Placement | undefined => {\n  if (a === undefined || b === undefined) {\n    return;\n  }\n\n  const [firstY, secondY] = a.top < b.top ? [a, b] : [b, a];\n  const [firstX, secondX] = a.left < b.left ? [a, b] : [b, a];\n  const distanceY = secondY.top - firstY.top - firstY.height;\n  const distanceX = secondX.left - firstX.left - firstX.width;\n\n  // if rects overlap we don't want to put placement between them\n  if (distanceX < 0 && distanceY < 0) {\n    return;\n  }\n\n  // if rects aren't aligned (vertically or horizontally)\n  // we don't want to put placement between them\n  if (distanceX >= 0 && distanceY >= 0) {\n    return;\n  }\n\n  if (distanceX < 0) {\n    const minX = Math.min(a.left, b.left);\n    const maxX = Math.max(a.left + a.width, b.left + b.width);\n    return {\n      parentRect,\n      type: \"between-children\",\n      y: firstY.top + firstY.height + distanceY / 2,\n      x: minX,\n      length: maxX - minX,\n      direction: \"horizontal\",\n    };\n  }\n\n  const minY = Math.min(a.top, b.top);\n  const maxY = Math.max(a.top + a.height, b.top + b.height);\n  return {\n    parentRect,\n    type: \"between-children\",\n    y: minY,\n    x: firstX.left + firstX.width + distanceX / 2,\n    length: maxY - minY,\n    direction: \"vertical\",\n  };\n};\n\nexport const getPlacementNextTo = (\n  parentRect: Rect,\n  rect: Rect | undefined,\n  childrenOrientation: ChildrenOrientation,\n  direction: \"forward\" | \"backward\",\n  padding = 5\n): Placement | undefined => {\n  if (rect === undefined) {\n    return;\n  }\n\n  const isForward = childrenOrientation.reverse\n    ? direction === \"backward\"\n    : direction === \"forward\";\n\n  const side =\n    childrenOrientation.type === \"horizontal\"\n      ? isForward\n        ? \"right\"\n        : \"left\"\n      : isForward\n        ? \"bottom\"\n        : \"top\";\n\n  const getMargin = (distnaceToParentEdge: number) =>\n    Math.min(padding * 2, Math.max(0, distnaceToParentEdge)) / 2;\n\n  if (side === \"top\") {\n    return {\n      parentRect,\n      type: \"next-to-child\",\n      x: rect.left,\n      y: rect.top - getMargin(rect.top),\n      length: rect.width,\n      direction: \"horizontal\",\n    };\n  }\n\n  if (side === \"bottom\") {\n    return {\n      parentRect,\n      type: \"next-to-child\",\n      x: rect.left,\n      y:\n        rect.top +\n        rect.height +\n        getMargin(parentRect.height - rect.top - rect.height),\n      length: rect.width,\n      direction: \"horizontal\",\n    };\n  }\n\n  if (side === \"left\") {\n    return {\n      parentRect,\n      type: \"next-to-child\",\n      x: rect.left - getMargin(rect.left),\n      y: rect.top,\n      length: rect.height,\n      direction: \"vertical\",\n    };\n  }\n\n  return {\n    parentRect,\n    type: \"next-to-child\",\n    x:\n      rect.left +\n      rect.width +\n      getMargin(parentRect.width - rect.left - rect.width),\n    y: rect.top,\n    length: rect.height,\n    direction: \"vertical\",\n  };\n};\n\nexport const getPlacementInside = (\n  parentRect: Rect,\n  childrenOrientation: ChildrenOrientation,\n  padding = 5\n): Placement => {\n  if (childrenOrientation.type === \"horizontal\") {\n    const safePadding = Math.min(parentRect.width / 2, padding);\n    return {\n      parentRect,\n      type: \"inside-parent\",\n      y: parentRect.top + safePadding,\n      x: parentRect.left + safePadding,\n      length: parentRect.height - safePadding * 2,\n      direction: \"vertical\",\n    };\n  }\n\n  const safePadding = Math.min(parentRect.height / 2, padding);\n  return {\n    parentRect,\n    type: \"inside-parent\",\n    y: parentRect.top + safePadding,\n    x: parentRect.left + safePadding,\n    length: parentRect.width - safePadding * 2,\n    direction: \"horizontal\",\n  };\n};\n\nconst getSegmentsOrder = (\n  aStart: number,\n  aEnd: number,\n  bStart: number,\n  bEnd: number\n): \"b-first\" | \"a-first\" | \"overlap\" => {\n  if (aStart >= bEnd) {\n    return \"b-first\";\n  }\n  if (bStart >= aEnd) {\n    return \"a-first\";\n  }\n  return \"overlap\";\n};\n\nexport const getTwoRectsOrientation = (\n  first: Rect,\n  second: Rect\n): ChildrenOrientation => {\n  const xOrder = getSegmentsOrder(\n    first.left,\n    first.left + first.width,\n    second.left,\n    second.left + second.width\n  );\n  const yOrder = getSegmentsOrder(\n    first.top,\n    first.top + first.height,\n    second.top,\n    second.top + second.height\n  );\n  if (xOrder !== \"overlap\" && yOrder === \"overlap\") {\n    return { type: \"horizontal\", reverse: xOrder === \"b-first\" };\n  }\n  if (xOrder === \"overlap\" && yOrder !== \"overlap\") {\n    return { type: \"vertical\", reverse: yOrder === \"b-first\" };\n  }\n\n  return { type: \"mixed\" };\n};\n\nexport const getRectsOrientation = (\n  first: Rect | undefined,\n  second: Rect,\n  third: Rect | undefined\n): ChildrenOrientation => {\n  const orientations = [\n    first && getTwoRectsOrientation(first, second),\n    third && getTwoRectsOrientation(second, third),\n  ];\n\n  // @todo: Maybe we should check that at least one is reversed.\n  // Need to test what works better, but keep an eye on false positives.\n  const allReverse = orientations.every(\n    (orientation) => orientation?.reverse !== false\n  );\n\n  const types = orientations.map((orientation) => orientation?.type);\n\n  const includesVertical = types.includes(\"vertical\");\n  const includesHorizontal = types.includes(\"horizontal\");\n\n  if (includesVertical && includesHorizontal === false) {\n    return { type: \"vertical\", reverse: allReverse };\n  }\n\n  if (includesHorizontal && includesVertical === false) {\n    return { type: \"horizontal\", reverse: allReverse };\n  }\n\n  return { type: \"mixed\" };\n};\n\n// Determines whether we should place the item before or after the closest child.\n// Returns the number that should be added to the closest child index to get the final index.\nexport const getIndexAdjustment = (\n  pointer: Point,\n  closestChildRect: Rect | undefined,\n  { type: orientationType, reverse }: ChildrenOrientation\n) => {\n  if (closestChildRect === undefined) {\n    return 0;\n  }\n\n  const { top, left, width, height } = closestChildRect;\n\n  if (orientationType === \"vertical\") {\n    const middleY = top + height / 2;\n    return pointer.y < middleY ? (reverse ? 1 : 0) : reverse ? 0 : 1;\n  }\n\n  if (orientationType === \"horizontal\") {\n    const middleX = left + width / 2;\n    return pointer.x < middleX ? (reverse ? 1 : 0) : reverse ? 0 : 1;\n  }\n\n  // For the \"mixed\" orientation,\n  // we are looking at whether the pointer is above or below the diagonal\n\n  // diagonal equation\n  const getDiagonalY = (diagonalX: number) => {\n    if (width === 0) {\n      return;\n    }\n    const slope = height / width;\n    const topRightCorner = { x: left + width, y: top };\n    return slope * (topRightCorner.x - diagonalX) + topRightCorner.y;\n  };\n\n  const diagonalY = getDiagonalY(pointer.x);\n  if (diagonalY === undefined) {\n    return 0;\n  }\n  return pointer.y < diagonalY ? 0 : 1;\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/index.ts",
    "content": "export { useDrag } from \"./use-drag\";\nexport { useDrop, type DropTarget } from \"./use-drop\";\nexport { useAutoScroll } from \"./use-auto-scroll\";\nexport {\n  PlacementIndicator,\n  computeIndicatorPlacement,\n} from \"./placement-indicator\";\nexport { useHold } from \"./use-hold\";\nexport { useDragCursor } from \"./use-drag-cursor\";\nexport { useSortable } from \"./use-sortable\";\nexport {\n  type Rect,\n  type Point,\n  type Placement,\n  type ChildrenOrientation,\n} from \"./geometry-utils\";\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/placement-indicator.tsx",
    "content": "import { Box } from \"../../box\";\nimport { css, theme } from \"../../../stitches.config\";\nimport {\n  type ChildrenOrientation,\n  type Placement,\n  getPlacementBetween,\n  getPlacementInside,\n  getPlacementNextTo,\n  type Rect,\n} from \"./geometry-utils\";\nimport { defaultGetValidChildren, type DropTarget } from \"./use-drop\";\n\nconst placementStyle = css({\n  boxSizing: \"content-box\",\n  position: \"absolute\",\n  background: theme.colors.backgroundPrimary,\n  pointerEvents: \"none\",\n});\n\nconst getRect = (placement: Placement) => {\n  if (placement.direction === \"horizontal\") {\n    return {\n      top: placement.y - 1,\n      left: placement.x,\n      width: placement.length,\n      height: 2,\n    };\n  }\n  return {\n    top: placement.y,\n    left: placement.x - 1,\n    width: 2,\n    height: placement.length,\n  };\n};\n\nconst applyScale = (rect: Rect, scale: number) => {\n  // Calculate in the \"scale\" that is applied to the canvas\n  const scaleFactor = scale / 100;\n  return {\n    top: rect.top * scaleFactor,\n    left: rect.left * scaleFactor,\n    width: rect.width * scaleFactor,\n    height: rect.height,\n  };\n};\n\nexport const PlacementIndicator = ({\n  placement,\n  scale = 100,\n}: {\n  placement: Placement;\n  scale?: number;\n}) => {\n  return (\n    <Box\n      data-placement-indicator\n      style={applyScale(getRect(placement), scale)}\n      className={placementStyle()}\n    />\n  );\n};\n\ntype PlacementIndicatorOptions = {\n  placement: DropTarget<unknown>[\"placement\"];\n  element: Element;\n\n  // Allows you to customize children\n  // that will be used to determine placement and indexWithinChildren\n  getValidChildren?: (parent: Element) => Element[] | HTMLCollection;\n\n  // Distance from an edge when placement is put next to an element edge\n  placementPadding?: number;\n\n  // If not provided, will be guessed automatically based\n  // on the actual orientation of the children\n  childrenOrientation?: ChildrenOrientation;\n};\n\nexport const computeIndicatorPlacement = (\n  options: PlacementIndicatorOptions\n) => {\n  const {\n    placement,\n    element,\n    getValidChildren = defaultGetValidChildren,\n  } = options;\n  const parentRect = element.getBoundingClientRect();\n  const children = getValidChildren(element);\n  const { closestChildIndex, indexAdjustment, childrenOrientation } = placement;\n\n  const closestChildRect: undefined | DOMRect =\n    children[closestChildIndex]?.getBoundingClientRect();\n\n  const neighbourChildIndex =\n    indexAdjustment === 0 ? closestChildIndex - 1 : closestChildIndex + 1;\n  const neighbourChildRect: undefined | DOMRect =\n    children[neighbourChildIndex]?.getBoundingClientRect();\n\n  let placementIndicator = getPlacementBetween(\n    parentRect,\n    closestChildRect,\n    neighbourChildRect\n  );\n\n  // If childrenOrientation is set explicitly, we want to honor it:\n  // discard placement generated by getPlacementBetween if it has wrong direction\n  if (\n    options.childrenOrientation !== undefined &&\n    placementIndicator !== undefined &&\n    placementIndicator.direction === options.childrenOrientation.type\n  ) {\n    placementIndicator = undefined;\n  }\n\n  if (placementIndicator === undefined) {\n    placementIndicator = getPlacementNextTo(\n      parentRect,\n      closestChildRect,\n      childrenOrientation,\n      indexAdjustment > 0 ? \"forward\" : \"backward\",\n      options.placementPadding\n    );\n  }\n\n  if (placementIndicator === undefined) {\n    placementIndicator = getPlacementInside(\n      parentRect,\n      childrenOrientation,\n      options.placementPadding\n    );\n  }\n\n  return placementIndicator;\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/sortable-list.stories.tsx",
    "content": "import type { Meta } from \"@storybook/react\";\nimport { useState, useRef } from \"react\";\nimport { StorySection } from \"../../storybook\";\nimport { Box } from \"../../box\";\nimport { styled } from \"../../../stitches.config\";\nimport { useDrop, type DropTarget } from \"./use-drop\";\nimport { useDrag } from \"./use-drag\";\nimport {\n  computeIndicatorPlacement,\n  PlacementIndicator,\n} from \"./placement-indicator\";\nimport { useAutoScroll } from \"./use-auto-scroll\";\nimport { theme } from \"../../../stitches.config\";\nimport type { Placement } from \"./geometry-utils\";\n\ntype ItemData = { id: string; text: string };\n\nconst ListItem = styled(\"li\", {\n  display: \"block\",\n  margin: 10,\n  background: theme.colors.foregroundMoreSubtle,\n  padding: 10,\n  userSelect: \"none\",\n});\n\nconst List = styled(\"ul\", {\n  display: \"block\",\n  margin: 0,\n  padding: 10,\n});\n\nconst Item = ({\n  data,\n  isDragging,\n}: {\n  data: ItemData;\n  isDragging: boolean;\n}) => {\n  return (\n    <ListItem css={{ opacity: isDragging ? 0.3 : 1 }} data-id={data.id}>\n      {data.text}\n    </ListItem>\n  );\n};\n\nexport const SortableList = ({\n  direction = \"vertical\",\n  reversed = false,\n}: {\n  direction?: \"horizontal\" | \"vertical\" | \"wrap\";\n  reversed?: boolean;\n}) => {\n  const [data, setData] = useState([\n    { id: \"0\", text: \"First\" },\n    { id: \"1\", text: \"Second\" },\n    { id: \"2\", text: \"Third\" },\n    { id: \"3\", text: \"Fourth\" },\n    { id: \"4\", text: \"Fifth\" },\n    { id: \"5\", text: \"Sixth\" },\n    { id: \"6\", text: \"Seventh\" },\n    { id: \"7\", text: \"Eighth\" },\n    { id: \"8\", text: \"Ninth\" },\n    { id: \"9\", text: \"Tenth\" },\n    { id: \"10\", text: \"Eleventh\" },\n    { id: \"11\", text: \"Twelfth\" },\n    { id: \"12\", text: \"Thirteenth\" },\n    { id: \"13\", text: \"Fourteenth\" },\n    { id: \"14\", text: \"Fifteenth\" },\n    { id: \"15\", text: \"Sixteenth\" },\n    { id: \"16\", text: \"Seventeenth\" },\n    { id: \"17\", text: \"Eighteenth\" },\n  ] as ItemData[]);\n\n  const [dropTarget, setDropTarget] = useState<DropTarget<true>>();\n  const [placementIndicator, setPlacementIndicator] = useState<\n    undefined | Placement\n  >();\n  const [dragItemId, setDragItemId] = useState<string>();\n  const rootRef = useRef<HTMLUListElement | null>(null);\n\n  const useDropHandlers = useDrop<true>({\n    elementToData(element) {\n      return element instanceof HTMLUListElement;\n    },\n    swapDropTarget(dropTarget) {\n      if (dropTarget) {\n        return dropTarget;\n      }\n\n      if (rootRef.current === null) {\n        throw new Error(\"Unexpected empty rootRef during drag\");\n      }\n\n      return { data: true, element: rootRef.current };\n    },\n    onDropTargetChange(dropTarget) {\n      setDropTarget(dropTarget);\n      if (dropTarget === undefined) {\n        setPlacementIndicator(undefined);\n      } else {\n        setPlacementIndicator(\n          computeIndicatorPlacement({\n            placement: dropTarget.placement,\n            element: dropTarget.element,\n          })\n        );\n      }\n    },\n  });\n\n  const autoScrollHandlers = useAutoScroll();\n\n  const useDragHandlers = useDrag<string>({\n    elementToData(element) {\n      const id = element instanceof HTMLLIElement && element.dataset.id;\n      return id || false;\n    },\n    onStart({ data }) {\n      setDragItemId(data);\n      autoScrollHandlers.setEnabled(true);\n      useDropHandlers.handleStart();\n    },\n    onMove: (point) => {\n      useDropHandlers.handleMove(point);\n      autoScrollHandlers.handleMove(point);\n    },\n    onEnd({ isCanceled }) {\n      if (dropTarget !== undefined && dragItemId !== undefined) {\n        const oldIndex = data.findIndex((item) => item.id === dragItemId);\n        if (oldIndex !== -1) {\n          let newIndex = dropTarget.indexWithinChildren;\n\n          // placement.index does not take into account the fact that the drag item will be removed.\n          // we need to do this to account for it.\n          if (oldIndex < newIndex) {\n            newIndex = Math.max(0, newIndex - 1);\n          }\n\n          if (oldIndex !== newIndex) {\n            const newData = [...data];\n            newData.splice(oldIndex, 1);\n            newData.splice(newIndex, 0, data[oldIndex]);\n            setData(newData);\n          }\n        }\n      }\n\n      useDropHandlers.handleEnd({ isCanceled });\n      autoScrollHandlers.setEnabled(false);\n      setDragItemId(undefined);\n      setDropTarget(undefined);\n      setPlacementIndicator(undefined);\n    },\n  });\n\n  return (\n    <StorySection title=\"Sortable list\">\n      <Box\n        css={{\n          height: direction === \"horizontal\" ? \"auto\" : 500,\n          width: direction === \"horizontal\" ? 500 : 200,\n          overflow: \"auto\",\n          background: \"white\",\n          color: \"black\",\n\n          // these are needed to make scroll work with column-reverse/row-reverse below\n          display: \"flex\",\n          flexDirection: direction === \"horizontal\" ? \"row\" : \"column\",\n\n          // to make DnD work we have to disable scrolling using touch\n          touchAction: \"none\",\n        }}\n        ref={autoScrollHandlers.targetRef}\n        onScroll={useDropHandlers.handleScroll}\n      >\n        <List\n          ref={(element) => {\n            useDropHandlers.rootRef(element);\n            useDragHandlers.rootRef(element);\n            rootRef.current = element;\n          }}\n          css={{\n            li: { cursor: dragItemId === undefined ? \"grab\" : \"default\" },\n            display: \"flex\",\n            flexDirection:\n              direction === \"vertical\"\n                ? reversed\n                  ? \"column-reverse\"\n                  : \"column\"\n                : reversed\n                  ? \"row-reverse\"\n                  : \"row\",\n            flexWrap: direction === \"wrap\" ? \"wrap\" : \"none\",\n          }}\n        >\n          {data.map((item) => (\n            <Item\n              key={item.id}\n              data={item}\n              isDragging={item.id === dragItemId}\n            />\n          ))}\n        </List>\n      </Box>\n      {placementIndicator && (\n        <PlacementIndicator placement={placementIndicator} />\n      )}\n    </StorySection>\n  );\n};\n\nexport default {\n  title: \"Primitives/DND/Sortable List\",\n  component: SortableList,\n} satisfies Meta<typeof SortableList>;\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-auto-scroll.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { getSpeed } from \"./use-auto-scroll\";\n\ndescribe(\"getSpeed\", () => {\n  const CONTAINER_START = 0;\n  const CONTAINER_END = 100;\n  const EDGE_DISTANCE_THRESHOLD = 10;\n  const MIN_SPEED = 0;\n  const MAX_SPEED = 10;\n\n  test(\"speed is never over mod(maxSpeed)\", () => {\n    expect(\n      getSpeed(\n        CONTAINER_END + 100,\n        CONTAINER_START,\n        CONTAINER_END,\n        EDGE_DISTANCE_THRESHOLD,\n        MIN_SPEED,\n        MAX_SPEED\n      )\n    ).toBe(MAX_SPEED);\n    expect(\n      getSpeed(\n        CONTAINER_START - 100,\n        CONTAINER_START,\n        CONTAINER_END,\n        EDGE_DISTANCE_THRESHOLD,\n        MIN_SPEED,\n        MAX_SPEED\n      )\n    ).toBe(-MAX_SPEED);\n  });\n\n  test(\"when at the middle of container, speed is minSpeed\", () => {\n    expect(\n      getSpeed(\n        CONTAINER_START + (CONTAINER_END - CONTAINER_START) / 2,\n        CONTAINER_START,\n        CONTAINER_END,\n        EDGE_DISTANCE_THRESHOLD,\n        MIN_SPEED,\n        MAX_SPEED\n      )\n    ).toBe(MIN_SPEED);\n  });\n\n  test(\"when within the edgeDistanceThreshold bounds, speed is minSpeed\", () => {\n    expect(\n      getSpeed(\n        CONTAINER_END - EDGE_DISTANCE_THRESHOLD - 1,\n        CONTAINER_START,\n        CONTAINER_END,\n        EDGE_DISTANCE_THRESHOLD,\n        MIN_SPEED,\n        MAX_SPEED\n      )\n    ).toBe(MIN_SPEED);\n\n    expect(\n      getSpeed(\n        CONTAINER_START + EDGE_DISTANCE_THRESHOLD + 1,\n        CONTAINER_START,\n        CONTAINER_END,\n        EDGE_DISTANCE_THRESHOLD,\n        MIN_SPEED,\n        MAX_SPEED\n      )\n    ).toBe(MIN_SPEED);\n  });\n\n  test.each([0.2, 0.3, 0.9])(\n    \"speed is proportional to how close the pointer is to the edge (proportion=%d)\",\n    (proportion) => {\n      expect(\n        getSpeed(\n          CONTAINER_END - EDGE_DISTANCE_THRESHOLD * (1 - proportion),\n          CONTAINER_START,\n          CONTAINER_END,\n          EDGE_DISTANCE_THRESHOLD,\n          MIN_SPEED,\n          MAX_SPEED\n        )\n      ).toBe(MIN_SPEED + (MAX_SPEED - MIN_SPEED) * proportion);\n\n      expect(\n        getSpeed(\n          CONTAINER_START + EDGE_DISTANCE_THRESHOLD * (1 - proportion),\n          CONTAINER_START,\n          CONTAINER_END,\n          EDGE_DISTANCE_THRESHOLD,\n          MIN_SPEED,\n          MAX_SPEED\n        )\n      ).toBe((MIN_SPEED + (MAX_SPEED - MIN_SPEED) * proportion) * -1);\n    }\n  );\n});\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-auto-scroll.ts",
    "content": "import { useRef, useMemo } from \"react\";\nimport type { Point } from \"./geometry-utils\";\n\n// Time between frames of scroll animation in milliseconds\nconst FRAME_PERIOD = 30;\n\nexport const getSpeed = (\n  pointerPosition: number,\n  containerStart: number,\n  containerEnd: number,\n  edgeDistanceThreshold: number,\n  minSpeed: number,\n  maxSpeed: number\n) => {\n  const thresholdSafe = Math.min(\n    (containerEnd - containerStart) / 2,\n    edgeDistanceThreshold\n  );\n\n  const startAdjusted = containerStart + thresholdSafe;\n  const endAdjusted = containerEnd - thresholdSafe;\n\n  const distanceFromEdgeToSpeed = (distance: number) => {\n    // between 0 and 1\n    const normalized = Math.min(distance, thresholdSafe) / thresholdSafe;\n\n    // speed in pixels per second\n    return minSpeed + normalized * (maxSpeed - minSpeed);\n  };\n\n  if (pointerPosition < startAdjusted) {\n    return -1 * distanceFromEdgeToSpeed(startAdjusted - pointerPosition);\n  }\n\n  if (pointerPosition > endAdjusted) {\n    return distanceFromEdgeToSpeed(pointerPosition - endAdjusted);\n  }\n\n  return 0;\n};\n\ntype UseAutoScrollProps = {\n  edgeDistanceThreshold?: number;\n\n  // If set to true the entire document will be scrolled.\n  // No need to set targetRef in this case.\n  fullscreen?: boolean;\n\n  // min/max speed of the scroll animation in pixels per second\n  minSpeed?: number;\n  maxSpeed?: number;\n};\n\ntype UseAutoScrollHandlers = {\n  handleMove: (pointerCoordinate: Point) => void;\n  setEnabled: (enabled: boolean) => void;\n  targetRef: (element: HTMLElement | null) => void;\n};\n\nexport const useAutoScroll = (\n  props: UseAutoScrollProps = {}\n): UseAutoScrollHandlers => {\n  // We want to use fresh props every time we use them,\n  // but we don't need to react to updates.\n  // So we can put them in a ref and make useMemo below very efficient.\n  const latestProps = useRef<UseAutoScrollProps>(props);\n  latestProps.current = props;\n\n  const state = useRef({\n    target: null as HTMLElement | null,\n    enabled: false,\n    prevTimestamp: 0,\n    speedX: 0,\n    speedY: 0,\n    stepScheduled: false,\n  });\n\n  return useMemo(() => {\n    const getViewportRect = () => {\n      if (latestProps.current.fullscreen) {\n        return {\n          top: 0,\n          left: 0,\n          bottom: window.innerHeight,\n          right: window.innerWidth,\n        };\n      }\n\n      if (state.current.target === null) {\n        return;\n      }\n\n      return state.current.target.getBoundingClientRect();\n    };\n\n    const scrollBy = (x: number, y: number) => {\n      if (latestProps.current.fullscreen) {\n        window.scrollBy(x, y);\n      }\n\n      if (state.current.target === null) {\n        return;\n      }\n\n      state.current.target.scrollBy(x, y);\n    };\n\n    const step = (timestamp: number) => {\n      state.current.stepScheduled = false;\n\n      if (\n        !state.current.enabled ||\n        (Math.round((state.current.speedX / 1000) * FRAME_PERIOD) === 0 &&\n          Math.round((state.current.speedY / 1000) * FRAME_PERIOD) === 0)\n      ) {\n        return;\n      }\n\n      const elapsed = timestamp - state.current.prevTimestamp;\n\n      // to avoid a big jump when auto-scroll becomes enabled\n      if (elapsed > FRAME_PERIOD * 100) {\n        state.current.prevTimestamp = timestamp;\n        scheduleStep();\n        return;\n      }\n\n      if (elapsed < FRAME_PERIOD) {\n        scheduleStep();\n        return;\n      }\n\n      state.current.prevTimestamp = timestamp;\n\n      scrollBy(\n        (state.current.speedX / 1000) * elapsed,\n        (state.current.speedY / 1000) * elapsed\n      );\n\n      scheduleStep();\n    };\n\n    const scheduleStep = () => {\n      if (!state.current.stepScheduled) {\n        state.current.stepScheduled = true;\n        window.requestAnimationFrame(step);\n      }\n    };\n\n    const {\n      edgeDistanceThreshold = 20,\n      minSpeed = 1,\n      maxSpeed = 500,\n    } = latestProps.current;\n\n    return {\n      handleMove({ x, y }) {\n        if (!state.current.enabled) {\n          return;\n        }\n\n        const rect = getViewportRect();\n        if (rect === undefined) {\n          return;\n        }\n\n        state.current.speedY = getSpeed(\n          y,\n          rect.top,\n          rect.bottom,\n          edgeDistanceThreshold,\n          minSpeed,\n          maxSpeed\n        );\n\n        state.current.speedX = getSpeed(\n          x,\n          rect.left,\n          rect.right,\n          edgeDistanceThreshold,\n          minSpeed,\n          maxSpeed\n        );\n\n        scheduleStep();\n      },\n      setEnabled(newEnabled) {\n        state.current.enabled = newEnabled;\n        scheduleStep();\n      },\n      targetRef: (element) => {\n        state.current.target = element;\n      },\n    };\n  }, []);\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-drag-cursor.ts",
    "content": "import { useEffect } from \"react\";\n\nexport const useDragCursor = (isDragging: boolean, cursor = \"grabbing\") => {\n  useEffect(() => {\n    if (isDragging) {\n      const html = document.documentElement;\n      const originalValue = html.style.getPropertyValue(\"cursor\");\n      const originalPriority = html.style.getPropertyPriority(\"cursor\");\n      html.style.setProperty(\"cursor\", cursor, \"important\");\n      return () => {\n        html.style.setProperty(\"cursor\", originalValue, originalPriority);\n      };\n    }\n  }, [isDragging, cursor]);\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-drag.ts",
    "content": "import { useRef, useEffect, useMemo, useState } from \"react\";\nimport type { Point } from \"./geometry-utils\";\n\ntype State<Data> = {\n  status: \"pending\" | \"dragging\" | \"idle\";\n  initialX: number;\n  initialY: number;\n  shifts: number;\n  dragItemData: Data | undefined;\n  pointerId: number | undefined;\n};\n\nconst initialState: State<unknown> = {\n  status: \"idle\",\n  initialX: 0,\n  initialY: 0,\n  shifts: 0,\n  dragItemData: undefined,\n  pointerId: undefined,\n};\n\ntype UseDragProps<DragItemData> = {\n  startDistanceThreashold?: number;\n  shiftDistanceThreshold?: number;\n  /**\n   * Checks whether the given element can be dragged.\n   * If `false` is returned, drag will not start.\n   */\n  elementToData: (element: Element) => DragItemData | false;\n  onStart: (event: { data: DragItemData }) => void;\n  onMove: (event: Point) => void;\n  onShiftChange?: (event: { shifts: number }) => void;\n  onEnd: (event: { isCanceled: boolean }) => void;\n};\n\ntype UseDragHandlers = {\n  rootRef: (element: HTMLElement | null) => void;\n  cancelCurrentDrag: () => void;\n};\n\nexport const useDrag = <DragItemData>(\n  props: UseDragProps<DragItemData>\n): UseDragHandlers => {\n  // We want to use fresh props every time we use them,\n  // but we don't need to react to updates.\n  // So we can put them in a ref and make useMemo below very efficient.\n  const latestProps = useRef<UseDragProps<DragItemData>>(props);\n  latestProps.current = props;\n\n  const state = useRef<State<DragItemData>>({\n    ...(initialState as State<DragItemData>),\n  });\n\n  const [rootElement, setRootElement] = useState<HTMLElement | null>(null);\n\n  const { handlePointerDown, end } = useMemo(() => {\n    const detectShift = (x: number) => {\n      const { onShiftChange, shiftDistanceThreshold = 20 } =\n        latestProps.current;\n\n      const deltaX = x - state.current.initialX;\n      const shifts =\n        deltaX > 0\n          ? Math.floor(deltaX / shiftDistanceThreshold)\n          : Math.ceil(deltaX / shiftDistanceThreshold);\n\n      if (shifts !== state.current.shifts) {\n        state.current.shifts = shifts;\n        onShiftChange?.({ shifts });\n      }\n    };\n\n    const handleDragStart = (event: Event) => {\n      // Prevent default drag behavior. For example, dragging a link.\n      event.preventDefault();\n    };\n\n    const handlePointerDown = (event: PointerEvent) => {\n      if (event.button !== 0 || state.current.status !== \"idle\") {\n        return;\n      }\n\n      const data = latestProps.current.elementToData(event.target as Element);\n\n      if (data === false) {\n        return;\n      }\n\n      state.current = {\n        ...state.current,\n        status: \"pending\",\n        initialX: event.clientX,\n        initialY: event.clientY,\n        pointerId: event.pointerId,\n        dragItemData: data,\n      };\n\n      window.addEventListener(\"pointermove\", handlePointerMove);\n      window.addEventListener(\"pointerup\", handlePointerUp);\n      window.addEventListener(\"pointercancel\", handlePointerCancel);\n      window.addEventListener(\"dragstart\", handleDragStart);\n    };\n\n    const handlePointerMove = (event: PointerEvent) => {\n      // prevent text selecting while dragging\n      event.preventDefault();\n      const {\n        startDistanceThreashold = 3,\n        onStart,\n        onMove,\n      } = latestProps.current;\n\n      if (event.pointerId !== state.current.pointerId) {\n        return;\n      }\n\n      const x = event.clientX;\n      const y = event.clientY;\n\n      // We want to start dragging only when the user has moved more than startDistanceThreashold.\n      if (\n        state.current.status === \"pending\" &&\n        Math.abs(x - state.current.initialX) < startDistanceThreashold &&\n        Math.abs(y - state.current.initialY) < startDistanceThreashold\n      ) {\n        return;\n      }\n\n      if (\n        state.current.status === \"pending\" &&\n        state.current.dragItemData !== undefined\n      ) {\n        onStart({ data: state.current.dragItemData });\n        // onStart may call cancel and reset state\n        if (state.current.status === \"pending\") {\n          state.current.status = \"dragging\";\n        }\n      }\n\n      if (state.current.status === \"dragging\") {\n        onMove({ x, y });\n        detectShift(x);\n      }\n    };\n\n    const end = (isCanceled: boolean) => {\n      if (state.current.status === \"dragging\") {\n        // A drag is basically a very slow click.\n        // But we don't want it to register as a click.\n        const addedAt = Date.now();\n        window.addEventListener(\n          \"click\",\n          (event) => {\n            // if more than 300ms have passed, we assume this click is unrelated to the drag.\n            if (Date.now() - addedAt > 300) {\n              return;\n            }\n            event.preventDefault();\n            event.stopPropagation();\n          },\n          { capture: true, once: true }\n        );\n\n        latestProps.current.onEnd({ isCanceled });\n      }\n\n      state.current = { ...(initialState as State<DragItemData>) };\n\n      window.removeEventListener(\"pointermove\", handlePointerMove);\n      window.removeEventListener(\"pointerup\", handlePointerUp);\n      window.removeEventListener(\"pointercancel\", handlePointerCancel);\n      window.removeEventListener(\"dragstart\", handleDragStart);\n    };\n\n    const handlePointerUp = (event: PointerEvent) => {\n      if (event.pointerId === state.current.pointerId) {\n        end(false);\n      }\n    };\n\n    const handlePointerCancel = (event: PointerEvent) => {\n      if (event.pointerId === state.current.pointerId) {\n        end(true);\n      }\n    };\n\n    return {\n      handlePointerDown,\n      end,\n    };\n  }, []);\n\n  useEffect(\n    () => () => {\n      // A component can be disposed of during dragging\n      // (e.g., by pressing Escape or closing the components panel while dragging).\n      if (state.current.status === \"dragging\") {\n        end(true);\n      }\n    },\n    [end]\n  );\n\n  useEffect(() => {\n    if (rootElement !== null) {\n      rootElement.addEventListener(\"pointerdown\", handlePointerDown);\n      return () => {\n        rootElement.removeEventListener(\"pointerdown\", handlePointerDown);\n      };\n    }\n  }, [rootElement, handlePointerDown]);\n\n  // We want to return a stable object to avoid re-renders when it's a dependency\n  return useMemo(() => {\n    return {\n      rootRef(element) {\n        setRootElement(element);\n      },\n      cancelCurrentDrag() {\n        end(true);\n      },\n    };\n  }, [end]);\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-drop.ts",
    "content": "import { useRef, useMemo } from \"react\";\nimport {\n  isEqualRect,\n  getArea,\n  getClosestRectIndex,\n  getIndexAdjustment,\n  type Rect,\n  type Point,\n  type Area,\n  type ChildrenOrientation,\n} from \"./geometry-utils\";\nimport { getLocalChildrenOrientation, getChildrenRects } from \"./dom-utils\";\n\n// Partial information about a drop target\n// used during the selection of a new drop target\ntype PartialDropTarget<Data> = {\n  data: Data;\n  element: Element;\n};\n\nexport type DropTarget<Data> = PartialDropTarget<Data> & {\n  rect: DOMRect;\n  indexWithinChildren: number;\n  placement: {\n    closestChildIndex: number;\n    indexAdjustment: number;\n    childrenOrientation: ChildrenOrientation;\n  };\n};\n\n// We pass around data, to avoid extra data lookups.\n// For example, data found in elementToData\n// doesn't have to be looked up again in swapDropTarget.\ntype UseDropProps<Data> = {\n  /**\n   * To check that the element can qualify as a drop target.\n   * If `false` is returned, the parent of the element\n   * will be suggested instead, and so on.\n   * It's important to choose a correct element,\n   * because it will be used for edge detection.\n   */\n  elementToData: (target: Element) => Data | false;\n\n  // Distance from an edge to determine \"area\" value for swapDropTarget\n  edgeDistanceThreshold?: number;\n\n  // Given the potential target that has passed the elementToData check,\n  // and the position of the pointer on the target,\n  // you can swap to another target\n  swapDropTarget: (\n    // undefined is passed when no suitable element is found under the pointer\n    dropTarget: (PartialDropTarget<Data> & { area: Area }) | undefined\n  ) =>\n    | undefined\n    | (PartialDropTarget<Data> & {\n        // Set \"final\" to true if you don't want to swap any further.\n        // (Normally swapDropTarget is called repeatedly until the output is the same as the input)\n        final?: boolean;\n      });\n\n  onDropTargetChange: (dropTarget: undefined | DropTarget<Data>) => void;\n\n  // Allows you to customize children\n  // that will be used to determine placement and indexWithinChildren\n  getValidChildren?: (parent: Element) => Element[] | HTMLCollection;\n\n  // If not provided, will be guessed automatically based\n  // on the actual orientation of the children\n  childrenOrientation?: ChildrenOrientation;\n};\n\ntype UseDropHandlers = {\n  handleMove: (pointerCoordinates: Point) => void;\n  handleScroll: () => void;\n  handleStart: () => void;\n  handleEnd: (event: { isCanceled: boolean }) => void;\n  rootRef: (target: Element | null) => void;\n  handleDomMutation: () => void;\n};\n\nconst getInitialState = <Data>() => {\n  return {\n    started: false,\n    pointerCoordinates: undefined as Point | undefined,\n    dropTarget: undefined as DropTarget<Data> | undefined,\n    childrenRectsCache: new WeakMap<Element, Rect[]>(),\n    lastInitialCandidate: undefined as\n      | (PartialDropTarget<Data> & { area: Area })\n      | undefined,\n  };\n};\n\n/**\n * accept by default only children without data-placement-indicator\n * to avoid including placement indicator element which is often put\n * along with other children and is always close to dragging point\n */\nexport const defaultGetValidChildren = (parent: Element) =>\n  Array.from(parent.children).filter(\n    (element) =>\n      // The 'hidden' attribute is utilized by the react-arial FocusScope\n      // as helper elements to enable focus lock, focus wrap, and other features.\n      // We should exclude them from the list of children.\n      element.hasAttribute(\"hidden\") === false &&\n      element.hasAttribute(\"data-placement-indicator\") === false\n  );\n\nexport const useDrop = <Data>(props: UseDropProps<Data>): UseDropHandlers => {\n  // We want to use fresh props every time we use them,\n  // but we don't need to react to updates.\n  // So we can put them in a ref and make useMemo below very efficient.\n  const latestProps = useRef<UseDropProps<Data>>(props);\n  latestProps.current = props;\n\n  const rootRef = useRef<Element | null>(null);\n  const state = useRef(getInitialState<Data>());\n\n  // We want to return a stable object to avoid re-renders when it's a dependency\n  return useMemo(() => {\n    const getChildrenRectsMemoized = (parent: Element) => {\n      const { getValidChildren = defaultGetValidChildren } =\n        latestProps.current;\n      const fromCache = state.current.childrenRectsCache.get(parent);\n      if (fromCache !== undefined) {\n        return fromCache;\n      }\n      const result = getChildrenRects(parent, getValidChildren(parent));\n      state.current.childrenRectsCache.set(parent, result);\n      return result;\n    };\n\n    const setDropTarget = (\n      partialDropTarget: undefined | PartialDropTarget<Data>\n    ) => {\n      if (partialDropTarget === undefined) {\n        state.current.dropTarget = undefined;\n        latestProps.current.onDropTargetChange(undefined);\n        return;\n      }\n\n      const { pointerCoordinates } = state.current;\n      if (pointerCoordinates === undefined) {\n        return;\n      }\n\n      const parentRect = partialDropTarget.element.getBoundingClientRect();\n\n      const pointerRelativeToParent = {\n        x: pointerCoordinates.x - parentRect.left,\n        y: pointerCoordinates.y - parentRect.top,\n      };\n\n      const childrenRects = getChildrenRectsMemoized(partialDropTarget.element);\n\n      const closestChildIndex =\n        childrenRects.length === 0\n          ? 0\n          : getClosestRectIndex(childrenRects, pointerRelativeToParent);\n      const closestChildRect = childrenRects[closestChildIndex] as\n        | Rect\n        | undefined;\n\n      const childrenOrientation =\n        latestProps.current.childrenOrientation ??\n        getLocalChildrenOrientation(\n          partialDropTarget.element,\n          latestProps.current.getValidChildren ?? ((parent) => parent.children),\n          childrenRects,\n          closestChildIndex\n        );\n\n      const indexAdjustment = getIndexAdjustment(\n        pointerRelativeToParent,\n        closestChildRect,\n        childrenOrientation\n      );\n\n      const indexWithinChildren = closestChildIndex + indexAdjustment;\n\n      const current = state.current.dropTarget;\n      if (\n        current === undefined ||\n        current.element !== partialDropTarget.element ||\n        current.indexWithinChildren !== indexWithinChildren ||\n        isEqualRect(current.rect, parentRect) === false\n      ) {\n        const dropTarget: DropTarget<Data> = {\n          ...partialDropTarget,\n          rect: parentRect,\n          indexWithinChildren,\n          placement: {\n            closestChildIndex,\n            indexAdjustment,\n            childrenOrientation,\n          },\n        };\n\n        state.current.dropTarget = dropTarget;\n        latestProps.current.onDropTargetChange(dropTarget);\n      }\n    };\n\n    const detectTarget = () => {\n      const {\n        edgeDistanceThreshold = 3,\n        elementToData,\n        swapDropTarget,\n      } = latestProps.current;\n\n      if (state.current.started === false) {\n        return;\n      }\n\n      const { pointerCoordinates } = state.current;\n      const root = rootRef.current;\n\n      const withArea = (\n        candidate: PartialDropTarget<Data> | null | undefined\n      ) => {\n        if (candidate == null || pointerCoordinates === undefined) {\n          return;\n        }\n        return {\n          ...candidate,\n          area: getArea(\n            pointerCoordinates,\n            edgeDistanceThreshold,\n            candidate.element.getBoundingClientRect()\n          ),\n        };\n      };\n\n      // @todo: Cache this?\n      // Not expensive by itself, but it may call elementToData multiple times.\n      let candidate = withArea(\n        root &&\n          findClosestDropTarget({\n            root,\n            initialElement:\n              pointerCoordinates &&\n              document.elementFromPoint(\n                pointerCoordinates.x,\n                pointerCoordinates.y\n              ),\n            elementToData,\n          })\n      );\n\n      // To avoid calling swapDropTarget unnecessarily on every pointermove\n      const isNewInitialCandidate =\n        candidate?.element !== state.current.lastInitialCandidate?.element ||\n        candidate?.area !== state.current.lastInitialCandidate?.area;\n      state.current.lastInitialCandidate = candidate;\n      if (isNewInitialCandidate === false && state.current.dropTarget) {\n        // Still need to call setDropTarget to update rect and/or placement.\n        // Because indexWithinChildren might have changed,\n        // or parent coordinates might have moved in case of a scroll\n        setDropTarget(state.current.dropTarget);\n        return;\n      }\n\n      let continueSwapping = true;\n      while (continueSwapping || candidate === undefined) {\n        const swappedTo = swapDropTarget(candidate);\n        if (swappedTo === undefined) {\n          candidate = undefined;\n          break;\n        }\n        continueSwapping =\n          swappedTo.element !== candidate?.element && swappedTo.final !== true;\n        candidate = withArea(swappedTo);\n      }\n\n      setDropTarget(candidate);\n    };\n\n    return {\n      handleMove(pointerCoordinates) {\n        state.current.pointerCoordinates = pointerCoordinates;\n        detectTarget();\n      },\n\n      handleScroll() {\n        detectTarget();\n      },\n\n      handleStart() {\n        state.current.started = true;\n      },\n\n      handleEnd() {\n        state.current = getInitialState();\n      },\n\n      rootRef(rootElement) {\n        rootRef.current = rootElement;\n      },\n\n      handleDomMutation() {\n        state.current.childrenRectsCache = new WeakMap();\n        state.current.lastInitialCandidate = undefined;\n        detectTarget();\n      },\n    };\n  }, []);\n};\n\nconst findClosestDropTarget = <Data>({\n  root,\n  initialElement,\n  elementToData,\n}: {\n  root: Element;\n  initialElement: Element | undefined | null;\n  elementToData: (target: Element) => Data | false;\n}): PartialDropTarget<Data> | undefined => {\n  // The element we get from elementFromPoint() might not be inside the root\n  if (initialElement === undefined || root.contains(initialElement) === false) {\n    return;\n  }\n\n  let currentElement = initialElement;\n  while (currentElement != null) {\n    const data = elementToData(currentElement);\n    if (data !== false) {\n      return { data: data, element: currentElement };\n    }\n    if (currentElement === root) {\n      break;\n    }\n    currentElement = currentElement.parentElement;\n  }\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-hold.ts",
    "content": "import { useEffect, useMemo, useRef } from \"react\";\n\ntype UseHoldProps<Data> = {\n  data: Data;\n  isEqual: (a: Data, b: Data) => boolean;\n  holdTimeThreshold: number;\n  onHold: (data: Data) => void;\n};\n\n// Detects when a piece of data stays the same for a given amount of time\nexport const useHold = <Data>(props: UseHoldProps<Data>) => {\n  const state = useRef({\n    currentData: undefined as Data | undefined,\n    timeoutId: undefined as NodeJS.Timeout | undefined,\n  });\n\n  // We want to use fresh props every time we use them,\n  // but we don't need to react to updates.\n  // So we can put them in a ref and make useMemo below very efficient.\n  const latestProps = useRef<UseHoldProps<Data>>(props);\n\n  useEffect(() => {\n    const data = props.data;\n    const { currentData } = state.current;\n    const { isEqual, holdTimeThreshold } = latestProps.current;\n\n    if (currentData !== undefined && isEqual(currentData, data)) {\n      return;\n    }\n\n    clearTimeout(state.current.timeoutId);\n\n    state.current.timeoutId = setTimeout(() => {\n      state.current.timeoutId = undefined;\n      latestProps.current.onHold(data);\n    }, holdTimeThreshold);\n  }, [props.data]);\n\n  // We want to return a stable object to avoid re-renders when it's a dependency\n  return useMemo(() => {\n    return {\n      reset() {\n        clearTimeout(state.current.timeoutId);\n        state.current = { currentData: undefined, timeoutId: undefined };\n      },\n    };\n  }, []);\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/dnd/use-sortable.tsx",
    "content": "import { useState, useRef } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useDrop, type DropTarget } from \"./use-drop\";\nimport { useDrag } from \"./use-drag\";\nimport type { Placement } from \"./geometry-utils\";\nimport { useDragCursor } from \"./use-drag-cursor\";\nimport {\n  PlacementIndicator,\n  computeIndicatorPlacement,\n} from \"./placement-indicator\";\n\ntype UseSortable<Item> = {\n  items: Array<Item>;\n  onSort?: (newIndex: number, oldIndex: number) => void;\n};\n\nconst getItemId = (element: Element) =>\n  element instanceof HTMLElement ? element.dataset?.id : undefined;\n\nexport const useSortable = <Item extends { id: string }>({\n  items,\n  onSort,\n}: UseSortable<Item>) => {\n  const [dropTarget, setDropTarget] = useState<DropTarget<true>>();\n  const [placementIndicator, setPlacementIndicator] = useState<\n    undefined | Placement\n  >();\n  const [dragItemId, setDragItemId] = useState<string>();\n  const rootRef = useRef<HTMLDivElement | null>(null);\n\n  useDragCursor(dragItemId !== undefined);\n\n  // drop target is always root\n  // we need useDrop only for dropTarget.placement & dropTarget.indexWithinChildren\n  const useDropHandlers = useDrop<true>({\n    elementToData() {\n      return true;\n    },\n    swapDropTarget() {\n      if (rootRef.current === null) {\n        throw new Error(\"Unexpected empty rootRef during drag\");\n      }\n      return { data: true, element: rootRef.current };\n    },\n    onDropTargetChange(dropTarget) {\n      setDropTarget(dropTarget);\n      if (dropTarget === undefined) {\n        setPlacementIndicator(undefined);\n      } else {\n        setPlacementIndicator(\n          computeIndicatorPlacement({\n            placement: dropTarget.placement,\n            element: dropTarget.element,\n          })\n        );\n      }\n    },\n  });\n\n  const useDragHandlers = useDrag<string>({\n    elementToData(element) {\n      // disable drag unless there are at least 2 items\n      if (items.length < 2) {\n        return false;\n      }\n\n      const closest = element.closest(\"[data-id]\");\n      if (closest === null) {\n        return false;\n      }\n\n      return getItemId(closest) || false;\n    },\n    onStart({ data }) {\n      setDragItemId(data);\n      useDropHandlers.handleStart();\n    },\n    onMove: (point) => {\n      useDropHandlers.handleMove(point);\n    },\n    onEnd({ isCanceled }) {\n      useDropHandlers.handleEnd({ isCanceled });\n      setDragItemId(undefined);\n      setDropTarget(undefined);\n      setPlacementIndicator(undefined);\n\n      if (isCanceled || dropTarget === undefined || dragItemId === undefined) {\n        return;\n      }\n\n      const oldIndex = items.findIndex((item) => item.id === dragItemId);\n      if (oldIndex !== -1) {\n        let newIndex = dropTarget.indexWithinChildren;\n\n        // placement.index does not take into account the fact that the drag item will be removed.\n        // we need to do this to account for it.\n        if (oldIndex < newIndex) {\n          newIndex = Math.max(0, newIndex - 1);\n        }\n\n        if (oldIndex !== newIndex) {\n          onSort?.(newIndex, oldIndex);\n        }\n      }\n    },\n  });\n\n  const placementIndicatorElement = placementIndicator\n    ? createPortal(\n        <PlacementIndicator placement={placementIndicator} />,\n        document.body\n      )\n    : undefined;\n\n  const sortableRefCallback = (element: HTMLDivElement | null) => {\n    useDropHandlers.rootRef(element);\n    useDragHandlers.rootRef(element);\n    rootRef.current = element;\n  };\n\n  return {\n    sortableRefCallback,\n    dragItemId,\n    placementIndicator: placementIndicatorElement,\n  };\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/is-truncated.tsx",
    "content": "import { type RefObject, useState, useEffect } from \"react\";\n\n/**\n * Checks whether text was truncated in an element with `text-overflow: ellipsis`\n */\nexport const useIsTruncated = (\n  ref: RefObject<null | HTMLElement>,\n  text: string\n) => {\n  const [isTruncated, setIsTruncated] = useState(false);\n\n  useEffect(() => {\n    if (ref.current) {\n      const { offsetWidth, scrollWidth } = ref.current;\n      // https://stackoverflow.com/a/10017343/478603\n      setIsTruncated(offsetWidth < scrollWidth);\n    }\n\n    // ref is in dependencies just to make eslint happy\n  }, [ref, text]);\n\n  return isTruncated;\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/list.tsx",
    "content": "import { usePress, type PressEvent } from \"@react-aria/interactions\";\nimport { Slot, type SlotProps } from \"@radix-ui/react-slot\";\nimport { ArrowFocus } from \"./arrow-focus\";\n\ntype ListProps = SlotProps & {\n  asChild?: boolean;\n};\n\nexport const List = ({ asChild, ...props }: ListProps) => {\n  const Component = asChild ? Slot : \"ul\";\n  return (\n    <ArrowFocus\n      render={({ handleKeyDown }) => {\n        return (\n          <Component role=\"listbox\" onKeyDown={handleKeyDown} {...props} />\n        );\n      }}\n    />\n  );\n};\nList.displayName = \"List\";\n\ntype ListItemProps = SlotProps & {\n  state?: \"disabled\" | \"selected\";\n  current?: boolean;\n  index?: number;\n  onSelect?: (event: PressEvent) => void;\n  asChild?: boolean;\n};\n\nexport const ListItem = ({\n  state,\n  current,\n  index,\n  onSelect,\n  asChild,\n  ...props\n}: ListItemProps) => {\n  const stateProp =\n    state === \"disabled\"\n      ? { \"data-state\": \"disabled\" }\n      : state === \"selected\"\n        ? { \"data-state\": \"selected\" }\n        : undefined;\n  const { pressProps } = usePress({\n    onPress(event) {\n      onSelect?.(event);\n    },\n  });\n  const Component = asChild ? Slot : \"li\";\n  return (\n    <Component\n      tabIndex={index === 0 ? 0 : -1}\n      role=\"option\"\n      key={index}\n      {...(state === \"selected\" ? { \"aria-selected\": true } : undefined)}\n      {...(current ? { \"aria-current\": true } : undefined)}\n      {...props}\n      {...stateProp}\n      {...pressProps}\n    />\n  );\n};\n\nListItem.displayName = \"ListItem\";\n\nexport const findNextListItemIndex = (\n  currentIndex: number,\n  total: number,\n  direction: \"next\" | \"previous\"\n) => {\n  const nextIndex =\n    direction === \"next\"\n      ? currentIndex + 1\n      : direction === \"previous\"\n        ? currentIndex - 1\n        : currentIndex;\n\n  if (nextIndex < 0) {\n    return total - 1;\n  }\n  if (nextIndex >= total) {\n    return 0;\n  }\n  return nextIndex;\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/numeric-gesture-control.stories.tsx",
    "content": "import { useRef, useEffect, type RefObject } from \"react\";\nimport {\n  numericScrubControl,\n  type NumericScrubValue,\n  type NumericScrubDirection,\n} from \"./numeric-gesture-control\";\nimport { StorySection } from \"../storybook\";\n\nconst useNumericScrubControl = ({\n  ref,\n  value,\n  direction,\n  acceleration,\n}: {\n  ref: RefObject<null | HTMLInputElement>;\n  value: NumericScrubValue;\n  direction: NumericScrubDirection;\n  acceleration: number;\n}) => {\n  useEffect(() => {\n    if (ref.current === null) {\n      return;\n    }\n    ref.current.value = String(value);\n    return numericScrubControl(ref.current, {\n      getInitialValue: () => value,\n      direction,\n      getAcceleration() {\n        return acceleration;\n      },\n      onValueInput: (event) => {\n        (event.target as HTMLInputElement).value = String(event.value);\n      },\n      onValueChange: (event) => {\n        event.preventDefault();\n        (event.target as HTMLInputElement).value = String(event.value);\n        (event.target as HTMLInputElement).select();\n      },\n    });\n  }, [direction, value, acceleration, ref]);\n};\n\nconst Input = ({\n  value,\n  direction,\n  acceleration,\n}: {\n  value: NumericScrubValue;\n  direction: NumericScrubDirection;\n  acceleration: number;\n}) => {\n  const ref = useRef<HTMLInputElement | null>(null);\n  useNumericScrubControl({ ref, value, direction, acceleration });\n  return <input defaultValue={value} ref={ref} />;\n};\n\nconst ConstrainedInput = ({\n  value,\n  direction,\n  minValue,\n  maxValue,\n}: {\n  value: NumericScrubValue;\n  direction: NumericScrubDirection;\n  minValue: number;\n  maxValue: number;\n}) => {\n  const ref = useRef<HTMLInputElement | null>(null);\n  useEffect(() => {\n    if (ref.current === null) {\n      return;\n    }\n    ref.current.value = String(value);\n    return numericScrubControl(ref.current, {\n      getInitialValue: () => value,\n      direction,\n      minValue,\n      maxValue,\n      getAcceleration: () => 1,\n      onValueInput: (event) => {\n        (event.target as HTMLInputElement).value = String(event.value);\n      },\n      onValueChange: (event) => {\n        event.preventDefault();\n        (event.target as HTMLInputElement).value = String(event.value);\n        (event.target as HTMLInputElement).select();\n      },\n    });\n  }, [direction, value, minValue, maxValue, ref]);\n  return <input defaultValue={value} ref={ref} />;\n};\n\nexport const NumericGestureControl = () => (\n  <>\n    <StorySection title=\"Horizontal control\">\n      <Input value={0} direction=\"horizontal\" acceleration={1} />\n    </StorySection>\n\n    <StorySection title=\"Vertical control\">\n      <Input value={0} direction=\"vertical\" acceleration={1} />\n    </StorySection>\n\n    <StorySection title=\"Constrained control (0-100)\">\n      <ConstrainedInput\n        value={50}\n        direction=\"horizontal\"\n        minValue={0}\n        maxValue={100}\n      />\n    </StorySection>\n  </>\n);\n\nexport default {\n  title: \"Primitives/Numeric Gesture Control\",\n  component: Input,\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/numeric-gesture-control.ts",
    "content": "/**\n * @description\n * - detects pointer movements in the specified direction\n * - avails an appropriate cursor\n * - dispatches {onValueChange} method while pointermove and pointerdown are active\n * - dispatched {onValueChange} recieves a new value: old value + accumulation of the pointer move axis\n * @example\n * numericScrubControl(document.querySelector('input'), {\n *   onValueChange: (event) => {\n *     event.preventDefault();\n *     event.target.value = event.value;\n *     event.target.select();\n *   }\n * });\n */\n\nimport { clamp } from \"@react-aria/utils\";\nimport { css } from \"../../stitches.config\";\n\nconst scrubUI = css({\n  \"*\": {\n    userSelect: \"none!important\",\n    touchAction: \"none!important\",\n  },\n});\n\nconst cursorUI = css({\n  variants: {\n    direction: {\n      horizontal: {\n        cursor: \"ew-resize!important\",\n        \"*\": {\n          cursor: \"ew-resize!important\",\n        },\n      },\n      vertical: {\n        cursor: \"ns-resize!important\",\n        \"*\": {\n          cursor: \"ns-resize!important\",\n        },\n      },\n    },\n  },\n});\n\nexport type NumericScrubDirection = \"horizontal\" | \"vertical\";\n\nexport type NumericScrubValue = number;\n\nexport type NumericScrubEvent = {\n  type: \"scrubend\" | \"scrubbing\";\n  target: HTMLElement | SVGElement;\n  value: NumericScrubValue;\n  preventDefault: () => void;\n};\n\ntype NumericScrubCallback = (event: NumericScrubEvent) => void;\n\nexport type NumericScrubOptions = {\n  inverse?: boolean;\n  getAcceleration?: () => number | undefined;\n  minValue?: NumericScrubValue;\n  maxValue?: NumericScrubValue;\n  distanceThreshold?: number;\n  getInitialValue: () => number;\n  onStart?: () => void;\n  getValue?: (\n    state: NumericScrubState,\n    movement: number,\n    options: NumericScrubOptions\n  ) => number;\n  direction?: NumericScrubDirection;\n  onValueInput?: NumericScrubCallback;\n  onValueChange?: NumericScrubCallback;\n  onAbort?: () => void;\n  onStatusChange?: (status: \"idle\" | \"scrubbing\") => void;\n  shouldHandleEvent?: (node: Node) => boolean;\n};\n\ntype NumericScrubState = {\n  value: number;\n  cursor?: SVGElement;\n  direction: NumericScrubDirection;\n  status: \"idle\" | \"scrubbing\";\n  /**\n   * On Windows, requestPointerLock might already be called,\n   * but document.pointerLockElement may not have been updated yet.\n   */\n  pointerCaptureRequested: boolean;\n};\n\nconst getValueDefault = (\n  state: NumericScrubState,\n  movement: number,\n  {\n    minValue = Number.MIN_SAFE_INTEGER,\n    maxValue = Number.MAX_SAFE_INTEGER,\n    getAcceleration = () => 1,\n  }: NumericScrubOptions\n) => {\n  // toFixed is needed to fix `1.3 - 1 = 0.30000000000000004`\n  const acceleration = getAcceleration() ?? 1;\n  const value = Number((state.value + movement * acceleration).toFixed(2));\n  return clamp(value, minValue, maxValue);\n};\n\nconst preventContextMenu = () => {\n  const handler = (event: MouseEvent) => {\n    event.preventDefault();\n  };\n  window.addEventListener(\"contextmenu\", handler);\n  return () => {\n    window.removeEventListener(\"contextmenu\", handler);\n  };\n};\n\nconst scrubTimeout = 150;\n\nconst addScrubUi = () => {\n  const className = scrubUI();\n  const timerId = setTimeout(() => {\n    // Fixes Safari hovers during scrubbing\n    window.document.documentElement.setAttribute(\"inert\", \"true\");\n  }, scrubTimeout);\n\n  window.document.documentElement.classList.add(className);\n\n  return () => {\n    clearTimeout(timerId);\n    window.document.documentElement.classList.remove(className);\n    window.document.documentElement.removeAttribute(\"inert\");\n  };\n};\n\nconst addCursorUI = (direction: NumericScrubDirection) => {\n  const cursorClassNames = cursorUI({ direction }).toString().split(\" \");\n\n  const timerId = setTimeout(() => {\n    for (const cursorClassName of cursorClassNames) {\n      window.document.documentElement.classList.add(cursorClassName);\n    }\n  }, scrubTimeout);\n\n  return () => {\n    for (const cursorClassName of cursorClassNames) {\n      window.document.documentElement.classList.remove(cursorClassName);\n    }\n    clearTimeout(timerId);\n  };\n};\n\nconst isWindows = () => {\n  if (typeof window !== \"undefined\") {\n    return navigator.platform.toLowerCase().includes(\"win\");\n  }\n\n  return false;\n};\n\nexport const numericScrubControl = (\n  targetNode: HTMLElement | SVGElement,\n  options: NumericScrubOptions\n) => {\n  const {\n    getInitialValue,\n    onStart,\n    getValue = getValueDefault,\n    direction = \"horizontal\",\n    distanceThreshold = 0,\n    onValueInput,\n    onValueChange,\n    onAbort,\n    onStatusChange,\n    shouldHandleEvent,\n  } = options;\n\n  const cleanupTasks: Array<() => void> = [];\n  const disposeOnCleanup = (fn: () => () => void) => cleanupTasks.push(fn());\n\n  const state: NumericScrubState = {\n    // We will read value lazyly in a moment it will be used to avoid having outdated value\n    value: -1,\n    cursor: undefined,\n    direction,\n    status: \"idle\",\n    pointerCaptureRequested: false,\n  };\n\n  // The appearance of the custom cursor is delayed, so we need to track the mouse position\n  // for its initial placement.\n  const mouseState = {\n    x: 0,\n    y: 0,\n  };\n\n  const cleanup = () => {\n    for (const task of [...cleanupTasks.reverse()]) {\n      task();\n    }\n\n    cleanupTasks.length = 0;\n\n    if (state.status === \"scrubbing\") {\n      state.status = \"idle\";\n      onStatusChange?.(\"idle\");\n    }\n  };\n\n  let disposeCursorUI: () => void | undefined;\n\n  // Called on ESC key press or in cases of third-party pointer lock exit.\n  const handlePointerLockChange = () => {\n    if (document.pointerLockElement !== targetNode) {\n      // Reset the value to the initial value\n      cleanup();\n      onAbort?.();\n      return;\n    }\n\n    disposeCursorUI?.();\n  };\n\n  // Cannot define `event:` as PointerEvent,\n  // because (HTMLElement | SVGElement).addEventListener(\"pointermove\", ...)\n  // takes (Event => void) as a callback\n  const handleEvent = (event: Event) => {\n    // For TypeScript\n    if (!(event instanceof PointerEvent)) {\n      return;\n    }\n\n    const { type } = event;\n\n    switch (type) {\n      case \"pointerup\": {\n        const shouldComponentUpdate = state.status === \"scrubbing\";\n\n        cleanup();\n\n        if (shouldComponentUpdate) {\n          onValueChange?.({\n            type: \"scrubend\",\n            target: targetNode,\n            value: state.value,\n            preventDefault: () => event.preventDefault(),\n          });\n        }\n\n        break;\n      }\n      case \"pointerdown\": {\n        cleanup();\n\n        if (\n          event.target &&\n          shouldHandleEvent?.(event.target as Node) === false\n        ) {\n          return;\n        }\n        // light touches don't register corresponding pointerup\n        if (event.pressure === 0 || event.button !== 0) {\n          break;\n        }\n\n        mouseState.x = event.clientX;\n        mouseState.y = event.clientY;\n\n        onStart?.();\n        state.value = getInitialValue();\n\n        disposeOnCleanup(() =>\n          requestPointerLock(state, mouseState, event, targetNode)\n        );\n        disposeOnCleanup(() => addScrubUi());\n\n        disposeCursorUI = addCursorUI(options.direction ?? \"horizontal\");\n        disposeOnCleanup(() => disposeCursorUI);\n\n        disposeOnCleanup(() => {\n          const abortController = new AbortController();\n          const eventOptions = {\n            signal: abortController.signal,\n          };\n\n          targetNode.addEventListener(\"pointermove\", handleEvent, eventOptions);\n\n          document.addEventListener(\n            \"pointerlockchange\",\n            handlePointerLockChange,\n            eventOptions\n          );\n\n          targetNode.addEventListener(\n            \"click\",\n            (event) => {\n              // Prevent the click event from firing during scrubbing\n              // Resolves issues with margin scrubbing and opening inputs after scrubbing\n              // Fixes unintended click events triggered during canvas resizing\n              if (state.status === \"scrubbing\") {\n                event.preventDefault();\n                event.stopImmediatePropagation();\n              }\n            },\n            eventOptions\n          );\n\n          return () => {\n            abortController.abort();\n          };\n        });\n\n        // Pointer event will stop firing on touch after ~300ms because browser starts scrolling the page.\n        // restoreUserSelect = setRootStyle(targetNode, \"user-select\", \"none\");\n        // In chrome mobile touch simulation, you will get the context menu because tapping and holding\n        // results in a right click.\n        disposeOnCleanup(preventContextMenu);\n        break;\n      }\n      case \"pointermove\": {\n        const { movementY, movementX } = event;\n\n        const movement = direction === \"horizontal\" ? movementX : -movementY;\n\n        // console.log(\"movement\", movement, event);\n        mouseState.x = event.clientX;\n        mouseState.y = event.clientY;\n\n        const nextValue = getValue(state, movement, options);\n        if (nextValue === state.value) {\n          break;\n        }\n        state.value = nextValue;\n\n        if (state.status !== \"scrubbing\") {\n          const initialValue = getInitialValue();\n          // If the value is not changing enough, we don't want to start scrubbing.\n          if (Math.abs(initialValue - nextValue) < distanceThreshold) {\n            return;\n          }\n          // We need to reset the value to the initial so that the actual value starts from the initial value\n          // when we start calling onValueInput.\n          state.value = initialValue;\n          state.status = \"scrubbing\";\n          onStatusChange?.(\"scrubbing\");\n        }\n        onValueInput?.({\n          type: \"scrubbing\",\n          target: targetNode,\n          value: state.value,\n          preventDefault: () => event.preventDefault(),\n        });\n\n        if (state.cursor) {\n          // When cursor moves out of the browser window\n          // we want it to come back from the other side\n          const top = wrapAround(\n            Number.parseFloat(state.cursor.style.top) + movementY,\n            0,\n            globalThis.innerHeight\n          );\n          const left = wrapAround(\n            Number.parseFloat(state.cursor.style.left) + movementX,\n            0,\n            globalThis.innerWidth\n          );\n\n          // We allow movement on both axis to allow user to\n          // move cursor away from the value text to not obscure it\n          state.cursor.style.top = `${top}px`;\n          state.cursor.style.left = `${left}px`;\n        }\n        break;\n      }\n\n      case \"pointercancel\": {\n        cleanup();\n        break;\n      }\n\n      case \"lostpointercapture\": {\n        // On Mac if this happens it's near 100% probability that pointerup event will not fire\n        if (isWindows()) {\n          // This Windows fix cause other bug, in some cases pointerup event will not fire\n          if (state.pointerCaptureRequested) {\n            break;\n          }\n        }\n\n        if (document.pointerLockElement === null) {\n          cleanup();\n        }\n        break;\n      }\n    }\n  };\n\n  const abortController = new AbortController();\n  const eventOptions = { signal: abortController.signal };\n\n  const eventNames = [\n    \"pointerup\",\n    \"pointerdown\",\n    \"pointercancel\",\n    \"lostpointercapture\",\n  ] as const;\n  eventNames.forEach((eventName) =>\n    targetNode.addEventListener(eventName, handleEvent, eventOptions)\n  );\n\n  // Prevents dragging of the input content\n  // Dragging breaks the setPointerCapture\n  targetNode.addEventListener(\n    \"dragstart\",\n    (event) => {\n      event.preventDefault();\n    },\n    eventOptions\n  );\n\n  return () => {\n    abortController.abort();\n    cleanup();\n  };\n};\n\nconst requestPointerLockSafe = async (targetNode: HTMLElement | SVGElement) => {\n  try {\n    return await targetNode.requestPointerLock({\n      unadjustedMovement: true,\n    });\n  } catch {\n    // Some platforms may not support unadjusted movement.\n    return await targetNode.requestPointerLock();\n  }\n};\n\nconst requestPointerLock = (\n  state: NumericScrubState,\n  mouseState: { x: number; y: number },\n  event: PointerEvent,\n  targetNode: HTMLElement | SVGElement\n) => {\n  const cleanupTasks: Array<() => void> = [];\n  const disposeOnCleanup = (fn: () => () => void) => cleanupTasks.push(fn());\n\n  const { pointerId } = event;\n\n  // Fixes an issue where setPointerCapture disrupts the input cursor if the input has a selection.\n  // To reproduce: click into the input and observe that everything is selected.\n  // Then click again and notice the cursor is not placed correctly.\n  window.getSelection()?.removeAllRanges();\n\n  disposeOnCleanup(() => {\n    targetNode.setPointerCapture(pointerId);\n    return () => {\n      if (targetNode.hasPointerCapture(pointerId)) {\n        targetNode.releasePointerCapture(pointerId);\n      }\n    };\n  });\n\n  let isDisposed = false;\n  disposeOnCleanup(() => () => {\n    isDisposed = true;\n  });\n\n  const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);\n\n  // Safari supports pointer lock well, but the issue lies with the pointer lock banner.\n  // It shifts the entire page down, which creates a poor user experience.\n  if (!isSafari) {\n    disposeOnCleanup(() => {\n      const timerId = window.setTimeout(() => {\n        state.pointerCaptureRequested = true;\n        requestPointerLockSafe(targetNode)\n          .then(() => {\n            state.pointerCaptureRequested = false;\n\n            if (isDisposed) {\n              if (targetNode.ownerDocument.pointerLockElement === targetNode) {\n                targetNode.ownerDocument.exitPointerLock();\n              }\n              return;\n            }\n\n            const cursorNode =\n              (targetNode.ownerDocument.querySelector(\n                \"#numeric-guesture-control-cursor\"\n              ) as SVGElement) ||\n              (new DOMParser().parseFromString(\n                `\n              <svg id=\"numeric-guesture-control-cursor\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" width=\"46\" height=\"15\">\n               <g transform=\"translate(2 3)\">\n                 <path d=\"M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z\" fill=\"#111\" fill-rule=\"evenodd\" stroke=\"#FFF\" stroke-width=\"2\"></path>\n                  <path d=\"M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L 31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z\" fill=\"#111\" fill-rule=\"evenodd\"></path>\n                </g>\n              </svg>`,\n                \"application/xml\"\n              ).documentElement as Element as SVGElement);\n\n            cursorNode.style.filter = `drop-shadow(${\n              state.direction === \"horizontal\" ? \"0 1px\" : \"1px 0\"\n            } 1.1px rgba(0,0,0,.4))`;\n            cursorNode.style.position = \"fixed\";\n            cursorNode.style.zIndex = Number.MAX_SAFE_INTEGER.toString();\n\n            cursorNode.style.left = `${mouseState.x}px`;\n            cursorNode.style.top = `${mouseState.y}px`;\n\n            cursorNode.style.transform = `translate(-50%, -50%) ${\n              state.direction === \"horizontal\"\n                ? \"rotate(0deg)\"\n                : \"rotate(90deg)\"\n            }`;\n            state.cursor = cursorNode;\n            if (state.cursor) {\n              targetNode.ownerDocument.documentElement.appendChild(\n                state.cursor\n              );\n            }\n          })\n          .catch((error) => {\n            state.pointerCaptureRequested = false;\n            console.error(\"requestPointerLock\", error);\n          });\n      }, scrubTimeout);\n\n      return () => {\n        state.pointerCaptureRequested = false;\n\n        if (state.cursor) {\n          state.cursor.remove();\n          state.cursor = undefined;\n        }\n\n        if (targetNode.ownerDocument.pointerLockElement === targetNode) {\n          targetNode.ownerDocument.exitPointerLock();\n        }\n\n        clearTimeout(timerId);\n      };\n    });\n  }\n\n  return () => {\n    for (const task of [...cleanupTasks.reverse()]) {\n      task();\n    }\n  };\n};\n\n// When the value is outside of the range make it come back to the range from the other side\n//   |        | . -> | .      |\n// . |        |   -> |      . |\nconst wrapAround = (value: number, min: number, max: number) => {\n  const range = max - min;\n\n  while (value < min) {\n    value += range;\n  }\n\n  while (value > max) {\n    value -= range;\n  }\n\n  return value;\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/numeric-input-arrow-keys.ts",
    "content": "// We increment by 10 when shift is pressed, by 0.1 when alt/option is pressed and by 1 by default.\nexport const handleNumericInputArrowKeys = (\n  value: number,\n  { altKey, shiftKey, key }: { altKey: boolean; shiftKey: boolean; key: string }\n) => {\n  if (key !== \"ArrowUp\" && key !== \"ArrowDown\") {\n    return value;\n  }\n  const delta = shiftKey ? 10 : altKey ? 0.1 : 1;\n  const multiplier = key === \"ArrowUp\" ? 1 : -1;\n  return Number((value + delta * multiplier).toFixed(1));\n};\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/small-button.stories.tsx",
    "content": "import { Grid } from \"../grid\";\nimport { Flex } from \"../flex\";\nimport { Text } from \"../text\";\nimport { XSmallIcon, TrashIcon, PlusIcon } from \"@webstudio-is/icons\";\nimport {\n  SmallButton as SmallButtonComponent,\n  smallButtonVariants,\n} from \"./small-button\";\nimport { StorySection } from \"../storybook\";\n\nexport default {\n  title: \"Primitives/Small Button\",\n  component: SmallButtonComponent,\n};\n\nconst icons = [\n  { icon: <XSmallIcon />, label: \"XSmallIcon\" },\n  { icon: <TrashIcon />, label: \"TrashIcon\" },\n  { icon: <PlusIcon />, label: \"PlusIcon\" },\n];\n\nexport const SmallButton = () => (\n  <>\n    <StorySection title=\"Variants\">\n      <Grid columns={4} gap=\"3\" align=\"center\" css={{ width: 400 }}>\n        <Text variant=\"labels\">Variant</Text>\n        {icons.map(({ label }) => (\n          <Text key={label} variant=\"labels\">\n            {label}\n          </Text>\n        ))}\n\n        {smallButtonVariants.map((variant) => (\n          <>\n            <Text variant=\"labels\">{variant}</Text>\n            {icons.map(({ icon, label }) => (\n              <Flex key={`${variant}-${label}`} gap=\"2\" align=\"center\">\n                <SmallButtonComponent variant={variant}>\n                  {icon}\n                </SmallButtonComponent>\n                <SmallButtonComponent variant={variant} disabled>\n                  {icon}\n                </SmallButtonComponent>\n              </Flex>\n            ))}\n          </>\n        ))}\n      </Grid>\n    </StorySection>\n\n    <StorySection title=\"Bleed\">\n      <Grid columns={2} gap=\"3\" align=\"center\" css={{ width: 200 }}>\n        <Text variant=\"labels\">Bleed (default)</Text>\n        <Text variant=\"labels\">No bleed</Text>\n        {icons.map(({ icon, label }) => (\n          <>\n            <Flex key={`bleed-${label}`} align=\"center\">\n              <SmallButtonComponent>{icon}</SmallButtonComponent>\n            </Flex>\n            <Flex key={`no-bleed-${label}`} align=\"center\">\n              <SmallButtonComponent bleed={false}>{icon}</SmallButtonComponent>\n            </Flex>\n          </>\n        ))}\n      </Grid>\n    </StorySection>\n\n    <StorySection title=\"States\">\n      <Grid columns={3} gap=\"3\" align=\"center\" css={{ width: 300 }}>\n        <Text variant=\"labels\">Default</Text>\n        <Text variant=\"labels\">Open state</Text>\n        <Text variant=\"labels\">Focused</Text>\n        {smallButtonVariants.map((variant) => (\n          <>\n            <Flex key={`default-${variant}`} align=\"center\">\n              <SmallButtonComponent variant={variant}>\n                <PlusIcon />\n              </SmallButtonComponent>\n            </Flex>\n            <Flex key={`open-${variant}`} align=\"center\">\n              <SmallButtonComponent variant={variant} data-state=\"open\">\n                <PlusIcon />\n              </SmallButtonComponent>\n            </Flex>\n            <Flex key={`focused-${variant}`} align=\"center\">\n              <SmallButtonComponent variant={variant} data-focused={true}>\n                <PlusIcon />\n              </SmallButtonComponent>\n            </Flex>\n          </>\n        ))}\n      </Grid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/small-button.tsx",
    "content": "import {\n  forwardRef,\n  type Ref,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { css, theme, type CSS } from \"../../stitches.config\";\n\nexport const smallButtonVariants = [\n  \"normal\",\n  \"contrast\",\n  \"destructive\",\n] as const;\n\n/**\n * data-state from Radix, might be set when <SmallButton> is asChild\n * https://www.radix-ui.com/docs/primitives/components/popover#trigger\n **/\nconst smallButtonStates = [\"open\"] as const;\n\nconst defaultColors = {\n  normal: theme.colors.foregroundSubtle,\n  destructive: theme.colors.foregroundSubtle,\n  contrast: theme.colors.foregroundContrastMain,\n};\n\nconst hoverColors = {\n  normal: theme.colors.foregroundMain,\n  destructive: theme.colors.foregroundDestructive,\n  contrast: theme.colors.foregroundContrastMain,\n};\n\nconst focusColors = {\n  normal: theme.colors.borderFocus,\n  destructive: theme.colors.borderFocus,\n  contrast: theme.colors.borderContrast,\n};\n\nconst perVariantStyle = (variant: (typeof smallButtonVariants)[number]) => ({\n  color: defaultColors[variant],\n\n  \"&:hover, &[data-state=open]\": {\n    color: hoverColors[variant],\n\n    \"&:disabled, &[data-disabled]\": {\n      color: theme.colors.foregroundDisabled,\n    },\n  },\n  \"&[data-focused=true], &:focus-visible\": {\n    borderRadius: theme.borderRadius[3],\n    outline: `1px solid ${focusColors[variant]}`,\n    \"&:disabled, &[data-disabled]\": {\n      outline: \"none\",\n    },\n  },\n});\n\nconst style = css({\n  all: \"unset\",\n  width: theme.spacing[9],\n  height: theme.spacing[9],\n  position: \"relative\",\n  \"&:disabled, &[data-disabled]\": {\n    color: theme.colors.foregroundDisabled,\n  },\n  variants: {\n    variant: {\n      normal: perVariantStyle(\"normal\"),\n      contrast: perVariantStyle(\"contrast\"),\n      destructive: perVariantStyle(\"destructive\"),\n    },\n    bleed: {\n      true: {\n        // We want to bleed outside of the 16px icon size because its too small\n        \"&::after\": {\n          content: '\"\"',\n          position: \"absolute\",\n          inset: `-${theme.spacing[4]}`,\n        },\n      },\n    },\n  },\n  defaultVariants: {\n    variant: \"normal\",\n    bleed: true,\n  },\n});\n\ntype Props = {\n  children: ReactNode;\n  variant?: (typeof smallButtonVariants)[number];\n  bleed?: boolean;\n  \"data-state\"?: (typeof smallButtonStates)[number];\n  \"data-focused\"?: boolean;\n  css?: CSS;\n} & Omit<ComponentProps<\"button\">, \"children\">;\n\nexport const SmallButton = forwardRef(\n  (\n    { variant, children, css, className, bleed, ...restProps }: Props,\n    ref: Ref<HTMLButtonElement>\n  ) => {\n    return (\n      <button\n        {...restProps}\n        className={style({ css, className, variant, bleed })}\n        ref={ref}\n      >\n        {children}\n      </button>\n    );\n  }\n);\nSmallButton.displayName = \"SmallButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/primitives/use-scrub.ts",
    "content": "import { useEffect, useRef } from \"react\";\nimport {\n  numericScrubControl,\n  type NumericScrubValue,\n} from \"./numeric-gesture-control\";\nimport { unstable_batchedUpdates as unstableBatchedUpdates } from \"react-dom\";\n\nexport const useScrub = ({\n  value,\n  distanceThreshold,\n  onChange,\n  onChangeComplete,\n  shouldHandleEvent,\n}: {\n  value: NumericScrubValue;\n  distanceThreshold?: number;\n  onChange: (value: NumericScrubValue) => void;\n  onChangeComplete?: (value: NumericScrubValue) => void;\n  shouldHandleEvent?: (node: EventTarget) => boolean;\n}) => {\n  const scrubRef = useRef<HTMLDivElement | null>(null);\n  const inputRef = useRef<HTMLInputElement | null>(null);\n\n  const onChangeRef = useRef(onChange);\n  onChangeRef.current = onChange;\n\n  const onChangeCompleteRef = useRef(onChangeComplete);\n  onChangeCompleteRef.current = onChangeComplete;\n\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  const shouldHandleEventRef = useRef(shouldHandleEvent);\n  shouldHandleEventRef.current = shouldHandleEvent;\n\n  // Since scrub is going to call onChange and onChangeComplete callbacks, it will result in a new value and potentially new callback refs.\n  // We need this effect to ONLY run when type or unit changes, but not when callbacks or value.value changes.\n  useEffect(() => {\n    const inputRefCurrent = inputRef.current;\n    const scrubRefCurrent = scrubRef.current;\n    if (inputRefCurrent === null || scrubRefCurrent === null) {\n      return;\n    }\n\n    return numericScrubControl(scrubRefCurrent, {\n      distanceThreshold,\n      getInitialValue: () => {\n        return valueRef.current;\n      },\n      onValueInput(event) {\n        // Moving focus to container of the input to hide the caret\n        // (it makes text harder to read and may jump around as you scrub)\n        scrubRef.current?.setAttribute(\"tabindex\", \"-1\");\n        scrubRef.current?.focus();\n\n        onChangeRef.current(event.value);\n      },\n      onValueChange(event) {\n        // Will work without but depends on order of setState updates\n        // at text-control, now fixed in both places (order of updates is right, and batched here)\n        unstableBatchedUpdates(() => {\n          onChangeCompleteRef.current?.(event.value);\n        });\n\n        // Returning focus that we've moved above\n        scrubRef.current?.removeAttribute(\"tabindex\");\n        inputRef.current?.focus();\n        inputRef.current?.select();\n      },\n      shouldHandleEvent: shouldHandleEventRef.current,\n    });\n  }, [distanceThreshold]);\n\n  return { scrubRef, inputRef };\n};\n"
  },
  {
    "path": "packages/design-system/src/components/pro-badge.stories.tsx",
    "content": "import { ProBadge as ProBadgeComponent } from \"./pro-badge\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Pro Badge\",\n  component: ProBadgeComponent,\n};\n\nexport const ProBadge = () => (\n  <>\n    <StorySection title=\"Labels\">\n      <StoryGrid horizontal>\n        <ProBadgeComponent>Pro</ProBadgeComponent>\n        <ProBadgeComponent>Enterprise</ProBadgeComponent>\n        <ProBadgeComponent>Upgrade</ProBadgeComponent>\n      </StoryGrid>\n    </StorySection>\n    <StorySection title=\"Truncated\">\n      <StoryGrid css={{ width: 80 }}>\n        <ProBadgeComponent>Very long enterprise plan name</ProBadgeComponent>\n        <ProBadgeComponent>Short</ProBadgeComponent>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/pro-badge.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { css, theme, type CSS } from \"../stitches.config\";\nimport { Text } from \"./text\";\n\nconst style = css({\n  display: \"inline-flex\",\n  borderRadius: theme.borderRadius[3],\n  px: theme.spacing[3],\n  py: theme.spacing[1],\n  height: theme.spacing[9],\n  color: theme.colors.foregroundContrastMain,\n  alignItems: \"center\",\n  maxWidth: \"100%\",\n  whiteSpace: \"nowrap\",\n  overflow: \"hidden\",\n  // @todo doesn't work in tooltips, needs a workaround\n  textOverflow: \"ellipsis\",\n  background: theme.colors.foregroundTextSubtle,\n});\n\nexport const ProBadge = ({\n  css,\n  children,\n}: {\n  children: ReactNode;\n  css?: CSS;\n}) => {\n  return <Text className={style({ css })}>{children}</Text>;\n};\n"
  },
  {
    "path": "packages/design-system/src/components/progress.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { Flex } from \"./flex\";\nimport { Button } from \"./button\";\nimport { Progress as ProgressComponent } from \"./progress\";\nimport { Text } from \"./text\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Progress\",\n  component: ProgressComponent,\n};\n\nexport const Progress = () => {\n  const [value, setValue] = useState(50);\n  const [transitionValue, setTransitionValue] = useState(0);\n  return (\n    <>\n      <StorySection title=\"Values\">\n        <StoryGrid css={{ width: 300 }}>\n          {[0, 25, 50, 75, 100].map((v) => (\n            <Flex key={v} gap=\"2\" align=\"center\">\n              <Text css={{ width: 40 }}>{v}%</Text>\n              <ProgressComponent value={v} />\n            </Flex>\n          ))}\n          <Flex gap=\"2\" align=\"center\">\n            <ProgressComponent value={value} />\n            <Button onClick={() => setValue(Math.max(0, value - 10))}>\n              -10\n            </Button>\n            <Button onClick={() => setValue(Math.min(100, value + 10))}>\n              +10\n            </Button>\n          </Flex>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Custom transition\">\n        <StoryGrid css={{ width: 300 }}>\n          <Text>Slow transition (1s)</Text>\n          <ProgressComponent\n            value={transitionValue}\n            transitionDuration=\"1000ms\"\n          />\n          <Flex gap=\"2\">\n            <Button onClick={() => setTransitionValue(0)}>0%</Button>\n            <Button onClick={() => setTransitionValue(50)}>50%</Button>\n            <Button onClick={() => setTransitionValue(100)}>100%</Button>\n          </Flex>\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/progress.tsx",
    "content": "import { Root, Indicator } from \"@radix-ui/react-progress\";\nimport { css, theme } from \"../stitches.config\";\nimport type { TransitionEventHandler } from \"react\";\n\nconst rootStyle = css({\n  width: 200,\n  height: 2,\n  overflow: \"hidden\",\n  borderRadius: 9999,\n  background: \"#fff\",\n  boxShadow: \"0 0 32px #4a4efa80\",\n});\n\nconst indicatorStyle = css({\n  width: \"100%\",\n  height: \"100%\",\n  background: theme.colors.brandBorderNavbar,\n  transitionDuration: \"200ms\",\n  transitionProperty: \"transform\",\n});\n\nexport const Progress = ({\n  value,\n  transitionDuration,\n  onTransitionEnd,\n}: {\n  value: number;\n  transitionDuration?: string;\n  onTransitionEnd?: TransitionEventHandler;\n}) => {\n  return (\n    <Root value={value} className={rootStyle()}>\n      <Indicator\n        className={indicatorStyle()}\n        style={{\n          transform: `translateX(-${100 - value}%)`,\n          transitionDuration,\n        }}\n        onTransitionEnd={onTransitionEnd}\n      />\n    </Root>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/radio.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { Label } from \"./label\";\nimport { RadioGroup, Radio as RadioComponent, RadioAndLabel } from \"./radio\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Radio\",\n  parameters: {\n    // to make the white background in the control visible\n    backgrounds: { default: \"Panel\" },\n  },\n};\n\nexport const Radio = () => {\n  const [value, setValue] = useState(\"A\");\n  return (\n    <>\n      <StorySection title=\"Enabled\">\n        <RadioGroup defaultValue=\"A\">\n          <StoryGrid horizontal>\n            <RadioComponent value=\"A\" />\n            <RadioComponent value=\"B\" />\n          </StoryGrid>\n        </RadioGroup>\n      </StorySection>\n\n      <StorySection title=\"Disabled\">\n        {/* @todo: We probably need to update Radix to make `disabled` work on Root.\n                   Because when items are disabled the group as whole is still focusable. */}\n        <RadioGroup defaultValue=\"A\" /* disabled */>\n          <StoryGrid horizontal>\n            <RadioComponent value=\"A\" disabled />\n            <RadioComponent value=\"B\" disabled />\n          </StoryGrid>\n        </RadioGroup>\n      </StorySection>\n\n      <StorySection title=\"Focussed (initially)\">\n        <RadioGroup defaultValue=\"A\">\n          <StoryGrid horizontal>\n            <RadioComponent value=\"A\" autoFocus />\n          </StoryGrid>\n        </RadioGroup>\n      </StorySection>\n\n      <StorySection title=\"With lables\">\n        <RadioGroup defaultValue=\"A\">\n          <RadioAndLabel>\n            <RadioComponent value=\"A\" id=\"A\" />\n            <Label htmlFor=\"A\">Label A</Label>\n          </RadioAndLabel>\n          <RadioAndLabel>\n            <RadioComponent value=\"B\" id=\"B\" />\n            <Label htmlFor=\"B\">Label B</Label>\n          </RadioAndLabel>\n        </RadioGroup>\n      </StorySection>\n\n      <StorySection title=\"Controlled\">\n        <RadioGroup value={value} onValueChange={setValue}>\n          <RadioAndLabel>\n            <RadioComponent value=\"A\" id=\"ctrl-A\" />\n            <Label htmlFor=\"ctrl-A\">Option A</Label>\n          </RadioAndLabel>\n          <RadioAndLabel>\n            <RadioComponent value=\"B\" id=\"ctrl-B\" />\n            <Label htmlFor=\"ctrl-B\">Option B</Label>\n          </RadioAndLabel>\n          <RadioAndLabel>\n            <RadioComponent value=\"C\" id=\"ctrl-C\" />\n            <Label htmlFor=\"ctrl-C\">Option C</Label>\n          </RadioAndLabel>\n        </RadioGroup>\n        <Label>Selected: {value}</Label>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/radio.tsx",
    "content": "/**\n * Implementation of the \"Radio\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=1794%3A5804\n */\n\nimport { forwardRef, type ComponentProps, type Ref } from \"react\";\nimport { RadioUncheckedIcon, RadioCheckedIcon } from \"@webstudio-is/icons\";\nimport * as Primitive from \"@radix-ui/react-radio-group\";\nimport { type CSS, css, theme } from \"../stitches.config\";\n\nexport { CheckboxAndLabel as RadioAndLabel } from \"./checkbox\";\n\nconst itemStyle = css({\n  all: \"unset\", // reset <button>\n  width: theme.spacing[9],\n  height: theme.spacing[9],\n  display: \"block\",\n  position: \"relative\",\n  borderRadius: theme.borderRadius.round,\n  color: theme.colors.foregroundMain,\n\n  \"&:focus-visible\": {\n    outline: `2px solid ${theme.colors.borderFocus}`,\n  },\n\n  \"&[data-state=checked]\": {\n    color: theme.colors.foregroundPrimary,\n  },\n\n  // [data-state] is needed to make selector specificity higher\n  \"&[data-state]:disabled\": {\n    color: theme.colors.foregroundDisabled,\n  },\n\n  \"&:not(:disabled)::before\": {\n    content: \"''\",\n    display: \"block\",\n    position: \"absolute\",\n    width: theme.spacing[7],\n    height: theme.spacing[7],\n    top: theme.spacing[2],\n    left: theme.spacing[2],\n    borderRadius: theme.borderRadius.round,\n    background: theme.colors.backgroundControls,\n  },\n});\n\nconst iconStyle = css({ position: \"relative\" });\n\n// We need this component basicslly just to get access to \"data-state\".\n// We could render both icons and hide one using CSS,\n// but that probably will be less performant.\nconst Button = forwardRef(\n  (\n    props: ComponentProps<\"button\"> & {\n      \"data-state\"?: \"checked\" | \"unchecked\";\n    },\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <button {...props} ref={ref}>\n      {props[\"data-state\"] === \"checked\" ? (\n        <RadioCheckedIcon className={iconStyle()} />\n      ) : (\n        <RadioUncheckedIcon className={iconStyle()} />\n      )}\n    </button>\n  )\n);\nButton.displayName = \"Button\";\n\nexport const Radio = forwardRef(\n  (\n    {\n      className,\n      css,\n      ...props\n    }: ComponentProps<typeof Primitive.Item> & { css?: CSS },\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <Primitive.Item\n      className={itemStyle({ className, css })}\n      {...props}\n      ref={ref}\n      asChild\n    >\n      <Button />\n    </Primitive.Item>\n  )\n);\nRadio.displayName = \"Radio\";\n\nexport const RadioGroup = Primitive.Root;\n"
  },
  {
    "path": "packages/design-system/src/components/scroll-area.stories.tsx",
    "content": "import { StorySection } from \"./storybook\";\nimport { ScrollArea as ScrollAreaComponent } from \"./scroll-area\";\nimport { Text } from \"./text\";\nimport { theme } from \"..\";\nimport { Flex } from \"./flex\";\n\nexport default {\n  title: \"Scroll Area\",\n};\n\nexport const ScrollArea = () => {\n  const content = (\n    <div style={{ height: 1000, width: 1000 }}>\n      {Array.from(new Array(100))\n        .map(() => {\n          return \"a\".repeat(300);\n        })\n        .join(\"\\n\")}\n    </div>\n  );\n  const css = {\n    height: 100,\n    width: 100,\n    background: theme.colors.backgroundPanel,\n  };\n  return (\n    <>\n      <StorySection title=\"Vertical\">\n        <ScrollAreaComponent css={css}>{content}</ScrollAreaComponent>\n      </StorySection>\n      <StorySection title=\"Horizontal\">\n        <ScrollAreaComponent css={css} direction=\"horizontal\">\n          {content}\n        </ScrollAreaComponent>\n      </StorySection>\n      <StorySection title=\"Both\">\n        <ScrollAreaComponent css={css} direction=\"both\">\n          {content}\n        </ScrollAreaComponent>\n      </StorySection>\n\n      <StorySection title=\"No overflow\">\n        <div style={{ height: 200, width: 200 }}>\n          <ScrollAreaComponent>\n            <Flex direction=\"column\" gap=\"2\" style={{ padding: 8 }}>\n              <Text>Short content</Text>\n              <Text>No scrollbar needed</Text>\n            </Flex>\n          </ScrollAreaComponent>\n        </div>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/scroll-area.tsx",
    "content": "import { forwardRef, type ComponentProps, type Ref } from \"react\";\nimport { styled, theme, css, type CSS } from \"../stitches.config\";\nimport { Root, Viewport, Scrollbar, Thumb } from \"@radix-ui/react-scroll-area\";\n\nconst ScrollAreaRoot = styled(Root, {\n  boxSizing: \"border-box\",\n  overflow: \"hidden\",\n  display: \"grid\",\n  // We had a case where some Windows 10 + Chrome 129 users couldn't scroll style panel.\n  willChange: \"transform\",\n});\n\nconst ScrollAreaThumb = styled(Thumb, {\n  position: \"relative\",\n  boxSizing: \"border-box\",\n  background: theme.colors.foregroundScrollBar,\n  borderRadius: theme.spacing[4],\n});\n\nconst ScrollAreaScrollbar = styled(Scrollbar, {\n  boxSizing: \"border-box\",\n  // ensures no selection\n  userSelect: \"none\",\n  // disable browser handling of all panning and scaleUp gestures on touch devices\n  padding: 2,\n  touchAction: \"none\",\n  '&[data-orientation=\"vertical\"]': {\n    width: theme.spacing[6],\n  },\n  '&[data-orientation=\"horizontal\"]': {\n    flexDirection: \"column\",\n    height: theme.spacing[6],\n    \"--radix-scroll-area-thumb-height\": theme.spacing[4],\n  },\n\n  variants: {\n    direction: {\n      both: {\n        \"&[data-orientation=vertical]\": {\n          marginBottom: theme.spacing[4],\n        },\n        '&[data-orientation=\"horizontal\"]': {\n          marginRight: theme.spacing[4],\n        },\n      },\n      horizontal: {},\n      vertical: {},\n    },\n  },\n});\n\nconst viewPortStyle = css({\n  boxSizing: \"border-box\",\n  width: \"100%\",\n  height: \"100%\",\n  borderRadius: \"inherit\",\n\n  variants: {\n    direction: {\n      // https://github.com/radix-ui/primitives/issues/926#issuecomment-1015279283\n      vertical: { \"& > div[style]\": { display: \"block !important\" } },\n      horizontal: {},\n      both: {},\n    },\n  },\n});\n\ntype ScrollAreaProps = {\n  css?: CSS;\n  direction?: \"vertical\" | \"horizontal\" | \"both\";\n  className?: string;\n} & Pick<ComponentProps<\"div\">, \"onScroll\" | \"children\">;\n\nexport const ScrollArea = forwardRef(\n  (\n    {\n      children,\n      css,\n      className,\n      onScroll,\n      direction = \"vertical\",\n    }: ScrollAreaProps,\n    ref: Ref<HTMLDivElement>\n  ) => {\n    return (\n      <ScrollAreaRoot scrollHideDelay={0} css={css} className={className}>\n        <Viewport\n          ref={ref}\n          className={viewPortStyle({ direction })}\n          onScroll={onScroll}\n        >\n          {children}\n        </Viewport>\n        {(direction === \"vertical\" || direction === \"both\") && (\n          <ScrollAreaScrollbar orientation=\"vertical\" direction={direction}>\n            <ScrollAreaThumb />\n          </ScrollAreaScrollbar>\n        )}\n        {(direction === \"horizontal\" || direction === \"both\") && (\n          <ScrollAreaScrollbar orientation=\"horizontal\" direction={direction}>\n            <ScrollAreaThumb />\n          </ScrollAreaScrollbar>\n        )}\n      </ScrollAreaRoot>\n    );\n  }\n);\nScrollArea.displayName = \"ScrollArea\";\n"
  },
  {
    "path": "packages/design-system/src/components/search-field.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\nimport { SearchField as SearchFieldComponent } from \"./search-field\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Search Field\",\n  component: SearchFieldComponent,\n};\n\nexport const SearchField = () => {\n  const [log, setLog] = useState<string[]>([]);\n  const addLog = (msg: string) => setLog((prev) => [...prev.slice(-4), msg]);\n\n  return (\n    <>\n      <StorySection title=\"States\">\n        <Flex direction=\"column\" gap=\"3\" css={{ width: 240 }}>\n          <Text variant=\"labels\">Empty</Text>\n          <SearchFieldComponent placeholder=\"Search…\" />\n          <Text variant=\"labels\">With value</Text>\n          <SearchFieldComponent value=\"somevalue\" title=\"Search\" />\n          <Text variant=\"labels\">Auto focus</Text>\n          <SearchFieldComponent autoFocus placeholder=\"Focused on mount\" />\n          <Text variant=\"labels\">Disabled</Text>\n          <SearchFieldComponent disabled placeholder=\"Disabled\" />\n        </Flex>\n      </StorySection>\n\n      <StorySection title=\"With abort callback\">\n        <Flex direction=\"column\" gap=\"3\" css={{ width: 240 }}>\n          <Text variant=\"labels\">Type and click X or press Escape</Text>\n          <SearchFieldComponent\n            placeholder=\"Type and clear…\"\n            onAbort={() => addLog(\"onAbort fired\")}\n          />\n          {log.map((entry, i) => (\n            <Text key={i} variant=\"mono\">\n              {entry}\n            </Text>\n          ))}\n        </Flex>\n      </StorySection>\n\n      <StorySection title=\"Error color\">\n        <Flex direction=\"column\" gap=\"3\" css={{ width: 240 }}>\n          <SearchFieldComponent color=\"error\" placeholder=\"Error state\" />\n        </Flex>\n      </StorySection>\n\n      <StorySection title=\"Sizes\">\n        <Flex direction=\"column\" gap=\"3\" css={{ width: 240 }}>\n          <Text variant=\"labels\">Size 1</Text>\n          <SearchFieldComponent size=\"1\" placeholder=\"Small search\" />\n          <Text variant=\"labels\">Size 2 (default)</Text>\n          <SearchFieldComponent size=\"2\" placeholder=\"Default search\" />\n        </Flex>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/search-field.tsx",
    "content": "import {\n  type ComponentProps,\n  type ForwardRefRenderFunction,\n  forwardRef,\n  useRef,\n  useState,\n  useEffect,\n  type ChangeEventHandler,\n  type KeyboardEventHandler,\n  type FormEventHandler,\n} from \"react\";\nimport { XCircledFilledIcon, SearchIcon } from \"@webstudio-is/icons\";\nimport { styled } from \"../stitches.config\";\nimport { theme } from \"../stitches.config\";\nimport { InputField } from \"./input-field\";\nimport { SmallIconButton } from \"./small-icon-button\";\nimport { Flex } from \"./flex\";\nimport { mergeRefs } from \"@react-aria/utils\";\n\nconst SearchIconStyled = styled(SearchIcon, {\n  // need to center icon vertically\n  display: \"block\",\n  color: theme.colors.foregroundSubtle,\n  padding: theme.spacing[3],\n});\n\nconst AbortButton = styled(SmallIconButton, {\n  variants: {\n    hidden: {\n      false: { visibility: \"hidden\" },\n      true: {},\n    },\n  },\n});\n\nconst SearchFieldBase: ForwardRefRenderFunction<\n  HTMLInputElement,\n  ComponentProps<typeof InputField> & { onAbort?: () => void }\n> = (props, ref) => {\n  const {\n    onChange,\n    onAbort,\n    value: propsValue = \"\",\n    onKeyDown,\n    ...rest\n  } = props;\n  const [value, setValue] = useState(String(propsValue));\n  const inputRef = useRef<HTMLInputElement>(null);\n  useEffect(() => {\n    setValue(String(propsValue));\n  }, [propsValue]);\n  const handleCancel = () => {\n    setValue(\"\");\n    onAbort?.();\n  };\n  return (\n    <InputField\n      {...rest}\n      ref={ref}\n      // search field implements own reset button\n      // type=search does not work here because\n      // brings native reset button\n      type=\"text\"\n      value={value}\n      inputRef={mergeRefs(inputRef, rest.inputRef)}\n      prefix={<SearchIconStyled />}\n      suffix={\n        <Flex align=\"center\" css={{ padding: theme.spacing[2] }}>\n          <AbortButton\n            hidden={value.length > 0 ? \"true\" : \"false\"}\n            aria-label=\"Reset search\"\n            title=\"Reset search\"\n            tabIndex={-1}\n            onClick={() => {\n              handleCancel();\n            }}\n            icon={<XCircledFilledIcon />}\n          />\n        </Flex>\n      }\n      onChange={(event) => {\n        setValue(event.target.value);\n        onChange?.(event);\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\" && value.length !== 0) {\n          event.preventDefault();\n          // Required to prevent first click closing the dialog.\n          event.stopPropagation();\n          handleCancel();\n        }\n        onKeyDown?.(event);\n      }}\n    />\n  );\n};\n\nexport const SearchField = forwardRef(SearchFieldBase);\n\ntype UseSearchFieldKeys = {\n  onMove: (event: { direction: \"next\" | \"previous\" | \"current\" }) => void;\n  onChange?: FormEventHandler<HTMLInputElement>;\n  onAbort?: () => void;\n};\n\nexport const useSearchFieldKeys = ({\n  onMove,\n  onChange,\n  onAbort,\n}: UseSearchFieldKeys) => {\n  const [search, setSearch] = useState(\"\");\n  const handleKeyDown: KeyboardEventHandler = ({ code }) => {\n    const keyMap = {\n      ArrowUp: \"previous\",\n      ArrowDown: \"next\",\n      Enter: \"current\",\n    } as const;\n    const direction = keyMap[code as keyof typeof keyMap];\n    if (direction !== undefined) {\n      onMove({ direction });\n    }\n  };\n\n  const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {\n    const { value } = event.currentTarget;\n    setSearch(value.trim());\n    onChange?.(event);\n  };\n\n  const handleCancel = () => {\n    setSearch(\"\");\n    onAbort?.();\n  };\n\n  return {\n    value: search,\n    onAbort: handleCancel,\n    onChange: handleChange,\n    onKeyDown: handleKeyDown,\n  };\n};\n"
  },
  {
    "path": "packages/design-system/src/components/section-title.stories.tsx",
    "content": "import {\n  SectionTitle as SectionTitleComponent,\n  SectionTitleLabel,\n  SectionTitleButton,\n} from \"./section-title\";\nimport { StoryGrid, StorySection } from \"./storybook\";\nimport type { ComponentProps } from \"react\";\nimport { PlusIcon } from \"@webstudio-is/icons\";\n\nexport default {\n  title: \"Section Title\",\n};\n\nconst Wrap = ({ children }: { children: React.ReactNode }) => (\n  <div style={{ width: 240, border: \"dashed 3px #e3e3e3\" }}>{children}</div>\n);\n\nconst Variants = ({\n  state,\n  inactive,\n}: {\n  state: ComponentProps<typeof SectionTitleComponent>[\"data-state\"];\n  inactive?: ComponentProps<typeof SectionTitleComponent>[\"inactive\"];\n}) => (\n  <>\n    <Wrap>\n      <SectionTitleComponent data-state={state} inactive={inactive}>\n        <SectionTitleLabel>Simplest</SectionTitleLabel>\n      </SectionTitleComponent>\n    </Wrap>\n    <Wrap>\n      <SectionTitleComponent\n        suffix={<SectionTitleButton prefix={<PlusIcon />} />}\n        data-state={state}\n        inactive={inactive}\n      >\n        <SectionTitleLabel>With button</SectionTitleLabel>\n      </SectionTitleComponent>\n    </Wrap>\n    <Wrap>\n      <SectionTitleComponent\n        dots={[\"local\", \"remote\"]}\n        suffix={<SectionTitleButton prefix={<PlusIcon />} />}\n        data-state={state}\n        inactive={inactive}\n      >\n        <SectionTitleLabel>With dots</SectionTitleLabel>\n      </SectionTitleComponent>\n    </Wrap>\n    <Wrap>\n      <SectionTitleComponent\n        dots={[\"local\"]}\n        suffix={<SectionTitleButton prefix={<PlusIcon />} />}\n        data-state={state}\n        inactive={inactive}\n      >\n        <SectionTitleLabel color=\"local\">With label</SectionTitleLabel>\n      </SectionTitleComponent>\n    </Wrap>\n    <Wrap>\n      <SectionTitleComponent\n        dots={[\"local\", \"remote\"]}\n        suffix={<SectionTitleButton prefix={<PlusIcon />} />}\n        data-state={state}\n        inactive={inactive}\n      >\n        <SectionTitleLabel>\n          Some title so long that it cannot possibly fit\n        </SectionTitleLabel>\n      </SectionTitleComponent>\n    </Wrap>\n    <Wrap>\n      <SectionTitleComponent data-state={state} inactive={inactive}>\n        <SectionTitleLabel>\n          Some title so long that it cannot possibly fit\n        </SectionTitleLabel>\n      </SectionTitleComponent>\n    </Wrap>\n  </>\n);\n\nexport const SectionTitle = () => (\n  <>\n    <StorySection title=\"Focused (intially)\">\n      <StoryGrid>\n        <Wrap>\n          <SectionTitleComponent\n            dots={[\"local\", \"remote\"]}\n            suffix={<SectionTitleButton prefix={<PlusIcon />} />}\n            autoFocus\n          >\n            <SectionTitleLabel>Title</SectionTitleLabel>\n          </SectionTitleComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Closed\">\n      <StoryGrid>\n        <Variants state=\"closed\" />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Open\">\n      <StoryGrid>\n        <Variants state=\"open\" />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Inactive\">\n      <StoryGrid>\n        <Variants inactive state=\"closed\" />\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Non-collapsible\">\n      <StoryGrid>\n        <Wrap>\n          <SectionTitleComponent collapsible={false}>\n            <SectionTitleLabel>Not collapsible</SectionTitleLabel>\n          </SectionTitleComponent>\n        </Wrap>\n        <Wrap>\n          <SectionTitleComponent\n            collapsible={false}\n            suffix={<SectionTitleButton prefix={<PlusIcon />} />}\n          >\n            <SectionTitleLabel>With button</SectionTitleLabel>\n          </SectionTitleComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Overwritten dot\">\n      <StoryGrid>\n        <Wrap>\n          <SectionTitleComponent dots={[\"overwritten\"]} data-state=\"closed\">\n            <SectionTitleLabel>Overwritten</SectionTitleLabel>\n          </SectionTitleComponent>\n        </Wrap>\n        <Wrap>\n          <SectionTitleComponent\n            dots={[\"local\", \"overwritten\", \"remote\"]}\n            data-state=\"closed\"\n          >\n            <SectionTitleLabel>All dots</SectionTitleLabel>\n          </SectionTitleComponent>\n        </Wrap>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/section-title.tsx",
    "content": "/**\n * Implementation of the \"Section Title\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=2%3A12361\n *\n * Designed to be used with Collapsible.Trigger\n */\n\nimport {\n  forwardRef,\n  type ReactNode,\n  type Ref,\n  type ComponentProps,\n  createContext,\n  useContext,\n} from \"react\";\nimport { ChevronRightIcon } from \"@webstudio-is/icons\";\nimport { theme, css, styled, type CSS } from \"../stitches.config\";\nimport { Button } from \"./button\";\nimport { ArrowFocus } from \"./primitives/arrow-focus\";\nimport { Label, isLabelButton } from \"./label\";\nimport { focusRingStyle } from \"./focus-ring\";\nimport { Flex } from \"./flex\";\n\nconst buttonContentColor = \"--ws-section-title-button-content-color\";\nconst labelTextColor = \"--ws-section-title-label-content-color\";\nconst chevronOpacity = \"--ws-section-title-chevron-display\";\n\nconst StyledButton = styled(Button, {});\n\nconst containerStyle = css({\n  position: \"relative\",\n  height: theme.spacing[14],\n  [buttonContentColor]: theme.colors.foregroundIconMain,\n  [labelTextColor]: theme.colors.foregroundMain,\n  \"&:hover\": {\n    [chevronOpacity]: 1,\n  },\n});\n\nconst titleButtonLayoutStyle = css({\n  all: \"unset\", // reset <button>\n  display: \"flex\",\n  gap: theme.spacing[5],\n  alignItems: \"center\",\n  width: \"100%\",\n  height: \"100%\",\n  boxSizing: \"border-box\",\n  paddingInline: theme.panel.paddingInline,\n});\n\nconst labelContainerStyle = css({\n  position: \"absolute\",\n  inset: 0,\n  pointerEvents: \"none\",\n});\n\nconst titleButtonStyle = css(titleButtonLayoutStyle, {\n  \"&:focus-visible\": focusRingStyle(),\n});\n\nconst suffixSlotStyle = css({\n  position: \"absolute\",\n  right: theme.spacing[4],\n  top: theme.spacing[4],\n});\n\nconst invisibleSuffixStyle = css({\n  visibility: \"hidden\",\n});\n\nconst chevronStyle = css({\n  width: theme.spacing[7],\n  opacity: `var(${chevronOpacity}, 0)`,\n  translate: \"-100%\",\n  transition: \"transform 150ms, opacity 200ms\",\n  color: theme.colors.backgroundIconSubtle,\n  variants: {\n    openState: {\n      open: {\n        rotate: \"90deg\",\n      },\n      closed: {},\n      inactive: {},\n    },\n  },\n});\n\nconst dotStyle = css({\n  width: theme.spacing[4],\n  height: theme.spacing[4],\n  borderRadius: theme.borderRadius.round,\n  marginRight: -2,\n  variants: {\n    color: {\n      local: { backgroundColor: theme.colors.foregroundLocalFlexUi },\n      overwritten: {\n        backgroundColor: theme.colors.foregroundOverwrittenFlexUi,\n      },\n      remote: { backgroundColor: theme.colors.foregroundRemoteFlexUi },\n    },\n  },\n});\n\nconst context = createContext<{\n  openState: \"open\" | \"closed\";\n  inactive: boolean;\n}>({\n  openState: \"closed\",\n  inactive: false,\n});\n\nexport const SectionTitle = forwardRef(\n  (\n    {\n      dots,\n      className,\n      css,\n      children,\n      suffix,\n      inactive = false,\n      collapsible = true,\n      ...props\n    }: ComponentProps<\"button\"> & {\n      inactive?: boolean;\n      collapsible?: boolean;\n      /** https://www.radix-ui.com/docs/primitives/components/collapsible#trigger */\n      \"data-state\"?: \"open\" | \"closed\";\n      dots?: Array<\"local\" | \"overwritten\" | \"remote\">;\n      css?: CSS;\n      /** Primarily for <SectionTitleButton> */\n      suffix?: ReactNode;\n    },\n    ref: Ref<HTMLButtonElement>\n  ) => {\n    const openState = props[\"data-state\"] ?? \"closed\";\n    const finalDots = openState === \"open\" ? [] : (dots ?? []);\n\n    return (\n      <context.Provider value={{ openState, inactive }}>\n        <ArrowFocus\n          render={({ handleKeyDown }) => (\n            <Flex\n              align=\"center\"\n              className={containerStyle({\n                className,\n                css,\n                color: inactive ? \"disabled\" : \"default\",\n              })}\n              data-state={openState}\n              onKeyDown={handleKeyDown}\n            >\n              {collapsible && (\n                <button\n                  className={titleButtonStyle()}\n                  data-state={openState}\n                  ref={ref}\n                  {...props}\n                >\n                  <ChevronRightIcon className={chevronStyle({ openState })} />\n                </button>\n              )}\n              {/*\n                If the label is itself a button, we don't want to nest a button inside another button.\n                Therefore, we render the label in a layer above the SectionTitle button\n              */}\n              <div className={labelContainerStyle()}>\n                <div className={titleButtonLayoutStyle({ openState })}>\n                  {children}\n\n                  {finalDots.length > 0 && (\n                    <Flex shrink={false}>\n                      {finalDots.map((color) => (\n                        <div key={color} className={dotStyle({ color })} />\n                      ))}\n                    </Flex>\n                  )}\n\n                  {suffix && (\n                    /* In case of text overflow we need to place here the same suffix*/\n                    <div className={invisibleSuffixStyle()}>{suffix}</div>\n                  )}\n                </div>\n              </div>\n\n              {suffix && <div className={suffixSlotStyle()}>{suffix}</div>}\n            </Flex>\n          )}\n        />\n      </context.Provider>\n    );\n  }\n);\nSectionTitle.displayName = \"SectionTitle\";\n\nexport const SectionTitleLabel = forwardRef(\n  (\n    {\n      css,\n      children,\n      ...props\n    }: Omit<ComponentProps<typeof Label>, \"truncate\" | \"text\">,\n    ref: Ref<HTMLLabelElement>\n  ) => {\n    const { openState, inactive } = useContext(context);\n\n    const commonCss = { flex: \"0 1 auto\" };\n    const color = inactive\n      ? \"inactive\"\n      : openState === \"closed\"\n        ? \"default\"\n        : props.color;\n\n    const isButton = isLabelButton(color);\n\n    return (\n      <Label\n        truncate\n        text=\"title\"\n        {...props}\n        color={color}\n        css={{\n          color:\n            openState === \"closed\" && inactive === false\n              ? `var(${labelTextColor})`\n              : undefined,\n          ...commonCss,\n          ...css,\n\n          // When we use a SectionTitle button, we can't directly render a label inside it.\n          // Instead, we need to render the label using a div that has position:absolute and pointer-events:none\n          // However, if the label itself is a button, we need to make sure that it remains clickable.\n          // @todo: move this logic to css\n          pointerEvents: isButton ? \"auto\" : \"inherit\",\n        }}\n        ref={ref}\n      >\n        {children}\n      </Label>\n    );\n  }\n);\nSectionTitleLabel.displayName = \"SectionTitleLabel\";\n\nexport const SectionTitleButton = forwardRef(\n  (\n    { css, ...props }: ComponentProps<typeof Button>,\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <StyledButton\n      tabIndex={-1}\n      color=\"ghost\"\n      {...props}\n      css={{ color: `var(${buttonContentColor})`, ...css }}\n      ref={ref}\n    />\n  )\n);\nSectionTitleButton.displayName = \"SectionTitleButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/select-button.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { NestedIconLabel } from \"./nested-icon-label\";\nimport { SelectButton as SelectButtonComponent } from \"./select-button\";\nimport { GapVerticalIcon } from \"@webstudio-is/icons\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Select Button\",\n};\n\nconst iconLabel = (\n  <NestedIconLabel color=\"local\">\n    <GapVerticalIcon />\n  </NestedIconLabel>\n);\n\nconst InteractiveDemo = () => {\n  const [clicked, setClicked] = useState(false);\n  return (\n    <StoryGrid horizontal>\n      <SelectButtonComponent onClick={() => setClicked(!clicked)}>\n        {clicked ? \"Clicked!\" : \"Click me\"}\n      </SelectButtonComponent>\n      <SelectButtonComponent\n        prefix={iconLabel}\n        onClick={() => setClicked(!clicked)}\n      >\n        {clicked ? \"Clicked!\" : \"With prefix\"}\n      </SelectButtonComponent>\n    </StoryGrid>\n  );\n};\n\nexport const SelectButton = () => {\n  return (\n    <>\n      <StorySection title=\"Closed\">\n        <StoryGrid horizontal>\n          <SelectButtonComponent data-placeholder>\n            No value\n          </SelectButtonComponent>\n          <SelectButtonComponent prefix={iconLabel} data-placeholder>\n            No value\n          </SelectButtonComponent>\n          <SelectButtonComponent>With value</SelectButtonComponent>\n          <SelectButtonComponent prefix={iconLabel}>\n            With value\n          </SelectButtonComponent>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Open\">\n        <StoryGrid horizontal>\n          <SelectButtonComponent data-placeholder data-state=\"open\">\n            No value\n          </SelectButtonComponent>\n          <SelectButtonComponent\n            prefix={iconLabel}\n            data-placeholder\n            data-state=\"open\"\n          >\n            No value\n          </SelectButtonComponent>\n          <SelectButtonComponent data-state=\"open\">\n            With value\n          </SelectButtonComponent>\n          <SelectButtonComponent prefix={iconLabel} data-state=\"open\">\n            With value\n          </SelectButtonComponent>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Disabled\">\n        <StoryGrid horizontal>\n          <SelectButtonComponent disabled data-placeholder>\n            No value\n          </SelectButtonComponent>\n          <SelectButtonComponent\n            disabled\n            prefix={\n              <NestedIconLabel color=\"local\" disabled>\n                <GapVerticalIcon />\n              </NestedIconLabel>\n            }\n            data-placeholder\n          >\n            No value\n          </SelectButtonComponent>\n          <SelectButtonComponent disabled>With value</SelectButtonComponent>\n          <SelectButtonComponent\n            disabled\n            prefix={\n              <NestedIconLabel color=\"local\" disabled>\n                <GapVerticalIcon />\n              </NestedIconLabel>\n            }\n          >\n            With value\n          </SelectButtonComponent>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Full width\">\n        <StoryGrid css={{ width: 120 }}>\n          <SelectButtonComponent fullWidth>\n            Some very long text\n          </SelectButtonComponent>\n          <SelectButtonComponent fullWidth prefix={iconLabel}>\n            Some very long text\n          </SelectButtonComponent>\n          <SelectButtonComponent fullWidth>Short</SelectButtonComponent>\n          <SelectButtonComponent fullWidth prefix={iconLabel}>\n            Short\n          </SelectButtonComponent>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Focused (initialy)\">\n        <StoryGrid horizontal>\n          <SelectButtonComponent prefix={iconLabel} autoFocus>\n            With value\n          </SelectButtonComponent>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Interactive\">\n        <InteractiveDemo />\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/select-button.tsx",
    "content": "/**\n * Implementation of the \"Select Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4-3263\n *\n * Primarily intended to be used as Trigger in <Select>.\n * Implemented separately in case we'll need it as a trigger for something else.\n */\n\nimport {\n  forwardRef,\n  type Ref,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\nimport { textVariants } from \"./text\";\nimport { theme, css, type CSS } from \"../stitches.config\";\nimport { ChevronDownIcon } from \"@webstudio-is/icons\";\n\nconst chevronColor = `--ws-select-button-chevron-color`;\nconst chevronStyle = css({ color: `var(${chevronColor})` });\n\nconst style = css({\n  all: \"unset\", // reset <button>\n  minWidth: 0,\n  height: theme.sizes.controlHeight,\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  alignItems: \"center\",\n  background: theme.colors.backgroundControls,\n  border: `1px solid transparent`,\n  borderRadius: theme.borderRadius[4],\n  paddingRight: theme.spacing[1],\n  paddingLeft: theme.spacing[1],\n  color: theme.colors.foregroundMain,\n  [chevronColor]: theme.colors.foregroundSubtle,\n  \"&:hover\": {\n    borderColor: theme.colors.borderMain,\n  },\n  \"&[data-placeholder]:not([data-state=open], :hover, :disabled)\": {\n    color: theme.colors.foregroundSubtle,\n  },\n  \"&:hover:not(:disabled), &[data-state=open]\": {\n    [chevronColor]: theme.colors.foregroundMain,\n  },\n  \"&:disabled\": {\n    background: theme.colors.backgroundInputDisabled,\n    color: theme.colors.foregroundDisabled,\n    [chevronColor]: theme.colors.borderMain,\n  },\n  \"&:focus-visible\": {\n    borderColor: theme.colors.borderFocus,\n  },\n  variants: {\n    fullWidth: { true: { width: \"100%\" } },\n  },\n});\n\nconst textStyle = css(textVariants.regular, {\n  overflow: \"hidden\",\n  textOverflow: \"ellipsis\",\n  whiteSpace: \"nowrap\",\n  flex: 1,\n  paddingRight: theme.spacing[2],\n  paddingLeft: theme.spacing[3],\n});\n\ntype Props = Omit<ComponentProps<\"button\">, \"prefix\"> & {\n  fullWidth?: boolean;\n  css?: CSS;\n  prefix?: ReactNode; // primarily for <NestedIconLabel>\n};\n\nexport const SelectButton = forwardRef(\n  (\n    { prefix, children, css, className, fullWidth, ...rest }: Props,\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <button\n      {...rest}\n      className={style({ css, className, fullWidth })}\n      ref={ref}\n    >\n      {prefix}\n      <span className={textStyle()}>{children}</span>\n      <ChevronDownIcon className={chevronStyle()} />\n    </button>\n  )\n);\nSelectButton.displayName = \"SelectButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/select.stories.tsx",
    "content": "import { GapVerticalIcon } from \"@webstudio-is/icons\";\nimport { useState } from \"react\";\nimport { NestedIconLabel } from \"./nested-icon-label\";\nimport { Select as SelectComponent, type SelectOption } from \"./select\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Select\",\n  component: SelectComponent,\n};\n\nconst fruits = [\"Apple\", \"Banana\", \"Orange\"];\n\nconst emojiItems = {\n  apple: { icon: \"🍎\" },\n  banana: { icon: \"🍌\" },\n  orange: { icon: \"🍊\" },\n  pear: { icon: \"🍐\" },\n  grape: { icon: \"🍇\" },\n} as const;\n\nconst descriptionOptions = [\n  { label: \"Apple\", description: \"An apple fruit\" },\n  { label: \"Banana\", description: \"A banana fruit\" },\n  {\n    label: \"Orange\",\n    description:\n      \"An orange fruit An orange fruit An orange fruit An orange fruit\",\n  },\n  { label: \"Pear\", description: \"A pear fruit\" },\n  { label: \"Grape\", description: \"A grape fruit\" },\n];\n\nexport const Select = () => {\n  const [simple, setSimple] = useState(fruits[0]);\n  const emojiOptions = Object.keys(emojiItems);\n  const [emoji, setEmoji] = useState(emojiOptions[0]);\n  const [desc, setDesc] = useState<(typeof descriptionOptions)[number]>(\n    descriptionOptions[0]\n  );\n  const manyItems = Array(100)\n    .fill(0)\n    .map((_, i) => `Item ${i}`);\n  const [boundary, setBoundary] = useState(manyItems[0]);\n\n  const getEmojiLabel = (option: SelectOption) =>\n    emoji && option in emojiItems\n      ? `${emojiItems[option as keyof typeof emojiItems]?.icon} ${option}`\n      : \"No fruit selected\";\n\n  return (\n    <StorySection title=\"Select\">\n      <Flex direction=\"column\" gap=\"5\" css={{ maxWidth: 300 }}>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Simple</Text>\n          <SelectComponent\n            name=\"fruit\"\n            options={fruits}\n            value={simple}\n            onChange={setSimple}\n          />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Placeholder</Text>\n          <SelectComponent placeholder=\"Select fruit\" options={fruits} />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Disabled</Text>\n          <SelectComponent disabled options={fruits} />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Full width</Text>\n          <SelectComponent\n            name=\"fruit\"\n            options={[\"Apple\", \"Banana\", \"Orange Orange Orange Orange Orange\"]}\n            defaultValue=\"Apple\"\n            fullWidth\n          />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">With icon prefix</Text>\n          <SelectComponent\n            prefix={\n              <NestedIconLabel>\n                <GapVerticalIcon />\n              </NestedIconLabel>\n            }\n            name=\"fruit\"\n            options={fruits}\n            defaultValue=\"Apple\"\n          />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Complex items (emoji)</Text>\n          <SelectComponent\n            name=\"fruit\"\n            options={emojiOptions}\n            value={emoji}\n            onChange={setEmoji}\n            getLabel={getEmojiLabel}\n          />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">With descriptions</Text>\n          <SelectComponent\n            name=\"fruit\"\n            options={descriptionOptions}\n            value={desc}\n            getValue={(v) => v.label}\n            onChange={setDesc}\n            getLabel={(o) => o.label}\n            getDescription={(o) => (\n              <div style={{ width: 150 }}>{o.description}</div>\n            )}\n          />\n        </Flex>\n        <Flex direction=\"column\" gap=\"1\">\n          <Text variant=\"labels\">Many items (100)</Text>\n          <SelectComponent\n            name=\"fruit\"\n            options={manyItems}\n            value={boundary}\n            onChange={setBoundary}\n          />\n        </Flex>\n      </Flex>\n    </StorySection>\n  );\n};\n\nexport const WithItemHighlight = () => {\n  const [value, setValue] = useState(fruits[0]);\n  const [highlighted, setHighlighted] = useState<string | undefined>();\n  return (\n    <StorySection title=\"With item highlight\">\n      <Flex direction=\"column\" gap=\"2\" style={{ maxWidth: 300 }}>\n        <Text variant=\"labels\">Highlighted: {highlighted ?? \"none\"}</Text>\n        <SelectComponent\n          options={fruits}\n          value={value}\n          onChange={setValue}\n          onItemHighlight={setHighlighted}\n        />\n      </Flex>\n    </StorySection>\n  );\n};\n\nexport const ControlledOpen = () => {\n  const [open, setOpen] = useState(false);\n  return (\n    <StorySection title=\"Controlled open\">\n      <Flex direction=\"column\" gap=\"2\" style={{ maxWidth: 300 }}>\n        <Text variant=\"labels\">Open: {String(open)}</Text>\n        <SelectComponent\n          options={fruits}\n          defaultValue=\"Apple\"\n          open={open}\n          onOpenChange={setOpen}\n        />\n      </Flex>\n    </StorySection>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/select.tsx",
    "content": "import * as Primitive from \"@radix-ui/react-select\";\nimport {\n  type ReactNode,\n  type Ref,\n  type ComponentProps,\n  type JSX,\n  useMemo,\n  forwardRef,\n  useState,\n  useEffect,\n  useRef,\n} from \"react\";\nimport { styled, theme } from \"../stitches.config\";\nimport {\n  menuCss,\n  menuItemCss,\n  menuItemIndicatorCss,\n  labelCss,\n  separatorCss,\n  MenuCheckedIcon,\n} from \"./menu\";\nimport { SelectButton } from \"./select-button\";\nimport { Box } from \"./box\";\nimport { ScrollArea } from \"./scroll-area\";\n\nexport const SelectContent = styled(Primitive.Content, menuCss, {\n  minWidth: \"var(--radix-select-trigger-width)\",\n  \"&[data-side=top]\": {\n    \"--ws-select-description-display-top\": \"block\",\n    \"--ws-select-description-order\": 0,\n  },\n  \"&[data-side=bottom]\": {\n    \"--ws-select-description-display-bottom\": \"block\",\n    \"--ws-select-description-order\": 2,\n  },\n});\n\nexport const SelectViewport = Primitive.Viewport;\n\nexport const SelectLabel = styled(Primitive.Label, labelCss);\n\nexport const SelectSeparator = styled(Primitive.Separator, separatorCss);\n\nexport const SelectGroup = Primitive.Group;\n\nconst StyledItem = styled(Primitive.Item, menuItemCss);\n\nconst StyledIndicator = styled(Primitive.ItemIndicator, menuItemIndicatorCss);\n\nconst scrollButtonStyles = {\n  display: \"flex\",\n  alignItems: \"center\",\n  justifyContent: \"center\",\n  height: 25,\n  color: theme.colors.foregroundMain,\n  cursor: \"default\",\n};\n\nexport const SelectScrollUpButton = styled(\n  Primitive.ScrollUpButton,\n  scrollButtonStyles\n);\n\nexport const SelectScrollDownButton = styled(\n  Primitive.ScrollDownButton,\n  scrollButtonStyles\n);\n\nconst SelectItemBase = (\n  { children, icon = <MenuCheckedIcon />, ...props }: SelectItemProps,\n  forwardedRef: Ref<HTMLDivElement>\n) => {\n  return (\n    <StyledItem {...props} withIndicator ref={forwardedRef}>\n      <StyledIndicator>{icon}</StyledIndicator>\n      <Primitive.ItemText>{children}</Primitive.ItemText>\n    </StyledItem>\n  );\n};\n\ntype SelectItemProps = ComponentProps<typeof StyledItem> & {\n  children: ReactNode;\n  icon?: ReactNode;\n};\nexport const SelectItem = forwardRef(SelectItemBase);\n\nexport type SelectOption = string;\n\nconst Description = styled(\"div\", menuItemCss);\n\n// Note this only works in combination with position: popper on Content component, because only popper exposes data-side attribute\nexport const SelectItemDescription = ({\n  children,\n  descriptions,\n}: {\n  children: ReactNode;\n  descriptions: ReactNode[];\n}) => {\n  return (\n    <>\n      <SelectSeparator\n        style={{\n          display: `var(--ws-select-description-display-bottom, none)`,\n          order: \"var(--ws-select-description-order)\",\n        }}\n      />\n\n      <Description\n        css={{\n          display: \"grid\",\n        }}\n        hint\n        style={{\n          order: \"var(--ws-select-description-order)\",\n        }}\n      >\n        {descriptions.map((descr, index) => (\n          <Box\n            css={{\n              gridColumn: \"1\",\n              gridRow: \"1\",\n              visibility: \"hidden\",\n            }}\n            key={index}\n          >\n            {descr}\n          </Box>\n        ))}\n        <Box\n          css={{\n            gridColumn: \"1\",\n            gridRow: \"1\",\n          }}\n        >\n          {children}\n        </Box>\n      </Description>\n\n      <SelectSeparator\n        style={{\n          display: `var(--ws-select-description-display-top, none)`,\n          order: \"var(--ws-select-description-order)\",\n        }}\n      />\n    </>\n  );\n};\n\ntype TriggerPassThroughProps = Omit<\n  ComponentProps<typeof Primitive.Trigger>,\n  \"onChange\" | \"value\" | \"defaultValue\" | \"asChild\" | \"prefix\"\n> &\n  Omit<ComponentProps<typeof SelectButton>, \"onChange\" | \"value\">;\n\nexport type SelectProps<Option = SelectOption> = {\n  options: readonly Option[];\n  defaultValue?: Option;\n  value?: Option;\n  onChange?: (option: Option) => void;\n  onItemHighlight?: (value?: Option) => void;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  placeholder?: string;\n  children?: ReactNode;\n  getDescription?: (option: Option) => ReactNode | undefined;\n  getItemProps?: (\n    option: Option\n  ) => Omit<ComponentProps<typeof SelectItem>, \"children\" | \"value\">;\n} & (Option extends string\n  ? {\n      getLabel?: (option: Option) => ReactNode | undefined;\n      getValue?: (option: Option) => string | undefined;\n    }\n  : {\n      getLabel: (option: Option) => ReactNode | undefined;\n      getValue: (option: Option) => string | undefined;\n    }) &\n  TriggerPassThroughProps;\n\nconst defaultGetValue = (option: unknown) => {\n  if (typeof option === \"string\") {\n    return option;\n  }\n  throw new Error(\n    `Cannot automatically convert ${typeof option} to string. Provide a getValue/getLabel/getDescription`\n  );\n};\n\nconst SelectBase = <Option,>(\n  {\n    options,\n    value,\n    defaultValue,\n    placeholder = \"Select an option\",\n    onChange,\n    onOpenChange,\n    onItemHighlight,\n    open,\n    getLabel = defaultGetValue,\n    getValue = defaultGetValue,\n    getDescription,\n    getItemProps,\n    name,\n    children,\n    prefix,\n    ...props\n  }: SelectProps<Option>,\n  forwardedRef: Ref<HTMLButtonElement>\n) => {\n  const valueToOption = useMemo(() => {\n    const map = new Map<string, Option>();\n    for (const option of options) {\n      map.set(getValue(option) ?? \"\", option);\n    }\n    return map;\n  }, [options, getValue]);\n\n  const [highlightedItem, setHighlightedItem] = useState<Option>();\n\n  const itemForDescription = highlightedItem ?? value ?? defaultValue;\n  const description = itemForDescription\n    ? getDescription?.(itemForDescription)\n    : undefined;\n\n  const descriptions = options.map((option) => getDescription?.(option));\n\n  // Allow reset select fix https://github.com/radix-ui/primitives/issues/2706\n  const [selectResetKeyFix, setSelectResetKeyFix] = useState(0);\n  const prevValue = useRef(value);\n\n  useEffect(() => {\n    if (prevValue.current !== undefined && value === undefined) {\n      setSelectResetKeyFix((prev) => prev + 1);\n    }\n\n    prevValue.current = value;\n  }, [value]);\n\n  return (\n    <Primitive.Root\n      key={selectResetKeyFix}\n      name={name}\n      // null because of https://github.com/radix-ui/primitives/issues/2706\n      value={value === undefined ? undefined : getValue(value)}\n      defaultValue={\n        defaultValue === undefined ? undefined : getValue(defaultValue)\n      }\n      onValueChange={(value) => {\n        const option = valueToOption.get(value);\n        if (option !== undefined) {\n          onChange?.(option);\n        }\n      }}\n      open={open}\n      onOpenChange={onOpenChange}\n    >\n      <Primitive.Trigger ref={forwardedRef} {...props} asChild>\n        <SelectButton prefix={prefix}>\n          <Primitive.Value placeholder={placeholder} />\n        </SelectButton>\n      </Primitive.Trigger>\n      <Primitive.Portal>\n        <SelectContent position=\"popper\">\n          <Box\n            css={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              maxHeight: theme.spacing[34],\n              order: 1,\n            }}\n          >\n            <ScrollArea>\n              <SelectViewport>\n                {children ||\n                  options.map((option, index) => {\n                    const value = getValue(option) ?? \"\";\n                    const { textValue, ...rest } = getItemProps?.(option) ?? {};\n                    return (\n                      <SelectItem\n                        key={value ?? index}\n                        value={value}\n                        textValue={textValue ?? value}\n                        onFocus={() => {\n                          onItemHighlight?.(option);\n                          setHighlightedItem(option);\n                        }}\n                        onBlur={() => {\n                          onItemHighlight?.(undefined);\n                          setHighlightedItem(undefined);\n                        }}\n                        text=\"sentence\"\n                        {...rest}\n                      >\n                        {getLabel(option)}\n                      </SelectItem>\n                    );\n                  })}\n              </SelectViewport>\n            </ScrollArea>\n          </Box>\n          {description && (\n            <SelectItemDescription descriptions={descriptions}>\n              {description}\n            </SelectItemDescription>\n          )}\n        </SelectContent>\n      </Primitive.Portal>\n    </Primitive.Root>\n  );\n};\n\nexport const Select = forwardRef(SelectBase) as <Option>(\n  props: SelectProps<Option> & { ref?: Ref<HTMLButtonElement> }\n) => JSX.Element | null;\n"
  },
  {
    "path": "packages/design-system/src/components/separator.stories.tsx",
    "content": "import { Separator as SeparatorComponent } from \"./separator\";\nimport { Flex } from \"./flex\";\nimport { Text } from \"./text\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Separator\",\n};\n\nexport const Separator = () => (\n  <>\n    <StorySection title=\"Horizontal (default)\">\n      <Flex direction=\"column\" gap=\"2\" css={{ width: 200 }}>\n        <Text>Above</Text>\n        <SeparatorComponent />\n        <Text>Below</Text>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Vertical\">\n      <Flex gap=\"2\" align=\"center\" css={{ height: 40 }}>\n        <Text>Left</Text>\n        <SeparatorComponent orientation=\"vertical\" />\n        <Text>Right</Text>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Decorative (no semantic role)\">\n      <Flex direction=\"column\" gap=\"2\" css={{ width: 200 }}>\n        <Text>Above</Text>\n        <SeparatorComponent decorative />\n        <Text>Below (separator has role=none)</Text>\n      </Flex>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/separator.tsx",
    "content": "/**\n * Implementation of the \"Separator\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A2613\n */\n\nimport { styled, theme, css } from \"../stitches.config\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nexport const separatorStyle = css({\n  border: \"none\",\n  margin: 0,\n  flexShrink: 0,\n  alignSelf: \"stretch\",\n  backgroundColor: theme.colors.borderMain,\n  cursor: \"default\",\n  '&[data-orientation=\"horizontal\"]': { height: theme.spacing[1] },\n  '&[data-orientation=\"vertical\"]': { width: theme.spacing[1] },\n});\n\nexport const Separator = styled(SeparatorPrimitive.Root, separatorStyle);\n"
  },
  {
    "path": "packages/design-system/src/components/small-icon-button.stories.tsx",
    "content": "import { TrashIcon } from \"@webstudio-is/icons\";\nimport {\n  SmallIconButton as SmallIconButtonComponent,\n  smallIconButtonStates,\n  smallIconButtonVariants,\n} from \"./small-icon-button\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nconst states = [undefined, ...smallIconButtonStates];\n\nexport const SmallIconButton = () => (\n  <>\n    <StorySection title=\"Variants & States\">\n      <StoryGrid>\n        {smallIconButtonVariants.map((variant) => (\n          <StoryGrid horizontal key={variant}>\n            {states.map((state) => (\n              <SmallIconButtonComponent\n                key={state ?? \"undefined\"}\n                title={`${variant} ${state}`}\n                icon={<TrashIcon />}\n                state={state}\n                variant={variant}\n              />\n            ))}\n          </StoryGrid>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Focus\">\n      <StoryGrid>\n        {smallIconButtonVariants.map((variant) => (\n          <StoryGrid horizontal key={variant}>\n            {states.map((state) => (\n              <SmallIconButtonComponent\n                key={state ?? \"undefined\"}\n                title={`${variant} ${state}`}\n                icon={<TrashIcon />}\n                state={state}\n                variant={variant}\n                focused\n              />\n            ))}\n          </StoryGrid>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Disabled across variants\">\n      <StoryGrid horizontal>\n        {smallIconButtonVariants.map((variant) => (\n          <SmallIconButtonComponent\n            key={variant}\n            title={variant}\n            icon={<TrashIcon />}\n            variant={variant}\n            disabled\n          />\n        ))}\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n\nexport default {\n  title: \"Small Icon Button\",\n  parameters: {\n    // to make the variant=contrast visible\n    backgrounds: { default: \"Maintenance Medium\" },\n  },\n};\n"
  },
  {
    "path": "packages/design-system/src/components/small-icon-button.tsx",
    "content": "/**\n * Implementation of the \"Small Icon Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A3171\n */\n\nimport {\n  forwardRef,\n  type Ref,\n  type ComponentProps,\n  type ReactNode,\n} from \"react\";\n\nimport { SmallButton, smallButtonVariants } from \"./primitives/small-button\";\n\nexport const smallIconButtonVariants = smallButtonVariants;\n\n/**\n * data-state from Radix, might be set when <SmallIconButton> is asChild\n * https://www.radix-ui.com/docs/primitives/components/popover#trigger\n * we don't mind about \"closed\" state considering it as no-state\n **/\nexport const smallIconButtonStates = [\"open\"] as const;\n\ntype Props = {\n  icon: ReactNode;\n  state?: (typeof smallIconButtonStates)[number];\n  focused?: boolean;\n  // might be set when <SmallIconButton> is asChild\n  \"data-state\"?: (typeof smallIconButtonStates)[number];\n} & Omit<ComponentProps<typeof SmallButton>, \"children\">;\n\nexport const SmallIconButton = forwardRef(\n  (\n    { state, \"data-state\": dataState, focused, icon, ...restProps }: Props,\n    ref: Ref<HTMLButtonElement>\n  ) => {\n    return (\n      <SmallButton\n        {...restProps}\n        data-state={state ?? dataState}\n        data-focused={focused}\n        ref={ref}\n      >\n        {icon}\n      </SmallButton>\n    );\n  }\n);\nSmallIconButton.displayName = \"SmallIconButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/small-toggle-button.stories.tsx",
    "content": "import { EyeOpenIcon, EyeClosedIcon } from \"@webstudio-is/icons\";\nimport {\n  SmallToggleButton as SmallToggleButtonComponent,\n  smallToggleButtonVariants,\n} from \"./small-toggle-button\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { theme } from \"../stitches.config\";\n\nexport const SmallToggleButton = () => (\n  <>\n    <StorySection title=\"Variants & States\">\n      <StoryGrid>\n        {smallToggleButtonVariants.map((variant) => (\n          <StoryGrid\n            css={\n              variant === \"contrast\"\n                ? {\n                    backgroundColor: theme.colors.backgroundActive,\n                    padding: theme.spacing[5],\n                  }\n                : {\n                    padding: theme.spacing[5],\n                  }\n            }\n            horizontal\n            key={variant}\n          >\n            {[false, true].map((defaultPressed) => (\n              <SmallToggleButtonComponent\n                key={defaultPressed ? \"pressed\" : \"unpressed\"}\n                title={`${variant} ${defaultPressed}`}\n                icon={defaultPressed ? <EyeClosedIcon /> : <EyeOpenIcon />}\n                defaultPressed={defaultPressed}\n                variant={variant}\n              />\n            ))}\n\n            {[false, true].map((defaultPressed) => (\n              <SmallToggleButtonComponent\n                key={defaultPressed ? \"pressed\" : \"unpressed\"}\n                title={`${variant} ${defaultPressed}`}\n                icon={defaultPressed ? <EyeClosedIcon /> : <EyeOpenIcon />}\n                defaultPressed={defaultPressed}\n                variant={variant}\n                disabled\n              />\n            ))}\n          </StoryGrid>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Focus\">\n      <StoryGrid>\n        {smallToggleButtonVariants.map((variant) => (\n          <StoryGrid\n            css={\n              variant === \"contrast\"\n                ? {\n                    backgroundColor: theme.colors.backgroundActive,\n                    padding: theme.spacing[5],\n                  }\n                : {\n                    padding: theme.spacing[5],\n                  }\n            }\n            horizontal\n            key={variant}\n          >\n            {[false, true].map((defaultPressed) => (\n              <SmallToggleButtonComponent\n                key={defaultPressed ? \"pressed\" : \"unpressed\"}\n                title={`${variant} ${defaultPressed}`}\n                icon={defaultPressed ? <EyeClosedIcon /> : <EyeOpenIcon />}\n                defaultPressed={defaultPressed}\n                variant={variant}\n                focused\n              />\n            ))}\n            {[false, true].map((defaultPressed) => (\n              <SmallToggleButtonComponent\n                key={defaultPressed ? \"pressed\" : \"unpressed\"}\n                title={`${variant} ${defaultPressed}`}\n                icon={defaultPressed ? <EyeClosedIcon /> : <EyeOpenIcon />}\n                defaultPressed={defaultPressed}\n                variant={variant}\n                focused\n                disabled\n              />\n            ))}\n          </StoryGrid>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Controlled pressed\">\n      <StoryGrid horizontal>\n        <SmallToggleButtonComponent icon={<EyeClosedIcon />} pressed={true} />\n        <SmallToggleButtonComponent icon={<EyeOpenIcon />} pressed={false} />\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n\nexport default {\n  title: \"Small Toggle Button\",\n  parameters: {\n    // to make the variant=contrast visible\n    backgrounds: { default: \"Panel\" },\n  },\n};\n"
  },
  {
    "path": "packages/design-system/src/components/small-toggle-button.tsx",
    "content": "/**\n * Implementation of the \"Small Toggle Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4%3A3236&t=Nn8L5VPCruHNv1no-0\n */\n\nimport { forwardRef, type ComponentProps, type ReactNode } from \"react\";\nimport type { CSS } from \"../stitches.config\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { SmallButton } from \"./primitives/small-button\";\n\nexport const smallToggleButtonVariants = [\"normal\", \"contrast\"] as const;\n\ntype Props = {\n  icon: ReactNode;\n  variant?: (typeof smallToggleButtonVariants)[number];\n  focused?: boolean;\n  pressed?: boolean;\n  defaultPressed?: boolean;\n  disabled?: boolean;\n  onPressedChange?(pressed: boolean): void;\n  css?: CSS;\n} & Omit<ComponentProps<typeof TogglePrimitive.Root>, \"children\">;\n\nexport const SmallToggleButton = forwardRef<HTMLButtonElement, Props>(\n  ({ focused, icon, ...restProps }, ref) => {\n    return (\n      <TogglePrimitive.Root asChild ref={ref} {...restProps}>\n        <SmallButton data-focused={focused}>{icon}</SmallButton>\n      </TogglePrimitive.Root>\n    );\n  }\n);\nSmallToggleButton.displayName = \"SmallToggleButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/storybook.tsx",
    "content": "/**\n * Some helpers for stories\n */\n\nimport type { ReactNode } from \"react\";\nimport { theme, css, type CSS } from \"../stitches.config\";\nimport { textVariants } from \"./text\";\n\nconst sectionStyle = css({\n  marginBottom: theme.spacing[10],\n  variants: {\n    withBorder: {\n      true: {\n        border: `1px dashed ${theme.colors.borderMain}`,\n        padding: theme.spacing[5],\n        marginBottom: theme.spacing[7],\n      },\n    },\n  },\n});\nconst titleStyle = css(textVariants.titles, {\n  marginTop: 0,\n  marginBottom: theme.spacing[5],\n  color: theme.colors.foregroundMain,\n});\nexport const StorySection = ({\n  title,\n  withBorder,\n  children,\n}: {\n  title: string;\n  withBorder?: boolean;\n  children: ReactNode;\n}) => (\n  <section className={sectionStyle({ withBorder })}>\n    <h3 className={titleStyle()}>{title}</h3>\n    {children}\n  </section>\n);\n\nconst gridStyle = css({\n  display: \"flex\",\n  flexWrap: \"wrap\",\n  gap: theme.spacing[7],\n  flexDirection: \"column\",\n  variants: { horizontal: { true: { flexDirection: \"row\" } } },\n});\nexport const StoryGrid = ({\n  children,\n  horizontal,\n  css,\n}: {\n  children: ReactNode;\n  horizontal?: boolean;\n  css?: CSS;\n}) => <div className={gridStyle({ horizontal, css })}>{children}</div>;\n"
  },
  {
    "path": "packages/design-system/src/components/switch.stories.tsx",
    "content": "import { useState } from \"react\";\nimport { Label } from \"./label\";\nimport { Switch } from \"./switch\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nexport default {\n  title: \"Switch\",\n};\n\nconst ControlledSwitch = () => {\n  const [checked, setChecked] = useState(false);\n  return (\n    <StoryGrid horizontal>\n      <Switch\n        id=\"switch-controlled\"\n        checked={checked}\n        onCheckedChange={setChecked}\n      />\n      <Label htmlFor=\"switch-controlled\">{checked ? \"On\" : \"Off\"}</Label>\n    </StoryGrid>\n  );\n};\n\nconst Story = () => {\n  return (\n    <>\n      <StorySection title=\"Enabled\">\n        <StoryGrid horizontal>\n          <Switch defaultChecked />\n          <Switch />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Disabled\">\n        <StoryGrid horizontal>\n          <Switch defaultChecked disabled />\n          <Switch disabled />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Focussed (initially)\">\n        <Switch autoFocus />\n      </StorySection>\n      <StorySection title=\"With label\">\n        <StoryGrid horizontal>\n          <Switch id=\"switch-label\" defaultChecked />\n          <Label htmlFor=\"switch-label\">Enable feature</Label>\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Controlled\">\n        <ControlledSwitch />\n      </StorySection>\n    </>\n  );\n};\n\nexport { Story as Switch };\n"
  },
  {
    "path": "packages/design-system/src/components/switch.tsx",
    "content": "/**\n * Implementation of the \"Switch\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=1086%3A3430&t=WVRoENiFP3BSlQa7-0\n */\n\nimport { forwardRef, type ComponentProps, type Ref } from \"react\";\nimport * as Primitive from \"@radix-ui/react-switch\";\nimport { type CSS, css, theme } from \"../stitches.config\";\n\nconst padding = theme.spacing[1];\nconst thumbOffset = `calc(${padding} + ${theme.spacing[1]})`;\n\nconst switchStyle = css({\n  all: \"unset\", // reset <button>\n  boxSizing: \"content-box\",\n  width: theme.spacing[11],\n  height: theme.spacing[8],\n  borderRadius: theme.borderRadius.pill,\n  position: \"relative\",\n  verticalAlign: \"middle\",\n\n  // in Figma there's an extra container with a padding\n  // so we need a pseudo element\n  padding,\n  \"&:before\": {\n    content: \"''\",\n    position: \"absolute\",\n    inset: padding,\n    borderRadius: theme.borderRadius.pill,\n    backgroundColor: theme.colors.backgroundNeutralDark,\n  },\n\n  \"&[data-state=checked]:not([data-disabled]):before, &[aria-checked=true]:not([data-disabled]):before\":\n    {\n      backgroundColor: theme.colors.backgroundPrimary,\n    },\n\n  \"&[data-disabled]:before\": {\n    backgroundColor: theme.colors.foregroundDisabled,\n  },\n\n  \"&:focus\": {\n    outline: `1px solid ${theme.colors.borderFocus}`,\n  },\n});\n\nconst thumbStyle = css({\n  width: theme.spacing[7],\n  height: theme.spacing[7],\n  borderRadius: theme.borderRadius.round,\n  backgroundColor: theme.colors.foregroundContrastMain,\n  position: \"absolute\",\n  top: thumbOffset,\n  left: thumbOffset,\n  transition: \"transform 100ms\",\n  \"&[data-state=checked]\": {\n    transform: `translateX(${theme.spacing[6]})`,\n  },\n});\n\nexport const Switch = forwardRef(\n  (\n    {\n      className,\n      css,\n      ...props\n    }: ComponentProps<typeof Primitive.Root> & { css?: CSS },\n    ref: Ref<HTMLButtonElement>\n  ) => (\n    <Primitive.Root\n      className={switchStyle({ className, css })}\n      {...props}\n      ref={ref}\n    >\n      <Primitive.Thumb className={thumbStyle()} />\n    </Primitive.Root>\n  )\n);\nSwitch.displayName = \"Switch\";\n"
  },
  {
    "path": "packages/design-system/src/components/text-area.stories.tsx",
    "content": "import { TextArea as TextAreaComponent } from \"./text-area\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { useState } from \"react\";\n\nexport default {\n  title: \"Text Area\",\n};\n\nconst exampleValue =\n  \"This is an example value of a text area. It's long enough to show how it wraps.\";\n\nexport const TextArea = () => {\n  const [value, setValue] = useState(exampleValue);\n\n  return (\n    <>\n      <StorySection title=\"Uncontrollable\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent placeholder=\"Enter value...\" />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"No AutoGrow, rows=3 (Manual resize only, no height limit)\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"No AutoGrow, rows=3, maxRows=6 (Manual resize up to 6 rows)\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} maxRows={6} />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"AutoGrow, no max (Auto-resize with no height limit.)\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} autoGrow />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"AutoGrow, maxRows=10 (Auto-resize up to 10 rows.)\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent\n            value={value}\n            onChange={setValue}\n            autoGrow\n            maxRows={10}\n          />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"With value\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Error\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} color=\"error\" />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Disabled\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} disabled />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Disabled error\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent\n            value={value}\n            onChange={setValue}\n            color=\"error\"\n            disabled\n          />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Focused (initialy)\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} autoFocus />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Rows test\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} rows={1} />\n          <TextAreaComponent value={value} onChange={setValue} rows={5} />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Width test\">\n        <StoryGrid css={{ width: 250 }}>\n          <TextAreaComponent value={value} onChange={setValue} />\n        </StoryGrid>\n      </StorySection>\n      <StorySection title=\"Mono\">\n        <StoryGrid css={{ width: 250 }}>\n          <TextAreaComponent variant=\"mono\" value={value} onChange={setValue} />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Uncontrolled with default value\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent defaultValue=\"This is a default value for an uncontrolled text area\" />\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Grow (manual resize disabled)\">\n        <StoryGrid css={{ width: 200 }}>\n          <TextAreaComponent value={value} onChange={setValue} grow />\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/text-area.tsx",
    "content": "/**\n * Implementation of the \"Text Area\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4-3389\n */\n\nimport { type ComponentProps, type Ref, forwardRef } from \"react\";\nimport { type CSS, css, theme } from \"../stitches.config\";\nimport { textVariants } from \"./text\";\nimport { Grid } from \"./grid\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { ScrollArea } from \"./scroll-area\";\n\nconst lineHeight = 16;\nconst paddingY = 3;\nconst borderWidth = 1;\n\nconst gridStyle = css({\n  color: theme.colors.foregroundMain,\n  borderRadius: theme.borderRadius[4],\n  border: `${borderWidth}px solid transparent`,\n  background: theme.colors.backgroundControls,\n  paddingTop: paddingY,\n  paddingBottom: paddingY,\n  boxSizing: \"border-box\",\n  resize: \"vertical\",\n  overflow: \"auto\",\n  width: \"100%\",\n  \"&:hover\": {\n    borderColor: theme.colors.borderMain,\n  },\n  \"&:focus-within\": {\n    borderColor: theme.colors.borderFocus,\n  },\n  \"&:has([data-color=error])\": {\n    borderColor: theme.colors.borderDestructiveMain,\n    \"&:focus-within\": {\n      outlineColor: theme.colors.borderDestructiveMain,\n    },\n  },\n  \"&:has(textarea:disabled)\": {\n    backgroundColor: theme.colors.backgroundInputDisabled,\n  },\n  variants: {\n    grow: {\n      true: {\n        resize: \"none\",\n      },\n    },\n    variant: {\n      regular: textVariants.regular,\n      mono: textVariants.mono,\n    },\n  },\n  defaultVariants: {\n    variant: \"regular\",\n  },\n});\n\nconst commonStyle = css({\n  border: \"none\",\n  paddingRight: theme.spacing[4],\n  paddingLeft: theme.spacing[3],\n  paddingTop: 0,\n  paddingBottom: 0,\n  boxSizing: \"border-box\",\n  gridArea: \"1 / 1 / 2 / 2\",\n  background: \"transparent\",\n  color: \"inherit\",\n  textWrap: \"wrap\",\n  overflowWrap: \"break-word\",\n  whiteSpace: \"pre-wrap\",\n  overflow: \"hidden\",\n  resize: \"none\",\n  outline: \"none\",\n  \"&::placeholder\": {\n    color: theme.colors.foregroundContrastSubtle,\n  },\n  \"&:disabled\": {\n    color: theme.colors.foregroundDisabled,\n  },\n  variants: {\n    variant: {\n      regular: textVariants.regular,\n      mono: textVariants.mono,\n    },\n  },\n  defaultVariants: {\n    variant: \"regular\",\n  },\n});\n\nconst textAreaStyle = css(commonStyle, {});\n\ntype Props = Omit<\n  ComponentProps<\"textarea\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> & {\n  css?: CSS;\n  rows?: number;\n  maxRows?: number;\n  color?: \"error\";\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  grow?: boolean;\n  autoGrow?: boolean;\n  variant?: \"regular\" | \"mono\";\n};\n\nexport const TextArea = forwardRef(\n  (\n    {\n      css,\n      className,\n      rows = 3,\n      maxRows,\n      color,\n      value,\n      onChange,\n      grow,\n      autoGrow,\n      variant = \"regular\",\n      defaultValue,\n      ...props\n    }: Props,\n    ref: Ref<HTMLTextAreaElement>\n  ) => {\n    const [textValue, setTextValue] = useControllableState({\n      prop: value,\n      defaultProp: defaultValue ?? \"\",\n      onChange,\n    });\n\n    // We could use `box-sizing:content-box` to avoid dealing with paddings and border here\n    // But then, the user of the component will not be able to set `width` reliably\n    const minHeight = rows * lineHeight + paddingY + paddingY + borderWidth * 2;\n\n    const height = autoGrow || grow ? undefined : minHeight;\n\n    const maxHeight = maxRows\n      ? maxRows * lineHeight + paddingY + paddingY + borderWidth * 2\n      : undefined;\n\n    return (\n      <Grid\n        className={gridStyle({\n          grow: grow || autoGrow,\n          variant,\n          css: { height, minHeight, maxHeight },\n        })}\n        onClick={(event) => {\n          if (event.target instanceof HTMLElement) {\n            // Focus textarea on click since it can't match parent height.\n            const textarea = event.target.querySelector(\"textarea\");\n            if (textarea) {\n              textarea.focus();\n            }\n          }\n        }}\n      >\n        <ScrollArea\n          css={{\n            height: \"100%\",\n            maxHeight: maxRows ? maxRows * lineHeight : undefined,\n            // Overwrite hack from scroll-area.tsx\n            \"& [data-radix-scroll-area-viewport] > div\": {\n              display: \"grid!important\",\n            },\n          }}\n        >\n          <div\n            className={commonStyle({\n              css: { visibility: \"hidden\", ...css },\n              className,\n              variant,\n            })}\n          >\n            {textValue}{\" \"}\n          </div>\n\n          <textarea\n            spellCheck={false}\n            className={textAreaStyle({\n              css,\n              className,\n              variant,\n            })}\n            data-color={color}\n            onChange={(event) => setTextValue(event.target.value)}\n            value={textValue}\n            rows={rows}\n            ref={ref}\n            {...props}\n          />\n        </ScrollArea>\n      </Grid>\n    );\n  }\n);\nTextArea.displayName = \"TextArea\";\n"
  },
  {
    "path": "packages/design-system/src/components/text.stories.tsx",
    "content": "import { textVariants, Text as TextComponent } from \"./text\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { Flex } from \"./flex\";\n\nconst variants = Object.keys(textVariants) as (keyof typeof textVariants)[];\n\nconst colors = [\n  \"main\",\n  \"contrast\",\n  \"subtle\",\n  \"moreSubtle\",\n  \"disabled\",\n  \"success\",\n  \"destructive\",\n] as const;\n\nexport default {\n  title: \"Text\",\n};\n\nexport const Text = () => (\n  <>\n    <StorySection title=\"Variants\">\n      {variants.map((variant) => (\n        <StorySection withBorder key={variant} title={variant}>\n          <TextComponent variant={variant}>\n            The quick brown fox jumps over the lazy dog\n          </TextComponent>\n        </StorySection>\n      ))}\n    </StorySection>\n\n    <StorySection title=\"Colors\">\n      <StoryGrid>\n        {colors.map((color) => (\n          <TextComponent key={color} color={color}>\n            {color}\n          </TextComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Alignment\">\n      <Flex direction=\"column\" gap=\"2\" css={{ width: 300 }}>\n        <TextComponent align=\"left\">Left aligned</TextComponent>\n        <TextComponent align=\"center\">Center aligned</TextComponent>\n        <TextComponent align=\"right\">Right aligned</TextComponent>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Truncate\">\n      <Flex direction=\"column\" gap=\"2\" css={{ width: 200 }}>\n        <TextComponent truncate>\n          This is a very long text that should be truncated at the container\n          edge\n        </TextComponent>\n        <TextComponent>\n          This is a very long text that should NOT be truncated and will wrap\n        </TextComponent>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"User select\">\n      <TextComponent userSelect=\"none\">\n        This text cannot be selected (userSelect: none)\n      </TextComponent>\n    </StorySection>\n\n    <StorySection title=\"Inline text\">\n      <TextComponent>\n        This is a paragraph with{\" \"}\n        <TextComponent inline color=\"destructive\">\n          inline destructive\n        </TextComponent>{\" \"}\n        and{\" \"}\n        <TextComponent inline color=\"success\">\n          inline success\n        </TextComponent>{\" \"}\n        text inside.\n      </TextComponent>\n    </StorySection>\n\n    <StorySection title=\"User select text\">\n      <TextComponent userSelect=\"text\">\n        This text can be selected (userSelect: text)\n      </TextComponent>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/text.ts",
    "content": "import { css, theme, styled } from \"../stitches.config\";\nimport { truncate } from \"../utilities\";\nimport { typography } from \"../__generated__/figma-design-tokens\";\n\nconst normalize = {\n  userSelect: \"none\",\n} as const;\n\ntype Variant = keyof typeof typography;\ntype VariantStyle = typeof normalize & (typeof typography)[Variant];\n\nexport const textVariants = {} as { [Key in Variant]: VariantStyle };\n\nlet variant: Variant;\nfor (variant in typography) {\n  textVariants[variant] = {\n    ...typography[variant as Variant],\n    ...normalize,\n  };\n}\n\nexport const textStyle = css({\n  margin: 0, // in case it's used with <p>\n  WebkitFontSmoothing: \"antialiased\",\n  variants: {\n    variant: textVariants,\n    color: {\n      main: { color: theme.colors.foregroundMain },\n      contrast: { color: theme.colors.foregroundContrastMain },\n      subtle: { color: theme.colors.foregroundSubtle },\n      moreSubtle: { color: theme.colors.foregroundTextMoreSubtle },\n      disabled: { color: theme.colors.foregroundDisabled },\n      success: { color: theme.colors.foregroundSuccessText },\n      destructive: {\n        color: theme.colors.foregroundDestructive,\n        // destructive in most cases used to show 3rd party errors\n        // we don't want it to break layout\n        overflowWrap: \"anywhere\",\n        userSelect: \"auto\",\n      },\n    },\n    align: {\n      left: { textAlign: \"left\" },\n      center: { textAlign: \"center\" },\n      right: { textAlign: \"right\" },\n    },\n    truncate: {\n      true: {\n        ...truncate(),\n\n        // To make sure text is not clipped vertically\n        pt: \"0.5em\",\n        pb: \"0.5em\",\n        mt: \"-0.5em\",\n        mb: \"-0.5em\",\n      },\n    },\n    userSelect: {\n      text: {\n        userSelect: \"text\",\n      },\n      none: {\n        userSelect: \"none\",\n      },\n    },\n    inline: {\n      true: {\n        display: \"inline\",\n      },\n    },\n  },\n  defaultVariants: { variant: \"regular\" },\n});\n\nexport const Text = styled(\"div\", textStyle);\n"
  },
  {
    "path": "packages/design-system/src/components/toast.stories.tsx",
    "content": "import { Toast as ToastComponent } from \"./toast\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { Image } from \"@webstudio-is/image\";\nimport { Box } from \"./box\";\nimport { css, theme } from \"../stitches.config\";\nimport { AlertCircleIcon } from \"@webstudio-is/icons\";\n\nexport default {\n  title: \"Toast\",\n};\n\nconst toastIconUrl = `data:image/svg+xml,${encodeURIComponent(\n  `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"64\" height=\"64\" viewBox=\"0 0 64 64\">\n    <rect width=\"64\" height=\"64\" rx=\"8\" fill=\"#e0e0e0\"/>\n    <rect x=\"16\" y=\"20\" width=\"32\" height=\"24\" rx=\"2\" fill=\"#888\"/>\n    <circle cx=\"26\" cy=\"30\" r=\"4\" fill=\"#bbb\"/>\n    <polygon points=\"18,42 30,32 38,38 46,30 46,42\" fill=\"#aaa\"/>\n  </svg>`\n)}`;\n\nconst imageWidth = css({\n  maxWidth: \"100%\",\n});\n\nconst ImageIcon = () => (\n  <Box css={{ width: theme.spacing[18] }}>\n    <Image\n      className={imageWidth()}\n      src={toastIconUrl}\n      optimize={false}\n      width={64}\n      loader={() => \"\"}\n    />\n  </Box>\n);\n\nexport const Toast = () => {\n  return (\n    <>\n      <StorySection title=\"Toast Design\">\n        <StoryGrid>\n          <ToastComponent>1. We are what repeatedly do.</ToastComponent>\n\n          <ToastComponent>\n            2. We are what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent>\n            3. We are what repeatedly do. We are what repeatedly do. Excellence\n            is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent>\n            3. We are what repeatedly do. We are what repeatedly do. Excellence\n            is not an act but a habit. We are what\n          </ToastComponent>\n\n          <ToastComponent>\n            4. We are what repeatedly do. We are what repeatedly do. We are what\n            repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent>\n            5. We are what repeatedly do. We are what repeatedly do. We are what\n            repeatedly do. We are what repeatedly do. Excellence is not an act\n            but a habit.\n          </ToastComponent>\n\n          <ToastComponent>\n            6. We are what repeatedly do. We are what repeatedly do. We are what\n            repeatedly do. We are what repeatedly do. We are what repeatedly do.\n            Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent variant=\"warning\">\n            We are what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent variant=\"error\">\n            We are what repeatedly do. Excellence is not an act but a habit. We\n            are what repeatedly do. Excellence is not an act but a habit. We are\n            what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n          <ToastComponent variant=\"success\">\n            We are what repeatedly do. Excellence is not an act but a habit. We\n            are what repeatedly do. Excellence is not an act but a habit. We are\n            what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Toast With Icon Design\">\n        <StoryGrid>\n          <ToastComponent icon={<AlertCircleIcon size={24} />}>\n            We are what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent\n            icon={<AlertCircleIcon size={24} />}\n            variant=\"warning\"\n          >\n            We are what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent icon={<AlertCircleIcon size={24} />} variant=\"error\">\n            We are what repeatedly do. Excellence is not an act but a habit. We\n            are what repeatedly do. Excellence is not an act but a habit. We are\n            what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent\n            variant=\"success\"\n            icon={<AlertCircleIcon size={24} />}\n          >\n            We are what repeatedly do. Excellence is not an act but a habit. We\n            are what repeatedly do. Excellence is not an act but a habit. We are\n            what repeatedly do. Excellence is not an act but a habit.\n          </ToastComponent>\n\n          <ToastComponent icon={<ImageIcon />}>\n            Asset already exists\n          </ToastComponent>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"Very long Toast\">\n        <StoryGrid>\n          <ToastComponent icon={<AlertCircleIcon size={24} />}>\n            {\"We are what repeatedly do. Excellence is not an act but a habit.\".repeat(\n              100\n            )}\n          </ToastComponent>\n        </StoryGrid>\n      </StorySection>\n\n      <StorySection title=\"With close and copy actions\">\n        <StoryGrid>\n          <ToastComponent\n            onClose={() => window.alert(\"Close clicked\")}\n            onCopy={() => window.alert(\"Copy clicked\")}\n          >\n            Toast with close and copy actions\n          </ToastComponent>\n          <ToastComponent\n            variant=\"error\"\n            icon={<AlertCircleIcon size={24} />}\n            onClose={() => window.alert(\"Close clicked\")}\n            onCopy={() => window.alert(\"Copy clicked\")}\n          >\n            Error toast with actions\n          </ToastComponent>\n        </StoryGrid>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/toast.tsx",
    "content": "import type { JSX } from \"react\";\nimport * as ToastPrimitive from \"@radix-ui/react-toast\";\nimport hotToast, {\n  resolveValue,\n  useToaster,\n  type Toast as HotToast,\n  type ToastOptions,\n} from \"react-hot-toast/headless\";\nimport { keyframes, rawTheme, styled, type CSS } from \"../stitches.config\";\nimport { Box } from \"./box\";\nimport { theme } from \"../stitches.config\";\nimport { Grid } from \"./grid\";\nimport { Text } from \"./text\";\nimport { Flex } from \"./flex\";\nimport { Tooltip } from \"./tooltip\";\nimport { Button } from \"./button\";\nimport { CopyIcon, LargeXIcon } from \"@webstudio-is/icons\";\n\nconst ANIMATION_SLIDE_LENGTH = 30;\n\nconst hide = keyframes({\n  \"0%\": { opacity: 1 },\n  \"100%\": { opacity: 0 },\n});\n\nconst slideIn = keyframes({\n  from: { transform: `translateY(calc(0px + ${ANIMATION_SLIDE_LENGTH}px))` },\n  to: { transform: \"translateY(0)\" },\n});\n\nconst swipeOut = keyframes({\n  from: { transform: \"translateX(var(--radix-toast-swipe-end-x))\" },\n  to: { transform: `translateX(calc(100% + ${ANIMATION_SLIDE_LENGTH}px))` },\n});\n\nconst StyledViewport = styled(ToastPrimitive.Viewport, {\n  position: \"absolute\",\n  top: 0,\n  right: 0,\n  display: \"flex\",\n  flexDirection: \"column\",\n  padding: theme.spacing[5],\n  gap: theme.spacing[5],\n  minWidth: 200,\n  width: \"auto\",\n  maxWidth: \"100vw\",\n  margin: 0,\n  listStyle: \"none\",\n  zIndex: theme.zIndices.max,\n  outline: \"none\",\n});\n\nconst AnimatedToast = styled(ToastPrimitive.Root, {\n  \"@media (prefers-reduced-motion: no-preference)\": {\n    '&[data-state=\"open\"]': {\n      animation: `${slideIn} 250ms cubic-bezier(0.16, 1, 0.3, 1)`,\n    },\n    '&[data-state=\"closed\"]': {\n      animation: `${hide} 105ms ease-in`,\n    },\n    '&[data-swipe=\"move\"]': {\n      transform: \"translateX(var(--radix-toast-swipe-move-x))\",\n    },\n    '&[data-swipe=\"cancel\"]': {\n      transform: \"translateX(0)\",\n      transition: \"transform 200ms ease-out\",\n    },\n    '&[data-swipe=\"end\"]': {\n      animation: `${swipeOut} 150ms ease-out`,\n    },\n  },\n});\n\nconst borderAccentBackgroundColor = \"--ws-toast-border-accent-background-color\";\nconst backgroundColor = \"--ws-toast-background-color\";\nconst borderColor = \"--ws-toast-border-color\";\nconst iconColor = \"--ws-toast-icon-color\";\n\nconst ToastVariants = styled(\"div\", {\n  [borderAccentBackgroundColor]: theme.colors.foregroundMain,\n  [backgroundColor]: theme.colors.backgroundNeutralNotification,\n  [borderColor]: theme.colors.borderNeutral,\n  [iconColor]: theme.colors.foregroundMain,\n\n  variants: {\n    variant: {\n      neutral: {},\n      warning: {\n        [backgroundColor]: theme.colors.backgroundAlertNotification,\n        [borderAccentBackgroundColor]: theme.colors.backgroundAlertMain,\n        [borderColor]: theme.colors.backgroundAlertMain,\n        [iconColor]: theme.colors.backgroundAlertMain,\n      },\n      error: {\n        [backgroundColor]: theme.colors.backgroundDestructiveNotification,\n        [borderAccentBackgroundColor]: theme.colors.backgroundDestructiveMain,\n        [borderColor]: theme.colors.backgroundDestructiveMain,\n        [iconColor]: theme.colors.foregroundDestructive,\n      },\n      success: {\n        [backgroundColor]: theme.colors.backgroundSuccessNotification,\n        [borderAccentBackgroundColor]: theme.colors.backgroundSuccessMain,\n        [borderColor]: theme.colors.backgroundSuccessMain,\n        [iconColor]: theme.colors.foregroundSuccess,\n      },\n    },\n  },\n});\n\ntype ToastVariant = React.ComponentProps<typeof ToastVariants>[\"variant\"];\n\nconst cssVar = (name: string) => `var(${name})`;\n\nconst InternalToast = ({\n  children,\n  variant,\n  icon,\n  sideButtons,\n}: {\n  children: React.ReactNode;\n  variant?: ToastVariant;\n  icon?: React.ReactNode;\n  sideButtons: React.ReactNode;\n}) => {\n  return (\n    <ToastVariants variant={variant}>\n      <Grid\n        css={{\n          display: \"grid\",\n          gridTemplateColumns: \"8px 1fr\",\n          pointerEvents: \"all\",\n        }}\n      >\n        <Box\n          css={{\n            backgroundColor: cssVar(borderAccentBackgroundColor),\n            borderTopLeftRadius: theme.borderRadius[5],\n            borderBottomLeftRadius: theme.borderRadius[5],\n          }}\n        ></Box>\n        <Grid\n          gap={\"3\"}\n          align={\"center\"}\n          css={{\n            backgroundColor: cssVar(backgroundColor),\n            padding: theme.panel.padding,\n            gridTemplateColumns: icon ? \"auto 1fr auto\" : \"1fr auto\",\n            borderBottomRightRadius: theme.borderRadius[5],\n            borderTopRightRadius: theme.borderRadius[5],\n            border: `1px solid ${cssVar(borderColor)}`,\n            borderLeft: \"none\",\n          }}\n        >\n          <Box\n            css={{\n              color: cssVar(iconColor),\n              display: icon ? \"contents\" : \"none\",\n            }}\n          >\n            {icon}\n          </Box>\n\n          <Grid gap={\"1\"}>\n            <ToastPrimitive.Description asChild>\n              <Text\n                css={{\n                  whiteSpace: \"pre-wrap\",\n                  wordBreak: \"break-word\",\n                  display: \"-webkit-box\",\n                  \"-webkit-line-clamp\": 20,\n                  \"-webkit-box-orient\": \"vertical\",\n                  overflow: \"hidden\",\n                  textOverflow: \"ellipsis\",\n                }}\n                variant={\"labels\"}\n              >\n                {children}\n              </Text>\n            </ToastPrimitive.Description>\n          </Grid>\n\n          {sideButtons}\n        </Grid>\n      </Grid>\n    </ToastVariants>\n  );\n};\n\nexport const Toast = ({\n  children,\n  variant,\n  icon,\n  onClose,\n  onCopy,\n}: {\n  onClose?: () => void;\n  onCopy?: () => void;\n  children: React.ReactNode;\n  variant?: ToastVariant;\n  icon?: React.ReactNode;\n}) => {\n  const sideButtons = (css: CSS) => (\n    <Flex css={css} wrap=\"wrapReverse\">\n      <Tooltip content={\"Copy to clipboard\"}>\n        <Button\n          css={{\n            \"&[data-state=auto]:hover\": {\n              background: \"rgba(0, 0, 0, 0.07)\",\n            },\n            \"&[data-state=auto]:active\": {\n              background: \"rgba(0, 0, 0, 0.04)\",\n            },\n          }}\n          color=\"ghost\"\n          onClick={() => {\n            onCopy?.();\n          }}\n          prefix={<CopyIcon />}\n        ></Button>\n      </Tooltip>\n      <Tooltip content={\"Close\"}>\n        <Button\n          css={{\n            \"&[data-state=auto]:hover\": {\n              background: \"rgba(0, 0, 0, 0.07)\",\n            },\n            \"&[data-state=auto]:active\": {\n              background: \"rgba(0, 0, 0, 0.04)\",\n            },\n          }}\n          color=\"ghost\"\n          onClick={() => {\n            onClose?.();\n          }}\n          prefix={<LargeXIcon />}\n        ></Button>\n      </Tooltip>\n    </Flex>\n  );\n\n  const buttonSize = Number.parseFloat(rawTheme.spacing[12]);\n  // Change the side button layout to vertical at a specific container size.\n  const layoutThreshold = `90px`;\n\n  /**\n   * Using `container-type: size` enables the @container (min-height) media query.\n   * This applies layout, style, and size containment to the container,\n   * meaning the element's size is computed in isolation, ignoring its child elements.\n   * We use `visibility: hidden` elements to set the container's size,\n   * placing all elements inside the same grid cell.\n   */\n  return (\n    <Grid\n      css={{\n        width: theme.spacing[33],\n      }}\n    >\n      {/* Toast with a horizontal layout for side buttons */}\n      <Box\n        css={{\n          gridColumn: \"1\",\n          gridRow: \"1\",\n          // Vertical button layout is applied after this height, so it must not affect the container height.\n          maxHeight: layoutThreshold,\n          overflow: \"hidden\",\n          // Element is used to set the container size.\n          visibility: \"hidden\",\n        }}\n      >\n        <InternalToast\n          variant={variant}\n          icon={icon}\n          sideButtons={sideButtons({})}\n        >\n          {children}\n        </InternalToast>\n      </Box>\n\n      {/* Toast with a vertical layout for side buttons. */}\n      <Box\n        css={{\n          gridColumn: \"1\",\n          gridRow: \"1\",\n          // Element is used to set the container size.\n          visibility: \"hidden\",\n        }}\n      >\n        <InternalToast\n          variant={variant}\n          icon={icon}\n          sideButtons={sideButtons({\n            width: \"min-content\",\n            // Ensure buttons do not affect the container height; only the content should.\n            maxHeight: buttonSize,\n          })}\n        >\n          {children}\n        </InternalToast>\n      </Box>\n\n      {/* Container with Toast */}\n      <Grid\n        css={{\n          gridColumn: \"1\",\n          gridRow: \"1\",\n          containerType: \"size\",\n        }}\n      >\n        <InternalToast\n          variant={variant}\n          icon={\n            icon ? (\n              <Box\n                css={{\n                  [`@container (min-height: ${layoutThreshold})`]: {\n                    alignSelf: \"start\",\n                  },\n                }}\n              >\n                {icon}\n              </Box>\n            ) : undefined\n          }\n          sideButtons={sideButtons({\n            [`@container (min-height: ${layoutThreshold})`]: {\n              width: \"min-content\",\n              alignSelf: \"start\",\n            },\n          })}\n        >\n          {children}\n        </InternalToast>\n      </Grid>\n    </Grid>\n  );\n};\n\nconst mapToVariant: Record<HotToast[\"type\"], ToastVariant> = {\n  success: \"success\",\n  error: \"error\",\n  loading: \"neutral\",\n  blank: \"neutral\",\n  custom: \"warning\",\n};\n\nexport const Toaster = () => {\n  const { toasts, handlers } = useToaster();\n  const { startPause, endPause } = handlers;\n\n  return (\n    <ToastPrimitive.ToastProvider>\n      {toasts.map((toastData) => {\n        const toastVariant = mapToVariant[toastData.type];\n        const children = resolveValue(toastData.message, toastData);\n\n        return (\n          <AnimatedToast\n            key={toastData.id}\n            onMouseEnter={startPause}\n            onMouseLeave={endPause}\n            duration={toastData.duration}\n          >\n            <Toast\n              variant={toastVariant}\n              onClose={() => {\n                hotToast.remove(toastData.id);\n              }}\n              onCopy={() => {\n                navigator.clipboard.writeText(children?.toString() ?? \"\");\n              }}\n              icon={toastData.icon}\n            >\n              {children}\n            </Toast>\n          </AnimatedToast>\n        );\n      })}\n      <StyledViewport />\n    </ToastPrimitive.ToastProvider>\n  );\n};\n\ntype Options = Pick<ToastOptions, \"duration\" | \"id\" | \"icon\">;\n\nexport const toast = {\n  info: (value: JSX.Element | string, options?: Options) =>\n    hotToast(value, options),\n  error: (value: JSX.Element | string, options?: Options) =>\n    hotToast.error(value, options),\n  warn: (value: JSX.Element | string, options?: Options) =>\n    hotToast.custom(value, options),\n  success: (value: JSX.Element | string, options?: Options) =>\n    hotToast.success(value, options),\n  dismiss: hotToast.dismiss,\n};\n"
  },
  {
    "path": "packages/design-system/src/components/toggle-button.stories.tsx",
    "content": "import { BorderRadiusIndividualIcon } from \"@webstudio-is/icons\";\nimport { ToggleButton as ToggleButtonComponent } from \"./toggle-button\";\nimport { StorySection, StoryGrid } from \"./storybook\";\n\nconst toggleButtonVariants = [\n  \"default\",\n  \"preset\",\n  \"local\",\n  \"overwritten\",\n  \"remote\",\n] as const;\n\nexport const ToggleButton = () => (\n  <>\n    <StorySection title=\"Variants\">\n      <StoryGrid horizontal>\n        {toggleButtonVariants.map((variant) => (\n          <ToggleButtonComponent key={variant} variant={variant}>\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleButtonComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Variants disabled\">\n      <StoryGrid horizontal>\n        {toggleButtonVariants.map((variant) => (\n          <ToggleButtonComponent key={variant} variant={variant} disabled>\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleButtonComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Variants on\">\n      <StoryGrid horizontal>\n        {toggleButtonVariants.map((variant) => (\n          <ToggleButtonComponent\n            key={variant}\n            variant={variant}\n            data-state=\"on\"\n          >\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleButtonComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Controlled pressed\">\n      <StoryGrid horizontal>\n        {toggleButtonVariants.map((variant) => (\n          <ToggleButtonComponent key={variant} variant={variant} pressed>\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleButtonComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Default pressed (uncontrolled)\">\n      <StoryGrid horizontal>\n        {toggleButtonVariants.map((variant) => (\n          <ToggleButtonComponent key={variant} variant={variant} defaultPressed>\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleButtonComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n\nexport default {\n  title: \"Toggle Button\",\n  parameters: {\n    // to make the variant=contrast visible\n    backgrounds: { default: \"Panel\" },\n  },\n};\n"
  },
  {
    "path": "packages/design-system/src/components/toggle-button.tsx",
    "content": "/**\n * Implementation of the \"Toggle Button\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=4-3199&t=lpT9jFuaiUnz1Foa-0\n */\n\nimport { forwardRef, type ComponentProps } from \"react\";\nimport type { CSS } from \"../stitches.config\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { IconButton } from \"./icon-button\";\n\ntype Props = {\n  variant?:\n    | \"default\"\n    | \"preset\"\n    | \"local\"\n    | \"overwritten\"\n    | \"remote\"\n    | undefined;\n  css?: CSS;\n} & ComponentProps<typeof TogglePrimitive.Root>;\n\nexport const ToggleButton = forwardRef<HTMLButtonElement, Props>(\n  ({ children, ...restProps }, ref) => {\n    return (\n      <TogglePrimitive.Root asChild ref={ref} {...restProps}>\n        <IconButton>{children}</IconButton>\n      </TogglePrimitive.Root>\n    );\n  }\n);\nToggleButton.displayName = \"ToggleButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/toggle-group.stories.tsx",
    "content": "import { BorderRadiusIndividualIcon } from \"@webstudio-is/icons\";\nimport {\n  ToggleGroup as ToggleGroupComponent,\n  ToggleGroupButton,\n} from \"./toggle-group\";\nimport { StorySection, StoryGrid } from \"./storybook\";\nimport { EnhancedTooltip, EnhancedTooltipProvider } from \"./enhanced-tooltip\";\n\nconst toggleGroupColors = [\n  \"default\",\n  \"preset\",\n  \"local\",\n  \"overwritten\",\n  \"remote\",\n] as const;\n\nconst ToggleGroupButtons = () => {\n  return (\n    <>\n      <ToggleGroupButton value=\"one\">\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n      <ToggleGroupButton value=\"two\" data-focused={true}>\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n      <ToggleGroupButton value=\"three\" data-hovered={true}>\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n      <ToggleGroupButton value=\"four\">\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n      <ToggleGroupButton\n        value=\"five\"\n        data-hovered={true}\n        data-focused={true}\n        aria-checked={true}\n      >\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n      <ToggleGroupButton value=\"six\">\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n      <ToggleGroupButton value=\"seven\">\n        <BorderRadiusIndividualIcon fill=\"currentColor\" />\n      </ToggleGroupButton>\n    </>\n  );\n};\n\nexport const ToggleGroup = () => (\n  <>\n    <StorySection title=\"Colors\">\n      <StoryGrid css={{ alignItems: \"flex-start\" }}>\n        {toggleGroupColors.map((color) => (\n          <ToggleGroupComponent\n            key={color}\n            color={color}\n            type=\"single\"\n            defaultValue=\"one\"\n          >\n            <ToggleGroupButtons />\n          </ToggleGroupComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Variants disabled\">\n      <StoryGrid css={{ alignItems: \"flex-start\" }}>\n        {toggleGroupColors.map((color) => (\n          <ToggleGroupComponent\n            key={color}\n            color={color}\n            type=\"single\"\n            defaultValue=\"one\"\n            disabled={true}\n          >\n            <ToggleGroupButtons />\n          </ToggleGroupComponent>\n        ))}\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"With text\">\n      <StoryGrid css={{ alignItems: \"flex-start\", flexGrow: 1 }}>\n        <EnhancedTooltipProvider>\n          <ToggleGroupComponent\n            color=\"default\"\n            type=\"single\"\n            defaultValue=\"one\"\n          >\n            <EnhancedTooltip content=\"One\">\n              <ToggleGroupButton value=\"one\">One</ToggleGroupButton>\n            </EnhancedTooltip>\n            <EnhancedTooltip content=\"Two\">\n              <ToggleGroupButton value=\"two\">Two</ToggleGroupButton>\n            </EnhancedTooltip>\n            <EnhancedTooltip content=\"Three\">\n              <ToggleGroupButton value=\"three\">Three</ToggleGroupButton>\n            </EnhancedTooltip>\n          </ToggleGroupComponent>\n        </EnhancedTooltipProvider>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"With tooltips\">\n      <StoryGrid css={{ alignItems: \"flex-start\" }}>\n        <EnhancedTooltipProvider>\n          <ToggleGroupComponent\n            color=\"default\"\n            type=\"single\"\n            defaultValue=\"one\"\n          >\n            <EnhancedTooltip content=\"One\">\n              <ToggleGroupButton value=\"one\">\n                <BorderRadiusIndividualIcon fill=\"currentColor\" />\n              </ToggleGroupButton>\n            </EnhancedTooltip>\n            <EnhancedTooltip content=\"Two\">\n              <ToggleGroupButton value=\"two\">\n                <BorderRadiusIndividualIcon fill=\"currentColor\" />\n              </ToggleGroupButton>\n            </EnhancedTooltip>\n            <EnhancedTooltip content=\"Three\">\n              <ToggleGroupButton value=\"three\">\n                <BorderRadiusIndividualIcon fill=\"currentColor\" />\n              </ToggleGroupButton>\n            </EnhancedTooltip>\n          </ToggleGroupComponent>\n        </EnhancedTooltipProvider>\n      </StoryGrid>\n    </StorySection>\n\n    <StorySection title=\"Multiple selection\">\n      <StoryGrid css={{ alignItems: \"flex-start\" }}>\n        <ToggleGroupComponent type=\"multiple\" defaultValue={[\"one\", \"three\"]}>\n          <ToggleGroupButton value=\"one\">\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleGroupButton>\n          <ToggleGroupButton value=\"two\">\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleGroupButton>\n          <ToggleGroupButton value=\"three\">\n            <BorderRadiusIndividualIcon fill=\"currentColor\" />\n          </ToggleGroupButton>\n        </ToggleGroupComponent>\n      </StoryGrid>\n    </StorySection>\n  </>\n);\n\nexport default {\n  title: \"Toggle Group\",\n  parameters: {\n    // to make the variant=contrast visible\n    backgrounds: { default: \"Panel\" },\n  },\n};\n"
  },
  {
    "path": "packages/design-system/src/components/toggle-group.tsx",
    "content": "/**\n * Implementation of the \"Toggle Group\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?type=design&node-id=4-2831&t=9qVuJbUcZqhAI06U-0\n */\n\nimport {\n  type ComponentProps,\n  type ElementRef,\n  createContext,\n  useContext,\n  forwardRef,\n} from \"react\";\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\";\nimport { styled, theme } from \"../stitches.config\";\nimport { IconButton } from \"./icon-button\";\nimport { textVariants } from \"./text\";\n\ntype Color = \"default\" | \"preset\" | \"local\" | \"remote\" | \"overwritten\";\n\nconst ToggleGroupContext = createContext<{\n  color?: Color;\n}>({});\n\ntype ToggleGroupProps = ComponentProps<\n  typeof ToggleGroupPrimitive.ToggleGroup\n> & {\n  color?: Color;\n};\n\nconst BaseToggleGroup = forwardRef<ElementRef<\"div\">, ToggleGroupProps>(\n  ({ color = \"default\", children, onValueChange, ...props }, ref) => {\n    return (\n      <ToggleGroupContext.Provider value={{ color }}>\n        <ToggleGroupPrimitive.ToggleGroup\n          ref={ref}\n          {...props}\n          onValueChange={(newValue: string | string[]) => {\n            // prevent unselecting buttons when only single can be selected\n            if (newValue !== \"\") {\n              onValueChange?.(newValue as string & string[]);\n            }\n          }}\n        >\n          {children}\n        </ToggleGroupPrimitive.ToggleGroup>\n      </ToggleGroupContext.Provider>\n    );\n  }\n);\n\nBaseToggleGroup.displayName = \"BaseToggleGroup\";\n\nexport const ToggleGroup = styled(BaseToggleGroup, {\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  flexDirection: \"row\",\n  alignItems: \"center\",\n  padding: 1,\n  background: theme.colors.backgroundControls,\n  border: `1px solid ${theme.colors.borderMain}`,\n  borderRadius: theme.borderRadius[4],\n});\n\nconst IconButtonStyled = styled(IconButton, {\n  \"&[data-focused=true], &:focus-visible\": {\n    // To not overlap focus-ring by the next button\n    zIndex: 0,\n    outline: `1px solid ${theme.colors.borderFocus}`,\n    outlineOffset: -1,\n  },\n  borderWidth: 0,\n  flexGrow: 1,\n});\n\nconst BaseToggleGroupButton = forwardRef<\n  ElementRef<\"button\">,\n  ComponentProps<typeof IconButton>\n>((props, ref) => {\n  const { color } = useContext(ToggleGroupContext);\n  return (\n    <IconButtonStyled\n      ref={ref}\n      {...props}\n      variant={\n        // default is unselected state\n        // when button is selected fallback to preset\n        props[\"aria-checked\"] === true\n          ? color === \"default\"\n            ? \"preset\"\n            : color\n          : \"default\"\n      }\n      css={{\n        height: theme.spacing[10],\n        ...textVariants.labels,\n      }}\n    />\n  );\n});\n\nBaseToggleGroupButton.displayName = \"BaseToggleGroupButton\";\n\ntype ToggleGroupButtonProps = ComponentProps<typeof ToggleGroupPrimitive.Item>;\n\nexport const ToggleGroupButton = forwardRef<\n  ElementRef<\"button\">,\n  ToggleGroupButtonProps\n>(({ children, ...props }, ref) => {\n  return (\n    <ToggleGroupPrimitive.Item ref={ref} {...props} asChild>\n      <BaseToggleGroupButton>{children}</BaseToggleGroupButton>\n    </ToggleGroupPrimitive.Item>\n  );\n});\n\nToggleGroupButton.displayName = \"ToggleGroupButton\";\n"
  },
  {
    "path": "packages/design-system/src/components/toolbar.stories.tsx",
    "content": "import { PlayIcon, ChevronDownIcon } from \"@webstudio-is/icons\";\nimport { theme } from \"../stitches.config\";\nimport {\n  Toolbar,\n  ToolbarButton,\n  ToolbarToggleItem,\n  ToolbarToggleGroup,\n  ToolbarSeparator,\n} from \"./toolbar\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Toolbar\",\n};\n\nexport const ToolbarDemo = () => (\n  <>\n    <StorySection title=\"Toggle groups\">\n      <Toolbar css={{ gap: theme.spacing[5] }}>\n        <ToolbarToggleGroup type=\"single\" value=\"2\">\n          <ToolbarToggleItem value=\"1\">\n            <PlayIcon size={22} />\n          </ToolbarToggleItem>\n          <ToolbarToggleItem value=\"2\">\n            <PlayIcon size={22} />\n          </ToolbarToggleItem>\n        </ToolbarToggleGroup>\n        <ToolbarSeparator />\n        <ToolbarToggleGroup type=\"single\" value=\"4\">\n          <ToolbarToggleItem value=\"3\" variant=\"subtle\">\n            <PlayIcon size={22} />\n          </ToolbarToggleItem>\n          <ToolbarToggleItem value=\"4\" variant=\"preview\">\n            <PlayIcon size={22} />\n          </ToolbarToggleItem>\n        </ToolbarToggleGroup>\n        <ToolbarSeparator />\n        <ToolbarToggleGroup type=\"single\">\n          <ToolbarToggleItem value=\"5\" focused>\n            <PlayIcon size={22} />\n          </ToolbarToggleItem>\n          <ToolbarToggleItem value=\"5\" variant=\"chevron\">\n            <ChevronDownIcon />\n          </ToolbarToggleItem>\n        </ToolbarToggleGroup>\n      </Toolbar>\n    </StorySection>\n    <StorySection title=\"Buttons\">\n      <Toolbar>\n        <ToolbarButton>\n          <PlayIcon size={22} />\n        </ToolbarButton>\n        <ToolbarButton variant=\"subtle\">\n          <PlayIcon size={22} />\n        </ToolbarButton>\n        <ToolbarButton variant=\"preview\">\n          <PlayIcon size={22} />\n        </ToolbarButton>\n        <ToolbarSeparator />\n        <ToolbarButton focused>\n          <PlayIcon size={22} />\n        </ToolbarButton>\n        <ToolbarButton variant=\"chevron\">\n          <ChevronDownIcon />\n        </ToolbarButton>\n      </Toolbar>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/toolbar.tsx",
    "content": "/**\n * Implementation of the \"Toolbar Toggle\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=1512%3A7422&t=BOWCrlme5NepfLUm-4\n */\nimport * as ToolbarPrimitive from \"@radix-ui/react-toolbar\";\nimport { Slot, type SlotProps } from \"@radix-ui/react-slot\";\nimport { css, styled, theme } from \"../stitches.config\";\nimport { separatorStyle } from \"./separator\";\nimport { textVariants } from \"./text\";\nimport { forwardRef, type Ref } from \"react\";\nimport { focusRingStyle } from \"./focus-ring\";\n\nexport const Toolbar = styled(ToolbarPrimitive.Root, {\n  display: \"flex\",\n  height: theme.spacing[15],\n  background: theme.colors.backgroundTopbar,\n  color: theme.colors.foregroundContrastMain,\n  alignItems: \"center\",\n  gap: theme.spacing[5],\n});\n\nexport const ToolbarToggleGroup = styled(ToolbarPrimitive.ToggleGroup, {\n  display: \"flex\",\n  alignItems: \"center\",\n});\n\nconst focusRing = focusRingStyle();\n\nconst toggleItemStyle = css(textVariants.labels, {\n  // reset styles\n  boxSizing: \"border-box\",\n  position: \"relative\",\n  py: 0,\n  px: theme.spacing[\"5\"],\n  appearance: \"none\",\n  border: \"none\",\n  outline: \"none\",\n  // center icon\n  display: \"flex\",\n  justifyContent: \"center\",\n  alignItems: \"center\",\n  // prevent shrinking inside flex box\n  flexShrink: 0,\n  // set size and shape\n  minWidth: theme.spacing[\"15\"],\n  minHeight: theme.spacing[\"15\"],\n\n  color: \"inherit\",\n  background: \"transparent\",\n  transition: \"200ms background\",\n\n  \"&:focus-visible\": focusRing,\n  \"&:hover, &[data-state=on], &[data-state=open], &[aria-checked=true]\": {\n    background: theme.colors.backgroundTopbarHover,\n  },\n  variants: {\n    // Just for story\n    focused: {\n      true: focusRing,\n    },\n    variant: {\n      subtle: {\n        color: theme.colors.foregroundTextMoreSubtle,\n        \"&:hover, &[data-state=on], &[aria-checked=true]\": {\n          color: \"inherit\",\n        },\n      },\n      preview: {\n        \"&[data-state=on]\": {\n          color: theme.colors.foregroundSuccess,\n        },\n      },\n      chevron: {\n        minWidth: \"auto\",\n        paddingInline: 0,\n        color: theme.colors.foregroundContrastSubtle,\n        \"&:hover, &:focus-visible, &[aria-expanded=true]\": {\n          color: theme.colors.foregroundContrastMain,\n        },\n        \"&:focus-visible\": focusRingStyle({ left: 0, right: 0 }),\n      },\n    },\n  },\n});\n\nexport const ToolbarToggleItem = styled(\n  ToolbarPrimitive.ToggleItem,\n  toggleItemStyle\n);\n\ntype ToolbarButtonProps = SlotProps & {\n  asChild?: boolean;\n};\n\nconst ToolbarButtonBase = forwardRef(\n  ({ asChild, ...props }: ToolbarButtonProps, ref: Ref<HTMLButtonElement>) => {\n    const Component = asChild ? Slot : \"button\";\n    return <Component {...props} ref={ref} />;\n  }\n);\nToolbarButtonBase.displayName = \"ToolbarButton\";\n\nexport const ToolbarButton = styled(ToolbarButtonBase, toggleItemStyle);\n\nexport const ToolbarSeparator = styled(\n  ToolbarPrimitive.Separator,\n  separatorStyle,\n  { background: theme.colors.borderDark }\n);\n"
  },
  {
    "path": "packages/design-system/src/components/tooltip.stories.tsx",
    "content": "import {\n  InputErrorsTooltip,\n  TooltipProvider,\n  Tooltip as TooltipDesign,\n} from \"./tooltip\";\nimport { Button } from \"./button\";\nimport { Box } from \"./box\";\nimport { Flex } from \"./flex\";\nimport { InputField } from \"./input-field\";\nimport { Text } from \"./text\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Tooltip\",\n};\n\nexport const Tooltip = () => (\n  <TooltipProvider>\n    <StorySection title=\"Tooltip\">\n      <Flex direction=\"column\" gap=\"6\" css={{ padding: 40 }}>\n        <Flex direction=\"column\" gap=\"2\">\n          <Text variant=\"labels\">Tooltip</Text>\n          <Flex gap=\"3\" align=\"center\">\n            <TooltipDesign content=\"HELLO\" open>\n              <Button>With tooltip</Button>\n            </TooltipDesign>\n            <TooltipDesign content={undefined}>\n              <Button>No tooltip content</Button>\n            </TooltipDesign>\n          </Flex>\n        </Flex>\n\n        <Flex direction=\"column\" gap=\"2\">\n          <Text variant=\"labels\">Input errors tooltip</Text>\n          <InputErrorsTooltip errors={[\"Error\"]} side=\"right\" open>\n            <InputField\n              id=\"input\"\n              placeholder=\"Input with error\"\n              css={{ width: 200 }}\n            />\n          </InputErrorsTooltip>\n        </Flex>\n\n        <Flex direction=\"column\" gap=\"2\">\n          <Text variant=\"labels\">Scrollable container with tooltips</Text>\n          <Box css={{ height: 100, width: 200, overflowY: \"scroll\" }}>\n            <Box css={{ height: 2000 }}>\n              <InputErrorsTooltip\n                errors={[\"Tooltip content\"]}\n                side=\"right\"\n                open={true}\n              >\n                <Button css={{ width: \"100%\", my: 10 }}>Tooltip 1</Button>\n              </InputErrorsTooltip>\n              <br />\n              <InputErrorsTooltip\n                errors={[\"Tooltip content\"]}\n                side=\"right\"\n                open={true}\n              >\n                <Button css={{ width: \"100%\", my: 10 }}>Tooltip 2</Button>\n              </InputErrorsTooltip>\n            </Box>\n          </Box>\n        </Flex>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Side placements\">\n      <Flex gap=\"6\" wrap=\"wrap\" css={{ padding: 60 }}>\n        <TooltipDesign content=\"Top tooltip\" side=\"top\" open>\n          <Button>Top</Button>\n        </TooltipDesign>\n        <TooltipDesign content=\"Right tooltip\" side=\"right\" open>\n          <Button>Right</Button>\n        </TooltipDesign>\n        <TooltipDesign content=\"Bottom tooltip\" side=\"bottom\" open>\n          <Button>Bottom</Button>\n        </TooltipDesign>\n        <TooltipDesign content=\"Left tooltip\" side=\"left\" open>\n          <Button>Left</Button>\n        </TooltipDesign>\n      </Flex>\n    </StorySection>\n\n    <StorySection title=\"Content variants\">\n      <Flex direction=\"column\" gap=\"6\" css={{ padding: 40 }}>\n        <Flex direction=\"column\" gap=\"2\">\n          <Text variant=\"labels\">Wrapped variant</Text>\n          <TooltipDesign\n            content=\"This is a tooltip with the wrapped variant that constrains the max width to a smaller size for compact content display\"\n            variant=\"wrapped\"\n            open\n          >\n            <Button>Wrapped</Button>\n          </TooltipDesign>\n        </Flex>\n        <Flex direction=\"column\" gap=\"2\">\n          <Text variant=\"labels\">Large variant</Text>\n          <TooltipDesign\n            content=\"This is a tooltip with the large variant that allows wider content before wrapping to the next line\"\n            variant=\"large\"\n            open\n          >\n            <Button>Large</Button>\n          </TooltipDesign>\n        </Flex>\n      </Flex>\n    </StorySection>\n  </TooltipProvider>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/tooltip.tsx",
    "content": "import type { Ref, ComponentProps, ReactNode, MouseEventHandler } from \"react\";\nimport { forwardRef, useEffect, useRef, useState } from \"react\";\nimport {\n  autoUpdate,\n  getOverflowAncestors,\n  type ReferenceElement,\n} from \"@floating-ui/dom\";\n\nimport { styled } from \"../stitches.config\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { Box } from \"./box\";\nimport { Text } from \"./text\";\nimport type { CSS } from \"../stitches.config\";\nimport { theme } from \"../stitches.config\";\n\nexport const TooltipProvider = TooltipPrimitive.TooltipProvider;\n\nexport type TooltipProps = ComponentProps<typeof TooltipPrimitive.Root> &\n  Omit<ComponentProps<typeof Content>, \"content\"> & {\n    children: ReactNode;\n    content: ReactNode;\n    delayDuration?: number;\n    disableHoverableContent?: boolean;\n    css?: CSS;\n  };\n\nconst Content = styled(TooltipPrimitive.Content, {\n  backgroundColor: theme.colors.backgroundTooltipMain,\n  color: theme.colors.foregroundContrastMain,\n  borderRadius: theme.borderRadius[7],\n  padding: theme.panel.padding,\n  position: \"relative\",\n\n  variants: {\n    variant: {\n      wrapped: {\n        maxWidth: theme.spacing[\"29\"],\n      },\n      large: {\n        maxWidth: theme.spacing[\"32\"],\n      },\n    },\n  },\n});\n\nconst Arrow = styled(TooltipPrimitive.Arrow, {\n  fill: theme.colors.backgroundTooltipMain,\n  marginTop: -0.5,\n});\n\nexport const Tooltip = forwardRef(\n  (\n    {\n      children,\n      content,\n      defaultOpen,\n      delayDuration,\n      disableHoverableContent,\n      open: openProp,\n      onOpenChange,\n      triggerProps,\n      ...props\n    }: TooltipProps & {\n      triggerProps?: ComponentProps<typeof TooltipPrimitive.Trigger>;\n    },\n    ref: Ref<HTMLDivElement>\n  ) => {\n    const triggerRef = useRef<HTMLButtonElement>(null);\n    // We need to intercept tooltip open\n    const [open, setOpen] = useControllableState({\n      prop: openProp,\n      defaultProp: defaultOpen ?? false,\n      onChange: (open) => {\n        onOpenChange?.(open);\n      },\n    });\n    const preventCloseRef = useRef(false);\n\n    /**\n     * When the mouse leaves Tooltip.Content and hovers over an iframe, the Radix Tooltip stays open.\n     * This occurs because Radix's grace area depends on the pointermove event, which iframes don't trigger.\n     *\n     * Two possible workarounds:\n     * 1. Set pointer-events: none on the canvas when the tooltip is open and content is hovered.\n     *    (This doesn't work well in Chrome, as scrolling stops working on elements hovered with pointer-events: none,\n     *    even after removing pointer-events.)\n     * 2. Close the tooltip on onMouseLeave.\n     *    (This breaks some grace area behavior, such as closing the tooltip when moving the mouse from the content to the trigger.)\n     *\n     * The simpler solution with fewer side effects is to close the tooltip on mouse leave.\n     */\n    const handleMouseLeave: MouseEventHandler<HTMLDivElement> = (event) => {\n      setOpen(false);\n      props.onMouseLeave?.(event);\n    };\n\n    const handleOpenChange = (open: boolean) => {\n      if (open === false && preventCloseRef.current) {\n        return;\n      }\n\n      setOpen(open);\n    };\n\n    // There's no way to prevent a rendered trigger from opening.\n    // This causes delay issues when an invisible tooltip forces other tooltips to show immediately.\n    return content == null ? (\n      children\n    ) : (\n      <TooltipPrimitive.Root\n        open={open}\n        defaultOpen={defaultOpen}\n        onOpenChange={handleOpenChange}\n        delayDuration={delayDuration}\n        disableHoverableContent={disableHoverableContent}\n      >\n        <TooltipPrimitive.Trigger\n          asChild\n          ref={triggerRef}\n          {...triggerProps}\n          onFocus={(event) => {\n            // Prevent the tooltip from opening on focus\n            // The main issue is that after dialogs or selects, the tooltip button is autofocused and causes the tooltip to open\n            event.preventDefault();\n          }}\n          onPointerMove={(event) => {\n            // The tooltip captures pointer events, which can be an issue when the tooltip trigger is also the popover trigger.\n            // This is related to Popover.Anchor, but sometimes it can't be placed above the tooltip trigger.\n            // To prevent pointer movements from affecting the tooltip, we check if there’s an element between the target and the current dialog.\n            let currentElement =\n              event.target instanceof Element ? event.target : null;\n\n            while (\n              currentElement !== null &&\n              currentElement !== event.currentTarget\n            ) {\n              if (currentElement.getAttribute(\"role\") === \"dialog\") {\n                event.preventDefault();\n                break;\n              }\n\n              currentElement = currentElement.parentElement;\n            }\n          }}\n        >\n          {children}\n        </TooltipPrimitive.Trigger>\n        {content != null && (\n          <TooltipPrimitive.Portal>\n            <Content\n              ref={ref}\n              side=\"top\"\n              align=\"center\"\n              sideOffset={2}\n              collisionPadding={8}\n              arrowPadding={8}\n              {...props}\n              onMouseLeave={handleMouseLeave}\n              onPointerDown={() => {\n                // Allows clicking on links or selecting text inside the tooltip.\n                // Prevent closing tooltip on content click.\n                // Can't use preventDefault() because it will prevent selecting code for copy/paste.\n                preventCloseRef.current = true;\n                requestAnimationFrame(() => {\n                  preventCloseRef.current = false;\n                });\n              }}\n            >\n              {typeof content === \"string\" ? <Text>{content}</Text> : content}\n              <Box css={{ color: \"transparent\" }}>\n                <Arrow offset={5} width={11} height={5} />\n              </Box>\n            </Content>\n          </TooltipPrimitive.Portal>\n        )}\n      </TooltipPrimitive.Root>\n    );\n  }\n);\n\nconst isReferenceElement = (value: unknown): value is ReferenceElement => {\n  return value instanceof Element || value instanceof Window;\n};\n\nTooltip.displayName = \"Tooltip\";\n\nexport const InputErrorsTooltip = ({\n  errors,\n  children,\n  side,\n  css,\n  ...rest\n}: Omit<TooltipProps, \"content\"> & {\n  errors?: string[];\n  children: ComponentProps<typeof Tooltip>[\"children\"];\n  side?: ComponentProps<typeof Tooltip>[\"side\"];\n}) => {\n  const content = errors?.map((error, index) => (\n    <Text key={index}>{error}</Text>\n  ));\n\n  const ref = useRef<HTMLDivElement>(null);\n  // Use collision boundary to hide tooltips if original element out of visible area in the scroll viewport\n  const [collisionBoundary, setCollisionBoundary] = useState<\n    | {\n        x: number;\n        y: number;\n        width: number;\n        height: number;\n      }\n    | undefined\n  >(undefined);\n\n  useEffect(() => {\n    if (ref.current != null) {\n      const ancestors = getOverflowAncestors(ref.current, [], false);\n      if (ancestors.length === 2) {\n        // Only window and window viewport - do nothing\n        return;\n      }\n\n      const nearestScrollableElement = ancestors[0];\n\n      if (\n        nearestScrollableElement instanceof HTMLElement &&\n        isReferenceElement(ancestors[1])\n      ) {\n        // Track collision boundary size/position changes\n        const cleanup = autoUpdate(\n          ancestors[1],\n          nearestScrollableElement,\n          () => {\n            const rect = nearestScrollableElement.getBoundingClientRect();\n\n            setCollisionBoundary((prev) => {\n              const newY = rect.y;\n              const newHeight = rect.height;\n\n              if (prev?.y === newY && prev.height === newHeight) {\n                return prev;\n              }\n\n              const next = {\n                x: 0,\n                width: window.visualViewport?.width ?? 100000,\n                y: newY,\n                height: newHeight,\n              };\n\n              return next;\n            });\n          }\n        );\n\n        return cleanup;\n      }\n    }\n  }, []);\n\n  // Wrap the error tooltip with its own provider to avoid logic intersection with ordinary tooltips.\n  // This is especially important for hover delays.\n  // Here we ensure that hovering over the tooltip trigger after any input will not show the tooltip immediately.\n  // --\n  // Additionally, we can't wrap the underlying Input with Tooltip because we are not rendering Tooltips in case of no errors.\n  // This causes a full re-render of the Input and loss of focus.\n  // Because of that, we are wrapping the Tooltip with the relative Box component and providing an invisible trigger for the Tooltip.\n  return (\n    <TooltipProvider>\n      <Box ref={ref as never} css={{ display: \"contents\" }}></Box>\n      <Box css={{ position: \"relative\" }}>\n        <Tooltip\n          {...rest}\n          collisionBoundary={collisionBoundary as never}\n          collisionPadding={-8}\n          hideWhenDetached={true}\n          content={\n            errors !== undefined && errors.length !== 0\n              ? (content ?? \" \")\n              : undefined\n          }\n          open={errors !== undefined && errors.length !== 0}\n          side={side ?? \"right\"}\n          css={css}\n        >\n          <Box\n            css={{\n              position: \"absolute\",\n              inset: 0,\n              visibility: \"hidden\",\n              // Uncomment for debugging\n              // backgroundColor: \"red\",\n              // opacity: 0.3,\n              // pointerEvents: \"none\",\n            }}\n          ></Box>\n        </Tooltip>\n        {children}\n      </Box>\n    </TooltipProvider>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/tree.stories.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport { EllipsesIcon, DotIcon } from \"@webstudio-is/icons\";\nimport { SmallIconButton } from \"./small-icon-button\";\nimport { Box } from \"./box\";\nimport {\n  TreeRoot,\n  TreeNode,\n  TreeNodeLabel,\n  TreeSortableItem,\n  type TreeDropTarget,\n} from \"./tree\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Tree\",\n};\n\ntype Node = {\n  name: string;\n  children: Node[];\n};\n\ntype FlatNode = {\n  name: string;\n  selector: string[];\n  level: number;\n  isExpanded?: boolean;\n  isLastChild: boolean;\n  dropTarget?: TreeDropTarget;\n};\n\nconst findNode = (data: Node, name?: string) => {\n  let matched: undefined | Node;\n  const traverse = (node: Node) => {\n    if (node.name === name) {\n      matched = node;\n      return;\n    }\n    for (const child of node.children) {\n      traverse(child);\n    }\n  };\n  traverse(data);\n  return matched;\n};\n\nconst move = ({\n  data,\n  sourceParentName,\n  sourceName,\n  dropTarget,\n}: {\n  data: Node;\n  sourceParentName: string;\n  sourceName: string;\n  dropTarget: DropTarget;\n}) => {\n  const newData = structuredClone(data);\n  const sourceParentNode = findNode(newData, sourceParentName);\n  const sourceNode = findNode(newData, sourceName);\n  const targetParentNode = findNode(newData, dropTarget.parentName);\n  const targetBeforeNode = findNode(newData, dropTarget.beforeName);\n  const targetAfterNode = findNode(newData, dropTarget.afterName);\n  if (sourceParentNode && sourceNode) {\n    const index = sourceParentNode.children.indexOf(sourceNode);\n    sourceParentNode.children.splice(index, 1);\n  }\n  if (targetParentNode && sourceNode) {\n    if (targetBeforeNode) {\n      const index = targetParentNode.children.indexOf(targetBeforeNode);\n      targetParentNode.children.splice(index, 0, sourceNode);\n    } else if (targetAfterNode) {\n      const index = targetParentNode.children.indexOf(targetAfterNode) + 1;\n      targetParentNode.children.splice(index, 0, sourceNode);\n    } else {\n      targetParentNode.children.push(sourceNode);\n    }\n  }\n  return newData;\n};\n\nconst initialData: Node = {\n  name: \"Root\",\n  children: Array.from(Array(10)).map((_, index) => ({\n    name: `Parent ${index + 1}`,\n    children: [\n      {\n        name: `Box ${index + 1}.1`,\n        children: [\n          { name: `Box ${index + 1}.2`, children: [] },\n          { name: `Container ${index + 1}.1`, children: [] },\n          { name: `Wrapper ${index + 1}.1`, children: [] },\n        ],\n      },\n      {\n        name: `Container ${index + 1}.2`,\n        children: [\n          { name: `Box ${index + 1}.3`, children: [] },\n          { name: `Container ${index + 1}.3`, children: [] },\n          { name: `Wrapper ${index + 1}.2`, children: [] },\n        ],\n      },\n      {\n        name: `Wrapper ${index + 1}.3`,\n        children: [\n          { name: `Box ${index + 1}.4`, children: [] },\n          { name: `Container ${index + 1}.4`, children: [] },\n          { name: `Wrapper ${index + 1}.4`, children: [] },\n        ],\n      },\n    ],\n  })),\n};\n\ntype DropTarget = {\n  parentName: string;\n  beforeName?: string;\n  afterName?: string;\n};\n\nconst getStoriesDropTarget = (\n  node: FlatNode,\n  dropTarget: undefined | TreeDropTarget\n): undefined | DropTarget => {\n  if (dropTarget === undefined) {\n    return;\n  }\n  const parentName = node.selector.at(-dropTarget.parentLevel - 1);\n  const beforeName =\n    dropTarget.beforeLevel === undefined\n      ? undefined\n      : node.selector.at(-dropTarget.beforeLevel - 1);\n  const afterName =\n    dropTarget.afterLevel === undefined\n      ? undefined\n      : node.selector.at(-dropTarget.afterLevel - 1);\n  if (parentName) {\n    return { parentName, beforeName, afterName };\n  }\n};\n\nexport const Tree = () => {\n  const [selectedItemId, setSelectedItemId] = useState(\"\");\n  const [expandedItems, setExpandedItems] = useState(new Set<string>());\n  const [data, setData] = useState(initialData);\n  const [dropTarget, setDropTarget] = useState<undefined | DropTarget>();\n  const flatTree = useMemo(() => {\n    const flatTree: FlatNode[] = [];\n    const traverse = (\n      node: Node,\n      selector: string[],\n      isLastChild = false,\n      level = 0\n    ) => {\n      let isExpanded;\n      if (level > 0 && node.children.length > 0) {\n        isExpanded = expandedItems.has(node.name);\n      }\n      // hide root folder\n      const flatNode: FlatNode = {\n        name: node.name,\n        selector,\n        level,\n        isExpanded,\n        isLastChild,\n      };\n      flatTree.push(flatNode);\n      let lastFlatNode = flatNode;\n      if (level === 0 || isExpanded) {\n        for (const child of node.children) {\n          const isLastChild = node.children.at(-1) === child;\n          lastFlatNode = traverse(\n            child,\n            [child.name, ...selector],\n            isLastChild,\n            level + 1\n          );\n        }\n      }\n      if (dropTarget?.beforeName === node.name) {\n        flatNode.dropTarget = {\n          parentLevel: level - 1,\n          beforeLevel: level,\n        };\n      }\n      if (dropTarget?.afterName === node.name) {\n        lastFlatNode.dropTarget = {\n          parentLevel: level - 1,\n          afterLevel: level,\n        };\n      }\n      return lastFlatNode;\n    };\n    traverse(data, [data.name], false);\n    return flatTree;\n  }, [data, expandedItems, dropTarget]);\n\n  return (\n    <>\n      <StorySection title=\"Interactive\">\n        <Box css={{ maxWidth: 300 }}>\n          <TreeRoot>\n            {flatTree.map((node) => {\n              const handleExpand = (isExpanded: boolean) => {\n                setExpandedItems((prevExpandedItems) => {\n                  const newExpandedItems = new Set(prevExpandedItems);\n                  if (isExpanded) {\n                    newExpandedItems.add(node.name);\n                  } else {\n                    newExpandedItems.delete(node.name);\n                  }\n                  return newExpandedItems;\n                });\n              };\n\n              return (\n                <TreeSortableItem\n                  key={node.name}\n                  level={node.level}\n                  isExpanded={node.isExpanded}\n                  isLastChild={node.isLastChild}\n                  data={node}\n                  // prevent dragging root\n                  canDrag={() => node.level > 0}\n                  onExpand={handleExpand}\n                  dropTarget={node.dropTarget}\n                  onDropTargetChange={(dropTarget) => {\n                    // prevent dropping into toplevel\n                    if (dropTarget && dropTarget.parentLevel > 0) {\n                      setDropTarget(getStoriesDropTarget(node, dropTarget));\n                    } else {\n                      setDropTarget(undefined);\n                    }\n                  }}\n                  onDrop={(sourceNode) => {\n                    if (dropTarget) {\n                      setData((data) =>\n                        move({\n                          data,\n                          sourceParentName: sourceNode.selector[1],\n                          sourceName: sourceNode.selector[0],\n                          dropTarget,\n                        })\n                      );\n                    }\n                    setDropTarget(undefined);\n                  }}\n                >\n                  <TreeNode\n                    level={node.level}\n                    isSelected={node.name === selectedItemId}\n                    isHighlighted={dropTarget?.parentName === node.name}\n                    isExpanded={node.isExpanded}\n                    onExpand={handleExpand}\n                    buttonProps={{\n                      onFocus: () => {\n                        setSelectedItemId(node.name);\n                      },\n                      onClick: () => {\n                        setSelectedItemId(node.name);\n                      },\n                    }}\n                    action={\n                      <SmallIconButton tabIndex={-1} icon={<EllipsesIcon />} />\n                    }\n                  >\n                    <TreeNodeLabel>{node.name}</TreeNodeLabel>\n                  </TreeNode>\n                </TreeSortableItem>\n              );\n            })}\n          </TreeRoot>\n        </Box>\n      </StorySection>\n\n      <StorySection title=\"Static nodes\">\n        <div style={{ maxWidth: 300 }}>\n          <TreeRoot>\n            <TreeNode\n              level={0}\n              isSelected={false}\n              isExpanded={true}\n              onExpand={() => {}}\n              buttonProps={{}}\n              action={<SmallIconButton tabIndex={-1} icon={<EllipsesIcon />} />}\n            >\n              <TreeNodeLabel>Root node</TreeNodeLabel>\n            </TreeNode>\n            <TreeNode\n              level={1}\n              isSelected={true}\n              isExpanded={undefined}\n              isActionVisible\n              buttonProps={{}}\n              action={<SmallIconButton tabIndex={-1} icon={<EllipsesIcon />} />}\n            >\n              <TreeNodeLabel prefix={<DotIcon />}>\n                With prefix and action visible\n              </TreeNodeLabel>\n            </TreeNode>\n            <TreeNode\n              level={1}\n              tabbable\n              isSelected={false}\n              isExpanded={false}\n              onExpand={() => {}}\n              buttonProps={{}}\n              action={<SmallIconButton tabIndex={-1} icon={<EllipsesIcon />} />}\n            >\n              <TreeNodeLabel>Tabbable collapsed node</TreeNodeLabel>\n            </TreeNode>\n            <TreeNode\n              level={1}\n              isSelected={false}\n              isHighlighted\n              isExpanded={undefined}\n              buttonProps={{}}\n              action={<SmallIconButton tabIndex={-1} icon={<EllipsesIcon />} />}\n            >\n              <TreeNodeLabel>Highlighted node</TreeNodeLabel>\n            </TreeNode>\n          </TreeRoot>\n        </div>\n      </StorySection>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/tree.tsx",
    "content": "import {\n  useEffect,\n  useInsertionEffect,\n  useRef,\n  useState,\n  type ComponentPropsWithoutRef,\n  type KeyboardEvent,\n  type ReactNode,\n} from \"react\";\nimport { FocusScope, useFocusManager } from \"@react-aria/focus\";\nimport { combine } from \"@atlaskit/pragmatic-drag-and-drop/combine\";\nimport {\n  draggable,\n  dropTargetForElements,\n} from \"@atlaskit/pragmatic-drag-and-drop/element/adapter\";\nimport { disableNativeDragPreview } from \"@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview\";\nimport {\n  attachInstruction,\n  extractInstruction,\n  type Instruction,\n  type ItemMode,\n} from \"@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item\";\nimport { autoScrollWindowForElements } from \"@atlaskit/pragmatic-drag-and-drop-auto-scroll/element\";\nimport { ChevronDownIcon, ChevronRightIcon } from \"@webstudio-is/icons\";\nimport { styled, theme } from \"../stitches.config\";\nimport { Box } from \"./box\";\nimport { Text } from \"./text\";\nimport { TreePositionIndicator } from \"./list-position-indicator\";\n\nconst treeNodeLevel = \"--tree-node-level\";\nconst treeNodeOutline = \"--tree-node-outline\";\nconst treeNodeBackgroundColor = \"--tree-node-background-color\";\nconst treeActionOpacity = \"--tree-action-opacity\";\nconst treeDepthBarsVisibility = \"--tree-depth-bars-visibility\";\nconst treeDepthBarsColor = \"--tree-depth-bars-color\";\n\nconst ITEM_PADDING_LEFT = 8;\n// extra padding on the right to make sure scrollbar doesn't obscure anything\nconst ITEM_PADDING_RIGHT = 10;\nconst BARS_GAP = 16;\nconst EXPAND_WIDTH = 24;\n\nconst TreeContainer = ({ children }: { children: ReactNode }) => {\n  const focusManager = useFocusManager();\n  return (\n    <Box\n      css={{\n        \"&:hover\": {\n          [treeDepthBarsVisibility]: \"visible\",\n        },\n      }}\n      onKeyDown={(event) => {\n        if (event.defaultPrevented) {\n          return;\n        }\n        if (event.key === \"ArrowUp\" || event.key === \"ArrowLeft\") {\n          focusManager?.focusPrevious({\n            accept: (node) => node.hasAttribute(\"data-tree-button\"),\n          });\n          // prevent scrolling\n          event.preventDefault();\n        }\n        if (event.key === \"ArrowDown\") {\n          focusManager?.focusNext({\n            accept: (node) => node.hasAttribute(\"data-tree-button\"),\n          });\n          // prevent scrolling\n          event.preventDefault();\n        }\n        if (event.key === \"ArrowRight\") {\n          focusManager?.focusNext({\n            accept: (node) =>\n              node.hasAttribute(\"data-tree-button\") ||\n              // try to focus button inside action\n              node.closest(\"[data-tree-action]\") !== null,\n          });\n          // prevent scrolling\n          event.preventDefault();\n        }\n      }}\n    >\n      {children}\n    </Box>\n  );\n};\n\nexport const TreeRoot = ({ children }: { children: ReactNode }) => {\n  return (\n    <FocusScope>\n      <TreeContainer>{children}</TreeContainer>\n    </FocusScope>\n  );\n};\n\nconst NodeContainer = styled(\"div\", {\n  position: \"relative\",\n  height: theme.sizes.controlHeight,\n  \"&:hover, &:has(:focus-visible), &:has([aria-current=true])\": {\n    [treeNodeBackgroundColor]: theme.colors.backgroundHover,\n    backgroundColor: `var(${treeNodeBackgroundColor})`,\n    [treeActionOpacity]: 1,\n  },\n  \"&:has([aria-selected=true])\": {\n    [treeNodeBackgroundColor]: theme.colors.backgroundItemCurrent,\n    backgroundColor: `var(${treeNodeBackgroundColor})`,\n    [treeDepthBarsColor]: theme.colors.borderItemChildLineCurrent,\n  },\n});\n\nconst DepthBars = styled(\"div\", {\n  visibility: `var(${treeDepthBarsVisibility}, hidden)`,\n  position: \"absolute\",\n  top: 0,\n  left: 0,\n  width: `calc((var(${treeNodeLevel}) - 1) * ${BARS_GAP}px)`,\n  height: \"100%\",\n  backgroundImage: `repeating-linear-gradient(\n    to right,\n    transparent,\n    transparent ${BARS_GAP - 1}px,\n    var(${treeDepthBarsColor}, ${theme.colors.borderItemChildLine}) ${BARS_GAP - 1}px,\n    var(${treeDepthBarsColor}, ${theme.colors.borderItemChildLine}) ${BARS_GAP}px\n  )`,\n});\n\nconst NodeButton = styled(\"button\", {\n  all: \"unset\",\n  boxSizing: \"border-box\",\n  display: \"flex\",\n  alignItems: \"center\",\n  userSelect: \"none\",\n  width: \"100%\",\n  height: \"inherit\",\n  minWidth: 0,\n  paddingLeft: `calc(${ITEM_PADDING_LEFT}px + var(${treeNodeLevel}) * 16px)`,\n  paddingRight: ITEM_PADDING_RIGHT,\n  flexBasis: 0,\n  flexGrow: 1,\n  position: \"relative\",\n});\n\nconst ExpandButton = styled(\"button\", {\n  all: \"unset\",\n  display: \"flex\",\n  justifyContent: \"center\",\n  alignItems: \"center\",\n  position: \"absolute\",\n  top: 0,\n  left: `calc(var(${treeNodeLevel}) * ${BARS_GAP}px - ${EXPAND_WIDTH / 2}px)`,\n  width: EXPAND_WIDTH,\n  height: \"inherit\",\n});\n\nconst ActionContainer = styled(\"div\", {\n  // use opacity to hide action instead of visibility\n  // to prevent focus loss while navigating with keyboard\n  opacity: `var(${treeActionOpacity}, 0)`,\n  position: \"sticky\",\n  translate: `-100% -100%`,\n  left: `calc(var(--sidebar-left-panel-width) - ${ITEM_PADDING_RIGHT}px)`,\n  height: \"inherit\",\n  display: \"inline-flex\",\n  justifyContent: \"center\",\n  alignItems: \"center\",\n  backgroundColor: `var(${treeNodeBackgroundColor})`,\n});\n\nconst DropIndicator = ({\n  instruction,\n}: {\n  instruction: undefined | Instruction;\n}) => {\n  if (instruction?.type === \"reorder-above\") {\n    const indent = instruction.currentLevel * instruction.indentPerLevel;\n    return (\n      <TreePositionIndicator\n        x={indent}\n        y=\"0\"\n        length={`calc(100% - ${indent}px)`}\n      />\n    );\n  }\n  if (instruction?.type === \"reorder-below\") {\n    const indent = instruction.currentLevel * instruction.indentPerLevel;\n    return (\n      <TreePositionIndicator\n        x={indent}\n        y=\"100%\"\n        length={`calc(100% - ${indent}px)`}\n      />\n    );\n  }\n  if (instruction?.type === \"reparent\") {\n    const indent = instruction.desiredLevel * instruction.indentPerLevel;\n    return (\n      <TreePositionIndicator\n        x={indent}\n        y=\"100%\"\n        length={`calc(100% - ${indent}px)`}\n      />\n    );\n  }\n};\n\nexport type TreeDropTarget = {\n  parentLevel: number;\n  beforeLevel?: number;\n  afterLevel?: number;\n};\n\nconst getTreeDropTarget = (\n  instruction: null | Instruction\n): undefined | TreeDropTarget => {\n  if (instruction?.type === \"make-child\") {\n    const afterLevel = instruction.currentLevel + 1;\n    return { parentLevel: afterLevel - 1, afterLevel };\n  }\n  if (instruction?.type === \"reorder-below\") {\n    const afterLevel = instruction.currentLevel;\n    return { parentLevel: afterLevel - 1, afterLevel };\n  }\n  if (instruction?.type === \"reorder-above\") {\n    const beforeLevel = instruction.currentLevel;\n    return { parentLevel: beforeLevel - 1, beforeLevel };\n  }\n  if (instruction?.type === \"reparent\") {\n    const afterLevel = instruction.desiredLevel;\n    return { parentLevel: afterLevel - 1, afterLevel };\n  }\n};\n\nconst getInstruction = (\n  treeDropTarget: undefined | TreeDropTarget\n): undefined | Instruction => {\n  if (treeDropTarget?.beforeLevel !== undefined) {\n    return {\n      type: \"reorder-above\",\n      currentLevel: treeDropTarget.beforeLevel,\n      indentPerLevel: BARS_GAP,\n    };\n  }\n  if (treeDropTarget?.afterLevel !== undefined) {\n    return {\n      type: \"reorder-below\",\n      currentLevel: treeDropTarget.afterLevel,\n      indentPerLevel: BARS_GAP,\n    };\n  }\n};\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\nconst useCallbackRef = <Fn extends Function>(fn: Fn) => {\n  const ref = useRef(fn);\n  useInsertionEffect(() => {\n    ref.current = fn;\n  });\n  return ref;\n};\n\nexport const TreeSortableItem = <Data,>({\n  level,\n  isExpanded,\n  isLastChild,\n  data,\n  canDrag,\n  dropTarget,\n  onDropTargetChange,\n  onDrop,\n  onExpand,\n  children,\n}: {\n  level: number;\n  isExpanded: undefined | boolean;\n  isLastChild: boolean;\n  data: Data;\n  canDrag: () => boolean;\n  dropTarget: undefined | TreeDropTarget;\n  onDropTargetChange: (\n    dropTarget: undefined | TreeDropTarget,\n    draggingData: Data\n  ) => void;\n  onDrop: (data: Data) => void;\n  onExpand: (isExpanded: boolean) => void;\n  children: ReactNode;\n}) => {\n  const elementRef = useRef<HTMLDivElement>(null);\n  const [isDragging, setIsDragging] = useState(false);\n  const [isDropOver, setIsDropOver] = useState(false);\n  const handleDropTargetChange = useCallbackRef(onDropTargetChange);\n  const handleDrop = useCallbackRef(onDrop);\n  const handleExpand = useCallbackRef(onExpand);\n  const handleCanDrag = useCallbackRef(canDrag);\n  const expandTimeout = useRef<undefined | number>(undefined);\n\n  useEffect(() => {\n    if (elementRef.current === null) {\n      return;\n    }\n    return combine(\n      draggable({\n        element: elementRef.current,\n        onGenerateDragPreview: ({ nativeSetDragImage }) => {\n          disableNativeDragPreview({ nativeSetDragImage });\n        },\n        getInitialData: () => ({\n          itemData: data,\n        }),\n        canDrag: () => handleCanDrag.current(),\n        onDrag: () => {\n          setIsDragging(true);\n        },\n        onDrop: () => {\n          setIsDragging(false);\n        },\n      }),\n      dropTargetForElements({\n        element: elementRef.current,\n        getData: ({ input, element }) => {\n          // this will 'attach' the instruction to your `data` object\n          let mode: ItemMode = \"standard\";\n          if (isLastChild) {\n            mode = \"last-in-group\";\n          }\n          if (isExpanded) {\n            mode = \"expanded\";\n          }\n          return attachInstruction(\n            {},\n            {\n              input,\n              element,\n              currentLevel: level,\n              indentPerLevel: BARS_GAP,\n              mode,\n            }\n          );\n        },\n        onDrag: (args) => {\n          const instruction = extractInstruction(args.self.data);\n          const dropTarget = getTreeDropTarget(instruction);\n          const draggingData = args.source.data.itemData as Data;\n          if (dropTarget) {\n            handleDropTargetChange.current(dropTarget, draggingData);\n          }\n        },\n        onDragEnter: () => {\n          // timeout in browser use only number as timeout id\n          window.clearTimeout(expandTimeout.current);\n          expandTimeout.current = window.setTimeout(() => {\n            handleExpand.current(true);\n          }, 1000);\n          setIsDropOver(true);\n        },\n        onDragLeave: (args) => {\n          window.clearTimeout(expandTimeout.current);\n          setIsDropOver(false);\n          const draggingData = args.source.data.itemData as Data;\n          handleDropTargetChange.current(undefined, draggingData);\n        },\n        onDrop: (args) => {\n          const draggingData = args.source.data.itemData as Data;\n          window.clearTimeout(expandTimeout.current);\n          handleDrop.current(draggingData);\n          setIsDropOver(false);\n        },\n      }),\n      autoScrollWindowForElements()\n    );\n  }, [\n    level,\n    isExpanded,\n    isLastChild,\n    data,\n    handleCanDrag,\n    handleDrop,\n    handleDropTargetChange,\n    handleExpand,\n  ]);\n\n  return (\n    <Box\n      ref={elementRef}\n      data-tree-sortable-item\n      data-level={level}\n      data-is-dragging={isDragging}\n      data-is-drop-over={isDropOver}\n      css={{\n        position: \"relative\",\n        \"&[data-is-drop-over=true]\": {\n          zIndex: 1,\n        },\n        \"&[data-is-dragging=true]\": {\n          [treeNodeOutline]: \"none\",\n        },\n      }}\n    >\n      {children}\n      <DropIndicator instruction={getInstruction(dropTarget)} />\n    </Box>\n  );\n};\n\nexport const TreeNode = ({\n  level,\n  tabbable,\n  isSelected,\n  isHighlighted,\n  isExpanded,\n  isActionVisible,\n  onExpand,\n  nodeProps,\n  buttonProps,\n  action,\n  children,\n}: {\n  level: number;\n  tabbable?: boolean;\n  isSelected: boolean;\n  isHighlighted?: boolean;\n  isExpanded?: undefined | boolean;\n  isActionVisible?: boolean;\n  onExpand?: (expanded: boolean, all: boolean) => void;\n  nodeProps?: ComponentPropsWithoutRef<\"div\">;\n  buttonProps: ComponentPropsWithoutRef<\"button\">;\n  action: ReactNode;\n  children: ReactNode;\n}) => {\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  // scroll the selected button into view when selected from canvas.\n  useEffect(() => {\n    if (isSelected) {\n      buttonRef.current?.scrollIntoView({\n        // smooth behavior in both canvas and navigator confuses chrome\n        behavior: \"auto\",\n        block: \"nearest\",\n      });\n    }\n  }, [isSelected]);\n\n  const handleKeydown = (event: KeyboardEvent<HTMLDivElement>) => {\n    nodeProps?.onKeyDown?.(event);\n\n    if (event.defaultPrevented) {\n      return;\n    }\n    if (event.key === \"ArrowLeft\" && isExpanded === true) {\n      onExpand?.(false, event.altKey);\n      // allow to collapse and then navigate to previous node\n      event.preventDefault();\n    }\n    if (event.key === \"ArrowRight\" && isExpanded === false) {\n      onExpand?.(true, event.altKey);\n      // allow to expand and then navigate to next node\n      event.preventDefault();\n    }\n    if (event.key === \" \") {\n      onExpand?.(isExpanded === false, event.altKey);\n      // prevent scrolling\n      event.preventDefault();\n    }\n  };\n\n  return (\n    <NodeContainer\n      {...nodeProps}\n      css={{\n        [treeNodeLevel]: level,\n        ...(isActionVisible && { [treeActionOpacity]: 1 }),\n      }}\n      onKeyDown={handleKeydown}\n    >\n      <DepthBars />\n      <NodeButton\n        {...buttonProps}\n        ref={buttonRef}\n        tabIndex={tabbable || level === 0 ? undefined : -1}\n        aria-selected={isSelected}\n        aria-current={isHighlighted}\n        data-tree-button\n      >\n        {children}\n      </NodeButton>\n      {isExpanded !== undefined && (\n        <ExpandButton\n          tabIndex={-1}\n          onClick={(event) => onExpand?.(isExpanded === false, event.altKey)}\n        >\n          {isExpanded ? (\n            <ChevronDownIcon size=\"12\" />\n          ) : (\n            <ChevronRightIcon size=\"12\" />\n          )}\n        </ExpandButton>\n      )}\n      <ActionContainer data-tree-action>{action}</ActionContainer>\n    </NodeContainer>\n  );\n};\n\nexport const TreeNodeLabel = ({\n  children,\n  prefix,\n  ...props\n}: {\n  children: ReactNode;\n  prefix?: ReactNode;\n}) => {\n  return (\n    <>\n      {prefix}\n      <Text\n        variant=\"labels\"\n        truncate\n        css={{\n          marginLeft: prefix ? theme.spacing[3] : 0,\n          flexBasis: 0,\n          flexGrow: 1,\n        }}\n        {...props}\n      >\n        {children}\n      </Text>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/design-system/src/components/two-rows-icon-button-container.stories.tsx",
    "content": "import { TwoRowsIconButtonContainer } from \"./two-rows-icon-button-container\";\nimport { SmallIconButton } from \"./small-icon-button\";\nimport { TrashIcon } from \"@webstudio-is/icons\";\nimport { StorySection } from \"./storybook\";\n\nexport default {\n  title: \"Two Rows Small Icon Button Container\",\n};\n\nexport const TwoRowsSmallIconButtonContainer = () => (\n  <>\n    <StorySection title=\"Single child\">\n      <TwoRowsIconButtonContainer>\n        <SmallIconButton variant=\"destructive\" icon={<TrashIcon />} />\n      </TwoRowsIconButtonContainer>\n    </StorySection>\n\n    <StorySection title=\"Empty container\">\n      <TwoRowsIconButtonContainer />\n    </StorySection>\n\n    <StorySection title=\"Multiple children\">\n      <TwoRowsIconButtonContainer>\n        <SmallIconButton variant=\"destructive\" icon={<TrashIcon />} />\n        <SmallIconButton icon={<TrashIcon />} />\n      </TwoRowsIconButtonContainer>\n    </StorySection>\n  </>\n);\n"
  },
  {
    "path": "packages/design-system/src/components/two-rows-icon-button-container.tsx",
    "content": "/**\n * Implementation of the \"Two Rows Small Icon Button Container\" component from:\n * https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?node-id=1844%3A5860\n */\n\nimport { forwardRef, type ReactNode, type Ref } from \"react\";\nimport { css, theme, type CSS } from \"../stitches.config\";\n\nconst curveStyle = css({\n  fill: theme.colors.borderMain,\n  alignSelf: \"start\",\n  variants: { rotated: { true: { transform: \"rotate(90deg)\" } } },\n});\n\nconst Curve = ({ rotated }: { rotated?: boolean }) => (\n  <svg\n    width=\"10\"\n    height=\"10\"\n    viewBox=\"0 0 10 10\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    className={curveStyle({ rotated })}\n  >\n    <path d=\"M10 10V6C10 2.68629 7.31371 0 4 0H0V1H4C6.76142 1 9 3.23858 9 6V10H10Z\" />\n  </svg>\n);\n\nconst style = css({\n  display: \"flex\",\n  flexDirection: \"column\",\n  width: 19,\n  gap: theme.spacing[3],\n  alignItems: \"center\",\n});\n\nexport const TwoRowsIconButtonContainer = forwardRef(\n  (\n    {\n      children,\n      className,\n      css,\n    }: {\n      children?: ReactNode;\n      css?: CSS;\n      className?: string;\n    },\n    ref: Ref<HTMLDivElement>\n  ) => (\n    <div ref={ref} className={style({ className, css })}>\n      <Curve />\n      {children}\n      <Curve rotated />\n    </div>\n  )\n);\nTwoRowsIconButtonContainer.displayName = \"TwoRowsIconButtonContainer\";\n"
  },
  {
    "path": "packages/design-system/src/index.ts",
    "content": "export * from \"./stitches.config\";\nexport * from \"./components/storybook\";\nexport * from \"./utilities\";\nexport { type SlotProps, Slot } from \"@radix-ui/react-slot\";\n// Aligned with Figma\n\nexport * from \"./components/text\";\nexport * from \"./components/panel-title\";\nexport * from \"./components/section-title\";\nexport * from \"./components/separator\";\nexport * from \"./components/button\";\nexport * from \"./components/label\";\nexport * from \"./components/select\";\nexport * from \"./components/combobox\";\nexport * from \"./components/dropdown-menu\";\nexport * from \"./components/context-menu\";\nexport * from \"./components/icon-button\"; // mostly aligned, but needs a demo and to use tokens\nexport * from \"./components/toggle-button\";\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogDescription,\n  DialogClose,\n  DialogMaximize,\n  DialogContent,\n  DialogTitle,\n  DialogTitleActions,\n  DialogActions,\n} from \"./components/dialog\";\nexport * from \"./components/floating-panel\";\nexport * from \"./components/popover\";\nexport {\n  MenuList,\n  MenuItemButton,\n  MenuCheckedIcon,\n  MenuItemIndicator,\n  menuItemCss,\n} from \"./components/menu\";\nexport * from \"./components/switch\";\nexport * from \"./components/toolbar\";\nexport * from \"./components/two-rows-icon-button-container\";\nexport * from \"./components/small-icon-button\";\nexport * from \"./components/list-position-indicator\";\nexport * from \"./components/position-grid\";\nexport * from \"./components/small-toggle-button\";\nexport * from \"./components/css-value-list-item\";\nexport * from \"./components/nested-icon-label\";\nexport * from \"./components/text-area\";\nexport * from \"./components/radio\";\nexport * from \"./components/checkbox\";\nexport * from \"./components/component-card\";\nexport * from \"./components/input-field\";\nexport * from \"./components/nested-input-button\";\nexport * from \"./components/panel-tabs\";\nexport * from \"./components/link\";\nexport * from \"./components/panel-banner\";\nexport * from \"./components/focus-ring\";\nexport * from \"./components/tree\";\nexport * from \"./components/command\";\nexport * from \"./components/gradient-picker\";\nexport * from \"./components/color-picker\";\n\n// Not aligned\n\nexport * from \"./components/toast\";\nexport * as Collapsible from \"@radix-ui/react-collapsible\";\nexport { AccessibleIcon } from \"@radix-ui/react-accessible-icon\";\nexport * from \"./components/toggle-group\";\nexport * from \"./components/progress\";\nexport { SearchField, useSearchFieldKeys } from \"./components/search-field\";\nexport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@radix-ui/react-tabs\";\nexport { Card } from \"./components/card\";\nexport * from \"./components/tooltip\";\nexport {\n  EnhancedTooltip,\n  EnhancedTooltipProvider,\n  useEnhancedTooltipProps,\n} from \"./components/enhanced-tooltip\";\nexport * from \"./components/avatar\";\nexport * from \"./components/pro-badge\";\n\n// No need to align\n\nexport { Box } from \"./components/box\";\nexport { Flex } from \"./components/flex\";\nexport { Grid } from \"./components/grid\";\nexport * from \"./components/primitives/dnd\";\nexport * from \"./components/primitives/numeric-gesture-control\";\nexport * from \"./components/primitives/is-truncated\";\nexport * from \"./components/primitives/arrow-focus\";\nexport * from \"./components/scroll-area\";\nexport * from \"./components/primitives/use-scrub\";\nexport * from \"./components/primitives/numeric-input-arrow-keys\";\nexport * from \"./components/primitives/list\";\nexport * from \"./components/kbd\";\n\n// Deprecated\nexport * from \"./components/__DEPRECATED__/list\";\n"
  },
  {
    "path": "packages/design-system/src/stitches.config.ts",
    "content": "import { createStitches } from \"@stitches/react\";\nimport type * as Stitches from \"@stitches/react\";\nexport type { VariantProps } from \"@stitches/react\";\nimport * as figma from \"./__generated__/figma-design-tokens\";\n\nconst spacing = {\n  0: \"0px\",\n  1: \"1px\",\n  2: \"2px\",\n  3: \"4px\",\n  4: \"6px\",\n  5: \"8px\",\n  6: \"10px\",\n  7: \"12px\",\n  8: \"14px\",\n  9: \"16px\",\n  10: \"20px\",\n  11: \"24px\",\n  12: \"28px\",\n  13: \"32px\",\n  14: \"36px\",\n  15: \"40px\",\n  16: \"44px\",\n  17: \"48px\",\n  18: \"56px\",\n  19: \"64px\",\n  20: \"80px\",\n  21: \"96px\",\n  22: \"112px\",\n  23: \"128px\",\n  24: \"144px\",\n  25: \"160px\",\n  26: \"176px\",\n  27: \"192px\",\n  28: \"208px\",\n  29: \"224px\",\n  30: \"240px\",\n  31: \"256px\",\n  32: \"288px\",\n  33: \"320px\",\n  34: \"384px\",\n  35: \"448px\",\n};\n\nconst { styled, css, getCssText, globalCss, keyframes, config, reset } =\n  createStitches({\n    theme: {\n      colors: figma.color,\n      fonts: {\n        ...figma.fontFamilies,\n        sans: figma.fontFamilies.inter,\n        mono: figma.fontFamilies.robotoMono,\n      },\n\n      opacity: {\n        1: \"0.4\",\n      },\n      spacing,\n      sizes: {\n        sidebarWidth: spacing[30],\n        controlHeight: spacing[11],\n      },\n      /**\n       * Use instead: textVariants / textStyles / <Text />\n       */\n      deprecatedFontSize: {\n        1: \"8px\",\n        2: \"10px\",\n        3: \"12px\",\n        // Legacy - don't use unless specified in Figma\n        4: \"14px\",\n        5: \"19px\",\n        6: \"21px\",\n        7: \"27px\",\n        8: \"35px\",\n        9: \"59px\",\n      },\n\n      borderRadius: {\n        1: \"1px\",\n        2: \"2px\",\n        3: \"3px\",\n        4: \"4px\",\n        5: \"5px\",\n        6: \"6px\",\n        7: \"8px\",\n        round: \"50%\",\n        pill: \"9999px\",\n      },\n      zIndices: {\n        max: \"999\",\n      },\n      easing: {\n        easeOutQuart: \"cubic-bezier(0.25, 1, 0.5, 1)\",\n        easeOut: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n      },\n      shadows: figma.boxShadow,\n\n      // Semantic values\n      panel: {\n        padding: `${spacing[5]} ${spacing[7]}`,\n        paddingInline: spacing[7],\n        paddingBlock: spacing[5],\n      },\n    },\n    media: {\n      tablet: \"(min-width: 768px)\",\n      hover: \"(any-hover: hover)\",\n    },\n    utils: {\n      p: (value: Stitches.PropertyValue<\"padding\">) => ({\n        padding: value,\n      }),\n      pt: (value: Stitches.PropertyValue<\"paddingTop\">) => ({\n        paddingTop: value,\n      }),\n      pr: (value: Stitches.PropertyValue<\"paddingRight\">) => ({\n        paddingRight: value,\n      }),\n      pb: (value: Stitches.PropertyValue<\"paddingBottom\">) => ({\n        paddingBottom: value,\n      }),\n      pl: (value: Stitches.PropertyValue<\"paddingLeft\">) => ({\n        paddingLeft: value,\n      }),\n      px: (value: Stitches.PropertyValue<\"paddingLeft\">) => ({\n        paddingInline: value,\n      }),\n      py: (value: Stitches.PropertyValue<\"paddingTop\">) => ({\n        paddingBlock: value,\n      }),\n\n      m: (value: Stitches.PropertyValue<\"margin\">) => ({\n        margin: value,\n      }),\n      mt: (value: Stitches.PropertyValue<\"marginTop\">) => ({\n        marginTop: value,\n      }),\n      mr: (value: Stitches.PropertyValue<\"marginRight\">) => ({\n        marginRight: value,\n      }),\n      mb: (value: Stitches.PropertyValue<\"marginBottom\">) => ({\n        marginBottom: value,\n      }),\n      ml: (value: Stitches.PropertyValue<\"marginLeft\">) => ({\n        marginLeft: value,\n      }),\n      mx: (value: Stitches.PropertyValue<\"marginLeft\">) => ({\n        marginInline: value,\n      }),\n      my: (value: Stitches.PropertyValue<\"marginTop\">) => ({\n        marginBlock: value,\n      }),\n\n      userSelect: (value: Stitches.PropertyValue<\"userSelect\">) => ({\n        WebkitUserSelect: value,\n        userSelect: value,\n      }),\n\n      size: (value: Stitches.PropertyValue<\"width\">) => ({\n        width: value,\n        height: value,\n      }),\n\n      appearance: (value: Stitches.PropertyValue<\"appearance\">) => ({\n        WebkitAppearance: value,\n        appearance: value,\n      }),\n      backgroundClip: (value: Stitches.PropertyValue<\"backgroundClip\">) => ({\n        WebkitBackgroundClip: value,\n        backgroundClip: value,\n      }),\n    },\n  });\n\ntype VariblesValues = typeof config.theme;\n\ntype VariblesNames = {\n  [GroupKey in keyof VariblesValues]: {\n    [VariableKey in keyof VariblesValues[GroupKey]]: string;\n  };\n};\n\nconst toVariblesNames = (values: VariblesValues): VariblesNames => {\n  const result: Record<string, Record<string, string>> = {};\n  for (const groupKey in values) {\n    const group = values[groupKey as keyof VariblesValues];\n    const groupResult: Record<string, string> = {};\n    for (const variableKey in group) {\n      groupResult[variableKey] = `$${groupKey}$${variableKey}`;\n    }\n    result[groupKey] = groupResult;\n  }\n  return result as VariblesNames;\n};\n\nexport const theme = toVariblesNames(config.theme);\n\nexport const rawTheme = config.theme;\n\nexport type CSS = Stitches.CSS<typeof config>;\n\nexport { styled, css, globalCss, keyframes, config };\n\nexport const flushCss = () => {\n  const css = getCssText();\n  reset();\n  return css;\n};\n"
  },
  {
    "path": "packages/design-system/src/utilities.ts",
    "content": "import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport warnOnce from \"warn-once\";\nimport type { CSS } from \"./stitches.config\";\n\nexport const canvasPointerEventsPropertyName = \"--canvas-pointer-events\";\n\nlet disableCount = 0;\n\nconst updateCanvasPointerEvents = () => {\n  if (disableCount < 0) {\n    // Should be impossible as counter control implemented as disposable resource\n    throw new Error(\"canvas pointer event counter can't be less than 0\");\n  }\n\n  // use ===1 instead of >0 for optimisation\n  if (disableCount === 1) {\n    document.documentElement.style.setProperty(\n      canvasPointerEventsPropertyName,\n      \"none\"\n    );\n    return;\n  }\n\n  if (disableCount === 0) {\n    document.documentElement.style.removeProperty(\n      canvasPointerEventsPropertyName\n    );\n  }\n};\n\n/**\n * Temporarily disables pointer events on the canvas using canvasPointerEventsPropertyName.\n * Use the returned disposable to re-enable them.\n *\n * Implemented as disposable to\n * - Ensure events are first disabled, then enabled in sequence.\n * - To support concurrent calls (internal counter tracks the number of disables/enables).\n **/\nexport const disableCanvasPointerEvents = () => {\n  let disposeCalled = false;\n\n  disableCount += 1;\n  updateCanvasPointerEvents();\n\n  return () => {\n    if (disposeCalled) {\n      // It's perfectly ok to dispose multiple times.\n      return;\n    }\n    disposeCalled = true;\n    disableCount -= 1;\n    updateCanvasPointerEvents();\n  };\n};\n\nexport const useDisableCanvasPointerEvents = () => {\n  const enableCanvasPointerEventsRef = useRef<undefined | (() => void)>(\n    undefined\n  );\n\n  const enableDisable = useMemo(\n    () => ({\n      enableCanvasPointerEvents: () => {\n        warnOnce(\n          enableCanvasPointerEventsRef.current === undefined,\n          \"enableCanvasPointerEvents was called before disableCanvasPointerEvents, this is not an issue but may indicate the problem with the code.\"\n        );\n        enableCanvasPointerEventsRef.current?.();\n      },\n      disableCanvasPointerEvents: () => {\n        enableCanvasPointerEventsRef.current?.();\n        enableCanvasPointerEventsRef.current = disableCanvasPointerEvents();\n      },\n    }),\n    []\n  );\n\n  useEffect(\n    () => () => {\n      enableCanvasPointerEventsRef.current?.();\n    },\n    [enableDisable]\n  );\n\n  return enableDisable;\n};\n\n/**\n * Uses ResizeObserver to notify about resize events, with start, end and timeout.\n */\nexport const useResize = ({\n  onResizeStart,\n  onResize,\n  onResizeEnd,\n  timeout = 300,\n}: {\n  onResizeStart?: (entries: Array<ResizeObserverEntry>) => void;\n  onResize?: (entries: Array<ResizeObserverEntry>) => void;\n  onResizeEnd?: (entries: Array<ResizeObserverEntry>) => void;\n  timeout?: number;\n}) => {\n  const [element, ref] = useState<HTMLElement | null>(null);\n  const timeoutRef = useRef<ReturnType<typeof setTimeout>>();\n  const onResizeStartRef = useRef(onResizeStart);\n  onResizeStartRef.current = onResizeStart;\n  const onResizeRef = useRef(onResize);\n  onResizeRef.current = onResize;\n  const onResizeEndRef = useRef(onResizeEnd);\n  onResizeEndRef.current = onResizeEnd;\n  const isResizingRef = useRef<boolean | undefined>();\n\n  useEffect(() => {\n    if (element === null) {\n      return;\n    }\n    // Mark resizing as on a new observer instance, we will use this to skip first resize event.\n    isResizingRef.current = undefined;\n    const observer = new ResizeObserver((entries) => {\n      // Resize observer called first time is not a start of resize\n      if (isResizingRef.current === undefined) {\n        isResizingRef.current = false;\n        return;\n      }\n      if (isResizingRef.current === false) {\n        isResizingRef.current = true;\n        onResizeStartRef.current?.(entries);\n      }\n      requestAnimationFrame(() => {\n        onResizeRef.current?.(entries);\n      });\n      clearTimeout(timeoutRef.current);\n      timeoutRef.current = setTimeout(() => {\n        onResizeEndRef.current?.(entries);\n        isResizingRef.current = false;\n      }, timeout);\n    });\n    observer.observe(element);\n    return () => {\n      clearTimeout(timeoutRef.current);\n      observer.disconnect();\n    };\n  }, [element, onResizeStartRef, onResizeRef, onResizeEndRef, timeout]);\n\n  return [element, ref] as const;\n};\n\nexport const truncate = (): CSS => ({\n  whiteSpace: \"nowrap\",\n  textOverflow: \"ellipsis\",\n  overflow: \"hidden\",\n});\n\nexport const useDebounceEffect = () => {\n  const [updateCallback, setUpdateCallback] = useState(() => () => {\n    /* empty */\n  });\n  useEffect(() => {\n    // Because of how our styles works we need to update after React render to be sure that\n    // all styles are applied\n    updateCallback();\n  }, [updateCallback]);\n  return useCallback((callback: () => void) => {\n    setUpdateCallback(() => callback);\n  }, []);\n};\n\n/**\n * Radix may steal focus back to the previously focused element\n * when opening popovers/dropdowns. This ensures the newly opened UI gets focus.\n */\nexport const focusFirstCollectionItem = (container: HTMLElement) => {\n  requestAnimationFrame(() => {\n    const firstItem = container.querySelector(\"[data-radix-collection-item]\");\n    if (firstItem instanceof HTMLElement) {\n      firstItem.focus();\n    }\n  });\n};\n"
  },
  {
    "path": "packages/design-system/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/design-system/tsconfig.typecheck.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": null\n  }\n}\n"
  },
  {
    "path": "packages/design-system/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  resolve: {\n    conditions: [\"webstudio\", \"browser\", \"development|production\"],\n  },\n  // resolve webstudio condition in tests\n  ssr: {\n    resolve: {\n      conditions: [\"webstudio\", \"node\", \"development|production\"],\n    },\n  },\n  test: {\n    environment: \"jsdom\",\n  },\n});\n"
  },
  {
    "path": "packages/domain/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/domain/README.md",
    "content": "# Webstudio Project\n\nProject related functionalities. This was temporarily placed here to reuse between other packages, but we need to split this into separate packages and probably remove this package entirely or keep it only for \"project\" specific functionality.\n"
  },
  {
    "path": "packages/domain/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/domain\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Domain\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/postgrest\": \"workspace:*\",\n    \"@webstudio-is/project\": \"workspace:*\",\n    \"@webstudio-is/project-build\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"nanoid\": \"^5.1.5\",\n    \"type-fest\": \"^4.37.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\"\n    },\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/domain/src/db/cname-from-user-id.ts",
    "content": "export const cnameFromUserId = async (userId: string) => {\n  const vowels = [\"a\", \"e\", \"i\", \"o\", \"u\"];\n  const consonants = [\n    \"b\",\n    \"c\",\n    \"d\",\n    \"f\",\n    \"g\",\n    \"h\",\n    \"j\",\n    \"k\",\n    \"l\",\n    \"m\",\n    \"n\",\n    \"p\",\n    \"q\",\n    \"r\",\n    \"s\",\n    \"t\",\n    \"v\",\n    \"w\",\n    \"x\",\n    \"y\",\n    \"z\",\n  ];\n\n  const secretBuffer = await crypto.subtle.digest(\n    \"SHA-256\",\n    new TextEncoder().encode(userId)\n  );\n\n  let result = \"\";\n\n  const array = new Uint8Array(secretBuffer);\n  const wordsLength = [\n    Math.max(4, array[0] % 7),\n    Math.max(4, array[0] % 7),\n    Math.max(4, array[0] % 7),\n  ];\n\n  let wordIndex = 0;\n  let wordLength = wordsLength[wordIndex];\n\n  for (let i = 0; i < array.length; i++) {\n    result +=\n      i % 2 === 0\n        ? consonants[array[i] % consonants.length]\n        : vowels[array[i] % vowels.length];\n    if (i >= wordLength) {\n      wordIndex++;\n      if (wordIndex >= wordsLength.length) {\n        break;\n      }\n      result += \"-\";\n      wordLength += wordsLength[wordIndex];\n    }\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "packages/domain/src/db/domain.ts",
    "content": "import {\n  authorizeProject,\n  type AppContext,\n  AuthorizationError,\n  createErrorResponse,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport { validateDomain } from \"./validate\";\nimport { cnameFromUserId } from \"./cname-from-user-id\";\nimport type { Project } from \"@webstudio-is/project\";\nimport type { Database } from \"@webstudio-is/postgrest/index.server\";\n\ntype Result = { success: false; error: string } | { success: true };\n\n/**\n * Creates 2 entries in the database:\n * at the \"domain\" table and at the \"projectDomain\" table\n */\nexport const create = async (\n  props: {\n    projectId: Project[\"id\"];\n    domain: string;\n    maxDomainsAllowedPerUser: number;\n  },\n  context: AppContext\n): Promise<Result> => {\n  // Only owner or admin of the project can create domains\n  const canCreateDomain = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"admin\" },\n    context\n  );\n\n  if (canCreateDomain === false) {\n    throw new AuthorizationError(\n      \"You don't have access to create this project domains\"\n    );\n  }\n\n  const project = await projectApi.loadById(props.projectId, context);\n\n  const { userId: ownerId } = project;\n\n  if (ownerId === null) {\n    throw new AuthorizationError(\"Project must have project userId defined\");\n  }\n\n  const totalDomainsCount = await countTotalDomains(ownerId, context);\n\n  if (totalDomainsCount >= props.maxDomainsAllowedPerUser) {\n    return {\n      success: false,\n      error:\n        \"You have reached the maximum number of allowed domains. Please upgrade to the Pro plan or higher to add unlimited domains.\",\n    };\n  }\n\n  const validationResult = validateDomain(props.domain);\n\n  if (validationResult.success === false) {\n    return validationResult;\n  }\n\n  const { domain } = validationResult;\n\n  // Create domain in domain table\n  const upsertResult = await context.postgrest.client\n    .from(\"Domain\")\n    .upsert(\n      {\n        id: crypto.randomUUID(),\n        domain,\n        status: \"INITIALIZING\",\n      },\n      // Do not update if exists\n      { onConflict: \"domain\", ignoreDuplicates: true }\n    )\n    .eq(\"domain\", domain);\n\n  if (upsertResult.error) {\n    return createErrorResponse(upsertResult.error);\n  }\n\n  // Get domain id (upsert in postgrest does not return anything in case of conflict and ignoreDuplicates)\n  const domainRow = await context.postgrest.client\n    .from(\"Domain\")\n    .select(\"id\")\n    .eq(\"domain\", domain)\n    .single();\n\n  if (domainRow.error) {\n    return createErrorResponse(domainRow.error);\n  }\n\n  const domainId = domainRow.data.id;\n  const txtRecord = crypto.randomUUID();\n\n  const result = await context.postgrest.client.from(\"ProjectDomain\").insert({\n    domainId,\n    projectId: props.projectId,\n    txtRecord,\n    cname: await cnameFromUserId(ownerId),\n  });\n\n  if (result.error) {\n    return createErrorResponse(result.error);\n  }\n\n  return { success: true };\n};\n\n/**\n * Verify TXT record of the domain, update domain status, start 3rd party domain initialization process\n */\nexport const verify = async (\n  props: {\n    projectId: string;\n    domainId: string;\n  },\n  context: AppContext\n): Promise<Result> => {\n  // Only owner or admin of the project can register domains\n  const canRegisterDomain = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"admin\" },\n    context\n  );\n\n  if (canRegisterDomain === false) {\n    throw new Error(\"You don't have access to create this project domains\");\n  }\n\n  const projectDomain = await context.postgrest.client\n    .from(\"ProjectDomain\")\n    .select(\n      `\n      txtRecord,\n      cname,\n      domain:Domain(*)\n      `\n    )\n    .eq(\"domainId\", props.domainId)\n    .eq(\"projectId\", props.projectId)\n    .single();\n\n  if (projectDomain.error) {\n    return createErrorResponse(projectDomain.error);\n  }\n\n  const domain = projectDomain.data.domain?.domain;\n\n  if (domain == null) {\n    return createErrorResponse(\"Domain not found\");\n  }\n\n  // @todo: TXT verification and domain initialization should be implemented in the future as queue service\n  const createDomainResult = await context.domain.domainTrpc.create.mutate({\n    domain,\n    txtRecord: projectDomain.data.txtRecord,\n  });\n\n  if (createDomainResult.success === false) {\n    return createDomainResult;\n  }\n\n  const domainUpdateResult = await context.postgrest.client\n    .from(\"Domain\")\n    .update({\n      status: \"PENDING\",\n      txtRecord: projectDomain.data.txtRecord,\n    })\n    .eq(\"id\", props.domainId);\n\n  if (domainUpdateResult.error) {\n    return createErrorResponse(domainUpdateResult.error);\n  }\n\n  return { success: true };\n};\n\n/**\n * Removes projectDomain entry\n */\nexport const remove = async (\n  props: {\n    projectId: string;\n    domainId: string;\n  },\n  context: AppContext\n): Promise<Result> => {\n  // Only owner or admin of the project can register domains\n  const canDeleteDomain = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"admin\" },\n    context\n  );\n\n  if (canDeleteDomain === false) {\n    throw new Error(\"You don't have access to delete this project domains\");\n  }\n\n  const deleteResult = await context.postgrest.client\n    .from(\"ProjectDomain\")\n    .delete()\n    .eq(\"domainId\", props.domainId)\n    .eq(\"projectId\", props.projectId);\n\n  if (deleteResult.error) {\n    return createErrorResponse(deleteResult.error);\n  }\n\n  return { success: true };\n};\n\ntype Status = \"active\" | \"pending\" | \"error\";\ntype StatusEnum = Uppercase<Status>;\n\ntype Domain = Database[\"public\"][\"Tables\"][\"Domain\"][\"Row\"];\n\ntype RefreshResult =\n  | { success: false; error: string }\n  | { success: true; domain: Domain };\n\nconst statusToStatusEnum = (status: Status): StatusEnum =>\n  status.toUpperCase() as StatusEnum;\n\n/**\n * Reads the status of the domain from the 3rd party provider\n * and updates the database accordingly\n *\n * @todo: In the future should be a read only status reader\n */\nexport const updateStatus = async (\n  props: {\n    projectId: Project[\"id\"];\n    domain: string;\n  },\n  context: AppContext\n): Promise<RefreshResult> => {\n  // Only owner or admin of the project can register domains\n  const canRefreshDomain = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"admin\" },\n    context\n  );\n\n  if (canRefreshDomain === false) {\n    throw new Error(\"You don't have access to refresh this project domains\");\n  }\n\n  const validationResult = validateDomain(props.domain);\n\n  if (validationResult.success === false) {\n    return validationResult;\n  }\n\n  const { domain } = validationResult;\n\n  // @todo: must be implemented as workflow/queue service part of 3rd party domain initialization process\n  const statusResult = await context.domain.domainTrpc.getStatus.query({\n    domain,\n  });\n\n  if (statusResult.success === false) {\n    return statusResult;\n  }\n\n  const { data } = statusResult;\n\n  // update domain status\n  const updatedDomainResult = await context.postgrest.client\n    .from(\"Domain\")\n    .update({\n      status: statusToStatusEnum(data.status),\n      error: data.status === \"error\" ? data.error : null,\n    })\n    .eq(\"domain\", domain)\n    .select(\"*\")\n    .single();\n\n  if (updatedDomainResult.error) {\n    return createErrorResponse(updatedDomainResult.error);\n  }\n\n  return { success: true, domain: updatedDomainResult.data };\n};\n\nexport const countTotalDomains = async (\n  userId: string,\n  context: AppContext\n): Promise<number> => {\n  const result = await context.postgrest.client\n    .from(\"Domain\")\n    .select(\"Project!ProjectDomain!inner(id)\", {\n      count: \"exact\",\n      head: true,\n    })\n    .eq(\"Project.userId\", userId)\n    .eq(\"Project.isDeleted\", false);\n\n  if (result.error) {\n    throw result.error;\n  }\n\n  return result.count ?? 0;\n};\n"
  },
  {
    "path": "packages/domain/src/db/index.ts",
    "content": "export * as db from \"./domain\";\n"
  },
  {
    "path": "packages/domain/src/db/validate.ts",
    "content": "export const validateDomain = (\n  domain: string\n): { success: false; error: string } | { success: true; domain: string } => {\n  try {\n    const domainUrl = new URL(`https://${domain}`);\n    const domainHost = domainUrl.host;\n\n    if (domainHost.split(\".\").length < 2) {\n      return {\n        success: false,\n        error: `The domain \"${domainHost}\" must have at least two levels.`,\n      };\n    }\n\n    if (domainHost.split(\".\").length > 4) {\n      return {\n        success: false,\n        error: `The domain \"${domainHost}\" must have at most four levels.`,\n      };\n    }\n    return {\n      success: true,\n      domain: domainHost,\n    };\n  } catch {\n    return {\n      success: false,\n      error: `Invalid domain ${domain}`,\n    };\n  }\n};\n"
  },
  {
    "path": "packages/domain/src/index.server.ts",
    "content": "export * from \"./trpc\";\nexport * from \"./db\";\n"
  },
  {
    "path": "packages/domain/src/index.ts",
    "content": "export type { DomainRouter } from \"./trpc\";\nexport { validateDomain } from \"./db/validate\";\n"
  },
  {
    "path": "packages/domain/src/rdap.ts",
    "content": "interface DNSList {\n  description: string;\n  publication: string;\n  services: [string[], string[]][];\n  version: string;\n}\n\n// Cache map to store TLD (top level domain) → RDAP server URL mappings\nconst dnsCache = new Map<string, string>();\n\n/**\n * Fetch the IANA DNS RDAP bootstrap JSON.\n */\nconst fetchDnsList = async (): Promise<undefined | DNSList> => {\n  try {\n    const response = await fetch(\"https://data.iana.org/rdap/dns.json\", {\n      headers: {\n        accept: \"application/json,application/rdap+json\",\n      },\n    });\n    if (response.ok) {\n      return response.json();\n    }\n  } catch {\n    // empty block\n  }\n};\n\n/**\n * Find the RDAP server URL for a given top-level domain (TLD).\n */\nconst findRdapServer = async (topLevelDomain: string) => {\n  if (dnsCache.size === 0) {\n    const dns = await fetchDnsList();\n    if (dns) {\n      for (const [domains, [server]] of dns.services) {\n        for (const domain of domains) {\n          dnsCache.set(domain, server);\n        }\n      }\n    }\n  }\n  return dnsCache.get(topLevelDomain);\n};\n\n/**\n * Extract the top-level domain from a full domain string.\n * Unicode is converted to ascii\n */\nconst getTopLevelDomain = (domain: string) => {\n  try {\n    return new URL(`https://${domain}`).hostname.split(\".\").at(-1);\n  } catch {\n    // invalid domain\n  }\n};\n\nconst fetchRdap = async (\n  rdapServer: string,\n  domain: string\n): Promise<undefined | string> => {\n  try {\n    const response = await fetch(`${rdapServer}domain/${domain}`, {\n      headers: {\n        accept: \"application/json,application/rdap+json\",\n      },\n    });\n    if (response.ok) {\n      return response.text();\n    }\n  } catch {\n    // empty block\n  }\n};\n\n/**\n * Determine whether a domain is using Cloudflare nameservers.\n * 1. Parse TLD from domain.\n * 2. Lookup RDAP server for that TLD.\n * 3. Fetch RDAP data for the domain.\n * 4. Search the raw response for \".ns.cloudflare.com\".\n */\nexport const isDomainUsingCloudflareNameservers = async (domain: string) => {\n  const topLevelDomain = getTopLevelDomain(domain);\n  if (!topLevelDomain) {\n    throw new Error(\"Could not parse the top level domain.\");\n  }\n\n  const rdapServer = await findRdapServer(topLevelDomain);\n  if (!rdapServer) {\n    console.error(\n      \"RDAP Server for the given top level domain could not be found.\"\n    );\n    return;\n  }\n\n  const data = await fetchRdap(rdapServer, domain);\n  if (data) {\n    // detect by nameservers rather than registrar url\n    // sometimes stored as *.NS.CLOUDFLARE.COM\n    return data.toLowerCase().includes(\".ns.cloudflare.com\");\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/domain/src/trpc/domain.ts",
    "content": "import { z } from \"zod\";\nimport { nanoid } from \"nanoid\";\nimport * as projectApi from \"@webstudio-is/project/index.server\";\nimport {\n  createProductionBuild,\n  unpublishBuild,\n} from \"@webstudio-is/project-build/index.server\";\nimport {\n  router,\n  procedure,\n  createErrorResponse,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { Templates } from \"@webstudio-is/sdk\";\nimport { db } from \"../db\";\nimport { isDomainUsingCloudflareNameservers } from \"../rdap\";\n\nexport const domainRouter = router({\n  getEntriToken: procedure.query(async ({ ctx }) => {\n    try {\n      const result = await ctx.entri.entryApi.getEntriToken();\n\n      return {\n        success: true,\n        token: result.token,\n        applicationId: result.applicationId,\n      } as const;\n    } catch (error) {\n      return createErrorResponse(error);\n    }\n  }),\n\n  findDomainRegistrar: procedure\n    .input(z.object({ domain: z.string() }))\n    .query(async ({ input }) => {\n      const isCloudflare = await isDomainUsingCloudflareNameservers(\n        input.domain\n      );\n      return {\n        known: isCloudflare !== undefined,\n        cnameFlattening: isCloudflare === true,\n      };\n    }),\n\n  project: procedure\n    .input(z.object({ projectId: z.string() }))\n    .query(async ({ input, ctx }) => {\n      try {\n        const project = await projectApi.loadById(input.projectId, ctx);\n\n        return {\n          success: true,\n          project,\n        } as const;\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n  publish: procedure\n    .input(\n      z.discriminatedUnion(\"destination\", [\n        z.object({\n          projectId: z.string(),\n          domains: z.array(z.string()),\n          destination: z.literal(\"saas\"),\n        }),\n        z.object({\n          projectId: z.string(),\n          destination: z.literal(\"static\"),\n          templates: z.array(Templates),\n        }),\n      ])\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        const project = await projectApi.loadById(input.projectId, ctx);\n\n        const name = `${project.id}-${nanoid()}.zip`;\n\n        const domains: string[] = [];\n\n        let hasCustomDomain = false;\n\n        if (input.destination === \"saas\") {\n          const currentProjectDomains = project.domainsVirtual;\n\n          if (input.domains.includes(project.domain)) {\n            domains.push(project.domain);\n          }\n\n          domains.push(\n            ...input.domains.filter((domain) =>\n              currentProjectDomains.some(\n                (projectDomain) =>\n                  projectDomain.domain === domain &&\n                  projectDomain.status === \"ACTIVE\" &&\n                  projectDomain.verified\n              )\n            )\n          );\n\n          hasCustomDomain = currentProjectDomains.some(\n            (projectDomain) =>\n              projectDomain.status === \"ACTIVE\" && projectDomain.verified\n          );\n        }\n\n        const build = await createProductionBuild(\n          {\n            projectId: input.projectId,\n            deployment:\n              input.destination === \"saas\"\n                ? {\n                    destination: input.destination,\n                    domains: domains,\n                    assetsDomain: project.domain,\n                    excludeWstdDomainFromSearch: hasCustomDomain,\n                  }\n                : {\n                    destination: input.destination,\n                    name,\n                    assetsDomain: project.domain,\n                    templates: input.templates,\n                  },\n          },\n          ctx\n        );\n\n        const { deploymentTrpc, env } = ctx.deployment;\n\n        if (env.BUILDER_ORIGIN === undefined) {\n          throw new Error(\"Missing env.BUILDER_ORIGIN\");\n        }\n\n        const result = await deploymentTrpc.publish.mutate({\n          // used to load build data from the builder see routes/rest.build.$buildId.ts\n          builderOrigin: env.BUILDER_ORIGIN,\n          githubSha: env.GITHUB_SHA,\n          buildId: build.id,\n          // preview support\n          branchName: env.GITHUB_REF_NAME,\n          destination: input.destination,\n          // action log helper (not used for deployment, but for action logs readablity)\n          logProjectName: `${project.title} - ${project.id}`,\n        });\n\n        if (input.destination === \"static\" && result.success) {\n          return { success: true as const, name };\n        }\n\n        return result;\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n  /**\n   * Unpublish a specific domain from the project\n   */\n  unpublish: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        domain: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        const { deploymentTrpc, env } = ctx.deployment;\n\n        // Call deployment service to delete the worker for this domain\n        const result = await deploymentTrpc.unpublish.mutate({\n          domain: input.domain,\n        });\n\n        // Extract subdomain for DB lookup (strip publisher host suffix)\n        // e.g., \"myproject.wstd.work\" → \"myproject\", \"custom.com\" → \"custom.com\"\n        const dbDomain = input.domain.replace(`.${env.PUBLISHER_HOST}`, \"\");\n\n        // Always unpublish in DB regardless of worker deletion result\n        await unpublishBuild(\n          { projectId: input.projectId, domain: dbDomain },\n          ctx\n        );\n\n        // If worker deletion failed (and not NOT_IMPLEMENTED), return error\n        if (result.success === false && result.error !== \"NOT_IMPLEMENTED\") {\n          return {\n            success: false,\n            message: `Failed to unpublish ${input.domain}: ${result.error}`,\n          };\n        }\n\n        return {\n          success: true,\n          message: `${input.domain} unpublished`,\n        };\n      } catch (error) {\n        console.error(\"Unpublish failed:\", error);\n        return {\n          success: false,\n          message: `Failed to unpublish ${input.domain}: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n        };\n      }\n    }),\n  /**\n   * Update *.wstd.* domain\n   */\n  updateProjectDomain: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        domain: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        await projectApi.updateDomain(\n          {\n            id: input.projectId,\n            domain: input.domain,\n          },\n          ctx\n        );\n\n        return { success: true } as const;\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n\n  /**\n   * Creates 2 entries in the database:\n   * at the \"domain\" table and at the \"projectDomain\" table\n   */\n  create: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        domain: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        const { userPlanFeatures } = ctx;\n        if (userPlanFeatures === undefined) {\n          throw new Error(\"Missing userPlanFeatures\");\n        }\n\n        return await db.create(\n          {\n            projectId: input.projectId,\n            domain: input.domain,\n            maxDomainsAllowedPerUser: userPlanFeatures.maxDomainsAllowedPerUser,\n          },\n          ctx\n        );\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n\n  verify: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        domainId: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        return await db.verify(\n          {\n            projectId: input.projectId,\n            domainId: input.domainId,\n          },\n          ctx\n        );\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n  remove: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        domainId: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        return await db.remove(\n          {\n            projectId: input.projectId,\n            domainId: input.domainId,\n          },\n          ctx\n        );\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n  countTotalDomains: procedure.query(async ({ ctx }) => {\n    try {\n      if (\n        ctx.authorization.type !== \"user\" &&\n        ctx.authorization.type !== \"token\"\n      ) {\n        throw new Error(\"Not authorized\");\n      }\n\n      const ownerId =\n        ctx.authorization.type === \"user\"\n          ? ctx.authorization.userId\n          : ctx.authorization.ownerId;\n\n      const data = await db.countTotalDomains(ownerId, ctx);\n      return { success: true, data } as const;\n    } catch (error) {\n      return createErrorResponse(error);\n    }\n  }),\n\n  updateStatus: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        domain: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      try {\n        return await db.updateStatus(\n          { projectId: input.projectId, domain: input.domain },\n          ctx\n        );\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n});\n\nexport type DomainRouter = typeof domainRouter;\n"
  },
  {
    "path": "packages/domain/src/trpc/index.ts",
    "content": "export * from \"./domain\";\n"
  },
  {
    "path": "packages/domain/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/feature-flags/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/feature-flags/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/feature-flags\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Feature flags used to enable/disable a feature over env variable\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\"\n  },\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/feature-flags/src/feature.ts",
    "content": "import * as flags from \"./flags\";\n\nlet env = \"\";\n\ntype Name = keyof typeof flags;\n\nexport const parse = (flags?: string | null): Array<Name> =>\n  // Supports both, space and comma separated items\n  (flags ?? \"\").split(/\\s|,/).filter(Boolean) as Array<Name>;\n\nexport const setLocal = (features: string) => {\n  if (flags) {\n    const parsed = parse(features).filter(\n      (flag) => flag in flags\n    ) as Array<Name>;\n    localStorage.setItem(\"features\", parsed.join(\",\"));\n  }\n};\n\nexport const readLocal = (): Array<Name> => {\n  try {\n    const flags = localStorage.getItem(\"features\");\n    return parse(flags);\n  } catch {\n    // Not having feature in localStorage or not having localStorage implemented, both should not throw.\n  }\n  return [];\n};\n\nexport const setEnv = (features: string) => {\n  env = features;\n};\n\n/**\n * Returns true/false if the feature is turned on.\n * A feature can be turned on:\n * - by default directly in ./flags\n * - by providing an environment variable server-side (locally or on the server): FEATURES=\"something1, something2\" pnpm dev\n * - by setting it in the browser console: localStorage.features = 'something1, something2', browser defined flag will override server-side flag\n */\nexport const isFeatureEnabled = (name: Name): boolean => {\n  if (env === \"*\") {\n    return true;\n  }\n  const defaultValue = flags[name];\n  const envValue = parse(env).includes(name);\n  const localValue = readLocal().includes(name);\n  // Any source can enable feature, first `true` value will result in enabling a feature.\n  // This also means you can't disable a feature if its already enabled in default value.\n  return localValue || envValue || defaultValue;\n};\n"
  },
  {
    "path": "packages/feature-flags/src/flags.ts",
    "content": "// Only for development, is not supposed to be enabled at all.\nexport const internalComponents = false;\nexport const unsupportedBrowsers = false;\nexport const resourceProp = false;\nexport const tailwind = false;\n"
  },
  {
    "path": "packages/feature-flags/src/index.ts",
    "content": "export * from \"./feature\";\n"
  },
  {
    "path": "packages/feature-flags/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/fonts/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/fonts/README.md",
    "content": "# Fonts utils\n\nFonts logic reusable across all systems.\n"
  },
  {
    "path": "packages/fonts/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/fonts\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Fonts utils\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\",\n    \"build\": \"rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"vitest\": \"^3.1.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"peerDependencies\": {\n    \"zod\": \"^3.19.1\"\n  },\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\",\n    \"types\": \"./lib/types/index.d.ts\",\n    \"import\": \"./lib/index.js\"\n  },\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/fonts/src/constants.ts",
    "content": "import type { FontFormat } from \"./schema\";\n\nexport const SYSTEM_FONTS = new Map([\n  [\n    \"System UI\",\n    {\n      stack: [\"system-ui\", \"sans-serif\"],\n      description:\n        \"System UI fonts are those native to the operating system interface. They are highly legible and easy to read at small sizes, contains many font weights, and is ideal for UI elements.\",\n    },\n  ],\n  // Modern font stacks\n  // https://github.com/system-fonts/modern-font-stacks\n  [\n    \"Transitional\",\n    {\n      stack: [\"Charter\", \"Bitstream Charter\", \"Sitka Text\", \"Cambria\", \"serif\"],\n      description:\n        \"Transitional typefaces are a mix between Old Style and Modern typefaces that was developed during The Enlightenment. One of the most famous examples of a Transitional typeface is Times New Roman, which was developed for the Times of London newspaper.\",\n    },\n  ],\n  [\n    \"Old Style\",\n    {\n      stack: [\n        \"Iowan Old Style\",\n        \"Palatino Linotype\",\n        \"URW Palladio L\",\n        \"P052\",\n        \"serif\",\n      ],\n      description:\n        \"Old Style typefaces are characterized by diagonal stress, low contrast between thick and thin strokes, and rounded serifs, and were developed in the Renaissance period. One of the most famous examples of an Old Style typeface is Garamond.\",\n    },\n  ],\n  [\n    \"Humanist\",\n    {\n      stack: [\n        \"Seravek\",\n        \"Gill Sans Nova\",\n        \"Ubuntu\",\n        \"Calibri\",\n        \"DejaVu Sans\",\n        \"source-sans-pro\",\n        \"sans-serif\",\n      ],\n      description:\n        \"Humanist typefaces are characterized by their organic, calligraphic forms and low contrast between thick and thin strokes. These typefaces are inspired by the handwriting of the Renaissance period and are often considered to be more legible and easier to read than other sans-serif typefaces.\",\n    },\n  ],\n  [\n    \"Geometric Humanist\",\n    {\n      stack: [\n        \"Avenir\",\n        \"Montserrat\",\n        \"Corbel\",\n        \"URW Gothic\",\n        \"source-sans-pro\",\n        \"sans-serif\",\n      ],\n      description:\n        \"Geometric Humanist typefaces are characterized by their clean, geometric forms and uniform stroke widths. These typefaces are often considered to be modern and sleek in appearance, and are often used for headlines and other display purposes. Futura is a famous example of this classification.\",\n    },\n  ],\n  [\n    \"Classical Humanist\",\n    {\n      stack: [\n        \"Optima\",\n        \"Candara\",\n        \"Noto Sans\",\n        \"source-sans-pro\",\n        \"sans-serif\",\n      ],\n      description:\n        \"Classical Humanist typefaces are characterized by how the strokes subtly widen as they reach the stroke terminals without ending in a serif. These typefaces are inspired by classical Roman capitals and the stone-carving on Renaissance-period tombstones.\",\n    },\n  ],\n  [\n    \"Neo-Grotesque\",\n    {\n      stack: [\n        \"Inter\",\n        \"Roboto\",\n        \"Helvetica Neue\",\n        \"Arial Nova\",\n        \"Nimbus Sans\",\n        \"Arial\",\n        \"sans-serif\",\n      ],\n      description:\n        \"Neo-Grotesque typefaces are a style of sans-serif that was developed in the late 19th and early 20th centuries and is characterized by its clean, geometric forms and uniform stroke widths. One of the most famous examples of a Neo-Grotesque typeface is Helvetica.\",\n    },\n  ],\n  [\n    \"Monospace Slab Serif\",\n    {\n      stack: [\"Nimbus Mono PS\", \"Courier New\", \"monospace\"],\n      description:\n        \"Monospace Slab Serif typefaces are characterized by their fixed-width letters, which have the same width regardless of their shape, and its simple, geometric forms. Used to emulate typewriter output for reports, tabular work and technical documentation.\",\n    },\n  ],\n  [\n    \"Monospace Code\",\n    {\n      stack: [\n        \"ui-monospace\",\n        \"Cascadia Code\",\n        \"Source Code Pro\",\n        \"Menlo\",\n        \"Consolas\",\n        \"DejaVu Sans Mono\",\n        \"monospace\",\n      ],\n      description:\n        \"Monospace Code typefaces are specifically designed for use in programming and other technical applications. These typefaces are characterized by their monospaced design, which means that all letters and characters have the same width, and their clear, legible forms.\",\n    },\n  ],\n  [\n    \"Industrial\",\n    {\n      stack: [\n        \"Bahnschrift\",\n        \"DIN Alternate\",\n        \"Franklin Gothic Medium\",\n        \"Nimbus Sans Narrow\",\n        \"sans-serif-condensed\",\n        \"sans-serif\",\n      ],\n      description:\n        \"Industrial typefaces originated in the late 19th century and was heavily influenced by the advancements in technology and industry during that time. Industrial typefaces are characterized by their bold, sans-serif letterforms, simple and straightforward appearance, and the use of straight lines and geometric shapes.\",\n    },\n  ],\n  [\n    \"Rounded Sans\",\n    {\n      stack: [\n        \"ui-rounded\",\n        \"Hiragino Maru Gothic ProN\",\n        \"Quicksand\",\n        \"Comfortaa\",\n        \"Manjari\",\n        \"Arial Rounded MT\",\n        \"Arial Rounded MT Bold\",\n        \"Calibri\",\n        \"source-sans-pro\",\n        \"sans-serif\",\n      ],\n      description:\n        \"Rounded typefaces are characterized by the rounded curved letterforms and give a softer, friendlier appearance. The rounded edges give the typeface a more organic and playful feel, making it suitable for use in informal or child-friendly designs. The rounded sans-serif style has been popular since the 1950s, and it continues to be widely used in advertising, branding, and other forms of graphic design.\",\n    },\n  ],\n  [\n    \"Slab Serif\",\n    {\n      stack: [\n        \"Rockwell\",\n        \"Rockwell Nova\",\n        \"Roboto Slab\",\n        \"DejaVu Serif\",\n        \"Sitka Small\",\n        \"serif\",\n      ],\n      description:\n        \"Slab Serif typefaces are characterized by the presence of thick, block-like serifs on the ends of each letterform. These serifs are usually unbracketed, meaning they do not have any curved or tapered transitions to the main stroke of the letter.\",\n    },\n  ],\n  [\n    \"Antique\",\n    {\n      stack: [\n        \"Superclarendon\",\n        \"Bookman Old Style\",\n        \"URW Bookman\",\n        \"URW Bookman L\",\n        \"Georgia Pro\",\n        \"Georgia\",\n        \"serif\",\n      ],\n      description:\n        \"Antique typefaces, also known as Egyptians, are a subset of serif typefaces that were popular in the 19th century. They are characterized by their block-like serifs and thick uniform stroke weight.\",\n    },\n  ],\n  [\n    \"Didone\",\n    {\n      stack: [\n        \"Didot\",\n        \"Bodoni MT\",\n        \"Noto Serif Display\",\n        \"URW Palladio L\",\n        \"P052\",\n        \"Sylfaen\",\n        \"serif\",\n      ],\n      description:\n        \"Didone typefaces, also known as Modern typefaces, are characterized by the high contrast between thick and thin strokes, vertical stress, and hairline serifs with no bracketing. The Didone style emerged in the late 18th century and gained popularity during the 19th century.\",\n    },\n  ],\n  [\n    \"Handwritten\",\n    {\n      stack: [\n        \"Segoe Print\",\n        \"Bradley Hand\",\n        \"Chilanka\",\n        \"TSCu_Comic\",\n        \"casual\",\n        \"cursive\",\n      ],\n      description:\n        \"Handwritten typefaces are designed to mimic the look and feel of handwriting. Despite the vast array of handwriting styles, this font stack tend to adopt a more informal and everyday style of handwriting.\",\n    },\n  ],\n  [\n    \"Arial\",\n    {\n      stack: [\"Arial\", \"Roboto\", \"sans-serif\"],\n      description:\n        \"A clean, sans-serif font designed for legibility and versatility. Ideal for modern, minimalistic designs or digital content that requires simplicity.\",\n    },\n  ],\n  [\n    \"Times New Roman\",\n    {\n      stack: [\"Times New Roman\", \"sans\"],\n      description:\n        \"A classic serif font known for its formal, professional appearance. Best suited for traditional documents, reports, and academic writing.\",\n    },\n  ],\n  [\n    \"Courier New\",\n    {\n      stack: [\"Courier New\", \"monospace\"],\n      description:\n        \"A monospaced serif font with uniform spacing, mimicking typewriter text. Perfect for coding, technical documents, or retro-styled designs.\",\n    },\n  ],\n  // Chineese fonts\n  [\n    \"SimSun\",\n    {\n      stack: [\"SimSun\", \"Songti SC, sans-serif\"],\n      description:\n        \"A traditional serif font designed for Chinese characters, offering clear and readable text. Ideal for formal Chinese documents or multilingual content requiring both Chinese and Latin text.\",\n    },\n  ],\n  [\n    \"PingFang SC\",\n    {\n      stack: [\"PingFang SC\", \"Microsoft Ya Hei\", \"sans-serif\"],\n      description:\n        \"A modern sans-serif font designed for simplified Chinese characters. Sleek and clean, it’s best for digital content and interfaces where modern, streamlined design is needed.\",\n    },\n  ],\n]);\n\nexport const DEFAULT_FONT_FALLBACK = \"sans-serif\";\n\nexport const FONT_FORMATS: Map<FontFormat, string> = new Map([\n  [\"woff\", \"woff\"],\n  [\"woff2\", \"woff2\"],\n  [\"ttf\", \"truetype\"],\n]);\n\nexport const FONT_MIME_TYPES = Array.from(FONT_FORMATS.keys())\n  .map((format) => `.${format}`)\n  .join(\", \");\n\nexport const FONT_STYLES = [\"normal\", \"italic\", \"oblique\"] as const;\nexport type FontStyle = (typeof FONT_STYLES)[number];\n"
  },
  {
    "path": "packages/fonts/src/font-weights.ts",
    "content": "export const fontWeights = {\n  \"100\": {\n    label: \"Thin\",\n    names: [\"thin\", \"hairline\"],\n  },\n  \"200\": {\n    label: \"Extra Light\",\n    names: [\"extra light\", \"extralight\", \"ultra light\", \"ultralight\"],\n  },\n  \"300\": {\n    label: \"Light\",\n    names: [\"light\"],\n  },\n  \"400\": {\n    label: \"Normal\",\n    names: [\"normal\", \"regular\"],\n  },\n  \"500\": {\n    label: \"Medium\",\n    names: [\"medium\"],\n  },\n  \"600\": {\n    label: \"Semi Bold\",\n    names: [\"semi bold\", \"semibold\", \"demi bold\", \"demibold\"],\n  },\n  \"700\": {\n    label: \"Bold\",\n    names: [\"bold\", \"bold\"],\n  },\n  \"800\": {\n    label: \"Extra Bold\",\n    names: [\"extra bold\", \"extrabold\", \"ultra bold\", \"ultrabold\"],\n  },\n  \"900\": {\n    label: \"Black\",\n    names: [\"black\", \"heavy\"],\n  },\n} as const;\n\nexport const fontWeightNames = new Map<string, FontWeight>(\n  Object.keys(fontWeights)\n    .map((weight) => {\n      const weightData = fontWeights[weight as FontWeight];\n      return weightData.names.map((name) => [name, weight]);\n    })\n    .flat() as Array<[string, FontWeight]>\n);\n\nexport type FontWeight = keyof typeof fontWeights;\n"
  },
  {
    "path": "packages/fonts/src/get-font-faces.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { getFontFaces, type PartialFontAsset } from \"./get-font-faces\";\n\ndescribe(\"getFontFaces()\", () => {\n  test(\"sanitize url\", () => {\n    const assets: Array<PartialFontAsset> = [\n      {\n        format: \"woff\",\n        meta: {\n          family: \"Roboto\",\n          style: \"normal\",\n          weight: 400,\n        },\n        name: `robot\"o.woff`,\n      },\n    ];\n    expect(getFontFaces(assets, { assetBaseUrl: \"/fonts/\" }))\n      .toMatchInlineSnapshot(`\n[\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Roboto\",\n    \"fontStyle\": \"normal\",\n    \"fontWeight\": 400,\n    \"src\": \"url(\"/fonts/robot\\\\\"o.woff\") format(\"woff\")\",\n  },\n]\n`);\n  });\n\n  test(\"different formats\", () => {\n    const assets: Array<PartialFontAsset> = [\n      {\n        format: \"woff\",\n        meta: {\n          family: \"Roboto\",\n          style: \"normal\",\n          weight: 400,\n        },\n        name: \"roboto.woff\",\n      },\n      {\n        format: \"ttf\",\n        meta: {\n          family: \"Roboto\",\n          style: \"normal\",\n          weight: 400,\n        },\n        name: \"roboto.ttf\",\n      },\n    ];\n    expect(getFontFaces(assets, { assetBaseUrl: \"/fonts/\" }))\n      .toMatchInlineSnapshot(`\n[\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Roboto\",\n    \"fontStyle\": \"normal\",\n    \"fontWeight\": 400,\n    \"src\": \"url(\"/fonts/roboto.woff\") format(\"woff\"), url(\"/fonts/roboto.ttf\") format(\"truetype\")\",\n  },\n]\n`);\n  });\n\n  test(\"different style\", () => {\n    const assets: Array<PartialFontAsset> = [\n      {\n        format: \"ttf\",\n        meta: {\n          family: \"Roboto\",\n          style: \"normal\",\n          weight: 400,\n        },\n        name: \"roboto.ttf\",\n      },\n      {\n        format: \"ttf\",\n        meta: {\n          family: \"Roboto\",\n          style: \"italic\",\n          weight: 400,\n        },\n        name: \"roboto-italic.ttf\",\n      },\n    ];\n    expect(getFontFaces(assets, { assetBaseUrl: \"/fonts/\" }))\n      .toMatchInlineSnapshot(`\n[\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Roboto\",\n    \"fontStyle\": \"normal\",\n    \"fontWeight\": 400,\n    \"src\": \"url(\"/fonts/roboto.ttf\") format(\"truetype\")\",\n  },\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Roboto\",\n    \"fontStyle\": \"italic\",\n    \"fontWeight\": 400,\n    \"src\": \"url(\"/fonts/roboto-italic.ttf\") format(\"truetype\")\",\n  },\n]\n`);\n  });\n\n  test(\"different weight\", () => {\n    const assets: Array<PartialFontAsset> = [\n      {\n        format: \"ttf\",\n        meta: {\n          family: \"Roboto\",\n          style: \"normal\",\n          weight: 400,\n        },\n        name: \"roboto.ttf\",\n      },\n      {\n        format: \"ttf\",\n        meta: {\n          family: \"Roboto\",\n          style: \"normal\",\n          weight: 500,\n        },\n        name: \"roboto-bold.ttf\",\n      },\n    ];\n    expect(getFontFaces(assets, { assetBaseUrl: \"/fonts/\" }))\n      .toMatchInlineSnapshot(`\n[\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Roboto\",\n    \"fontStyle\": \"normal\",\n    \"fontWeight\": 400,\n    \"src\": \"url(\"/fonts/roboto.ttf\") format(\"truetype\")\",\n  },\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Roboto\",\n    \"fontStyle\": \"normal\",\n    \"fontWeight\": 500,\n    \"src\": \"url(\"/fonts/roboto-bold.ttf\") format(\"truetype\")\",\n  },\n]\n`);\n  });\n\n  test(\"variable font\", () => {\n    const assets: Array<PartialFontAsset> = [\n      {\n        format: \"ttf\",\n        meta: {\n          family: \"Inter\",\n          variationAxes: {\n            wght: { name: \"wght\", min: 100, default: 400, max: 1000 },\n            wdth: { name: \"wdth\", min: 25, default: 100, max: 151 },\n            opsz: { name: \"opsz\", min: 8, default: 14, max: 144 },\n            GRAD: { name: \"GRAD\", min: -200, default: 0, max: 150 },\n            slnt: { name: \"slnt\", min: -10, default: 0, max: 0 },\n            XTRA: { name: \"XTRA\", min: 323, default: 468, max: 603 },\n            XOPQ: { name: \"XOPQ\", min: 27, default: 96, max: 175 },\n            YOPQ: { name: \"YOPQ\", min: 25, default: 79, max: 135 },\n            YTLC: { name: \"YTLC\", min: 416, default: 514, max: 570 },\n            YTUC: { name: \"YTUC\", min: 528, default: 712, max: 760 },\n            YTAS: { name: \"YTAS\", min: 649, default: 750, max: 854 },\n            YTDE: { name: \"YTDE\", min: -305, default: -203, max: -98 },\n            YTFI: { name: \"YTFI\", min: 560, default: 738, max: 788 },\n          },\n        },\n        name: \"inter.ttf\",\n      },\n    ];\n    expect(getFontFaces(assets, { assetBaseUrl: \"/fonts/\" }))\n      .toMatchInlineSnapshot(`\n[\n  {\n    \"fontDisplay\": \"swap\",\n    \"fontFamily\": \"Inter\",\n    \"fontStretch\": \"25% 151%\",\n    \"fontStyle\": \"normal\",\n    \"fontWeight\": \"100 1000\",\n    \"src\": \"url(\"/fonts/inter.ttf\") format(\"truetype\")\",\n  },\n]\n`);\n  });\n});\n"
  },
  {
    "path": "packages/fonts/src/get-font-faces.ts",
    "content": "import { FONT_FORMATS } from \"./constants\";\nimport type { FontMeta, FontFormat, FontMetaStatic } from \"./schema\";\n\nexport type PartialFontAsset = {\n  format: FontFormat;\n  meta: FontMeta;\n  name: string;\n};\n\nexport type FontFace = {\n  fontFamily: string;\n  fontDisplay: \"swap\" | \"auto\" | \"block\" | \"fallback\" | \"optional\";\n  src: string;\n  fontStyle?: FontMetaStatic[\"style\"];\n  fontWeight?: number | string;\n  fontStretch?: string;\n};\n\n// Use JSON.stringify to escape double quotes and backslashes in strings as it automatically replaces \" with \\\" and \\ with \\\\.\nconst sanitizeCssUrl = (str: string) => JSON.stringify(str);\n\nconst formatFace = (\n  asset: PartialFontAsset,\n  format: string,\n  url: string\n): FontFace => {\n  if (\"variationAxes\" in asset.meta) {\n    const { wght, wdth } = asset.meta?.variationAxes ?? {};\n    return {\n      fontFamily: asset.meta.family,\n      fontStyle: \"normal\",\n      fontDisplay: \"swap\",\n      src: `url(${sanitizeCssUrl(url)}) format(\"${format}\")`,\n      fontStretch: wdth ? `${wdth.min}% ${wdth.max}%` : undefined,\n      fontWeight: wght ? `${wght.min} ${wght.max}` : undefined,\n    };\n  }\n  return {\n    fontFamily: asset.meta.family,\n    fontStyle: asset.meta.style,\n    fontWeight: asset.meta.weight,\n    fontDisplay: \"swap\",\n    src: `url(${sanitizeCssUrl(url)}) format(\"${format}\")`,\n  };\n};\n\nconst getKey = (asset: PartialFontAsset) => {\n  if (\"variationAxes\" in asset.meta) {\n    return asset.meta.family + Object.values(asset.meta.variationAxes).join(\"\");\n  }\n  return asset.meta.family + asset.meta.style + asset.meta.weight;\n};\n\nexport const getFontFaces = (\n  assets: Array<PartialFontAsset>,\n  options: {\n    assetBaseUrl: string;\n  }\n): Array<FontFace> => {\n  const { assetBaseUrl } = options;\n  const faces = new Map();\n  for (const asset of assets) {\n    const url = `${assetBaseUrl}${asset.name}`;\n    const assetKey = getKey(asset);\n    const face = faces.get(assetKey);\n    const format = FONT_FORMATS.get(asset.format);\n    if (format === undefined) {\n      // Should never happen since we allow only uploading formats we support\n      continue;\n    }\n\n    if (face === undefined) {\n      const face = formatFace(asset, format, url);\n      faces.set(assetKey, face);\n      continue;\n    }\n\n    // We already have that font face, so we need to add the new src\n    face.src += `, url(${sanitizeCssUrl(url)}) format(\"${format}\")`;\n  }\n  return Array.from(faces.values());\n};\n"
  },
  {
    "path": "packages/fonts/src/index.ts",
    "content": "export * from \"./constants\";\nexport * from \"./get-font-faces\";\nexport * from \"./schema\";\nexport * from \"./font-weights\";\n"
  },
  {
    "path": "packages/fonts/src/schema.ts",
    "content": "import { z } from \"zod\";\nimport { FONT_STYLES } from \"./constants\";\n\nexport const FontFormat = z.union([\n  z.literal(\"ttf\"),\n  z.literal(\"woff\"),\n  z.literal(\"woff2\"),\n]);\nexport type FontFormat = z.infer<typeof FontFormat>;\n\nconst AxisName = z.enum([\n  \"wght\",\n  \"wdth\",\n  \"slnt\",\n  \"opsz\",\n  \"ital\",\n  \"GRAD\",\n  \"XTRA\",\n  \"XOPQ\",\n  \"YOPQ\",\n  \"YTLC\",\n  \"YTUC\",\n  \"YTAS\",\n  \"YTDE\",\n  \"YTFI\",\n]);\n\nconst VariationAxes = z.record(\n  AxisName,\n  z.object({\n    name: z.string(),\n    min: z.number(),\n    default: z.number(),\n    max: z.number(),\n  })\n);\n\nexport type VariationAxes = z.infer<typeof VariationAxes>;\n\nexport const FontMetaStatic = z.object({\n  family: z.string(),\n  style: z.enum(FONT_STYLES),\n  weight: z.number(),\n});\n\nexport type FontMetaStatic = z.infer<typeof FontMetaStatic>;\n\nconst FontMetaVariable = z.object({\n  family: z.string(),\n  variationAxes: VariationAxes,\n});\n\nexport const FontMeta = z.union([FontMetaStatic, FontMetaVariable]);\n\nexport type FontMeta = z.infer<typeof FontMeta>;\n"
  },
  {
    "path": "packages/fonts/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/fonts/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/generate-arg-types/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/generate-arg-types\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Generate components property types\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/html-data\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"fast-glob\": \"^3.3.2\",\n    \"react-docgen-typescript\": \"^2.2.2\",\n    \"typescript\": \"5.8.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\"\n  },\n  \"bin\": {\n    \"generate-arg-types\": \"./src/cli.ts\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/generate-arg-types/src/arg-types.ts",
    "content": "import type { PropItem } from \"react-docgen-typescript\";\nimport { PropMeta } from \"@webstudio-is/sdk\";\n\nexport type FilterPredicate = (prop: PropItem) => boolean;\n\nexport const propsToArgTypes = (\n  props: Record<string, PropItem>,\n  exclude: string[]\n) => {\n  const entries = Object.entries(props);\n  return (\n    entries\n      .sort((item1, item2) => {\n        return item1[0].localeCompare(item2[0]);\n      })\n      // Exclude webstudio builder props see react-sdk/src/tree/webstudio-component.tsx\n      .filter(([propName]) => propName.startsWith(\"data-ws-\") === false)\n      .filter(([propName]) => propName.startsWith(\"$webstudio\") === false)\n      .filter(([propName]) => propName.startsWith(\"$\") === false)\n      // Exclude props that are in the exclude list\n      .filter(([propName]) => exclude.includes(propName) === false)\n      .filter(([_propName, propItem]) => {\n        for (const { fileName } of propItem.declarations ?? []) {\n          // ignore aria attributes\n          if (fileName.endsWith(\"/@types/react/index.d.ts\")) {\n            return false;\n          }\n        }\n        return true;\n      })\n      .map(([propName, propItem]) => {\n        // Remove @see and @deprecated from description also {@link ...} is removed as it always go after @see\n        propItem.description = propItem.description\n          .split(\"\\n@see\")[0]\n          .split(\"\\n@deprecated\")[0];\n        return [propName, propItem] as const;\n      })\n      .reduce(\n        (result, current) => {\n          const [propName, prop] = current;\n\n          const argType = getArgType(prop);\n          if (argType != null) {\n            result[propName] = argType;\n          }\n          return result;\n        },\n        {} as Record<string, PropMeta>\n      )\n  );\n};\n\nconst matchers = {\n  color: new RegExp(\"(background|color)\", \"i\"),\n  date: /Date$/,\n};\n\nexport const getArgType = (propItem: PropItem): PropMeta | undefined => {\n  const { type, name, description, defaultValue } = propItem;\n\n  // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n  const makePropMeta = (type: string, control: string, extra?: {}) => {\n    let value = defaultValue?.value;\n    // react-docgen-typescript incorrectly parse jsdoc default values as strings\n    // to fix check and cast to correct type\n    if (type === \"boolean\") {\n      if (value === \"true\") {\n        value = true;\n      }\n      if (value === \"false\") {\n        value = false;\n      }\n    }\n    if (type === \"number\" && typeof value === \"string\") {\n      value = Number(value);\n    }\n    return PropMeta.parse({\n      type,\n      required: propItem.required,\n      control,\n      ...(value == null ? {} : { defaultValue: value }),\n      ...(description ? { description } : {}),\n      ...extra,\n    });\n  };\n\n  // args that end with background or color e.g. iconColor\n  if (matchers.color.test(name) && type.name === \"string\") {\n    return makePropMeta(\"string\", \"color\");\n  }\n\n  try {\n    switch (type.name) {\n      case \"boolean\":\n      case \"Booleanish\":\n        return makePropMeta(\"boolean\", \"boolean\");\n      case \"number\":\n        return makePropMeta(\"number\", \"number\");\n      case \"string\":\n        return makePropMeta(\"string\", \"text\");\n      case \"string | number | readonly string[]\":\n        return makePropMeta(\"string\", \"text\");\n      case \"string | number\":\n      case \"number | string\":\n        if (defaultValue?.value === \"\") {\n          return makePropMeta(\"number\", \"number\", { defaultValue: undefined });\n        } else if (\n          defaultValue?.value == null ||\n          typeof defaultValue.value === \"number\"\n        ) {\n          return makePropMeta(\"number\", \"number\");\n        } else {\n          return makePropMeta(\"string\", \"text\");\n        }\n      case \"enum\": {\n        const options = type.value.map(({ value }: { value: string }) =>\n          // remove additional quotes from enum values\n          value.replace(/^\"(.*)\"$/, \"$1\")\n        );\n        return makePropMeta(\n          \"string\",\n          options.length <= 3 ? \"radio\" : \"select\",\n          { options }\n        );\n      }\n      case \"function\":\n      case \"symbol\":\n        return;\n      default:\n        // cast complex aria types to string\n        if (name === \"role\" || name.startsWith(\"aria-\")) {\n          return makePropMeta(\"string\", \"text\");\n        }\n        // ignore the rest of complex types\n        return;\n    }\n  } catch (error) {\n    console.info(\"Error while parsing prop:\", propItem);\n    throw error;\n  }\n};\n"
  },
  {
    "path": "packages/generate-arg-types/src/cli.ts",
    "content": "#!/usr/bin/env tsx\n\nimport { mkdirSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { withCustomConfig } from \"react-docgen-typescript\";\nimport fg from \"fast-glob\";\nimport { propsToArgTypes } from \"./arg-types\";\nimport { parseArgs, type ParseArgsConfig } from \"node:util\";\nimport { addDescriptions } from \"./props/add-descriptions\";\n\nconst GENERATED_FILES_DIR = \"__generated__\";\n\nconst options = {\n  shouldExtractLiteralValuesFromEnum: true,\n  shouldRemoveUndefinedFromOptional: true,\n};\n\nconst CLI_ARGS_OPTIONS = {\n  allowPositionals: true,\n  options: {\n    exclude: {\n      type: \"string\",\n      multiple: true,\n      short: \"e\",\n    },\n  },\n  strict: true,\n} as const satisfies ParseArgsConfig;\n\nconst cliArgs = parseArgs({ args: process.argv.slice(2), ...CLI_ARGS_OPTIONS });\n\nconst componentsGlobString = cliArgs.positionals.join(\" \");\n\nconst tsConfigPath = path.resolve(process.cwd(), \"./tsconfig.json\");\n\nif (typeof componentsGlobString === \"undefined\") {\n  throw new Error(\n    \"Please provide glob patterns (space separated) as arguments to match your components\"\n  );\n}\n\n// Search for components\nconst globs = componentsGlobString.split(\" \");\nconst componentFiles = fg.sync(globs);\n\nconsole.info(`Resolved tscofig.json at ${tsConfigPath}\\n`);\nconsole.info(`Glob patterns used: \\n${globs.join(\"\\n\")}\\n`);\nconsole.info(`Excluding props: \\n${cliArgs.values.exclude?.join(\"\\n\")}\\n`);\nconsole.info(`Found files to process: \\n${componentFiles.join(\"\\n\")}\\n`);\n\nif (componentFiles.length === 0) {\n  throw new Error(\"No component files found\");\n}\n\n// Create a parser with using your typescript config\nconst tsConfigParser = withCustomConfig(tsConfigPath, options);\n\ntype ComponentNameType = string;\ntype CustomDescriptionsType = {\n  [key in ComponentNameType]: {\n    [key in string]: string;\n  };\n};\n\n(async function run() {\n  const customDescriptionsByDir: { [key in string]: CustomDescriptionsType } =\n    {};\n  // For each component file generate argTypes based on the propTypes\n  for (const filePath of componentFiles) {\n    const fileDir = path.dirname(filePath);\n    const generatedDir = path.join(fileDir, GENERATED_FILES_DIR);\n\n    /**\n     * Every library can define a src/props-description.ts file which exports `propsDescription`.\n     * The type of this object is `CustomDescriptions`.\n     * These descriptions override the generic ones from this package which are located in ./props/descriptions.ts\n     */\n    const customDescriptionsDir = path.join(process.cwd(), fileDir);\n    let customDescriptions: CustomDescriptionsType =\n      customDescriptionsByDir[customDescriptionsDir];\n\n    if (customDescriptions == null) {\n      try {\n        const { propsDescriptions } = await import(\n          path.join(customDescriptionsDir, \"props-descriptions.ts\")\n        );\n        customDescriptionsByDir[customDescriptionsDir] = propsDescriptions;\n        customDescriptions = propsDescriptions;\n      } catch (error) {\n        customDescriptions = {};\n      }\n    }\n\n    const basename = path.basename(filePath, \".tsx\");\n    const generatedFile = `${basename}.props.ts`;\n    const generatedPath = path.join(generatedDir, generatedFile);\n\n    const componentDocs = tsConfigParser.parse(filePath);\n\n    if (componentDocs.length === 0) {\n      console.error(`No propTypes found for ${filePath}`);\n      continue;\n    }\n\n    let fileContent = `import type { PropMeta } from \"@webstudio-is/sdk\";\\n`;\n\n    if (componentDocs.length === 1) {\n      const argTypes = propsToArgTypes(\n        componentDocs[0].props,\n        cliArgs.values.exclude ?? []\n      );\n\n      const componentName = componentDocs[0].displayName;\n\n      addDescriptions(\n        componentName,\n        argTypes,\n        customDescriptions[componentName]\n      );\n\n      fileContent = `${fileContent}\n      export const props: Record<string, PropMeta> = ${JSON.stringify(\n        argTypes\n      )}`;\n    } else {\n      for (const componentDoc of componentDocs) {\n        const argTypes = propsToArgTypes(\n          componentDoc.props,\n          cliArgs.values.exclude ?? []\n        );\n\n        const componentName = componentDoc.displayName;\n\n        addDescriptions(\n          componentName,\n          argTypes,\n          customDescriptions[componentName]\n        );\n\n        fileContent = `${fileContent}\n        export const props${componentName}: Record<string, PropMeta> = ${JSON.stringify(\n          argTypes\n        )}`;\n      }\n    }\n\n    mkdirSync(generatedDir, { recursive: true });\n    writeFileSync(generatedPath, fileContent);\n\n    console.info(`Done generating argTypes for ${generatedPath}`);\n  }\n})();\n"
  },
  {
    "path": "packages/generate-arg-types/src/props/add-descriptions.ts",
    "content": "import {\n  reactPropsToStandardAttributes,\n  standardAttributesToReactProps,\n} from \"@webstudio-is/react-sdk\";\nimport { ariaAttributes, attributesByTag } from \"@webstudio-is/html-data\";\nimport { propsToArgTypes } from \"../arg-types\";\n\nconst ignoreComponents = new Set([\"Embed\"]);\n\n/**\n * Adds descriptions to component props (argTypes).\n *\n * There are a number of description sources. Below is a list sorted by most specific to less specific.\n *\n * 1. Custom descriptions located in the component's package @webstudio-is/html-data/src/props-descriptions.ts module.\n * 2. Shared overridePropsDescriptions located in this package's @webstudio-is/html-data module.\n * 3. Component's meta props descriptions (extracted from inline TypeScript docs).\n * 4. Generic htmlPropsDescriptions located in this package's @webstudio-is/html-data module.\n */\nexport const addDescriptions = (\n  componenName: string,\n  argTypes: ReturnType<typeof propsToArgTypes>,\n  customDescriptions: { [key in string]: string } = {}\n) => {\n  if (componenName && ignoreComponents.has(componenName)) {\n    return;\n  }\n\n  Object.entries(argTypes).forEach(([propName, meta]) => {\n    const description = getDescription(\n      propName,\n      meta.description,\n      customDescriptions\n    );\n\n    if (typeof description === \"string\") {\n      argTypes[propName].description = description;\n    }\n  });\n};\n\nconst attributeDescriptions: Record<string, string> = {};\nfor (const attribute of Object.values(attributesByTag).flat()) {\n  if (attribute) {\n    attributeDescriptions[attribute.name] = attribute.description;\n  }\n}\nfor (const attribute of ariaAttributes) {\n  attributeDescriptions[attribute.name] = attribute.description;\n}\n\nexport const getDescription = (\n  propName: string,\n  currentDescription: string | undefined,\n  customDescriptions: { [key in string]: string } = {}\n): string | undefined => {\n  const name =\n    standardAttributesToReactProps[propName] ||\n    reactPropsToStandardAttributes[propName] ||\n    propName.toLowerCase();\n  return (\n    customDescriptions[propName] ||\n    customDescriptions[name] ||\n    currentDescription ||\n    attributeDescriptions[propName] ||\n    attributeDescriptions[name]\n  );\n};\n"
  },
  {
    "path": "packages/generate-arg-types/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/html-data/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/html-data/README.md",
    "content": "# CSS Data\n\nGenerated configs and collections from mdn-data into consumable form.\n"
  },
  {
    "path": "packages/html-data/bin/aria.ts",
    "content": "import { aria } from \"aria-query\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport {\n  coreMetas,\n  createScope,\n  elementComponent,\n  Prop,\n  type Instance,\n  type Instances,\n  type Props,\n} from \"@webstudio-is/sdk\";\nimport { generateWebstudioComponent } from \"@webstudio-is/react-sdk\";\nimport {\n  findByTags,\n  getAttr,\n  getTextContent,\n  loadPage,\n  parseHtml,\n} from \"./crawler\";\n\ntype Attribute = {\n  name: string;\n  description: string;\n  type: \"string\" | \"boolean\" | \"number\" | \"select\";\n  options?: string[];\n};\n\nconst overrides: Record<string, Partial<Attribute>> = {\n  \"aria-label\": {\n    description:\n      \"Provides the accessible name that describes an interactive element if no other accessible name exists, for example in a button that contains an image with no text.\",\n  },\n};\n\nconst html = await loadPage(\"aria1.3\", \"https://www.w3.org/TR/wai-aria-1.3\");\nconst document = parseHtml(html);\nconst list = findByTags(document, \"dl\").find(\n  (table) => getAttr(table, \"id\")?.value === \"index_state_prop\"\n);\nconst terms = findByTags(list, \"dt\");\nconst details = findByTags(list, \"dd\");\nconst descriptions = new Map<string, string>();\nfor (let index = 0; index < terms.length; index += 1) {\n  const term = getTextContent(terms[index]);\n  const detail = getTextContent(details[index]);\n  descriptions.set(term, detail);\n}\n\nconst attributes: Attribute[] = [\n  {\n    name: \"role\",\n    description:\n      \"Defines an explicit role for an element for use by assistive technologies.\",\n    type: \"string\",\n  },\n];\nfor (const [name, meta] of aria.entries()) {\n  const attribute: Attribute = {\n    name,\n    description: descriptions.get(name) ?? \"\",\n    type: \"string\",\n    ...overrides[name],\n  };\n  if (meta.type === \"string\" || meta.type === \"boolean\") {\n    attribute.type = meta.type;\n  } else if (meta.type === \"number\" || meta.type === \"integer\") {\n    attribute.type = \"number\";\n  } else if (meta.type === \"token\" || meta.type === \"tokenlist\") {\n    attribute.type = \"select\";\n    attribute.options = meta.values?.map((item) => item.toString());\n  } else if (meta.type === \"tristate\") {\n    attribute.type = \"select\";\n    attribute.options = [\"false\", \"mixed\", \"true\"];\n  } else {\n    meta.type satisfies \"id\" | \"idlist\" | \"tristate\";\n  }\n  attributes.push(attribute);\n}\n\nconst ariaContent = `type Attribute = {\n  name: string,\n  description: string,\n  required?: boolean,\n  type: 'string' | 'boolean' | 'number' | 'select' | 'url',\n  options?: string[]\n}\n\nexport const ariaAttributes: Attribute[] = ${JSON.stringify(attributes, null, 2)};\n`;\nawait mkdir(\"./src/__generated__\", { recursive: true });\nawait writeFile(\"./src/__generated__/aria.ts\", ariaContent);\n\n// generate jsx for testing react types\n\nconst instances: Instances = new Map();\nconst props: Props = new Map();\n\nlet id = 0;\nconst getId = () => {\n  id += 1;\n  return id.toString();\n};\n\nconst instance: Instance = {\n  type: \"instance\",\n  id: getId(),\n  component: elementComponent,\n  tag: \"div\",\n  children: [],\n};\ninstances.set(instance.id, instance);\nfor (const { name, type, options } of attributes) {\n  const id = getId();\n  const instanceId = instance.id;\n  if (type === \"string\") {\n    const prop: Prop = { id, instanceId, type, name, value: \"\" };\n    props.set(prop.id, prop);\n    continue;\n  }\n  if (type === \"boolean\") {\n    const prop: Prop = { id, instanceId, type, name, value: true };\n    props.set(prop.id, prop);\n    continue;\n  }\n  if (type === \"select\") {\n    const prop: Prop = {\n      id,\n      instanceId,\n      type: \"string\",\n      name,\n      value: options?.[0] ?? \"\",\n    };\n    props.set(prop.id, prop);\n    continue;\n  }\n  if (type === \"number\") {\n    const prop: Prop = {\n      id,\n      instanceId,\n      type: \"number\",\n      name,\n      value: 0,\n    };\n    props.set(prop.id, prop);\n    continue;\n  }\n  throw Error(`Unknown attribute ${name} with type ${type}`);\n}\n\nawait mkdir(\"./src/__generated__\", { recursive: true });\nawait writeFile(\n  \"./src/__generated__/aria-jsx-test.tsx\",\n  generateWebstudioComponent({\n    name: \"Page\",\n    scope: createScope(),\n    metas: new Map(Object.entries(coreMetas)),\n    instances,\n    props,\n    dataSources: new Map(),\n    rootInstanceId: instance.id,\n    classesMap: new Map(),\n    parameters: [],\n  }) + \"export { Page }\"\n);\n"
  },
  {
    "path": "packages/html-data/bin/attributes.ts",
    "content": "import { mkdir, writeFile } from \"node:fs/promises\";\nimport hash from \"@emotion/hash\";\nimport {\n  coreMetas,\n  createScope,\n  elementComponent,\n  type Instance,\n  type Instances,\n  type Prop,\n  type Props,\n} from \"@webstudio-is/sdk\";\nimport { generateWebstudioComponent } from \"@webstudio-is/react-sdk\";\nimport {\n  findByClasses,\n  findByTags,\n  getAttr,\n  getTextContent,\n  loadHtmlIndices,\n  loadSvgSinglePage,\n  parseHtml,\n} from \"./crawler\";\nimport { possibleStandardNames } from \"./possible-standard-names\";\nimport { ignoredTags } from \"./overrides\";\n\nconst validHtmlAttributes = new Set<string>();\n\ntype Attribute = {\n  name: string;\n  description: string;\n  required?: boolean;\n  type: \"string\" | \"boolean\" | \"number\" | \"select\" | \"url\";\n  options?: string[];\n};\n\nconst overrides: Record<string, Record<string, false | Partial<Attribute>>> = {\n  \"*\": {\n    // react has own opinions about it\n    style: false,\n    // specific to input in react types\n    enterkeyhint: false,\n    inert: false,\n    popover: false,\n    writingsuggestions: false,\n    hidden: {\n      // \"until-found\"; \"hidden\"; the empty string\n      type: \"boolean\",\n      options: undefined,\n    },\n  },\n  a: {\n    href: { type: \"url\", required: true },\n    target: { required: true },\n    download: { type: \"boolean\", required: true },\n  },\n  blockquote: {\n    cite: { required: true },\n  },\n  form: {\n    action: { required: true },\n    method: { required: true },\n    enctype: { required: true },\n  },\n  area: {\n    ping: false,\n  },\n  button: {\n    type: { required: true },\n    command: false,\n    commandfor: false,\n    popovertarget: false,\n    popovertargetaction: false,\n  },\n  label: {\n    for: { required: true },\n  },\n  dialog: {\n    closedby: false,\n  },\n  img: {\n    src: { required: true },\n    alt: { required: true },\n    width: { required: true },\n    height: { required: true },\n    ismap: false,\n  },\n  input: {\n    name: { required: true },\n    value: { required: true },\n    checked: { required: true },\n    type: { required: true },\n    placeholder: { required: true },\n    required: { required: true },\n    autofocus: { required: true },\n    alpha: false,\n    colorspace: false,\n    // react types have it only in textarea\n    dirname: false,\n    popovertarget: false,\n    popovertargetaction: false,\n  },\n  textarea: {\n    name: { required: true },\n    placeholder: { required: true },\n    required: { required: true },\n    autofocus: { required: true },\n  },\n  select: {\n    name: { required: true },\n    required: { required: true },\n    autofocus: { required: true },\n    // mutltiple mode is not considered accessible\n    // and we cannot express it in builder so easier to remove\n    multiple: false,\n  },\n  option: {\n    label: { required: true },\n    value: { required: true },\n    disabled: { required: true },\n    // enforce fake value attribute on select element\n    selected: false,\n  },\n};\n\n// Crawl WHATWG HTML.\nconst html = await loadHtmlIndices();\nconst document = parseHtml(html);\nconst table = findByTags(document, \"table\").find(\n  (table) => getAttr(table, \"id\")?.value === \"attributes-1\"\n);\nconst [tbody] = findByTags(table, \"tbody\");\nconst rows = findByTags(tbody, \"tr\");\n\nconst attributesByTag: Record<string, Attribute[]> = {};\n// textarea does not have value attribute and text content is used as initial value\n// introduce fake value attribute to manage initial state similar to input\nattributesByTag.textarea = [\n  {\n    name: \"value\",\n    description: \"Value of the form control\",\n    type: \"string\",\n    required: true,\n  },\n];\n// select does not have value attribute and selected options are used as initial value\n// introduce fake value attribute to manage initial state similar to input\nattributesByTag.select = [\n  {\n    name: \"value\",\n    description: \"Value of the form control\",\n    type: \"string\",\n    required: true,\n  },\n];\n\nfor (const row of rows) {\n  const attribute = getTextContent(row.childNodes[0]).trim();\n  const elements = getTextContent(row.childNodes[1]).trim();\n  const description = getTextContent(row.childNodes[2]).trim();\n  const tags = /HTML elements/.test(elements)\n    ? [\"*\"]\n    : elements.split(/;/g).map((d) => d.replace(/\\([^)]+\\)/g, \"\").trim());\n  let value = getTextContent(row.childNodes[3]).trim().toLowerCase();\n  if (value.endsWith(\";\")) {\n    value = value.slice(0, -1);\n  }\n  let possibleOptions = value\n    .split(/\\s*;\\s*/)\n    .filter((item) => item.startsWith('\"') && item.endsWith('\"'))\n    .map((item) => item.slice(1, -1));\n  if (attribute === \"target\" || attribute === \"formtarget\") {\n    possibleOptions = [\"_blank\", \"_self\", \"_parent\", \"_top\"];\n  }\n  if (value.includes(\"input type keyword\")) {\n    possibleOptions = [\n      \"hidden\",\n      \"text\",\n      \"search\",\n      \"tel\",\n      \"url\",\n      \"email\",\n      \"password\",\n      \"date\",\n      \"month\",\n      \"week\",\n      \"time\",\n      \"datetime-local\",\n      \"number\",\n      \"range\",\n      \"color\",\n      \"checkbox\",\n      \"radio\",\n      \"file\",\n      \"submit\",\n      \"image\",\n      \"reset\",\n      \"button\",\n    ];\n  }\n  let type: \"string\" | \"boolean\" | \"number\" | \"select\" = \"string\";\n  let options: undefined | string[];\n  if (possibleOptions.length > 0) {\n    type = \"select\";\n    options = possibleOptions;\n  } else if (value.includes(\"boolean attribute\")) {\n    type = \"boolean\";\n  } else if (\n    (value.includes(\"number\") || value.includes(\"integer\")) &&\n    !value.includes(\"list\") &&\n    !value.includes(\"string\")\n  ) {\n    type = \"number\";\n  }\n  validHtmlAttributes.add(attribute);\n  for (let tag of tags) {\n    tag = tag.toLowerCase().trim();\n    if (/custom elements/i.test(tag)) {\n      continue;\n    }\n    if (ignoredTags.includes(tag)) {\n      continue;\n    }\n    if (!attributesByTag[tag]) {\n      attributesByTag[tag] = [];\n    }\n    const attributes = attributesByTag[tag];\n    if (!attributes.some((item) => item.name === attribute)) {\n      const override = overrides[tag]?.[attribute];\n      if (override !== false) {\n        attributes.push({\n          name: attribute,\n          description,\n          type,\n          options,\n          ...override,\n        });\n      }\n    }\n  }\n}\n\n{\n  const svg = await loadSvgSinglePage();\n  const document = parseHtml(svg);\n  const attributeOptions = new Map<string, string[]>();\n  // find all property definition and extract there keywords\n  for (const propdef of findByClasses(document, \"propdef\")) {\n    let options: undefined | string[];\n    for (const row of findByTags(propdef, \"tr\")) {\n      const [nameNode, valueNode] = row.childNodes;\n      const name = getTextContent(nameNode);\n      const list = getTextContent(valueNode)\n        .trim()\n        .split(/\\s+\\|\\s+/);\n      if (\n        name.toLowerCase().includes(\"value\") &&\n        list.every((item) => item.match(/^[a-zA-Z-]+$/))\n      ) {\n        options = list;\n      }\n    }\n    for (const propNameNode of findByClasses(propdef, \"propdef-title\")) {\n      const propName = getTextContent(propNameNode).slice(1, -1);\n      if (options) {\n        attributeOptions.set(propName, options);\n      }\n    }\n  }\n\n  for (const summary of findByClasses(document, \"element-summary\")) {\n    const [tag] = findByClasses(summary, \"element-summary-name\").map((item) =>\n      getTextContent(item).slice(1, -1)\n    );\n    // ignore existing\n    if (attributesByTag[tag] || ignoredTags.includes(tag)) {\n      continue;\n    }\n    const attributes = new Set<string>();\n    const [dl] = findByTags(summary, \"dl\");\n    for (let index = 0; index < dl.childNodes.length; index += 1) {\n      const child = dl.childNodes[index];\n      if (getTextContent(child).toLowerCase().includes(\"attributes\")) {\n        const dd = dl.childNodes[index + 1];\n        for (const attrNameNode of findByClasses(dd, \"attr-name\")) {\n          const attrName = getTextContent(attrNameNode).slice(1, -1);\n          // skip events\n          if (attrName.startsWith(\"on\") || attrName === \"style\") {\n            continue;\n          }\n          validHtmlAttributes.add(attrName);\n          attributes.add(attrName);\n        }\n      }\n    }\n    attributesByTag[tag] = Array.from(attributes)\n      .sort()\n      .map((name) => {\n        let options = attributeOptions.get(name);\n        if (name === \"externalResourcesRequired\") {\n          options = [\"true\", \"false\"];\n        }\n        if (name === \"accumulate\") {\n          options = [\"none\", \"sum\"];\n        }\n        if (name === \"additive\") {\n          options = [\"replace\", \"sum\"];\n        }\n        if (name === \"preserveAlpha\") {\n          options = [\"true\", \"false\"];\n        }\n        if (options) {\n          return { name, description: \"\", type: \"select\", options };\n        }\n        return { name, description: \"\", type: \"string\" };\n      });\n  }\n}\n\n// sort tags and attributes\nconst tags = Object.keys(attributesByTag).sort();\nconst attributesByHash = new Map<string, Attribute>();\nconst reusableAttributesByHash = new Map<string, Attribute>();\nfor (const tag of tags) {\n  const attributes = attributesByTag[tag];\n  delete attributesByTag[tag];\n  attributes.sort((left, right) => left.name.localeCompare(right.name));\n  if (attributes.length > 0) {\n    for (const attribute of attributes) {\n      const attributeHash = hash(JSON.stringify(attribute));\n      if (attributesByHash.has(attributeHash)) {\n        reusableAttributesByHash.set(attributeHash, attribute);\n      } else {\n        attributesByHash.set(attributeHash, attribute);\n      }\n    }\n    attributesByTag[tag] = attributes;\n  }\n}\n\nlet attributesContent = `type Attribute = {\n  name: string,\n  description: string,\n  required?: boolean,\n  type: 'string' | 'boolean' | 'number' | 'select' | 'url',\n  options?: string[]\n}\n\n`;\n\nconst attributeVariableByHash = new Map<string, string>();\nfor (const [key, attribute] of reusableAttributesByHash) {\n  const normalizedName = attribute.name\n    .replaceAll(\"-\", \"_\")\n    .replaceAll(\":\", \"_\");\n  const variableName = `attribute_${normalizedName}_${key}`;\n  attributeVariableByHash.set(key, variableName);\n  attributesContent += `const ${variableName}: Attribute = ${JSON.stringify(attribute, null, 2)};\\n\\n`;\n}\n\nconst serializableAttributesByTag: Record<\n  string,\n  Array<string | Attribute>\n> = {};\nfor (const tag of tags) {\n  const attributes = attributesByTag[tag];\n  serializableAttributesByTag[tag] = attributes.map((attribute) => {\n    const key = hash(JSON.stringify(attribute));\n    const variableName = attributeVariableByHash.get(key);\n    if (variableName) {\n      return variableName;\n    }\n    return attribute;\n  });\n}\n\nattributesContent += `\nexport const attributesByTag: Record<string, undefined | Attribute[]> = ${JSON.stringify(serializableAttributesByTag, null, 2)};\n`;\nfor (const variableName of attributeVariableByHash.values()) {\n  attributesContent = attributesContent.replaceAll(\n    `\"${variableName}\"`,\n    variableName\n  );\n}\n\nawait mkdir(\"./src/__generated__\", { recursive: true });\nawait writeFile(\"./src/__generated__/attributes.ts\", attributesContent);\n\n// generate jsx for testing react types\n\nconst instances: Instances = new Map();\nconst props: Props = new Map();\n\nlet id = 0;\nconst getId = () => {\n  id += 1;\n  return id.toString();\n};\n\nconst body: Instance = {\n  type: \"instance\",\n  id: getId(),\n  component: elementComponent,\n  tag: \"body\",\n  children: [],\n};\ninstances.set(body.id, body);\n\nfor (const entry of Object.entries(attributesByTag)) {\n  let [tag] = entry;\n  const [_tag, attributes] = entry;\n  if (tag === \"*\") {\n    tag = \"div\";\n  }\n  const instance: Instance = {\n    type: \"instance\",\n    id: getId(),\n    component: elementComponent,\n    tag,\n    children: [],\n  };\n  body.children.push({ type: \"id\", value: instance.id });\n  instances.set(instance.id, instance);\n  for (const { name, type, options } of attributes) {\n    const id = getId();\n    const instanceId = instance.id;\n    if (type === \"string\" || type === \"url\") {\n      const prop: Prop = { id, instanceId, type: \"string\", name, value: \"\" };\n      props.set(prop.id, prop);\n      continue;\n    }\n    if (type === \"boolean\") {\n      const prop: Prop = { id, instanceId, type, name, value: true };\n      props.set(prop.id, prop);\n      continue;\n    }\n    if (type === \"select\") {\n      const prop: Prop = {\n        id,\n        instanceId,\n        type: \"string\",\n        name,\n        value: options?.[0] ?? \"\",\n      };\n      props.set(prop.id, prop);\n      continue;\n    }\n    if (type === \"number\") {\n      const prop: Prop = {\n        id,\n        instanceId,\n        type: \"number\",\n        name,\n        value: 0,\n      };\n      props.set(prop.id, prop);\n      continue;\n    }\n    (type) satisfies never;\n    throw Error(`Unknown attribute ${name} with type ${type}`);\n  }\n}\n\nawait mkdir(\"./src/__generated__\", { recursive: true });\nawait writeFile(\n  \"./src/__generated__/attributes-jsx-test.tsx\",\n  generateWebstudioComponent({\n    name: \"Page\",\n    scope: createScope(),\n    metas: new Map(Object.entries(coreMetas)),\n    instances,\n    props,\n    dataSources: new Map(),\n    rootInstanceId: body.id,\n    classesMap: new Map(),\n    parameters: [],\n  }) + \"export { Page }\"\n);\n\n// react does not have this one\npossibleStandardNames[\"dirname\"] = \"dirName\";\nconst standardAttributesToReactProps: Record<string, string> = {};\nconst reactPropsToStandardAttributes: Record<string, string> = {};\nfor (const [htmlAttribute, reactProperty] of Object.entries(\n  possibleStandardNames\n)) {\n  if (\n    validHtmlAttributes.has(htmlAttribute) &&\n    htmlAttribute !== reactProperty\n  ) {\n    standardAttributesToReactProps[htmlAttribute] = reactProperty;\n    reactPropsToStandardAttributes[reactProperty] = htmlAttribute;\n  }\n}\n\nlet standardAttributesContent = \"\";\nstandardAttributesContent += `export const standardAttributesToReactProps: Record<string, string> = ${JSON.stringify(standardAttributesToReactProps, null, 2)};\\n\\n`;\nstandardAttributesContent += `export const reactPropsToStandardAttributes: Record<string, string> = ${JSON.stringify(reactPropsToStandardAttributes, null, 2)};\\n`;\n\nawait mkdir(\"../react-sdk/src/__generated__\", { recursive: true });\nawait writeFile(\n  \"../react-sdk/src/__generated__/standard-attributes.ts\",\n  standardAttributesContent\n);\n"
  },
  {
    "path": "packages/html-data/bin/crawler.ts",
    "content": "import { readFile, writeFile } from \"node:fs/promises\";\nimport { Parser, defaultTreeAdapter, type DefaultTreeAdapterMap } from \"parse5\";\n\ntype Document = DefaultTreeAdapterMap[\"document\"];\n\ntype ChildNode = DefaultTreeAdapterMap[\"childNode\"];\n\ntype Node = DefaultTreeAdapterMap[\"node\"];\n\ntype NodeWithChildren = Extract<ChildNode, { childNodes: ChildNode[] }>;\n\ntype Element = DefaultTreeAdapterMap[\"element\"];\n\ntype Attribute = Element[\"attrs\"][number];\n\nexport const findByTags = (\n  node: undefined | Node,\n  tagName: string,\n  result: NodeWithChildren[] = []\n): NodeWithChildren[] => {\n  if (node && \"childNodes\" in node) {\n    if (\"tagName\" in node && node.tagName === tagName) {\n      result.push(node);\n    }\n    for (const child of node.childNodes) {\n      findByTags(child, tagName, result);\n    }\n  }\n  return result;\n};\n\nexport const findByClasses = (\n  node: undefined | Node,\n  className: string,\n  result: NodeWithChildren[] = []\n): NodeWithChildren[] => {\n  if (node && \"childNodes\" in node) {\n    if (\n      \"tagName\" in node &&\n      node.attrs.some(\n        (item) =>\n          item.name === \"class\" && item.value.split(/\\s+/).includes(className)\n      )\n    ) {\n      result.push(node);\n    }\n    for (const child of node.childNodes) {\n      findByClasses(child, className, result);\n    }\n  }\n  return result;\n};\n\nexport const getAttr = (\n  node: undefined | NodeWithChildren,\n  name: string\n): undefined | Attribute => {\n  return node?.attrs.find((attr) => attr.name === name);\n};\n\nexport const getTextContent = (node: ChildNode) => {\n  if (\"value\" in node) {\n    return node.value;\n  }\n  let result = \"\";\n  if (\"childNodes\" in node) {\n    for (const child of node.childNodes) {\n      result += getTextContent(child);\n    }\n  }\n  return result;\n};\n\nexport const parseHtml = (html: string): Document => {\n  return Parser.parse(html, { treeAdapter: defaultTreeAdapter });\n};\n\nexport const loadPage = async (name: string, url: string) => {\n  // prefer cached file to avoid too many requests on debug\n  const cachedFile = `./node_modules/.cache/${name}.html`;\n  let text;\n  try {\n    text = await readFile(cachedFile, \"utf-8\");\n  } catch {\n    const response = await fetch(url);\n    text = await response.text();\n    await writeFile(cachedFile, text);\n  }\n  return text;\n};\n\nexport const loadHtmlIndices = () =>\n  loadPage(\n    \"html-spec-indices\",\n    \"https://html.spec.whatwg.org/multipage/indices.html\"\n  );\n\nexport const loadSvgSinglePage = () =>\n  loadPage(\"svg-spec\", \"https://www.w3.org/TR/SVG11/single-page.html\");\n"
  },
  {
    "path": "packages/html-data/bin/elements.ts",
    "content": "import { dirname } from \"node:path\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport {\n  findByClasses,\n  findByTags,\n  getTextContent,\n  loadHtmlIndices,\n  loadSvgSinglePage,\n  parseHtml,\n} from \"./crawler\";\nimport { ignoredTags } from \"./overrides\";\n\n// Crawl WHATWG HTML.\n\ntype Element = {\n  description: string;\n  categories: string[];\n  children: string[];\n};\n\nconst elementsByTag: Record<string, Element> = {};\n\n/**\n * scrape elements table with content model\n */\n{\n  const html = await loadHtmlIndices();\n  const document = parseHtml(html);\n\n  const table = findByTags(document, \"table\").find((table) => {\n    const [caption] = findByTags(table, \"caption\");\n    return getTextContent(caption).toLowerCase().includes(\"list of elements\");\n  });\n  const [tbody] = findByTags(table, \"tbody\");\n  const rows = findByTags(tbody, \"tr\");\n  const parseList = (text: string) => {\n    return text\n      .trim()\n      .split(/\\s*;\\s*/)\n      .map((item) => (item.endsWith(\"*\") ? item.slice(0, -1) : item));\n  };\n  for (const row of rows) {\n    const elements = getTextContent(row.childNodes[0])\n      .trim()\n      .split(/\\s*,\\s*/)\n      .filter((tag) => {\n        // skip \"SVG svg\" amd \"MathML math\"\n        return !tag.includes(\" \");\n      });\n    const description = getTextContent(row.childNodes[1]);\n    let categories = parseList(getTextContent(row.childNodes[2])).map(\n      (item) => {\n        if (item === \"heading\") {\n          // legend and summary refer to it as heading content\n          return \"heading content\";\n        }\n        return item;\n      }\n    );\n    categories.unshift(\"html-element\");\n    let children = parseList(getTextContent(row.childNodes[4]));\n    for (const tag of elements) {\n      if (ignoredTags.includes(tag)) {\n        continue;\n      }\n      // textarea does not have value attribute and text content is used as initial value\n      // introduce fake value attribute to manage initial state similar to input\n      if (tag === \"textarea\") {\n        children = [];\n      }\n      // move interactive category from details to summary\n      // so details content could accept other interactive elements\n      if (tag === \"details\") {\n        categories = categories.filter((item) => item !== \"interactive\");\n      }\n      if (tag === \"summary\") {\n        categories.push(\"interactive\");\n      }\n      // hgroup also accepts paragraphs\n      // https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element\n      // https://github.com/whatwg/html/pull/11524\n      if (tag === \"hgroup\") {\n        children.push(\"p\");\n      }\n      elementsByTag[tag] = {\n        description,\n        categories,\n        children: children.includes(\"empty\") ? [] : children,\n      };\n    }\n  }\n}\n\n{\n  const svg = await loadSvgSinglePage();\n  const document = parseHtml(svg);\n  const summaries = findByClasses(document, \"element-summary\");\n  for (const summary of summaries) {\n    const [tag] = findByClasses(summary, \"element-summary-name\").map((item) =>\n      getTextContent(item).slice(1, -1)\n    );\n    if (ignoredTags.includes(tag)) {\n      continue;\n    }\n    const children: string[] = [];\n    const [dl] = findByTags(summary, \"dl\");\n    for (let index = 0; index < dl.childNodes.length; index += 1) {\n      const child = dl.childNodes[index];\n      if (getTextContent(child).toLowerCase().includes(\"content model\")) {\n        const dd = dl.childNodes[index + 1];\n        for (const elementName of findByClasses(dd, \"element-name\")) {\n          children.push(getTextContent(elementName).slice(1, -1));\n        }\n      }\n    }\n    if (elementsByTag[tag]) {\n      console.info(`${tag} element from SVG specification is skipped`);\n      continue;\n    }\n    const categories = tag === \"svg\" ? [\"flow\", \"phrasing\"] : [\"none\"];\n    categories.unshift(tag === \"svg\" ? \"html-element\" : \"svg-element\");\n    elementsByTag[tag] = {\n      description: \"\",\n      categories,\n      children,\n    };\n  }\n}\n\nconst contentModel = `type Element = {\n  description: string;\n  categories: string[];\n  children: string[];\n};\n\nexport const elementsByTag: Record<string, Element> = ${JSON.stringify(elementsByTag, null, 2)};\n`;\nconst contentModelFile = \"./src/__generated__/elements.ts\";\nawait mkdir(dirname(contentModelFile), { recursive: true });\nawait writeFile(contentModelFile, contentModel);\n\nconst tags: string[] = [];\nfor (const tag of Object.keys(elementsByTag)) {\n  tags.push(tag);\n}\nconst getTagScore = (tag: string) => {\n  if (tag === \"div\") {\n    return 20;\n  }\n  if (tag === \"span\") {\n    return 10;\n  }\n  return 0;\n};\n// put div and span first\ntags.sort((left, right) => getTagScore(right) - getTagScore(left));\nconst tagsContent = `export const tags: string[] = ${JSON.stringify(tags, null, 2)};\n`;\nconst tagsFile = \"../sdk/src/__generated__/tags.ts\";\nawait mkdir(dirname(tagsFile), { recursive: true });\nawait writeFile(tagsFile, tagsContent);\n"
  },
  {
    "path": "packages/html-data/bin/overrides.ts",
    "content": "export const ignoredTags: string[] = [\n  \"base\",\n  \"template\",\n  \"meta\",\n  \"noscript\",\n  \"link\",\n  \"script\",\n  \"style\",\n  \"title\",\n  \"glyph\",\n  \"glyphRef\",\n  \"altGlyph\",\n  \"altGlyphDef\",\n  \"altGlyphItem\",\n  \"animateColor\",\n  \"color-profile\",\n  \"missing-glyph\",\n  \"vkern\",\n  \"hkern\",\n  \"cursor\",\n  \"tref\",\n  \"font\",\n  \"font-face\",\n  \"font-face-format\",\n  \"font-face-name\",\n  \"font-face-src\",\n  \"font-face-uri\",\n];\n"
  },
  {
    "path": "packages/html-data/bin/possible-standard-names.ts",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\n// When adding attributes to the HTML or SVG allowed attribute list, be sure to\n// also add them to this module to ensure casing and incorrect name\n// warnings.\nexport const possibleStandardNames: Record<string, string> = {\n  // HTML\n  accept: \"accept\",\n  acceptcharset: \"acceptCharset\",\n  \"accept-charset\": \"acceptCharset\",\n  accesskey: \"accessKey\",\n  action: \"action\",\n  allowfullscreen: \"allowFullScreen\",\n  alt: \"alt\",\n  as: \"as\",\n  async: \"async\",\n  autocapitalize: \"autoCapitalize\",\n  autocomplete: \"autoComplete\",\n  autocorrect: \"autoCorrect\",\n  autofocus: \"autoFocus\",\n  autoplay: \"autoPlay\",\n  autosave: \"autoSave\",\n  capture: \"capture\",\n  cellpadding: \"cellPadding\",\n  cellspacing: \"cellSpacing\",\n  challenge: \"challenge\",\n  charset: \"charSet\",\n  checked: \"checked\",\n  children: \"children\",\n  cite: \"cite\",\n  class: \"className\",\n  classid: \"classID\",\n  classname: \"className\",\n  cols: \"cols\",\n  colspan: \"colSpan\",\n  content: \"content\",\n  contenteditable: \"contentEditable\",\n  contextmenu: \"contextMenu\",\n  controls: \"controls\",\n  controlslist: \"controlsList\",\n  coords: \"coords\",\n  crossorigin: \"crossOrigin\",\n  dangerouslysetinnerhtml: \"dangerouslySetInnerHTML\",\n  data: \"data\",\n  datetime: \"dateTime\",\n  default: \"default\",\n  defaultchecked: \"defaultChecked\",\n  defaultvalue: \"defaultValue\",\n  defer: \"defer\",\n  dir: \"dir\",\n  disabled: \"disabled\",\n  disablepictureinpicture: \"disablePictureInPicture\",\n  disableremoteplayback: \"disableRemotePlayback\",\n  download: \"download\",\n  draggable: \"draggable\",\n  enctype: \"encType\",\n  enterkeyhint: \"enterKeyHint\",\n  fetchpriority: \"fetchPriority\",\n  for: \"htmlFor\",\n  form: \"form\",\n  formmethod: \"formMethod\",\n  formaction: \"formAction\",\n  formenctype: \"formEncType\",\n  formnovalidate: \"formNoValidate\",\n  formtarget: \"formTarget\",\n  frameborder: \"frameBorder\",\n  headers: \"headers\",\n  height: \"height\",\n  hidden: \"hidden\",\n  high: \"high\",\n  href: \"href\",\n  hreflang: \"hrefLang\",\n  htmlfor: \"htmlFor\",\n  httpequiv: \"httpEquiv\",\n  \"http-equiv\": \"httpEquiv\",\n  icon: \"icon\",\n  id: \"id\",\n  imagesizes: \"imageSizes\",\n  imagesrcset: \"imageSrcSet\",\n  inert: \"inert\",\n  innerhtml: \"innerHTML\",\n  inputmode: \"inputMode\",\n  integrity: \"integrity\",\n  is: \"is\",\n  itemid: \"itemID\",\n  itemprop: \"itemProp\",\n  itemref: \"itemRef\",\n  itemscope: \"itemScope\",\n  itemtype: \"itemType\",\n  keyparams: \"keyParams\",\n  keytype: \"keyType\",\n  kind: \"kind\",\n  label: \"label\",\n  lang: \"lang\",\n  list: \"list\",\n  loop: \"loop\",\n  low: \"low\",\n  manifest: \"manifest\",\n  marginwidth: \"marginWidth\",\n  marginheight: \"marginHeight\",\n  max: \"max\",\n  maxlength: \"maxLength\",\n  media: \"media\",\n  mediagroup: \"mediaGroup\",\n  method: \"method\",\n  min: \"min\",\n  minlength: \"minLength\",\n  multiple: \"multiple\",\n  muted: \"muted\",\n  name: \"name\",\n  nomodule: \"noModule\",\n  nonce: \"nonce\",\n  novalidate: \"noValidate\",\n  open: \"open\",\n  optimum: \"optimum\",\n  pattern: \"pattern\",\n  placeholder: \"placeholder\",\n  playsinline: \"playsInline\",\n  poster: \"poster\",\n  preload: \"preload\",\n  profile: \"profile\",\n  radiogroup: \"radioGroup\",\n  readonly: \"readOnly\",\n  referrerpolicy: \"referrerPolicy\",\n  rel: \"rel\",\n  required: \"required\",\n  reversed: \"reversed\",\n  role: \"role\",\n  rows: \"rows\",\n  rowspan: \"rowSpan\",\n  sandbox: \"sandbox\",\n  scope: \"scope\",\n  scoped: \"scoped\",\n  scrolling: \"scrolling\",\n  seamless: \"seamless\",\n  selected: \"selected\",\n  shape: \"shape\",\n  size: \"size\",\n  sizes: \"sizes\",\n  span: \"span\",\n  spellcheck: \"spellCheck\",\n  src: \"src\",\n  srcdoc: \"srcDoc\",\n  srclang: \"srcLang\",\n  srcset: \"srcSet\",\n  start: \"start\",\n  step: \"step\",\n  style: \"style\",\n  summary: \"summary\",\n  tabindex: \"tabIndex\",\n  target: \"target\",\n  title: \"title\",\n  type: \"type\",\n  usemap: \"useMap\",\n  value: \"value\",\n  width: \"width\",\n  wmode: \"wmode\",\n  wrap: \"wrap\",\n\n  // SVG\n  about: \"about\",\n  accentheight: \"accentHeight\",\n  \"accent-height\": \"accentHeight\",\n  accumulate: \"accumulate\",\n  additive: \"additive\",\n  alignmentbaseline: \"alignmentBaseline\",\n  \"alignment-baseline\": \"alignmentBaseline\",\n  allowreorder: \"allowReorder\",\n  alphabetic: \"alphabetic\",\n  amplitude: \"amplitude\",\n  arabicform: \"arabicForm\",\n  \"arabic-form\": \"arabicForm\",\n  ascent: \"ascent\",\n  attributename: \"attributeName\",\n  attributetype: \"attributeType\",\n  autoreverse: \"autoReverse\",\n  azimuth: \"azimuth\",\n  basefrequency: \"baseFrequency\",\n  baselineshift: \"baselineShift\",\n  \"baseline-shift\": \"baselineShift\",\n  baseprofile: \"baseProfile\",\n  bbox: \"bbox\",\n  begin: \"begin\",\n  bias: \"bias\",\n  by: \"by\",\n  calcmode: \"calcMode\",\n  capheight: \"capHeight\",\n  \"cap-height\": \"capHeight\",\n  clip: \"clip\",\n  clippath: \"clipPath\",\n  \"clip-path\": \"clipPath\",\n  clippathunits: \"clipPathUnits\",\n  cliprule: \"clipRule\",\n  \"clip-rule\": \"clipRule\",\n  color: \"color\",\n  colorinterpolation: \"colorInterpolation\",\n  \"color-interpolation\": \"colorInterpolation\",\n  colorinterpolationfilters: \"colorInterpolationFilters\",\n  \"color-interpolation-filters\": \"colorInterpolationFilters\",\n  colorprofile: \"colorProfile\",\n  \"color-profile\": \"colorProfile\",\n  colorrendering: \"colorRendering\",\n  \"color-rendering\": \"colorRendering\",\n  contentscripttype: \"contentScriptType\",\n  contentstyletype: \"contentStyleType\",\n  cursor: \"cursor\",\n  cx: \"cx\",\n  cy: \"cy\",\n  d: \"d\",\n  datatype: \"datatype\",\n  decelerate: \"decelerate\",\n  descent: \"descent\",\n  diffuseconstant: \"diffuseConstant\",\n  direction: \"direction\",\n  display: \"display\",\n  divisor: \"divisor\",\n  dominantbaseline: \"dominantBaseline\",\n  \"dominant-baseline\": \"dominantBaseline\",\n  dur: \"dur\",\n  dx: \"dx\",\n  dy: \"dy\",\n  edgemode: \"edgeMode\",\n  elevation: \"elevation\",\n  enablebackground: \"enableBackground\",\n  \"enable-background\": \"enableBackground\",\n  end: \"end\",\n  exponent: \"exponent\",\n  externalresourcesrequired: \"externalResourcesRequired\",\n  fill: \"fill\",\n  fillopacity: \"fillOpacity\",\n  \"fill-opacity\": \"fillOpacity\",\n  fillrule: \"fillRule\",\n  \"fill-rule\": \"fillRule\",\n  filter: \"filter\",\n  filterres: \"filterRes\",\n  filterunits: \"filterUnits\",\n  floodopacity: \"floodOpacity\",\n  \"flood-opacity\": \"floodOpacity\",\n  floodcolor: \"floodColor\",\n  \"flood-color\": \"floodColor\",\n  focusable: \"focusable\",\n  fontfamily: \"fontFamily\",\n  \"font-family\": \"fontFamily\",\n  fontsize: \"fontSize\",\n  \"font-size\": \"fontSize\",\n  fontsizeadjust: \"fontSizeAdjust\",\n  \"font-size-adjust\": \"fontSizeAdjust\",\n  fontstretch: \"fontStretch\",\n  \"font-stretch\": \"fontStretch\",\n  fontstyle: \"fontStyle\",\n  \"font-style\": \"fontStyle\",\n  fontvariant: \"fontVariant\",\n  \"font-variant\": \"fontVariant\",\n  fontweight: \"fontWeight\",\n  \"font-weight\": \"fontWeight\",\n  format: \"format\",\n  from: \"from\",\n  fx: \"fx\",\n  fy: \"fy\",\n  g1: \"g1\",\n  g2: \"g2\",\n  glyphname: \"glyphName\",\n  \"glyph-name\": \"glyphName\",\n  glyphorientationhorizontal: \"glyphOrientationHorizontal\",\n  \"glyph-orientation-horizontal\": \"glyphOrientationHorizontal\",\n  glyphorientationvertical: \"glyphOrientationVertical\",\n  \"glyph-orientation-vertical\": \"glyphOrientationVertical\",\n  glyphref: \"glyphRef\",\n  gradienttransform: \"gradientTransform\",\n  gradientunits: \"gradientUnits\",\n  hanging: \"hanging\",\n  horizadvx: \"horizAdvX\",\n  \"horiz-adv-x\": \"horizAdvX\",\n  horizoriginx: \"horizOriginX\",\n  \"horiz-origin-x\": \"horizOriginX\",\n  ideographic: \"ideographic\",\n  imagerendering: \"imageRendering\",\n  \"image-rendering\": \"imageRendering\",\n  in2: \"in2\",\n  in: \"in\",\n  inlist: \"inlist\",\n  intercept: \"intercept\",\n  k1: \"k1\",\n  k2: \"k2\",\n  k3: \"k3\",\n  k4: \"k4\",\n  k: \"k\",\n  kernelmatrix: \"kernelMatrix\",\n  kernelunitlength: \"kernelUnitLength\",\n  kerning: \"kerning\",\n  keypoints: \"keyPoints\",\n  keysplines: \"keySplines\",\n  keytimes: \"keyTimes\",\n  lengthadjust: \"lengthAdjust\",\n  letterspacing: \"letterSpacing\",\n  \"letter-spacing\": \"letterSpacing\",\n  lightingcolor: \"lightingColor\",\n  \"lighting-color\": \"lightingColor\",\n  limitingconeangle: \"limitingConeAngle\",\n  local: \"local\",\n  markerend: \"markerEnd\",\n  \"marker-end\": \"markerEnd\",\n  markerheight: \"markerHeight\",\n  markermid: \"markerMid\",\n  \"marker-mid\": \"markerMid\",\n  markerstart: \"markerStart\",\n  \"marker-start\": \"markerStart\",\n  markerunits: \"markerUnits\",\n  markerwidth: \"markerWidth\",\n  mask: \"mask\",\n  maskcontentunits: \"maskContentUnits\",\n  maskunits: \"maskUnits\",\n  mathematical: \"mathematical\",\n  mode: \"mode\",\n  numoctaves: \"numOctaves\",\n  offset: \"offset\",\n  opacity: \"opacity\",\n  operator: \"operator\",\n  order: \"order\",\n  orient: \"orient\",\n  orientation: \"orientation\",\n  origin: \"origin\",\n  overflow: \"overflow\",\n  overlineposition: \"overlinePosition\",\n  \"overline-position\": \"overlinePosition\",\n  overlinethickness: \"overlineThickness\",\n  \"overline-thickness\": \"overlineThickness\",\n  paintorder: \"paintOrder\",\n  \"paint-order\": \"paintOrder\",\n  panose1: \"panose1\",\n  \"panose-1\": \"panose1\",\n  pathlength: \"pathLength\",\n  patterncontentunits: \"patternContentUnits\",\n  patterntransform: \"patternTransform\",\n  patternunits: \"patternUnits\",\n  pointerevents: \"pointerEvents\",\n  \"pointer-events\": \"pointerEvents\",\n  points: \"points\",\n  pointsatx: \"pointsAtX\",\n  pointsaty: \"pointsAtY\",\n  pointsatz: \"pointsAtZ\",\n  popover: \"popover\",\n  popovertarget: \"popoverTarget\",\n  popovertargetaction: \"popoverTargetAction\",\n  prefix: \"prefix\",\n  preservealpha: \"preserveAlpha\",\n  preserveaspectratio: \"preserveAspectRatio\",\n  primitiveunits: \"primitiveUnits\",\n  property: \"property\",\n  r: \"r\",\n  radius: \"radius\",\n  refx: \"refX\",\n  refy: \"refY\",\n  renderingintent: \"renderingIntent\",\n  \"rendering-intent\": \"renderingIntent\",\n  repeatcount: \"repeatCount\",\n  repeatdur: \"repeatDur\",\n  requiredextensions: \"requiredExtensions\",\n  requiredfeatures: \"requiredFeatures\",\n  resource: \"resource\",\n  restart: \"restart\",\n  result: \"result\",\n  results: \"results\",\n  rotate: \"rotate\",\n  rx: \"rx\",\n  ry: \"ry\",\n  scale: \"scale\",\n  security: \"security\",\n  seed: \"seed\",\n  shaperendering: \"shapeRendering\",\n  \"shape-rendering\": \"shapeRendering\",\n  slope: \"slope\",\n  spacing: \"spacing\",\n  specularconstant: \"specularConstant\",\n  specularexponent: \"specularExponent\",\n  speed: \"speed\",\n  spreadmethod: \"spreadMethod\",\n  startoffset: \"startOffset\",\n  stddeviation: \"stdDeviation\",\n  stemh: \"stemh\",\n  stemv: \"stemv\",\n  stitchtiles: \"stitchTiles\",\n  stopcolor: \"stopColor\",\n  \"stop-color\": \"stopColor\",\n  stopopacity: \"stopOpacity\",\n  \"stop-opacity\": \"stopOpacity\",\n  strikethroughposition: \"strikethroughPosition\",\n  \"strikethrough-position\": \"strikethroughPosition\",\n  strikethroughthickness: \"strikethroughThickness\",\n  \"strikethrough-thickness\": \"strikethroughThickness\",\n  string: \"string\",\n  stroke: \"stroke\",\n  strokedasharray: \"strokeDasharray\",\n  \"stroke-dasharray\": \"strokeDasharray\",\n  strokedashoffset: \"strokeDashoffset\",\n  \"stroke-dashoffset\": \"strokeDashoffset\",\n  strokelinecap: \"strokeLinecap\",\n  \"stroke-linecap\": \"strokeLinecap\",\n  strokelinejoin: \"strokeLinejoin\",\n  \"stroke-linejoin\": \"strokeLinejoin\",\n  strokemiterlimit: \"strokeMiterlimit\",\n  \"stroke-miterlimit\": \"strokeMiterlimit\",\n  strokewidth: \"strokeWidth\",\n  \"stroke-width\": \"strokeWidth\",\n  strokeopacity: \"strokeOpacity\",\n  \"stroke-opacity\": \"strokeOpacity\",\n  suppresscontenteditablewarning: \"suppressContentEditableWarning\",\n  suppresshydrationwarning: \"suppressHydrationWarning\",\n  surfacescale: \"surfaceScale\",\n  systemlanguage: \"systemLanguage\",\n  tablevalues: \"tableValues\",\n  targetx: \"targetX\",\n  targety: \"targetY\",\n  textanchor: \"textAnchor\",\n  \"text-anchor\": \"textAnchor\",\n  textdecoration: \"textDecoration\",\n  \"text-decoration\": \"textDecoration\",\n  textlength: \"textLength\",\n  textrendering: \"textRendering\",\n  \"text-rendering\": \"textRendering\",\n  to: \"to\",\n  transform: \"transform\",\n  transformorigin: \"transformOrigin\",\n  \"transform-origin\": \"transformOrigin\",\n  typeof: \"typeof\",\n  u1: \"u1\",\n  u2: \"u2\",\n  underlineposition: \"underlinePosition\",\n  \"underline-position\": \"underlinePosition\",\n  underlinethickness: \"underlineThickness\",\n  \"underline-thickness\": \"underlineThickness\",\n  unicode: \"unicode\",\n  unicodebidi: \"unicodeBidi\",\n  \"unicode-bidi\": \"unicodeBidi\",\n  unicoderange: \"unicodeRange\",\n  \"unicode-range\": \"unicodeRange\",\n  unitsperem: \"unitsPerEm\",\n  \"units-per-em\": \"unitsPerEm\",\n  unselectable: \"unselectable\",\n  valphabetic: \"vAlphabetic\",\n  \"v-alphabetic\": \"vAlphabetic\",\n  values: \"values\",\n  vectoreffect: \"vectorEffect\",\n  \"vector-effect\": \"vectorEffect\",\n  version: \"version\",\n  vertadvy: \"vertAdvY\",\n  \"vert-adv-y\": \"vertAdvY\",\n  vertoriginx: \"vertOriginX\",\n  \"vert-origin-x\": \"vertOriginX\",\n  vertoriginy: \"vertOriginY\",\n  \"vert-origin-y\": \"vertOriginY\",\n  vhanging: \"vHanging\",\n  \"v-hanging\": \"vHanging\",\n  videographic: \"vIdeographic\",\n  \"v-ideographic\": \"vIdeographic\",\n  viewbox: \"viewBox\",\n  viewtarget: \"viewTarget\",\n  visibility: \"visibility\",\n  vmathematical: \"vMathematical\",\n  \"v-mathematical\": \"vMathematical\",\n  vocab: \"vocab\",\n  widths: \"widths\",\n  wordspacing: \"wordSpacing\",\n  \"word-spacing\": \"wordSpacing\",\n  writingmode: \"writingMode\",\n  \"writing-mode\": \"writingMode\",\n  x1: \"x1\",\n  x2: \"x2\",\n  x: \"x\",\n  xchannelselector: \"xChannelSelector\",\n  xheight: \"xHeight\",\n  \"x-height\": \"xHeight\",\n  xlinkactuate: \"xlinkActuate\",\n  \"xlink:actuate\": \"xlinkActuate\",\n  xlinkarcrole: \"xlinkArcrole\",\n  \"xlink:arcrole\": \"xlinkArcrole\",\n  xlinkhref: \"xlinkHref\",\n  \"xlink:href\": \"xlinkHref\",\n  xlinkrole: \"xlinkRole\",\n  \"xlink:role\": \"xlinkRole\",\n  xlinkshow: \"xlinkShow\",\n  \"xlink:show\": \"xlinkShow\",\n  xlinktitle: \"xlinkTitle\",\n  \"xlink:title\": \"xlinkTitle\",\n  xlinktype: \"xlinkType\",\n  \"xlink:type\": \"xlinkType\",\n  xmlbase: \"xmlBase\",\n  \"xml:base\": \"xmlBase\",\n  xmllang: \"xmlLang\",\n  \"xml:lang\": \"xmlLang\",\n  xmlns: \"xmlns\",\n  \"xml:space\": \"xmlSpace\",\n  xmlnsxlink: \"xmlnsXlink\",\n  \"xmlns:xlink\": \"xmlnsXlink\",\n  xmlspace: \"xmlSpace\",\n  y1: \"y1\",\n  y2: \"y2\",\n  y: \"y\",\n  ychannelselector: \"yChannelSelector\",\n  z: \"z\",\n  zoomandpan: \"zoomAndPan\",\n};\n"
  },
  {
    "path": "packages/html-data/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/html-data\",\n  \"version\": \"0.0.0\",\n  \"description\": \"HTML Data\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit -p tsconfig.typecheck.json\",\n    \"build:elements\": \"tsx --conditions=webstudio ./bin/elements.ts && prettier --write ./src/__generated__\",\n    \"build:attributes\": \"tsx --conditions=webstudio ./bin/attributes.ts && prettier --write ./src/__generated__ ../react-sdk/src/__generated__\",\n    \"build:aria\": \"tsx --conditions=webstudio ./bin/aria.ts && prettier --write ./src/__generated__\"\n  },\n  \"devDependencies\": {\n    \"@emotion/hash\": \"^0.9.2\",\n    \"@types/aria-query\": \"^5.0.4\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"aria-query\": \"^5.3.2\",\n    \"parse5\": \"7.3.0\"\n  },\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/html-data/src/__generated__/aria-jsx-test.tsx",
    "content": "const Page = () => {\n  return (\n    <div\n      role={\"\"}\n      aria-activedescendant={\"\"}\n      aria-atomic={true}\n      aria-autocomplete={\"inline\"}\n      aria-braillelabel={\"\"}\n      aria-brailleroledescription={\"\"}\n      aria-busy={true}\n      aria-checked={\"false\"}\n      aria-colcount={0}\n      aria-colindex={0}\n      aria-colspan={0}\n      aria-controls={\"\"}\n      aria-current={\"page\"}\n      aria-describedby={\"\"}\n      aria-description={\"\"}\n      aria-details={\"\"}\n      aria-disabled={true}\n      aria-dropeffect={\"copy\"}\n      aria-errormessage={\"\"}\n      aria-expanded={true}\n      aria-flowto={\"\"}\n      aria-grabbed={true}\n      aria-haspopup={\"false\"}\n      aria-hidden={true}\n      aria-invalid={\"grammar\"}\n      aria-keyshortcuts={\"\"}\n      aria-label={\"\"}\n      aria-labelledby={\"\"}\n      aria-level={0}\n      aria-live={\"assertive\"}\n      aria-modal={true}\n      aria-multiline={true}\n      aria-multiselectable={true}\n      aria-orientation={\"vertical\"}\n      aria-owns={\"\"}\n      aria-placeholder={\"\"}\n      aria-posinset={0}\n      aria-pressed={\"false\"}\n      aria-readonly={true}\n      aria-relevant={\"additions\"}\n      aria-required={true}\n      aria-roledescription={\"\"}\n      aria-rowcount={0}\n      aria-rowindex={0}\n      aria-rowspan={0}\n      aria-selected={true}\n      aria-setsize={0}\n      aria-sort={\"ascending\"}\n      aria-valuemax={0}\n      aria-valuemin={0}\n      aria-valuenow={0}\n      aria-valuetext={\"\"}\n    />\n  );\n};\nexport { Page };\n"
  },
  {
    "path": "packages/html-data/src/__generated__/aria.ts",
    "content": "type Attribute = {\n  name: string;\n  description: string;\n  required?: boolean;\n  type: \"string\" | \"boolean\" | \"number\" | \"select\" | \"url\";\n  options?: string[];\n};\n\nexport const ariaAttributes: Attribute[] = [\n  {\n    name: \"role\",\n    description:\n      \"Defines an explicit role for an element for use by assistive technologies.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-activedescendant\",\n    description:\n      \"Identifies the currently active element when DOM focus is on a composite widget, combobox, textbox, group, or application.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-atomic\",\n    description:\n      \"Indicates whether assistive technologies will present all, or only parts of, the changed region based on the change notifications defined by the aria-relevant attribute.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-autocomplete\",\n    description:\n      \"Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for a combobox, searchbox, or textbox and specifies how predictions would be presented if they were made.\",\n    type: \"select\",\n    options: [\"inline\", \"list\", \"both\", \"none\"],\n  },\n  {\n    name: \"aria-braillelabel\",\n    description:\n      \"Defines a string value that labels the current element, which is intended to be converted into Braille. See related aria-label.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-brailleroledescription\",\n    description:\n      \"Defines a human-readable, author-localized abbreviated description for the role of an element, which is intended to be converted into Braille. See related aria-roledescription.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-busy\",\n    description:\n      \"Indicates an element is being modified and that assistive technologies could wait until the modifications are complete before exposing them to the user.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-checked\",\n    description:\n      'Indicates the current \"checked\" state of checkboxes, radio buttons, and other widgets. See related aria-pressed and aria-selected.',\n    type: \"select\",\n    options: [\"false\", \"mixed\", \"true\"],\n  },\n  {\n    name: \"aria-colcount\",\n    description:\n      \"Defines the total number of columns in a table, grid, or treegrid. See related aria-colindex.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-colindex\",\n    description:\n      \"Defines an element's column index or position with respect to the total number of columns within a table, grid, or treegrid. See related aria-colindextext, aria-colcount, and aria-colspan.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-colspan\",\n    description:\n      \"Defines the number of columns spanned by a cell or gridcell within a table, grid, or treegrid. See related aria-colindex and aria-rowspan.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-controls\",\n    description:\n      \"Identifies the element (or elements) whose contents or presence are controlled by the current element. See related aria-owns.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-current\",\n    description:\n      \"Indicates the element that represents the current item within a container or set of related elements.\",\n    type: \"select\",\n    options: [\"page\", \"step\", \"location\", \"date\", \"time\", \"true\", \"false\"],\n  },\n  {\n    name: \"aria-describedby\",\n    description:\n      \"Identifies the element (or elements) that describes the object. See related aria-labelledby and aria-description.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-description\",\n    description:\n      \"Defines a string value that describes or annotates the current element. See related aria-describedby.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-details\",\n    description:\n      \"Identifies the element (or elements) that provide additional information related to the object. See related aria-describedby.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-disabled\",\n    description:\n      \"Indicates that the element is perceivable but disabled, so it is not editable or otherwise operable. See related aria-hidden and aria-readonly.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-dropeffect\",\n    description:\n      \"[Deprecated in ARIA 1.1] Indicates what functions can be performed when a dragged object is released on the drop target.\",\n    type: \"select\",\n    options: [\"copy\", \"execute\", \"link\", \"move\", \"none\", \"popup\"],\n  },\n  {\n    name: \"aria-errormessage\",\n    description:\n      \"Identifies the element (or elements) that provides an error message for an object. See related aria-invalid and aria-describedby.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-expanded\",\n    description:\n      \"Indicates whether a grouping element that is the accessibility child of or is controlled by this element is expanded or collapsed.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-flowto\",\n    description:\n      \"Identifies the next element (or elements) in an alternate reading order of content which, at the user's discretion, allows assistive technology to override the general default of reading in document source order.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-grabbed\",\n    description:\n      '[Deprecated in ARIA 1.1] Indicates an element\\'s \"grabbed\" state in a drag-and-drop operation.',\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-haspopup\",\n    description:\n      \"Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element.\",\n    type: \"select\",\n    options: [\"false\", \"true\", \"menu\", \"listbox\", \"tree\", \"grid\", \"dialog\"],\n  },\n  {\n    name: \"aria-hidden\",\n    description:\n      \"Indicates whether the element is exposed to an accessibility API. See related aria-disabled.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-invalid\",\n    description:\n      \"Indicates the entered value does not conform to the format expected by the application. See related aria-errormessage.\",\n    type: \"select\",\n    options: [\"grammar\", \"false\", \"spelling\", \"true\"],\n  },\n  {\n    name: \"aria-keyshortcuts\",\n    description:\n      \"Defines keyboard shortcuts that an author has implemented to activate or give focus to an element.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-label\",\n    description:\n      \"Provides the accessible name that describes an interactive element if no other accessible name exists, for example in a button that contains an image with no text.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-labelledby\",\n    description:\n      \"Identifies the element (or elements) that labels the current element. See related aria-label and aria-describedby.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-level\",\n    description:\n      \"Defines the hierarchical level of an element within a structure.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-live\",\n    description:\n      \"Indicates that an element will be updated, and describes the types of updates the user agents, assistive technologies, and user can expect from the live region.\",\n    type: \"select\",\n    options: [\"assertive\", \"off\", \"polite\"],\n  },\n  {\n    name: \"aria-modal\",\n    description: \"Indicates whether an element is modal when displayed.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-multiline\",\n    description:\n      \"Indicates whether a text box accepts multiple lines of input or only a single line.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-multiselectable\",\n    description:\n      \"Indicates that the user can select more than one item from the current selectable descendants.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-orientation\",\n    description:\n      \"Indicates whether the element's orientation is horizontal, vertical, or unknown/ambiguous.\",\n    type: \"select\",\n    options: [\"vertical\", \"undefined\", \"horizontal\"],\n  },\n  {\n    name: \"aria-owns\",\n    description:\n      \"Identifies an element (or elements) in order to define a visual, functional, or contextual parent/child relationship between DOM elements where the DOM hierarchy cannot be used to represent the relationship. See related aria-controls.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-placeholder\",\n    description:\n      \"Defines a short hint (a word or short phrase) intended to aid the user with data entry when the control has no value. A hint could be a sample value or a brief description of the expected format.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-posinset\",\n    description:\n      \"Defines an element's number or position in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM. See related aria-setsize.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-pressed\",\n    description:\n      'Indicates the current \"pressed\" state of toggle buttons. See related aria-checked and aria-selected.',\n    type: \"select\",\n    options: [\"false\", \"mixed\", \"true\"],\n  },\n  {\n    name: \"aria-readonly\",\n    description:\n      \" Indicates that the element is not editable, but is otherwise operable. See related aria-disabled.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-relevant\",\n    description:\n      \"Indicates what notifications the user agent will trigger when the accessibility tree within a live region is modified. See related aria-atomic.\",\n    type: \"select\",\n    options: [\"additions\", \"all\", \"removals\", \"text\"],\n  },\n  {\n    name: \"aria-required\",\n    description:\n      \"Indicates that user input is required on the element before a form can be submitted.\",\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-roledescription\",\n    description:\n      \"Defines a human-readable, author-localized description for the role of an element.\",\n    type: \"string\",\n  },\n  {\n    name: \"aria-rowcount\",\n    description:\n      \"Defines the total number of rows in a table, grid, or treegrid. See related aria-rowindex.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-rowindex\",\n    description:\n      \"Defines an element's row index or position with respect to the total number of rows within a table, grid, or treegrid. See related aria-rowindextext, aria-rowcount, and aria-rowspan.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-rowspan\",\n    description:\n      \"Defines the number of rows spanned by a cell or gridcell within a table, grid, or treegrid. See related aria-rowindex and aria-colspan.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-selected\",\n    description:\n      'Indicates the current \"selected\" state of various widgets. See related aria-checked and aria-pressed.',\n    type: \"boolean\",\n  },\n  {\n    name: \"aria-setsize\",\n    description:\n      \"Defines the number of items in the current set of listitems or treeitems. Not required if all elements in the set are present in the DOM. See related aria-posinset.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-sort\",\n    description:\n      \"Indicates if items in a table or grid are sorted in ascending or descending order.\",\n    type: \"select\",\n    options: [\"ascending\", \"descending\", \"none\", \"other\"],\n  },\n  {\n    name: \"aria-valuemax\",\n    description: \"Defines the maximum allowed value for a range widget.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-valuemin\",\n    description: \"Defines the minimum allowed value for a range widget.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-valuenow\",\n    description:\n      \"Defines the current value for a range widget. See related aria-valuetext.\",\n    type: \"number\",\n  },\n  {\n    name: \"aria-valuetext\",\n    description:\n      \"Defines the human readable text alternative of aria-valuenow for a range widget.\",\n    type: \"string\",\n  },\n];\n"
  },
  {
    "path": "packages/html-data/src/__generated__/attributes-jsx-test.tsx",
    "content": "const Page = () => {\n  return (\n    <body>\n      <div\n        accessKey={\"\"}\n        autoCapitalize={\"on\"}\n        autoCorrect={\"on\"}\n        autoFocus={true}\n        contentEditable={\"true\"}\n        dir={\"ltr\"}\n        draggable={\"true\"}\n        hidden={true}\n        id={\"\"}\n        inputMode={\"none\"}\n        is={\"\"}\n        itemID={\"\"}\n        itemProp={\"\"}\n        itemRef={\"\"}\n        itemScope={true}\n        itemType={\"\"}\n        lang={\"\"}\n        nonce={\"\"}\n        slot={\"\"}\n        spellCheck={\"true\"}\n        tabIndex={0}\n        title={\"\"}\n        translate={\"yes\"}\n        className={`${\"\"}`}\n      />\n      <a\n        download={true}\n        href={\"\"}\n        hrefLang={\"\"}\n        ping={\"\"}\n        referrerPolicy={\"\"}\n        rel={\"\"}\n        target={\"_blank\"}\n        type={\"\"}\n      />\n      <abbr title={\"\"} />\n      <animate\n        accumulate={\"none\"}\n        additive={\"replace\"}\n        alignmentBaseline={\"auto\"}\n        attributeName={\"\"}\n        attributeType={\"\"}\n        baselineShift={\"\"}\n        begin={\"\"}\n        by={\"\"}\n        calcMode={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        dur={\"\"}\n        enableBackground={\"\"}\n        end={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        from={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        keySplines={\"\"}\n        keyTimes={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        max={\"\"}\n        min={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        repeatCount={\"\"}\n        repeatDur={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        restart={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        to={\"\"}\n        unicodeBidi={\"normal\"}\n        values={\"\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <animateMotion\n        accumulate={\"none\"}\n        additive={\"replace\"}\n        begin={\"\"}\n        by={\"\"}\n        calcMode={\"\"}\n        dur={\"\"}\n        end={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        from={\"\"}\n        id={\"\"}\n        keyPoints={\"\"}\n        keySplines={\"\"}\n        keyTimes={\"\"}\n        max={\"\"}\n        min={\"\"}\n        origin={\"\"}\n        path={\"\"}\n        repeatCount={\"\"}\n        repeatDur={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        restart={\"\"}\n        rotate={\"\"}\n        systemLanguage={\"\"}\n        to={\"\"}\n        values={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <animateTransform\n        accumulate={\"none\"}\n        additive={\"replace\"}\n        attributeName={\"\"}\n        attributeType={\"\"}\n        begin={\"\"}\n        by={\"\"}\n        calcMode={\"\"}\n        dur={\"\"}\n        end={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        from={\"\"}\n        id={\"\"}\n        keySplines={\"\"}\n        keyTimes={\"\"}\n        max={\"\"}\n        min={\"\"}\n        repeatCount={\"\"}\n        repeatDur={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        restart={\"\"}\n        systemLanguage={\"\"}\n        to={\"\"}\n        type={\"\"}\n        values={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <area\n        alt={\"\"}\n        coords={\"\"}\n        download={\"\"}\n        href={\"\"}\n        referrerPolicy={\"\"}\n        rel={\"\"}\n        shape={\"circle\"}\n        target={\"_blank\"}\n      />\n      <audio\n        autoPlay={true}\n        controls={true}\n        crossOrigin={\"anonymous\"}\n        loop={true}\n        muted={true}\n        preload={\"none\"}\n        src={\"\"}\n      />\n      <bdo dir={\"ltr\"} />\n      <blockquote cite={\"\"} />\n      <button\n        disabled={true}\n        form={\"\"}\n        formAction={\"\"}\n        formEncType={\"application/x-www-form-urlencoded\"}\n        formMethod={\"get\"}\n        formNoValidate={true}\n        formTarget={\"_blank\"}\n        name={\"\"}\n        type={\"submit\"}\n        value={\"\"}\n      />\n      <canvas height={0} width={0} />\n      <circle\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        cx={\"\"}\n        cy={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        r={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <clipPath\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        clipPathUnits={\"\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <col span={0} />\n      <colgroup span={0} />\n      <data value={\"\"} />\n      <defs\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <del cite={\"\"} dateTime={\"\"} />\n      <desc\n        id={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <details name={\"\"} open={true} />\n      <dfn title={\"\"} />\n      <dialog open={true} />\n      <ellipse\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        cx={\"\"}\n        cy={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        rx={\"\"}\n        ry={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <embed height={0} src={\"\"} type={\"\"} width={0} />\n      <feBlend\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        in2={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        mode={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feColorMatrix\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        type={\"\"}\n        unicodeBidi={\"normal\"}\n        values={\"\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feComponentTransfer\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feComposite\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        in2={\"\"}\n        k1={\"\"}\n        k2={\"\"}\n        k3={\"\"}\n        k4={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        operator={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feConvolveMatrix\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        bias={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        divisor={\"\"}\n        dominantBaseline={\"auto\"}\n        edgeMode={\"\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kernelMatrix={\"\"}\n        kernelUnitLength={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        order={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAlpha={\"true\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        targetX={\"\"}\n        targetY={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feDiffuseLighting\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        diffuseConstant={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kernelUnitLength={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        surfaceScale={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feDisplacementMap\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        in2={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        scale={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xChannelSelector={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        yChannelSelector={\"\"}\n        className={`${\"\"}`}\n      />\n      <feDistantLight\n        azimuth={\"\"}\n        elevation={\"\"}\n        id={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <feFlood\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feFuncA\n        amplitude={\"\"}\n        exponent={\"\"}\n        id={\"\"}\n        intercept={\"\"}\n        offset={\"\"}\n        slope={\"\"}\n        tableValues={\"\"}\n        type={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <feFuncB\n        amplitude={\"\"}\n        exponent={\"\"}\n        id={\"\"}\n        intercept={\"\"}\n        offset={\"\"}\n        slope={\"\"}\n        tableValues={\"\"}\n        type={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <feFuncG\n        amplitude={\"\"}\n        exponent={\"\"}\n        id={\"\"}\n        intercept={\"\"}\n        offset={\"\"}\n        slope={\"\"}\n        tableValues={\"\"}\n        type={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <feFuncR\n        amplitude={\"\"}\n        exponent={\"\"}\n        id={\"\"}\n        intercept={\"\"}\n        offset={\"\"}\n        slope={\"\"}\n        tableValues={\"\"}\n        type={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <feGaussianBlur\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stdDeviation={\"\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feImage\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAspectRatio={\"\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feMerge\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feMergeNode id={\"\"} in={\"\"} xmlBase={\"\"} xmlLang={\"\"} xmlSpace={\"\"} />\n      <feMorphology\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        operator={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        radius={\"\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feOffset\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        dx={\"\"}\n        dy={\"\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <fePointLight\n        id={\"\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        z={\"\"}\n      />\n      <feSpecularLighting\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kernelUnitLength={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        specularConstant={\"\"}\n        specularExponent={\"\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        surfaceScale={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feSpotLight\n        id={\"\"}\n        limitingConeAngle={\"\"}\n        pointsAtX={\"\"}\n        pointsAtY={\"\"}\n        pointsAtZ={\"\"}\n        specularExponent={\"\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        z={\"\"}\n      />\n      <feTile\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        in={\"\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <feTurbulence\n        alignmentBaseline={\"auto\"}\n        baseFrequency={\"\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        numOctaves={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        result={\"\"}\n        seed={\"\"}\n        shapeRendering={\"auto\"}\n        stitchTiles={\"\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        type={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <fieldset disabled={true} form={\"\"} name={\"\"} />\n      <filter\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        filterRes={\"\"}\n        filterUnits={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        primitiveUnits={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <foreignObject\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <form\n        acceptCharset={\"\"}\n        action={\"\"}\n        autoComplete={\"on\"}\n        encType={\"application/x-www-form-urlencoded\"}\n        method={\"get\"}\n        name={\"\"}\n        noValidate={true}\n        target={\"_blank\"}\n      />\n      <g\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <iframe\n        allow={\"\"}\n        allowFullScreen={true}\n        height={0}\n        loading={\"lazy\"}\n        name={\"\"}\n        referrerPolicy={\"\"}\n        sandbox={\"\"}\n        src={\"\"}\n        srcDoc={\"\"}\n        width={0}\n      />\n      <image\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAspectRatio={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <img\n        alt={\"\"}\n        crossOrigin={\"anonymous\"}\n        decoding={\"sync\"}\n        fetchPriority={\"auto\"}\n        height={0}\n        loading={\"lazy\"}\n        referrerPolicy={\"\"}\n        sizes={\"\"}\n        src={\"\"}\n        srcSet={\"\"}\n        useMap={\"\"}\n        width={0}\n      />\n      <input\n        accept={\"\"}\n        alt={\"\"}\n        autoComplete={\"\"}\n        checked={true}\n        disabled={true}\n        form={\"\"}\n        formAction={\"\"}\n        formEncType={\"application/x-www-form-urlencoded\"}\n        formMethod={\"get\"}\n        formNoValidate={true}\n        formTarget={\"_blank\"}\n        height={0}\n        list={\"\"}\n        max={\"\"}\n        maxLength={0}\n        min={\"\"}\n        minLength={0}\n        multiple={true}\n        name={\"\"}\n        pattern={\"\"}\n        placeholder={\"\"}\n        readOnly={true}\n        required={true}\n        size={0}\n        src={\"\"}\n        step={0}\n        title={\"\"}\n        type={\"hidden\"}\n        value={\"\"}\n        width={0}\n      />\n      <ins cite={\"\"} dateTime={\"\"} />\n      <label htmlFor={\"\"} />\n      <li value={0} />\n      <line\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x1={\"\"}\n        x2={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y1={\"\"}\n        y2={\"\"}\n        className={`${\"\"}`}\n      />\n      <linearGradient\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        gradientTransform={\"\"}\n        gradientUnits={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        shapeRendering={\"auto\"}\n        spreadMethod={\"\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x1={\"\"}\n        x2={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y1={\"\"}\n        y2={\"\"}\n        className={`${\"\"}`}\n      />\n      <map name={\"\"} />\n      <marker\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        markerHeight={\"\"}\n        markerUnits={\"\"}\n        markerWidth={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        orient={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAspectRatio={\"\"}\n        refX={\"\"}\n        refY={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        viewBox={\"\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <mask\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        maskContentUnits={\"\"}\n        maskUnits={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <metadata id={\"\"} xmlBase={\"\"} xmlLang={\"\"} xmlSpace={\"\"} />\n      <meter high={0} low={0} max={0} min={0} optimum={0} value={0} />\n      <mpath\n        externalResourcesRequired={\"true\"}\n        id={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <object data={\"\"} form={\"\"} height={0} name={\"\"} type={\"\"} width={0} />\n      <ol reversed={true} start={0} type={\"1\"} />\n      <optgroup disabled={true} label={\"\"} />\n      <option disabled={true} label={\"\"} value={\"\"} />\n      <output htmlFor={\"\"} form={\"\"} name={\"\"} />\n      <path\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        d={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pathLength={\"\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <pattern\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        patternContentUnits={\"\"}\n        patternTransform={\"\"}\n        patternUnits={\"\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAspectRatio={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        viewBox={\"\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <polygon\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        points={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <polyline\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        points={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <progress max={0} value={0} />\n      <q cite={\"\"} />\n      <radialGradient\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        cx={\"\"}\n        cy={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        fx={\"\"}\n        fy={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        gradientTransform={\"\"}\n        gradientUnits={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        r={\"\"}\n        shapeRendering={\"auto\"}\n        spreadMethod={\"\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <rect\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        rx={\"\"}\n        ry={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <select\n        autoComplete={\"\"}\n        disabled={true}\n        form={\"\"}\n        name={\"\"}\n        required={true}\n        size={0}\n        value={\"\"}\n      />\n      <set\n        attributeName={\"\"}\n        attributeType={\"\"}\n        begin={\"\"}\n        dur={\"\"}\n        end={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        id={\"\"}\n        max={\"\"}\n        min={\"\"}\n        repeatCount={\"\"}\n        repeatDur={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        restart={\"\"}\n        systemLanguage={\"\"}\n        to={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n      />\n      <slot name={\"\"} />\n      <source\n        height={0}\n        media={\"\"}\n        sizes={\"\"}\n        src={\"\"}\n        srcSet={\"\"}\n        type={\"\"}\n        width={0}\n      />\n      <stop\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        offset={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <svg\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        baseProfile={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        contentScriptType={\"\"}\n        contentStyleType={\"\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAspectRatio={\"\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        version={\"\"}\n        viewBox={\"\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        zoomAndPan={\"\"}\n        className={`${\"\"}`}\n      />\n      <switch\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <symbol\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        preserveAspectRatio={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        viewBox={\"\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <td colSpan={0} headers={\"\"} rowSpan={0} />\n      <text\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        dx={\"\"}\n        dy={\"\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        lengthAdjust={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        rotate={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        textLength={\"\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <textPath\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        method={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        spacing={\"\"}\n        startOffset={\"\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        className={`${\"\"}`}\n      />\n      <textarea\n        autoComplete={\"\"}\n        cols={0}\n        dirName={\"\"}\n        disabled={true}\n        form={\"\"}\n        maxLength={0}\n        minLength={0}\n        name={\"\"}\n        placeholder={\"\"}\n        readOnly={true}\n        required={true}\n        rows={0}\n        value={\"\"}\n        wrap={\"soft\"}\n      />\n      <th abbr={\"\"} colSpan={0} headers={\"\"} rowSpan={0} scope={\"row\"} />\n      <time dateTime={\"\"} />\n      <track\n        default={true}\n        kind={\"subtitles\"}\n        label={\"\"}\n        src={\"\"}\n        srcLang={\"\"}\n      />\n      <tspan\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        dx={\"\"}\n        dy={\"\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        lengthAdjust={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        rotate={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        textLength={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <use\n        alignmentBaseline={\"auto\"}\n        baselineShift={\"\"}\n        clip={\"\"}\n        clipPath={\"\"}\n        clipRule={\"nonzero\"}\n        color={\"\"}\n        colorInterpolation={\"auto\"}\n        colorInterpolationFilters={\"auto\"}\n        colorProfile={\"\"}\n        colorRendering={\"auto\"}\n        cursor={\"\"}\n        direction={\"ltr\"}\n        display={\"inline\"}\n        dominantBaseline={\"auto\"}\n        enableBackground={\"\"}\n        externalResourcesRequired={\"true\"}\n        fill={\"\"}\n        fillOpacity={\"\"}\n        fillRule={\"nonzero\"}\n        filter={\"\"}\n        floodColor={\"\"}\n        floodOpacity={\"\"}\n        fontFamily={\"\"}\n        fontSize={\"\"}\n        fontSizeAdjust={\"\"}\n        fontStretch={\"normal\"}\n        fontStyle={\"normal\"}\n        fontVariant={\"normal\"}\n        fontWeight={\"\"}\n        glyphOrientationHorizontal={\"\"}\n        glyphOrientationVertical={\"\"}\n        height={\"\"}\n        id={\"\"}\n        imageRendering={\"auto\"}\n        kerning={\"\"}\n        letterSpacing={\"\"}\n        lightingColor={\"\"}\n        markerEnd={\"\"}\n        markerMid={\"\"}\n        markerStart={\"\"}\n        mask={\"\"}\n        opacity={\"\"}\n        overflow={\"visible\"}\n        pointerEvents={\"visiblePainted\"}\n        requiredExtensions={\"\"}\n        requiredFeatures={\"\"}\n        shapeRendering={\"auto\"}\n        stopColor={\"\"}\n        stopOpacity={\"\"}\n        stroke={\"\"}\n        strokeDasharray={\"\"}\n        strokeDashoffset={\"\"}\n        strokeLinecap={\"butt\"}\n        strokeLinejoin={\"miter\"}\n        strokeMiterlimit={\"\"}\n        strokeOpacity={\"\"}\n        strokeWidth={\"\"}\n        systemLanguage={\"\"}\n        textAnchor={\"start\"}\n        textDecoration={\"\"}\n        textRendering={\"auto\"}\n        transform={\"\"}\n        unicodeBidi={\"normal\"}\n        visibility={\"visible\"}\n        width={\"\"}\n        wordSpacing={\"\"}\n        writingMode={\"lr-tb\"}\n        x={\"\"}\n        xlinkActuate={\"\"}\n        xlinkArcrole={\"\"}\n        xlinkHref={\"\"}\n        xlinkRole={\"\"}\n        xlinkShow={\"\"}\n        xlinkTitle={\"\"}\n        xlinkType={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        y={\"\"}\n        className={`${\"\"}`}\n      />\n      <video\n        autoPlay={true}\n        controls={true}\n        crossOrigin={\"anonymous\"}\n        height={0}\n        loop={true}\n        muted={true}\n        playsInline={true}\n        poster={\"\"}\n        preload={\"none\"}\n        src={\"\"}\n        width={0}\n      />\n      <view\n        externalResourcesRequired={\"true\"}\n        id={\"\"}\n        preserveAspectRatio={\"\"}\n        viewBox={\"\"}\n        viewTarget={\"\"}\n        xmlBase={\"\"}\n        xmlLang={\"\"}\n        xmlSpace={\"\"}\n        zoomAndPan={\"\"}\n      />\n    </body>\n  );\n};\nexport { Page };\n"
  },
  {
    "path": "packages/html-data/src/__generated__/attributes.ts",
    "content": "type Attribute = {\n  name: string;\n  description: string;\n  required?: boolean;\n  type: \"string\" | \"boolean\" | \"number\" | \"select\" | \"url\";\n  options?: string[];\n};\n\nconst attribute_accumulate_1kar0jt: Attribute = {\n  name: \"accumulate\",\n  description: \"\",\n  type: \"select\",\n  options: [\"none\", \"sum\"],\n};\n\nconst attribute_additive_fsyc7w: Attribute = {\n  name: \"additive\",\n  description: \"\",\n  type: \"select\",\n  options: [\"replace\", \"sum\"],\n};\n\nconst attribute_begin_4942fq: Attribute = {\n  name: \"begin\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_by_1s7zscl: Attribute = {\n  name: \"by\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_calcMode_un1adz: Attribute = {\n  name: \"calcMode\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_dur_e0odti: Attribute = {\n  name: \"dur\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_end_131xh48: Attribute = {\n  name: \"end\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_externalResourcesRequired_1bnmqma: Attribute = {\n  name: \"externalResourcesRequired\",\n  description: \"\",\n  type: \"select\",\n  options: [\"true\", \"false\"],\n};\n\nconst attribute_fill_rz4yt2: Attribute = {\n  name: \"fill\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_from_w9fqm8: Attribute = {\n  name: \"from\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_id_1wtm880: Attribute = {\n  name: \"id\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_keySplines_911cut: Attribute = {\n  name: \"keySplines\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_keyTimes_pvnmun: Attribute = {\n  name: \"keyTimes\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_max_r1k4jl: Attribute = {\n  name: \"max\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_min_a76syq: Attribute = {\n  name: \"min\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_repeatCount_1yxzal3: Attribute = {\n  name: \"repeatCount\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_repeatDur_l7trje: Attribute = {\n  name: \"repeatDur\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_requiredExtensions_c4vtka: Attribute = {\n  name: \"requiredExtensions\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_requiredFeatures_15shkxl: Attribute = {\n  name: \"requiredFeatures\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_restart_wiuzbf: Attribute = {\n  name: \"restart\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_systemLanguage_ytoswp: Attribute = {\n  name: \"systemLanguage\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_to_1xfnnez: Attribute = {\n  name: \"to\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_values_1jkm3eq: Attribute = {\n  name: \"values\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_actuate_h23qr: Attribute = {\n  name: \"xlink:actuate\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_arcrole_lqsmgf: Attribute = {\n  name: \"xlink:arcrole\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_href_18c6swo: Attribute = {\n  name: \"xlink:href\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_role_7sfop7: Attribute = {\n  name: \"xlink:role\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_show_im5v4m: Attribute = {\n  name: \"xlink:show\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_title_1xpi33x: Attribute = {\n  name: \"xlink:title\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xlink_type_zka1lg: Attribute = {\n  name: \"xlink:type\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xml_base_wl2bkx: Attribute = {\n  name: \"xml:base\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xml_lang_1hno5fx: Attribute = {\n  name: \"xml:lang\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_xml_space_bj3sfm: Attribute = {\n  name: \"xml:space\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_attributeName_1xatduy: Attribute = {\n  name: \"attributeName\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_attributeType_itbtnl: Attribute = {\n  name: \"attributeType\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_referrerpolicy_tpprqt: Attribute = {\n  name: \"referrerpolicy\",\n  description: \"Referrer policy for fetches initiated by the element\",\n  type: \"string\",\n};\n\nconst attribute_rel_kwlf9m: Attribute = {\n  name: \"rel\",\n  description:\n    \"Relationship between the location in the document containing the hyperlink and the destination resource\",\n  type: \"string\",\n};\n\nconst attribute_alignment_baseline_1mavux3: Attribute = {\n  name: \"alignment-baseline\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"auto\",\n    \"baseline\",\n    \"before-edge\",\n    \"text-before-edge\",\n    \"middle\",\n    \"central\",\n    \"after-edge\",\n    \"text-after-edge\",\n    \"ideographic\",\n    \"alphabetic\",\n    \"hanging\",\n    \"mathematical\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_baseline_shift_1992be8: Attribute = {\n  name: \"baseline-shift\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_clip_17eben: Attribute = {\n  name: \"clip\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_clip_path_l9pwtb: Attribute = {\n  name: \"clip-path\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_clip_rule_l41gyg: Attribute = {\n  name: \"clip-rule\",\n  description: \"\",\n  type: \"select\",\n  options: [\"nonzero\", \"evenodd\", \"inherit\"],\n};\n\nconst attribute_color_j22118: Attribute = {\n  name: \"color\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_color_interpolation_12ntgve: Attribute = {\n  name: \"color-interpolation\",\n  description: \"\",\n  type: \"select\",\n  options: [\"auto\", \"sRGB\", \"linearRGB\", \"inherit\"],\n};\n\nconst attribute_color_interpolation_filters_1ng37c4: Attribute = {\n  name: \"color-interpolation-filters\",\n  description: \"\",\n  type: \"select\",\n  options: [\"auto\", \"sRGB\", \"linearRGB\", \"inherit\"],\n};\n\nconst attribute_color_profile_1arfqgt: Attribute = {\n  name: \"color-profile\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_color_rendering_39twr5: Attribute = {\n  name: \"color-rendering\",\n  description: \"\",\n  type: \"select\",\n  options: [\"auto\", \"optimizeSpeed\", \"optimizeQuality\", \"inherit\"],\n};\n\nconst attribute_cursor_41nrie: Attribute = {\n  name: \"cursor\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_direction_1aonvp7: Attribute = {\n  name: \"direction\",\n  description: \"\",\n  type: \"select\",\n  options: [\"ltr\", \"rtl\", \"inherit\"],\n};\n\nconst attribute_display_139qswi: Attribute = {\n  name: \"display\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"inline\",\n    \"block\",\n    \"list-item\",\n    \"run-in\",\n    \"compact\",\n    \"marker\",\n    \"table\",\n    \"inline-table\",\n    \"table-row-group\",\n    \"table-header-group\",\n    \"table-footer-group\",\n    \"table-row\",\n    \"table-column-group\",\n    \"table-column\",\n    \"table-cell\",\n    \"table-caption\",\n    \"none\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_dominant_baseline_1pb95lg: Attribute = {\n  name: \"dominant-baseline\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"auto\",\n    \"use-script\",\n    \"no-change\",\n    \"reset-size\",\n    \"ideographic\",\n    \"alphabetic\",\n    \"hanging\",\n    \"mathematical\",\n    \"central\",\n    \"middle\",\n    \"text-after-edge\",\n    \"text-before-edge\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_enable_background_1tjvhfc: Attribute = {\n  name: \"enable-background\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_fill_opacity_1ki263g: Attribute = {\n  name: \"fill-opacity\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_fill_rule_1vp7xd6: Attribute = {\n  name: \"fill-rule\",\n  description: \"\",\n  type: \"select\",\n  options: [\"nonzero\", \"evenodd\", \"inherit\"],\n};\n\nconst attribute_filter_xm2t59: Attribute = {\n  name: \"filter\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_flood_color_vdztlb: Attribute = {\n  name: \"flood-color\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_flood_opacity_wsup97: Attribute = {\n  name: \"flood-opacity\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_font_family_1oj20lz: Attribute = {\n  name: \"font-family\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_font_size_1522ioq: Attribute = {\n  name: \"font-size\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_font_size_adjust_1986ttn: Attribute = {\n  name: \"font-size-adjust\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_font_stretch_1szmgzt: Attribute = {\n  name: \"font-stretch\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"normal\",\n    \"wider\",\n    \"narrower\",\n    \"ultra-condensed\",\n    \"extra-condensed\",\n    \"condensed\",\n    \"semi-condensed\",\n    \"semi-expanded\",\n    \"expanded\",\n    \"extra-expanded\",\n    \"ultra-expanded\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_font_style_10u7qj4: Attribute = {\n  name: \"font-style\",\n  description: \"\",\n  type: \"select\",\n  options: [\"normal\", \"italic\", \"oblique\", \"inherit\"],\n};\n\nconst attribute_font_variant_11wrur: Attribute = {\n  name: \"font-variant\",\n  description: \"\",\n  type: \"select\",\n  options: [\"normal\", \"small-caps\", \"inherit\"],\n};\n\nconst attribute_font_weight_18kq5ko: Attribute = {\n  name: \"font-weight\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_glyph_orientation_horizontal_1x9zgr4: Attribute = {\n  name: \"glyph-orientation-horizontal\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_glyph_orientation_vertical_lrbm8t: Attribute = {\n  name: \"glyph-orientation-vertical\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_image_rendering_7zonn5: Attribute = {\n  name: \"image-rendering\",\n  description: \"\",\n  type: \"select\",\n  options: [\"auto\", \"optimizeSpeed\", \"optimizeQuality\", \"inherit\"],\n};\n\nconst attribute_kerning_sbqwuk: Attribute = {\n  name: \"kerning\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_letter_spacing_utdryb: Attribute = {\n  name: \"letter-spacing\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_lighting_color_2qihow: Attribute = {\n  name: \"lighting-color\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_marker_end_1u3sadl: Attribute = {\n  name: \"marker-end\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_marker_mid_5umtul: Attribute = {\n  name: \"marker-mid\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_marker_start_110wno8: Attribute = {\n  name: \"marker-start\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_mask_q5u9f7: Attribute = {\n  name: \"mask\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_opacity_1lpnivz: Attribute = {\n  name: \"opacity\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_overflow_7ql0qh: Attribute = {\n  name: \"overflow\",\n  description: \"\",\n  type: \"select\",\n  options: [\"visible\", \"hidden\", \"scroll\", \"auto\", \"inherit\"],\n};\n\nconst attribute_pointer_events_vrdynn: Attribute = {\n  name: \"pointer-events\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"visiblePainted\",\n    \"visibleFill\",\n    \"visibleStroke\",\n    \"visible\",\n    \"painted\",\n    \"fill\",\n    \"stroke\",\n    \"all\",\n    \"none\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_shape_rendering_r2u534: Attribute = {\n  name: \"shape-rendering\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"auto\",\n    \"optimizeSpeed\",\n    \"crispEdges\",\n    \"geometricPrecision\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_stop_color_1cuxwox: Attribute = {\n  name: \"stop-color\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stop_opacity_15fkjpj: Attribute = {\n  name: \"stop-opacity\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stroke_1uzwglv: Attribute = {\n  name: \"stroke\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stroke_dasharray_3u6g90: Attribute = {\n  name: \"stroke-dasharray\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stroke_dashoffset_y5ku5x: Attribute = {\n  name: \"stroke-dashoffset\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stroke_linecap_ivmqaf: Attribute = {\n  name: \"stroke-linecap\",\n  description: \"\",\n  type: \"select\",\n  options: [\"butt\", \"round\", \"square\", \"inherit\"],\n};\n\nconst attribute_stroke_linejoin_16dr5ik: Attribute = {\n  name: \"stroke-linejoin\",\n  description: \"\",\n  type: \"select\",\n  options: [\"miter\", \"round\", \"bevel\", \"inherit\"],\n};\n\nconst attribute_stroke_miterlimit_jeie8q: Attribute = {\n  name: \"stroke-miterlimit\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stroke_opacity_g8k2u: Attribute = {\n  name: \"stroke-opacity\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_stroke_width_dc7451: Attribute = {\n  name: \"stroke-width\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_text_anchor_1jtk2wl: Attribute = {\n  name: \"text-anchor\",\n  description: \"\",\n  type: \"select\",\n  options: [\"start\", \"middle\", \"end\", \"inherit\"],\n};\n\nconst attribute_text_decoration_9s86ib: Attribute = {\n  name: \"text-decoration\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_text_rendering_31hh4f: Attribute = {\n  name: \"text-rendering\",\n  description: \"\",\n  type: \"select\",\n  options: [\n    \"auto\",\n    \"optimizeSpeed\",\n    \"optimizeLegibility\",\n    \"geometricPrecision\",\n    \"inherit\",\n  ],\n};\n\nconst attribute_unicode_bidi_mqhd45: Attribute = {\n  name: \"unicode-bidi\",\n  description: \"\",\n  type: \"select\",\n  options: [\"normal\", \"embed\", \"bidi-override\", \"inherit\"],\n};\n\nconst attribute_visibility_1n5r3h2: Attribute = {\n  name: \"visibility\",\n  description: \"\",\n  type: \"select\",\n  options: [\"visible\", \"hidden\", \"collapse\", \"inherit\"],\n};\n\nconst attribute_word_spacing_1f6bo7g: Attribute = {\n  name: \"word-spacing\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_writing_mode_1nx1p7l: Attribute = {\n  name: \"writing-mode\",\n  description: \"\",\n  type: \"select\",\n  options: [\"lr-tb\", \"rl-tb\", \"tb-rl\", \"lr\", \"rl\", \"tb\", \"inherit\"],\n};\n\nconst attribute_class_1nw9qfk: Attribute = {\n  name: \"class\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_transform_1czbvkp: Attribute = {\n  name: \"transform\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_span_1o27uf0: Attribute = {\n  name: \"span\",\n  description: \"Number of columns spanned by the element\",\n  type: \"number\",\n};\n\nconst attribute_title_of8t36: Attribute = {\n  name: \"title\",\n  description: \"Full term or expansion of abbreviation\",\n  type: \"string\",\n};\n\nconst attribute_cx_3dwqow: Attribute = {\n  name: \"cx\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_cy_1aonem4: Attribute = {\n  name: \"cy\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_height_10887hn: Attribute = {\n  name: \"height\",\n  description: \"Vertical dimension\",\n  type: \"number\",\n};\n\nconst attribute_src_hol6ri: Attribute = {\n  name: \"src\",\n  description: \"Address of the resource\",\n  type: \"string\",\n};\n\nconst attribute_width_d9q964: Attribute = {\n  name: \"width\",\n  description: \"Horizontal dimension\",\n  type: \"number\",\n};\n\nconst attribute_height_19cj0d2: Attribute = {\n  name: \"height\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_in_17xz3x2: Attribute = {\n  name: \"in\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_result_1qdqhl9: Attribute = {\n  name: \"result\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_type_klrbjn: Attribute = {\n  name: \"type\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_width_5n2j7z: Attribute = {\n  name: \"width\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_x_1perc20: Attribute = {\n  name: \"x\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_y_14t62ez: Attribute = {\n  name: \"y\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_in2_1uopqqq: Attribute = {\n  name: \"in2\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_kernelUnitLength_bb79ku: Attribute = {\n  name: \"kernelUnitLength\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_amplitude_rx1hwp: Attribute = {\n  name: \"amplitude\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_exponent_1mrxirx: Attribute = {\n  name: \"exponent\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_intercept_1nnqkjm: Attribute = {\n  name: \"intercept\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_offset_6sg3je: Attribute = {\n  name: \"offset\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_slope_b2yu7c: Attribute = {\n  name: \"slope\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_tableValues_m3qznf: Attribute = {\n  name: \"tableValues\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_operator_1okxrbs: Attribute = {\n  name: \"operator\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_surfaceScale_1vwnl30: Attribute = {\n  name: \"surfaceScale\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_specularExponent_9llg98: Attribute = {\n  name: \"specularExponent\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_z_lmg84i: Attribute = {\n  name: \"z\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_form_1v3e5z4: Attribute = {\n  name: \"form\",\n  description: \"Associates the element with a form element\",\n  type: \"string\",\n};\n\nconst attribute_name_kxpyia: Attribute = {\n  name: \"name\",\n  description:\n    \"Name of the element to use for form submission and in the form.elements API\",\n  type: \"string\",\n};\n\nconst attribute_preserveAspectRatio_19as3ta: Attribute = {\n  name: \"preserveAspectRatio\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_crossorigin_jl1m2v: Attribute = {\n  name: \"crossorigin\",\n  description: \"How the element handles crossorigin requests\",\n  type: \"select\",\n  options: [\"anonymous\", \"use-credentials\"],\n};\n\nconst attribute_loading_yzzdw4: Attribute = {\n  name: \"loading\",\n  description: \"Used when determining loading deferral\",\n  type: \"select\",\n  options: [\"lazy\", \"eager\"],\n};\n\nconst attribute_alt_1j06s5r: Attribute = {\n  name: \"alt\",\n  description: \"Replacement text for use when images are not available\",\n  type: \"string\",\n};\n\nconst attribute_disabled_1ceu012: Attribute = {\n  name: \"disabled\",\n  description: \"Whether the form control is disabled\",\n  type: \"boolean\",\n};\n\nconst attribute_formaction_19t419n: Attribute = {\n  name: \"formaction\",\n  description: \"URL to use for form submission\",\n  type: \"string\",\n};\n\nconst attribute_formenctype_def0ia: Attribute = {\n  name: \"formenctype\",\n  description: \"Entry list encoding type to use for form submission\",\n  type: \"select\",\n  options: [\n    \"application/x-www-form-urlencoded\",\n    \"multipart/form-data\",\n    \"text/plain\",\n  ],\n};\n\nconst attribute_formmethod_1thl9gp: Attribute = {\n  name: \"formmethod\",\n  description: \"Variant to use for form submission\",\n  type: \"select\",\n  options: [\"get\", \"post\", \"dialog\"],\n};\n\nconst attribute_formnovalidate_13jgki2: Attribute = {\n  name: \"formnovalidate\",\n  description: \"Bypass form control validation for form submission\",\n  type: \"boolean\",\n};\n\nconst attribute_formtarget_kvbc93: Attribute = {\n  name: \"formtarget\",\n  description: \"Navigable for form submission\",\n  type: \"select\",\n  options: [\"_blank\", \"_self\", \"_parent\", \"_top\"],\n};\n\nconst attribute_cite_a034lz: Attribute = {\n  name: \"cite\",\n  description:\n    \"Link to the source of the quotation or more information about the edit\",\n  type: \"string\",\n};\n\nconst attribute_datetime_1opb1av: Attribute = {\n  name: \"datetime\",\n  description: \"Date and (optionally) time of the change\",\n  type: \"string\",\n};\n\nconst attribute_x1_157vosv: Attribute = {\n  name: \"x1\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_x2_1j95tt9: Attribute = {\n  name: \"x2\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_y1_1m9zbl: Attribute = {\n  name: \"y1\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_y2_gxmray: Attribute = {\n  name: \"y2\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_name_14zrnwf: Attribute = {\n  name: \"name\",\n  description: \"Name of content navigable\",\n  type: \"string\",\n};\n\nconst attribute_type_1swzf44: Attribute = {\n  name: \"type\",\n  description: \"Type of embedded resource\",\n  type: \"string\",\n};\n\nconst attribute_viewBox_xml4ya: Attribute = {\n  name: \"viewBox\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_points_1nvd68l: Attribute = {\n  name: \"points\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_max_1m5derl: Attribute = {\n  name: \"max\",\n  description: \"Upper bound of range\",\n  type: \"number\",\n};\n\nconst attribute_value_1or32ty: Attribute = {\n  name: \"value\",\n  description: \"Current value of the element\",\n  type: \"number\",\n};\n\nconst attribute_gradientTransform_1tvyqxj: Attribute = {\n  name: \"gradientTransform\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_gradientUnits_1r3vhmq: Attribute = {\n  name: \"gradientUnits\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_r_3bhckq: Attribute = {\n  name: \"r\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_spreadMethod_y391sn: Attribute = {\n  name: \"spreadMethod\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_rx_14j28ki: Attribute = {\n  name: \"rx\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_ry_1po03yo: Attribute = {\n  name: \"ry\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_autocomplete_n0adlq: Attribute = {\n  name: \"autocomplete\",\n  description: \"Hint for form autofill feature\",\n  type: \"string\",\n};\n\nconst attribute_name_1nwqzo5: Attribute = {\n  name: \"name\",\n  description:\n    \"Name of the element to use for form submission and in the form.elements API\",\n  type: \"string\",\n  required: true,\n};\n\nconst attribute_required_bgte6f: Attribute = {\n  name: \"required\",\n  description: \"Whether the control is required for form submission\",\n  type: \"boolean\",\n  required: true,\n};\n\nconst attribute_size_imvude: Attribute = {\n  name: \"size\",\n  description: \"Size of the control\",\n  type: \"number\",\n};\n\nconst attribute_value_15qkpz: Attribute = {\n  name: \"value\",\n  description: \"Value of the form control\",\n  type: \"string\",\n  required: true,\n};\n\nconst attribute_sizes_o9chmv: Attribute = {\n  name: \"sizes\",\n  description: \"Image sizes for different page layouts\",\n  type: \"string\",\n};\n\nconst attribute_srcset_1xpiw3a: Attribute = {\n  name: \"srcset\",\n  description:\n    \"Images to use in different situations, e.g., high-resolution displays, small monitors, etc.\",\n  type: \"string\",\n};\n\nconst attribute_dx_1qijkbt: Attribute = {\n  name: \"dx\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_dy_1me9r5b: Attribute = {\n  name: \"dy\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_rotate_nmq93d: Attribute = {\n  name: \"rotate\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_maxlength_1txfzsx: Attribute = {\n  name: \"maxlength\",\n  description: \"Maximum length of value\",\n  type: \"number\",\n};\n\nconst attribute_minlength_xf39d6: Attribute = {\n  name: \"minlength\",\n  description: \"Minimum length of value\",\n  type: \"number\",\n};\n\nconst attribute_placeholder_1bihs5f: Attribute = {\n  name: \"placeholder\",\n  description: \"User-visible label to be placed within the form control\",\n  type: \"string\",\n  required: true,\n};\n\nconst attribute_readonly_860ef5: Attribute = {\n  name: \"readonly\",\n  description: \"Whether to allow the value to be edited by the user\",\n  type: \"boolean\",\n};\n\nconst attribute_colspan_r4n987: Attribute = {\n  name: \"colspan\",\n  description: \"Number of columns that the cell is to span\",\n  type: \"number\",\n};\n\nconst attribute_headers_gc0t0h: Attribute = {\n  name: \"headers\",\n  description: \"The header cells for this cell\",\n  type: \"string\",\n};\n\nconst attribute_rowspan_o1tr8d: Attribute = {\n  name: \"rowspan\",\n  description: \"Number of rows that the cell is to span\",\n  type: \"number\",\n};\n\nconst attribute_label_kiwyp8: Attribute = {\n  name: \"label\",\n  description: \"User-visible label\",\n  type: \"string\",\n};\n\nconst attribute_lengthAdjust_8jgvje: Attribute = {\n  name: \"lengthAdjust\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_textLength_1br26vp: Attribute = {\n  name: \"textLength\",\n  description: \"\",\n  type: \"string\",\n};\n\nconst attribute_autoplay_1a0sp4x: Attribute = {\n  name: \"autoplay\",\n  description:\n    \"Hint that the media resource can be started automatically when the page is loaded\",\n  type: \"boolean\",\n};\n\nconst attribute_controls_i44ww0: Attribute = {\n  name: \"controls\",\n  description: \"Show user agent controls\",\n  type: \"boolean\",\n};\n\nconst attribute_loop_i1hxl: Attribute = {\n  name: \"loop\",\n  description: \"Whether to loop the media resource\",\n  type: \"boolean\",\n};\n\nconst attribute_muted_1hc6ujd: Attribute = {\n  name: \"muted\",\n  description: \"Whether to mute the media resource by default\",\n  type: \"boolean\",\n};\n\nconst attribute_preload_1a7kb09: Attribute = {\n  name: \"preload\",\n  description: \"Hints how much buffering the media resource will likely need\",\n  type: \"select\",\n  options: [\"none\", \"metadata\", \"auto\"],\n};\n\nconst attribute_zoomAndPan_1dv6jzw: Attribute = {\n  name: \"zoomAndPan\",\n  description: \"\",\n  type: \"string\",\n};\n\nexport const attributesByTag: Record<string, undefined | Attribute[]> = {\n  \"*\": [\n    {\n      name: \"accesskey\",\n      description: \"Keyboard shortcut to activate or focus element\",\n      type: \"string\",\n    },\n    {\n      name: \"autocapitalize\",\n      description:\n        \"Recommended autocapitalization behavior (for supported input methods)\",\n      type: \"select\",\n      options: [\"on\", \"off\", \"none\", \"sentences\", \"words\", \"characters\"],\n    },\n    {\n      name: \"autocorrect\",\n      description:\n        \"Recommended autocorrection behavior (for supported input methods)\",\n      type: \"select\",\n      options: [\"on\", \"off\"],\n    },\n    {\n      name: \"autofocus\",\n      description: \"Automatically focus the element when the page is loaded\",\n      type: \"boolean\",\n    },\n    {\n      name: \"class\",\n      description: \"Classes to which the element belongs\",\n      type: \"string\",\n    },\n    {\n      name: \"contenteditable\",\n      description: \"Whether the element is editable\",\n      type: \"select\",\n      options: [\"true\", \"plaintext-only\", \"false\"],\n    },\n    {\n      name: \"dir\",\n      description: \"The text directionality of the element\",\n      type: \"select\",\n      options: [\"ltr\", \"rtl\", \"auto\"],\n    },\n    {\n      name: \"draggable\",\n      description: \"Whether the element is draggable\",\n      type: \"select\",\n      options: [\"true\", \"false\"],\n    },\n    {\n      name: \"hidden\",\n      description: \"Whether the element is relevant\",\n      type: \"boolean\",\n    },\n    {\n      name: \"id\",\n      description: \"The element's ID\",\n      type: \"string\",\n    },\n    {\n      name: \"inputmode\",\n      description: \"Hint for selecting an input modality\",\n      type: \"select\",\n      options: [\n        \"none\",\n        \"text\",\n        \"tel\",\n        \"email\",\n        \"url\",\n        \"numeric\",\n        \"decimal\",\n        \"search\",\n      ],\n    },\n    {\n      name: \"is\",\n      description: \"Creates a customized built-in element\",\n      type: \"string\",\n    },\n    {\n      name: \"itemid\",\n      description: \"Global identifier for a microdata item\",\n      type: \"string\",\n    },\n    {\n      name: \"itemprop\",\n      description: \"Property names of a microdata item\",\n      type: \"string\",\n    },\n    {\n      name: \"itemref\",\n      description: \"Referenced elements\",\n      type: \"string\",\n    },\n    {\n      name: \"itemscope\",\n      description: \"Introduces a microdata item\",\n      type: \"boolean\",\n    },\n    {\n      name: \"itemtype\",\n      description: \"Item types of a microdata item\",\n      type: \"string\",\n    },\n    {\n      name: \"lang\",\n      description: \"Language of the element\",\n      type: \"string\",\n    },\n    {\n      name: \"nonce\",\n      description:\n        \"Cryptographic nonce used in Content Security Policy checks [CSP]\",\n      type: \"string\",\n    },\n    {\n      name: \"slot\",\n      description: \"The element's desired slot\",\n      type: \"string\",\n    },\n    {\n      name: \"spellcheck\",\n      description:\n        \"Whether the element is to have its spelling and grammar checked\",\n      type: \"select\",\n      options: [\"true\", \"false\"],\n    },\n    {\n      name: \"tabindex\",\n      description:\n        \"Whether the element is focusable and sequentially focusable, and\\n     the relative order of the element for the purposes of sequential focus navigation\",\n      type: \"number\",\n    },\n    {\n      name: \"title\",\n      description: \"Advisory information for the element\",\n      type: \"string\",\n    },\n    {\n      name: \"translate\",\n      description:\n        \"Whether the element is to be translated when the page is localized\",\n      type: \"select\",\n      options: [\"yes\", \"no\"],\n    },\n  ],\n  a: [\n    {\n      name: \"download\",\n      description:\n        \"Whether to download the resource instead of navigating to it, and its filename if so\",\n      type: \"boolean\",\n      required: true,\n    },\n    {\n      name: \"href\",\n      description: \"Address of the hyperlink\",\n      type: \"url\",\n      required: true,\n    },\n    {\n      name: \"hreflang\",\n      description: \"Language of the linked resource\",\n      type: \"string\",\n    },\n    {\n      name: \"ping\",\n      description: \"URLs to ping\",\n      type: \"string\",\n    },\n    attribute_referrerpolicy_tpprqt,\n    attribute_rel_kwlf9m,\n    {\n      name: \"target\",\n      description: \"Navigable for hyperlink navigation\",\n      type: \"select\",\n      options: [\"_blank\", \"_self\", \"_parent\", \"_top\"],\n      required: true,\n    },\n    {\n      name: \"type\",\n      description: \"Hint for the type of the referenced resource\",\n      type: \"string\",\n    },\n  ],\n  abbr: [attribute_title_of8t36],\n  animate: [\n    attribute_accumulate_1kar0jt,\n    attribute_additive_fsyc7w,\n    attribute_alignment_baseline_1mavux3,\n    attribute_attributeName_1xatduy,\n    attribute_attributeType_itbtnl,\n    attribute_baseline_shift_1992be8,\n    attribute_begin_4942fq,\n    attribute_by_1s7zscl,\n    attribute_calcMode_un1adz,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_dur_e0odti,\n    attribute_enable_background_1tjvhfc,\n    attribute_end_131xh48,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_from_w9fqm8,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_keySplines_911cut,\n    attribute_keyTimes_pvnmun,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_max_r1k4jl,\n    attribute_min_a76syq,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_repeatCount_1yxzal3,\n    attribute_repeatDur_l7trje,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_restart_wiuzbf,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_to_1xfnnez,\n    attribute_unicode_bidi_mqhd45,\n    attribute_values_1jkm3eq,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  animateMotion: [\n    attribute_accumulate_1kar0jt,\n    attribute_additive_fsyc7w,\n    attribute_begin_4942fq,\n    attribute_by_1s7zscl,\n    attribute_calcMode_un1adz,\n    attribute_dur_e0odti,\n    attribute_end_131xh48,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_from_w9fqm8,\n    attribute_id_1wtm880,\n    {\n      name: \"keyPoints\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_keySplines_911cut,\n    attribute_keyTimes_pvnmun,\n    attribute_max_r1k4jl,\n    attribute_min_a76syq,\n    {\n      name: \"origin\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"path\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_repeatCount_1yxzal3,\n    attribute_repeatDur_l7trje,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_restart_wiuzbf,\n    attribute_rotate_nmq93d,\n    attribute_systemLanguage_ytoswp,\n    attribute_to_1xfnnez,\n    attribute_values_1jkm3eq,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  animateTransform: [\n    attribute_accumulate_1kar0jt,\n    attribute_additive_fsyc7w,\n    attribute_attributeName_1xatduy,\n    attribute_attributeType_itbtnl,\n    attribute_begin_4942fq,\n    attribute_by_1s7zscl,\n    attribute_calcMode_un1adz,\n    attribute_dur_e0odti,\n    attribute_end_131xh48,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_from_w9fqm8,\n    attribute_id_1wtm880,\n    attribute_keySplines_911cut,\n    attribute_keyTimes_pvnmun,\n    attribute_max_r1k4jl,\n    attribute_min_a76syq,\n    attribute_repeatCount_1yxzal3,\n    attribute_repeatDur_l7trje,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_restart_wiuzbf,\n    attribute_systemLanguage_ytoswp,\n    attribute_to_1xfnnez,\n    attribute_type_klrbjn,\n    attribute_values_1jkm3eq,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  area: [\n    attribute_alt_1j06s5r,\n    {\n      name: \"coords\",\n      description: \"Coordinates for the shape to be created in an image map\",\n      type: \"string\",\n    },\n    {\n      name: \"download\",\n      description:\n        \"Whether to download the resource instead of navigating to it, and its filename if so\",\n      type: \"string\",\n    },\n    {\n      name: \"href\",\n      description: \"Address of the hyperlink\",\n      type: \"string\",\n    },\n    attribute_referrerpolicy_tpprqt,\n    attribute_rel_kwlf9m,\n    {\n      name: \"shape\",\n      description: \"The kind of shape to be created in an image map\",\n      type: \"select\",\n      options: [\"circle\", \"default\", \"poly\", \"rect\"],\n    },\n    {\n      name: \"target\",\n      description: \"Navigable for hyperlink navigation\",\n      type: \"select\",\n      options: [\"_blank\", \"_self\", \"_parent\", \"_top\"],\n    },\n  ],\n  audio: [\n    attribute_autoplay_1a0sp4x,\n    attribute_controls_i44ww0,\n    attribute_crossorigin_jl1m2v,\n    attribute_loop_i1hxl,\n    attribute_muted_1hc6ujd,\n    attribute_preload_1a7kb09,\n    attribute_src_hol6ri,\n  ],\n  bdo: [\n    {\n      name: \"dir\",\n      description: \"The text directionality of the element\",\n      type: \"select\",\n      options: [\"ltr\", \"rtl\"],\n    },\n  ],\n  blockquote: [\n    {\n      name: \"cite\",\n      description:\n        \"Link to the source of the quotation or more information about the edit\",\n      type: \"string\",\n      required: true,\n    },\n  ],\n  button: [\n    attribute_disabled_1ceu012,\n    attribute_form_1v3e5z4,\n    attribute_formaction_19t419n,\n    attribute_formenctype_def0ia,\n    attribute_formmethod_1thl9gp,\n    attribute_formnovalidate_13jgki2,\n    attribute_formtarget_kvbc93,\n    attribute_name_kxpyia,\n    {\n      name: \"type\",\n      description: \"Type of button\",\n      type: \"select\",\n      options: [\"submit\", \"reset\", \"button\"],\n      required: true,\n    },\n    {\n      name: \"value\",\n      description: \"Value to be used for form submission\",\n      type: \"string\",\n    },\n  ],\n  canvas: [attribute_height_10887hn, attribute_width_d9q964],\n  circle: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_cx_3dwqow,\n    attribute_cy_1aonem4,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_r_3bhckq,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  clipPath: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    {\n      name: \"clipPathUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  col: [attribute_span_1o27uf0],\n  colgroup: [attribute_span_1o27uf0],\n  data: [\n    {\n      name: \"value\",\n      description: \"Machine-readable value\",\n      type: \"string\",\n    },\n  ],\n  defs: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  del: [attribute_cite_a034lz, attribute_datetime_1opb1av],\n  desc: [\n    attribute_class_1nw9qfk,\n    attribute_id_1wtm880,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  details: [\n    {\n      name: \"name\",\n      description: \"Name of group of mutually-exclusive details elements\",\n      type: \"string\",\n    },\n    {\n      name: \"open\",\n      description: \"Whether the details are visible\",\n      type: \"boolean\",\n    },\n  ],\n  dfn: [attribute_title_of8t36],\n  dialog: [\n    {\n      name: \"open\",\n      description: \"Whether the dialog box is showing\",\n      type: \"boolean\",\n    },\n  ],\n  ellipse: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_cx_3dwqow,\n    attribute_cy_1aonem4,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_rx_14j28ki,\n    attribute_ry_1po03yo,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  embed: [\n    attribute_height_10887hn,\n    attribute_src_hol6ri,\n    attribute_type_1swzf44,\n    attribute_width_d9q964,\n  ],\n  feBlend: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_in2_1uopqqq,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    {\n      name: \"mode\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feColorMatrix: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_type_klrbjn,\n    attribute_unicode_bidi_mqhd45,\n    attribute_values_1jkm3eq,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feComponentTransfer: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feComposite: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_in2_1uopqqq,\n    {\n      name: \"k1\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"k2\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"k3\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"k4\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_operator_1okxrbs,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feConvolveMatrix: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    {\n      name: \"bias\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    {\n      name: \"divisor\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_dominant_baseline_1pb95lg,\n    {\n      name: \"edgeMode\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    {\n      name: \"kernelMatrix\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_kernelUnitLength_bb79ku,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    {\n      name: \"order\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    {\n      name: \"preserveAlpha\",\n      description: \"\",\n      type: \"select\",\n      options: [\"true\", \"false\"],\n    },\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    {\n      name: \"targetX\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"targetY\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feDiffuseLighting: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    {\n      name: \"diffuseConstant\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kernelUnitLength_bb79ku,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_surfaceScale_1vwnl30,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feDisplacementMap: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_in2_1uopqqq,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    {\n      name: \"scale\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    {\n      name: \"xChannelSelector\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n    {\n      name: \"yChannelSelector\",\n      description: \"\",\n      type: \"string\",\n    },\n  ],\n  feDistantLight: [\n    {\n      name: \"azimuth\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"elevation\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_id_1wtm880,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  feFlood: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feFuncA: [\n    attribute_amplitude_rx1hwp,\n    attribute_exponent_1mrxirx,\n    attribute_id_1wtm880,\n    attribute_intercept_1nnqkjm,\n    attribute_offset_6sg3je,\n    attribute_slope_b2yu7c,\n    attribute_tableValues_m3qznf,\n    attribute_type_klrbjn,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  feFuncB: [\n    attribute_amplitude_rx1hwp,\n    attribute_exponent_1mrxirx,\n    attribute_id_1wtm880,\n    attribute_intercept_1nnqkjm,\n    attribute_offset_6sg3je,\n    attribute_slope_b2yu7c,\n    attribute_tableValues_m3qznf,\n    attribute_type_klrbjn,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  feFuncG: [\n    attribute_amplitude_rx1hwp,\n    attribute_exponent_1mrxirx,\n    attribute_id_1wtm880,\n    attribute_intercept_1nnqkjm,\n    attribute_offset_6sg3je,\n    attribute_slope_b2yu7c,\n    attribute_tableValues_m3qznf,\n    attribute_type_klrbjn,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  feFuncR: [\n    attribute_amplitude_rx1hwp,\n    attribute_exponent_1mrxirx,\n    attribute_id_1wtm880,\n    attribute_intercept_1nnqkjm,\n    attribute_offset_6sg3je,\n    attribute_slope_b2yu7c,\n    attribute_tableValues_m3qznf,\n    attribute_type_klrbjn,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  feGaussianBlur: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    {\n      name: \"stdDeviation\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feImage: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_preserveAspectRatio_19as3ta,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feMerge: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feMergeNode: [\n    attribute_id_1wtm880,\n    attribute_in_17xz3x2,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  feMorphology: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_operator_1okxrbs,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    {\n      name: \"radius\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feOffset: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_dx_1qijkbt,\n    attribute_dy_1me9r5b,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  fePointLight: [\n    attribute_id_1wtm880,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n    attribute_z_lmg84i,\n  ],\n  feSpecularLighting: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kernelUnitLength_bb79ku,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    {\n      name: \"specularConstant\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_specularExponent_9llg98,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_surfaceScale_1vwnl30,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feSpotLight: [\n    attribute_id_1wtm880,\n    {\n      name: \"limitingConeAngle\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"pointsAtX\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"pointsAtY\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"pointsAtZ\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_specularExponent_9llg98,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n    attribute_z_lmg84i,\n  ],\n  feTile: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_in_17xz3x2,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  feTurbulence: [\n    attribute_alignment_baseline_1mavux3,\n    {\n      name: \"baseFrequency\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    {\n      name: \"numOctaves\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_result_1qdqhl9,\n    {\n      name: \"seed\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_shape_rendering_r2u534,\n    {\n      name: \"stitchTiles\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_type_klrbjn,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  fieldset: [\n    {\n      name: \"disabled\",\n      description:\n        \"Whether the descendant form controls, except any inside legend, are disabled\",\n      type: \"boolean\",\n    },\n    attribute_form_1v3e5z4,\n    attribute_name_kxpyia,\n  ],\n  filter: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    {\n      name: \"filterRes\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"filterUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    {\n      name: \"primitiveUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  foreignObject: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  form: [\n    {\n      name: \"accept-charset\",\n      description: \"Character encodings to use for form submission\",\n      type: \"string\",\n    },\n    {\n      name: \"action\",\n      description: \"URL to use for form submission\",\n      type: \"string\",\n      required: true,\n    },\n    {\n      name: \"autocomplete\",\n      description:\n        \"Default setting for autofill feature for controls in the form\",\n      type: \"select\",\n      options: [\"on\", \"off\"],\n    },\n    {\n      name: \"enctype\",\n      description: \"Entry list encoding type to use for form submission\",\n      type: \"select\",\n      options: [\n        \"application/x-www-form-urlencoded\",\n        \"multipart/form-data\",\n        \"text/plain\",\n      ],\n      required: true,\n    },\n    {\n      name: \"method\",\n      description: \"Variant to use for form submission\",\n      type: \"select\",\n      options: [\"get\", \"post\", \"dialog\"],\n      required: true,\n    },\n    {\n      name: \"name\",\n      description: \"Name of form to use in the document.forms API\",\n      type: \"string\",\n    },\n    {\n      name: \"novalidate\",\n      description: \"Bypass form control validation for form submission\",\n      type: \"boolean\",\n    },\n    {\n      name: \"target\",\n      description: \"Navigable for form submission\",\n      type: \"select\",\n      options: [\"_blank\", \"_self\", \"_parent\", \"_top\"],\n    },\n  ],\n  g: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  iframe: [\n    {\n      name: \"allow\",\n      description: \"Permissions policy to be applied to the iframe's contents\",\n      type: \"string\",\n    },\n    {\n      name: \"allowfullscreen\",\n      description:\n        \"Whether to allow the iframe's contents to use requestFullscreen()\",\n      type: \"boolean\",\n    },\n    attribute_height_10887hn,\n    attribute_loading_yzzdw4,\n    attribute_name_14zrnwf,\n    attribute_referrerpolicy_tpprqt,\n    {\n      name: \"sandbox\",\n      description: \"Security rules for nested content\",\n      type: \"string\",\n    },\n    attribute_src_hol6ri,\n    {\n      name: \"srcdoc\",\n      description: \"A document to render in the iframe\",\n      type: \"string\",\n    },\n    attribute_width_d9q964,\n  ],\n  image: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_preserveAspectRatio_19as3ta,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  img: [\n    {\n      name: \"alt\",\n      description: \"Replacement text for use when images are not available\",\n      type: \"string\",\n      required: true,\n    },\n    attribute_crossorigin_jl1m2v,\n    {\n      name: \"decoding\",\n      description:\n        \"Decoding hint to use when processing this image for presentation\",\n      type: \"select\",\n      options: [\"sync\", \"async\", \"auto\"],\n    },\n    {\n      name: \"fetchpriority\",\n      description: \"Sets the priority for fetches initiated by the element\",\n      type: \"select\",\n      options: [\"auto\", \"high\", \"low\"],\n    },\n    {\n      name: \"height\",\n      description: \"Vertical dimension\",\n      type: \"number\",\n      required: true,\n    },\n    attribute_loading_yzzdw4,\n    attribute_referrerpolicy_tpprqt,\n    attribute_sizes_o9chmv,\n    {\n      name: \"src\",\n      description: \"Address of the resource\",\n      type: \"string\",\n      required: true,\n    },\n    attribute_srcset_1xpiw3a,\n    {\n      name: \"usemap\",\n      description: \"Name of image map to use\",\n      type: \"string\",\n    },\n    {\n      name: \"width\",\n      description: \"Horizontal dimension\",\n      type: \"number\",\n      required: true,\n    },\n  ],\n  input: [\n    {\n      name: \"accept\",\n      description: \"Hint for expected file type in file upload controls\",\n      type: \"string\",\n    },\n    attribute_alt_1j06s5r,\n    attribute_autocomplete_n0adlq,\n    {\n      name: \"checked\",\n      description: \"Whether the control is checked\",\n      type: \"boolean\",\n      required: true,\n    },\n    attribute_disabled_1ceu012,\n    attribute_form_1v3e5z4,\n    attribute_formaction_19t419n,\n    attribute_formenctype_def0ia,\n    attribute_formmethod_1thl9gp,\n    attribute_formnovalidate_13jgki2,\n    attribute_formtarget_kvbc93,\n    attribute_height_10887hn,\n    {\n      name: \"list\",\n      description: \"List of autocomplete options\",\n      type: \"string\",\n    },\n    {\n      name: \"max\",\n      description: \"Maximum value\",\n      type: \"string\",\n    },\n    attribute_maxlength_1txfzsx,\n    {\n      name: \"min\",\n      description: \"Minimum value\",\n      type: \"string\",\n    },\n    attribute_minlength_xf39d6,\n    {\n      name: \"multiple\",\n      description: \"Whether to allow multiple values\",\n      type: \"boolean\",\n    },\n    attribute_name_1nwqzo5,\n    {\n      name: \"pattern\",\n      description: \"Pattern to be matched by the form control's value\",\n      type: \"string\",\n    },\n    attribute_placeholder_1bihs5f,\n    attribute_readonly_860ef5,\n    attribute_required_bgte6f,\n    attribute_size_imvude,\n    attribute_src_hol6ri,\n    {\n      name: \"step\",\n      description: \"Granularity to be matched by the form control's value\",\n      type: \"number\",\n    },\n    {\n      name: \"title\",\n      description: \"Description of pattern (when used with pattern attribute)\",\n      type: \"string\",\n    },\n    {\n      name: \"type\",\n      description: \"Type of form control\",\n      type: \"select\",\n      options: [\n        \"hidden\",\n        \"text\",\n        \"search\",\n        \"tel\",\n        \"url\",\n        \"email\",\n        \"password\",\n        \"date\",\n        \"month\",\n        \"week\",\n        \"time\",\n        \"datetime-local\",\n        \"number\",\n        \"range\",\n        \"color\",\n        \"checkbox\",\n        \"radio\",\n        \"file\",\n        \"submit\",\n        \"image\",\n        \"reset\",\n        \"button\",\n      ],\n      required: true,\n    },\n    attribute_value_15qkpz,\n    attribute_width_d9q964,\n  ],\n  ins: [attribute_cite_a034lz, attribute_datetime_1opb1av],\n  label: [\n    {\n      name: \"for\",\n      description: \"Associate the label with form control\",\n      type: \"string\",\n      required: true,\n    },\n  ],\n  li: [\n    {\n      name: \"value\",\n      description: \"Ordinal value of the list item\",\n      type: \"number\",\n    },\n  ],\n  line: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x1_157vosv,\n    attribute_x2_1j95tt9,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y1_1m9zbl,\n    attribute_y2_gxmray,\n  ],\n  linearGradient: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_gradientTransform_1tvyqxj,\n    attribute_gradientUnits_1r3vhmq,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_shape_rendering_r2u534,\n    attribute_spreadMethod_y391sn,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x1_157vosv,\n    attribute_x2_1j95tt9,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y1_1m9zbl,\n    attribute_y2_gxmray,\n  ],\n  map: [\n    {\n      name: \"name\",\n      description: \"Name of image map to reference from the usemap attribute\",\n      type: \"string\",\n    },\n  ],\n  marker: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    {\n      name: \"markerHeight\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"markerUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"markerWidth\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    {\n      name: \"orient\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_preserveAspectRatio_19as3ta,\n    {\n      name: \"refX\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"refY\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_viewBox_xml4ya,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  mask: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    {\n      name: \"maskContentUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"maskUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  metadata: [\n    attribute_id_1wtm880,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  meter: [\n    {\n      name: \"high\",\n      description: \"Low limit of high range\",\n      type: \"number\",\n    },\n    {\n      name: \"low\",\n      description: \"High limit of low range\",\n      type: \"number\",\n    },\n    attribute_max_1m5derl,\n    {\n      name: \"min\",\n      description: \"Lower bound of range\",\n      type: \"number\",\n    },\n    {\n      name: \"optimum\",\n      description: \"Optimum value in gauge\",\n      type: \"number\",\n    },\n    attribute_value_1or32ty,\n  ],\n  mpath: [\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_id_1wtm880,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  object: [\n    {\n      name: \"data\",\n      description: \"Address of the resource\",\n      type: \"string\",\n    },\n    attribute_form_1v3e5z4,\n    attribute_height_10887hn,\n    attribute_name_14zrnwf,\n    attribute_type_1swzf44,\n    attribute_width_d9q964,\n  ],\n  ol: [\n    {\n      name: \"reversed\",\n      description: \"Number the list backwards\",\n      type: \"boolean\",\n    },\n    {\n      name: \"start\",\n      description: \"Starting value of the list\",\n      type: \"number\",\n    },\n    {\n      name: \"type\",\n      description: \"Kind of list marker\",\n      type: \"select\",\n      options: [\"1\", \"a\", \"a\", \"i\", \"i\"],\n    },\n  ],\n  optgroup: [attribute_disabled_1ceu012, attribute_label_kiwyp8],\n  option: [\n    {\n      name: \"disabled\",\n      description: \"Whether the form control is disabled\",\n      type: \"boolean\",\n      required: true,\n    },\n    {\n      name: \"label\",\n      description: \"User-visible label\",\n      type: \"string\",\n      required: true,\n    },\n    {\n      name: \"value\",\n      description: \"Value to be used for form submission\",\n      type: \"string\",\n      required: true,\n    },\n  ],\n  output: [\n    {\n      name: \"for\",\n      description: \"Specifies controls from which the output was calculated\",\n      type: \"string\",\n    },\n    attribute_form_1v3e5z4,\n    attribute_name_kxpyia,\n  ],\n  path: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    {\n      name: \"d\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    {\n      name: \"pathLength\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  pattern: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    {\n      name: \"patternContentUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"patternTransform\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"patternUnits\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_pointer_events_vrdynn,\n    attribute_preserveAspectRatio_19as3ta,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_viewBox_xml4ya,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  polygon: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_points_1nvd68l,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  polyline: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_points_1nvd68l,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  progress: [attribute_max_1m5derl, attribute_value_1or32ty],\n  q: [attribute_cite_a034lz],\n  radialGradient: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_cx_3dwqow,\n    attribute_cy_1aonem4,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    {\n      name: \"fx\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"fy\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_gradientTransform_1tvyqxj,\n    attribute_gradientUnits_1r3vhmq,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_r_3bhckq,\n    attribute_shape_rendering_r2u534,\n    attribute_spreadMethod_y391sn,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  rect: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_rx_14j28ki,\n    attribute_ry_1po03yo,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  select: [\n    attribute_autocomplete_n0adlq,\n    attribute_disabled_1ceu012,\n    attribute_form_1v3e5z4,\n    attribute_name_1nwqzo5,\n    attribute_required_bgte6f,\n    attribute_size_imvude,\n    attribute_value_15qkpz,\n  ],\n  set: [\n    attribute_attributeName_1xatduy,\n    attribute_attributeType_itbtnl,\n    attribute_begin_4942fq,\n    attribute_dur_e0odti,\n    attribute_end_131xh48,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_id_1wtm880,\n    attribute_max_r1k4jl,\n    attribute_min_a76syq,\n    attribute_repeatCount_1yxzal3,\n    attribute_repeatDur_l7trje,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_restart_wiuzbf,\n    attribute_systemLanguage_ytoswp,\n    attribute_to_1xfnnez,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  slot: [\n    {\n      name: \"name\",\n      description: \"Name of shadow tree slot\",\n      type: \"string\",\n    },\n  ],\n  source: [\n    attribute_height_10887hn,\n    {\n      name: \"media\",\n      description: \"Applicable media\",\n      type: \"string\",\n    },\n    attribute_sizes_o9chmv,\n    attribute_src_hol6ri,\n    attribute_srcset_1xpiw3a,\n    attribute_type_1swzf44,\n    attribute_width_d9q964,\n  ],\n  stop: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_offset_6sg3je,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  svg: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    {\n      name: \"baseProfile\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    {\n      name: \"contentScriptType\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"contentStyleType\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_preserveAspectRatio_19as3ta,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    {\n      name: \"version\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_viewBox_xml4ya,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n    attribute_zoomAndPan_1dv6jzw,\n  ],\n  switch: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  symbol: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_preserveAspectRatio_19as3ta,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_viewBox_xml4ya,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  td: [\n    attribute_colspan_r4n987,\n    attribute_headers_gc0t0h,\n    attribute_rowspan_o1tr8d,\n  ],\n  text: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_dx_1qijkbt,\n    attribute_dy_1me9r5b,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_lengthAdjust_8jgvje,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_rotate_nmq93d,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_textLength_1br26vp,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  textPath: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    {\n      name: \"method\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    {\n      name: \"spacing\",\n      description: \"\",\n      type: \"string\",\n    },\n    {\n      name: \"startOffset\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n  ],\n  textarea: [\n    attribute_autocomplete_n0adlq,\n    {\n      name: \"cols\",\n      description: \"Maximum number of characters per line\",\n      type: \"number\",\n    },\n    {\n      name: \"dirname\",\n      description:\n        \"Name of form control to use for sending the element's directionality in form submission\",\n      type: \"string\",\n    },\n    attribute_disabled_1ceu012,\n    attribute_form_1v3e5z4,\n    attribute_maxlength_1txfzsx,\n    attribute_minlength_xf39d6,\n    attribute_name_1nwqzo5,\n    attribute_placeholder_1bihs5f,\n    attribute_readonly_860ef5,\n    attribute_required_bgte6f,\n    {\n      name: \"rows\",\n      description: \"Number of lines to show\",\n      type: \"number\",\n    },\n    attribute_value_15qkpz,\n    {\n      name: \"wrap\",\n      description:\n        \"How the value of the form control is to be wrapped for form submission\",\n      type: \"select\",\n      options: [\"soft\", \"hard\"],\n    },\n  ],\n  th: [\n    {\n      name: \"abbr\",\n      description:\n        \"Alternative label to use for the header cell when referencing the cell in other contexts\",\n      type: \"string\",\n    },\n    attribute_colspan_r4n987,\n    attribute_headers_gc0t0h,\n    attribute_rowspan_o1tr8d,\n    {\n      name: \"scope\",\n      description: \"Specifies which cells the header cell applies to\",\n      type: \"select\",\n      options: [\"row\", \"col\", \"rowgroup\", \"colgroup\"],\n    },\n  ],\n  time: [\n    {\n      name: \"datetime\",\n      description: \"Machine-readable value\",\n      type: \"string\",\n    },\n  ],\n  track: [\n    {\n      name: \"default\",\n      description: \"Enable the track if no other text track is more suitable\",\n      type: \"boolean\",\n    },\n    {\n      name: \"kind\",\n      description: \"The type of text track\",\n      type: \"select\",\n      options: [\n        \"subtitles\",\n        \"captions\",\n        \"descriptions\",\n        \"chapters\",\n        \"metadata\",\n      ],\n    },\n    attribute_label_kiwyp8,\n    attribute_src_hol6ri,\n    {\n      name: \"srclang\",\n      description: \"Language of the text track\",\n      type: \"string\",\n    },\n  ],\n  tspan: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_dx_1qijkbt,\n    attribute_dy_1me9r5b,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_lengthAdjust_8jgvje,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_rotate_nmq93d,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_textLength_1br26vp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  use: [\n    attribute_alignment_baseline_1mavux3,\n    attribute_baseline_shift_1992be8,\n    attribute_class_1nw9qfk,\n    attribute_clip_17eben,\n    attribute_clip_path_l9pwtb,\n    attribute_clip_rule_l41gyg,\n    attribute_color_j22118,\n    attribute_color_interpolation_12ntgve,\n    attribute_color_interpolation_filters_1ng37c4,\n    attribute_color_profile_1arfqgt,\n    attribute_color_rendering_39twr5,\n    attribute_cursor_41nrie,\n    attribute_direction_1aonvp7,\n    attribute_display_139qswi,\n    attribute_dominant_baseline_1pb95lg,\n    attribute_enable_background_1tjvhfc,\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_fill_rz4yt2,\n    attribute_fill_opacity_1ki263g,\n    attribute_fill_rule_1vp7xd6,\n    attribute_filter_xm2t59,\n    attribute_flood_color_vdztlb,\n    attribute_flood_opacity_wsup97,\n    attribute_font_family_1oj20lz,\n    attribute_font_size_1522ioq,\n    attribute_font_size_adjust_1986ttn,\n    attribute_font_stretch_1szmgzt,\n    attribute_font_style_10u7qj4,\n    attribute_font_variant_11wrur,\n    attribute_font_weight_18kq5ko,\n    attribute_glyph_orientation_horizontal_1x9zgr4,\n    attribute_glyph_orientation_vertical_lrbm8t,\n    attribute_height_19cj0d2,\n    attribute_id_1wtm880,\n    attribute_image_rendering_7zonn5,\n    attribute_kerning_sbqwuk,\n    attribute_letter_spacing_utdryb,\n    attribute_lighting_color_2qihow,\n    attribute_marker_end_1u3sadl,\n    attribute_marker_mid_5umtul,\n    attribute_marker_start_110wno8,\n    attribute_mask_q5u9f7,\n    attribute_opacity_1lpnivz,\n    attribute_overflow_7ql0qh,\n    attribute_pointer_events_vrdynn,\n    attribute_requiredExtensions_c4vtka,\n    attribute_requiredFeatures_15shkxl,\n    attribute_shape_rendering_r2u534,\n    attribute_stop_color_1cuxwox,\n    attribute_stop_opacity_15fkjpj,\n    attribute_stroke_1uzwglv,\n    attribute_stroke_dasharray_3u6g90,\n    attribute_stroke_dashoffset_y5ku5x,\n    attribute_stroke_linecap_ivmqaf,\n    attribute_stroke_linejoin_16dr5ik,\n    attribute_stroke_miterlimit_jeie8q,\n    attribute_stroke_opacity_g8k2u,\n    attribute_stroke_width_dc7451,\n    attribute_systemLanguage_ytoswp,\n    attribute_text_anchor_1jtk2wl,\n    attribute_text_decoration_9s86ib,\n    attribute_text_rendering_31hh4f,\n    attribute_transform_1czbvkp,\n    attribute_unicode_bidi_mqhd45,\n    attribute_visibility_1n5r3h2,\n    attribute_width_5n2j7z,\n    attribute_word_spacing_1f6bo7g,\n    attribute_writing_mode_1nx1p7l,\n    attribute_x_1perc20,\n    attribute_xlink_actuate_h23qr,\n    attribute_xlink_arcrole_lqsmgf,\n    attribute_xlink_href_18c6swo,\n    attribute_xlink_role_7sfop7,\n    attribute_xlink_show_im5v4m,\n    attribute_xlink_title_1xpi33x,\n    attribute_xlink_type_zka1lg,\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_y_14t62ez,\n  ],\n  video: [\n    attribute_autoplay_1a0sp4x,\n    attribute_controls_i44ww0,\n    attribute_crossorigin_jl1m2v,\n    attribute_height_10887hn,\n    attribute_loop_i1hxl,\n    attribute_muted_1hc6ujd,\n    {\n      name: \"playsinline\",\n      description:\n        \"Encourage the user agent to display video content within the element's playback area\",\n      type: \"boolean\",\n    },\n    {\n      name: \"poster\",\n      description: \"Poster frame to show prior to video playback\",\n      type: \"string\",\n    },\n    attribute_preload_1a7kb09,\n    attribute_src_hol6ri,\n    attribute_width_d9q964,\n  ],\n  view: [\n    attribute_externalResourcesRequired_1bnmqma,\n    attribute_id_1wtm880,\n    attribute_preserveAspectRatio_19as3ta,\n    attribute_viewBox_xml4ya,\n    {\n      name: \"viewTarget\",\n      description: \"\",\n      type: \"string\",\n    },\n    attribute_xml_base_wl2bkx,\n    attribute_xml_lang_1hno5fx,\n    attribute_xml_space_bj3sfm,\n    attribute_zoomAndPan_1dv6jzw,\n  ],\n};\n"
  },
  {
    "path": "packages/html-data/src/__generated__/elements.ts",
    "content": "type Element = {\n  description: string;\n  categories: string[];\n  children: string[];\n};\n\nexport const elementsByTag: Record<string, Element> = {\n  a: {\n    description: \"Hyperlink\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"interactive\", \"palpable\"],\n    children: [\"transparent\"],\n  },\n  abbr: {\n    description: \"Abbreviation\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  address: {\n    description: \"Contact information for a page or article element\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  area: {\n    description: \"Hyperlink or dead area on an image map\",\n    categories: [\"html-element\", \"flow\", \"phrasing\"],\n    children: [],\n  },\n  article: {\n    description: \"Self-contained syndicatable or reusable composition\",\n    categories: [\"html-element\", \"flow\", \"sectioning\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  aside: {\n    description: \"Sidebar for tangentially related content\",\n    categories: [\"html-element\", \"flow\", \"sectioning\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  audio: {\n    description: \"Audio player\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"embedded\",\n      \"interactive\",\n      \"palpable\",\n    ],\n    children: [\"source\", \"track\", \"transparent\"],\n  },\n  b: {\n    description: \"Keywords\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  bdi: {\n    description: \"Text directionality isolation\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  bdo: {\n    description: \"Text directionality formatting\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  blockquote: {\n    description: \"A section quoted from another source\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  body: {\n    description: \"Document body\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  br: {\n    description: \"Line break, e.g. in poem or postal address\",\n    categories: [\"html-element\", \"flow\", \"phrasing\"],\n    children: [],\n  },\n  button: {\n    description: \"Button control\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"interactive\",\n      \"listed\",\n      \"labelable\",\n      \"submittable\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [\"phrasing\"],\n  },\n  canvas: {\n    description: \"Scriptable bitmap canvas\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"embedded\", \"palpable\"],\n    children: [\"transparent\"],\n  },\n  caption: {\n    description: \"Table caption\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  cite: {\n    description: \"Title of a work\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  code: {\n    description: \"Computer code\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  col: {\n    description: \"Table column\",\n    categories: [\"html-element\", \"none\"],\n    children: [],\n  },\n  colgroup: {\n    description: \"Group of columns in a table\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"col\", \"template\"],\n  },\n  data: {\n    description: \"Machine-readable equivalent\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  datalist: {\n    description: \"Container for options for combo box control\",\n    categories: [\"html-element\", \"flow\", \"phrasing\"],\n    children: [\"phrasing\", \"option\", \"script-supporting elements\"],\n  },\n  dd: {\n    description: \"Content for corresponding dt element(s)\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  del: {\n    description: \"A removal from the document\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"transparent\"],\n  },\n  details: {\n    description: \"Disclosure control for hiding details\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"summary\", \"flow\"],\n  },\n  dfn: {\n    description: \"Defining instance\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  dialog: {\n    description: \"Dialog box or window\",\n    categories: [\"html-element\", \"flow\"],\n    children: [\"flow\"],\n  },\n  div: {\n    description:\n      \"Generic flow container, or container for name-value groups in dl elements\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  dl: {\n    description:\n      \"Association list consisting of zero or more name-value groups\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"dt\", \"dd\", \"div\", \"script-supporting elements\"],\n  },\n  dt: {\n    description: \"Legend for corresponding dd element(s)\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  em: {\n    description: \"Stress emphasis\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  embed: {\n    description: \"Plugin\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"embedded\",\n      \"interactive\",\n      \"palpable\",\n    ],\n    children: [],\n  },\n  fieldset: {\n    description: \"Group of form controls\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"listed\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [\"legend\", \"flow\"],\n  },\n  figcaption: {\n    description: \"Caption for figure\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  figure: {\n    description: \"Figure with optional caption\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"figcaption\", \"flow\"],\n  },\n  footer: {\n    description: \"Footer for a page or section\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  form: {\n    description: \"User-submittable form\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  h1: {\n    description: \"Heading\",\n    categories: [\"html-element\", \"flow\", \"heading content\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  h2: {\n    description: \"Heading\",\n    categories: [\"html-element\", \"flow\", \"heading content\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  h3: {\n    description: \"Heading\",\n    categories: [\"html-element\", \"flow\", \"heading content\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  h4: {\n    description: \"Heading\",\n    categories: [\"html-element\", \"flow\", \"heading content\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  h5: {\n    description: \"Heading\",\n    categories: [\"html-element\", \"flow\", \"heading content\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  h6: {\n    description: \"Heading\",\n    categories: [\"html-element\", \"flow\", \"heading content\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  head: {\n    description: \"Container for document metadata\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"metadata content\"],\n  },\n  header: {\n    description: \"Introductory or navigational aids for a page or section\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  hgroup: {\n    description: \"Heading container\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\n      \"h1\",\n      \"h2\",\n      \"h3\",\n      \"h4\",\n      \"h5\",\n      \"h6\",\n      \"script-supporting elements\",\n      \"p\",\n    ],\n  },\n  hr: {\n    description: \"Thematic break\",\n    categories: [\"html-element\", \"flow\"],\n    children: [],\n  },\n  html: {\n    description: \"Root element\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"head\", \"body\"],\n  },\n  i: {\n    description: \"Alternate voice\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  iframe: {\n    description: \"Child navigable\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"embedded\",\n      \"interactive\",\n      \"palpable\",\n    ],\n    children: [],\n  },\n  img: {\n    description: \"Image\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"embedded\",\n      \"interactive\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [],\n  },\n  input: {\n    description: \"Form control\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"interactive\",\n      \"listed\",\n      \"labelable\",\n      \"submittable\",\n      \"resettable\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [],\n  },\n  ins: {\n    description: \"An addition to the document\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"transparent\"],\n  },\n  kbd: {\n    description: \"User input\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  label: {\n    description: \"Caption for a form control\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"interactive\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  legend: {\n    description: \"Caption for fieldset\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"phrasing\", \"heading content\"],\n  },\n  li: {\n    description: \"List item\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  main: {\n    description: \"Container for the dominant contents of the document\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  map: {\n    description: \"Image map\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"transparent\", \"area\"],\n  },\n  mark: {\n    description: \"Highlight\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  menu: {\n    description: \"Menu of commands\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"li\", \"script-supporting elements\"],\n  },\n  meter: {\n    description: \"Gauge\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"labelable\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  nav: {\n    description: \"Section with navigational links\",\n    categories: [\"html-element\", \"flow\", \"sectioning\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  object: {\n    description: \"Image, child navigable, or plugin\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"embedded\",\n      \"interactive\",\n      \"listed\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [\"transparent\"],\n  },\n  ol: {\n    description: \"Ordered list\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"li\", \"script-supporting elements\"],\n  },\n  optgroup: {\n    description: \"Group of options in a list box\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"option\", \"script-supporting elements\"],\n  },\n  option: {\n    description: \"Option in a list box or combo box control\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"text\"],\n  },\n  output: {\n    description: \"Calculated output value\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"listed\",\n      \"labelable\",\n      \"resettable\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [\"phrasing\"],\n  },\n  p: {\n    description: \"Paragraph\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  picture: {\n    description: \"Image\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"embedded\", \"palpable\"],\n    children: [\"source\", \"one img\", \"script-supporting elements\"],\n  },\n  pre: {\n    description: \"Block of preformatted text\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  progress: {\n    description: \"Progress bar\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"labelable\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  q: {\n    description: \"Quotation\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  rp: {\n    description: \"Parenthesis for ruby annotation text\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"text\"],\n  },\n  rt: {\n    description: \"Ruby annotation text\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"phrasing\"],\n  },\n  ruby: {\n    description: \"Ruby annotation(s)\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\", \"rt\", \"rp\"],\n  },\n  s: {\n    description: \"Inaccurate text\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  samp: {\n    description: \"Computer output\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  search: {\n    description: \"Container for search controls\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  section: {\n    description: \"Generic document or application section\",\n    categories: [\"html-element\", \"flow\", \"sectioning\", \"palpable\"],\n    children: [\"flow\"],\n  },\n  select: {\n    description: \"List box control\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"interactive\",\n      \"listed\",\n      \"labelable\",\n      \"submittable\",\n      \"resettable\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [\"option\", \"optgroup\", \"script-supporting elements\"],\n  },\n  slot: {\n    description: \"Shadow tree slot\",\n    categories: [\"html-element\", \"flow\", \"phrasing\"],\n    children: [\"transparent\"],\n  },\n  small: {\n    description: \"Side comment\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  source: {\n    description: \"Image source for img or media source for video or audio\",\n    categories: [\"html-element\", \"none\"],\n    children: [],\n  },\n  span: {\n    description: \"Generic phrasing container\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  strong: {\n    description: \"Importance\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  sub: {\n    description: \"Subscript\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  summary: {\n    description: \"Caption for details\",\n    categories: [\"html-element\", \"none\", \"interactive\"],\n    children: [\"phrasing\", \"heading content\"],\n  },\n  sup: {\n    description: \"Superscript\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  table: {\n    description: \"Table\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\n      \"caption\",\n      \"colgroup\",\n      \"thead\",\n      \"tbody\",\n      \"tfoot\",\n      \"tr\",\n      \"script-supporting elements\",\n    ],\n  },\n  tbody: {\n    description: \"Group of rows in a table\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"tr\", \"script-supporting elements\"],\n  },\n  td: {\n    description: \"Table cell\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"flow\"],\n  },\n  textarea: {\n    description: \"Multiline text controls\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"interactive\",\n      \"listed\",\n      \"labelable\",\n      \"submittable\",\n      \"resettable\",\n      \"form-associated\",\n      \"palpable\",\n    ],\n    children: [],\n  },\n  tfoot: {\n    description: \"Group of footer rows in a table\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"tr\", \"script-supporting elements\"],\n  },\n  th: {\n    description: \"Table header cell\",\n    categories: [\"html-element\", \"interactive\"],\n    children: [\"flow\"],\n  },\n  thead: {\n    description: \"Group of heading rows in a table\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"tr\", \"script-supporting elements\"],\n  },\n  time: {\n    description: \"Machine-readable equivalent of date- or time-related data\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  tr: {\n    description: \"Table row\",\n    categories: [\"html-element\", \"none\"],\n    children: [\"th\", \"td\", \"script-supporting elements\"],\n  },\n  track: {\n    description: \"Timed text track\",\n    categories: [\"html-element\", \"none\"],\n    children: [],\n  },\n  u: {\n    description: \"Unarticulated annotation\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  ul: {\n    description: \"List\",\n    categories: [\"html-element\", \"flow\", \"palpable\"],\n    children: [\"li\", \"script-supporting elements\"],\n  },\n  var: {\n    description: \"Variable\",\n    categories: [\"html-element\", \"flow\", \"phrasing\", \"palpable\"],\n    children: [\"phrasing\"],\n  },\n  video: {\n    description: \"Video player\",\n    categories: [\n      \"html-element\",\n      \"flow\",\n      \"phrasing\",\n      \"embedded\",\n      \"interactive\",\n      \"palpable\",\n    ],\n    children: [\"source\", \"track\", \"transparent\"],\n  },\n  wbr: {\n    description: \"Line breaking opportunity\",\n    categories: [\"html-element\", \"flow\", \"phrasing\"],\n    children: [],\n  },\n  svg: {\n    description: \"\",\n    categories: [\"html-element\", \"flow\", \"phrasing\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  g: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  defs: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  desc: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [],\n  },\n  symbol: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  use: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  image: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  switch: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"a\",\n      \"foreignObject\",\n      \"g\",\n      \"image\",\n      \"svg\",\n      \"switch\",\n      \"text\",\n      \"use\",\n    ],\n  },\n  path: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  rect: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  circle: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  ellipse: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  line: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  polyline: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  polygon: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n    ],\n  },\n  text: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"altGlyph\",\n      \"textPath\",\n      \"tref\",\n      \"tspan\",\n      \"a\",\n    ],\n  },\n  tspan: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"a\",\n      \"altGlyph\",\n      \"animate\",\n      \"animateColor\",\n      \"set\",\n      \"tref\",\n      \"tspan\",\n    ],\n  },\n  textPath: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"a\",\n      \"altGlyph\",\n      \"animate\",\n      \"animateColor\",\n      \"set\",\n      \"tref\",\n      \"tspan\",\n    ],\n  },\n  marker: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  linearGradient: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"animate\",\n      \"animateTransform\",\n      \"set\",\n      \"stop\",\n    ],\n  },\n  radialGradient: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"animate\",\n      \"animateTransform\",\n      \"set\",\n      \"stop\",\n    ],\n  },\n  stop: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"animateColor\", \"set\"],\n  },\n  pattern: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  clipPath: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"text\",\n      \"use\",\n    ],\n  },\n  mask: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"animate\",\n      \"animateColor\",\n      \"animateMotion\",\n      \"animateTransform\",\n      \"set\",\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"circle\",\n      \"ellipse\",\n      \"line\",\n      \"path\",\n      \"polygon\",\n      \"polyline\",\n      \"rect\",\n      \"defs\",\n      \"g\",\n      \"svg\",\n      \"symbol\",\n      \"use\",\n      \"linearGradient\",\n      \"radialGradient\",\n      \"a\",\n      \"altGlyphDef\",\n      \"clipPath\",\n      \"color-profile\",\n      \"cursor\",\n      \"filter\",\n      \"font\",\n      \"font-face\",\n      \"foreignObject\",\n      \"image\",\n      \"marker\",\n      \"mask\",\n      \"pattern\",\n      \"script\",\n      \"style\",\n      \"switch\",\n      \"text\",\n      \"view\",\n    ],\n  },\n  filter: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\n      \"desc\",\n      \"metadata\",\n      \"title\",\n      \"feBlend\",\n      \"feColorMatrix\",\n      \"feComponentTransfer\",\n      \"feComposite\",\n      \"feConvolveMatrix\",\n      \"feDiffuseLighting\",\n      \"feDisplacementMap\",\n      \"feFlood\",\n      \"feGaussianBlur\",\n      \"feImage\",\n      \"feMerge\",\n      \"feMorphology\",\n      \"feOffset\",\n      \"feSpecularLighting\",\n      \"feTile\",\n      \"feTurbulence\",\n      \"animate\",\n      \"set\",\n    ],\n  },\n  feDistantLight: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  fePointLight: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feSpotLight: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feBlend: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feColorMatrix: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feComponentTransfer: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"feFuncA\", \"feFuncB\", \"feFuncG\", \"feFuncR\"],\n  },\n  feFuncR: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feFuncG: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feFuncB: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feFuncA: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feComposite: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feConvolveMatrix: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feDiffuseLighting: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [],\n  },\n  feDisplacementMap: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feFlood: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"animateColor\", \"set\"],\n  },\n  feGaussianBlur: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feImage: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"animateTransform\", \"set\"],\n  },\n  feMerge: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"feMergeNode\"],\n  },\n  feMergeNode: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feMorphology: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feOffset: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feSpecularLighting: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [],\n  },\n  feTile: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  feTurbulence: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"animate\", \"set\"],\n  },\n  view: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"desc\", \"metadata\", \"title\"],\n  },\n  animate: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"desc\", \"metadata\", \"title\"],\n  },\n  set: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"desc\", \"metadata\", \"title\"],\n  },\n  animateMotion: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"mpath\"],\n  },\n  mpath: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"desc\", \"metadata\", \"title\"],\n  },\n  animateTransform: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [\"desc\", \"metadata\", \"title\"],\n  },\n  metadata: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [],\n  },\n  foreignObject: {\n    description: \"\",\n    categories: [\"svg-element\", \"none\"],\n    children: [],\n  },\n};\n"
  },
  {
    "path": "packages/html-data/src/index.ts",
    "content": "export * from \"./__generated__/elements\";\nexport * from \"./__generated__/attributes\";\nexport * from \"./__generated__/aria\";\nexport * from \"./pseudo-classes\";\n"
  },
  {
    "path": "packages/html-data/src/pseudo-classes.ts",
    "content": "// https://drafts.csswg.org/selectors\n\nconst location = [\n  // ':link',\n  \":visited\",\n  // ':any-link',\n  \":local-link\",\n  // ':target',\n  // ':target-within',\n];\n\nconst userAction = [\":hover\", \":focus-visible\", \":focus-within\", \":active\"];\n\nconst ability = [\n  // \":enabled\",\n  \":disabled\",\n];\n\nconst validity = [\n  // \":valid\",\n  \":invalid\",\n  // \":user-valid\",\n  \":user-invalid\",\n];\n\nconst required = [\n  \":required\",\n  // \":optional\"\n];\n\nexport const pseudoClassesByTag: Record<string, string[]> = {\n  \"*\": userAction,\n  a: [...location],\n  area: [...location],\n  button: [...ability],\n  label: [],\n  input: [\n    \":placeholder-shown\",\n    // @todo temporary until proper pseudo elements support is added\n    \"::placeholder\",\n    ...ability,\n    ...validity,\n    ...required,\n    \":checked\",\n    // \":indeterminate\",\n    // :in-range\n    // :out-of-range\n    // \":open\",\n  ],\n  textarea: [\n    \":placeholder-shown\",\n    // @todo temporary until proper pseudo elements support is added\n    \"::placeholder\",\n    ...ability,\n    ...validity,\n    ...required,\n  ],\n  select: [\n    ...ability,\n    ...validity,\n    ...required,\n    // \":open\"\n  ],\n  optgroup: [...ability],\n  option: [...ability, \":checked\", \":default\"],\n  fieldset: [...ability, ...validity],\n  progress: [\":indeterminate\"],\n  details: [\":open\"],\n  dialog: [\":open\"],\n};\n"
  },
  {
    "path": "packages/html-data/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/html-data/tsconfig.typecheck.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": null\n  }\n}\n"
  },
  {
    "path": "packages/http-client/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/http-client/README.md",
    "content": "# Webstudio HTTP Client\n\nEnables to optimize the http calls to Webstudio API.\n"
  },
  {
    "path": "packages/http-client/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/http-client\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio HTTP Client\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/sdk\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"vitest\": \"^3.1.2\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\",\n      \"types\": \"./lib/types/index.d.ts\",\n      \"import\": \"./lib/index.js\",\n      \"default\": \"./src/index.ts\"\n    }\n  },\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/http-client/src/index.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { parseBuilderUrl } from \"./index\";\n\ntest(\"parseBuilderUrl wstd.dev\", async () => {\n  expect(\n    parseBuilderUrl(\"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.wstd.dev\")\n  ).toMatchInlineSnapshot(`\n    {\n      \"projectId\": \"090e6e14-ae50-4b2e-bd22-71733cec05bb\",\n      \"sourceOrigin\": \"https://wstd.dev\",\n    }\n  `);\n});\n\ntest(\"parseBuilderUrl localhost\", async () => {\n  expect(\n    parseBuilderUrl(\"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.localhost\")\n  ).toMatchInlineSnapshot(`\n    {\n      \"projectId\": \"090e6e14-ae50-4b2e-bd22-71733cec05bb\",\n      \"sourceOrigin\": \"https://localhost\",\n    }\n  `);\n});\n\ntest(\"parseBuilderUrl localhost\", async () => {\n  expect(parseBuilderUrl(\"https://p-eee.localhost\")).toMatchInlineSnapshot(`\n    {\n      \"projectId\": undefined,\n      \"sourceOrigin\": \"https://p-eee.localhost\",\n    }\n  `);\n});\n\ntest(\"parseBuilderUrl development.webstudio.is\", async () => {\n  expect(\n    parseBuilderUrl(\n      \"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.development.webstudio.is\"\n    )\n  ).toMatchInlineSnapshot(`\n    {\n      \"projectId\": \"090e6e14-ae50-4b2e-bd22-71733cec05bb\",\n      \"sourceOrigin\": \"https://development.webstudio.is\",\n    }\n  `);\n});\n\ntest(\"parseBuilderUrl main.development.webstudio.is\", async () => {\n  expect(\n    parseBuilderUrl(\n      \"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb-dot-main.development.webstudio.is\"\n    )\n  ).toMatchInlineSnapshot(`\n    {\n      \"projectId\": \"090e6e14-ae50-4b2e-bd22-71733cec05bb\",\n      \"sourceOrigin\": \"https://main.development.webstudio.is\",\n    }\n  `);\n});\n\ntest(\"parseBuilderUrl branch.development.webstudio.is\", async () => {\n  expect(\n    parseBuilderUrl(\n      \"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb-dot-branch.development.webstudio.is\"\n    )\n  ).toMatchInlineSnapshot(`\n{\n  \"projectId\": \"090e6e14-ae50-4b2e-bd22-71733cec05bb\",\n  \"sourceOrigin\": \"https://branch.development.webstudio.is\",\n}\n`);\n});\n\ntest(\"parseBuilderUrl apps.webstudio.is\", async () => {\n  expect(\n    parseBuilderUrl(\n      \"https://p-090e6e14-ae50-4b2e-bd22-71733cec05bb.apps.webstudio.is\"\n    )\n  ).toMatchInlineSnapshot(`\n    {\n      \"projectId\": \"090e6e14-ae50-4b2e-bd22-71733cec05bb\",\n      \"sourceOrigin\": \"https://apps.webstudio.is\",\n    }\n  `);\n});\n\ntest(\"parseBuilderUrl apps.webstudio.is\", async () => {\n  expect(parseBuilderUrl(\"https://apps.webstudio.is\")).toMatchInlineSnapshot(`\n    {\n      \"projectId\": undefined,\n      \"sourceOrigin\": \"https://apps.webstudio.is\",\n    }\n  `);\n});\n"
  },
  {
    "path": "packages/http-client/src/index.ts",
    "content": "import type {\n  Asset,\n  Breakpoint,\n  DataSource,\n  Deployment,\n  Instance,\n  Page,\n  Pages,\n  Prop,\n  Resource,\n  StyleDecl,\n  StyleDeclKey,\n  StyleSource,\n  StyleSourceSelection,\n} from \"@webstudio-is/sdk\";\n\nexport type Data = {\n  page: Page;\n  pages: Array<Page>;\n  build: {\n    id: string;\n    projectId: string;\n    version: number;\n    createdAt: string;\n    updatedAt: string;\n    pages: Pages;\n    breakpoints: [Breakpoint[\"id\"], Breakpoint][];\n    styles: [StyleDeclKey, StyleDecl][];\n    styleSources: [StyleSource[\"id\"], StyleSource][];\n    styleSourceSelections: [Instance[\"id\"], StyleSourceSelection][];\n    props: [Prop[\"id\"], Prop][];\n    instances: [Instance[\"id\"], Instance][];\n    dataSources: [DataSource[\"id\"], DataSource][];\n    resources: [Resource[\"id\"], Resource][];\n    deployment?: Deployment | undefined;\n  };\n  assets: Array<Asset>;\n  origin?: string;\n};\n\n// @todo: broken as expects non 200 code\nconst getLatestBuildUsingProjectId = async (params: {\n  projectId: string;\n  origin: string;\n  authToken: string;\n}): Promise<{ buildId: string | null }> => {\n  const { origin, projectId, authToken } = params;\n\n  const { sourceOrigin } = parseBuilderUrl(origin);\n\n  const url = new URL(sourceOrigin);\n  url.pathname = `/rest/buildId/${projectId}`;\n\n  const headers = new Headers();\n  headers.set(\"x-auth-token\", authToken);\n\n  const response = await fetch(url.href, { headers });\n\n  if (response.ok) {\n    return await response.json();\n  }\n\n  const message = await response.text();\n  throw new Error(message.slice(0, 1000));\n};\n\nexport const loadProjectDataByBuildId = async (\n  params: {\n    buildId: string;\n    origin: string;\n  } & (\n    | {\n        seviceToken: string;\n      }\n    | { authToken: string }\n  )\n): Promise<Data> => {\n  const { sourceOrigin } = parseBuilderUrl(params.origin);\n\n  const url = new URL(sourceOrigin);\n\n  url.pathname = `/rest/build/${params.buildId}`;\n\n  const headers = new Headers();\n  if (\"seviceToken\" in params) {\n    headers.set(\"Authorization\", params.seviceToken);\n  } else {\n    headers.set(\"x-auth-token\", params.authToken);\n  }\n\n  const response = await fetch(url.href, {\n    headers,\n  });\n\n  if (response.ok) {\n    return await response.json();\n  }\n\n  const message = await response.text();\n  throw new Error(message.slice(0, 1000));\n};\n\nexport const loadProjectDataByProjectId = async (params: {\n  projectId: string;\n  origin: string;\n  authToken: string;\n}): Promise<Data> => {\n  const result = await getLatestBuildUsingProjectId(params);\n  if (result.buildId === null) {\n    throw new Error(`The project is not published yet`);\n  }\n\n  return await loadProjectDataByBuildId({\n    buildId: result.buildId,\n    origin: params.origin,\n    authToken: params.authToken,\n  });\n};\n\n// For easier detecting the builder URL\nconst buildProjectDomainPrefix = \"p-\";\n\nexport const parseBuilderUrl = (urlStr: string) => {\n  const url = new URL(urlStr);\n\n  const fragments = url.host.split(\".\");\n  // Regular expression to match the prefix, UUID, and any optional string after '-dot-'\n  const re =\n    /^(?<prefix>[a-z-]+)(?<uuid>[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})(-dot-(?<branch>.*))?/;\n  const match = fragments[0].match(re);\n\n  // Extract prefix, projectId (UUID), and branch (if exists)\n  const prefix = match?.groups?.prefix;\n  const projectId = match?.groups?.uuid;\n  const branch = match?.groups?.branch;\n\n  if (prefix !== buildProjectDomainPrefix) {\n    return {\n      projectId: undefined,\n      sourceOrigin: url.origin,\n    };\n  }\n\n  if (projectId === undefined) {\n    return {\n      projectId: undefined,\n      sourceOrigin: url.origin,\n    };\n  }\n\n  fragments[0] = fragments[0].replace(re, branch ?? \"\");\n\n  const sourceUrl = new URL(url.origin);\n  sourceUrl.protocol = \"https\";\n  sourceUrl.host = fragments.filter(Boolean).join(\".\");\n\n  return {\n    projectId,\n    sourceOrigin: sourceUrl.origin,\n  };\n};\n"
  },
  {
    "path": "packages/http-client/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/http-client/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/icons/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/icons/generate.ts",
    "content": "import { mkdir, readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, extname } from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { type Config, optimize } from \"svgo\";\nimport { convertSvgToJsx } from \"@svgo/jsx\";\nimport { pascalCase } from \"change-case\";\n\nconst prefixCache = new Map<string, string>();\nconst getHashedPrefix = (path?: string) => {\n  if (path === undefined) {\n    return \"svg\";\n  }\n  const cached = prefixCache.get(path);\n  if (cached !== undefined) {\n    return cached;\n  }\n  const hash = createHash(\"sha256\").update(path).digest(\"hex\").slice(0, 8);\n  prefixCache.set(path, hash);\n  return hash;\n};\n\nconst sharedPlugins: Config[\"plugins\"] = [\n  {\n    name: \"preset-default\",\n    params: {\n      overrides: {\n        // preserve viewBox\n        removeViewBox: false,\n        convertTransform: false,\n        inlineStyles: false,\n        cleanupIds: false,\n      },\n    },\n  },\n  // convert width/height to viewBox if missing\n  { name: \"removeDimensions\" },\n  {\n    name: \"prefixIds\",\n    params: {\n      prefix: (_node: unknown, info: { path?: string }) =>\n        `c${getHashedPrefix(info.path)}`,\n      prefixClassNames: false,\n    },\n  },\n];\n\ntype GenerateOptions = {\n  exportName: string;\n  file: string;\n  content: string;\n};\n\nexport const generateStringExport = (options: GenerateOptions) => {\n  const { data: optimized } = optimize(options.content, {\n    path: options.file,\n    plugins: [\n      ...sharedPlugins,\n      {\n        name: \"addAttributesToSVGElement\",\n        params: {\n          attributes: [\n            {\n              fill: \"currentColor\",\n              width: \"100%\",\n              height: \"100%\",\n              style: \"display: block;\",\n            },\n          ],\n        },\n      },\n    ],\n  });\n  return `export const ${options.exportName} = \\`${optimized.trim()}\\`;`;\n};\n\nconst generateComponentExport = (options: GenerateOptions) => {\n  const { jsx } = convertSvgToJsx({\n    target: \"react-dom\",\n    file: options.file,\n    svg: options.content,\n    svgProps: {\n      width: \"{size}\",\n      height: \"{size}\",\n      fill: \"{fill}\",\n      \"{...props}\": null,\n      ref: \"{forwardedRef}\",\n    },\n    plugins: sharedPlugins,\n  });\n  return `\nexport const ${options.exportName}: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      ${jsx}\n    );\n  }\n);\n${options.exportName}.displayName = \"${options.exportName}\";\n`.trim();\n};\n\nconst transformComponentName = (filename: string) => {\n  const name = basename(filename, extname(filename));\n  // digits cannot start variable name\n  return `${pascalCase(name, { mergeAmbiguousCharacters: true }).replace(/^[0-9]/, (char) => `_${char}`)}Icon`;\n};\n\nlet stringContent = \"\";\nlet componentsContent = `\nimport { forwardRef } from \"react\";\nimport type { IconComponent } from \"../types\";\n\n`.trimStart();\n\nconst start = process.hrtime.bigint();\nlet count = 0;\n\nfor (const name of await readdir(\"./icons\")) {\n  if (name.endsWith(\".svg\")) {\n    count += 1;\n    const exportName = transformComponentName(name);\n    const file = `./icons/${name}`;\n    const content = await readFile(file, \"utf-8\");\n    stringContent +=\n      generateStringExport({ exportName, file, content }) + \"\\n\\n\";\n    componentsContent +=\n      generateComponentExport({ exportName, file, content }) + \"\\n\\n\";\n  }\n}\n\nawait mkdir(\"./src/__generated__\", { recursive: true });\nawait writeFile(\"./src/__generated__/svg.ts\", stringContent);\nawait writeFile(\"./src/__generated__/components.tsx\", componentsContent);\n\nconst end = process.hrtime.bigint();\n\nconsole.info(`Compiled ${count} icons in ${(end - start) / BigInt(1e6)}ms`);\n"
  },
  {
    "path": "packages/icons/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/icons\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio Icons\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"rm -rf lib && esbuild src/index.ts src/__generated__/svg.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --declarationDir lib/types\",\n    \"generate\": \"rm -fr src/__generated__ && tsx generate.ts && prettier --write src/__generated__\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@svgo/jsx\": \"^0.4.2\",\n    \"@types/react\": \"^18.2.70\",\n    \"@webstudio-is/design-system\": \"workspace:*\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"change-case\": \"^5.4.4\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"svgo\": \"^3.0.2\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\",\n      \"types\": \"./lib/types/index.d.ts\",\n      \"import\": \"./lib/index.js\"\n    },\n    \"./svg\": {\n      \"webstudio\": \"./src/__generated__/svg.ts\",\n      \"types\": \"./lib/types/__generated__/svg.d.ts\",\n      \"import\": \"./lib/__generated__/svg.js\"\n    }\n  },\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/icons/src/__generated__/components.tsx",
    "content": "import { forwardRef } from \"react\";\nimport type { IconComponent } from \"../types\";\n\nexport const AccordionIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.056 8H14.5V4.101a1.3 1.3 0 0 0-1.3-1.299H2.8a1.3 1.3 0 0 0-1.3 1.3V8H13.056ZM13.056 13.198h.145a1.3 1.3 0 0 0 1.299-1.3V8h-13v3.899a1.3 1.3 0 0 0 1.3 1.299h10.256Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m10.026 4.913.975.976.976-.976M10.026 10.111l.975.976.976-.976\"\n        />\n      </svg>\n    );\n  }\n);\nAccordionIcon.displayName = \"AccordionIcon\";\n\nexport const AddTemplateInstanceIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.5 2H3.333A1.333 1.333 0 0 0 2 3.333V4.5M14 12.667c0 .021 0 .042-.002.063M11.5 14h1.167a1.333 1.333 0 0 0 1.331-1.27m0 0V11.5M2 11.5v1.167A1.333 1.333 0 0 0 3.333 14H4.5M7 14h2M2 7v2M8.461 4.77H14M11.23 2v5.538\"\n        />\n      </svg>\n    );\n  }\n);\nAddTemplateInstanceIcon.displayName = \"AddTemplateInstanceIcon\";\n\nexport const AiLoadingIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        id=\"c86546c68__evA4hI5dD261\"\n        shapeRendering=\"geometricPrecision\"\n        textRendering=\"geometricPrecision\"\n        viewBox=\"0 0 17 17\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <style>\n          {\n            \"@keyframes evA4hI5dD263_s_do{0%,to{stroke-dashoffset:6.2}17.5%,82.5%{stroke-dashoffset:-6.24}}@keyframes evA4hI5dD264_s_do{0%,10%,90%,to{stroke-dashoffset:6.8}22.5%,77.5%{stroke-dashoffset:-6.8}}@keyframes evA4hI5dD265_s_do{0%,17.5%,82.5%,to{stroke-dashoffset:2.32}27.5%,72.5%{stroke-dashoffset:-2.32}}@keyframes evA4hI5dD266_s_do{0%,22.5%,77.5%,to{stroke-dashoffset:2.47}32.5%,67.5%{stroke-dashoffset:-2.47}}@keyframes evA4hI5dD267_s_do{0%,27.5%,72.5%,to{stroke-dashoffset:5.42}40%,60%{stroke-dashoffset:-5.42}}@keyframes evA4hI5dD268_s_do{0%,32.5%,67.5%,to{stroke-dashoffset:6.13}50%{stroke-dashoffset:-6.1}}#c86546c68__evA4hI5dD263{animation:evA4hI5dD263_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD264{animation:evA4hI5dD264_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD265{animation:evA4hI5dD265_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD266{animation:evA4hI5dD266_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD267{animation:evA4hI5dD267_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD268{animation:evA4hI5dD268_s_do 4000ms linear infinite normal forwards}\"\n          }\n        </style>\n        <path\n          fill=\"none\"\n          stroke=\"rgba(255,255,255,0.4)\"\n          strokeDasharray=\"0\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"2.2\"\n          d=\"m2.816 12.956 1.513-6.051a2.563 2.563 0 0 1 4.974 0l1.159 4.634a1.888 1.888 0 0 0 1.816 1.417 1.892 1.892 0 0 0 1.872-1.872V4.956\"\n        />\n        <path\n          id=\"c86546c68__evA4hI5dD263\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeDasharray=\"6.24\"\n          strokeDashoffset=\"6.2\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.3\"\n          d=\"m2.816 12.956 1.513-6.051\"\n        />\n        <path\n          id=\"c86546c68__evA4hI5dD264\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeDasharray=\"6.8\"\n          strokeDashoffset=\"6.8\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.3\"\n          d=\"M4.329 6.905a2.563 2.563 0 0 1 4.974 0\"\n        />\n        <path\n          id=\"c86546c68__evA4hI5dD265\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeDasharray=\"2.32\"\n          strokeDashoffset=\"2.32\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.3\"\n          d=\"m9.303 6.905.561 2.243\"\n        />\n        <path\n          id=\"c86546c68__evA4hI5dD266\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeDasharray=\"2.47\"\n          strokeDashoffset=\"2.47\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.3\"\n          d=\"m9.864 9.148.598 2.391\"\n        />\n        <path\n          id=\"c86546c68__evA4hI5dD267\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeDasharray=\"5.42\"\n          strokeDashoffset=\"5.42\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.3\"\n          d=\"M10.462 11.54a1.888 1.888 0 0 0 1.816 1.416 1.892 1.892 0 0 0 1.872-1.872\"\n        />\n        <path\n          id=\"c86546c68__evA4hI5dD268\"\n          fill=\"none\"\n          stroke=\"#fff\"\n          strokeDasharray=\"6.13\"\n          strokeDashoffset=\"6.13\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"3.3\"\n          d=\"M14.15 11.084V4.956\"\n        />\n      </svg>\n    );\n  }\n);\nAiLoadingIcon.displayName = \"AiLoadingIcon\";\n\nexport const AiIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          d=\"m1.5 12.588 1.735-6.94a2.94 2.94 0 0 1 5.705 0l1.33 5.315a2.165 2.165 0 0 0 2.083 1.625 2.17 2.17 0 0 0 2.147-2.147v-7.03\"\n        />\n      </svg>\n    );\n  }\n);\nAiIcon.displayName = \"AiIcon\";\n\nexport const AlertCircleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 24 24\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"m11.28 1.023-.56.06A10.685 10.685 0 0 0 7.18 2.12c-1.135.551-1.987 1.165-2.942 2.119-.961.959-1.595 1.841-2.141 2.981C.301 10.968.728 15.286 3.226 18.633c.475.636 1.527 1.683 2.174 2.164 3.964 2.948 9.266 2.937 13.237-.027.609-.454 1.679-1.524 2.133-2.133 2.974-3.985 2.974-9.289 0-13.274-.454-.608-1.523-1.677-2.13-2.128-1.595-1.186-3.275-1.875-5.217-2.139C13 1.038 11.574.99 11.28 1.023m2.04 2.078c2.563.387 4.804 1.83 6.24 4.019.303.463.743 1.359.932 1.9.346.993.485 1.845.485 2.98 0 1.493-.257 2.621-.897 3.94-.705 1.454-1.769 2.667-3.153 3.592-.789.528-2.051 1.056-3.019 1.265a9.053 9.053 0 0 1-7.538-1.778c-1.513-1.212-2.648-2.99-3.103-4.859-.186-.763-.244-1.272-.244-2.16 0-1.493.257-2.621.897-3.94a8.983 8.983 0 0 1 5.24-4.594c.705-.233 1.272-.348 2.18-.442.322-.033 1.571.015 1.98.077m-1.625 3.956a1.04 1.04 0 0 0-.567.459l-.108.184v4.606l.121.197c.068.11.205.253.311.325.471.316 1.102.171 1.407-.325l.121-.197V7.7l-.108-.184a1.005 1.005 0 0 0-1.177-.459m0 7.998a1.05 1.05 0 0 0-.567.461c-.091.156-.108.23-.108.484 0 .257.016.327.113.492.518.882 1.865.526 1.865-.492a.994.994 0 0 0-.535-.888 1.17 1.17 0 0 0-.768-.057\"\n        />\n      </svg>\n    );\n  }\n);\nAlertCircleIcon.displayName = \"AlertCircleIcon\";\n\nexport const AlertIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.487 12 9.153 2.667a1.333 1.333 0 0 0-2.32 0L1.5 12a1.333 1.333 0 0 0 1.167 2h10.666a1.333 1.333 0 0 0 1.154-2ZM8 6v2.667M8 11.333h.007\"\n        />\n      </svg>\n    );\n  }\n);\nAlertIcon.displayName = \"AlertIcon\";\n\nexport const AlignBaselineIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5 8h13M6.7 8.06v5.14a1.3 1.3 0 0 1-1.3 1.3H4.1a1.3 1.3 0 0 1-1.3-1.3V8.205M6.7 5.4V2.8a1.3 1.3 0 0 0-1.3-1.3H4.1a1.3 1.3 0 0 0-1.3 1.3v2.6M13.2 8.108v3.142a1.3 1.3 0 0 1-1.3 1.3h-1.3a1.3 1.3 0 0 1-1.3-1.3V8.108M13.2 5.4V2.8a1.3 1.3 0 0 0-1.3-1.3h-1.3a1.3 1.3 0 0 0-1.3 1.3v2.6\"\n        />\n        <path\n          fill=\"currentColor\"\n          d=\"M2.737 14.024V8.231h3.89v5.793h-3.89ZM9.44 12.085V8h3.89v4.085H9.44Z\"\n        />\n      </svg>\n    );\n  }\n);\nAlignBaselineIcon.displayName = \"AlignBaselineIcon\";\n\nexport const AlignCenterHorizontalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5 8h13M6.7 10.6v2.6a1.3 1.3 0 0 1-1.3 1.3H4.1a1.3 1.3 0 0 1-1.3-1.3v-2.6M6.7 5.4V2.8a1.3 1.3 0 0 0-1.3-1.3H4.1a1.3 1.3 0 0 0-1.3 1.3v2.6M13.2 10.6v.65a1.3 1.3 0 0 1-1.3 1.3h-1.3a1.3 1.3 0 0 1-1.3-1.3v-.65M9.3 5.4v-.65c0-.715.585-1.3 1.3-1.3h1.3a1.3 1.3 0 0 1 1.3 1.3v.65\"\n        />\n      </svg>\n    );\n  }\n);\nAlignCenterHorizontalIcon.displayName = \"AlignCenterHorizontalIcon\";\n\nexport const AlignContentCenterIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.368 9.34h.716c.542 0 .982-.403.982-.9v-.88c0-.497-.44-.9-.982-.9h-.716c-.542 0-.98.403-.98.9v.88c0 .497.438.9.98.9ZM7.69 9.34h.716c.542 0 .981-.403.981-.9v-.88c0-.497-.44-.9-.981-.9H7.69c-.542 0-.982.403-.982.9v.88c0 .497.44.9.982.9ZM3.011 9.34h.716c.542 0 .982-.403.982-.9v-.88c0-.497-.44-.9-.982-.9h-.716c-.542 0-.981.403-.981.9v.88c0 .497.44.9.981.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.548 15.49h-13M14.5.493h-13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignContentCenterIcon.displayName = \"AlignContentCenterIcon\";\n\nexport const AlignContentEndIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.368 12.982h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9h-.716c-.542 0-.98.403-.98.9v.88c0 .496.438.9.98.9ZM7.69 12.982h.716c.542 0 .981-.403.981-.9v-.879c0-.497-.44-.9-.981-.9H7.69c-.542 0-.982.403-.982.9v.88c0 .496.44.9.982.9ZM3.011 12.982h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9h-.716c-.542 0-.981.403-.981.9v.88c0 .496.44.9.981.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.548 15.49h-13M14.5.493h-13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignContentEndIcon.displayName = \"AlignContentEndIcon\";\n\nexport const AlignContentSpaceAroundIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.867 4.409h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.88c0-.496-.44-.9-.982-.9ZM3.867 8.912h-.716c-.542 0-.981.403-.981.9v.88c0 .496.44.9.981.9h.716c.542 0 .982-.404.982-.9v-.88c0-.497-.44-.9-.982-.9ZM8.367 4.409h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.88c0-.496-.44-.9-.982-.9ZM12.867 4.409h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .981-.403.981-.9v-.88c0-.496-.439-.9-.98-.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.452.51h13M1.5 15.507h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignContentSpaceAroundIcon.displayName = \"AlignContentSpaceAroundIcon\";\n\nexport const AlignContentSpaceBetweenIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.867 3.055h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9ZM3.867 10.357h-.716c-.542 0-.981.403-.981.9v.88c0 .496.44.9.981.9h.716c.542 0 .982-.404.982-.9v-.88c0-.497-.44-.9-.982-.9ZM8.367 3.055h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9ZM12.867 3.055h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .981-.403.981-.9v-.879c0-.497-.439-.9-.98-.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5 1h13M1.5 15h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignContentSpaceBetweenIcon.displayName = \"AlignContentSpaceBetweenIcon\";\n\nexport const AlignContentStartIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.632 3.018h-.716c-.542 0-.982.403-.982.9v.879c0 .497.44.9.982.9h.716c.542 0 .98-.403.98-.9v-.88c0-.496-.438-.9-.98-.9ZM8.31 3.018h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.88c0-.496-.44-.9-.982-.9ZM12.989 3.018h-.716c-.542 0-.982.403-.982.9v.879c0 .497.44.9.982.9h.716c.542 0 .981-.403.981-.9v-.88c0-.496-.44-.9-.981-.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.452.51h13M1.5 15.507h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignContentStartIcon.displayName = \"AlignContentStartIcon\";\n\nexport const AlignContentStretchIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.867 3.987h-.716c-.542 0-.981.403-.981.9v6.365c0 .496.44.9.981.9h.716c.542 0 .982-.403.982-.9V4.886c0-.497-.44-.9-.982-.9ZM8.367 3.987h-.716c-.542 0-.981.403-.981.9v6.365c0 .496.44.9.981.9h.716c.542 0 .982-.403.982-.9V4.886c0-.497-.44-.9-.982-.9ZM12.867 3.987h-.716c-.542 0-.981.403-.981.9v6.365c0 .496.44.9.981.9h.716c.542 0 .981-.403.981-.9V4.886c0-.497-.439-.9-.98-.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5.52h13M1.5 15.475h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignContentStretchIcon.displayName = \"AlignContentStretchIcon\";\n\nexport const AlignEndHorizontalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.4 1.5H4.1a1.3 1.3 0 0 0-1.3 1.3v7.8a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3V2.8a1.3 1.3 0 0 0-1.3-1.3ZM11.9 6.05h-1.3a1.3 1.3 0 0 0-1.3 1.3v3.25a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3V7.35a1.3 1.3 0 0 0-1.3-1.3ZM14.5 14.5h-13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignEndHorizontalIcon.displayName = \"AlignEndHorizontalIcon\";\n\nexport const AlignHorizontalJustifyCenterIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.167 3.45H2.834c-.736 0-1.333.576-1.333 1.286v6.429c0 .71.597 1.285 1.333 1.285h1.333c.737 0 1.334-.575 1.334-1.285V4.736c0-.71-.597-1.286-1.334-1.286ZM13.166 4.75h-1.334c-.736 0-1.333.582-1.333 1.3v3.9c0 .718.597 1.3 1.333 1.3h1.334c.736 0 1.333-.582 1.333-1.3v-3.9c0-.718-.597-1.3-1.333-1.3ZM8 1.5v13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignHorizontalJustifyCenterIcon.displayName =\n  \"AlignHorizontalJustifyCenterIcon\";\n\nexport const AlignHorizontalJustifyEndIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.1 3.45H2.8a1.3 1.3 0 0 0-1.3 1.3v6.5a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3v-6.5a1.3 1.3 0 0 0-1.3-1.3ZM10.6 4.75H9.3A1.3 1.3 0 0 0 8 6.05v3.9a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3v-3.9a1.3 1.3 0 0 0-1.3-1.3ZM14.5 1.5v13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignHorizontalJustifyEndIcon.displayName = \"AlignHorizontalJustifyEndIcon\";\n\nexport const AlignHorizontalJustifyStartIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6.667 3.5H5.333C4.597 3.5 4 4.076 4 4.786v6.428c0 .71.597 1.286 1.333 1.286h1.334C7.403 12.5 8 11.924 8 11.214V4.786C8 4.076 7.403 3.5 6.667 3.5ZM13.333 4.833H12c-.736 0-1.333.627-1.333 1.4v4.2c0 .774.597 1.4 1.333 1.4h1.333c.737 0 1.334-.627 1.334-1.4v-4.2c0-.773-.597-1.4-1.334-1.4ZM1.333 1.5v13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignHorizontalJustifyStartIcon.displayName = \"AlignHorizontalJustifyStartIcon\";\n\nexport const AlignHorizontalSpaceAroundIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.667 4.667H7.333C6.597 4.667 6 5.264 6 6v4c0 .736.597 1.333 1.333 1.333h1.334C9.403 11.333 10 10.736 10 10V6c0-.736-.597-1.333-1.333-1.333ZM2.667 14.667V1.333M13.333 14.667V1.333\"\n        />\n      </svg>\n    );\n  }\n);\nAlignHorizontalSpaceAroundIcon.displayName = \"AlignHorizontalSpaceAroundIcon\";\n\nexport const AlignHorizontalSpaceBetweenIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.667 3.333H3.333C2.597 3.333 2 3.93 2 4.667v6.666c0 .737.597 1.334 1.333 1.334h1.334c.736 0 1.333-.597 1.333-1.334V4.667c0-.737-.597-1.334-1.333-1.334ZM12.667 4.667h-1.334C10.597 4.667 10 5.264 10 6v4c0 .736.597 1.333 1.333 1.333h1.334c.736 0 1.333-.597 1.333-1.333V6c0-.736-.597-1.333-1.333-1.333ZM2 1.333v13.334M14 1.333v13.334\"\n        />\n      </svg>\n    );\n  }\n);\nAlignHorizontalSpaceBetweenIcon.displayName = \"AlignHorizontalSpaceBetweenIcon\";\n\nexport const AlignSelfBaselineIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6.048 8.269v3.634c0 .719.583 1.301 1.301 1.301H8.65c.719 0 1.302-.582 1.302-1.3V8.268m0-2.22V4.096c0-.719-.583-1.301-1.302-1.301h-1.3c-.72 0-1.302.582-1.302 1.3v1.952M1.503 8h13\"\n        />\n        <path fill=\"currentColor\" d=\"M6.04 12.755V8.48h3.776v4.275H6.039Z\" />\n      </svg>\n    );\n  }\n);\nAlignSelfBaselineIcon.displayName = \"AlignSelfBaselineIcon\";\n\nexport const AlignSelfCenterIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6.048 6.048V4.097c0-.719.583-1.301 1.301-1.301H8.65c.719 0 1.302.582 1.302 1.3v1.952m0 3.904v1.951c0 .719-.583 1.301-1.302 1.301h-1.3a1.301 1.301 0 0 1-1.302-1.3V9.951M1.503 8h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignSelfCenterIcon.displayName = \"AlignSelfCenterIcon\";\n\nexport const AlignSelfEndIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.35 11.903h1.3c.719 0 1.302-.582 1.302-1.3V2.795c0-.719-.583-1.301-1.302-1.301h-1.3c-.72 0-1.302.582-1.302 1.3v7.807c0 .719.583 1.301 1.301 1.301ZM1.503 14.505h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignSelfEndIcon.displayName = \"AlignSelfEndIcon\";\n\nexport const AlignSelfStartIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.65 4.097h-1.3c-.719 0-1.302.582-1.302 1.3v7.807c0 .719.583 1.301 1.302 1.301h1.3c.72 0 1.302-.582 1.302-1.3V5.397c0-.719-.583-1.301-1.301-1.301ZM14.497 1.495h-13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignSelfStartIcon.displayName = \"AlignSelfStartIcon\";\n\nexport const AlignSelfStretchIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.651 3.447h-1.3a1.3 1.3 0 0 0-1.3 1.3v6.5a1.3 1.3 0 0 0 1.3 1.299h1.3a1.3 1.3 0 0 0 1.3-1.3v-6.5a1.3 1.3 0 0 0-1.3-1.3ZM14.499 1.491h-13M14.501 14.509h-13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignSelfStretchIcon.displayName = \"AlignSelfStretchIcon\";\n\nexport const AlignStartHorizontalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.4 4.097H4.1c-.72 0-1.302.582-1.302 1.3v7.807c0 .719.583 1.301 1.301 1.301H5.4c.719 0 1.301-.582 1.301-1.3V5.397c0-.719-.582-1.301-1.3-1.301ZM11.906 4.097h-1.301c-.719 0-1.301.582-1.301 1.3v3.254c0 .718.582 1.3 1.3 1.3h1.302c.718 0 1.3-.582 1.3-1.3V5.398c0-.719-.582-1.301-1.3-1.301ZM1.5 2h13\"\n        />\n      </svg>\n    );\n  }\n);\nAlignStartHorizontalIcon.displayName = \"AlignStartHorizontalIcon\";\n\nexport const AnimationGroupIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.5 8.3C6.3 7.7 7.8 8 9.3 11c.3-1.8 1.2-4.5 2.1-6M4.208 1.5H2.944A1.444 1.444 0 0 0 1.5 2.944v1.264m13 0V2.944A1.444 1.444 0 0 0 13.056 1.5h-1.264M14.5 13.056c0 .023 0 .046-.002.069M11.792 14.5h1.264a1.444 1.444 0 0 0 1.442-1.375m0 0v-1.333m-12.998 0v1.264A1.444 1.444 0 0 0 2.944 14.5h1.264m2.709-13h2.166m-2.166 13h2.166M1.5 6.917v2.166m13-2.166v2.166\"\n        />\n      </svg>\n    );\n  }\n);\nAnimationGroupIcon.displayName = \"AnimationGroupIcon\";\n\nexport const ArrowDownAZIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\".667\"\n          d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 13.333V2.667M13.333 5.333H10M10 6.667V4.333a1.667 1.667 0 1 1 3.333 0v2.334M10 9.333h3.333l-3.333 4h3.333\"\n        />\n      </svg>\n    );\n  }\n);\nArrowDownAZIcon.displayName = \"ArrowDownAZIcon\";\n\nexport const ArrowDownNarrowWideIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\".667\"\n          d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 13.333V2.667M7.333 2.667H10M7.333 5.333H12M7.333 8H14\"\n        />\n      </svg>\n    );\n  }\n);\nArrowDownNarrowWideIcon.displayName = \"ArrowDownNarrowWideIcon\";\n\nexport const ArrowDownWideNarrowIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\".667\"\n          d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 13.333V2.667M7.333 2.667H14M7.333 5.333H12M7.333 8H10\"\n        />\n      </svg>\n    );\n  }\n);\nArrowDownWideNarrowIcon.displayName = \"ArrowDownWideNarrowIcon\";\n\nexport const ArrowDownZAIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\".667\"\n          d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 2.667v10.666M10 2.667h3.333l-3.333 4h3.333M10 13.333V11a1.667 1.667 0 1 1 3.333 0v2.333M13.333 12H10\"\n        />\n      </svg>\n    );\n  }\n);\nArrowDownZAIcon.displayName = \"ArrowDownZAIcon\";\n\nexport const ArrowDownIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 3.333v9.334M12.667 8 8 12.667 3.333 8\"\n        />\n      </svg>\n    );\n  }\n);\nArrowDownIcon.displayName = \"ArrowDownIcon\";\n\nexport const ArrowLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 8H3.333M8 3.333 3.333 8 8 12.667\"\n        />\n      </svg>\n    );\n  }\n);\nArrowLeftIcon.displayName = \"ArrowLeftIcon\";\n\nexport const ArrowRightLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\".667\"\n          d=\"m11 2 2.667 2.667m0 0L11 7.333m2.667-2.666H3M5.667 14 3 11.333m0 0 2.667-2.666M3 11.333h10.667\"\n        />\n      </svg>\n    );\n  }\n);\nArrowRightLeftIcon.displayName = \"ArrowRightLeftIcon\";\n\nexport const ArrowRightIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.333 8h9.334M8 3.333 12.667 8 8 12.667\"\n        />\n      </svg>\n    );\n  }\n);\nArrowRightIcon.displayName = \"ArrowRightIcon\";\n\nexport const ArrowUpIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 12.667V3.333M3.333 8 8 3.333 12.667 8\"\n        />\n      </svg>\n    );\n  }\n);\nArrowUpIcon.displayName = \"ArrowUpIcon\";\n\nexport const AspectRatioIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5 4v7.667h7.667\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m3 5 2-2 2 2M11.667 9.667l2 2-2 2\"\n        />\n      </svg>\n    );\n  }\n);\nAspectRatioIcon.displayName = \"AspectRatioIcon\";\n\nexport const AsteriskIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 3.96v8.08M11.498 5.98l-6.996 4.04M4.502 5.98l6.996 4.04\"\n        />\n      </svg>\n    );\n  }\n);\nAsteriskIcon.displayName = \"AsteriskIcon\";\n\nexport const AttachmentIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.823 13.501 14 8.2M10.667 4l-5.61 5.724a1.333 1.333 0 0 0 1.886 1.885l5.61-5.724a2.667 2.667 0 0 0-3.772-3.77l-5.61 5.723a4 4 0 0 0 2.83 6.829 3.922 3.922 0 0 0 2.804-1.147\"\n        />\n      </svg>\n    );\n  }\n);\nAttachmentIcon.displayName = \"AttachmentIcon\";\n\nexport const AutoScrollIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m5.502 10.5 2.5-5 2.5 5M6.126 9.25h3.75M2 14V2M14.003 14V2\"\n        />\n      </svg>\n    );\n  }\n);\nAutoScrollIcon.displayName = \"AutoScrollIcon\";\n\nexport const BlockquoteIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          d=\"M6.667 8H3.333a.667.667 0 0 1-.666-.667V5a.667.667 0 0 1 .666-.667H6A.667.667 0 0 1 6.667 5v3Zm0 0C6.667 9.667 6 10.667 4 11.667M13.333 8H10a.667.667 0 0 1-.667-.667V5A.667.667 0 0 1 10 4.333h2.667a.667.667 0 0 1 .666.667v3Zm0 0c0 1.667-.666 2.667-2.666 3.667\"\n        />\n      </svg>\n    );\n  }\n);\nBlockquoteIcon.displayName = \"BlockquoteIcon\";\n\nexport const BodyIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM2 6h12\"\n        />\n      </svg>\n    );\n  }\n);\nBodyIcon.displayName = \"BodyIcon\";\n\nexport const BoldIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4 8h6a2.667 2.667 0 0 1 0 5.333H4.667A.667.667 0 0 1 4 12.667V3.333a.667.667 0 0 1 .667-.666h4.666a2.667 2.667 0 1 1 0 5.333\"\n        />\n      </svg>\n    );\n  }\n);\nBoldIcon.displayName = \"BoldIcon\";\n\nexport const BorderRadiusBottomLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3 3v6.364A3.636 3.636 0 0 0 6.636 13H13\"\n        />\n      </svg>\n    );\n  }\n);\nBorderRadiusBottomLeftIcon.displayName = \"BorderRadiusBottomLeftIcon\";\n\nexport const BorderRadiusBottomRightIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13 3v6.364A3.636 3.636 0 0 1 9.364 13H3\"\n        />\n      </svg>\n    );\n  }\n);\nBorderRadiusBottomRightIcon.displayName = \"BorderRadiusBottomRightIcon\";\n\nexport const BorderRadiusIndividualIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 4.667V3.333A1.333 1.333 0 0 1 3.333 2h1.334M11.333 2h1.334A1.333 1.333 0 0 1 14 3.333v1.334M14 11.333v1.334A1.333 1.333 0 0 1 12.667 14h-1.334M4.667 14H3.333A1.334 1.334 0 0 1 2 12.667v-1.334\"\n        />\n      </svg>\n    );\n  }\n);\nBorderRadiusIndividualIcon.displayName = \"BorderRadiusIndividualIcon\";\n\nexport const BorderRadiusTopLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13 3H6.636A3.636 3.636 0 0 0 3 6.636V13\"\n        />\n      </svg>\n    );\n  }\n);\nBorderRadiusTopLeftIcon.displayName = \"BorderRadiusTopLeftIcon\";\n\nexport const BorderRadiusTopRightIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3 3h6.364A3.636 3.636 0 0 1 13 6.636V13\"\n        />\n      </svg>\n    );\n  }\n);\nBorderRadiusTopRightIcon.displayName = \"BorderRadiusTopRightIcon\";\n\nexport const BorderRadiusIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"#11181C\"\n          fillRule=\"evenodd\"\n          d=\"M1.917 10.666a8.75 8.75 0 0 1 8.75-8.75h2.666a.75.75 0 0 1 0 1.5h-2.667a7.25 7.25 0 0 0-7.25 7.25v2.667a.75.75 0 0 1-1.5 0v-2.667Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nBorderRadiusIcon.displayName = \"BorderRadiusIcon\";\n\nexport const BorderWidthBottomIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 14H2m0-3.667v-7C2 2.597 2.597 2 3.333 2h9.334C13.403 2 14 2.597 14 3.333v7\"\n        />\n      </svg>\n    );\n  }\n);\nBorderWidthBottomIcon.displayName = \"BorderWidthBottomIcon\";\n\nexport const BorderWidthIndividualIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.4 1.744h5.2M14.5 10.356v-5.2M1.5 10.356v-5.2M5.4 14.256h5.2\"\n        />\n      </svg>\n    );\n  }\n);\nBorderWidthIndividualIcon.displayName = \"BorderWidthIndividualIcon\";\n\nexport const BorderWidthLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 14V2m3.667 0h7C13.403 2 14 2.597 14 3.333v9.334c0 .736-.597 1.333-1.333 1.333h-7\"\n        />\n      </svg>\n    );\n  }\n);\nBorderWidthLeftIcon.displayName = \"BorderWidthLeftIcon\";\n\nexport const BorderWidthRightIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 2v12m-3.667 0h-7A1.333 1.333 0 0 1 2 12.667V3.333C2 2.597 2.597 2 3.333 2h7\"\n        />\n      </svg>\n    );\n  }\n);\nBorderWidthRightIcon.displayName = \"BorderWidthRightIcon\";\n\nexport const BorderWidthTopIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 2h12m0 3.667v7c0 .736-.597 1.333-1.333 1.333H3.333A1.333 1.333 0 0 1 2 12.667v-7\"\n        />\n      </svg>\n    );\n  }\n);\nBorderWidthTopIcon.displayName = \"BorderWidthTopIcon\";\n\nexport const BoxIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"\n        />\n      </svg>\n    );\n  }\n);\nBoxIcon.displayName = \"BoxIcon\";\n\nexport const BracesIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.333 2h-.666a1.333 1.333 0 0 0-1.334 1.333v3.334A1.333 1.333 0 0 1 2 8a1.333 1.333 0 0 1 1.333 1.333v3.334c0 .733.6 1.333 1.334 1.333h.666M10.667 14h.666a1.333 1.333 0 0 0 1.334-1.333V9.333C12.667 8.6 13.267 8 14 8a1.333 1.333 0 0 1-1.333-1.333V3.333A1.333 1.333 0 0 0 11.333 2h-.666\"\n        />\n      </svg>\n    );\n  }\n);\nBracesIcon.displayName = \"BracesIcon\";\n\nexport const BrushCleaningIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.667 14.667 10 12m2.667-2.667a.667.667 0 0 0 .666-.666V8A1.333 1.333 0 0 0 12 6.667h-2A.667.667 0 0 1 9.333 6V2.667a1.333 1.333 0 1 0-2.666 0V6A.667.667 0 0 1 6 6.667H4A1.333 1.333 0 0 0 2.667 8v.667a.667.667 0 0 0 .666.666m9.334 0H3.333m9.334 0 1.315 4.512a.667.667 0 0 1-.649.822H2.667a.668.668 0 0 1-.649-.822l1.315-4.512m2 5.334L6 12\"\n        />\n      </svg>\n    );\n  }\n);\nBrushCleaningIcon.displayName = \"BrushCleaningIcon\";\n\nexport const BugIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m5.333 1.333 1.254 1.254M9.413 2.587l1.254-1.254M6 4.753v-.666a2.002 2.002 0 1 1 4 0v.666\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 13.333c-2.2 0-4-1.8-4-4v-2a2.667 2.667 0 0 1 2.667-2.666h2.666A2.667 2.667 0 0 1 12 7.333v2c0 2.2-1.8 4-4 4ZM8 13.333v-6\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.353 6C3.067 5.867 2 4.733 2 3.333M4 8.667H1.333M2 14c0-1.4 1.133-2.6 2.533-2.667M13.98 3.333c0 1.4-1.067 2.534-2.333 2.667M14.667 8.667H12M11.467 11.333C12.867 11.4 14 12.6 14 14\"\n        />\n      </svg>\n    );\n  }\n);\nBugIcon.displayName = \"BugIcon\";\n\nexport const ButtonElementIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M1.833 8a3.5 3.5 0 0 1 3.5-3.5h5.334a3.5 3.5 0 1 1 0 7H5.333a3.5 3.5 0 0 1-3.5-3.5Zm3.5-4.5a4.5 4.5 0 0 0 0 9h5.334a4.5 4.5 0 1 0 0-9H5.333ZM5.72 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm4.28-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nButtonElementIcon.displayName = \"ButtonElementIcon\";\n\nexport const CalendarIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.333 9.333H8V12M10.667 1.333V4M2 6.667h12M5.333 1.333V4\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2.667H3.333C2.597 2.667 2 3.264 2 4v9.333c0 .737.597 1.334 1.333 1.334h9.334c.736 0 1.333-.597 1.333-1.334V4c0-.736-.597-1.333-1.333-1.333Z\"\n        />\n      </svg>\n    );\n  }\n);\nCalendarIcon.displayName = \"CalendarIcon\";\n\nexport const CheckCircleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <rect width=\"12\" height=\"12\" x=\"2\" y=\"2\" stroke=\"currentColor\" rx=\"6\" />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.998 7.928 7.381 9.31 10 6.69\"\n        />\n      </svg>\n    );\n  }\n);\nCheckCircleIcon.displayName = \"CheckCircleIcon\";\n\nexport const CheckMarkSmallIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.998 7.928 7.381 9.31 10 6.69\"\n        />\n      </svg>\n    );\n  }\n);\nCheckMarkSmallIcon.displayName = \"CheckMarkSmallIcon\";\n\nexport const CheckMarkIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"1.091\"\n          d=\"m13.636 3.667-8 8L2 8.03\"\n        />\n      </svg>\n    );\n  }\n);\nCheckMarkIcon.displayName = \"CheckMarkIcon\";\n\nexport const CheckboxCheckedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m6 8.238 1.383 1.383L10.003 7\"\n        />\n      </svg>\n    );\n  }\n);\nCheckboxCheckedIcon.displayName = \"CheckboxCheckedIcon\";\n\nexport const ChevronDownIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m4 6 4 4 4-4\"\n        />\n      </svg>\n    );\n  }\n);\nChevronDownIcon.displayName = \"ChevronDownIcon\";\n\nexport const ChevronFilledUpIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path fill=\"currentColor\" d=\"M13 11 8 5l-5 6\" />\n      </svg>\n    );\n  }\n);\nChevronFilledUpIcon.displayName = \"ChevronFilledUpIcon\";\n\nexport const ChevronLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <g id=\"c1893fb9d__icon/chevron left\">\n          <path\n            id=\"c1893fb9d__vector\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth=\"1.3\"\n            d=\"m9.256 4.499-3.512 3.5 3.512 3.502\"\n          />\n        </g>\n      </svg>\n    );\n  }\n);\nChevronLeftIcon.displayName = \"ChevronLeftIcon\";\n\nexport const ChevronRightIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m6 12 4-4-4-4\"\n        />\n      </svg>\n    );\n  }\n);\nChevronRightIcon.displayName = \"ChevronRightIcon\";\n\nexport const ChevronUpIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12 10 8 6l-4 4\"\n        />\n      </svg>\n    );\n  }\n);\nChevronUpIcon.displayName = \"ChevronUpIcon\";\n\nexport const ChevronsLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.069 12.656 2.413 8l4.656-4.656M13.587 12.656 8.931 8l4.656-4.656\"\n        />\n      </svg>\n    );\n  }\n);\nChevronsLeftIcon.displayName = \"ChevronsLeftIcon\";\n\nexport const CircleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M8 3.65a4.35 4.35 0 1 0 0 8.7 4.35 4.35 0 0 0 0-8.7ZM2.35 8a5.65 5.65 0 1 1 11.3 0 5.65 5.65 0 0 1-11.3 0Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nCircleIcon.displayName = \"CircleIcon\";\n\nexport const CloudIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M11.667 12.667H6a4.667 4.667 0 1 1 4.473-6h1.194a3 3 0 1 1 0 6Z\"\n        />\n      </svg>\n    );\n  }\n);\nCloudIcon.displayName = \"CloudIcon\";\n\nexport const CollapsibleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 14.667v-4M8 5.333v-4M2.5 8h-1M6.5 8h-1M10.5 8h-1M14.5 8h-1M10 12.667l-2-2-2 2M10 3.333l-2 2-2-2\"\n        />\n      </svg>\n    );\n  }\n);\nCollapsibleIcon.displayName = \"CollapsibleIcon\";\n\nexport const CommitIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM2 8h4M10 8h4\"\n        />\n      </svg>\n    );\n  }\n);\nCommitIcon.displayName = \"CommitIcon\";\n\nexport const ContentBlockIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 2H3.333A1.333 1.333 0 0 0 2 3.333v9.334A1.333 1.333 0 0 0 3.333 14h9.334A1.334 1.334 0 0 0 14 12.667V8\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.25 1.75a1.414 1.414 0 1 1 2 2L8.24 9.76a1.333 1.333 0 0 1-.568.336l-1.916.56a.334.334 0 0 1-.413-.413l.56-1.916c.063-.214.179-.41.337-.568L12.25 1.75Z\"\n        />\n      </svg>\n    );\n  }\n);\nContentBlockIcon.displayName = \"ContentBlockIcon\";\n\nexport const ContentEmbedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.667 5.333h4M4.667 8H10M14 8.75V3.5A1.5 1.5 0 0 0 12.5 2h-9A1.5 1.5 0 0 0 2 3.5v9.252c0 .69.56 1.25 1.25 1.25v0H6M13.686 14.265l1.4-1.4-1.4-1.4\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m9.486 11.465-1.4 1.4 1.4 1.4M12.306 10.612l-1.441 4.321\"\n        />\n      </svg>\n    );\n  }\n);\nContentEmbedIcon.displayName = \"ContentEmbedIcon\";\n\nexport const ContentIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.667 5.333h4M4.666 8H10m-5.333 2.667h4M2 3.5A1.5 1.5 0 0 1 3.5 2h9A1.5 1.5 0 0 1 14 3.5v9.188c0 .724-.588 1.312-1.313 1.312H3.313A1.312 1.312 0 0 1 2 12.687V3.5Z\"\n        />\n      </svg>\n    );\n  }\n);\nContentIcon.displayName = \"ContentIcon\";\n\nexport const CopyIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.214 5.5H6.786c-.71 0-1.286.576-1.286 1.286v6.428c0 .71.576 1.286 1.286 1.286h6.428c.71 0 1.286-.576 1.286-1.286V6.786c0-.71-.576-1.286-1.286-1.286Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.786 10.5A1.29 1.29 0 0 1 1.5 9.214V2.786A1.29 1.29 0 0 1 2.786 1.5h6.428A1.29 1.29 0 0 1 10.5 2.786\"\n        />\n      </svg>\n    );\n  }\n);\nCopyIcon.displayName = \"CopyIcon\";\n\nexport const CropIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.167 1.5v9.333A1.333 1.333 0 0 0 5.5 12.167h9.333m-2.666 2.666V5.5a1.333 1.333 0 0 0-1.334-1.333H1.5\"\n        />\n      </svg>\n    );\n  }\n);\nCropIcon.displayName = \"CropIcon\";\n\nexport const DashedBorderIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.714 8H2M8.857 8H7.143M14 8h-1.714\"\n        />\n      </svg>\n    );\n  }\n);\nDashedBorderIcon.displayName = \"DashedBorderIcon\";\n\nexport const DescriptionIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.333 3.135a.47.47 0 0 0-.802-.332L4.275 5.058a.933.933 0 0 1-.664.275H2A.667.667 0 0 0 1.333 6v4a.667.667 0 0 0 .667.667h1.61a.934.934 0 0 1 .665.275l2.256 2.256a.47.47 0 0 0 .802-.333v-9.73ZM10.667 6a3.333 3.333 0 0 1 0 4M12.91 12.243a6 6 0 0 0 0-8.486\"\n        />\n      </svg>\n    );\n  }\n);\nDescriptionIcon.displayName = \"DescriptionIcon\";\n\nexport const DialogIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"#D2D2D2\"\n          d=\"M13.444 1H2.556C1.696 1 1 1.696 1 2.556v10.888C1 14.304 1.696 15 2.556 15h10.888c.86 0 1.556-.696 1.556-1.556V2.556C15 1.696 14.304 1 13.444 1Z\"\n        />\n        <path\n          fill=\"#fff\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M11.334 5.667H4.667v4.666h6.667V5.667Z\"\n        />\n      </svg>\n    );\n  }\n);\nDialogIcon.displayName = \"DialogIcon\";\n\nexport const DimensionsIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.833 6.833v-4h4\"\n        />\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M3.687 2.98a.5.5 0 0 0-.707.707l8.48 8.48H8.666a.5.5 0 1 0 0 1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 1 0-1 0v2.793l-8.48-8.48Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nDimensionsIcon.displayName = \"DimensionsIcon\";\n\nexport const DiscordIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          d=\"M4.565 7.429c0 .151.066.296.183.404A.655.655 0 0 0 5.189 8c.166 0 .325-.06.442-.167a.548.548 0 0 0 .183-.404.548.548 0 0 0-.183-.404.655.655 0 0 0-.442-.168.655.655 0 0 0-.441.168.548.548 0 0 0-.183.404Zm5.621 0c0 .151.066.296.183.404a.655.655 0 0 0 .442.167c.165 0 .324-.06.441-.167a.548.548 0 0 0 .183-.404.548.548 0 0 0-.183-.404.655.655 0 0 0-.441-.168.655.655 0 0 0-.442.168.548.548 0 0 0-.183.404Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M.5 11.273c0-2.744 1.072-6.37 2.142-7.841 0 0 1.072-.49 5.358-.49 4.285 0 5.356.491 5.356.491 1.072 1.47 2.144 5.096 2.144 7.84-.357.492-1.608 1.57-3.75 1.961L9.864 11.08a8.973 8.973 0 0 1-3.729 0L4.25 13.234c-2.142-.392-3.393-1.47-3.75-1.96Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.25 10.294c.326.298 1.012.597 1.885.785a8.976 8.976 0 0 0 3.73 0c.873-.188 1.558-.487 1.885-.785\"\n        />\n      </svg>\n    );\n  }\n);\nDiscordIcon.displayName = \"DiscordIcon\";\n\nexport const DotIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z\"\n        />\n      </svg>\n    );\n  }\n);\nDotIcon.displayName = \"DotIcon\";\n\nexport const DottedBorderIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.464 8a.667.667 0 1 0 1.333 0 .667.667 0 0 0-1.333 0ZM9.202 8a.667.667 0 1 0 1.334 0 .667.667 0 0 0-1.334 0ZM12.94 8a.667.667 0 1 0 1.334 0 .667.667 0 0 0-1.333 0ZM1.726 8a.667.667 0 1 0 1.333 0 .667.667 0 0 0-1.333 0Z\"\n        />\n      </svg>\n    );\n  }\n);\nDottedBorderIcon.displayName = \"DottedBorderIcon\";\n\nexport const DownloadIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth=\"2\"\n        viewBox=\"0 0 24 24\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path d=\"M12 15V3M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n        <path d=\"m7 10 5 5 5-5\" />\n      </svg>\n    );\n  }\n);\nDownloadIcon.displayName = \"DownloadIcon\";\n\nexport const DragHandleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6 8.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334ZM6 4a.667.667 0 1 0 0-1.333A.667.667 0 0 0 6 4ZM6 13.333A.667.667 0 1 0 6 12a.667.667 0 0 0 0 1.333ZM10 8.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334ZM10 4a.667.667 0 1 0 0-1.333A.667.667 0 0 0 10 4ZM10 13.333A.667.667 0 1 0 10 12a.667.667 0 0 0 0 1.333Z\"\n        />\n      </svg>\n    );\n  }\n);\nDragHandleIcon.displayName = \"DragHandleIcon\";\n\nexport const DynamicPageIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 12.203H3.855a1.474 1.474 0 0 1-1.473-1.473V2.81a1.474 1.474 0 0 1 1.473-1.474h6.079l3.684 3.683v5.71a1.474 1.474 0 0 1-1.474 1.474H8Zm0 0 .01 2.462m0 0H2.39m5.618 0h5.618\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.198 1.888V3.73a1.474 1.474 0 0 0 1.473 1.474h2.947\"\n        />\n      </svg>\n    );\n  }\n);\nDynamicPageIcon.displayName = \"DynamicPageIcon\";\n\nexport const EllipseIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M3.373 5.736C2.203 6.405 1.65 7.229 1.65 8c0 .77.553 1.595 1.723 2.264C4.525 10.922 6.159 11.35 8 11.35c1.84 0 3.475-.428 4.627-1.086C13.797 9.595 14.35 8.771 14.35 8c0-.77-.553-1.595-1.723-2.264C11.475 5.078 9.841 4.65 8 4.65c-1.84 0-3.475.428-4.627 1.086Zm-.645-1.129C4.109 3.817 5.975 3.35 8 3.35c2.025 0 3.89.468 5.272 1.257 1.364.78 2.378 1.955 2.378 3.393 0 1.438-1.014 2.614-2.378 3.393-1.382.79-3.247 1.257-5.272 1.257-2.025 0-3.89-.468-5.272-1.257C1.364 10.613.35 9.438.35 8c0-1.438 1.014-2.614 2.378-3.393Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nEllipseIcon.displayName = \"EllipseIcon\";\n\nexport const EllipsesIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.112 8a.888.888 0 1 0 1.777 0 .888.888 0 0 0-1.777 0ZM12.724 8a.888.888 0 1 0 1.777 0 .888.888 0 0 0-1.777 0ZM1.499 8a.888.888 0 1 0 1.777 0 .888.888 0 0 0-1.777 0Z\"\n        />\n      </svg>\n    );\n  }\n);\nEllipsesIcon.displayName = \"EllipsesIcon\";\n\nexport const EmailIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.333 2.667H2.667c-.737 0-1.334.597-1.334 1.333v8c0 .736.597 1.333 1.334 1.333h10.666c.737 0 1.334-.597 1.334-1.333V4c0-.736-.597-1.333-1.334-1.333Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m14.667 4.667-5.98 3.8a1.293 1.293 0 0 1-1.374 0l-5.98-3.8\"\n        />\n      </svg>\n    );\n  }\n);\nEmailIcon.displayName = \"EmailIcon\";\n\nexport const EmbedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12 10.667 14.667 8 12 5.333M4 5.333 1.333 8 4 10.667M9.667 2.667 6.333 13.333\"\n        />\n      </svg>\n    );\n  }\n);\nEmbedIcon.displayName = \"EmbedIcon\";\n\nexport const ExtensionIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.5 8.5h6m-6 0h-6m6 0V14M13.5 8.5v4.667c0 .736-.597 1.333-1.333 1.333H2.833A1.333 1.333 0 0 1 1.5 13.167V3.833c0-.736.597-1.333 1.333-1.333H7.5v6\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinejoin=\"round\"\n          d=\"M9.5.5v6h6V2.207A1.707 1.707 0 0 0 13.793.5H9.5Z\"\n        />\n      </svg>\n    );\n  }\n);\nExtensionIcon.displayName = \"ExtensionIcon\";\n\nexport const ExternalLinkIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 2h4v4M6.667 9.333 14 2M12 8.667v4A1.334 1.334 0 0 1 10.667 14H3.333A1.334 1.334 0 0 1 2 12.667V5.333A1.333 1.333 0 0 1 3.333 4h4\"\n        />\n      </svg>\n    );\n  }\n);\nExternalLinkIcon.displayName = \"ExternalLinkIcon\";\n\nexport const EyeClosedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m10 12-.481-2.167M1.333 5.333a7.097 7.097 0 0 0 13.334 0M13.333 10l-1.15-1.367M2.667 10l1.15-1.367M6 12l.481-2.167\"\n        />\n      </svg>\n    );\n  }\n);\nEyeClosedIcon.displayName = \"EyeClosedIcon\";\n\nexport const EyeOpenIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.375 8.232a.667.667 0 0 1 0-.464 7.167 7.167 0 0 1 13.25 0 .666.666 0 0 1 0 .464 7.166 7.166 0 0 1-13.25 0Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z\"\n          className=\"ws-eye-open-pupil\"\n        />\n      </svg>\n    );\n  }\n);\nEyeOpenIcon.displayName = \"EyeOpenIcon\";\n\nexport const EyedropperIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m1.502 14.498.65-.65h1.947L9.94 8.008\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.151 13.849V11.9L7.993 6.06M9.94 4.113l2.207-2.207a1.377 1.377 0 0 1 1.947 1.947L11.887 6.06l.26.26A1.376 1.376 0 1 1 10.2 8.266L7.733 5.8a1.377 1.377 0 1 1 1.948-1.947l.26.26Z\"\n        />\n      </svg>\n    );\n  }\n);\nEyedropperIcon.displayName = \"EyedropperIcon\";\n\nexport const FolderIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.333 13.333A1.333 1.333 0 0 0 14.667 12V5.333A1.334 1.334 0 0 0 13.333 4H8.067a1.333 1.333 0 0 1-1.127-.6l-.54-.8A1.333 1.333 0 0 0 5.287 2h-2.62a1.333 1.333 0 0 0-1.334 1.333V12a1.333 1.333 0 0 0 1.334 1.333h10.666Z\"\n        />\n      </svg>\n    );\n  }\n);\nFolderIcon.displayName = \"FolderIcon\";\n\nexport const FooterIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM4 12h8\"\n        />\n      </svg>\n    );\n  }\n);\nFooterIcon.displayName = \"FooterIcon\";\n\nexport const FormTextAreaIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.522C14 2.682 13.318 2 12.478 2h-9C2.662 2 2 2.662 2 3.478v9.189C2 13.403 2.597 14 3.333 14ZM3.83 6.636V3.943\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m10.496 13.76 1.724-1.725 1.717-1.717\"\n        />\n      </svg>\n    );\n  }\n);\nFormTextAreaIcon.displayName = \"FormTextAreaIcon\";\n\nexport const FormTextFieldIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.333 2.667H4a2 2 0 0 1 2 2 2 2 0 0 1 2-2h.667M8.667 13.333H8a2 2 0 0 1-2-2 2 2 0 0 1-2 2h-.667M3.333 10.667h-.666a1.333 1.333 0 0 1-1.334-1.334V6.667a1.333 1.333 0 0 1 1.334-1.334h.666M8.667 5.333h4.666a1.333 1.333 0 0 1 1.334 1.334v2.666a1.333 1.333 0 0 1-1.334 1.334H8.667M6 4.667v6.666\"\n        />\n      </svg>\n    );\n  }\n);\nFormTextFieldIcon.displayName = \"FormTextFieldIcon\";\n\nexport const FormIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.184 5.5h.731a.75.75 0 0 0 .75-.75v-1.5a.75.75 0 0 0-.75-.75H2.085a.75.75 0 0 0-.75.75v1.5c0 .414.336.75.75.75h11.099ZM13.184 10.5h.732a.75.75 0 0 0 .75-.75v-1.5a.75.75 0 0 0-.75-.75H2.084a.75.75 0 0 0-.75.75v1.5c0 .414.336.75.75.75h11.099Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          d=\"M6.613 14.5h.222a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-5.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5H6.613Z\"\n        />\n      </svg>\n    );\n  }\n);\nFormIcon.displayName = \"FormIcon\";\n\nexport const GapHorizontalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2h-2v12h2M3.333 14h2V2h-2\"\n        />\n      </svg>\n    );\n  }\n);\nGapHorizontalIcon.displayName = \"GapHorizontalIcon\";\n\nexport const GapVerticalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 12.667v-2H2v2M2 3.333v2h12v-2\"\n        />\n      </svg>\n    );\n  }\n);\nGapVerticalIcon.displayName = \"GapVerticalIcon\";\n\nexport const GearIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.147 1.333h-.294A1.333 1.333 0 0 0 6.52 2.667v.12a1.333 1.333 0 0 1-.667 1.153l-.286.167a1.333 1.333 0 0 1-1.334 0l-.1-.054a1.333 1.333 0 0 0-1.82.487l-.146.253a1.333 1.333 0 0 0 .486 1.82l.1.067a1.333 1.333 0 0 1 .667 1.147v.34a1.333 1.333 0 0 1-.667 1.16l-.1.06a1.333 1.333 0 0 0-.486 1.82l.146.253a1.334 1.334 0 0 0 1.82.487l.1-.054a1.334 1.334 0 0 1 1.334 0l.286.167a1.333 1.333 0 0 1 .667 1.153v.12a1.333 1.333 0 0 0 1.333 1.334h.294a1.333 1.333 0 0 0 1.333-1.334v-.12a1.334 1.334 0 0 1 .667-1.153l.286-.167a1.334 1.334 0 0 1 1.334 0l.1.054a1.333 1.333 0 0 0 1.82-.487l.146-.26a1.334 1.334 0 0 0-.486-1.82l-.1-.053a1.333 1.333 0 0 1-.667-1.16v-.334a1.333 1.333 0 0 1 .667-1.16l.1-.06a1.334 1.334 0 0 0 .486-1.82l-.146-.253a1.333 1.333 0 0 0-1.82-.487l-.1.054a1.333 1.333 0 0 1-1.334 0l-.286-.167a1.333 1.333 0 0 1-.667-1.153v-.12a1.333 1.333 0 0 0-1.333-1.334Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z\"\n        />\n      </svg>\n    );\n  }\n);\nGearIcon.displayName = \"GearIcon\";\n\nexport const GithubIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 22 22\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M11 1C5.475 1 1 5.475 1 11a9.994 9.994 0 0 0 6.838 9.488c.5.087.687-.213.687-.476 0-.237-.013-1.024-.013-1.862-2.512.463-3.162-.612-3.362-1.175-.113-.288-.6-1.175-1.025-1.413-.35-.187-.85-.65-.013-.662.788-.013 1.35.725 1.538 1.025.9 1.512 2.337 1.087 2.912.825.088-.65.35-1.088.638-1.338-2.225-.25-4.55-1.112-4.55-4.937 0-1.088.387-1.987 1.025-2.688-.1-.25-.45-1.274.1-2.65 0 0 .837-.262 2.75 1.026a9.28 9.28 0 0 1 2.5-.338c.85 0 1.7.112 2.5.337 1.912-1.3 2.75-1.024 2.75-1.024.55 1.375.2 2.4.1 2.65.637.7 1.025 1.587 1.025 2.687 0 3.838-2.337 4.688-4.562 4.938.362.312.675.912.675 1.85 0 1.337-.013 2.412-.013 2.75 0 .262.188.574.688.474A10.016 10.016 0 0 0 21 11c0-5.525-4.475-10-10-10Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nGithubIcon.displayName = \"GithubIcon\";\n\nexport const GoogleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          d=\"M14.72 8.16c0-.497-.045-.975-.127-1.433H8v2.711h3.767a3.228 3.228 0 0 1-1.406 2.107v1.762h2.272c1.323-1.222 2.087-3.016 2.087-5.148Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          d=\"M8 15c1.89 0 3.475-.624 4.633-1.693l-2.272-1.762c-.624.42-1.42.674-2.361.674-1.82 0-3.366-1.228-3.92-2.883H1.75v1.808A6.993 6.993 0 0 0 8 15Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          d=\"M4.08 9.33A4.195 4.195 0 0 1 3.857 8c0-.465.083-.91.223-1.33V4.863H1.75A6.914 6.914 0 0 0 1 8c0 1.133.274 2.195.75 3.137l1.815-1.412.515-.395Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          d=\"M8 3.787c1.03 0 1.947.357 2.679 1.044l2.005-2.005C11.468 1.694 9.89 1 8 1a6.988 6.988 0 0 0-6.25 3.863L4.08 6.67C4.634 5.015 6.18 3.787 8 3.787Z\"\n        />\n      </svg>\n    );\n  }\n);\nGoogleIcon.displayName = \"GoogleIcon\";\n\nexport const GradientConicIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <g\n          fill=\"#11181C\"\n          clipPath=\"url(#c15388f97__paint0_angular_9413_9319_clip_path)\"\n        >\n          <path d=\"M8 8V0a8 8 0 0 1 4 1.072Z\" />\n          <path fillOpacity=\".909\" d=\"m8 8 4-6.928A8 8 0 0 1 14.928 4Z\" />\n          <path fillOpacity=\".818\" d=\"m8 8 6.928-4A8 8 0 0 1 16 8Z\" />\n          <path fillOpacity=\".727\" d=\"M8 8h8a8 8 0 0 1-1.072 4Z\" />\n          <path fillOpacity=\".636\" d=\"m8 8 6.928 4A8 8 0 0 1 12 14.928Z\" />\n          <path fillOpacity=\".545\" d=\"m8 8 4 6.928A8 8 0 0 1 8 16Z\" />\n          <path fillOpacity=\".455\" d=\"M8 8v8a8 8 0 0 1-4-1.072Z\" />\n          <path fillOpacity=\".364\" d=\"m8 8-4 6.928A8 8 0 0 1 1.072 12Z\" />\n          <path fillOpacity=\".273\" d=\"m8 8-6.928 4A8 8 0 0 1 0 8Z\" />\n          <path fillOpacity=\".182\" d=\"M8 8H0a8 8 0 0 1 1.072-4Z\" />\n          <path fillOpacity=\".091\" d=\"M8 8 1.072 4A8 8 0 0 1 4 1.072Z\" />\n          <path fill=\"none\" d=\"M8 8 4 1.072A8 8 0 0 1 8 0Z\" />\n        </g>\n        <defs>\n          <clipPath id=\"c15388f97__paint0_angular_9413_9319_clip_path\">\n            <path d=\"M2 5.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C3.52 2 4.08 2 5.2 2h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C14 3.52 14 4.08 14 5.2v5.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C12.48 14 11.92 14 10.8 14H5.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C2 12.48 2 11.92 2 10.8V5.2Z\" />\n          </clipPath>\n        </defs>\n      </svg>\n    );\n  }\n);\nGradientConicIcon.displayName = \"GradientConicIcon\";\n\nexport const GradientLinearIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"url(#c1fa533d5__gradient-linear_svg__a)\"\n          d=\"M2 5.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C3.52 2 4.08 2 5.2 2h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C14 3.52 14 4.08 14 5.2v5.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C12.48 14 11.92 14 10.8 14H5.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C2 12.48 2 11.92 2 10.8V5.2Z\"\n        />\n        <defs>\n          <linearGradient\n            id=\"c1fa533d5__gradient-linear_svg__a\"\n            x1=\"8\"\n            x2=\"8\"\n            y1=\"2\"\n            y2=\"14\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\"0\" stopColor=\"#11181C\" />\n            <stop offset=\"1\" stopColor=\"#11181C\" stopOpacity=\"0\" />\n          </linearGradient>\n        </defs>\n      </svg>\n    );\n  }\n);\nGradientLinearIcon.displayName = \"GradientLinearIcon\";\n\nexport const GradientRadialIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"url(#c539a7588__paint0_radial_9420_9425)\"\n          d=\"M2 5.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C3.52 2 4.08 2 5.2 2h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C14 3.52 14 4.08 14 5.2v5.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C12.48 14 11.92 14 10.8 14H5.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C2 12.48 2 11.92 2 10.8V5.2Z\"\n        />\n        <defs>\n          <radialGradient\n            id=\"c539a7588__paint0_radial_9420_9425\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(8 8) rotate(135) scale(8.48528)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor=\"#11181C\" />\n            <stop offset=\"1\" stopColor=\"#11181C\" stopOpacity=\"0\" />\n          </radialGradient>\n        </defs>\n      </svg>\n    );\n  }\n);\nGradientRadialIcon.displayName = \"GradientRadialIcon\";\n\nexport const GrowIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4 8h4M6 10 4 8l2-2M12 8H8M10 6l2 2-2 2M2 14V2M14.003 14V2\"\n        />\n      </svg>\n    );\n  }\n);\nGrowIcon.displayName = \"GrowIcon\";\n\nexport const HeaderIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM4 4h8\"\n        />\n      </svg>\n    );\n  }\n);\nHeaderIcon.displayName = \"HeaderIcon\";\n\nexport const HeadingIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4 8h8M4 13.333V2.667M12 13.333V2.667\"\n        />\n      </svg>\n    );\n  }\n);\nHeadingIcon.displayName = \"HeadingIcon\";\n\nexport const HelpIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6.057 6c.161-.446.48-.821.898-1.06a2.11 2.11 0 0 1 1.391-.248c.48.08.914.322 1.227.684.313.361.484.831.484 1.29 0 1.334-2.059 2-2.059 2\"\n        />\n        <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 11.333h0\"\n        />\n      </svg>\n    );\n  }\n);\nHelpIcon.displayName = \"HelpIcon\";\n\nexport const HomeIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 14V8.667A.667.667 0 0 0 9.333 8H6.667A.667.667 0 0 0 6 8.667V14\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 6.667a1.333 1.333 0 0 1 .473-1.019l4.666-4a1.333 1.333 0 0 1 1.722 0l4.666 4A1.332 1.332 0 0 1 14 6.667v6A1.334 1.334 0 0 1 12.667 14H3.333A1.334 1.334 0 0 1 2 12.667v-6Z\"\n        />\n      </svg>\n    );\n  }\n);\nHomeIcon.displayName = \"HomeIcon\";\n\nexport const ImageIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6 7.333a1.333 1.333 0 1 0 0-2.666 1.333 1.333 0 0 0 0 2.666ZM14 10l-2.057-2.057a1.333 1.333 0 0 0-1.886 0L4 14\"\n        />\n      </svg>\n    );\n  }\n);\nImageIcon.displayName = \"ImageIcon\";\n\nexport const InfoCircleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 11.516V7.853\"\n        />\n        <rect\n          width=\"1.4\"\n          height=\"1.4\"\n          x=\"7.3\"\n          y=\"4.484\"\n          fill=\"currentColor\"\n          rx=\".7\"\n        />\n      </svg>\n    );\n  }\n);\nInfoCircleIcon.displayName = \"InfoCircleIcon\";\n\nexport const ItemIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 6h.007M5.333 10H14M5.333 6H14\"\n        />\n      </svg>\n    );\n  }\n);\nItemIcon.displayName = \"ItemIcon\";\n\nexport const JCSpaceAroundIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M1.7 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\"\n          clipRule=\"evenodd\"\n        />\n        <path d=\"M4.5 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H5.3a.8.8 0 0 1-.8-.8V4.8ZM8.5 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H9.3a.8.8 0 0 1-.8-.8V4.8Z\" />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M14.3 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nJCSpaceAroundIcon.displayName = \"JCSpaceAroundIcon\";\n\nexport const JCSpaceBetweenIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M2.2 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\"\n          clipRule=\"evenodd\"\n        />\n        <path d=\"M4 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H4.8a.8.8 0 0 1-.8-.8V4.8ZM9 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H9.8a.8.8 0 0 1-.8-.8V4.8Z\" />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M13.8 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nJCSpaceBetweenIcon.displayName = \"JCSpaceBetweenIcon\";\n\nexport const LabelIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6.476 11.074V4.926m0 6.148h3.77v-.615m-3.77.615h-.722m.722-6.148h.632m-.632 0h-.722\"\n        />\n      </svg>\n    );\n  }\n);\nLabelIcon.displayName = \"LabelIcon\";\n\nexport const LargeXIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"\n        />\n      </svg>\n    );\n  }\n);\nLargeXIcon.displayName = \"LargeXIcon\";\n\nexport const LifeBuoyIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <circle cx=\"7.995\" cy=\"7.995\" r=\"6.665\" stroke=\"#000\" />\n        <path d=\"m3.287 3.287 2.826 2.826M9.886 6.113l2.827-2.826M9.886 9.887l2.827 2.826M6.113 9.887l-2.827 2.826\" />\n        <circle cx=\"7.995\" cy=\"7.995\" r=\"2.665\" stroke=\"#000\" />\n      </svg>\n    );\n  }\n);\nLifeBuoyIcon.displayName = \"LifeBuoyIcon\";\n\nexport const Link2UnlinkedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 4.667h1.333a3.333 3.333 0 0 1 0 6.666H10m-4 0H4.667a3.333 3.333 0 0 1 0-6.666H6\"\n        />\n      </svg>\n    );\n  }\n);\nLink2UnlinkedIcon.displayName = \"Link2UnlinkedIcon\";\n\nexport const Link2Icon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6 11.333H4.667a3.333 3.333 0 0 1 0-6.666H6M10 4.667h1.333a3.334 3.334 0 0 1 0 6.666H10M5.333 8h5.334\"\n        />\n      </svg>\n    );\n  }\n);\nLink2Icon.displayName = \"Link2Icon\";\n\nexport const LinkIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.943 11.771 8 12.714A3.333 3.333 0 0 1 3.286 8l.943-.943M7.057 4.229 8 3.286A3.333 3.333 0 1 1 12.714 8l-.943.943M6.114 9.886l3.772-3.772\"\n        />\n      </svg>\n    );\n  }\n);\nLinkIcon.displayName = \"LinkIcon\";\n\nexport const ListItemIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path d=\"M3.7 6.175a.85.85 0 1 1-1.7 0 .85.85 0 0 1 1.7 0Z\" />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M5 6.175c0-.345.28-.625.625-.625h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 6.175ZM5 10.05c0-.345.28-.625.625-.625h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 10.05Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nListItemIcon.displayName = \"ListItemIcon\";\n\nexport const ListViewIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M3.333 2.5a.833.833 0 0 0-.833.833v9.334c0 .46.373.833.833.833h9.334c.46 0 .833-.373.833-.833V3.333a.833.833 0 0 0-.833-.833H3.333ZM1.5 3.333c0-1.012.82-1.833 1.833-1.833h9.334c1.012 0 1.833.82 1.833 1.833v9.334c0 1.012-.82 1.833-1.833 1.833H3.333A1.833 1.833 0 0 1 1.5 12.667V3.333Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 6H2M14 10H2\"\n        />\n      </svg>\n    );\n  }\n);\nListViewIcon.displayName = \"ListViewIcon\";\n\nexport const ListIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path d=\"M3.7 4.35a.85.85 0 1 1-1.7 0 .85.85 0 0 1 1.7 0Z\" />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M5 4.35c0-.346.28-.626.625-.626h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 4.35Z\"\n          clipRule=\"evenodd\"\n        />\n        <path d=\"M3.7 8A.85.85 0 1 1 2 8a.85.85 0 0 1 1.7 0Z\" />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M5 8c0-.345.28-.625.625-.625h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 8Z\"\n          clipRule=\"evenodd\"\n        />\n        <path d=\"M3.7 11.65a.85.85 0 1 1-1.7 0 .85.85 0 0 1 1.7 0Z\" />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M5 11.65c0-.346.28-.626.625-.626h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 11.65Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nListIcon.displayName = \"ListIcon\";\n\nexport const LoadingDotsIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        id=\"cfdbcf7a5__eEMFTOz1Zbw1\"\n        shapeRendering=\"geometricPrecision\"\n        textRendering=\"geometricPrecision\"\n        viewBox=\"0 0 300 300\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <style>\n          {\n            \"@keyframes eEMFTOz1Zbw12_to__to{0%,26.666667%,56.666667%,6.333333%,77%,to{transform:translate(150px,180px)}16.666667%,67%{transform:translate(150px,120px);animation-timing-function:cubic-bezier(.42,0,.58,1)}}@keyframes eEMFTOz1Zbw13_to__to{0%,20%,50%,70%,to{transform:translate(80px,180px)}10%,60%{transform:translate(80px,120px);animation-timing-function:cubic-bezier(.42,0,.58,1)}}@keyframes eEMFTOz1Zbw14_to__to{0%,13.333333%,33.333333%,63.333333%,83.333333%,to{transform:translate(220px,180px)}23.333333%,73.333333%{transform:translate(220px,120px);animation-timing-function:cubic-bezier(.42,0,.58,1)}}#cfdbcf7a5__eEMFTOz1Zbw12_to{animation:eEMFTOz1Zbw12_to__to 2000ms linear infinite normal forwards}#cfdbcf7a5__eEMFTOz1Zbw13_to{animation:eEMFTOz1Zbw13_to__to 2000ms linear infinite normal forwards}#cfdbcf7a5__eEMFTOz1Zbw14_to{animation:eEMFTOz1Zbw14_to__to 2000ms linear infinite normal forwards}\"\n          }\n        </style>\n        <circle\n          id=\"cfdbcf7a5__eEMFTOz1Zbw12_to\"\n          r=\"20\"\n          strokeWidth=\"0\"\n          transform=\"translate(150,180) translate(0,0)\"\n        />\n        <circle\n          id=\"cfdbcf7a5__eEMFTOz1Zbw13_to\"\n          r=\"20\"\n          strokeWidth=\"0\"\n          transform=\"translate(80,180) translate(0,0)\"\n        />\n        <circle\n          id=\"cfdbcf7a5__eEMFTOz1Zbw14_to\"\n          r=\"20\"\n          strokeWidth=\"0\"\n          transform=\"translate(220,180) translate(0,0)\"\n        />\n      </svg>\n    );\n  }\n);\nLoadingDotsIcon.displayName = \"LoadingDotsIcon\";\n\nexport const MarkdownEmbedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.667 5.333h4M4.667 8H10M14 8.75V3.5A1.5 1.5 0 0 0 12.5 2h-9A1.5 1.5 0 0 0 2 3.5v9.191c0 .725.588 1.313 1.313 1.313h4.691\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 14.314v-4l2 2.03 2-2.03v4\"\n        />\n      </svg>\n    );\n  }\n);\nMarkdownEmbedIcon.displayName = \"MarkdownEmbedIcon\";\n\nexport const MaximizeIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 2h4v4M6 14H2v-4M14 2 9.334 6.667M2 14l4.667-4.667\"\n        />\n      </svg>\n    );\n  }\n);\nMaximizeIcon.displayName = \"MaximizeIcon\";\n\nexport const MenuIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.667 8h10.666M2.667 4h10.666M2.667 12h10.666\"\n        />\n      </svg>\n    );\n  }\n);\nMenuIcon.displayName = \"MenuIcon\";\n\nexport const MicOffIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m1.333 1.333 13 13M12.593 8.82c.048-.27.073-.545.074-.82V6.667M3.333 6.667V8a4.667 4.667 0 0 0 8 3.333M10 6.227V3.333a2 2 0 0 0-3.787-.886\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6 6v2a2 2 0 0 0 3.413 1.413M8 12.667v2\"\n        />\n      </svg>\n    );\n  }\n);\nMicOffIcon.displayName = \"MicOffIcon\";\n\nexport const MicIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 1.333a2 2 0 0 0-2 2V8a2 2 0 1 0 4 0V3.333a2 2 0 0 0-2-2Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 6.667V8a4.666 4.666 0 1 1-9.334 0V6.667M8 12.667v2\"\n        />\n      </svg>\n    );\n  }\n);\nMicIcon.displayName = \"MicIcon\";\n\nexport const MinimizeIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.667 9.333h4v4M13.333 6.667h-4v-4M9.333 6.667 14 2M2 14l4.667-4.667\"\n        />\n      </svg>\n    );\n  }\n);\nMinimizeIcon.displayName = \"MinimizeIcon\";\n\nexport const MinusIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.333 8h9.334\"\n        />\n      </svg>\n    );\n  }\n);\nMinusIcon.displayName = \"MinusIcon\";\n\nexport const NavigationMenuIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 7v5.833c0 .645-.597 1.167-1.333 1.167H3.333C2.597 14 2 13.478 2 12.833V7\"\n        />\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.978 2.003H2.6a.6.6 0 0 0-.6.6v1.4h11.971v-1.4a.6.6 0 0 0-.6-.6h-2.393Z\"\n        />\n      </svg>\n    );\n  }\n);\nNavigationMenuIcon.displayName = \"NavigationMenuIcon\";\n\nexport const NavigatorIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.5 8H15M1 4.5h10.5M4.5 11.5H15\"\n        />\n      </svg>\n    );\n  }\n);\nNavigatorIcon.displayName = \"NavigatorIcon\";\n\nexport const NewFolderIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 6.667v4M6 8.667h4M13.333 13.333A1.333 1.333 0 0 0 14.667 12V5.333A1.334 1.334 0 0 0 13.333 4H8.067a1.333 1.333 0 0 1-1.127-.6l-.54-.8A1.333 1.333 0 0 0 5.287 2h-2.62a1.333 1.333 0 0 0-1.334 1.333V12a1.333 1.333 0 0 0 1.334 1.333h10.666Z\"\n        />\n      </svg>\n    );\n  }\n);\nNewFolderIcon.displayName = \"NewFolderIcon\";\n\nexport const NewPageIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 1.333H4a1.333 1.333 0 0 0-1.333 1.334v10.666A1.333 1.333 0 0 0 4 14.667h8a1.333 1.333 0 0 0 1.333-1.334V4.667L10 1.333Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.333 1.333V4a1.333 1.333 0 0 0 1.334 1.333h2.666M6 10h4M8 12V8\"\n        />\n      </svg>\n    );\n  }\n);\nNewPageIcon.displayName = \"NewPageIcon\";\n\nexport const NoWrapIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9ZM8.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9ZM13.434 3.667h-.98c-.543 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.541 0 .98-.403.98-.9v-7.2c0-.497-.439-.9-.98-.9Z\"\n        />\n      </svg>\n    );\n  }\n);\nNoWrapIcon.displayName = \"NoWrapIcon\";\n\nexport const NotebookAndPenIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.933 1.5H4a1.35 1.35 0 0 0-.943.38c-.25.245-.39.575-.39.92v10.4c0 .345.14.676.39.92.25.243.59.38.943.38h8c.354 0 .693-.137.943-.38.25-.244.39-.575.39-.92V8.39M1.333 4.167H4M1.333 6.833H4M1.333 9.5H4M1.333 12.167H4\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.252 3.917a1.416 1.416 0 1 0-2.003-2.002L8.91 5.256a1.333 1.333 0 0 0-.337.57l-.558 1.913a.333.333 0 0 0 .413.413l1.914-.558c.215-.063.41-.179.569-.337l3.342-3.34Z\"\n        />\n      </svg>\n    );\n  }\n);\nNotebookAndPenIcon.displayName = \"NotebookAndPenIcon\";\n\nexport const OfflineIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.524 3.947C1.83 4.792 1.428 6.728 1.352 7.59A4.667 4.667 0 0 0 6 12.667h6.23m1.558-.879a3 3 0 0 0-2.121-5.121h-1.194a4.667 4.667 0 0 0-2.421-2.859c-.78-.381-1.673-.403-2.54-.403v0\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          d=\"M1.632 1.997 13.583 13.95\"\n        />\n      </svg>\n    );\n  }\n);\nOfflineIcon.displayName = \"OfflineIcon\";\n\nexport const OrderFirstIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.547 3.667h-.981c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.982.9h.98c.543 0 .982-.403.982-.9v-7.2c0-.497-.44-.9-.981-.9Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9ZM13.434 3.667h-.98c-.543 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.541 0 .98-.403.98-.9v-7.2c0-.497-.439-.9-.98-.9Z\"\n        />\n      </svg>\n    );\n  }\n);\nOrderFirstIcon.displayName = \"OrderFirstIcon\";\n\nexport const OrderLastIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.547 3.667h-.981c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.982.9h.98c.543 0 .982-.403.982-.9v-7.2c0-.497-.44-.9-.981-.9ZM8.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.434 3.667h-.98c-.543 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.541 0 .98-.403.98-.9v-7.2c0-.497-.439-.9-.98-.9Z\"\n        />\n      </svg>\n    );\n  }\n);\nOrderLastIcon.displayName = \"OrderLastIcon\";\n\nexport const OverlayIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"#D2D2D2\"\n          d=\"M13.444 1H2.556C1.696 1 1 1.696 1 2.556v10.888C1 14.304 1.696 15 2.556 15h10.888c.86 0 1.556-.696 1.556-1.556V2.556C15 1.696 14.304 1 13.444 1Z\"\n        />\n      </svg>\n    );\n  }\n);\nOverlayIcon.displayName = \"OverlayIcon\";\n\nexport const PageIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.833 2h-5.5A1.333 1.333 0 0 0 3 3.333v10a1.333 1.333 0 0 0 1.333 1.334h7.5a1.333 1.333 0 0 0 1.334-1.334v-8L9.833 2Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.167 2.5v1.667A1.333 1.333 0 0 0 10.5 5.5h2.667\"\n        />\n      </svg>\n    );\n  }\n);\nPageIcon.displayName = \"PageIcon\";\n\nexport const PaintBrushIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m6.04 7.933 5.38-5.373a1.9 1.9 0 1 1 2.687 2.687l-5.374 5.386M4.713 9.96c-1.106 0-2 .9-2 2.013 0 .887-1.666 1.014-1.333 1.347.72.733 1.66 1.347 2.667 1.347 1.466 0 2.666-1.2 2.666-2.694a2.007 2.007 0 0 0-2-2.013Z\"\n        />\n      </svg>\n    );\n  }\n);\nPaintBrushIcon.displayName = \"PaintBrushIcon\";\n\nexport const PhoneIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.5 11.24v1.96a1.308 1.308 0 0 1-1.425 1.308A12.934 12.934 0 0 1 7.434 12.5a12.745 12.745 0 0 1-3.922-3.922 12.935 12.935 0 0 1-2.007-5.667 1.307 1.307 0 0 1 1.301-1.425h1.96a1.307 1.307 0 0 1 1.308 1.124c.083.628.236 1.244.458 1.837a1.307 1.307 0 0 1-.294 1.38l-.83.83a10.458 10.458 0 0 0 3.921 3.921l.83-.83a1.307 1.307 0 0 1 1.38-.294c.593.221 1.209.375 1.836.458a1.307 1.307 0 0 1 1.125 1.326Z\"\n        />\n      </svg>\n    );\n  }\n);\nPhoneIcon.displayName = \"PhoneIcon\";\n\nexport const PlayIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m4 2 9.333 6L4 14V2Z\"\n        />\n      </svg>\n    );\n  }\n);\nPlayIcon.displayName = \"PlayIcon\";\n\nexport const PluginIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M4.687 2.667a2.317 2.317 0 0 1 4.633 0v.35h.039c.447 0 .815 0 1.116.02.312.021.6.067.876.181a2.65 2.65 0 0 1 1.434 1.435c.114.276.16.563.181.875.01.147.015.31.018.489h.353a2.317 2.317 0 0 1 0 4.633h-.35v.51c0 .537 0 .98-.03 1.34-.03.374-.096.716-.26 1.036a2.65 2.65 0 0 1-1.157 1.159c-.321.163-.663.228-1.037.259-.36.03-.802.03-1.34.03H8.67a.65.65 0 0 1-.65-.65v-1.167a.85.85 0 1 0-1.7 0v1.166a.65.65 0 0 1-.65.65h-.827c-.537 0-.98 0-1.34-.03-.373-.03-.715-.095-1.036-.258a2.65 2.65 0 0 1-1.158-1.159c-.164-.32-.23-.662-.26-1.036-.03-.36-.03-.803-.03-1.34V10a.65.65 0 0 1 .65-.65h1a1.017 1.017 0 0 0 0-2.033h-1a.65.65 0 0 1-.65-.65v-.023c0-.447 0-.815.021-1.116.022-.312.067-.6.182-.875a2.65 2.65 0 0 1 1.434-1.435c.276-.114.563-.16.875-.18.302-.021.67-.021 1.117-.021h.039v-.35ZM7.003 1.65c-.561 0-1.016.455-1.016 1.017v1a.65.65 0 0 1-.65.65H4.67c-.475 0-.799 0-1.05.017-.246.017-.375.048-.467.085-.33.137-.593.4-.73.731-.038.091-.069.22-.086.467a7.71 7.71 0 0 0-.014.4h.347a2.317 2.317 0 0 1 0 4.633h-.35v.483c0 .571 0 .96.025 1.261.024.293.068.445.122.552.13.254.336.46.59.59.107.055.259.098.552.122.301.025.69.025 1.26.025h.15v-.516a2.15 2.15 0 0 1 4.3 0v.516c.474 0 .81-.003 1.078-.025.293-.024.445-.067.553-.122.254-.13.46-.336.59-.59.054-.107.098-.259.122-.552.024-.3.025-.69.025-1.26V10a.65.65 0 0 1 .65-.65h1a1.017 1.017 0 0 0 0-2.033h-1a.65.65 0 0 1-.65-.65c0-.475 0-.799-.018-1.05-.017-.246-.047-.376-.085-.467a1.35 1.35 0 0 0-.73-.73c-.092-.038-.222-.069-.467-.086a17.153 17.153 0 0 0-1.05-.017H8.67a.65.65 0 0 1-.65-.65v-1c0-.562-.455-1.017-1.017-1.017Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nPluginIcon.displayName = \"PluginIcon\";\n\nexport const PlusIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 8h12M8 2v12\"\n        />\n      </svg>\n    );\n  }\n);\nPlusIcon.displayName = \"PlusIcon\";\n\nexport const PopoverIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 6V4a1.334 1.334 0 0 0-1.333-1.333h-10A1.333 1.333 0 0 0 1.333 4v6.667c0 .733.6 1.333 1.334 1.333h2.666\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.333 8.667h-4C8.597 8.667 8 9.264 8 10v2c0 .736.597 1.333 1.333 1.333h4c.737 0 1.334-.597 1.334-1.333v-2c0-.736-.597-1.333-1.334-1.333Z\"\n        />\n      </svg>\n    );\n  }\n);\nPopoverIcon.displayName = \"PopoverIcon\";\n\nexport const RadioCheckedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z\"\n        />\n      </svg>\n    );\n  }\n);\nRadioCheckedIcon.displayName = \"RadioCheckedIcon\";\n\nexport const RadioGroupIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"#000\"\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.839 8a1.82 1.82 0 1 1-3.64 0 1.82 1.82 0 0 1 3.64 0Z\"\n        />\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.019 13.027a5.027 5.027 0 1 0 0-10.054 5.027 5.027 0 0 0 0 10.054Z\"\n        />\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          d=\"M12.629 12.077v0a6.73 6.73 0 0 0-.337-8.565v0\"\n        />\n      </svg>\n    );\n  }\n);\nRadioGroupIcon.displayName = \"RadioGroupIcon\";\n\nexport const RadioUncheckedIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"\n        />\n      </svg>\n    );\n  }\n);\nRadioUncheckedIcon.displayName = \"RadioUncheckedIcon\";\n\nexport const RangeContain50Icon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M11 8.5v-1c0-.552-.597-1-1.333-1H6.333C5.597 6.5 5 6.948 5 7.5v1c0 .552.597 1 1.333 1h3.334c.736 0 1.333-.448 1.333-1ZM1.452 1.5h13M1.5 14.507h13\"\n        />\n      </svg>\n    );\n  }\n);\nRangeContain50Icon.displayName = \"RangeContain50Icon\";\n\nexport const RangeContainIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5 14.507h13M11 11.5v-1c0-.552-.597-1-1.333-1H6.333C5.597 9.5 5 9.948 5 10.5v1c0 .552.597 1 1.333 1h3.334c.736 0 1.333-.448 1.333-1ZM1.452 1.5h13\"\n        />\n      </svg>\n    );\n  }\n);\nRangeContainIcon.displayName = \"RangeContainIcon\";\n\nexport const RangeCoverIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M11 13.5v-1c0-.552-.597-1-1.333-1H6.333c-.736 0-1.333.448-1.333 1v1c0 .552.597 1 1.333 1h3.334c.736 0 1.333-.448 1.333-1ZM1.5 9.5h13M1.452 1.5h13\"\n        />\n      </svg>\n    );\n  }\n);\nRangeCoverIcon.displayName = \"RangeCoverIcon\";\n\nexport const RefreshIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M8 1.5h-.002A7 7 0 0 0 3.16 3.467l-.006.006-.653.653V2a.5.5 0 0 0-1 0v3.333a.498.498 0 0 0 .144.352l.005.004A.498.498 0 0 0 2 5.833h3.357a.5.5 0 1 0 0-1h-2.15l.65-.65A6 6 0 0 1 8.001 2.5 5.5 5.5 0 0 1 13.5 8a.5.5 0 0 0 1 0A6.5 6.5 0 0 0 8 1.5Zm-6 6a.5.5 0 0 1 .5.5A5.5 5.5 0 0 0 8 13.5a6 6 0 0 0 4.143-1.683l.759-.76h-2.235a.5.5 0 1 1 0-1H14a.5.5 0 0 1 .5.5v3.334a.5.5 0 0 1-1 0v-2.017l-.653.653-.006.006A7 7 0 0 1 8 14.5H8A6.5 6.5 0 0 1 1.5 8a.5.5 0 0 1 .5-.5Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nRefreshIcon.displayName = \"RefreshIcon\";\n\nexport const RepeatColumnIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.25 12.417V3.083c0-.736-.597-1.333-1.333-1.333H7.083c-.736 0-1.333.597-1.333 1.333v9.334c0 .736.597 1.333 1.333 1.333h1.834c.736 0 1.333-.597 1.333-1.333ZM10.25 5.75h-4M10.25 9.75h-4\"\n        />\n      </svg>\n    );\n  }\n);\nRepeatColumnIcon.displayName = \"RepeatColumnIcon\";\n\nexport const RepeatGridIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM2 6h12M2 10h12M6 2v12M10 2v12\"\n        />\n      </svg>\n    );\n  }\n);\nRepeatGridIcon.displayName = \"RepeatGridIcon\";\n\nexport const RepeatRowIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 5.5H3.333C2.597 5.5 2 6.097 2 6.833v1.834C2 9.403 2.597 10 3.333 10h9.334C13.403 10 14 9.403 14 8.667V6.833c0-.736-.597-1.333-1.333-1.333ZM6 5.5v4M10 5.5v4\"\n        />\n      </svg>\n    );\n  }\n);\nRepeatRowIcon.displayName = \"RepeatRowIcon\";\n\nexport const ResetIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M6.322 1.78a.783.783 0 0 1-.068 1.104L4.85 4.124h4.453A4.696 4.696 0 0 1 14 8.819c0 2.686-2.2 4.35-4.696 4.35h-1.63a.783.783 0 0 1 0-1.564h1.63c1.828 0 3.13-1.15 3.13-2.786a3.13 3.13 0 0 0-3.13-3.13H4.851l1.403 1.24A.783.783 0 1 1 5.217 8.1L2.264 5.493a.783.783 0 0 1 0-1.173L5.217 1.71a.783.783 0 0 1 1.105.068Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nResetIcon.displayName = \"ResetIcon\";\n\nexport const ResourceIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.667 5.333h4M4.667 8H10M14 8.75V3.5A1.5 1.5 0 0 0 12.5 2h-9A1.5 1.5 0 0 0 2 3.5v9.252c0 .69.56 1.25 1.25 1.25v0H6\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.45 14.5h-.7a1.75 1.75 0 1 1 0-3.5h.7M12.55 11h.7a1.75 1.75 0 1 1 0 3.5h-.7M10.1 12.75h2.8\"\n        />\n      </svg>\n    );\n  }\n);\nResourceIcon.displayName = \"ResourceIcon\";\n\nexport const SearchIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667ZM14 14l-2.867-2.867\"\n        />\n      </svg>\n    );\n  }\n);\nSearchIcon.displayName = \"SearchIcon\";\n\nexport const SectionLinkIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.667 14.667H12a1.333 1.333 0 0 0 1.333-1.334V4.667L10 1.333H4a1.333 1.333 0 0 0-1.333 1.334v2.666\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.333 1.333V4a1.333 1.333 0 0 0 1.334 1.333h2.666M2 10h4\"\n        />\n      </svg>\n    );\n  }\n);\nSectionLinkIcon.displayName = \"SectionLinkIcon\";\n\nexport const SelectIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.184 10.33h.149c.736 0 1.332-.597 1.332-1.332V6.332c0-.735-.596-1.332-1.332-1.332H2.667c-.735 0-1.332.597-1.332 1.332v2.666c0 .735.597 1.332 1.333 1.332h10.516Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m10.078 7.165 1 1 1-1\"\n        />\n      </svg>\n    );\n  }\n);\nSelectIcon.displayName = \"SelectIcon\";\n\nexport const SettingsIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M13.987 6H9.32M13.932 6h-12M14 10.5H2\"\n        />\n        <rect\n          width=\"4\"\n          height=\"4\"\n          x=\"1.932\"\n          y=\"8.534\"\n          fill=\"#fff\"\n          stroke=\"currentColor\"\n          rx=\"2\"\n        />\n        <rect\n          width=\"4\"\n          height=\"4\"\n          x=\"10.068\"\n          y=\"4\"\n          fill=\"#fff\"\n          stroke=\"currentColor\"\n          rx=\"2\"\n        />\n      </svg>\n    );\n  }\n);\nSettingsIcon.displayName = \"SettingsIcon\";\n\nexport const ShadowInsetIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <rect\n          width=\"13\"\n          height=\"13\"\n          x=\"1.5\"\n          y=\"1.5\"\n          stroke=\"currentColor\"\n          rx=\"6.5\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.804 7.833a4.196 4.196 0 1 1 8.392 0\"\n        />\n      </svg>\n    );\n  }\n);\nShadowInsetIcon.displayName = \"ShadowInsetIcon\";\n\nexport const ShadowNormalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 12.327a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M15 10v0c-2.774 5.944-11.226 5.944-14 0v0\"\n        />\n      </svg>\n    );\n  }\n);\nShadowNormalIcon.displayName = \"ShadowNormalIcon\";\n\nexport const ShieldIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        viewBox=\"0 0 24 24\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\" />\n      </svg>\n    );\n  }\n);\nShieldIcon.displayName = \"ShieldIcon\";\n\nexport const ShrinkIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M8 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 8 1ZM1.333 7.5a.5.5 0 0 0 0 1h2.793L2.98 9.646a.5.5 0 0 0 .707.708l2-2a.499.499 0 0 0 .146-.351v-.006a.498.498 0 0 0-.146-.35l-2-2a.5.5 0 1 0-.707.707L4.126 7.5H1.333Zm8.834.503a.496.496 0 0 0 .146.35l2 2a.5.5 0 0 0 .707-.707L11.874 8.5h2.793a.5.5 0 1 0 0-1h-2.793l1.146-1.146a.5.5 0 1 0-.707-.708l-2 2a.498.498 0 0 0-.146.351v.006ZM8.5 5.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1ZM8 9a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 8 9Zm.5 4.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nShrinkIcon.displayName = \"ShrinkIcon\";\n\nexport const SliderIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.015 8.265h-6M8.015 8.265h-6\"\n        />\n        <rect\n          width=\"3\"\n          height=\"3\"\n          x=\"4.026\"\n          y=\"6.5\"\n          fill=\"#fff\"\n          stroke=\"currentColor\"\n          rx=\"1.5\"\n        />\n      </svg>\n    );\n  }\n);\nSliderIcon.displayName = \"SliderIcon\";\n\nexport const SlotComponentIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.5 2H3.333A1.333 1.333 0 0 0 2 3.333V4.5M14 4.5V3.333A1.333 1.333 0 0 0 12.667 2H11.5M14 12.667c0 .021 0 .042-.002.063M11.5 14h1.167a1.333 1.333 0 0 0 1.331-1.27m0 0V11.5M2 11.5v1.167A1.333 1.333 0 0 0 3.333 14H4.5M7 2h2M7 14h2M2 7v2M14 7v2\"\n        />\n      </svg>\n    );\n  }\n);\nSlotComponentIcon.displayName = \"SlotComponentIcon\";\n\nexport const SpinnerIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        id=\"c765c5cf3__e2CRglijn891\"\n        shapeRendering=\"geometricPrecision\"\n        textRendering=\"geometricPrecision\"\n        viewBox=\"0 0 128 128\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <style>\n          {\n            \"@keyframes e2CRglijn892_tr__tr{0%{transform:translate(64px,64px) rotate(90deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}50%{transform:translate(64px,64px) rotate(810deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}to{transform:translate(64px,64px) rotate(1530deg)}}@keyframes e2CRglijn892_s_p{0%,to{stroke:#39fbbb}25%{stroke:#4a4efa}50%{stroke:#e63cfe}75%{stroke:#ffae3c}}@keyframes e2CRglijn892_s_do{0%{stroke-dashoffset:251.89}2.5%,52.5%{stroke-dashoffset:263.88;animation-timing-function:cubic-bezier(.42,0,.58,1)}25%,75%{stroke-dashoffset:131.945}to{stroke-dashoffset:251.885909}}#c765c5cf3__e2CRglijn892_tr{animation:e2CRglijn892_tr__tr 3000ms linear infinite normal forwards}#c765c5cf3__e2CRglijn892{animation-name:e2CRglijn892_s_p,e2CRglijn892_s_do;animation-duration:3000ms;animation-fill-mode:forwards;animation-timing-function:linear;animation-direction:normal;animation-iteration-count:infinite}\"\n          }\n        </style>\n        <g\n          id=\"c765c5cf3__e2CRglijn892_tr\"\n          transform=\"translate(64,64) rotate(90)\"\n        >\n          <circle\n            id=\"c765c5cf3__e2CRglijn892\"\n            r=\"42\"\n            fill=\"none\"\n            stroke=\"#39fbbb\"\n            strokeDasharray=\"263.89\"\n            strokeDashoffset=\"251.89\"\n            strokeLinecap=\"round\"\n            strokeWidth=\"16\"\n            transform=\"scale(-1,1) translate(0,0)\"\n          />\n        </g>\n      </svg>\n    );\n  }\n);\nSpinnerIcon.displayName = \"SpinnerIcon\";\n\nexport const StaggerAnimationIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.222 5H8.778A.778.778 0 0 0 8 5.778v5.444c0 .43.348.778.778.778h5.444c.43 0 .778-.348.778-.778V5.778A.778.778 0 0 0 14.222 5ZM5.444 6H1.556A.556.556 0 0 0 1 6.556v3.888c0 .307.249.556.556.556h3.888A.556.556 0 0 0 6 10.444V6.556A.556.556 0 0 0 5.444 6Z\"\n        />\n      </svg>\n    );\n  }\n);\nStaggerAnimationIcon.displayName = \"StaggerAnimationIcon\";\n\nexport const StopIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <rect width=\"9\" height=\"9\" x=\"3.5\" y=\"3.5\" fill=\"currentColor\" rx=\"1\" />\n      </svg>\n    );\n  }\n);\nStopIcon.displayName = \"StopIcon\";\n\nexport const StretchVerticalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.333 1.333H4c-.736 0-1.333.597-1.333 1.334v10.666c0 .737.597 1.334 1.333 1.334h1.333c.737 0 1.334-.597 1.334-1.334V2.667c0-.737-.597-1.334-1.334-1.334ZM12 1.333h-1.333c-.737 0-1.334.597-1.334 1.334v10.666c0 .737.597 1.334 1.334 1.334H12c.736 0 1.333-.597 1.333-1.334V2.667c0-.737-.597-1.334-1.333-1.334Z\"\n        />\n      </svg>\n    );\n  }\n);\nStretchVerticalIcon.displayName = \"StretchVerticalIcon\";\n\nexport const SubscriptIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.667 3.333 8 8.667M8 3.333 2.667 8.667M13.333 12.667h-2.666c0-1 .293-1.334 1-1.667.706-.333 1.666-.78 1.666-1.667 0-.313-.113-.62-.32-.86a1.407 1.407 0 0 0-1.746-.293c-.28.16-.494.413-.6.713\"\n        />\n      </svg>\n    );\n  }\n);\nSubscriptIcon.displayName = \"SubscriptIcon\";\n\nexport const SuperscriptIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.667 12.667 8 7.333M8 12.667 2.667 7.333M13.333 8h-2.666c0-1 .294-1.333 1-1.667.705-.333 1.666-.777 1.666-1.665 0-.315-.113-.62-.322-.86a1.403 1.403 0 0 0-1.745-.29c-.28.159-.492.409-.6.706\"\n        />\n      </svg>\n    );\n  }\n);\nSuperscriptIcon.displayName = \"SuperscriptIcon\";\n\nexport const SwitchIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.667 4H5.333a4 4 0 1 0 0 8h5.334a4 4 0 0 0 0-8Z\"\n        />\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.333 9.333a1.333 1.333 0 1 0 0-2.666 1.333 1.333 0 0 0 0 2.666Z\"\n        />\n      </svg>\n    );\n  }\n);\nSwitchIcon.displayName = \"SwitchIcon\";\n\nexport const TabsIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.75 5H13M9.75 5H7.401A1.684 1.684 0 0 1 6 4.25v0l-.5-.75-.5-.75L4.5 2m5.25 3-1-1.5-.5-.75-.5-.75M4.5 2H3v0a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 3 14h10v0a1.5 1.5 0 0 0 1.5-1.5v0-6A1.5 1.5 0 0 0 13 5v0M4.5 2h3.25m0 0h2.375v0c.547 0 1.057.273 1.36.728l.015.022.5.75L13 5\"\n        />\n      </svg>\n    );\n  }\n);\nTabsIcon.displayName = \"TabsIcon\";\n\nexport const TerminalIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4.667 7.333 6 6 4.667 4.667M7.333 8.667H10\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"\n        />\n      </svg>\n    );\n  }\n);\nTerminalIcon.displayName = \"TerminalIcon\";\n\nexport const TextAlignCenterIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M2 3.5a.5.5 0 0 0 0 1h12a.5.5 0 0 0 0-1H2ZM4.167 8a.5.5 0 0 1 .5-.5h6.666a.5.5 0 0 1 0 1H4.667a.5.5 0 0 1-.5-.5Zm-1.334 4a.5.5 0 0 1 .5-.5h9.334a.5.5 0 0 1 0 1H3.333a.5.5 0 0 1-.5-.5Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nTextAlignCenterIcon.displayName = \"TextAlignCenterIcon\";\n\nexport const TextAlignJustifyIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M2 3.5a.5.5 0 0 0 0 1h12a.5.5 0 0 0 0-1H2ZM1.5 8a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H2a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H2a.5.5 0 0 1-.5-.5Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nTextAlignJustifyIcon.displayName = \"TextAlignJustifyIcon\";\n\nexport const TextAlignLeftIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10 8H2M11.333 12H2M14 4H2\"\n        />\n      </svg>\n    );\n  }\n);\nTextAlignLeftIcon.displayName = \"TextAlignLeftIcon\";\n\nexport const TextAlignRightIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M2 3.5a.5.5 0 0 0 0 1h12a.5.5 0 0 0 0-1H2ZM5.5 8a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H6a.5.5 0 0 1-.5-.5Zm-1.333 4a.5.5 0 0 1 .5-.5H14a.5.5 0 0 1 0 1H4.667a.5.5 0 0 1-.5-.5Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nTextAlignRightIcon.displayName = \"TextAlignRightIcon\";\n\nexport const TextAnimationIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.476 4.3 7.817 3l6.935 1.817-.34 1.3m-6.245 4.386 2.601.682m.517-7.276-1.817 6.935M2 9.938V9h5v.938M3.562 14h1.875M4.5 9v5\"\n        />\n      </svg>\n    );\n  }\n);\nTextAnimationIcon.displayName = \"TextAnimationIcon\";\n\nexport const TextCapitalizeIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5 10.889 4.389 5.11l2.889 5.778M2.222 9.444h4.334M12.333 10.889a2.167 2.167 0 1 0 0-4.334 2.167 2.167 0 0 0 0 4.334ZM14.5 6.556v4.333\"\n        />\n      </svg>\n    );\n  }\n);\nTextCapitalizeIcon.displayName = \"TextCapitalizeIcon\";\n\nexport const TextItalicIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2.667h-6M9.333 13.333h-6M10 2.667 6 13.333\"\n        />\n      </svg>\n    );\n  }\n);\nTextItalicIcon.displayName = \"TextItalicIcon\";\n\nexport const TextLowercaseIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.937 11.25a2.437 2.437 0 1 0 0-4.875 2.437 2.437 0 0 0 0 4.875ZM6.375 6.375v4.875M12.063 11.25a2.437 2.437 0 1 0 0-4.875 2.437 2.437 0 0 0 0 4.875ZM9.625 4.75v6.5\"\n        />\n      </svg>\n    );\n  }\n);\nTextLowercaseIcon.displayName = \"TextLowercaseIcon\";\n\nexport const TextStrikethroughIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.667 2.667H6a2 2 0 0 0-1.887 2.666M9.333 8a2.667 2.667 0 1 1 0 5.333H4M2.667 8h10.666\"\n        />\n      </svg>\n    );\n  }\n);\nTextStrikethroughIcon.displayName = \"TextStrikethroughIcon\";\n\nexport const TextTruncateIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 11V3.333A1.334 1.334 0 0 0 12.667 2H3.333A1.333 1.333 0 0 0 2 3.333v9.334A1.333 1.333 0 0 0 3.333 14H5.5\"\n        />\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 13.375a.375.375 0 1 0 .75 0 .375.375 0 0 0-.75 0ZM10.813 13.375a.375.375 0 1 0 .75 0 .375.375 0 0 0-.75 0ZM13.625 13.375a.375.375 0 1 0 .75 0 .375.375 0 0 0-.75 0Z\"\n        />\n      </svg>\n    );\n  }\n);\nTextTruncateIcon.displayName = \"TextTruncateIcon\";\n\nexport const TextUnderlineIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4 2.667v4a4 4 0 0 0 8 0v-4M2.667 13.333h10.666\"\n        />\n      </svg>\n    );\n  }\n);\nTextUnderlineIcon.displayName = \"TextUnderlineIcon\";\n\nexport const TextUppercaseIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M1.5 10.811 4.31 5.19l2.812 5.622M2.202 9.406h4.217M9.933 8h3.162a1.406 1.406 0 1 1 0 2.811H9.933V5.19h2.81a1.406 1.406 0 1 1 0 2.811\"\n        />\n      </svg>\n    );\n  }\n);\nTextUppercaseIcon.displayName = \"TextUppercaseIcon\";\n\nexport const TextIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2.667 4.667v-2h10.666v2M6 13.333h4M8 2.667v10.666\"\n        />\n      </svg>\n    );\n  }\n);\nTextIcon.displayName = \"TextIcon\";\n\nexport const TooltipIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 10a1.333 1.333 0 0 1-1.333 1.333h-8L2 14V3.333A1.333 1.333 0 0 1 3.333 2h9.334A1.333 1.333 0 0 1 14 3.333V10Z\"\n        />\n      </svg>\n    );\n  }\n);\nTooltipIcon.displayName = \"TooltipIcon\";\n\nexport const TrashIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M2 4h12M12.667 4v9.333c0 .667-.667 1.334-1.334 1.334H4.667c-.667 0-1.334-.667-1.334-1.334V4M5.333 4V2.667C5.333 2 6 1.333 6.667 1.333h2.666c.667 0 1.334.667 1.334 1.334V4M6.667 7.333v4M9.333 7.333v4\"\n        />\n      </svg>\n    );\n  }\n);\nTrashIcon.displayName = \"TrashIcon\";\n\nexport const TriggerIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M11.915 7.354v-.647a1.294 1.294 0 1 0-2.587 0\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.328 6.707V6.06a1.294 1.294 0 1 0-2.587 0v.647\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M6.74 6.383V2.826a1.293 1.293 0 1 0-2.586 0v6.467\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M11.914 7.353a1.293 1.293 0 0 1 2.587 0v1.94a5.174 5.174 0 0 1-5.174 5.174H8.034c-1.81 0-2.91-.556-3.874-1.513l-2.328-2.328a1.293 1.293 0 0 1 1.83-1.824L4.8 9.94\"\n        />\n      </svg>\n    );\n  }\n);\nTriggerIcon.displayName = \"TriggerIcon\";\n\nexport const UpgradeIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M10.667 8 8 5.333 5.333 8M8 10.667V5.333\"\n        />\n      </svg>\n    );\n  }\n);\nUpgradeIcon.displayName = \"UpgradeIcon\";\n\nexport const UploadIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 8.667V14M2.667 9.933a4.667 4.667 0 1 1 7.806-4.6h1.194a3 3 0 0 1 1.666 5.495\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.333 11.333 8 8.667l2.667 2.666\"\n        />\n      </svg>\n    );\n  }\n);\nUploadIcon.displayName = \"UploadIcon\";\n\nexport const VideoIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          d=\"M1.833 5.333c0-.46.373-.833.834-.833h6.666c.46 0 .834.373.834.833v5.334c0 .46-.374.833-.834.833H2.667a.833.833 0 0 1-.834-.833V5.333ZM2.667 3.5C1.654 3.5.833 4.32.833 5.333v5.334c0 1.012.821 1.833 1.834 1.833h6.666c1.013 0 1.834-.82 1.834-1.833V9.6l2.704 1.803a.833.833 0 0 0 1.296-.693V5.247a.833.833 0 0 0-1.254-.72l-2.746 1.602v-.796c0-1.012-.821-1.833-1.834-1.833H2.667Zm8.5 3.787V8.4l3 2V5.537l-3 1.75Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nVideoIcon.displayName = \"VideoIcon\";\n\nexport const ViewportIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M5.173 1.35h.157a.65.65 0 0 1 0 1.3H5.2c-.57 0-.96 0-1.26.025-.294.024-.446.068-.553.122a1.35 1.35 0 0 0-.59.59c-.054.107-.098.259-.122.552-.025.301-.025.69-.025 1.261v.13a.65.65 0 0 1-1.3 0v-.157c0-.537 0-.98.03-1.34.03-.373.095-.715.259-1.036a2.65 2.65 0 0 1 1.158-1.158c.32-.164.663-.229 1.036-.26.36-.029.803-.029 1.34-.029Zm6.888 1.325c-.301-.024-.69-.025-1.261-.025h-.13a.65.65 0 1 1 0-1.3h.157c.537 0 .98 0 1.34.03.373.03.715.095 1.036.259.499.254.904.66 1.158 1.158.164.32.229.663.26 1.036.029.36.029.803.029 1.34v.157a.65.65 0 1 1-1.3 0V5.2c0-.57 0-.96-.025-1.26-.024-.294-.068-.446-.122-.553a1.35 1.35 0 0 0-.59-.59c-.107-.054-.26-.098-.552-.122ZM2 10.02a.65.65 0 0 1 .65.65v.13c0 .57 0 .96.025 1.26.024.294.068.446.122.553.13.254.336.46.59.59.107.054.259.098.552.122.301.025.69.025 1.261.025h.13a.65.65 0 1 1 0 1.3h-.157c-.537 0-.98 0-1.34-.03-.373-.03-.715-.095-1.036-.259a2.65 2.65 0 0 1-1.158-1.158c-.164-.32-.23-.663-.26-1.037-.029-.36-.029-.802-.029-1.34v-.156a.65.65 0 0 1 .65-.65Zm12 0a.65.65 0 0 1 .65.65v.157c0 .537 0 .98-.03 1.34-.03.373-.095.715-.259 1.036a2.65 2.65 0 0 1-1.158 1.158c-.32.164-.663.23-1.037.26-.36.029-.802.029-1.34.029h-.156a.65.65 0 1 1 0-1.3h.13c.57 0 .96 0 1.26-.025.294-.024.446-.068.553-.122.254-.13.46-.336.59-.59.054-.107.098-.259.122-.552.025-.301.025-.69.025-1.261v-.13a.65.65 0 0 1 .65-.65ZM8 10.033a2.033 2.033 0 1 0 0-4.066 2.033 2.033 0 0 0 0 4.066Zm0 1.3a3.333 3.333 0 1 0 0-6.666 3.333 3.333 0 0 0 0 6.666Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nViewportIcon.displayName = \"ViewportIcon\";\n\nexport const VimeoIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14.993 4.801c-.062 1.363-1.014 3.228-2.855 5.597-1.903 2.473-3.514 3.71-4.83 3.71-.817 0-1.508-.753-2.072-2.26l-1.13-4.143c-.419-1.506-.868-2.259-1.349-2.26a4.332 4.332 0 0 0-1.099.66L1 5.257c.69-.607 1.372-1.214 2.043-1.822.921-.796 1.613-1.215 2.074-1.257 1.09-.105 1.76.64 2.012 2.234.272 1.72.461 2.79.566 3.208.315 1.427.66 2.14 1.038 2.14.292 0 .733-.463 1.32-1.39.586-.925.9-1.63.942-2.113.084-.798-.23-1.198-.942-1.199-.357.005-.71.084-1.036.23.688-2.253 2.002-3.349 3.942-3.285 1.439.042 2.117.975 2.034 2.798Z\"\n        />\n      </svg>\n    );\n  }\n);\nVimeoIcon.displayName = \"VimeoIcon\";\n\nexport const WebhookFormIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12 11.32H8.007c-.734 0-1.3.627-1.654 1.267a2.666 2.666 0 0 1-5.02-1.254A2.62 2.62 0 0 1 1.713 10\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M4 11.333 6.087 7.48c.353-.647.066-1.453-.334-2.067a2.667 2.667 0 1 1 4.594-2.706\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m8 4 2.087 3.82c.353.647 1.18.847 1.913.847A2.667 2.667 0 0 1 12 14\"\n        />\n      </svg>\n    );\n  }\n);\nWebhookFormIcon.displayName = \"WebhookFormIcon\";\n\nexport const Webstudio1cIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.206 1.206 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.8-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.206 1.206 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fillRule=\"evenodd\"\n          d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nWebstudio1cIcon.displayName = \"Webstudio1cIcon\";\n\nexport const WebstudioIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"url(#c69f97277__paint0_linear_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint1_linear_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint2_linear_5232_4740)\"\n          fillOpacity=\".8\"\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint3_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint4_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint5_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"#E63CFE\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint6_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint7_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint8_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint9_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint10_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint11_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint12_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint13_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint14_linear_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint15_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint16_linear_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint17_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\"\n          clipRule=\"evenodd\"\n        />\n        <path\n          fill=\"url(#c69f97277__paint18_radial_5232_4740)\"\n          fillRule=\"evenodd\"\n          d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\"\n          clipRule=\"evenodd\"\n        />\n        <defs>\n          <radialGradient\n            id=\"c69f97277__paint3_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(11.2336 9.30762) rotate(-64.165) scale(7.2853)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".832\" stopColor=\"#4A4EFA\" stopOpacity=\"0\" />\n            <stop offset=\"1\" stopColor=\"#4A4EFA\" stopOpacity=\".75\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint4_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(11.0923 8.82676) scale(1.62424 3.47362)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".169\" stopColor=\"#11417A\" />\n            <stop offset=\".926\" stopColor=\"#4069D4\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint5_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(12.9724 13.2501) rotate(-90) scale(5.37233 2.57477)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".349\" stopColor=\"#E63CFE\" />\n            <stop offset=\"1\" stopColor=\"#E63CFE\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint6_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(7.59145 9.4488) rotate(-85.7675) scale(5.53509 4.91478)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor=\"#39FBBB\" stopOpacity=\".5\" />\n            <stop offset=\".689\" stopColor=\"#39FBBB\" />\n            <stop offset=\"1\" stopColor=\"#39FBBB\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint7_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(15.4778 12.5651) rotate(-122.629) scale(13.2696 3.2283)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor=\"#E63CFE\" />\n            <stop offset=\"1\" stopColor=\"#E63CFE\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint8_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(7.81141 11.1572) rotate(-85.0198) scale(5.97107 5.13635)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".557\" stopColor=\"#4A4EFA\" />\n            <stop offset=\".935\" stopColor=\"#4A4EFA\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint9_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(6.19224 12.9644) rotate(-68.6367) scale(6.23573 3.72131)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".613\" stopColor=\"#E63CFE\" />\n            <stop offset=\"1\" stopColor=\"#E63CFE\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint10_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(12.2257 8.7415) rotate(176.233) scale(7.74243 19.5729)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".814\" stopColor=\"#4A98D0\" stopOpacity=\"0\" />\n            <stop offset=\".973\" stopColor=\"#11417A\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint11_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(5.72725 13.2501) rotate(-90) scale(5.54893 4.6041)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".458\" stopColor=\"#FFAE3C\" />\n            <stop offset=\"1\" stopColor=\"#FFAE3C\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint12_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(12.4559 13.2501) rotate(-90) scale(5.00159)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".192\" stopColor=\"#E63CFE\" />\n            <stop offset=\"1\" stopColor=\"#E63CFE\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint13_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(9.52162 13.2501) rotate(-90) scale(3.76368 1.78782)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".174\" stopColor=\"#FFAE3C\" />\n            <stop offset=\"1\" stopColor=\"#FFAE3C\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint15_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(3.35867 5.95923) rotate(161.042) scale(1.68855 8.24528)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".12\" stopColor=\"#4A4EFA\" />\n            <stop offset=\"1\" stopColor=\"#4A4EFA\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint17_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(6.03209 13.2501) rotate(-90) scale(5.42506 5.92649)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".465\" stopColor=\"#FFAE3C\" />\n            <stop offset=\".926\" stopColor=\"#FFAE3C\" stopOpacity=\"0\" />\n          </radialGradient>\n          <radialGradient\n            id=\"c69f97277__paint18_radial_5232_4740\"\n            cx=\"0\"\n            cy=\"0\"\n            r=\"1\"\n            gradientTransform=\"translate(6.03209 13.2501) rotate(-90) scale(11.8703)\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".844\" stopColor=\"#E63CFE\" stopOpacity=\"0\" />\n            <stop offset=\"1\" stopColor=\"#E63CFE\" />\n          </radialGradient>\n          <linearGradient\n            id=\"c69f97277__paint0_linear_5232_4740\"\n            x1=\"16.343\"\n            x2=\"12.418\"\n            y1=\".102\"\n            y2=\"12.794\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor=\"#4A4EFA\" />\n            <stop offset=\".549\" stopColor=\"#E63CFE\" />\n          </linearGradient>\n          <linearGradient\n            id=\"c69f97277__paint1_linear_5232_4740\"\n            x1=\"11.413\"\n            x2=\"13.636\"\n            y1=\"7.419\"\n            y2=\"8.138\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".16\" stopColor=\"#4A4EFA\" />\n            <stop offset=\".946\" stopColor=\"#4A4EFA\" stopOpacity=\"0\" />\n          </linearGradient>\n          <linearGradient\n            id=\"c69f97277__paint2_linear_5232_4740\"\n            x1=\"11.75\"\n            x2=\"13.147\"\n            y1=\"6.351\"\n            y2=\"6.798\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor=\"#39FBBB\" />\n            <stop offset=\"1\" stopColor=\"#39FBBB\" stopOpacity=\"0\" />\n          </linearGradient>\n          <linearGradient\n            id=\"c69f97277__paint14_linear_5232_4740\"\n            x1=\".588\"\n            x2=\"3.972\"\n            y1=\"3.255\"\n            y2=\"13.784\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop offset=\".17\" stopColor=\"#E63CFE\" />\n            <stop offset=\".709\" stopColor=\"#FFAE3C\" />\n          </linearGradient>\n          <linearGradient\n            id=\"c69f97277__paint16_linear_5232_4740\"\n            x1=\"4.844\"\n            x2=\"4.37\"\n            y1=\"8.176\"\n            y2=\"8.331\"\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor=\"#E63CFE\" stopOpacity=\".33\" />\n            <stop offset=\"1\" stopColor=\"#E63CFE\" stopOpacity=\"0\" />\n          </linearGradient>\n        </defs>\n      </svg>\n    );\n  }\n);\nWebstudioIcon.displayName = \"WebstudioIcon\";\n\nexport const WindowInfoIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM8 11.516V7.853\"\n        />\n        <rect\n          width=\"1.4\"\n          height=\"1.4\"\n          x=\"7.3\"\n          y=\"4.484\"\n          fill=\"currentColor\"\n          rx=\".7\"\n        />\n      </svg>\n    );\n  }\n);\nWindowInfoIcon.displayName = \"WindowInfoIcon\";\n\nexport const WindowTitleIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.667 4H3.333C2.597 4 2 4.497 2 5.111v7.778C2 13.503 2.597 14 3.333 14h9.334c.736 0 1.333-.498 1.333-1.111V5.11C14 4.497 13.403 4 12.667 4ZM4 2h8\"\n        />\n      </svg>\n    );\n  }\n);\nWindowTitleIcon.displayName = \"WindowTitleIcon\";\n\nexport const WrapIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M3.49 2.237h-.98c-.542 0-.982.403-.982.9V6.07c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9V3.137c0-.497-.44-.9-.981-.9ZM3.49 9.03h-.98c-.542 0-.982.403-.982.9v2.933c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9V9.93c0-.497-.44-.9-.981-.9ZM8.49 2.237h-.98c-.542 0-.982.403-.982.9V6.07c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9V3.137c0-.497-.44-.9-.981-.9ZM7 11.397H13.641a.862.862 0 0 0 .862-.862v0-5.173 0a.862.862 0 0 0-.862-.862H12m-5 6.897 2-2m-2 2 2 2\"\n        />\n      </svg>\n    );\n  }\n);\nWrapIcon.displayName = \"WrapIcon\";\n\nexport const XAxisRotateIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 17\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M9.473 12.528c-.579 1.944-1.563 3.222-2.681 3.222-1.78 0-3.223-3.246-3.223-7.25s1.442-7.25 3.223-7.25c1.78 0 3.222 3.246 3.222 7.25\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M7.597 6.083 10.014 8.5l2.417-2.417\"\n        />\n      </svg>\n    );\n  }\n);\nXAxisRotateIcon.displayName = \"XAxisRotateIcon\";\n\nexport const XAxisIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.636 2.68v8.275h8.275M2.09 14.502l3.546-3.547\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m12.134 8.591 2.364 2.364-2.364 2.365\"\n        />\n      </svg>\n    );\n  }\n);\nXAxisIcon.displayName = \"XAxisIcon\";\n\nexport const XCircledFilledIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"currentColor\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"\n        />\n        <path\n          stroke=\"#fff\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m5.333 10.667 5.334-5.334M10.667 10.667 5.333 5.333\"\n        />\n      </svg>\n    );\n  }\n);\nXCircledFilledIcon.displayName = \"XCircledFilledIcon\";\n\nexport const XLogoIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fill=\"#000\"\n          d=\"M9.142 7.081 13.609 2H12.55L8.671 6.412 5.573 2H2l4.685 6.672L2 14h1.059l4.096-4.66L10.427 14H14L9.141 7.081Zm-1.45 1.65-.475-.665L3.44 2.78h1.626l3.048 4.266.475.664 3.962 5.546h-1.626L7.692 8.73Z\"\n        />\n      </svg>\n    );\n  }\n);\nXLogoIcon.displayName = \"XLogoIcon\";\n\nexport const XSmallIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"#000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m5.333 10.667 5.334-5.334M10.667 10.667 5.333 5.333\"\n        />\n      </svg>\n    );\n  }\n);\nXSmallIcon.displayName = \"XSmallIcon\";\n\nexport const XIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"\n        />\n      </svg>\n    );\n  }\n);\nXIcon.displayName = \"XIcon\";\n\nexport const XmlIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.163 9.02v-4L8.83 1.686h-5.5A1.333 1.333 0 0 0 1.997 3.02v6\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M8.164 2.186v1.667a1.333 1.333 0 0 0 1.333 1.333h2.667M1.997 11.314l3 3M4.997 11.314l-3 3M6.997 14.314v-3l1.5 1.523 1.5-1.523v3M12.163 11.314v3h1.84\"\n        />\n      </svg>\n    );\n  }\n);\nXmlIcon.displayName = \"XmlIcon\";\n\nexport const YAxisRotateIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 17\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M12.028 9.973c1.944-.579 3.222-1.563 3.222-2.681 0-1.78-3.246-3.223-7.25-3.223S.75 5.511.75 7.292c0 1.78 3.246 3.222 7.25 3.222\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.583 8.097 8 10.514 5.583 12.93\"\n        />\n      </svg>\n    );\n  }\n);\nYAxisRotateIcon.displayName = \"YAxisRotateIcon\";\n\nexport const YAxisIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.636 2.68v8.275h8.275M2.09 14.502l3.546-3.547\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"m3.272 3.862 2.364-2.364L8 3.862\"\n        />\n      </svg>\n    );\n  }\n);\nYAxisIcon.displayName = \"YAxisIcon\";\n\nexport const Youtube1cIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          fillRule=\"evenodd\"\n          d=\"M13.47 3.299a1.771 1.771 0 0 1 1.238 1.263C15 5.675 15 8 15 8s0 2.325-.293 3.438a1.771 1.771 0 0 1-1.238 1.263C12.38 13 8 13 8 13s-4.378 0-5.47-.299a1.771 1.771 0 0 1-1.237-1.263C1 10.325 1 8 1 8s0-2.325.293-3.438A1.771 1.771 0 0 1 2.53 3.299C3.622 3 8 3 8 3s4.378 0 5.47.299Zm-3.232 4.7L6.6 10.144V5.857L10.238 8Z\"\n          clipRule=\"evenodd\"\n        />\n      </svg>\n    );\n  }\n);\nYoutube1cIcon.displayName = \"Youtube1cIcon\";\n\nexport const YoutubeIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path d=\"M.892 11.528a18.047 18.047 0 0 1 0-7.482A1.496 1.496 0 0 1 1.94 3a37.082 37.082 0 0 1 12.12 0 1.497 1.497 0 0 1 1.048 1.047 18.046 18.046 0 0 1 0 7.482 1.497 1.497 0 0 1-1.048 1.048 37.077 37.077 0 0 1-12.12 0 1.497 1.497 0 0 1-1.048-1.048Z\" />\n        <path d=\"m6.5 10.3 4-2.4-4-2.4v4.8Z\" />\n      </svg>\n    );\n  }\n);\nYoutubeIcon.displayName = \"YoutubeIcon\";\n\nexport const ZAxisRotateIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 17\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M14 9.28a5.993 5.993 0 0 1-2.007 3.736 6.04 6.04 0 0 1-7.962-.024 5.98 5.98 0 0 1-.988-7.867A6.025 6.025 0 0 1 6.526 2.69a6.049 6.049 0 0 1 4.23.462 6.01 6.01 0 0 1 2.868 3.129M14 2.53v3.75h-3.766\"\n        />\n      </svg>\n    );\n  }\n);\nZAxisRotateIcon.displayName = \"ZAxisRotateIcon\";\n\nexport const ZAxisIcon: IconComponent = forwardRef(\n  ({ fill = \"none\", size = 16, ...props }, forwardedRef) => {\n    return (\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        viewBox=\"0 0 16 16\"\n        width={size}\n        height={size}\n        fill={fill}\n        {...props}\n        ref={forwardedRef}\n      >\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          d=\"M5.636 2.09v8.274h8.275M2.09 13.91l3.546-3.546m-3.547 3.547h3.547m-3.547 0v-3.744\"\n        />\n      </svg>\n    );\n  }\n);\nZAxisIcon.displayName = \"ZAxisIcon\";\n"
  },
  {
    "path": "packages/icons/src/__generated__/svg.ts",
    "content": "export const AccordionIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.056 8H14.5V4.101a1.3 1.3 0 0 0-1.3-1.299H2.8a1.3 1.3 0 0 0-1.3 1.3V8H13.056ZM13.056 13.198h.145a1.3 1.3 0 0 0 1.299-1.3V8h-13v3.899a1.3 1.3 0 0 0 1.3 1.299h10.256Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m10.026 4.913.975.976.976-.976M10.026 10.111l.975.976.976-.976\"/></svg>`;\n\nexport const AddTemplateInstanceIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.5 2H3.333A1.333 1.333 0 0 0 2 3.333V4.5M14 12.667c0 .021 0 .042-.002.063M11.5 14h1.167a1.333 1.333 0 0 0 1.331-1.27m0 0V11.5M2 11.5v1.167A1.333 1.333 0 0 0 3.333 14H4.5M7 14h2M2 7v2M8.461 4.77H14M11.23 2v5.538\"/></svg>`;\n\nexport const AiLoadingIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"c86546c68__evA4hI5dD261\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" viewBox=\"0 0 17 17\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><style>@keyframes evA4hI5dD263_s_do{0%,to{stroke-dashoffset:6.2}17.5%,82.5%{stroke-dashoffset:-6.24}}@keyframes evA4hI5dD264_s_do{0%,10%,90%,to{stroke-dashoffset:6.8}22.5%,77.5%{stroke-dashoffset:-6.8}}@keyframes evA4hI5dD265_s_do{0%,17.5%,82.5%,to{stroke-dashoffset:2.32}27.5%,72.5%{stroke-dashoffset:-2.32}}@keyframes evA4hI5dD266_s_do{0%,22.5%,77.5%,to{stroke-dashoffset:2.47}32.5%,67.5%{stroke-dashoffset:-2.47}}@keyframes evA4hI5dD267_s_do{0%,27.5%,72.5%,to{stroke-dashoffset:5.42}40%,60%{stroke-dashoffset:-5.42}}@keyframes evA4hI5dD268_s_do{0%,32.5%,67.5%,to{stroke-dashoffset:6.13}50%{stroke-dashoffset:-6.1}}#c86546c68__evA4hI5dD263{animation:evA4hI5dD263_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD264{animation:evA4hI5dD264_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD265{animation:evA4hI5dD265_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD266{animation:evA4hI5dD266_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD267{animation:evA4hI5dD267_s_do 4000ms linear infinite normal forwards}#c86546c68__evA4hI5dD268{animation:evA4hI5dD268_s_do 4000ms linear infinite normal forwards}</style><path fill=\"none\" stroke=\"rgba(255,255,255,0.4)\" stroke-dasharray=\"0\" stroke-linecap=\"round\" stroke-width=\"2.2\" d=\"m2.816 12.956 1.513-6.051a2.563 2.563 0 0 1 4.974 0l1.159 4.634a1.888 1.888 0 0 0 1.816 1.417 1.892 1.892 0 0 0 1.872-1.872V4.956\"/><path id=\"c86546c68__evA4hI5dD263\" fill=\"none\" stroke=\"#fff\" stroke-dasharray=\"6.24\" stroke-dashoffset=\"6.2\" stroke-linecap=\"round\" stroke-width=\"3.3\" d=\"m2.816 12.956 1.513-6.051\"/><path id=\"c86546c68__evA4hI5dD264\" fill=\"none\" stroke=\"#fff\" stroke-dasharray=\"6.8\" stroke-dashoffset=\"6.8\" stroke-linecap=\"round\" stroke-width=\"3.3\" d=\"M4.329 6.905a2.563 2.563 0 0 1 4.974 0\"/><path id=\"c86546c68__evA4hI5dD265\" fill=\"none\" stroke=\"#fff\" stroke-dasharray=\"2.32\" stroke-dashoffset=\"2.32\" stroke-linecap=\"round\" stroke-width=\"3.3\" d=\"m9.303 6.905.561 2.243\"/><path id=\"c86546c68__evA4hI5dD266\" fill=\"none\" stroke=\"#fff\" stroke-dasharray=\"2.47\" stroke-dashoffset=\"2.47\" stroke-linecap=\"round\" stroke-width=\"3.3\" d=\"m9.864 9.148.598 2.391\"/><path id=\"c86546c68__evA4hI5dD267\" fill=\"none\" stroke=\"#fff\" stroke-dasharray=\"5.42\" stroke-dashoffset=\"5.42\" stroke-linecap=\"round\" stroke-width=\"3.3\" d=\"M10.462 11.54a1.888 1.888 0 0 0 1.816 1.416 1.892 1.892 0 0 0 1.872-1.872\"/><path id=\"c86546c68__evA4hI5dD268\" fill=\"none\" stroke=\"#fff\" stroke-dasharray=\"6.13\" stroke-dashoffset=\"6.13\" stroke-linecap=\"round\" stroke-width=\"3.3\" d=\"M14.15 11.084V4.956\"/></svg>`;\n\nexport const AiIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" d=\"m1.5 12.588 1.735-6.94a2.94 2.94 0 0 1 5.705 0l1.33 5.315a2.165 2.165 0 0 0 2.083 1.625 2.17 2.17 0 0 0 2.147-2.147v-7.03\"/></svg>`;\n\nexport const AlertCircleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"m11.28 1.023-.56.06A10.685 10.685 0 0 0 7.18 2.12c-1.135.551-1.987 1.165-2.942 2.119-.961.959-1.595 1.841-2.141 2.981C.301 10.968.728 15.286 3.226 18.633c.475.636 1.527 1.683 2.174 2.164 3.964 2.948 9.266 2.937 13.237-.027.609-.454 1.679-1.524 2.133-2.133 2.974-3.985 2.974-9.289 0-13.274-.454-.608-1.523-1.677-2.13-2.128-1.595-1.186-3.275-1.875-5.217-2.139C13 1.038 11.574.99 11.28 1.023m2.04 2.078c2.563.387 4.804 1.83 6.24 4.019.303.463.743 1.359.932 1.9.346.993.485 1.845.485 2.98 0 1.493-.257 2.621-.897 3.94-.705 1.454-1.769 2.667-3.153 3.592-.789.528-2.051 1.056-3.019 1.265a9.053 9.053 0 0 1-7.538-1.778c-1.513-1.212-2.648-2.99-3.103-4.859-.186-.763-.244-1.272-.244-2.16 0-1.493.257-2.621.897-3.94a8.983 8.983 0 0 1 5.24-4.594c.705-.233 1.272-.348 2.18-.442.322-.033 1.571.015 1.98.077m-1.625 3.956a1.04 1.04 0 0 0-.567.459l-.108.184v4.606l.121.197c.068.11.205.253.311.325.471.316 1.102.171 1.407-.325l.121-.197V7.7l-.108-.184a1.005 1.005 0 0 0-1.177-.459m0 7.998a1.05 1.05 0 0 0-.567.461c-.091.156-.108.23-.108.484 0 .257.016.327.113.492.518.882 1.865.526 1.865-.492a.994.994 0 0 0-.535-.888 1.17 1.17 0 0 0-.768-.057\"/></svg>`;\n\nexport const AlertIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.487 12 9.153 2.667a1.333 1.333 0 0 0-2.32 0L1.5 12a1.333 1.333 0 0 0 1.167 2h10.666a1.333 1.333 0 0 0 1.154-2ZM8 6v2.667M8 11.333h.007\"/></svg>`;\n\nexport const AlignBaselineIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5 8h13M6.7 8.06v5.14a1.3 1.3 0 0 1-1.3 1.3H4.1a1.3 1.3 0 0 1-1.3-1.3V8.205M6.7 5.4V2.8a1.3 1.3 0 0 0-1.3-1.3H4.1a1.3 1.3 0 0 0-1.3 1.3v2.6M13.2 8.108v3.142a1.3 1.3 0 0 1-1.3 1.3h-1.3a1.3 1.3 0 0 1-1.3-1.3V8.108M13.2 5.4V2.8a1.3 1.3 0 0 0-1.3-1.3h-1.3a1.3 1.3 0 0 0-1.3 1.3v2.6\"/><path fill=\"currentColor\" d=\"M2.737 14.024V8.231h3.89v5.793h-3.89ZM9.44 12.085V8h3.89v4.085H9.44Z\"/></svg>`;\n\nexport const AlignCenterHorizontalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5 8h13M6.7 10.6v2.6a1.3 1.3 0 0 1-1.3 1.3H4.1a1.3 1.3 0 0 1-1.3-1.3v-2.6M6.7 5.4V2.8a1.3 1.3 0 0 0-1.3-1.3H4.1a1.3 1.3 0 0 0-1.3 1.3v2.6M13.2 10.6v.65a1.3 1.3 0 0 1-1.3 1.3h-1.3a1.3 1.3 0 0 1-1.3-1.3v-.65M9.3 5.4v-.65c0-.715.585-1.3 1.3-1.3h1.3a1.3 1.3 0 0 1 1.3 1.3v.65\"/></svg>`;\n\nexport const AlignContentCenterIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.368 9.34h.716c.542 0 .982-.403.982-.9v-.88c0-.497-.44-.9-.982-.9h-.716c-.542 0-.98.403-.98.9v.88c0 .497.438.9.98.9ZM7.69 9.34h.716c.542 0 .981-.403.981-.9v-.88c0-.497-.44-.9-.981-.9H7.69c-.542 0-.982.403-.982.9v.88c0 .497.44.9.982.9ZM3.011 9.34h.716c.542 0 .982-.403.982-.9v-.88c0-.497-.44-.9-.982-.9h-.716c-.542 0-.981.403-.981.9v.88c0 .497.44.9.981.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.548 15.49h-13M14.5.493h-13\"/></svg>`;\n\nexport const AlignContentEndIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.368 12.982h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9h-.716c-.542 0-.98.403-.98.9v.88c0 .496.438.9.98.9ZM7.69 12.982h.716c.542 0 .981-.403.981-.9v-.879c0-.497-.44-.9-.981-.9H7.69c-.542 0-.982.403-.982.9v.88c0 .496.44.9.982.9ZM3.011 12.982h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9h-.716c-.542 0-.981.403-.981.9v.88c0 .496.44.9.981.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.548 15.49h-13M14.5.493h-13\"/></svg>`;\n\nexport const AlignContentSpaceAroundIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.867 4.409h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.88c0-.496-.44-.9-.982-.9ZM3.867 8.912h-.716c-.542 0-.981.403-.981.9v.88c0 .496.44.9.981.9h.716c.542 0 .982-.404.982-.9v-.88c0-.497-.44-.9-.982-.9ZM8.367 4.409h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.88c0-.496-.44-.9-.982-.9ZM12.867 4.409h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .981-.403.981-.9v-.88c0-.496-.439-.9-.98-.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.452.51h13M1.5 15.507h13\"/></svg>`;\n\nexport const AlignContentSpaceBetweenIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.867 3.055h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9ZM3.867 10.357h-.716c-.542 0-.981.403-.981.9v.88c0 .496.44.9.981.9h.716c.542 0 .982-.404.982-.9v-.88c0-.497-.44-.9-.982-.9ZM8.367 3.055h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.879c0-.497-.44-.9-.982-.9ZM12.867 3.055h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .981-.403.981-.9v-.879c0-.497-.439-.9-.98-.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5 1h13M1.5 15h13\"/></svg>`;\n\nexport const AlignContentStartIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.632 3.018h-.716c-.542 0-.982.403-.982.9v.879c0 .497.44.9.982.9h.716c.542 0 .98-.403.98-.9v-.88c0-.496-.438-.9-.98-.9ZM8.31 3.018h-.716c-.542 0-.981.403-.981.9v.879c0 .497.44.9.981.9h.716c.542 0 .982-.403.982-.9v-.88c0-.496-.44-.9-.982-.9ZM12.989 3.018h-.716c-.542 0-.982.403-.982.9v.879c0 .497.44.9.982.9h.716c.542 0 .981-.403.981-.9v-.88c0-.496-.44-.9-.981-.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.452.51h13M1.5 15.507h13\"/></svg>`;\n\nexport const AlignContentStretchIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.867 3.987h-.716c-.542 0-.981.403-.981.9v6.365c0 .496.44.9.981.9h.716c.542 0 .982-.403.982-.9V4.886c0-.497-.44-.9-.982-.9ZM8.367 3.987h-.716c-.542 0-.981.403-.981.9v6.365c0 .496.44.9.981.9h.716c.542 0 .982-.403.982-.9V4.886c0-.497-.44-.9-.982-.9ZM12.867 3.987h-.716c-.542 0-.981.403-.981.9v6.365c0 .496.44.9.981.9h.716c.542 0 .981-.403.981-.9V4.886c0-.497-.439-.9-.98-.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5.52h13M1.5 15.475h13\"/></svg>`;\n\nexport const AlignEndHorizontalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.4 1.5H4.1a1.3 1.3 0 0 0-1.3 1.3v7.8a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3V2.8a1.3 1.3 0 0 0-1.3-1.3ZM11.9 6.05h-1.3a1.3 1.3 0 0 0-1.3 1.3v3.25a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3V7.35a1.3 1.3 0 0 0-1.3-1.3ZM14.5 14.5h-13\"/></svg>`;\n\nexport const AlignHorizontalJustifyCenterIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.167 3.45H2.834c-.736 0-1.333.576-1.333 1.286v6.429c0 .71.597 1.285 1.333 1.285h1.333c.737 0 1.334-.575 1.334-1.285V4.736c0-.71-.597-1.286-1.334-1.286ZM13.166 4.75h-1.334c-.736 0-1.333.582-1.333 1.3v3.9c0 .718.597 1.3 1.333 1.3h1.334c.736 0 1.333-.582 1.333-1.3v-3.9c0-.718-.597-1.3-1.333-1.3ZM8 1.5v13\"/></svg>`;\n\nexport const AlignHorizontalJustifyEndIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.1 3.45H2.8a1.3 1.3 0 0 0-1.3 1.3v6.5a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3v-6.5a1.3 1.3 0 0 0-1.3-1.3ZM10.6 4.75H9.3A1.3 1.3 0 0 0 8 6.05v3.9a1.3 1.3 0 0 0 1.3 1.3h1.3a1.3 1.3 0 0 0 1.3-1.3v-3.9a1.3 1.3 0 0 0-1.3-1.3ZM14.5 1.5v13\"/></svg>`;\n\nexport const AlignHorizontalJustifyStartIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.667 3.5H5.333C4.597 3.5 4 4.076 4 4.786v6.428c0 .71.597 1.286 1.333 1.286h1.334C7.403 12.5 8 11.924 8 11.214V4.786C8 4.076 7.403 3.5 6.667 3.5ZM13.333 4.833H12c-.736 0-1.333.627-1.333 1.4v4.2c0 .774.597 1.4 1.333 1.4h1.333c.737 0 1.334-.627 1.334-1.4v-4.2c0-.773-.597-1.4-1.334-1.4ZM1.333 1.5v13\"/></svg>`;\n\nexport const AlignHorizontalSpaceAroundIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.667 4.667H7.333C6.597 4.667 6 5.264 6 6v4c0 .736.597 1.333 1.333 1.333h1.334C9.403 11.333 10 10.736 10 10V6c0-.736-.597-1.333-1.333-1.333ZM2.667 14.667V1.333M13.333 14.667V1.333\"/></svg>`;\n\nexport const AlignHorizontalSpaceBetweenIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.667 3.333H3.333C2.597 3.333 2 3.93 2 4.667v6.666c0 .737.597 1.334 1.333 1.334h1.334c.736 0 1.333-.597 1.333-1.334V4.667c0-.737-.597-1.334-1.333-1.334ZM12.667 4.667h-1.334C10.597 4.667 10 5.264 10 6v4c0 .736.597 1.333 1.333 1.333h1.334c.736 0 1.333-.597 1.333-1.333V6c0-.736-.597-1.333-1.333-1.333ZM2 1.333v13.334M14 1.333v13.334\"/></svg>`;\n\nexport const AlignSelfBaselineIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.048 8.269v3.634c0 .719.583 1.301 1.301 1.301H8.65c.719 0 1.302-.582 1.302-1.3V8.268m0-2.22V4.096c0-.719-.583-1.301-1.302-1.301h-1.3c-.72 0-1.302.582-1.302 1.3v1.952M1.503 8h13\"/><path fill=\"currentColor\" d=\"M6.04 12.755V8.48h3.776v4.275H6.039Z\"/></svg>`;\n\nexport const AlignSelfCenterIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.048 6.048V4.097c0-.719.583-1.301 1.301-1.301H8.65c.719 0 1.302.582 1.302 1.3v1.952m0 3.904v1.951c0 .719-.583 1.301-1.302 1.301h-1.3a1.301 1.301 0 0 1-1.302-1.3V9.951M1.503 8h13\"/></svg>`;\n\nexport const AlignSelfEndIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.35 11.903h1.3c.719 0 1.302-.582 1.302-1.3V2.795c0-.719-.583-1.301-1.302-1.301h-1.3c-.72 0-1.302.582-1.302 1.3v7.807c0 .719.583 1.301 1.301 1.301ZM1.503 14.505h13\"/></svg>`;\n\nexport const AlignSelfStartIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.65 4.097h-1.3c-.719 0-1.302.582-1.302 1.3v7.807c0 .719.583 1.301 1.302 1.301h1.3c.72 0 1.302-.582 1.302-1.3V5.397c0-.719-.583-1.301-1.301-1.301ZM14.497 1.495h-13\"/></svg>`;\n\nexport const AlignSelfStretchIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.651 3.447h-1.3a1.3 1.3 0 0 0-1.3 1.3v6.5a1.3 1.3 0 0 0 1.3 1.299h1.3a1.3 1.3 0 0 0 1.3-1.3v-6.5a1.3 1.3 0 0 0-1.3-1.3ZM14.499 1.491h-13M14.501 14.509h-13\"/></svg>`;\n\nexport const AlignStartHorizontalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.4 4.097H4.1c-.72 0-1.302.582-1.302 1.3v7.807c0 .719.583 1.301 1.301 1.301H5.4c.719 0 1.301-.582 1.301-1.3V5.397c0-.719-.582-1.301-1.3-1.301ZM11.906 4.097h-1.301c-.719 0-1.301.582-1.301 1.3v3.254c0 .718.582 1.3 1.3 1.3h1.302c.718 0 1.3-.582 1.3-1.3V5.398c0-.719-.582-1.301-1.3-1.301ZM1.5 2h13\"/></svg>`;\n\nexport const AnimationGroupIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.5 8.3C6.3 7.7 7.8 8 9.3 11c.3-1.8 1.2-4.5 2.1-6M4.208 1.5H2.944A1.444 1.444 0 0 0 1.5 2.944v1.264m13 0V2.944A1.444 1.444 0 0 0 13.056 1.5h-1.264M14.5 13.056c0 .023 0 .046-.002.069M11.792 14.5h1.264a1.444 1.444 0 0 0 1.442-1.375m0 0v-1.333m-12.998 0v1.264A1.444 1.444 0 0 0 2.944 14.5h1.264m2.709-13h2.166m-2.166 13h2.166M1.5 6.917v2.166m13-2.166v2.166\"/></svg>`;\n\nexport const ArrowDownAZIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\".667\" d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 13.333V2.667M13.333 5.333H10M10 6.667V4.333a1.667 1.667 0 1 1 3.333 0v2.334M10 9.333h3.333l-3.333 4h3.333\"/></svg>`;\n\nexport const ArrowDownNarrowWideIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\".667\" d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 13.333V2.667M7.333 2.667H10M7.333 5.333H12M7.333 8H14\"/></svg>`;\n\nexport const ArrowDownWideNarrowIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\".667\" d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 13.333V2.667M7.333 2.667H14M7.333 5.333H12M7.333 8H10\"/></svg>`;\n\nexport const ArrowDownZAIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\".667\" d=\"m2 10.667 2.667 2.666 2.666-2.666M4.667 2.667v10.666M10 2.667h3.333l-3.333 4h3.333M10 13.333V11a1.667 1.667 0 1 1 3.333 0v2.333M13.333 12H10\"/></svg>`;\n\nexport const ArrowDownIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 3.333v9.334M12.667 8 8 12.667 3.333 8\"/></svg>`;\n\nexport const ArrowLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 8H3.333M8 3.333 3.333 8 8 12.667\"/></svg>`;\n\nexport const ArrowRightLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\".667\" d=\"m11 2 2.667 2.667m0 0L11 7.333m2.667-2.666H3M5.667 14 3 11.333m0 0 2.667-2.666M3 11.333h10.667\"/></svg>`;\n\nexport const ArrowRightIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.333 8h9.334M8 3.333 12.667 8 8 12.667\"/></svg>`;\n\nexport const ArrowUpIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 12.667V3.333M3.333 8 8 3.333 12.667 8\"/></svg>`;\n\nexport const AspectRatioIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 4v7.667h7.667\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m3 5 2-2 2 2M11.667 9.667l2 2-2 2\"/></svg>`;\n\nexport const AsteriskIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 3.96v8.08M11.498 5.98l-6.996 4.04M4.502 5.98l6.996 4.04\"/></svg>`;\n\nexport const AttachmentIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.823 13.501 14 8.2M10.667 4l-5.61 5.724a1.333 1.333 0 0 0 1.886 1.885l5.61-5.724a2.667 2.667 0 0 0-3.772-3.77l-5.61 5.723a4 4 0 0 0 2.83 6.829 3.922 3.922 0 0 0 2.804-1.147\"/></svg>`;\n\nexport const AutoScrollIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m5.502 10.5 2.5-5 2.5 5M6.126 9.25h3.75M2 14V2M14.003 14V2\"/></svg>`;\n\nexport const BlockquoteIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M6.667 8H3.333a.667.667 0 0 1-.666-.667V5a.667.667 0 0 1 .666-.667H6A.667.667 0 0 1 6.667 5v3Zm0 0C6.667 9.667 6 10.667 4 11.667M13.333 8H10a.667.667 0 0 1-.667-.667V5A.667.667 0 0 1 10 4.333h2.667a.667.667 0 0 1 .666.667v3Zm0 0c0 1.667-.666 2.667-2.666 3.667\"/></svg>`;\n\nexport const BodyIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM2 6h12\"/></svg>`;\n\nexport const BoldIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 8h6a2.667 2.667 0 0 1 0 5.333H4.667A.667.667 0 0 1 4 12.667V3.333a.667.667 0 0 1 .667-.666h4.666a2.667 2.667 0 1 1 0 5.333\"/></svg>`;\n\nexport const BorderRadiusBottomLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 3v6.364A3.636 3.636 0 0 0 6.636 13H13\"/></svg>`;\n\nexport const BorderRadiusBottomRightIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13 3v6.364A3.636 3.636 0 0 1 9.364 13H3\"/></svg>`;\n\nexport const BorderRadiusIndividualIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 4.667V3.333A1.333 1.333 0 0 1 3.333 2h1.334M11.333 2h1.334A1.333 1.333 0 0 1 14 3.333v1.334M14 11.333v1.334A1.333 1.333 0 0 1 12.667 14h-1.334M4.667 14H3.333A1.334 1.334 0 0 1 2 12.667v-1.334\"/></svg>`;\n\nexport const BorderRadiusTopLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13 3H6.636A3.636 3.636 0 0 0 3 6.636V13\"/></svg>`;\n\nexport const BorderRadiusTopRightIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 3h6.364A3.636 3.636 0 0 1 13 6.636V13\"/></svg>`;\n\nexport const BorderRadiusIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"#11181C\" fill-rule=\"evenodd\" d=\"M1.917 10.666a8.75 8.75 0 0 1 8.75-8.75h2.666a.75.75 0 0 1 0 1.5h-2.667a7.25 7.25 0 0 0-7.25 7.25v2.667a.75.75 0 0 1-1.5 0v-2.667Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const BorderWidthBottomIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 14H2m0-3.667v-7C2 2.597 2.597 2 3.333 2h9.334C13.403 2 14 2.597 14 3.333v7\"/></svg>`;\n\nexport const BorderWidthIndividualIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.4 1.744h5.2M14.5 10.356v-5.2M1.5 10.356v-5.2M5.4 14.256h5.2\"/></svg>`;\n\nexport const BorderWidthLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 14V2m3.667 0h7C13.403 2 14 2.597 14 3.333v9.334c0 .736-.597 1.333-1.333 1.333h-7\"/></svg>`;\n\nexport const BorderWidthRightIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 2v12m-3.667 0h-7A1.333 1.333 0 0 1 2 12.667V3.333C2 2.597 2.597 2 3.333 2h7\"/></svg>`;\n\nexport const BorderWidthTopIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 2h12m0 3.667v7c0 .736-.597 1.333-1.333 1.333H3.333A1.333 1.333 0 0 1 2 12.667v-7\"/></svg>`;\n\nexport const BoxIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"/></svg>`;\n\nexport const BracesIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.333 2h-.666a1.333 1.333 0 0 0-1.334 1.333v3.334A1.333 1.333 0 0 1 2 8a1.333 1.333 0 0 1 1.333 1.333v3.334c0 .733.6 1.333 1.334 1.333h.666M10.667 14h.666a1.333 1.333 0 0 0 1.334-1.333V9.333C12.667 8.6 13.267 8 14 8a1.333 1.333 0 0 1-1.333-1.333V3.333A1.333 1.333 0 0 0 11.333 2h-.666\"/></svg>`;\n\nexport const BrushCleaningIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.667 14.667 10 12m2.667-2.667a.667.667 0 0 0 .666-.666V8A1.333 1.333 0 0 0 12 6.667h-2A.667.667 0 0 1 9.333 6V2.667a1.333 1.333 0 1 0-2.666 0V6A.667.667 0 0 1 6 6.667H4A1.333 1.333 0 0 0 2.667 8v.667a.667.667 0 0 0 .666.666m9.334 0H3.333m9.334 0 1.315 4.512a.667.667 0 0 1-.649.822H2.667a.668.668 0 0 1-.649-.822l1.315-4.512m2 5.334L6 12\"/></svg>`;\n\nexport const BugIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m5.333 1.333 1.254 1.254M9.413 2.587l1.254-1.254M6 4.753v-.666a2.002 2.002 0 1 1 4 0v.666\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 13.333c-2.2 0-4-1.8-4-4v-2a2.667 2.667 0 0 1 2.667-2.666h2.666A2.667 2.667 0 0 1 12 7.333v2c0 2.2-1.8 4-4 4ZM8 13.333v-6\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.353 6C3.067 5.867 2 4.733 2 3.333M4 8.667H1.333M2 14c0-1.4 1.133-2.6 2.533-2.667M13.98 3.333c0 1.4-1.067 2.534-2.333 2.667M14.667 8.667H12M11.467 11.333C12.867 11.4 14 12.6 14 14\"/></svg>`;\n\nexport const ButtonElementIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M1.833 8a3.5 3.5 0 0 1 3.5-3.5h5.334a3.5 3.5 0 1 1 0 7H5.333a3.5 3.5 0 0 1-3.5-3.5Zm3.5-4.5a4.5 4.5 0 0 0 0 9h5.334a4.5 4.5 0 1 0 0-9H5.333ZM5.72 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm4.28-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const CalendarIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.333 9.333H8V12M10.667 1.333V4M2 6.667h12M5.333 1.333V4\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2.667H3.333C2.597 2.667 2 3.264 2 4v9.333c0 .737.597 1.334 1.333 1.334h9.334c.736 0 1.333-.597 1.333-1.334V4c0-.736-.597-1.333-1.333-1.333Z\"/></svg>`;\n\nexport const CheckCircleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><rect width=\"12\" height=\"12\" x=\"2\" y=\"2\" stroke=\"currentColor\" rx=\"6\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.998 7.928 7.381 9.31 10 6.69\"/></svg>`;\n\nexport const CheckMarkSmallIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.998 7.928 7.381 9.31 10 6.69\"/></svg>`;\n\nexport const CheckMarkIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.091\" d=\"m13.636 3.667-8 8L2 8.03\"/></svg>`;\n\nexport const CheckboxCheckedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6 8.238 1.383 1.383L10.003 7\"/></svg>`;\n\nexport const ChevronDownIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 6 4 4 4-4\"/></svg>`;\n\nexport const ChevronFilledUpIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" d=\"M13 11 8 5l-5 6\"/></svg>`;\n\nexport const ChevronLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><g id=\"c1893fb9d__icon/chevron left\"><path id=\"c1893fb9d__vector\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.3\" d=\"m9.256 4.499-3.512 3.5 3.512 3.502\"/></g></svg>`;\n\nexport const ChevronRightIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6 12 4-4-4-4\"/></svg>`;\n\nexport const ChevronUpIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 10 8 6l-4 4\"/></svg>`;\n\nexport const ChevronsLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.069 12.656 2.413 8l4.656-4.656M13.587 12.656 8.931 8l4.656-4.656\"/></svg>`;\n\nexport const CircleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 3.65a4.35 4.35 0 1 0 0 8.7 4.35 4.35 0 0 0 0-8.7ZM2.35 8a5.65 5.65 0 1 1 11.3 0 5.65 5.65 0 0 1-11.3 0Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const CloudIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.667 12.667H6a4.667 4.667 0 1 1 4.473-6h1.194a3 3 0 1 1 0 6Z\"/></svg>`;\n\nexport const CollapsibleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 14.667v-4M8 5.333v-4M2.5 8h-1M6.5 8h-1M10.5 8h-1M14.5 8h-1M10 12.667l-2-2-2 2M10 3.333l-2 2-2-2\"/></svg>`;\n\nexport const CommitIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM2 8h4M10 8h4\"/></svg>`;\n\nexport const ContentBlockIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 2H3.333A1.333 1.333 0 0 0 2 3.333v9.334A1.333 1.333 0 0 0 3.333 14h9.334A1.334 1.334 0 0 0 14 12.667V8\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.25 1.75a1.414 1.414 0 1 1 2 2L8.24 9.76a1.333 1.333 0 0 1-.568.336l-1.916.56a.334.334 0 0 1-.413-.413l.56-1.916c.063-.214.179-.41.337-.568L12.25 1.75Z\"/></svg>`;\n\nexport const ContentEmbedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.667 5.333h4M4.667 8H10M14 8.75V3.5A1.5 1.5 0 0 0 12.5 2h-9A1.5 1.5 0 0 0 2 3.5v9.252c0 .69.56 1.25 1.25 1.25v0H6M13.686 14.265l1.4-1.4-1.4-1.4\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m9.486 11.465-1.4 1.4 1.4 1.4M12.306 10.612l-1.441 4.321\"/></svg>`;\n\nexport const ContentIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.667 5.333h4M4.666 8H10m-5.333 2.667h4M2 3.5A1.5 1.5 0 0 1 3.5 2h9A1.5 1.5 0 0 1 14 3.5v9.188c0 .724-.588 1.312-1.313 1.312H3.313A1.312 1.312 0 0 1 2 12.687V3.5Z\"/></svg>`;\n\nexport const CopyIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.214 5.5H6.786c-.71 0-1.286.576-1.286 1.286v6.428c0 .71.576 1.286 1.286 1.286h6.428c.71 0 1.286-.576 1.286-1.286V6.786c0-.71-.576-1.286-1.286-1.286Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.786 10.5A1.29 1.29 0 0 1 1.5 9.214V2.786A1.29 1.29 0 0 1 2.786 1.5h6.428A1.29 1.29 0 0 1 10.5 2.786\"/></svg>`;\n\nexport const CropIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.167 1.5v9.333A1.333 1.333 0 0 0 5.5 12.167h9.333m-2.666 2.666V5.5a1.333 1.333 0 0 0-1.334-1.333H1.5\"/></svg>`;\n\nexport const DashedBorderIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.714 8H2M8.857 8H7.143M14 8h-1.714\"/></svg>`;\n\nexport const DescriptionIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.333 3.135a.47.47 0 0 0-.802-.332L4.275 5.058a.933.933 0 0 1-.664.275H2A.667.667 0 0 0 1.333 6v4a.667.667 0 0 0 .667.667h1.61a.934.934 0 0 1 .665.275l2.256 2.256a.47.47 0 0 0 .802-.333v-9.73ZM10.667 6a3.333 3.333 0 0 1 0 4M12.91 12.243a6 6 0 0 0 0-8.486\"/></svg>`;\n\nexport const DialogIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"#D2D2D2\" d=\"M13.444 1H2.556C1.696 1 1 1.696 1 2.556v10.888C1 14.304 1.696 15 2.556 15h10.888c.86 0 1.556-.696 1.556-1.556V2.556C15 1.696 14.304 1 13.444 1Z\"/><path fill=\"#fff\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.334 5.667H4.667v4.666h6.667V5.667Z\"/></svg>`;\n\nexport const DimensionsIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.833 6.833v-4h4\"/><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3.687 2.98a.5.5 0 0 0-.707.707l8.48 8.48H8.666a.5.5 0 1 0 0 1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 1 0-1 0v2.793l-8.48-8.48Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const DiscordIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" d=\"M4.565 7.429c0 .151.066.296.183.404A.655.655 0 0 0 5.189 8c.166 0 .325-.06.442-.167a.548.548 0 0 0 .183-.404.548.548 0 0 0-.183-.404.655.655 0 0 0-.442-.168.655.655 0 0 0-.441.168.548.548 0 0 0-.183.404Zm5.621 0c0 .151.066.296.183.404a.655.655 0 0 0 .442.167c.165 0 .324-.06.441-.167a.548.548 0 0 0 .183-.404.548.548 0 0 0-.183-.404.655.655 0 0 0-.441-.168.655.655 0 0 0-.442.168.548.548 0 0 0-.183.404Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M.5 11.273c0-2.744 1.072-6.37 2.142-7.841 0 0 1.072-.49 5.358-.49 4.285 0 5.356.491 5.356.491 1.072 1.47 2.144 5.096 2.144 7.84-.357.492-1.608 1.57-3.75 1.961L9.864 11.08a8.973 8.973 0 0 1-3.729 0L4.25 13.234c-2.142-.392-3.393-1.47-3.75-1.96Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.25 10.294c.326.298 1.012.597 1.885.785a8.976 8.976 0 0 0 3.73 0c.873-.188 1.558-.487 1.885-.785\"/></svg>`;\n\nexport const DotIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z\"/></svg>`;\n\nexport const DottedBorderIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.464 8a.667.667 0 1 0 1.333 0 .667.667 0 0 0-1.333 0ZM9.202 8a.667.667 0 1 0 1.334 0 .667.667 0 0 0-1.334 0ZM12.94 8a.667.667 0 1 0 1.334 0 .667.667 0 0 0-1.333 0ZM1.726 8a.667.667 0 1 0 1.333 0 .667.667 0 0 0-1.333 0Z\"/></svg>`;\n\nexport const DownloadIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" viewBox=\"0 0 24 24\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M12 15V3M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"/><path d=\"m7 10 5 5 5-5\"/></svg>`;\n\nexport const DragHandleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 8.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334ZM6 4a.667.667 0 1 0 0-1.333A.667.667 0 0 0 6 4ZM6 13.333A.667.667 0 1 0 6 12a.667.667 0 0 0 0 1.333ZM10 8.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334ZM10 4a.667.667 0 1 0 0-1.333A.667.667 0 0 0 10 4ZM10 13.333A.667.667 0 1 0 10 12a.667.667 0 0 0 0 1.333Z\"/></svg>`;\n\nexport const DynamicPageIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 12.203H3.855a1.474 1.474 0 0 1-1.473-1.473V2.81a1.474 1.474 0 0 1 1.473-1.474h6.079l3.684 3.683v5.71a1.474 1.474 0 0 1-1.474 1.474H8Zm0 0 .01 2.462m0 0H2.39m5.618 0h5.618\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.198 1.888V3.73a1.474 1.474 0 0 0 1.473 1.474h2.947\"/></svg>`;\n\nexport const EllipseIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3.373 5.736C2.203 6.405 1.65 7.229 1.65 8c0 .77.553 1.595 1.723 2.264C4.525 10.922 6.159 11.35 8 11.35c1.84 0 3.475-.428 4.627-1.086C13.797 9.595 14.35 8.771 14.35 8c0-.77-.553-1.595-1.723-2.264C11.475 5.078 9.841 4.65 8 4.65c-1.84 0-3.475.428-4.627 1.086Zm-.645-1.129C4.109 3.817 5.975 3.35 8 3.35c2.025 0 3.89.468 5.272 1.257 1.364.78 2.378 1.955 2.378 3.393 0 1.438-1.014 2.614-2.378 3.393-1.382.79-3.247 1.257-5.272 1.257-2.025 0-3.89-.468-5.272-1.257C1.364 10.613.35 9.438.35 8c0-1.438 1.014-2.614 2.378-3.393Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const EllipsesIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.112 8a.888.888 0 1 0 1.777 0 .888.888 0 0 0-1.777 0ZM12.724 8a.888.888 0 1 0 1.777 0 .888.888 0 0 0-1.777 0ZM1.499 8a.888.888 0 1 0 1.777 0 .888.888 0 0 0-1.777 0Z\"/></svg>`;\n\nexport const EmailIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.333 2.667H2.667c-.737 0-1.334.597-1.334 1.333v8c0 .736.597 1.333 1.334 1.333h10.666c.737 0 1.334-.597 1.334-1.333V4c0-.736-.597-1.333-1.334-1.333Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m14.667 4.667-5.98 3.8a1.293 1.293 0 0 1-1.374 0l-5.98-3.8\"/></svg>`;\n\nexport const EmbedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 10.667 14.667 8 12 5.333M4 5.333 1.333 8 4 10.667M9.667 2.667 6.333 13.333\"/></svg>`;\n\nexport const ExtensionIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.5 8.5h6m-6 0h-6m6 0V14M13.5 8.5v4.667c0 .736-.597 1.333-1.333 1.333H2.833A1.333 1.333 0 0 1 1.5 13.167V3.833c0-.736.597-1.333 1.333-1.333H7.5v6\"/><path stroke=\"currentColor\" stroke-linejoin=\"round\" d=\"M9.5.5v6h6V2.207A1.707 1.707 0 0 0 13.793.5H9.5Z\"/></svg>`;\n\nexport const ExternalLinkIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 2h4v4M6.667 9.333 14 2M12 8.667v4A1.334 1.334 0 0 1 10.667 14H3.333A1.334 1.334 0 0 1 2 12.667V5.333A1.333 1.333 0 0 1 3.333 4h4\"/></svg>`;\n\nexport const EyeClosedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m10 12-.481-2.167M1.333 5.333a7.097 7.097 0 0 0 13.334 0M13.333 10l-1.15-1.367M2.667 10l1.15-1.367M6 12l.481-2.167\"/></svg>`;\n\nexport const EyeOpenIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.375 8.232a.667.667 0 0 1 0-.464 7.167 7.167 0 0 1 13.25 0 .666.666 0 0 1 0 .464 7.166 7.166 0 0 1-13.25 0Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z\" class=\"ws-eye-open-pupil\"/></svg>`;\n\nexport const EyedropperIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m1.502 14.498.65-.65h1.947L9.94 8.008\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.151 13.849V11.9L7.993 6.06M9.94 4.113l2.207-2.207a1.377 1.377 0 0 1 1.947 1.947L11.887 6.06l.26.26A1.376 1.376 0 1 1 10.2 8.266L7.733 5.8a1.377 1.377 0 1 1 1.948-1.947l.26.26Z\"/></svg>`;\n\nexport const FolderIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.333 13.333A1.333 1.333 0 0 0 14.667 12V5.333A1.334 1.334 0 0 0 13.333 4H8.067a1.333 1.333 0 0 1-1.127-.6l-.54-.8A1.333 1.333 0 0 0 5.287 2h-2.62a1.333 1.333 0 0 0-1.334 1.333V12a1.333 1.333 0 0 0 1.334 1.333h10.666Z\"/></svg>`;\n\nexport const FooterIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM4 12h8\"/></svg>`;\n\nexport const FormTextAreaIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.522C14 2.682 13.318 2 12.478 2h-9C2.662 2 2 2.662 2 3.478v9.189C2 13.403 2.597 14 3.333 14ZM3.83 6.636V3.943\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m10.496 13.76 1.724-1.725 1.717-1.717\"/></svg>`;\n\nexport const FormTextFieldIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.333 2.667H4a2 2 0 0 1 2 2 2 2 0 0 1 2-2h.667M8.667 13.333H8a2 2 0 0 1-2-2 2 2 0 0 1-2 2h-.667M3.333 10.667h-.666a1.333 1.333 0 0 1-1.334-1.334V6.667a1.333 1.333 0 0 1 1.334-1.334h.666M8.667 5.333h4.666a1.333 1.333 0 0 1 1.334 1.334v2.666a1.333 1.333 0 0 1-1.334 1.334H8.667M6 4.667v6.666\"/></svg>`;\n\nexport const FormIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.184 5.5h.731a.75.75 0 0 0 .75-.75v-1.5a.75.75 0 0 0-.75-.75H2.085a.75.75 0 0 0-.75.75v1.5c0 .414.336.75.75.75h11.099ZM13.184 10.5h.732a.75.75 0 0 0 .75-.75v-1.5a.75.75 0 0 0-.75-.75H2.084a.75.75 0 0 0-.75.75v1.5c0 .414.336.75.75.75h11.099Z\"/><path fill=\"currentColor\" d=\"M6.613 14.5h.222a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-5.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5H6.613Z\"/></svg>`;\n\nexport const GapHorizontalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2h-2v12h2M3.333 14h2V2h-2\"/></svg>`;\n\nexport const GapVerticalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 12.667v-2H2v2M2 3.333v2h12v-2\"/></svg>`;\n\nexport const GearIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.147 1.333h-.294A1.333 1.333 0 0 0 6.52 2.667v.12a1.333 1.333 0 0 1-.667 1.153l-.286.167a1.333 1.333 0 0 1-1.334 0l-.1-.054a1.333 1.333 0 0 0-1.82.487l-.146.253a1.333 1.333 0 0 0 .486 1.82l.1.067a1.333 1.333 0 0 1 .667 1.147v.34a1.333 1.333 0 0 1-.667 1.16l-.1.06a1.333 1.333 0 0 0-.486 1.82l.146.253a1.334 1.334 0 0 0 1.82.487l.1-.054a1.334 1.334 0 0 1 1.334 0l.286.167a1.333 1.333 0 0 1 .667 1.153v.12a1.333 1.333 0 0 0 1.333 1.334h.294a1.333 1.333 0 0 0 1.333-1.334v-.12a1.334 1.334 0 0 1 .667-1.153l.286-.167a1.334 1.334 0 0 1 1.334 0l.1.054a1.333 1.333 0 0 0 1.82-.487l.146-.26a1.334 1.334 0 0 0-.486-1.82l-.1-.053a1.333 1.333 0 0 1-.667-1.16v-.334a1.333 1.333 0 0 1 .667-1.16l.1-.06a1.334 1.334 0 0 0 .486-1.82l-.146-.253a1.333 1.333 0 0 0-1.82-.487l-.1.054a1.333 1.333 0 0 1-1.334 0l-.286-.167a1.333 1.333 0 0 1-.667-1.153v-.12a1.333 1.333 0 0 0-1.333-1.334Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z\"/></svg>`;\n\nexport const GithubIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 22 22\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M11 1C5.475 1 1 5.475 1 11a9.994 9.994 0 0 0 6.838 9.488c.5.087.687-.213.687-.476 0-.237-.013-1.024-.013-1.862-2.512.463-3.162-.612-3.362-1.175-.113-.288-.6-1.175-1.025-1.413-.35-.187-.85-.65-.013-.662.788-.013 1.35.725 1.538 1.025.9 1.512 2.337 1.087 2.912.825.088-.65.35-1.088.638-1.338-2.225-.25-4.55-1.112-4.55-4.937 0-1.088.387-1.987 1.025-2.688-.1-.25-.45-1.274.1-2.65 0 0 .837-.262 2.75 1.026a9.28 9.28 0 0 1 2.5-.338c.85 0 1.7.112 2.5.337 1.912-1.3 2.75-1.024 2.75-1.024.55 1.375.2 2.4.1 2.65.637.7 1.025 1.587 1.025 2.687 0 3.838-2.337 4.688-4.562 4.938.362.312.675.912.675 1.85 0 1.337-.013 2.412-.013 2.75 0 .262.188.574.688.474A10.016 10.016 0 0 0 21 11c0-5.525-4.475-10-10-10Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const GoogleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" d=\"M14.72 8.16c0-.497-.045-.975-.127-1.433H8v2.711h3.767a3.228 3.228 0 0 1-1.406 2.107v1.762h2.272c1.323-1.222 2.087-3.016 2.087-5.148Z\"/><path fill=\"currentColor\" d=\"M8 15c1.89 0 3.475-.624 4.633-1.693l-2.272-1.762c-.624.42-1.42.674-2.361.674-1.82 0-3.366-1.228-3.92-2.883H1.75v1.808A6.993 6.993 0 0 0 8 15Z\"/><path fill=\"currentColor\" d=\"M4.08 9.33A4.195 4.195 0 0 1 3.857 8c0-.465.083-.91.223-1.33V4.863H1.75A6.914 6.914 0 0 0 1 8c0 1.133.274 2.195.75 3.137l1.815-1.412.515-.395Z\"/><path fill=\"currentColor\" d=\"M8 3.787c1.03 0 1.947.357 2.679 1.044l2.005-2.005C11.468 1.694 9.89 1 8 1a6.988 6.988 0 0 0-6.25 3.863L4.08 6.67C4.634 5.015 6.18 3.787 8 3.787Z\"/></svg>`;\n\nexport const GradientConicIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><g fill=\"#11181C\" clip-path=\"url(#c15388f97__paint0_angular_9413_9319_clip_path)\"><path d=\"M8 8V0a8 8 0 0 1 4 1.072Z\"/><path fill-opacity=\".909\" d=\"m8 8 4-6.928A8 8 0 0 1 14.928 4Z\"/><path fill-opacity=\".818\" d=\"m8 8 6.928-4A8 8 0 0 1 16 8Z\"/><path fill-opacity=\".727\" d=\"M8 8h8a8 8 0 0 1-1.072 4Z\"/><path fill-opacity=\".636\" d=\"m8 8 6.928 4A8 8 0 0 1 12 14.928Z\"/><path fill-opacity=\".545\" d=\"m8 8 4 6.928A8 8 0 0 1 8 16Z\"/><path fill-opacity=\".455\" d=\"M8 8v8a8 8 0 0 1-4-1.072Z\"/><path fill-opacity=\".364\" d=\"m8 8-4 6.928A8 8 0 0 1 1.072 12Z\"/><path fill-opacity=\".273\" d=\"m8 8-6.928 4A8 8 0 0 1 0 8Z\"/><path fill-opacity=\".182\" d=\"M8 8H0a8 8 0 0 1 1.072-4Z\"/><path fill-opacity=\".091\" d=\"M8 8 1.072 4A8 8 0 0 1 4 1.072Z\"/><path fill=\"none\" d=\"M8 8 4 1.072A8 8 0 0 1 8 0Z\"/></g><defs><clipPath id=\"c15388f97__paint0_angular_9413_9319_clip_path\"><path d=\"M2 5.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C3.52 2 4.08 2 5.2 2h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C14 3.52 14 4.08 14 5.2v5.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C12.48 14 11.92 14 10.8 14H5.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C2 12.48 2 11.92 2 10.8V5.2Z\"/></clipPath></defs></svg>`;\n\nexport const GradientLinearIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"url(#c1fa533d5__gradient-linear_svg__a)\" d=\"M2 5.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C3.52 2 4.08 2 5.2 2h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C14 3.52 14 4.08 14 5.2v5.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C12.48 14 11.92 14 10.8 14H5.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C2 12.48 2 11.92 2 10.8V5.2Z\"/><defs><linearGradient id=\"c1fa533d5__gradient-linear_svg__a\" x1=\"8\" x2=\"8\" y1=\"2\" y2=\"14\" gradientUnits=\"userSpaceOnUse\"><stop offset=\"0\" stop-color=\"#11181C\"/><stop offset=\"1\" stop-color=\"#11181C\" stop-opacity=\"0\"/></linearGradient></defs></svg>`;\n\nexport const GradientRadialIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"url(#c539a7588__paint0_radial_9420_9425)\" d=\"M2 5.2c0-1.12 0-1.68.218-2.108a2 2 0 0 1 .874-.874C3.52 2 4.08 2 5.2 2h5.6c1.12 0 1.68 0 2.108.218a2 2 0 0 1 .874.874C14 3.52 14 4.08 14 5.2v5.6c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874C12.48 14 11.92 14 10.8 14H5.2c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C2 12.48 2 11.92 2 10.8V5.2Z\"/><defs><radialGradient id=\"c539a7588__paint0_radial_9420_9425\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(8 8) rotate(135) scale(8.48528)\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#11181C\"/><stop offset=\"1\" stop-color=\"#11181C\" stop-opacity=\"0\"/></radialGradient></defs></svg>`;\n\nexport const GrowIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 8h4M6 10 4 8l2-2M12 8H8M10 6l2 2-2 2M2 14V2M14.003 14V2\"/></svg>`;\n\nexport const HeaderIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM4 4h8\"/></svg>`;\n\nexport const HeadingIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 8h8M4 13.333V2.667M12 13.333V2.667\"/></svg>`;\n\nexport const HelpIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.057 6c.161-.446.48-.821.898-1.06a2.11 2.11 0 0 1 1.391-.248c.48.08.914.322 1.227.684.313.361.484.831.484 1.29 0 1.334-2.059 2-2.059 2\"/><circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 11.333h0\"/></svg>`;\n\nexport const HomeIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 14V8.667A.667.667 0 0 0 9.333 8H6.667A.667.667 0 0 0 6 8.667V14\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6.667a1.333 1.333 0 0 1 .473-1.019l4.666-4a1.333 1.333 0 0 1 1.722 0l4.666 4A1.332 1.332 0 0 1 14 6.667v6A1.334 1.334 0 0 1 12.667 14H3.333A1.334 1.334 0 0 1 2 12.667v-6Z\"/></svg>`;\n\nexport const ImageIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 7.333a1.333 1.333 0 1 0 0-2.666 1.333 1.333 0 0 0 0 2.666ZM14 10l-2.057-2.057a1.333 1.333 0 0 0-1.886 0L4 14\"/></svg>`;\n\nexport const InfoCircleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 11.516V7.853\"/><rect width=\"1.4\" height=\"1.4\" x=\"7.3\" y=\"4.484\" fill=\"currentColor\" rx=\".7\"/></svg>`;\n\nexport const ItemIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 6h.007M5.333 10H14M5.333 6H14\"/></svg>`;\n\nexport const JCSpaceAroundIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M1.7 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\" clip-rule=\"evenodd\"/><path d=\"M4.5 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H5.3a.8.8 0 0 1-.8-.8V4.8ZM8.5 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H9.3a.8.8 0 0 1-.8-.8V4.8Z\"/><path fill-rule=\"evenodd\" d=\"M14.3 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const JCSpaceBetweenIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M2.2 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\" clip-rule=\"evenodd\"/><path d=\"M4 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H4.8a.8.8 0 0 1-.8-.8V4.8ZM9 4.8a.8.8 0 0 1 .8-.8h1.4a.8.8 0 0 1 .8.8v6.4a.8.8 0 0 1-.8.8H9.8a.8.8 0 0 1-.8-.8V4.8Z\"/><path fill-rule=\"evenodd\" d=\"M13.8 1.5c.442 0 .8.344.8.768v11.464c0 .424-.358.768-.8.768-.442 0-.8-.344-.8-.768V2.268c0-.424.358-.768.8-.768Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const LabelIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.476 11.074V4.926m0 6.148h3.77v-.615m-3.77.615h-.722m.722-6.148h.632m-.632 0h-.722\"/></svg>`;\n\nexport const LargeXIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"/></svg>`;\n\nexport const LifeBuoyIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><circle cx=\"7.995\" cy=\"7.995\" r=\"6.665\" stroke=\"#000\"/><path d=\"m3.287 3.287 2.826 2.826M9.886 6.113l2.827-2.826M9.886 9.887l2.827 2.826M6.113 9.887l-2.827 2.826\"/><circle cx=\"7.995\" cy=\"7.995\" r=\"2.665\" stroke=\"#000\"/></svg>`;\n\nexport const Link2UnlinkedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 4.667h1.333a3.333 3.333 0 0 1 0 6.666H10m-4 0H4.667a3.333 3.333 0 0 1 0-6.666H6\"/></svg>`;\n\nexport const Link2Icon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 11.333H4.667a3.333 3.333 0 0 1 0-6.666H6M10 4.667h1.333a3.334 3.334 0 0 1 0 6.666H10M5.333 8h5.334\"/></svg>`;\n\nexport const LinkIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.943 11.771 8 12.714A3.333 3.333 0 0 1 3.286 8l.943-.943M7.057 4.229 8 3.286A3.333 3.333 0 1 1 12.714 8l-.943.943M6.114 9.886l3.772-3.772\"/></svg>`;\n\nexport const ListItemIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M3.7 6.175a.85.85 0 1 1-1.7 0 .85.85 0 0 1 1.7 0Z\"/><path fill-rule=\"evenodd\" d=\"M5 6.175c0-.345.28-.625.625-.625h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 6.175ZM5 10.05c0-.345.28-.625.625-.625h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 10.05Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const ListViewIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M3.333 2.5a.833.833 0 0 0-.833.833v9.334c0 .46.373.833.833.833h9.334c.46 0 .833-.373.833-.833V3.333a.833.833 0 0 0-.833-.833H3.333ZM1.5 3.333c0-1.012.82-1.833 1.833-1.833h9.334c1.012 0 1.833.82 1.833 1.833v9.334c0 1.012-.82 1.833-1.833 1.833H3.333A1.833 1.833 0 0 1 1.5 12.667V3.333Z\" clip-rule=\"evenodd\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 6H2M14 10H2\"/></svg>`;\n\nexport const ListIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M3.7 4.35a.85.85 0 1 1-1.7 0 .85.85 0 0 1 1.7 0Z\"/><path fill-rule=\"evenodd\" d=\"M5 4.35c0-.346.28-.626.625-.626h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 4.35Z\" clip-rule=\"evenodd\"/><path d=\"M3.7 8A.85.85 0 1 1 2 8a.85.85 0 0 1 1.7 0Z\"/><path fill-rule=\"evenodd\" d=\"M5 8c0-.345.28-.625.625-.625h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 8Z\" clip-rule=\"evenodd\"/><path d=\"M3.7 11.65a.85.85 0 1 1-1.7 0 .85.85 0 0 1 1.7 0Z\"/><path fill-rule=\"evenodd\" d=\"M5 11.65c0-.346.28-.626.625-.626h7.75a.625.625 0 1 1 0 1.25h-7.75A.625.625 0 0 1 5 11.65Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const LoadingDotsIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"cfdbcf7a5__eEMFTOz1Zbw1\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" viewBox=\"0 0 300 300\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><style>@keyframes eEMFTOz1Zbw12_to__to{0%,26.666667%,56.666667%,6.333333%,77%,to{transform:translate(150px,180px)}16.666667%,67%{transform:translate(150px,120px);animation-timing-function:cubic-bezier(.42,0,.58,1)}}@keyframes eEMFTOz1Zbw13_to__to{0%,20%,50%,70%,to{transform:translate(80px,180px)}10%,60%{transform:translate(80px,120px);animation-timing-function:cubic-bezier(.42,0,.58,1)}}@keyframes eEMFTOz1Zbw14_to__to{0%,13.333333%,33.333333%,63.333333%,83.333333%,to{transform:translate(220px,180px)}23.333333%,73.333333%{transform:translate(220px,120px);animation-timing-function:cubic-bezier(.42,0,.58,1)}}#cfdbcf7a5__eEMFTOz1Zbw12_to{animation:eEMFTOz1Zbw12_to__to 2000ms linear infinite normal forwards}#cfdbcf7a5__eEMFTOz1Zbw13_to{animation:eEMFTOz1Zbw13_to__to 2000ms linear infinite normal forwards}#cfdbcf7a5__eEMFTOz1Zbw14_to{animation:eEMFTOz1Zbw14_to__to 2000ms linear infinite normal forwards}</style><circle id=\"cfdbcf7a5__eEMFTOz1Zbw12_to\" r=\"20\" stroke-width=\"0\" transform=\"translate(150,180) translate(0,0)\"/><circle id=\"cfdbcf7a5__eEMFTOz1Zbw13_to\" r=\"20\" stroke-width=\"0\" transform=\"translate(80,180) translate(0,0)\"/><circle id=\"cfdbcf7a5__eEMFTOz1Zbw14_to\" r=\"20\" stroke-width=\"0\" transform=\"translate(220,180) translate(0,0)\"/></svg>`;\n\nexport const MarkdownEmbedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.667 5.333h4M4.667 8H10M14 8.75V3.5A1.5 1.5 0 0 0 12.5 2h-9A1.5 1.5 0 0 0 2 3.5v9.191c0 .725.588 1.313 1.313 1.313h4.691\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 14.314v-4l2 2.03 2-2.03v4\"/></svg>`;\n\nexport const MaximizeIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 2h4v4M6 14H2v-4M14 2 9.334 6.667M2 14l4.667-4.667\"/></svg>`;\n\nexport const MenuIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 8h10.666M2.667 4h10.666M2.667 12h10.666\"/></svg>`;\n\nexport const MicOffIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m1.333 1.333 13 13M12.593 8.82c.048-.27.073-.545.074-.82V6.667M3.333 6.667V8a4.667 4.667 0 0 0 8 3.333M10 6.227V3.333a2 2 0 0 0-3.787-.886\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 6v2a2 2 0 0 0 3.413 1.413M8 12.667v2\"/></svg>`;\n\nexport const MicIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 1.333a2 2 0 0 0-2 2V8a2 2 0 1 0 4 0V3.333a2 2 0 0 0-2-2Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 6.667V8a4.666 4.666 0 1 1-9.334 0V6.667M8 12.667v2\"/></svg>`;\n\nexport const MinimizeIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 9.333h4v4M13.333 6.667h-4v-4M9.333 6.667 14 2M2 14l4.667-4.667\"/></svg>`;\n\nexport const MinusIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.333 8h9.334\"/></svg>`;\n\nexport const NavigationMenuIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 7v5.833c0 .645-.597 1.167-1.333 1.167H3.333C2.597 14 2 13.478 2 12.833V7\"/><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.978 2.003H2.6a.6.6 0 0 0-.6.6v1.4h11.971v-1.4a.6.6 0 0 0-.6-.6h-2.393Z\"/></svg>`;\n\nexport const NavigatorIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.5 8H15M1 4.5h10.5M4.5 11.5H15\"/></svg>`;\n\nexport const NewFolderIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 6.667v4M6 8.667h4M13.333 13.333A1.333 1.333 0 0 0 14.667 12V5.333A1.334 1.334 0 0 0 13.333 4H8.067a1.333 1.333 0 0 1-1.127-.6l-.54-.8A1.333 1.333 0 0 0 5.287 2h-2.62a1.333 1.333 0 0 0-1.334 1.333V12a1.333 1.333 0 0 0 1.334 1.333h10.666Z\"/></svg>`;\n\nexport const NewPageIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 1.333H4a1.333 1.333 0 0 0-1.333 1.334v10.666A1.333 1.333 0 0 0 4 14.667h8a1.333 1.333 0 0 0 1.333-1.334V4.667L10 1.333Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.333 1.333V4a1.333 1.333 0 0 0 1.334 1.333h2.666M6 10h4M8 12V8\"/></svg>`;\n\nexport const NoWrapIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9ZM8.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9ZM13.434 3.667h-.98c-.543 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.541 0 .98-.403.98-.9v-7.2c0-.497-.439-.9-.98-.9Z\"/></svg>`;\n\nexport const NotebookAndPenIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.933 1.5H4a1.35 1.35 0 0 0-.943.38c-.25.245-.39.575-.39.92v10.4c0 .345.14.676.39.92.25.243.59.38.943.38h8c.354 0 .693-.137.943-.38.25-.244.39-.575.39-.92V8.39M1.333 4.167H4M1.333 6.833H4M1.333 9.5H4M1.333 12.167H4\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.252 3.917a1.416 1.416 0 1 0-2.003-2.002L8.91 5.256a1.333 1.333 0 0 0-.337.57l-.558 1.913a.333.333 0 0 0 .413.413l1.914-.558c.215-.063.41-.179.569-.337l3.342-3.34Z\"/></svg>`;\n\nexport const OfflineIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.524 3.947C1.83 4.792 1.428 6.728 1.352 7.59A4.667 4.667 0 0 0 6 12.667h6.23m1.558-.879a3 3 0 0 0-2.121-5.121h-1.194a4.667 4.667 0 0 0-2.421-2.859c-.78-.381-1.673-.403-2.54-.403v0\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" d=\"M1.632 1.997 13.583 13.95\"/></svg>`;\n\nexport const OrderFirstIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.547 3.667h-.981c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.982.9h.98c.543 0 .982-.403.982-.9v-7.2c0-.497-.44-.9-.981-.9Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9ZM13.434 3.667h-.98c-.543 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.541 0 .98-.403.98-.9v-7.2c0-.497-.439-.9-.98-.9Z\"/></svg>`;\n\nexport const OrderLastIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.547 3.667h-.981c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.982.9h.98c.543 0 .982-.403.982-.9v-7.2c0-.497-.44-.9-.981-.9ZM8.49 3.667h-.98c-.542 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9v-7.2c0-.497-.44-.9-.981-.9Z\"/><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.434 3.667h-.98c-.543 0-.982.403-.982.9v7.2c0 .497.44.9.981.9h.982c.541 0 .98-.403.98-.9v-7.2c0-.497-.439-.9-.98-.9Z\"/></svg>`;\n\nexport const OverlayIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"#D2D2D2\" d=\"M13.444 1H2.556C1.696 1 1 1.696 1 2.556v10.888C1 14.304 1.696 15 2.556 15h10.888c.86 0 1.556-.696 1.556-1.556V2.556C15 1.696 14.304 1 13.444 1Z\"/></svg>`;\n\nexport const PageIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.833 2h-5.5A1.333 1.333 0 0 0 3 3.333v10a1.333 1.333 0 0 0 1.333 1.334h7.5a1.333 1.333 0 0 0 1.334-1.334v-8L9.833 2Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.167 2.5v1.667A1.333 1.333 0 0 0 10.5 5.5h2.667\"/></svg>`;\n\nexport const PaintBrushIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m6.04 7.933 5.38-5.373a1.9 1.9 0 1 1 2.687 2.687l-5.374 5.386M4.713 9.96c-1.106 0-2 .9-2 2.013 0 .887-1.666 1.014-1.333 1.347.72.733 1.66 1.347 2.667 1.347 1.466 0 2.666-1.2 2.666-2.694a2.007 2.007 0 0 0-2-2.013Z\"/></svg>`;\n\nexport const PhoneIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.5 11.24v1.96a1.308 1.308 0 0 1-1.425 1.308A12.934 12.934 0 0 1 7.434 12.5a12.745 12.745 0 0 1-3.922-3.922 12.935 12.935 0 0 1-2.007-5.667 1.307 1.307 0 0 1 1.301-1.425h1.96a1.307 1.307 0 0 1 1.308 1.124c.083.628.236 1.244.458 1.837a1.307 1.307 0 0 1-.294 1.38l-.83.83a10.458 10.458 0 0 0 3.921 3.921l.83-.83a1.307 1.307 0 0 1 1.38-.294c.593.221 1.209.375 1.836.458a1.307 1.307 0 0 1 1.125 1.326Z\"/></svg>`;\n\nexport const PlayIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 2 9.333 6L4 14V2Z\"/></svg>`;\n\nexport const PluginIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M4.687 2.667a2.317 2.317 0 0 1 4.633 0v.35h.039c.447 0 .815 0 1.116.02.312.021.6.067.876.181a2.65 2.65 0 0 1 1.434 1.435c.114.276.16.563.181.875.01.147.015.31.018.489h.353a2.317 2.317 0 0 1 0 4.633h-.35v.51c0 .537 0 .98-.03 1.34-.03.374-.096.716-.26 1.036a2.65 2.65 0 0 1-1.157 1.159c-.321.163-.663.228-1.037.259-.36.03-.802.03-1.34.03H8.67a.65.65 0 0 1-.65-.65v-1.167a.85.85 0 1 0-1.7 0v1.166a.65.65 0 0 1-.65.65h-.827c-.537 0-.98 0-1.34-.03-.373-.03-.715-.095-1.036-.258a2.65 2.65 0 0 1-1.158-1.159c-.164-.32-.23-.662-.26-1.036-.03-.36-.03-.803-.03-1.34V10a.65.65 0 0 1 .65-.65h1a1.017 1.017 0 0 0 0-2.033h-1a.65.65 0 0 1-.65-.65v-.023c0-.447 0-.815.021-1.116.022-.312.067-.6.182-.875a2.65 2.65 0 0 1 1.434-1.435c.276-.114.563-.16.875-.18.302-.021.67-.021 1.117-.021h.039v-.35ZM7.003 1.65c-.561 0-1.016.455-1.016 1.017v1a.65.65 0 0 1-.65.65H4.67c-.475 0-.799 0-1.05.017-.246.017-.375.048-.467.085-.33.137-.593.4-.73.731-.038.091-.069.22-.086.467a7.71 7.71 0 0 0-.014.4h.347a2.317 2.317 0 0 1 0 4.633h-.35v.483c0 .571 0 .96.025 1.261.024.293.068.445.122.552.13.254.336.46.59.59.107.055.259.098.552.122.301.025.69.025 1.26.025h.15v-.516a2.15 2.15 0 0 1 4.3 0v.516c.474 0 .81-.003 1.078-.025.293-.024.445-.067.553-.122.254-.13.46-.336.59-.59.054-.107.098-.259.122-.552.024-.3.025-.69.025-1.26V10a.65.65 0 0 1 .65-.65h1a1.017 1.017 0 0 0 0-2.033h-1a.65.65 0 0 1-.65-.65c0-.475 0-.799-.018-1.05-.017-.246-.047-.376-.085-.467a1.35 1.35 0 0 0-.73-.73c-.092-.038-.222-.069-.467-.086a17.153 17.153 0 0 0-1.05-.017H8.67a.65.65 0 0 1-.65-.65v-1c0-.562-.455-1.017-1.017-1.017Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const PlusIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 8h12M8 2v12\"/></svg>`;\n\nexport const PopoverIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 6V4a1.334 1.334 0 0 0-1.333-1.333h-10A1.333 1.333 0 0 0 1.333 4v6.667c0 .733.6 1.333 1.334 1.333h2.666\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.333 8.667h-4C8.597 8.667 8 9.264 8 10v2c0 .736.597 1.333 1.333 1.333h4c.737 0 1.334-.597 1.334-1.333v-2c0-.736-.597-1.333-1.334-1.333Z\"/></svg>`;\n\nexport const RadioCheckedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"/><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z\"/></svg>`;\n\nexport const RadioGroupIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"#000\" stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.839 8a1.82 1.82 0 1 1-3.64 0 1.82 1.82 0 0 1 3.64 0Z\"/><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.019 13.027a5.027 5.027 0 1 0 0-10.054 5.027 5.027 0 0 0 0 10.054Z\"/><path stroke=\"#000\" stroke-linecap=\"round\" d=\"M12.629 12.077v0a6.73 6.73 0 0 0-.337-8.565v0\"/></svg>`;\n\nexport const RadioUncheckedIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"/></svg>`;\n\nexport const RangeContain50Icon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11 8.5v-1c0-.552-.597-1-1.333-1H6.333C5.597 6.5 5 6.948 5 7.5v1c0 .552.597 1 1.333 1h3.334c.736 0 1.333-.448 1.333-1ZM1.452 1.5h13M1.5 14.507h13\"/></svg>`;\n\nexport const RangeContainIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5 14.507h13M11 11.5v-1c0-.552-.597-1-1.333-1H6.333C5.597 9.5 5 9.948 5 10.5v1c0 .552.597 1 1.333 1h3.334c.736 0 1.333-.448 1.333-1ZM1.452 1.5h13\"/></svg>`;\n\nexport const RangeCoverIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11 13.5v-1c0-.552-.597-1-1.333-1H6.333c-.736 0-1.333.448-1.333 1v1c0 .552.597 1 1.333 1h3.334c.736 0 1.333-.448 1.333-1ZM1.5 9.5h13M1.452 1.5h13\"/></svg>`;\n\nexport const RefreshIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 1.5h-.002A7 7 0 0 0 3.16 3.467l-.006.006-.653.653V2a.5.5 0 0 0-1 0v3.333a.498.498 0 0 0 .144.352l.005.004A.498.498 0 0 0 2 5.833h3.357a.5.5 0 1 0 0-1h-2.15l.65-.65A6 6 0 0 1 8.001 2.5 5.5 5.5 0 0 1 13.5 8a.5.5 0 0 0 1 0A6.5 6.5 0 0 0 8 1.5Zm-6 6a.5.5 0 0 1 .5.5A5.5 5.5 0 0 0 8 13.5a6 6 0 0 0 4.143-1.683l.759-.76h-2.235a.5.5 0 1 1 0-1H14a.5.5 0 0 1 .5.5v3.334a.5.5 0 0 1-1 0v-2.017l-.653.653-.006.006A7 7 0 0 1 8 14.5H8A6.5 6.5 0 0 1 1.5 8a.5.5 0 0 1 .5-.5Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const RepeatColumnIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.25 12.417V3.083c0-.736-.597-1.333-1.333-1.333H7.083c-.736 0-1.333.597-1.333 1.333v9.334c0 .736.597 1.333 1.333 1.333h1.834c.736 0 1.333-.597 1.333-1.333ZM10.25 5.75h-4M10.25 9.75h-4\"/></svg>`;\n\nexport const RepeatGridIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM2 6h12M2 10h12M6 2v12M10 2v12\"/></svg>`;\n\nexport const RepeatRowIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 5.5H3.333C2.597 5.5 2 6.097 2 6.833v1.834C2 9.403 2.597 10 3.333 10h9.334C13.403 10 14 9.403 14 8.667V6.833c0-.736-.597-1.333-1.333-1.333ZM6 5.5v4M10 5.5v4\"/></svg>`;\n\nexport const ResetIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M6.322 1.78a.783.783 0 0 1-.068 1.104L4.85 4.124h4.453A4.696 4.696 0 0 1 14 8.819c0 2.686-2.2 4.35-4.696 4.35h-1.63a.783.783 0 0 1 0-1.564h1.63c1.828 0 3.13-1.15 3.13-2.786a3.13 3.13 0 0 0-3.13-3.13H4.851l1.403 1.24A.783.783 0 1 1 5.217 8.1L2.264 5.493a.783.783 0 0 1 0-1.173L5.217 1.71a.783.783 0 0 1 1.105.068Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const ResourceIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.667 5.333h4M4.667 8H10M14 8.75V3.5A1.5 1.5 0 0 0 12.5 2h-9A1.5 1.5 0 0 0 2 3.5v9.252c0 .69.56 1.25 1.25 1.25v0H6\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.45 14.5h-.7a1.75 1.75 0 1 1 0-3.5h.7M12.55 11h.7a1.75 1.75 0 1 1 0 3.5h-.7M10.1 12.75h2.8\"/></svg>`;\n\nexport const SearchIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.333 12.667A5.333 5.333 0 1 0 7.333 2a5.333 5.333 0 0 0 0 10.667ZM14 14l-2.867-2.867\"/></svg>`;\n\nexport const SectionLinkIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 14.667H12a1.333 1.333 0 0 0 1.333-1.334V4.667L10 1.333H4a1.333 1.333 0 0 0-1.333 1.334v2.666\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.333 1.333V4a1.333 1.333 0 0 0 1.334 1.333h2.666M2 10h4\"/></svg>`;\n\nexport const SelectIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.184 10.33h.149c.736 0 1.332-.597 1.332-1.332V6.332c0-.735-.596-1.332-1.332-1.332H2.667c-.735 0-1.332.597-1.332 1.332v2.666c0 .735.597 1.332 1.333 1.332h10.516Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m10.078 7.165 1 1 1-1\"/></svg>`;\n\nexport const SettingsIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M13.987 6H9.32M13.932 6h-12M14 10.5H2\"/><rect width=\"4\" height=\"4\" x=\"1.932\" y=\"8.534\" fill=\"#fff\" stroke=\"currentColor\" rx=\"2\"/><rect width=\"4\" height=\"4\" x=\"10.068\" y=\"4\" fill=\"#fff\" stroke=\"currentColor\" rx=\"2\"/></svg>`;\n\nexport const ShadowInsetIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><rect width=\"13\" height=\"13\" x=\"1.5\" y=\"1.5\" stroke=\"currentColor\" rx=\"6.5\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.804 7.833a4.196 4.196 0 1 1 8.392 0\"/></svg>`;\n\nexport const ShadowNormalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 12.327a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M15 10v0c-2.774 5.944-11.226 5.944-14 0v0\"/></svg>`;\n\nexport const ShieldIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 24 24\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/></svg>`;\n\nexport const ShrinkIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M8 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 8 1ZM1.333 7.5a.5.5 0 0 0 0 1h2.793L2.98 9.646a.5.5 0 0 0 .707.708l2-2a.499.499 0 0 0 .146-.351v-.006a.498.498 0 0 0-.146-.35l-2-2a.5.5 0 1 0-.707.707L4.126 7.5H1.333Zm8.834.503a.496.496 0 0 0 .146.35l2 2a.5.5 0 0 0 .707-.707L11.874 8.5h2.793a.5.5 0 1 0 0-1h-2.793l1.146-1.146a.5.5 0 1 0-.707-.708l-2 2a.498.498 0 0 0-.146.351v.006ZM8.5 5.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1ZM8 9a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 8 9Zm.5 4.5a.5.5 0 0 0-1 0v1a.5.5 0 0 0 1 0v-1Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const SliderIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.015 8.265h-6M8.015 8.265h-6\"/><rect width=\"3\" height=\"3\" x=\"4.026\" y=\"6.5\" fill=\"#fff\" stroke=\"currentColor\" rx=\"1.5\"/></svg>`;\n\nexport const SlotComponentIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.5 2H3.333A1.333 1.333 0 0 0 2 3.333V4.5M14 4.5V3.333A1.333 1.333 0 0 0 12.667 2H11.5M14 12.667c0 .021 0 .042-.002.063M11.5 14h1.167a1.333 1.333 0 0 0 1.331-1.27m0 0V11.5M2 11.5v1.167A1.333 1.333 0 0 0 3.333 14H4.5M7 2h2M7 14h2M2 7v2M14 7v2\"/></svg>`;\n\nexport const SpinnerIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"c765c5cf3__e2CRglijn891\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" viewBox=\"0 0 128 128\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><style>@keyframes e2CRglijn892_tr__tr{0%{transform:translate(64px,64px) rotate(90deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}50%{transform:translate(64px,64px) rotate(810deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}to{transform:translate(64px,64px) rotate(1530deg)}}@keyframes e2CRglijn892_s_p{0%,to{stroke:#39fbbb}25%{stroke:#4a4efa}50%{stroke:#e63cfe}75%{stroke:#ffae3c}}@keyframes e2CRglijn892_s_do{0%{stroke-dashoffset:251.89}2.5%,52.5%{stroke-dashoffset:263.88;animation-timing-function:cubic-bezier(.42,0,.58,1)}25%,75%{stroke-dashoffset:131.945}to{stroke-dashoffset:251.885909}}#c765c5cf3__e2CRglijn892_tr{animation:e2CRglijn892_tr__tr 3000ms linear infinite normal forwards}#c765c5cf3__e2CRglijn892{animation-name:e2CRglijn892_s_p,e2CRglijn892_s_do;animation-duration:3000ms;animation-fill-mode:forwards;animation-timing-function:linear;animation-direction:normal;animation-iteration-count:infinite}</style><g id=\"c765c5cf3__e2CRglijn892_tr\" transform=\"translate(64,64) rotate(90)\"><circle id=\"c765c5cf3__e2CRglijn892\" r=\"42\" fill=\"none\" stroke=\"#39fbbb\" stroke-dasharray=\"263.89\" stroke-dashoffset=\"251.89\" stroke-linecap=\"round\" stroke-width=\"16\" transform=\"scale(-1,1) translate(0,0)\"/></g></svg>`;\n\nexport const StaggerAnimationIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.222 5H8.778A.778.778 0 0 0 8 5.778v5.444c0 .43.348.778.778.778h5.444c.43 0 .778-.348.778-.778V5.778A.778.778 0 0 0 14.222 5ZM5.444 6H1.556A.556.556 0 0 0 1 6.556v3.888c0 .307.249.556.556.556h3.888A.556.556 0 0 0 6 10.444V6.556A.556.556 0 0 0 5.444 6Z\"/></svg>`;\n\nexport const StopIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><rect width=\"9\" height=\"9\" x=\"3.5\" y=\"3.5\" fill=\"currentColor\" rx=\"1\"/></svg>`;\n\nexport const StretchVerticalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.333 1.333H4c-.736 0-1.333.597-1.333 1.334v10.666c0 .737.597 1.334 1.333 1.334h1.333c.737 0 1.334-.597 1.334-1.334V2.667c0-.737-.597-1.334-1.334-1.334ZM12 1.333h-1.333c-.737 0-1.334.597-1.334 1.334v10.666c0 .737.597 1.334 1.334 1.334H12c.736 0 1.333-.597 1.333-1.334V2.667c0-.737-.597-1.334-1.333-1.334Z\"/></svg>`;\n\nexport const SubscriptIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 3.333 8 8.667M8 3.333 2.667 8.667M13.333 12.667h-2.666c0-1 .293-1.334 1-1.667.706-.333 1.666-.78 1.666-1.667 0-.313-.113-.62-.32-.86a1.407 1.407 0 0 0-1.746-.293c-.28.16-.494.413-.6.713\"/></svg>`;\n\nexport const SuperscriptIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 12.667 8 7.333M8 12.667 2.667 7.333M13.333 8h-2.666c0-1 .294-1.333 1-1.667.705-.333 1.666-.777 1.666-1.665 0-.315-.113-.62-.322-.86a1.403 1.403 0 0 0-1.745-.29c-.28.159-.492.409-.6.706\"/></svg>`;\n\nexport const SwitchIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.667 4H5.333a4 4 0 1 0 0 8h5.334a4 4 0 0 0 0-8Z\"/><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.333 9.333a1.333 1.333 0 1 0 0-2.666 1.333 1.333 0 0 0 0 2.666Z\"/></svg>`;\n\nexport const TabsIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.75 5H13M9.75 5H7.401A1.684 1.684 0 0 1 6 4.25v0l-.5-.75-.5-.75L4.5 2m5.25 3-1-1.5-.5-.75-.5-.75M4.5 2H3v0a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 3 14h10v0a1.5 1.5 0 0 0 1.5-1.5v0-6A1.5 1.5 0 0 0 13 5v0M4.5 2h3.25m0 0h2.375v0c.547 0 1.057.273 1.36.728l.015.022.5.75L13 5\"/></svg>`;\n\nexport const TerminalIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4.667 7.333 6 6 4.667 4.667M7.333 8.667H10\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2Z\"/></svg>`;\n\nexport const TextAlignCenterIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M2 3.5a.5.5 0 0 0 0 1h12a.5.5 0 0 0 0-1H2ZM4.167 8a.5.5 0 0 1 .5-.5h6.666a.5.5 0 0 1 0 1H4.667a.5.5 0 0 1-.5-.5Zm-1.334 4a.5.5 0 0 1 .5-.5h9.334a.5.5 0 0 1 0 1H3.333a.5.5 0 0 1-.5-.5Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const TextAlignJustifyIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M2 3.5a.5.5 0 0 0 0 1h12a.5.5 0 0 0 0-1H2ZM1.5 8a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H2a.5.5 0 0 1-.5-.5Zm0 4a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H2a.5.5 0 0 1-.5-.5Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const TextAlignLeftIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10 8H2M11.333 12H2M14 4H2\"/></svg>`;\n\nexport const TextAlignRightIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M2 3.5a.5.5 0 0 0 0 1h12a.5.5 0 0 0 0-1H2ZM5.5 8a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1H6a.5.5 0 0 1-.5-.5Zm-1.333 4a.5.5 0 0 1 .5-.5H14a.5.5 0 0 1 0 1H4.667a.5.5 0 0 1-.5-.5Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const TextAnimationIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.476 4.3 7.817 3l6.935 1.817-.34 1.3m-6.245 4.386 2.601.682m.517-7.276-1.817 6.935M2 9.938V9h5v.938M3.562 14h1.875M4.5 9v5\"/></svg>`;\n\nexport const TextCapitalizeIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5 10.889 4.389 5.11l2.889 5.778M2.222 9.444h4.334M12.333 10.889a2.167 2.167 0 1 0 0-4.334 2.167 2.167 0 0 0 0 4.334ZM14.5 6.556v4.333\"/></svg>`;\n\nexport const TextItalicIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2.667h-6M9.333 13.333h-6M10 2.667 6 13.333\"/></svg>`;\n\nexport const TextLowercaseIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.937 11.25a2.437 2.437 0 1 0 0-4.875 2.437 2.437 0 0 0 0 4.875ZM6.375 6.375v4.875M12.063 11.25a2.437 2.437 0 1 0 0-4.875 2.437 2.437 0 0 0 0 4.875ZM9.625 4.75v6.5\"/></svg>`;\n\nexport const TextStrikethroughIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.667 2.667H6a2 2 0 0 0-1.887 2.666M9.333 8a2.667 2.667 0 1 1 0 5.333H4M2.667 8h10.666\"/></svg>`;\n\nexport const TextTruncateIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 11V3.333A1.334 1.334 0 0 0 12.667 2H3.333A1.333 1.333 0 0 0 2 3.333v9.334A1.333 1.333 0 0 0 3.333 14H5.5\"/><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 13.375a.375.375 0 1 0 .75 0 .375.375 0 0 0-.75 0ZM10.813 13.375a.375.375 0 1 0 .75 0 .375.375 0 0 0-.75 0ZM13.625 13.375a.375.375 0 1 0 .75 0 .375.375 0 0 0-.75 0Z\"/></svg>`;\n\nexport const TextUnderlineIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 2.667v4a4 4 0 0 0 8 0v-4M2.667 13.333h10.666\"/></svg>`;\n\nexport const TextUppercaseIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M1.5 10.811 4.31 5.19l2.812 5.622M2.202 9.406h4.217M9.933 8h3.162a1.406 1.406 0 1 1 0 2.811H9.933V5.19h2.81a1.406 1.406 0 1 1 0 2.811\"/></svg>`;\n\nexport const TextIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 4.667v-2h10.666v2M6 13.333h4M8 2.667v10.666\"/></svg>`;\n\nexport const TooltipIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 10a1.333 1.333 0 0 1-1.333 1.333h-8L2 14V3.333A1.333 1.333 0 0 1 3.333 2h9.334A1.333 1.333 0 0 1 14 3.333V10Z\"/></svg>`;\n\nexport const TrashIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 4h12M12.667 4v9.333c0 .667-.667 1.334-1.334 1.334H4.667c-.667 0-1.334-.667-1.334-1.334V4M5.333 4V2.667C5.333 2 6 1.333 6.667 1.333h2.666c.667 0 1.334.667 1.334 1.334V4M6.667 7.333v4M9.333 7.333v4\"/></svg>`;\n\nexport const TriggerIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.915 7.354v-.647a1.294 1.294 0 1 0-2.587 0\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.328 6.707V6.06a1.294 1.294 0 1 0-2.587 0v.647\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6.74 6.383V2.826a1.293 1.293 0 1 0-2.586 0v6.467\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M11.914 7.353a1.293 1.293 0 0 1 2.587 0v1.94a5.174 5.174 0 0 1-5.174 5.174H8.034c-1.81 0-2.91-.556-3.874-1.513l-2.328-2.328a1.293 1.293 0 0 1 1.83-1.824L4.8 9.94\"/></svg>`;\n\nexport const UpgradeIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M10.667 8 8 5.333 5.333 8M8 10.667V5.333\"/></svg>`;\n\nexport const UploadIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 8.667V14M2.667 9.933a4.667 4.667 0 1 1 7.806-4.6h1.194a3 3 0 0 1 1.666 5.495\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.333 11.333 8 8.667l2.667 2.666\"/></svg>`;\n\nexport const VideoIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" fill-rule=\"evenodd\" d=\"M1.833 5.333c0-.46.373-.833.834-.833h6.666c.46 0 .834.373.834.833v5.334c0 .46-.374.833-.834.833H2.667a.833.833 0 0 1-.834-.833V5.333ZM2.667 3.5C1.654 3.5.833 4.32.833 5.333v5.334c0 1.012.821 1.833 1.834 1.833h6.666c1.013 0 1.834-.82 1.834-1.833V9.6l2.704 1.803a.833.833 0 0 0 1.296-.693V5.247a.833.833 0 0 0-1.254-.72l-2.746 1.602v-.796c0-1.012-.821-1.833-1.834-1.833H2.667Zm8.5 3.787V8.4l3 2V5.537l-3 1.75Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const ViewportIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M5.173 1.35h.157a.65.65 0 0 1 0 1.3H5.2c-.57 0-.96 0-1.26.025-.294.024-.446.068-.553.122a1.35 1.35 0 0 0-.59.59c-.054.107-.098.259-.122.552-.025.301-.025.69-.025 1.261v.13a.65.65 0 0 1-1.3 0v-.157c0-.537 0-.98.03-1.34.03-.373.095-.715.259-1.036a2.65 2.65 0 0 1 1.158-1.158c.32-.164.663-.229 1.036-.26.36-.029.803-.029 1.34-.029Zm6.888 1.325c-.301-.024-.69-.025-1.261-.025h-.13a.65.65 0 1 1 0-1.3h.157c.537 0 .98 0 1.34.03.373.03.715.095 1.036.259.499.254.904.66 1.158 1.158.164.32.229.663.26 1.036.029.36.029.803.029 1.34v.157a.65.65 0 1 1-1.3 0V5.2c0-.57 0-.96-.025-1.26-.024-.294-.068-.446-.122-.553a1.35 1.35 0 0 0-.59-.59c-.107-.054-.26-.098-.552-.122ZM2 10.02a.65.65 0 0 1 .65.65v.13c0 .57 0 .96.025 1.26.024.294.068.446.122.553.13.254.336.46.59.59.107.054.259.098.552.122.301.025.69.025 1.261.025h.13a.65.65 0 1 1 0 1.3h-.157c-.537 0-.98 0-1.34-.03-.373-.03-.715-.095-1.036-.259a2.65 2.65 0 0 1-1.158-1.158c-.164-.32-.23-.663-.26-1.037-.029-.36-.029-.802-.029-1.34v-.156a.65.65 0 0 1 .65-.65Zm12 0a.65.65 0 0 1 .65.65v.157c0 .537 0 .98-.03 1.34-.03.373-.095.715-.259 1.036a2.65 2.65 0 0 1-1.158 1.158c-.32.164-.663.23-1.037.26-.36.029-.802.029-1.34.029h-.156a.65.65 0 1 1 0-1.3h.13c.57 0 .96 0 1.26-.025.294-.024.446-.068.553-.122.254-.13.46-.336.59-.59.054-.107.098-.259.122-.552.025-.301.025-.69.025-1.261v-.13a.65.65 0 0 1 .65-.65ZM8 10.033a2.033 2.033 0 1 0 0-4.066 2.033 2.033 0 0 0 0 4.066Zm0 1.3a3.333 3.333 0 1 0 0-6.666 3.333 3.333 0 0 0 0 6.666Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const VimeoIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14.993 4.801c-.062 1.363-1.014 3.228-2.855 5.597-1.903 2.473-3.514 3.71-4.83 3.71-.817 0-1.508-.753-2.072-2.26l-1.13-4.143c-.419-1.506-.868-2.259-1.349-2.26a4.332 4.332 0 0 0-1.099.66L1 5.257c.69-.607 1.372-1.214 2.043-1.822.921-.796 1.613-1.215 2.074-1.257 1.09-.105 1.76.64 2.012 2.234.272 1.72.461 2.79.566 3.208.315 1.427.66 2.14 1.038 2.14.292 0 .733-.463 1.32-1.39.586-.925.9-1.63.942-2.113.084-.798-.23-1.198-.942-1.199-.357.005-.71.084-1.036.23.688-2.253 2.002-3.349 3.942-3.285 1.439.042 2.117.975 2.034 2.798Z\"/></svg>`;\n\nexport const WebhookFormIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 11.32H8.007c-.734 0-1.3.627-1.654 1.267a2.666 2.666 0 0 1-5.02-1.254A2.62 2.62 0 0 1 1.713 10\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M4 11.333 6.087 7.48c.353-.647.066-1.453-.334-2.067a2.667 2.667 0 1 1 4.594-2.706\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m8 4 2.087 3.82c.353.647 1.18.847 1.913.847A2.667 2.667 0 0 1 12 14\"/></svg>`;\n\nexport const Webstudio1cIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.206 1.206 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.8-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.206 1.206 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill-rule=\"evenodd\" d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const WebstudioIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"url(#c69f97277__paint0_linear_5232_4740)\" fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint1_linear_5232_4740)\" fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint2_linear_5232_4740)\" fill-opacity=\".8\" fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint3_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint4_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint5_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"m12.32 12.416 2.62-8.085a1.205 1.205 0 1 0-2.292-.746l-2.62 8.084a1.205 1.205 0 1 0 2.292.747Z\" clip-rule=\"evenodd\"/><path fill=\"#E63CFE\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint6_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint7_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint8_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint9_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint10_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint11_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint12_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint13_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M8 7.624c.297 0 .517.175.704.394.207.243.373.545.514.866.634 1.44.753 3.241.753 3.241a1.207 1.207 0 0 0 1.285 1.122 1.207 1.207 0 0 0 1.12-1.287s-.16-2.25-.952-4.05C10.744 6.364 9.594 5.208 8 5.208c-1.594 0-2.744 1.156-3.424 2.7-.792 1.801-.951 4.05-.951 4.05a1.207 1.207 0 0 0 1.12 1.288 1.207 1.207 0 0 0 1.284-1.122s.119-1.8.753-3.24a3.52 3.52 0 0 1 .514-.867c.187-.22.406-.394.704-.394Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint14_linear_5232_4740)\" fill-rule=\"evenodd\" d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint15_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint16_linear_5232_4740)\" fill-rule=\"evenodd\" d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint17_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\" clip-rule=\"evenodd\"/><path fill=\"url(#c69f97277__paint18_radial_5232_4740)\" fill-rule=\"evenodd\" d=\"M5.973 11.669 3.352 3.585a1.205 1.205 0 1 0-2.293.746l2.622 8.084a1.205 1.205 0 1 0 2.292-.746Z\" clip-rule=\"evenodd\"/><defs><radialGradient id=\"c69f97277__paint3_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(11.2336 9.30762) rotate(-64.165) scale(7.2853)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".832\" stop-color=\"#4A4EFA\" stop-opacity=\"0\"/><stop offset=\"1\" stop-color=\"#4A4EFA\" stop-opacity=\".75\"/></radialGradient><radialGradient id=\"c69f97277__paint4_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(11.0923 8.82676) scale(1.62424 3.47362)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".169\" stop-color=\"#11417A\"/><stop offset=\".926\" stop-color=\"#4069D4\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint5_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(12.9724 13.2501) rotate(-90) scale(5.37233 2.57477)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".349\" stop-color=\"#E63CFE\"/><stop offset=\"1\" stop-color=\"#E63CFE\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint6_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(7.59145 9.4488) rotate(-85.7675) scale(5.53509 4.91478)\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#39FBBB\" stop-opacity=\".5\"/><stop offset=\".689\" stop-color=\"#39FBBB\"/><stop offset=\"1\" stop-color=\"#39FBBB\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint7_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(15.4778 12.5651) rotate(-122.629) scale(13.2696 3.2283)\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#E63CFE\"/><stop offset=\"1\" stop-color=\"#E63CFE\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint8_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(7.81141 11.1572) rotate(-85.0198) scale(5.97107 5.13635)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".557\" stop-color=\"#4A4EFA\"/><stop offset=\".935\" stop-color=\"#4A4EFA\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint9_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(6.19224 12.9644) rotate(-68.6367) scale(6.23573 3.72131)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".613\" stop-color=\"#E63CFE\"/><stop offset=\"1\" stop-color=\"#E63CFE\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint10_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(12.2257 8.7415) rotate(176.233) scale(7.74243 19.5729)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".814\" stop-color=\"#4A98D0\" stop-opacity=\"0\"/><stop offset=\".973\" stop-color=\"#11417A\"/></radialGradient><radialGradient id=\"c69f97277__paint11_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(5.72725 13.2501) rotate(-90) scale(5.54893 4.6041)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".458\" stop-color=\"#FFAE3C\"/><stop offset=\"1\" stop-color=\"#FFAE3C\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint12_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(12.4559 13.2501) rotate(-90) scale(5.00159)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".192\" stop-color=\"#E63CFE\"/><stop offset=\"1\" stop-color=\"#E63CFE\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint13_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(9.52162 13.2501) rotate(-90) scale(3.76368 1.78782)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".174\" stop-color=\"#FFAE3C\"/><stop offset=\"1\" stop-color=\"#FFAE3C\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint15_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(3.35867 5.95923) rotate(161.042) scale(1.68855 8.24528)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".12\" stop-color=\"#4A4EFA\"/><stop offset=\"1\" stop-color=\"#4A4EFA\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint17_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(6.03209 13.2501) rotate(-90) scale(5.42506 5.92649)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".465\" stop-color=\"#FFAE3C\"/><stop offset=\".926\" stop-color=\"#FFAE3C\" stop-opacity=\"0\"/></radialGradient><radialGradient id=\"c69f97277__paint18_radial_5232_4740\" cx=\"0\" cy=\"0\" r=\"1\" gradientTransform=\"translate(6.03209 13.2501) rotate(-90) scale(11.8703)\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".844\" stop-color=\"#E63CFE\" stop-opacity=\"0\"/><stop offset=\"1\" stop-color=\"#E63CFE\"/></radialGradient><linearGradient id=\"c69f97277__paint0_linear_5232_4740\" x1=\"16.343\" x2=\"12.418\" y1=\".102\" y2=\"12.794\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#4A4EFA\"/><stop offset=\".549\" stop-color=\"#E63CFE\"/></linearGradient><linearGradient id=\"c69f97277__paint1_linear_5232_4740\" x1=\"11.413\" x2=\"13.636\" y1=\"7.419\" y2=\"8.138\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".16\" stop-color=\"#4A4EFA\"/><stop offset=\".946\" stop-color=\"#4A4EFA\" stop-opacity=\"0\"/></linearGradient><linearGradient id=\"c69f97277__paint2_linear_5232_4740\" x1=\"11.75\" x2=\"13.147\" y1=\"6.351\" y2=\"6.798\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#39FBBB\"/><stop offset=\"1\" stop-color=\"#39FBBB\" stop-opacity=\"0\"/></linearGradient><linearGradient id=\"c69f97277__paint14_linear_5232_4740\" x1=\".588\" x2=\"3.972\" y1=\"3.255\" y2=\"13.784\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".17\" stop-color=\"#E63CFE\"/><stop offset=\".709\" stop-color=\"#FFAE3C\"/></linearGradient><linearGradient id=\"c69f97277__paint16_linear_5232_4740\" x1=\"4.844\" x2=\"4.37\" y1=\"8.176\" y2=\"8.331\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#E63CFE\" stop-opacity=\".33\"/><stop offset=\"1\" stop-color=\"#E63CFE\" stop-opacity=\"0\"/></linearGradient></defs></svg>`;\n\nexport const WindowInfoIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 2H3.333C2.597 2 2 2.597 2 3.333v9.334C2 13.403 2.597 14 3.333 14h9.334c.736 0 1.333-.597 1.333-1.333V3.333C14 2.597 13.403 2 12.667 2ZM8 11.516V7.853\"/><rect width=\"1.4\" height=\"1.4\" x=\"7.3\" y=\"4.484\" fill=\"currentColor\" rx=\".7\"/></svg>`;\n\nexport const WindowTitleIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.667 4H3.333C2.597 4 2 4.497 2 5.111v7.778C2 13.503 2.597 14 3.333 14h9.334c.736 0 1.333-.498 1.333-1.111V5.11C14 4.497 13.403 4 12.667 4ZM4 2h8\"/></svg>`;\n\nexport const WrapIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3.49 2.237h-.98c-.542 0-.982.403-.982.9V6.07c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9V3.137c0-.497-.44-.9-.981-.9ZM3.49 9.03h-.98c-.542 0-.982.403-.982.9v2.933c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9V9.93c0-.497-.44-.9-.981-.9ZM8.49 2.237h-.98c-.542 0-.982.403-.982.9V6.07c0 .497.44.9.981.9h.982c.542 0 .981-.403.981-.9V3.137c0-.497-.44-.9-.981-.9ZM7 11.397H13.641a.862.862 0 0 0 .862-.862v0-5.173 0a.862.862 0 0 0-.862-.862H12m-5 6.897 2-2m-2 2 2 2\"/></svg>`;\n\nexport const XAxisRotateIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 17\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9.473 12.528c-.579 1.944-1.563 3.222-2.681 3.222-1.78 0-3.223-3.246-3.223-7.25s1.442-7.25 3.223-7.25c1.78 0 3.222 3.246 3.222 7.25\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M7.597 6.083 10.014 8.5l2.417-2.417\"/></svg>`;\n\nexport const XAxisIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.636 2.68v8.275h8.275M2.09 14.502l3.546-3.547\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m12.134 8.591 2.364 2.364-2.364 2.365\"/></svg>`;\n\nexport const XCircledFilledIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 14.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Z\"/><path stroke=\"#fff\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m5.333 10.667 5.334-5.334M10.667 10.667 5.333 5.333\"/></svg>`;\n\nexport const XLogoIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"#000\" d=\"M9.142 7.081 13.609 2H12.55L8.671 6.412 5.573 2H2l4.685 6.672L2 14h1.059l4.096-4.66L10.427 14H14L9.141 7.081Zm-1.45 1.65-.475-.665L3.44 2.78h1.626l3.048 4.266.475.664 3.962 5.546h-1.626L7.692 8.73Z\"/></svg>`;\n\nexport const XSmallIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"#000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m5.333 10.667 5.334-5.334M10.667 10.667 5.333 5.333\"/></svg>`;\n\nexport const XIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"/></svg>`;\n\nexport const XmlIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.163 9.02v-4L8.83 1.686h-5.5A1.333 1.333 0 0 0 1.997 3.02v6\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8.164 2.186v1.667a1.333 1.333 0 0 0 1.333 1.333h2.667M1.997 11.314l3 3M4.997 11.314l-3 3M6.997 14.314v-3l1.5 1.523 1.5-1.523v3M12.163 11.314v3h1.84\"/></svg>`;\n\nexport const YAxisRotateIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 17\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.028 9.973c1.944-.579 3.222-1.563 3.222-2.681 0-1.78-3.246-3.223-7.25-3.223S.75 5.511.75 7.292c0 1.78 3.246 3.222 7.25 3.222\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.583 8.097 8 10.514 5.583 12.93\"/></svg>`;\n\nexport const YAxisIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.636 2.68v8.275h8.275M2.09 14.502l3.546-3.547\"/><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m3.272 3.862 2.364-2.364L8 3.862\"/></svg>`;\n\nexport const Youtube1cIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill-rule=\"evenodd\" d=\"M13.47 3.299a1.771 1.771 0 0 1 1.238 1.263C15 5.675 15 8 15 8s0 2.325-.293 3.438a1.771 1.771 0 0 1-1.238 1.263C12.38 13 8 13 8 13s-4.378 0-5.47-.299a1.771 1.771 0 0 1-1.237-1.263C1 10.325 1 8 1 8s0-2.325.293-3.438A1.771 1.771 0 0 1 2.53 3.299C3.622 3 8 3 8 3s4.378 0 5.47.299Zm-3.232 4.7L6.6 10.144V5.857L10.238 8Z\" clip-rule=\"evenodd\"/></svg>`;\n\nexport const YoutubeIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path d=\"M.892 11.528a18.047 18.047 0 0 1 0-7.482A1.496 1.496 0 0 1 1.94 3a37.082 37.082 0 0 1 12.12 0 1.497 1.497 0 0 1 1.048 1.047 18.046 18.046 0 0 1 0 7.482 1.497 1.497 0 0 1-1.048 1.048 37.077 37.077 0 0 1-12.12 0 1.497 1.497 0 0 1-1.048-1.048Z\"/><path d=\"m6.5 10.3 4-2.4-4-2.4v4.8Z\"/></svg>`;\n\nexport const ZAxisRotateIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 17\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M14 9.28a5.993 5.993 0 0 1-2.007 3.736 6.04 6.04 0 0 1-7.962-.024 5.98 5.98 0 0 1-.988-7.867A6.025 6.025 0 0 1 6.526 2.69a6.049 6.049 0 0 1 4.23.462 6.01 6.01 0 0 1 2.868 3.129M14 2.53v3.75h-3.766\"/></svg>`;\n\nexport const ZAxisIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5.636 2.09v8.274h8.275M2.09 13.91l3.546-3.546m-3.547 3.547h3.547m-3.547 0v-3.744\"/></svg>`;\n"
  },
  {
    "path": "packages/icons/src/index.stories.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport type { Meta } from \"@storybook/react\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport * as icons from \"./index\";\n\nexport const Icons = ({\n  testColor = false,\n}: {\n  testColor?: boolean;\n}): ReactNode => {\n  return (\n    <StorySection title=\"Icons\">\n      <div\n        style={{\n          display: \"flex\",\n          flexWrap: \"wrap\",\n          ...(testColor ? { color: \"red\" } : {}),\n        }}\n      >\n        {Object.entries(icons).map(\n          // eslint-disable-next-line @typescript-eslint/no-explicit-any\n          ([name, Icon]: [string, any]) => {\n            if (name.endsWith(\"Icon\") === false) {\n              return null;\n            }\n            return (\n              <div\n                key={name}\n                style={{\n                  width: 150,\n                  height: 100,\n                  margin: 5,\n                  padding: 5,\n                  border: \"solid 1px #f5f5f5\",\n                  display: \"flex\",\n                  flexDirection: \"column\",\n                  alignItems: \"center\",\n                  justifyContent: \"center\",\n                }}\n              >\n                <Icon width=\"32\" height=\"32\" color=\"black\" />\n                <div\n                  style={{\n                    marginTop: 16,\n                    fontFamily: \"Arial\",\n                    textAlign: \"center\",\n                    wordWrap: \"break-word\",\n                    width: \"100%\",\n                    fontSize: \"14px\",\n                    color: \"#5a5a5a\",\n                  }}\n                >\n                  {name.replace(/Icon$/, \"\")}\n                </div>\n              </div>\n            );\n          }\n        )}\n      </div>\n    </StorySection>\n  );\n};\n\nconst IconsMeta: Meta<typeof Icons> = {\n  title: \"Icons\",\n  component: Icons,\n};\nexport default IconsMeta;\n"
  },
  {
    "path": "packages/icons/src/index.ts",
    "content": "export * from \"./__generated__/components\";\nexport * from \"./types\";\n"
  },
  {
    "path": "packages/icons/src/types.ts",
    "content": "import type {\n  SVGAttributes,\n  RefAttributes,\n  ForwardRefExoticComponent,\n} from \"react\";\n\nexport interface IconProps extends SVGAttributes<SVGElement> {\n  children?: never;\n  color?: string;\n  size?: number | string;\n}\n\nexport type IconComponent = ForwardRefExoticComponent<\n  IconProps & RefAttributes<SVGSVGElement>\n>;\n\nexport type IconRecord = Record<string, IconComponent>;\nexport type IconRecords = Record<string, IconRecord>;\n"
  },
  {
    "path": "packages/icons/svg-string.ts",
    "content": "import { readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, extname } from \"node:path\";\nimport { createHash } from \"node:crypto\"; // Import createHash\nimport { type Config, optimize } from \"svgo\";\n\nconst pascalcase = (string: string) => {\n  return string\n    .replace(/[^A-Z0-9]+([A-Z0-9])?/gi, (_invalid, char) =>\n      char == null ? \"\" : char.toUpperCase()\n    )\n    .replace(/^[a-z]/, (char) => char.toUpperCase());\n};\n\nconst transformComponentName = (filename: string) => {\n  const name = basename(filename, extname(filename));\n  // digits cannot start variable name\n  return `${pascalcase(name).replace(/^[0-9]/, (char) => `_${char}`)}Icon`;\n};\n\nconst prefixCache = new Map<string, string>();\nconst getHashedPrefix = (path?: string) => {\n  if (path === undefined) {\n    return \"svg\";\n  }\n  const cached = prefixCache.get(path);\n  if (cached !== undefined) {\n    return cached;\n  }\n  const hash = createHash(\"sha256\").update(path).digest(\"hex\").slice(0, 8);\n  prefixCache.set(path, hash);\n  return hash;\n};\n\nconst plugins: Config[\"plugins\"] = [\n  {\n    name: \"preset-default\",\n    params: {\n      overrides: {\n        // preserve viewBox\n        removeViewBox: false,\n        convertTransform: false,\n        inlineStyles: false,\n        cleanupIds: false,\n      },\n    },\n  },\n  // convert width/height to viewBox if missing\n  { name: \"removeDimensions\" },\n  {\n    name: \"prefixIds\",\n    params: {\n      prefix: (_node: unknown, info: { path?: string }) =>\n        getHashedPrefix(info.path),\n      prefixClassNames: false,\n    },\n  },\n  {\n    name: \"addAttributesToSVGElement\",\n    params: {\n      attributes: [\n        {\n          fill: \"currentColor\",\n          width: \"100%\",\n          height: \"100%\",\n          style: \"display: block;\",\n        },\n      ],\n    },\n  },\n];\n\nlet moduleContent = \"\";\n\nfor (const name of await readdir(\"./icons\")) {\n  if (name.endsWith(\".svg\")) {\n    const exportName = transformComponentName(name);\n    const content = await readFile(`./icons/${name}`, \"utf-8\");\n    const { data: optimized } = optimize(content, {\n      path: `./icons/${name}`,\n      plugins,\n    });\n    moduleContent += `export const ${exportName} = \\`${optimized.trim()}\\`;\\n`;\n  }\n}\n\nawait writeFile(\"./src/__generated__/svg.ts\", moduleContent);\n"
  },
  {
    "path": "packages/icons/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"**/*.stories.tsx\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/icons/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"],\n  \"compilerOptions\": {\n    \"isolatedDeclarations\": true\n  }\n}\n"
  },
  {
    "path": "packages/image/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/image\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Image optimization\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\",\n    \"build\": \"rm -rf lib && esbuild src/index.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"warn-once\": \"^0.1.1\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/design-system\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vitest\": \"^3.1.2\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\",\n    \"types\": \"./lib/types/index.d.ts\",\n    \"import\": \"./lib/index.js\"\n  },\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/image/src/image-dev.stories.tsx",
    "content": "// Story for image development, see https://github.com/webstudio-is/webstudio/issues/387\n\nimport type * as React from \"react\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { Image as ImagePrimitive, wsImageLoader } from \"./\";\n\n// to not allow include local assets everywhere, just enable it for this file\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore\nimport localLogoImage from \"../storybook-assets/logo.webp\";\n\nexport default {\n  title: \"Image dev\",\n};\n\ntype ImageProps = React.ComponentProps<typeof ImagePrimitive>;\n\n/**\n * In case you need to test img with real cloudflare trasforms\n * set  USE_CLOUDFLARE_IMAGE_TRANSFORM = true\n **/\nconst USE_CLOUDFLARE_IMAGE_TRANSFORM = false;\n\n// For cloudflare image transform testing, logo should be the most consistent image on the project\nconst REMOTE_SELF_DOMAIN_IMAGE = \"https://webstudio.is/logo.webp\";\n\nconst imageSrc = USE_CLOUDFLARE_IMAGE_TRANSFORM\n  ? REMOTE_SELF_DOMAIN_IMAGE\n  : localLogoImage;\n\nconst ImageBase = (\n  args: Omit<ImageProps, \"loader\"> & {\n    style?: React.HTMLAttributes<\"img\">[\"style\"];\n  }\n) => {\n  const style = {\n    maxWidth: \"100%\",\n    display: \"block\",\n    ...args.style,\n  };\n\n  return (\n    <ImagePrimitive\n      {...args}\n      optimize={true}\n      loader={wsImageLoader}\n      style={style}\n    />\n  );\n};\n\nconst SectionTitle = ({ children }: { children: React.ReactNode }) => (\n  <h3 style={{ marginTop: 24, marginBottom: 8 }}>{children}</h3>\n);\n\n/**\n * All image variants demonstrated together.\n **/\nexport const ImageDev = () => (\n  <StorySection title=\"Image Dev\">\n    <SectionTitle>Fixed width image</SectionTitle>\n    <p>Load images depending on image width and device per pixel ratio.</p>\n    <ImageBase src={imageSrc} width=\"300\" height=\"400\" />\n\n    <SectionTitle>Fixed width image (cover)</SectionTitle>\n    <p>\n      Preserve ratio using object-fit: cover. Load images depending on image\n      width and device per pixel ratio.\n    </p>\n    <ImageBase\n      src={imageSrc}\n      width=\"300\"\n      height=\"400\"\n      style={{ objectFit: \"cover\" }}\n    />\n\n    <SectionTitle>Unknown width image</SectionTitle>\n    <p>Load images depending on the viewport width.</p>\n    <ImageBase src={imageSrc} />\n\n    <SectionTitle>Aspect ratio image</SectionTitle>\n    <p>\n      Fit width of the parent container, has own aspect-ratio and\n      object-fit=cover. Load images depending on the viewport width.\n    </p>\n    <div style={{ width: \"50%\" }}>\n      <ImageBase\n        src={imageSrc}\n        style={{ aspectRatio: \"2/1\", objectFit: \"cover\", width: \"100%\" }}\n      />\n    </div>\n\n    <SectionTitle>Fill parent image</SectionTitle>\n    <p>\n      Fill width and height of the relative parent container, object-fit=cover.\n      Load images depending on the viewport width.\n    </p>\n    <div style={{ width: \"50%\", aspectRatio: \"2/1\", position: \"relative\" }}>\n      <ImageBase\n        src={imageSrc}\n        style={{\n          objectFit: \"cover\",\n          position: \"absolute\",\n          width: \"100%\",\n          height: \"100%\",\n        }}\n      />\n    </div>\n\n    <SectionTitle>Hero image</SectionTitle>\n    <p>\n      &quot;sizes&quot; attribute explicitly equal to 100vw allowing to skip the\n      default behavior. Load images depending on the viewport width.\n    </p>\n    <ImageBase\n      src={imageSrc}\n      sizes=\"100vw\"\n      style={{\n        aspectRatio: \"3/1\",\n        objectFit: \"cover\",\n        width: \"100%\",\n      }}\n    />\n  </StorySection>\n);\n"
  },
  {
    "path": "packages/image/src/image-loader.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { wsImageLoader } from \"./image-loaders\";\n\nconst decodePathFragment = (fragment: string) => {\n  return decodeURIComponent(fragment);\n};\n\nconst encodePathFragment = (fragment: string) => {\n  return encodeURIComponent(fragment).replace(/%2F/g, \"/\");\n};\n\ndescribe(\"Asset image transforms\", () => {\n  test(\"width is number\", () => {\n    const imageBaseUrl = \"/cgi/image/\";\n\n    const assetName = \"Привет_Мир__2F__BQNEuP8O9N79eVwPfbBJg.webp\";\n    const result = wsImageLoader({\n      width: 128,\n      src: assetName,\n      quality: 100,\n    });\n\n    const resultUrl = new URL(result, \"https://any-domain.any\");\n\n    expect(result).toMatchInlineSnapshot(\n      `\"/cgi/image/%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82_%D0%9C%D0%B8%D1%80__2F__BQNEuP8O9N79eVwPfbBJg.webp?width=128&quality=100&format=auto\"`\n    );\n\n    expect(\n      decodePathFragment(resultUrl.pathname.slice(imageBaseUrl.length))\n    ).toBe(assetName);\n  });\n\n  test(\"strip /cgi/asset from src\", () => {\n    expect(\n      wsImageLoader({\n        width: 128,\n        src: \"/cgi/asset/my-image.webp\",\n        quality: 100,\n      })\n    ).toEqual(\"/cgi/image/my-image.webp?width=128&quality=100&format=auto\");\n  });\n});\n\ndescribe(\"Remote src image transforms\", () => {\n  test(\"width is number\", () => {\n    const imageBaseUrl = \"/cgi/image/\";\n\n    const remoteSrc = \"https://example.com/lo%3Fgo.webp?a=1\";\n\n    const result = wsImageLoader({\n      width: 128,\n      src: remoteSrc,\n      quality: 100,\n    });\n\n    const resultUrl = new URL(result, \"https://any-domain.any\");\n\n    expect(\n      decodePathFragment(resultUrl.pathname.slice(imageBaseUrl.length))\n    ).toBe(remoteSrc);\n\n    expect(resultUrl.searchParams.get(\"width\")).toBe(\"128\");\n\n    expect(result).toMatchInlineSnapshot(\n      `\"/cgi/image/https%3A//example.com/lo%253Fgo.webp%3Fa%3D1?width=128&quality=100&format=auto\"`\n    );\n  });\n\n  test(\"Double encoded fragment\", () => {\n    const imageBaseUrl = \"/cgi/image/\";\n\n    const remoteSrc = encodePathFragment(\n      \"https://ex%2Fample.com/lo%3Fgo.webp?a=1\"\n    );\n\n    const result = wsImageLoader({\n      width: 128,\n      src: remoteSrc,\n      quality: 100,\n    });\n\n    const resultUrl = new URL(result, \"https://any-domain.any\");\n\n    expect(\n      decodePathFragment(resultUrl.pathname.slice(imageBaseUrl.length))\n    ).toBe(remoteSrc);\n\n    expect(resultUrl.searchParams.get(\"width\")).toBe(\"128\");\n  });\n});\n"
  },
  {
    "path": "packages/image/src/image-loaders.ts",
    "content": "import warnOnce from \"warn-once\";\nimport { allSizes, type ImageLoader } from \"./image-optimize\";\n\nconst NON_EXISTING_DOMAIN = \"https://a3cbcbec-cdb1-4ea4-ad60-43c795308ddc.ddc\";\n\nconst joinPath = (...segments: string[]) => {\n  return segments\n    .filter((segment) => segment !== \"\") // Remove empty segments\n    .map((segment) => segment.replace(/(^\\/+|\\/+$)/g, \"\")) // Remove leading and trailing slashes from each segment\n    .join(\"/\");\n};\n\nconst encodePathFragment = (fragment: string) => {\n  return encodeURIComponent(fragment).replace(/%2F/g, \"/\");\n};\n\n/**\n * Default image loader in case of no loader provided\n * https://developers.cloudflare.com/images/image-resizing/url-format/\n **/\nexport const wsImageLoader: ImageLoader = (props) => {\n  const width = props.format === \"raw\" ? 16 : props.width;\n  const quality = props.format === \"raw\" ? 100 : props.quality;\n\n  if (process.env.NODE_ENV !== \"production\") {\n    warnOnce(\n      allSizes.includes(width) === false,\n      \"Width must be only from allowed values\"\n    );\n  }\n\n  // support both \"/cgi/asset/name\" and \"name\" as inputs\n  let src = props.src;\n  if (src.startsWith(\"/cgi/asset\")) {\n    src = src.slice(\"/cgi/asset\".length);\n  }\n\n  const resultUrl = new URL(\"/cgi/image/\", NON_EXISTING_DOMAIN);\n\n  if (props.format !== \"raw\") {\n    resultUrl.searchParams.set(\"width\", width.toString());\n    resultUrl.searchParams.set(\"quality\", quality.toString());\n\n    if (props.height != null) {\n      resultUrl.searchParams.set(\"height\", props.height.toString());\n    }\n\n    if (props.fit != null) {\n      resultUrl.searchParams.set(\"fit\", props.fit);\n    }\n  }\n  resultUrl.searchParams.set(\"format\", props.format ?? \"auto\");\n\n  resultUrl.pathname = joinPath(resultUrl.pathname, encodePathFragment(src));\n\n  if (resultUrl.href.startsWith(NON_EXISTING_DOMAIN)) {\n    return `${resultUrl.pathname}?${resultUrl.searchParams.toString()}`;\n  }\n\n  // Cloudflare docs say that we don't need to urlencode the path params\n  return resultUrl.href;\n};\n\nexport type VideoLoader = (options: { src: string }) => string;\n\nexport const wsVideoLoader: VideoLoader = ({ src }) => {\n  if (src.startsWith(\"/cgi/asset/\")) {\n    src = src.slice(\"/cgi/asset/\".length);\n  }\n  return `/cgi/video/${src}`;\n};\n"
  },
  {
    "path": "packages/image/src/image-optimize.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { getImageAttributes } from \"./image-optimize\";\nimport { wsImageLoader } from \"./image-loaders\";\n\ndescribe(\"Image optimizations applied\", () => {\n  test(\"width is number, create pixel density descriptor 'x'\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: 100,\n      src: \"logo.webp\",\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"sizes\": \"100vw\",\n        \"src\": \"/cgi/image/logo.webp?width=256&quality=100&format=auto\",\n        \"srcSet\": \"/cgi/image/logo.webp?width=16&quality=100&format=auto 16w, /cgi/image/logo.webp?width=32&quality=100&format=auto 32w, /cgi/image/logo.webp?width=48&quality=100&format=auto 48w, /cgi/image/logo.webp?width=64&quality=100&format=auto 64w, /cgi/image/logo.webp?width=96&quality=100&format=auto 96w, /cgi/image/logo.webp?width=128&quality=100&format=auto 128w, /cgi/image/logo.webp?width=256&quality=100&format=auto 256w\",\n      }\n    `);\n  });\n\n  test(\"width is number, url is absolute, create pixel density descriptor 'x'\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: 100,\n      src: \"https://webstudio.is/logo.webp\",\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"sizes\": \"100vw\",\n        \"src\": \"/cgi/image/https%3A//webstudio.is/logo.webp?width=256&quality=100&format=auto\",\n        \"srcSet\": \"/cgi/image/https%3A//webstudio.is/logo.webp?width=16&quality=100&format=auto 16w, /cgi/image/https%3A//webstudio.is/logo.webp?width=32&quality=100&format=auto 32w, /cgi/image/https%3A//webstudio.is/logo.webp?width=48&quality=100&format=auto 48w, /cgi/image/https%3A//webstudio.is/logo.webp?width=64&quality=100&format=auto 64w, /cgi/image/https%3A//webstudio.is/logo.webp?width=96&quality=100&format=auto 96w, /cgi/image/https%3A//webstudio.is/logo.webp?width=128&quality=100&format=auto 128w, /cgi/image/https%3A//webstudio.is/logo.webp?width=256&quality=100&format=auto 256w\",\n      }\n    `);\n  });\n\n  test(\"width is undefined, create 'w' descriptor and sizes prop\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: undefined,\n      src: \"logo.webp\",\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 90,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"sizes\": \"(min-width: 1280px) 50vw, 100vw\",\n        \"src\": \"/cgi/image/logo.webp?width=3840&quality=90&format=auto\",\n        \"srcSet\": \"/cgi/image/logo.webp?width=384&quality=90&format=auto 384w, /cgi/image/logo.webp?width=640&quality=90&format=auto 640w, /cgi/image/logo.webp?width=750&quality=90&format=auto 750w, /cgi/image/logo.webp?width=828&quality=90&format=auto 828w, /cgi/image/logo.webp?width=1080&quality=90&format=auto 1080w, /cgi/image/logo.webp?width=1200&quality=90&format=auto 1200w, /cgi/image/logo.webp?width=1920&quality=90&format=auto 1920w, /cgi/image/logo.webp?width=2048&quality=90&format=auto 2048w, /cgi/image/logo.webp?width=3840&quality=90&format=auto 3840w\",\n      }\n    `);\n  });\n\n  test(\"width is undefined and size defined, creates 'w' descriptor and use input sizes props\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: undefined,\n      src: \"logo.webp\",\n      srcSet: undefined,\n      sizes: \"100vw\",\n      quality: 70,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"sizes\": \"100vw\",\n        \"src\": \"/cgi/image/logo.webp?width=3840&quality=70&format=auto\",\n        \"srcSet\": \"/cgi/image/logo.webp?width=640&quality=70&format=auto 640w, /cgi/image/logo.webp?width=750&quality=70&format=auto 750w, /cgi/image/logo.webp?width=828&quality=70&format=auto 828w, /cgi/image/logo.webp?width=1080&quality=70&format=auto 1080w, /cgi/image/logo.webp?width=1200&quality=70&format=auto 1200w, /cgi/image/logo.webp?width=1920&quality=70&format=auto 1920w, /cgi/image/logo.webp?width=2048&quality=70&format=auto 2048w, /cgi/image/logo.webp?width=3840&quality=70&format=auto 3840w\",\n      }\n    `);\n  });\n\n  test(\"width is undefined and size defined, creates 'w' descriptor and use input sizes props, resizeOrigin defined\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: undefined,\n      src: \"logo.webp\",\n      srcSet: undefined,\n      sizes: \"100vw\",\n      quality: 70,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"sizes\": \"100vw\",\n        \"src\": \"/cgi/image/logo.webp?width=3840&quality=70&format=auto\",\n        \"srcSet\": \"/cgi/image/logo.webp?width=640&quality=70&format=auto 640w, /cgi/image/logo.webp?width=750&quality=70&format=auto 750w, /cgi/image/logo.webp?width=828&quality=70&format=auto 828w, /cgi/image/logo.webp?width=1080&quality=70&format=auto 1080w, /cgi/image/logo.webp?width=1200&quality=70&format=auto 1200w, /cgi/image/logo.webp?width=1920&quality=70&format=auto 1920w, /cgi/image/logo.webp?width=2048&quality=70&format=auto 2048w, /cgi/image/logo.webp?width=3840&quality=70&format=auto 3840w\",\n      }\n    `);\n  });\n\n  test(\"custom loader\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: undefined,\n      src: \"https://webstudio.is/logo.webp\",\n      srcSet: undefined,\n      sizes: \"100vw\",\n      quality: 70,\n      loader: (props) =>\n        `${new URL(props.src).pathname}?w=${\n          props.format !== \"raw\" ? props.width : 0\n        }&q=${props.format !== \"raw\" ? props.quality : 0}`,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"sizes\": \"100vw\",\n        \"src\": \"/logo.webp?w=3840&q=70\",\n        \"srcSet\": \"/logo.webp?w=640&q=70 640w, /logo.webp?w=750&q=70 750w, /logo.webp?w=828&q=70 828w, /logo.webp?w=1080&q=70 1080w, /logo.webp?w=1200&q=70 1200w, /logo.webp?w=1920&q=70 1920w, /logo.webp?w=2048&q=70 2048w, /logo.webp?w=3840&q=70 3840w\",\n      }\n    `);\n  });\n});\n\ndescribe(\"Image optimizations not applied\", () => {\n  test(\"optimize is false\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: false,\n      width: 100,\n      src: \"logo.webp\",\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"src\": \"/cgi/image/logo.webp?format=raw\",\n      }\n    `);\n  });\n\n  test(\"optimize is false and url absolute\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: false,\n      width: 100,\n      src: \"https://webstudio.is/logo.webp\",\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"src\": \"https://webstudio.is/logo.webp\",\n      }\n    `);\n  });\n\n  test(\"srcSet is defined\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: 100,\n      src: \"https://webstudio.is/logo.webp\",\n      srcSet: \"user-defined-srcset\",\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`\n      {\n        \"src\": \"https://webstudio.is/logo.webp\",\n        \"srcSet\": \"user-defined-srcset\",\n      }\n    `);\n  });\n\n  test(\"src is empty\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: 100,\n      src: \"\",\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`undefined`);\n  });\n\n  test(\"src is undefined\", () => {\n    const imgAttr = getImageAttributes({\n      optimize: true,\n      width: 100,\n      src: undefined,\n      srcSet: undefined,\n      sizes: undefined,\n      quality: 100,\n      loader: wsImageLoader,\n    });\n\n    expect(imgAttr).toMatchInlineSnapshot(`undefined`);\n  });\n});\n"
  },
  {
    "path": "packages/image/src/image-optimize.ts",
    "content": "/**\n * # Responsive Image component helpers.\n *\n * ## Quick summary about img srcset and sizes attributes:\n *\n * There are 2 ways to define what image will be loaded in the img property srcset.\n *\n * 1. via pixel density descriptor 'x', like `srcset=\"photo-small.jpg 1x, photo-medium.jpg 1.5x, photo-huge.jpg 2x\"`\n *   src will be selected depending on `device-pixel-ratio`.\n *\n * 2. via viewport width descriptor 'w' and sizes property containing source size descriptors, like\n *   `srcset=\"photo-small.jpg 320w, photo-medium.jpg 640w, photo-huge.jpg 1280w\"`\n *   `sizes=\"(max-width: 600px) 400px, (max-width: 1200px) 70vw, 50vw\"`\n *\n *   The browser finds the first matching media query from source size descriptors,\n *   then use source size value to generate internally srcset\n *   with pixel density descriptors dividing width descriptor value by source size value.\n *\n *   Using the example above for viewport width 800px.\n *   The first matching media query is (max-width: 1200px)\n *   source size value is 70vw  equal to 800px * 0,7 = 560px\n *\n *   browser internal srcset will be (we divide `w` descriptor by source size value):\n *   photo-small.jpg 320w/560px, photo-medium.jpg 640w/560px, photo-huge.jpg 1280w/560px =>\n *   photo-small.jpg 0.57x, photo-medium.jpg 1.14x, photo-huge.jpg 2.28x\n *\n *   Finally same rules as for pixel density descriptor 'x' are applied.\n *\n * ## Algorithm (without optimizations):\n *\n * We have a predefined array of all supported image sizes allSizes, this is the real width of an image in pixels.\n * This is good for caching, as we can cache image with specific width and then use it for different devices.\n *\n * > allSizes array is a tradeoff between cache and the best possible image size you deliver to the user.\n * > If allSizes.length is too small, you will deliver too big images to the user,\n * > if allSizes.length is too big, you will have many caches misses.\n *\n * If img has a defined width property.\n *   1. filter allSizes to exclude loading images higher that maxDevicePixelRatio * img.width\n *\n *\n * If img has no defined width property.\n *   1. Generate srcset = allSizes.map((w) => `${getImageSrcAtWidth(w)} ${w}w`)\n *   2. Use sizes property, or if it is not defined use opinionated DEFAULT_SIZES = \"(min-width: 1280px) 50vw, 100vw\";\n *\n * Optimizations applied now:\n *\n * - If the sizes property is defined, we can exclude from `srcsets` all images\n *   which are smaller than the `smallestRatio * smallesDeviceSize`\n *\n * Future (not implemented) optimizations and improvements:\n *\n * - Knowing image size on different viewport widths we can provide nondefault sizes property\n * - Knowledge of Image aspect-ratio would allow cropping images serverside.\n * - Early hints for high priority images https://blog.cloudflare.com/early-hints/\n * - Slow networks optimizations\n * - 404 etc processing with CSS - https://bitsofco.de/styling-broken-images/ (has some opinionated issues) or js solution with custom user fallback.\n *\n * # Attributions\n *\n * The MIT License (MIT)\n *\n * applies to:\n *\n * - https://github.com/vercel/next.js, Copyright (c) 2022 Vercel, Inc.\n *\n * The MIT License (MIT)\n *\n * Copyright (c) 2022 Vercel, Inc.\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software\n * and associated documentation files (the \"Software\"), to deal in the Software without restriction,\n * including without limitation the rights to use, copy, modify, merge, publish, distribute,\n * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software\n * is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies\n * or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,\n * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,\n * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\n * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n **/\n\nexport type ImageLoader = (\n  props:\n    | {\n        width: number;\n        quality: number;\n        src: string;\n        format?: \"auto\";\n        height?: number;\n        fit?: \"pad\";\n      }\n    | { src: string; format: \"raw\" }\n) => string;\n\n/**\n * max(...imageSizes) must be less then min(...deviceSizes)\n **/\nconst imageSizes = [16, 32, 48, 64, 96, 128, 256, 384];\nconst deviceSizes = [640, 750, 828, 1080, 1200, 1920, 2048, 3840];\n\nexport const allSizes: number[] = [...imageSizes, ...deviceSizes];\n\n/**\n * https://github.com/vercel/next.js/blob/canary/packages/next/client/image.tsx\n **/\nconst getWidths = (\n  width: number | undefined,\n  sizes: string | undefined\n): { widths: number[]; kind: \"w\" | \"x\" } => {\n  if (sizes) {\n    // Find all the \"vw\" percent sizes used in the sizes prop\n    const viewportWidthRe = /(^|\\s)(1?\\d?\\d)vw/g;\n    const percentSizes = [];\n    for (let match; (match = viewportWidthRe.exec(sizes)); match) {\n      percentSizes.push(Number.parseInt(match[2], 10));\n    }\n\n    if (percentSizes.length) {\n      // we can exclude from srcSets all images which are smaller than the smallestRatio * smallesDeviceSize\n      const smallestRatio = Math.min(...percentSizes) * 0.01;\n      return {\n        widths: allSizes.filter(\n          (size) => size >= deviceSizes[0] * smallestRatio\n        ),\n        kind: \"w\",\n      };\n    }\n    return { widths: allSizes, kind: \"w\" };\n  }\n\n  if (width == null) {\n    return { widths: deviceSizes, kind: \"w\" };\n  }\n\n  // Max device pixel ratio capped at 2; higher ratios offer negligible benefits\n  // See Twitter Engineering's article on capping image fidelity: https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html\n  const MAX_DEVICE_PIXEL_RATIO = 2;\n\n  let index = allSizes.findIndex(\n    (size) => size >= MAX_DEVICE_PIXEL_RATIO * width\n  );\n  index = index < 0 ? allSizes.length : index;\n\n  return {\n    widths: allSizes.slice(0, index + 1),\n    kind: \"w\",\n  };\n\n  /*\n  // Leave it here for future optimisations - icon like images\n\n  const widths = [\n    ...new Set(\n      [width, width * 2].map(\n        (w) => allSizes.find((p) => p >= w) || allSizes[allSizes.length - 1]\n      )\n    ),\n  ];\n  return { widths, kind: \"x\" };\n  */\n};\n\nconst generateImgAttrs = ({\n  src,\n  width,\n  quality,\n  sizes,\n  loader,\n}: {\n  src: string;\n  quality: number;\n  width: number | undefined;\n  sizes: string | undefined;\n  loader: ImageLoader;\n}): {\n  src: string;\n  srcSet: string | undefined;\n  sizes: string | undefined;\n} => {\n  const { widths, kind } = getWidths(width, sizes);\n\n  return {\n    sizes: !sizes && kind === \"w\" ? \"100vw\" : sizes,\n    srcSet: widths\n      .map(\n        (w, i) =>\n          `${loader({ src, quality, width: w })} ${\n            kind === \"w\" ? w : i + 1\n          }${kind}`\n      )\n      .join(\", \"),\n\n    // Must be last, to prevent Safari to load images twice\n    src: loader({\n      src,\n      quality,\n      width: widths[widths.length - 1],\n    }),\n  };\n};\n\nconst getInt = (value: unknown): number | undefined => {\n  if (typeof value === \"number\") {\n    return Math.round(value);\n  }\n\n  if (typeof value === \"string\") {\n    const vNum = Number.parseFloat(value);\n\n    if (!Number.isNaN(vNum)) {\n      return Math.round(vNum);\n    }\n  }\n\n  return;\n};\n\n/**\n * DEFAULT_SIZES Just an assumption that most images (except hero and icons) are 100% wide on mobile and 50% on desktop.\n * For icons width are usually set explicitly so DEFAULT_SIZES is not applied.\n * For hero images, we can allow in UI to select sizes=100vw explicitly.\n * Anyway, the best would be to calculate this based on canvas data from different breakpoints.\n * See ../component-utils/image for detailed description\n **/\nconst DEFAULT_SIZES = \"(min-width: 1280px) 50vw, 100vw\";\n\nconst DEFAULT_QUALITY = 80;\n\n/**\n * URL.canParse(props.src)\n */\nconst UrlCanParse = (url: string): boolean => {\n  try {\n    new URL(url);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nexport const getImageAttributes = (props: {\n  src: string | undefined;\n  srcSet: string | undefined;\n  sizes: string | undefined;\n  width: string | number | undefined;\n  quality: string | number | undefined;\n  loader: ImageLoader;\n  optimize: boolean;\n}):\n  | {\n      src: string;\n      srcSet?: string;\n      sizes?: string;\n    }\n  | undefined => {\n  const width = getInt(props.width);\n\n  const quality = Math.max(\n    Math.min(getInt(props.quality) ?? DEFAULT_QUALITY, 100),\n    0\n  );\n\n  if (props.src != null && props.src !== \"\") {\n    // Data URIs should be used as-is without optimization or loader processing\n    if (props.src.startsWith(\"data:\")) {\n      return { src: props.src };\n    }\n\n    if (props.srcSet == null && props.optimize) {\n      const sizes =\n        props.sizes ?? (props.width == null ? DEFAULT_SIZES : undefined);\n\n      return generateImgAttrs({\n        src: props.src,\n        width,\n        quality,\n        sizes,\n        loader: props.loader,\n      });\n    }\n\n    const resAttrs: {\n      src: string;\n      srcSet?: string;\n      sizes?: string;\n    } = {\n      src: UrlCanParse(props.src)\n        ? props.src\n        : props.loader({ src: props.src, format: \"raw\" }),\n    };\n\n    if (props.srcSet != null) {\n      resAttrs.srcSet = props.srcSet;\n    }\n\n    if (props.sizes != null) {\n      resAttrs.sizes = props.sizes;\n    }\n\n    return resAttrs;\n  }\n};\n"
  },
  {
    "path": "packages/image/src/image.tsx",
    "content": "import {\n  forwardRef,\n  type ComponentProps,\n  type ForwardRefExoticComponent,\n} from \"react\";\nimport { getImageAttributes, type ImageLoader } from \"./image-optimize\";\n\nconst defaultTag = \"img\";\n\ntype ImageProps = ComponentProps<typeof defaultTag> & {\n  quality?: number;\n  /** Optimize the image for enhanced performance. */\n  optimize?: boolean;\n  loader: ImageLoader;\n};\n\nexport const Image: ForwardRefExoticComponent<ImageProps> = forwardRef(\n  (\n    {\n      quality,\n      loader,\n      optimize = true,\n      loading = \"lazy\",\n      decoding = \"async\",\n      ...imageProps\n    },\n    ref\n  ) => {\n    const imageAttributes = getImageAttributes({\n      src: imageProps.src,\n      srcSet: imageProps.srcSet,\n      sizes: imageProps.sizes,\n      width: imageProps.width,\n      quality,\n      loader,\n      optimize,\n    }) ?? { src: imagePlaceholderDataUrl };\n\n    return (\n      <img\n        alt=\"\"\n        {...imageProps}\n        {...imageAttributes}\n        decoding={decoding}\n        loading={loading}\n        ref={ref}\n      />\n    );\n  }\n);\n\nImage.displayName = \"Image\";\n\nexport const imagePlaceholderDataUrl: string = `data:image/svg+xml;base64,${btoa(`<svg\n  width=\"140\"\n  height=\"140\"\n  viewBox=\"0 0 600 600\"\n  fill=\"none\"\n  xmlns=\"http://www.w3.org/2000/svg\"\n  >\n  <rect width=\"600\" height=\"600\" fill=\"#DFE3E6\" />\n  <path\n    fill-rule=\"evenodd\"\n    clip-rule=\"evenodd\"\n    d=\"M450 170H150C141.716 170 135 176.716 135 185V415C135 423.284 141.716 430 150 430H450C458.284 430 465 423.284 465 415V185C465 176.716 458.284 170 450 170ZM150 145C127.909 145 110 162.909 110 185V415C110 437.091 127.909 455 150 455H450C472.091 455 490 437.091 490 415V185C490 162.909 472.091 145 450 145H150Z\"\n    fill=\"#C1C8CD\"\n  />\n  <path\n    d=\"M237.135 235.012C237.135 255.723 220.345 272.512 199.635 272.512C178.924 272.512 162.135 255.723 162.135 235.012C162.135 214.301 178.924 197.512 199.635 197.512C220.345 197.512 237.135 214.301 237.135 235.012Z\"\n    fill=\"#C1C8CD\"\n  />\n  <path\n    d=\"M160 405V367.205L221.609 306.364L256.552 338.628L358.161 234L440 316.043V405H160Z\"\n    fill=\"#C1C8CD\"\n  />\n</svg>`)}`;\n"
  },
  {
    "path": "packages/image/src/index.ts",
    "content": "export { Image, imagePlaceholderDataUrl } from \"./image\";\nexport type { ImageLoader } from \"./image-optimize\";\nexport * from \"./image-loaders\";\nexport * from \"./image-optimize\";\n"
  },
  {
    "path": "packages/image/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"exclude\": [\"**/*.stories.tsx\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/image/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"],\n  \"compilerOptions\": {\n    \"isolatedDeclarations\": true\n  }\n}\n"
  },
  {
    "path": "packages/postgrest/README.md",
    "content": "# Postgrest\n\n## Postgrest-js\n\n[postgrest-js best doc](https://supabase.com/docs/reference/javascript/select)\n\n## Generated Types\n\n```bash\npnpm generate-types\n```\n\nor in case of non devcontainer environment\n\n```bash\ndocker compose exec -iT app bash\ncd /workspaces/webstudio/packages/postgrest\npnpm generate-types\n```\n\n## Playground\n\n```bash\npnpm playground ./playground/{file}.ts\n# OR\npnpm tsx --env-file ../../apps/builder/.env ./playground/{file}.ts\n```\n\n## Next steps.\n\nUse https://supabase.com/docs/reference/cli/supabase-db-start or directly https://github.com/djrobstep/migra for migrations.\n\nSupabase can be used with `--db-url` flag to not reproduce \"local\" env\n\n## Sql testing\n\n```sql\nCREATE SCHEMA IF NOT EXISTS pgtap;\nDROP EXTENSION pgtap;\nCREATE EXTENSION IF NOT EXISTS pgtap WITH SCHEMA pgtap;\n```\n\n```bash\npnpx supabase test new latest-builds\n```\n\n```shell\ndocker run --rm --network host -v ./supabase/tests:/tests -e PGOPTIONS='--search_path=pgtap,public' supabase/pg_prove:3.36 pg_prove -d \"postgresql://postgres:pass@localhost/webstudio\" --ext .sql /tests\n# OR\npnpm run db-test\n```\n"
  },
  {
    "path": "packages/postgrest/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/postgrest\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio Project Build\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\",\n      \"import\": \"./src/index.server.ts\"\n    }\n  },\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"generate-types\": \"pnpx supabase gen types --lang=typescript --db-url postgresql://postgres:pass@localhost/webstudio > ./src/__generated__/db-types.ts && prettier --write ./src/__generated__/db-types.ts\",\n    \"playground\": \"pnpm tsx --env-file ../../apps/builder/.env\",\n    \"db-test\": \"docker run --rm --network host -v ./supabase/tests:/tests -e PGOPTIONS='--search_path=pgtap,public' supabase/pg_prove:3.36 pg_prove -d ${DIRECT_URL:-postgresql://postgres:pass@localhost/webstudio} --ext .sql /tests\"\n  },\n  \"dependencies\": {\n    \"@supabase/postgrest-js\": \"^1.19.3\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\"\n  }\n}\n"
  },
  {
    "path": "packages/postgrest/playground/domains.ts",
    "content": "#!/usr/bin/env ./playground/pnpm-playground\n\nimport { createClient } from \"../src/index.server\";\n\nconst client = createClient(\n  process.env.POSTGREST_URL!,\n  process.env.POSTGREST_API_KEY!\n);\n\nconst result = await client\n  .from(\"Project\")\n  .select(\"domainsVirtual(*, latestBuildVirtual(*))\")\n  .eq(\"id\", \"090e6e14-ae50-4b2e-bd22-71733cec05bb\");\n\nconsole.info(JSON.stringify(result, null, \" \"));\n\n/*\nconst domain = \"hello.world\";\nconst result = await client\n  .from(\"Domain\")\n  .upsert(\n    {\n      id: crypto.randomUUID(),\n      domain,\n      status: \"INITIALIZING\",\n      error: null,\n    },\n    { onConflict: \"domain\", ignoreDuplicates: false }\n  )\n  .eq(\"domain\", domain)\n  .select(\"*\");\n\nconsole.info(JSON.stringify(result, null, \" \"));\n*/\n\n/*\nconst result = await client\n  .from(\"ProjectWithDomain\")\n  .select(\"Domain(*), latestBuildVirtual(*)\")\n  .eq(\"projectId\", \"1bbe90ae-f0b3-4b2d-925f-26c75f824344\");\n\n  const result = await client\n  .from(\"Project\")\n  .select(\n    \"ProjectDomain(Domain(*), latestBuildVirtual(*)), latestBuildVirtual(*)\"\n  )\n  .eq(\"id\", \"1bbe90ae-f0b3-4b2d-925f-26c75f824344\");\n\n  if (result.error) {\n    throw result.error;\n  }\n\n  console.info(JSON.stringify(result.data, null, \" \"));\n  */\n"
  },
  {
    "path": "packages/postgrest/playground/pnpm-playground",
    "content": "#!/bin/bash\n\n# !/usr/bin/env pnpm playground will not work\n# The shebang line only takes a single path to an executable,\n# and everything after the path is treated as a single string, not as separate arguments.\n\npnpm playground \"$@\"\n\n"
  },
  {
    "path": "packages/postgrest/src/__generated__/db-types.ts",
    "content": "export type Json =\n  | string\n  | number\n  | boolean\n  | null\n  | { [key: string]: Json | undefined }\n  | Json[];\n\nexport type Database = {\n  graphql_public: {\n    Tables: {\n      [_ in never]: never;\n    };\n    Views: {\n      [_ in never]: never;\n    };\n    Functions: {\n      graphql: {\n        Args: {\n          extensions?: Json;\n          operationName?: string;\n          query?: string;\n          variables?: Json;\n        };\n        Returns: Json;\n      };\n    };\n    Enums: {\n      [_ in never]: never;\n    };\n    CompositeTypes: {\n      [_ in never]: never;\n    };\n  };\n  public: {\n    Tables: {\n      _prisma_migrations: {\n        Row: {\n          applied_steps_count: number;\n          checksum: string;\n          finished_at: string | null;\n          id: string;\n          logs: string | null;\n          migration_name: string;\n          rolled_back_at: string | null;\n          started_at: string;\n        };\n        Insert: {\n          applied_steps_count?: number;\n          checksum: string;\n          finished_at?: string | null;\n          id: string;\n          logs?: string | null;\n          migration_name: string;\n          rolled_back_at?: string | null;\n          started_at?: string;\n        };\n        Update: {\n          applied_steps_count?: number;\n          checksum?: string;\n          finished_at?: string | null;\n          id?: string;\n          logs?: string | null;\n          migration_name?: string;\n          rolled_back_at?: string | null;\n          started_at?: string;\n        };\n        Relationships: [];\n      };\n      Asset: {\n        Row: {\n          description: string | null;\n          filename: string | null;\n          id: string;\n          name: string;\n          projectId: string;\n        };\n        Insert: {\n          description?: string | null;\n          filename?: string | null;\n          id: string;\n          name: string;\n          projectId: string;\n        };\n        Update: {\n          description?: string | null;\n          filename?: string | null;\n          id?: string;\n          name?: string;\n          projectId?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Asset_name_fkey\";\n            columns: [\"name\"];\n            isOneToOne: false;\n            referencedRelation: \"File\";\n            referencedColumns: [\"name\"];\n          },\n        ];\n      };\n      AuthorizationToken: {\n        Row: {\n          canClone: boolean;\n          canCopy: boolean;\n          canPublish: boolean;\n          createdAt: string;\n          name: string;\n          projectId: string;\n          relation: Database[\"public\"][\"Enums\"][\"AuthorizationRelation\"];\n          token: string;\n        };\n        Insert: {\n          canClone?: boolean;\n          canCopy?: boolean;\n          canPublish?: boolean;\n          createdAt?: string;\n          name?: string;\n          projectId: string;\n          relation?: Database[\"public\"][\"Enums\"][\"AuthorizationRelation\"];\n          token: string;\n        };\n        Update: {\n          canClone?: boolean;\n          canCopy?: boolean;\n          canPublish?: boolean;\n          createdAt?: string;\n          name?: string;\n          projectId?: string;\n          relation?: Database[\"public\"][\"Enums\"][\"AuthorizationRelation\"];\n          token?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"AuthorizationToken_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"AuthorizationToken_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      Build: {\n        Row: {\n          breakpoints: string;\n          createdAt: string;\n          dataSources: string;\n          deployment: string | null;\n          id: string;\n          instances: string;\n          isCleaned: boolean | null;\n          lastTransactionId: string | null;\n          marketplaceProduct: string;\n          pages: string;\n          projectId: string;\n          props: string;\n          publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          resources: string;\n          styles: string;\n          styleSources: string;\n          styleSourceSelections: string;\n          updatedAt: string;\n          version: number;\n        };\n        Insert: {\n          breakpoints?: string;\n          createdAt?: string;\n          dataSources?: string;\n          deployment?: string | null;\n          id: string;\n          instances?: string;\n          isCleaned?: boolean | null;\n          lastTransactionId?: string | null;\n          marketplaceProduct?: string;\n          pages: string;\n          projectId: string;\n          props?: string;\n          publishStatus?: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          resources?: string;\n          styles?: string;\n          styleSources?: string;\n          styleSourceSelections?: string;\n          updatedAt?: string;\n          version?: number;\n        };\n        Update: {\n          breakpoints?: string;\n          createdAt?: string;\n          dataSources?: string;\n          deployment?: string | null;\n          id?: string;\n          instances?: string;\n          isCleaned?: boolean | null;\n          lastTransactionId?: string | null;\n          marketplaceProduct?: string;\n          pages?: string;\n          projectId?: string;\n          props?: string;\n          publishStatus?: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          resources?: string;\n          styles?: string;\n          styleSources?: string;\n          styleSourceSelections?: string;\n          updatedAt?: string;\n          version?: number;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      ClientReferences: {\n        Row: {\n          createdAt: string;\n          reference: string;\n          service: string;\n          userId: string;\n        };\n        Insert: {\n          createdAt?: string;\n          reference?: string;\n          service: string;\n          userId: string;\n        };\n        Update: {\n          createdAt?: string;\n          reference?: string;\n          service?: string;\n          userId?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"ClientReferences_userId_fkey\";\n            columns: [\"userId\"];\n            isOneToOne: false;\n            referencedRelation: \"User\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      Domain: {\n        Row: {\n          createdAt: string;\n          domain: string;\n          error: string | null;\n          id: string;\n          status: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          txtRecord: string | null;\n          updatedAt: string;\n        };\n        Insert: {\n          createdAt?: string;\n          domain: string;\n          error?: string | null;\n          id: string;\n          status?: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          txtRecord?: string | null;\n          updatedAt?: string;\n        };\n        Update: {\n          createdAt?: string;\n          domain?: string;\n          error?: string | null;\n          id?: string;\n          status?: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          txtRecord?: string | null;\n          updatedAt?: string;\n        };\n        Relationships: [];\n      };\n      domainsVirtual: {\n        Row: {\n          cname: string;\n          createdAt: string;\n          domain: string;\n          domainId: string;\n          domainTxtRecord: string | null;\n          error: string | null;\n          expectedTxtRecord: string;\n          id: string;\n          projectId: string;\n          status: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          updatedAt: string;\n          verified: boolean;\n        };\n        Insert: {\n          cname: string;\n          createdAt: string;\n          domain: string;\n          domainId: string;\n          domainTxtRecord?: string | null;\n          error?: string | null;\n          expectedTxtRecord: string;\n          id: string;\n          projectId: string;\n          status?: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          updatedAt: string;\n          verified?: boolean;\n        };\n        Update: {\n          cname?: string;\n          createdAt?: string;\n          domain?: string;\n          domainId?: string;\n          domainTxtRecord?: string | null;\n          error?: string | null;\n          expectedTxtRecord?: string;\n          id?: string;\n          projectId?: string;\n          status?: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          updatedAt?: string;\n          verified?: boolean;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"domainsVirtual_domainId_fkey\";\n            columns: [\"domainId\"];\n            isOneToOne: false;\n            referencedRelation: \"Domain\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"domainsVirtual_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"domainsVirtual_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      File: {\n        Row: {\n          createdAt: string;\n          description: string | null;\n          format: string;\n          isDeleted: boolean;\n          meta: string;\n          name: string;\n          size: number;\n          status: Database[\"public\"][\"Enums\"][\"UploadStatus\"];\n          updatedAt: string;\n          uploaderProjectId: string | null;\n        };\n        Insert: {\n          createdAt?: string;\n          description?: string | null;\n          format: string;\n          isDeleted?: boolean;\n          meta?: string;\n          name: string;\n          size: number;\n          status?: Database[\"public\"][\"Enums\"][\"UploadStatus\"];\n          updatedAt?: string;\n          uploaderProjectId?: string | null;\n        };\n        Update: {\n          createdAt?: string;\n          description?: string | null;\n          format?: string;\n          isDeleted?: boolean;\n          meta?: string;\n          name?: string;\n          size?: number;\n          status?: Database[\"public\"][\"Enums\"][\"UploadStatus\"];\n          updatedAt?: string;\n          uploaderProjectId?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"File_uploaderProjectId_fkey\";\n            columns: [\"uploaderProjectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"File_uploaderProjectId_fkey\";\n            columns: [\"uploaderProjectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      latestBuildVirtual: {\n        Row: {\n          buildId: string;\n          createdAt: string;\n          domain: string;\n          domainsVirtualId: string;\n          projectId: string;\n          publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          updatedAt: string;\n        };\n        Insert: {\n          buildId: string;\n          createdAt: string;\n          domain: string;\n          domainsVirtualId: string;\n          projectId: string;\n          publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          updatedAt: string;\n        };\n        Update: {\n          buildId?: string;\n          createdAt?: string;\n          domain?: string;\n          domainsVirtualId?: string;\n          projectId?: string;\n          publishStatus?: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          updatedAt?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"latestBuildVirtual_buildId_fkey\";\n            columns: [\"buildId\"];\n            isOneToOne: true;\n            referencedRelation: \"Build\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"latestBuildVirtual_buildId_fkey\";\n            columns: [\"buildId\"];\n            isOneToOne: true;\n            referencedRelation: \"LatestStaticBuildPerProject\";\n            referencedColumns: [\"buildId\"];\n          },\n          {\n            foreignKeyName: \"latestBuildVirtual_buildId_fkey\";\n            columns: [\"buildId\"];\n            isOneToOne: true;\n            referencedRelation: \"published_builds\";\n            referencedColumns: [\"buildId\"];\n          },\n          {\n            foreignKeyName: \"latestBuildVirtual_domainsVirtualId_fkey\";\n            columns: [\"domainsVirtualId\"];\n            isOneToOne: true;\n            referencedRelation: \"domainsVirtual\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"latestBuildVirtual_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: true;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"latestBuildVirtual_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: true;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      Product: {\n        Row: {\n          createdAt: string;\n          description: string | null;\n          features: string[] | null;\n          id: string;\n          images: string[] | null;\n          meta: Json;\n          name: string;\n        };\n        Insert: {\n          createdAt?: string;\n          description?: string | null;\n          features?: string[] | null;\n          id: string;\n          images?: string[] | null;\n          meta: Json;\n          name: string;\n        };\n        Update: {\n          createdAt?: string;\n          description?: string | null;\n          features?: string[] | null;\n          id?: string;\n          images?: string[] | null;\n          meta?: Json;\n          name?: string;\n        };\n        Relationships: [];\n      };\n      Project: {\n        Row: {\n          createdAt: string;\n          domain: string;\n          id: string;\n          isDeleted: boolean;\n          marketplaceApprovalStatus: Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"];\n          previewImageAssetId: string | null;\n          tags: string[] | null;\n          title: string;\n          userId: string | null;\n        };\n        Insert: {\n          createdAt?: string;\n          domain: string;\n          id: string;\n          isDeleted?: boolean;\n          marketplaceApprovalStatus?: Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"];\n          previewImageAssetId?: string | null;\n          tags?: string[] | null;\n          title: string;\n          userId?: string | null;\n        };\n        Update: {\n          createdAt?: string;\n          domain?: string;\n          id?: string;\n          isDeleted?: boolean;\n          marketplaceApprovalStatus?: Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"];\n          previewImageAssetId?: string | null;\n          tags?: string[] | null;\n          title?: string;\n          userId?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Project_previewImageAssetId_id_fkey\";\n            columns: [\"previewImageAssetId\", \"id\"];\n            isOneToOne: false;\n            referencedRelation: \"Asset\";\n            referencedColumns: [\"id\", \"projectId\"];\n          },\n          {\n            foreignKeyName: \"Project_userId_fkey\";\n            columns: [\"userId\"];\n            isOneToOne: false;\n            referencedRelation: \"User\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      ProjectDomain: {\n        Row: {\n          cname: string;\n          createdAt: string;\n          domainId: string;\n          projectId: string;\n          txtRecord: string;\n        };\n        Insert: {\n          cname: string;\n          createdAt?: string;\n          domainId: string;\n          projectId: string;\n          txtRecord: string;\n        };\n        Update: {\n          cname?: string;\n          createdAt?: string;\n          domainId?: string;\n          projectId?: string;\n          txtRecord?: string;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"ProjectDomain_domainId_fkey\";\n            columns: [\"domainId\"];\n            isOneToOne: false;\n            referencedRelation: \"Domain\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"ProjectDomain_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"ProjectDomain_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      Team: {\n        Row: {\n          id: string;\n        };\n        Insert: {\n          id: string;\n        };\n        Update: {\n          id?: string;\n        };\n        Relationships: [];\n      };\n      TransactionLog: {\n        Row: {\n          createdAt: string;\n          customerEmail: string | null;\n          customerId: string | null;\n          eventCreated: number | null;\n          eventData: Json | null;\n          eventId: string;\n          eventType: string | null;\n          paymentIntent: string | null;\n          productId: string | null;\n          status: string | null;\n          subscriptionId: string | null;\n          userId: string | null;\n        };\n        Insert: {\n          createdAt?: string;\n          customerEmail?: string | null;\n          customerId?: string | null;\n          eventCreated?: number | null;\n          eventData?: Json | null;\n          eventId: string;\n          eventType?: string | null;\n          paymentIntent?: string | null;\n          productId?: string | null;\n          status?: string | null;\n          subscriptionId?: string | null;\n          userId?: string | null;\n        };\n        Update: {\n          createdAt?: string;\n          customerEmail?: string | null;\n          customerId?: string | null;\n          eventCreated?: number | null;\n          eventData?: Json | null;\n          eventId?: string;\n          eventType?: string | null;\n          paymentIntent?: string | null;\n          productId?: string | null;\n          status?: string | null;\n          subscriptionId?: string | null;\n          userId?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"TransactionLog_productId_fkey\";\n            columns: [\"productId\"];\n            isOneToOne: false;\n            referencedRelation: \"Product\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"TransactionLog_userId_fkey\";\n            columns: [\"userId\"];\n            isOneToOne: false;\n            referencedRelation: \"User\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      User: {\n        Row: {\n          createdAt: string;\n          email: string | null;\n          id: string;\n          image: string | null;\n          projectsTags: Json;\n          provider: string | null;\n          teamId: string | null;\n          username: string | null;\n        };\n        Insert: {\n          createdAt?: string;\n          email?: string | null;\n          id: string;\n          image?: string | null;\n          projectsTags?: Json;\n          provider?: string | null;\n          teamId?: string | null;\n          username?: string | null;\n        };\n        Update: {\n          createdAt?: string;\n          email?: string | null;\n          id?: string;\n          image?: string | null;\n          projectsTags?: Json;\n          provider?: string | null;\n          teamId?: string | null;\n          username?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"User_teamId_fkey\";\n            columns: [\"teamId\"];\n            isOneToOne: false;\n            referencedRelation: \"Team\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n    };\n    Views: {\n      ApprovedMarketplaceProduct: {\n        Row: {\n          authorizationToken: string | null;\n          marketplaceProduct: string | null;\n          projectId: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      DashboardProject: {\n        Row: {\n          createdAt: string | null;\n          domain: string | null;\n          id: string | null;\n          isDeleted: boolean | null;\n          isPublished: boolean | null;\n          marketplaceApprovalStatus:\n            | Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"]\n            | null;\n          previewImageAssetId: string | null;\n          tags: string[] | null;\n          title: string | null;\n          userId: string | null;\n        };\n        Insert: {\n          createdAt?: string | null;\n          domain?: string | null;\n          id?: string | null;\n          isDeleted?: boolean | null;\n          isPublished?: never;\n          marketplaceApprovalStatus?:\n            | Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"]\n            | null;\n          previewImageAssetId?: string | null;\n          tags?: string[] | null;\n          title?: string | null;\n          userId?: string | null;\n        };\n        Update: {\n          createdAt?: string | null;\n          domain?: string | null;\n          id?: string | null;\n          isDeleted?: boolean | null;\n          isPublished?: never;\n          marketplaceApprovalStatus?:\n            | Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"]\n            | null;\n          previewImageAssetId?: string | null;\n          tags?: string[] | null;\n          title?: string | null;\n          userId?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Project_previewImageAssetId_id_fkey\";\n            columns: [\"previewImageAssetId\", \"id\"];\n            isOneToOne: false;\n            referencedRelation: \"Asset\";\n            referencedColumns: [\"id\", \"projectId\"];\n          },\n          {\n            foreignKeyName: \"Project_userId_fkey\";\n            columns: [\"userId\"];\n            isOneToOne: false;\n            referencedRelation: \"User\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      LatestStaticBuildPerProject: {\n        Row: {\n          buildId: string | null;\n          projectId: string | null;\n          publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"] | null;\n          updatedAt: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      published_builds: {\n        Row: {\n          buildId: string | null;\n          createdAt: string | null;\n          domains: string | null;\n          projectId: string | null;\n        };\n        Insert: {\n          buildId?: string | null;\n          createdAt?: string | null;\n          domains?: never;\n          projectId?: string | null;\n        };\n        Update: {\n          buildId?: string | null;\n          createdAt?: string | null;\n          domains?: never;\n          projectId?: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"DashboardProject\";\n            referencedColumns: [\"id\"];\n          },\n          {\n            foreignKeyName: \"Build_projectId_fkey\";\n            columns: [\"projectId\"];\n            isOneToOne: false;\n            referencedRelation: \"Project\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      user_publish_count: {\n        Row: {\n          count: number | null;\n          user_id: string | null;\n        };\n        Relationships: [\n          {\n            foreignKeyName: \"Project_userId_fkey\";\n            columns: [\"user_id\"];\n            isOneToOne: false;\n            referencedRelation: \"User\";\n            referencedColumns: [\"id\"];\n          },\n        ];\n      };\n      UserProduct: {\n        Row: {\n          customerEmail: string | null;\n          customerId: string | null;\n          productId: string | null;\n          subscriptionId: string | null;\n          userId: string | null;\n        };\n        Relationships: [];\n      };\n    };\n    Functions: {\n      clone_project: {\n        Args: {\n          domain: string;\n          project_id: string;\n          title: string;\n          user_id: string;\n        };\n        Returns: {\n          createdAt: string;\n          domain: string;\n          id: string;\n          isDeleted: boolean;\n          marketplaceApprovalStatus: Database[\"public\"][\"Enums\"][\"MarketplaceApprovalStatus\"];\n          previewImageAssetId: string | null;\n          tags: string[] | null;\n          title: string;\n          userId: string | null;\n        };\n        SetofOptions: {\n          from: \"*\";\n          to: \"Project\";\n          isOneToOne: true;\n          isSetofReturn: false;\n        };\n      };\n      create_production_build: {\n        Args: { deployment: string; project_id: string };\n        Returns: string;\n      };\n      database_cleanup: {\n        Args: { from_date?: string; to_date?: string };\n        Returns: undefined;\n      };\n      domainsVirtual: {\n        Args: { \"\": Database[\"public\"][\"Tables\"][\"Project\"][\"Row\"] };\n        Returns: {\n          cname: string;\n          createdAt: string;\n          domain: string;\n          domainId: string;\n          domainTxtRecord: string | null;\n          error: string | null;\n          expectedTxtRecord: string;\n          id: string;\n          projectId: string;\n          status: Database[\"public\"][\"Enums\"][\"DomainStatus\"];\n          updatedAt: string;\n          verified: boolean;\n        }[];\n        SetofOptions: {\n          from: '\"Project\"';\n          to: \"domainsVirtual\";\n          isOneToOne: false;\n          isSetofReturn: true;\n        };\n      };\n      latestBuildVirtual:\n        | {\n            Args: { \"\": Database[\"public\"][\"Tables\"][\"domainsVirtual\"][\"Row\"] };\n            Returns: {\n              buildId: string;\n              createdAt: string;\n              domain: string;\n              domainsVirtualId: string;\n              projectId: string;\n              publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n              updatedAt: string;\n            };\n            SetofOptions: {\n              from: '\"domainsVirtual\"';\n              to: \"latestBuildVirtual\";\n              isOneToOne: true;\n              isSetofReturn: true;\n            };\n          }\n        | {\n            Args: { \"\": Database[\"public\"][\"Tables\"][\"Project\"][\"Row\"] };\n            Returns: {\n              buildId: string;\n              createdAt: string;\n              domain: string;\n              domainsVirtualId: string;\n              projectId: string;\n              publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n              updatedAt: string;\n            };\n            SetofOptions: {\n              from: '\"Project\"';\n              to: \"latestBuildVirtual\";\n              isOneToOne: true;\n              isSetofReturn: true;\n            };\n          };\n      latestProjectDomainBuildVirtual: {\n        Args: { \"\": Database[\"public\"][\"Tables\"][\"Project\"][\"Row\"] };\n        Returns: {\n          buildId: string;\n          createdAt: string;\n          domain: string;\n          domainsVirtualId: string;\n          projectId: string;\n          publishStatus: Database[\"public\"][\"Enums\"][\"PublishStatus\"];\n          updatedAt: string;\n        };\n        SetofOptions: {\n          from: '\"Project\"';\n          to: \"latestBuildVirtual\";\n          isOneToOne: true;\n          isSetofReturn: true;\n        };\n      };\n      restore_development_build: {\n        Args: { from_build_id: string; project_id: string };\n        Returns: string;\n      };\n    };\n    Enums: {\n      AuthorizationRelation:\n        | \"viewers\"\n        | \"editors\"\n        | \"builders\"\n        | \"administrators\";\n      DomainStatus: \"INITIALIZING\" | \"ACTIVE\" | \"ERROR\" | \"PENDING\";\n      MarketplaceApprovalStatus:\n        | \"UNLISTED\"\n        | \"PENDING\"\n        | \"APPROVED\"\n        | \"REJECTED\";\n      PublishStatus: \"PENDING\" | \"PUBLISHED\" | \"FAILED\";\n      UploadStatus: \"UPLOADING\" | \"UPLOADED\";\n    };\n    CompositeTypes: {\n      [_ in never]: never;\n    };\n  };\n};\n\ntype DatabaseWithoutInternals = Omit<Database, \"__InternalSupabase\">;\n\ntype DefaultSchema = DatabaseWithoutInternals[Extract<\n  keyof Database,\n  \"public\"\n>];\n\nexport type Tables<\n  DefaultSchemaTableNameOrOptions extends\n    | keyof (DefaultSchema[\"Tables\"] & DefaultSchema[\"Views\"])\n    | { schema: keyof DatabaseWithoutInternals },\n  TableName extends DefaultSchemaTableNameOrOptions extends {\n    schema: keyof DatabaseWithoutInternals;\n  }\n    ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Tables\"] &\n        DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Views\"])\n    : never = never,\n> = DefaultSchemaTableNameOrOptions extends {\n  schema: keyof DatabaseWithoutInternals;\n}\n  ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Tables\"] &\n      DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Views\"])[TableName] extends {\n      Row: infer R;\n    }\n    ? R\n    : never\n  : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema[\"Tables\"] &\n        DefaultSchema[\"Views\"])\n    ? (DefaultSchema[\"Tables\"] &\n        DefaultSchema[\"Views\"])[DefaultSchemaTableNameOrOptions] extends {\n        Row: infer R;\n      }\n      ? R\n      : never\n    : never;\n\nexport type TablesInsert<\n  DefaultSchemaTableNameOrOptions extends\n    | keyof DefaultSchema[\"Tables\"]\n    | { schema: keyof DatabaseWithoutInternals },\n  TableName extends DefaultSchemaTableNameOrOptions extends {\n    schema: keyof DatabaseWithoutInternals;\n  }\n    ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Tables\"]\n    : never = never,\n> = DefaultSchemaTableNameOrOptions extends {\n  schema: keyof DatabaseWithoutInternals;\n}\n  ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Tables\"][TableName] extends {\n      Insert: infer I;\n    }\n    ? I\n    : never\n  : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema[\"Tables\"]\n    ? DefaultSchema[\"Tables\"][DefaultSchemaTableNameOrOptions] extends {\n        Insert: infer I;\n      }\n      ? I\n      : never\n    : never;\n\nexport type TablesUpdate<\n  DefaultSchemaTableNameOrOptions extends\n    | keyof DefaultSchema[\"Tables\"]\n    | { schema: keyof DatabaseWithoutInternals },\n  TableName extends DefaultSchemaTableNameOrOptions extends {\n    schema: keyof DatabaseWithoutInternals;\n  }\n    ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Tables\"]\n    : never = never,\n> = DefaultSchemaTableNameOrOptions extends {\n  schema: keyof DatabaseWithoutInternals;\n}\n  ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions[\"schema\"]][\"Tables\"][TableName] extends {\n      Update: infer U;\n    }\n    ? U\n    : never\n  : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema[\"Tables\"]\n    ? DefaultSchema[\"Tables\"][DefaultSchemaTableNameOrOptions] extends {\n        Update: infer U;\n      }\n      ? U\n      : never\n    : never;\n\nexport type Enums<\n  DefaultSchemaEnumNameOrOptions extends\n    | keyof DefaultSchema[\"Enums\"]\n    | { schema: keyof DatabaseWithoutInternals },\n  EnumName extends DefaultSchemaEnumNameOrOptions extends {\n    schema: keyof DatabaseWithoutInternals;\n  }\n    ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions[\"schema\"]][\"Enums\"]\n    : never = never,\n> = DefaultSchemaEnumNameOrOptions extends {\n  schema: keyof DatabaseWithoutInternals;\n}\n  ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions[\"schema\"]][\"Enums\"][EnumName]\n  : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema[\"Enums\"]\n    ? DefaultSchema[\"Enums\"][DefaultSchemaEnumNameOrOptions]\n    : never;\n\nexport type CompositeTypes<\n  PublicCompositeTypeNameOrOptions extends\n    | keyof DefaultSchema[\"CompositeTypes\"]\n    | { schema: keyof DatabaseWithoutInternals },\n  CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {\n    schema: keyof DatabaseWithoutInternals;\n  }\n    ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions[\"schema\"]][\"CompositeTypes\"]\n    : never = never,\n> = PublicCompositeTypeNameOrOptions extends {\n  schema: keyof DatabaseWithoutInternals;\n}\n  ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions[\"schema\"]][\"CompositeTypes\"][CompositeTypeName]\n  : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema[\"CompositeTypes\"]\n    ? DefaultSchema[\"CompositeTypes\"][PublicCompositeTypeNameOrOptions]\n    : never;\n\nexport const Constants = {\n  graphql_public: {\n    Enums: {},\n  },\n  public: {\n    Enums: {\n      AuthorizationRelation: [\n        \"viewers\",\n        \"editors\",\n        \"builders\",\n        \"administrators\",\n      ],\n      DomainStatus: [\"INITIALIZING\", \"ACTIVE\", \"ERROR\", \"PENDING\"],\n      MarketplaceApprovalStatus: [\n        \"UNLISTED\",\n        \"PENDING\",\n        \"APPROVED\",\n        \"REJECTED\",\n      ],\n      PublishStatus: [\"PENDING\", \"PUBLISHED\", \"FAILED\"],\n      UploadStatus: [\"UPLOADING\", \"UPLOADED\"],\n    },\n  },\n} as const;\n"
  },
  {
    "path": "packages/postgrest/src/index.server.ts",
    "content": "import type { Database } from \"./__generated__/db-types\";\nimport { PostgrestClient } from \"@supabase/postgrest-js\";\nexport type { Database } from \"./__generated__/db-types\";\n\nexport type Client = PostgrestClient<Database>;\n\nexport const createClient = (url: string, apiKey: string): Client => {\n  const client = new PostgrestClient<Database>(url, {\n    headers: {\n      apikey: apiKey,\n      Authorization: `Bearer ${apiKey}`,\n    },\n  });\n\n  return client;\n};\n"
  },
  {
    "path": "packages/postgrest/supabase/SQL-TESTS-AI.md",
    "content": "# Sql Testing AI Helpers\n\nExtract schema\n\n```bash\npnpx supabase db dump -s public --db-url postgresql://postgres:pass@localhost/webstudio > schema.sql\n```\n\nPromt Examples\n\n---\n\nBelow is a partial dump of the SQL schema:\n\n```sql\n\nCREATE TABLE IF NOT EXISTS \"public\".\"User\" (\n    \"id\" \"text\" NOT NULL,\n    \"createdAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    \"email\" \"text\",\n    \"image\" \"text\",\n    \"provider\" \"text\",\n    \"username\" \"text\",\n    \"teamId\" \"text\"\n);\n\n\nCREATE TABLE IF NOT EXISTS \"public\".\"Domain\" (\n    \"id\" \"text\" NOT NULL,\n    \"domain\" \"text\" NOT NULL,\n    \"createdAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    \"txtRecord\" \"text\",\n    \"status\" \"public\".\"DomainStatus\" DEFAULT 'INITIALIZING'::\"public\".\"DomainStatus\" NOT NULL,\n    \"error\" \"text\",\n    \"updatedAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS \"public\".\"Build\" (\n    \"id\" \"text\" NOT NULL,\n    \"createdAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    \"pages\" \"text\" NOT NULL,\n    \"projectId\" \"text\" NOT NULL,\n    \"styleSources\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"styles\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"breakpoints\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"styleSourceSelections\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"props\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"instances\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"updatedAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    \"version\" integer DEFAULT 0 NOT NULL,\n    \"deployment\" \"text\",\n    \"publishStatus\" \"public\".\"PublishStatus\" DEFAULT 'PENDING'::\"public\".\"PublishStatus\" NOT NULL,\n    \"dataSources\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"lastTransactionId\" \"text\",\n    \"resources\" \"text\" DEFAULT '[]'::\"text\" NOT NULL,\n    \"marketplaceProduct\" \"text\" DEFAULT '{}'::\"text\" NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS \"public\".\"Project\" (\n    \"id\" \"text\" NOT NULL,\n    \"title\" \"text\" NOT NULL,\n    \"domain\" \"text\" NOT NULL,\n    \"userId\" \"text\",\n    \"isDeleted\" boolean DEFAULT false NOT NULL,\n    \"createdAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    \"previewImageAssetId\" \"text\",\n    \"marketplaceApprovalStatus\" \"public\".\"MarketplaceApprovalStatus\" DEFAULT 'UNLISTED'::\"public\".\"MarketplaceApprovalStatus\" NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS \"public\".\"ProjectDomain\" (\n    \"projectId\" \"text\" NOT NULL,\n    \"domainId\" \"text\" NOT NULL,\n    \"createdAt\" timestamp(3) with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,\n    \"txtRecord\" \"text\" NOT NULL,\n    \"cname\" \"text\" NOT NULL\n);\n\nCREATE OR REPLACE FUNCTION \"public\".\"latestBuildVirtual\"(\"public\".\"Project\") RETURNS SETOF \"public\".\"latestBuildVirtual\"\n    LANGUAGE \"sql\" STABLE ROWS 1\n    AS $_$ -- The function is expected to return 1 row\n\n\nSELECT\n    b.id AS \"buildId\",\n    b.\"projectId\",\n    -- Use CASE to determine which domain to select based on conditions\n    CASE\n        WHEN (b.deployment::jsonb ->> 'projectDomain') = p.domain\n             OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[p.domain])\n        THEN p.domain\n        ELSE d.domain\n    END AS \"domain\",\n    b.\"createdAt\",\n    b.\"publishStatus\"\nFROM \"Build\" b\nJOIN \"Project\" p ON b.\"projectId\" = p.id\nLEFT JOIN \"ProjectDomain\" pd ON pd.\"projectId\" = p.id\nLEFT JOIN \"Domain\" d ON d.id = pd.\"domainId\"\nWHERE b.\"projectId\" = $1.id\n  AND b.deployment IS NOT NULL\n  -- 'destination' IS NULL for backward compatibility; 'destination' = 'saas' for non-static builds\n  AND ((b.deployment::jsonb ->> 'destination') IS NULL OR (b.deployment::jsonb ->> 'destination') = 'saas')\n  AND (\n      -- Check if 'projectDomain' matches p.domain\n      (b.deployment::jsonb ->> 'projectDomain') = p.domain\n      -- Check if 'domains' contains p.domain or d.domain\n      OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[p.domain])\n      OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[d.domain])\n  )\nORDER BY b.\"createdAt\" DESC\nLIMIT 1;\n\n$_$;\n\n```\n\nCreate a pgTAP test for the function \"public\".\"latestBuildVirtual\" that takes \"public\".\"Project\" as an argument.\n\n---\n\n## Additional Instructions\n\nThese instructions are sometimes necessary, while at other times they may not be required.\n\nAll fields, such as `projectId`, already reference their corresponding table columns (e.g., `\"Project\".\"id\"`), but foreign key constraints are omitted.\n\n- **Data Integrity:** Do not create new tables for `\"public\".\"Project\"` or related entities. These tables already exist in the database. DO NOT TRUNCATE DROP or DELETE Existing tables.\n\n- **Data Accuracy:** When inserting new data, adhere strictly to the NULL constraints. Ensure all fields without default values are filled correctly in every `INSERT` statement.\n  you may omit fields with default values if they are not directly relevant to the tests, except for `\"createdAt\"` and `\"updatedAt\"` fields, which should always be specified with explicit timestamp values.\n\n- **Timestamp Fields:** For fields like `\"createdAt\"` and `\"updatedAt\"`, always use specific timestamp values rather than functions like `NOW()`.\n\n- **Naming Conventions:** Since field and function names use camelCase, always wrap them in double quotes (`\"\"`) to ensure proper referencing.\n\n- **Test Coverage:** In addition to standard tests, cover additional cases to validate that the function returns the correct data in different scenarios:\n\n  1. When a project has changed its domain.\n  2. When builds with the new domain do not exist and when they do exist.\n  3. When the domain is either in `Build.deployment.projectDomain` or included in `Build.deployment.domains`.\n\n- **Function Calls:** Use precise casting for function calls, such as `(p.*)::Project`, to ensure accuracy in test execution.\n\nEnsure the tests comprehensively validate the function’s behavior in all specified cases.\n\nIn addition to the specific test cases mentioned, any other test scenarios the model identifies would be highly appreciated.\n\nExamples of calling the function:\n\n```sql\n\nselect \"public\".\"latestBuildVirtual\"((p.*)::\"Project\") from \"public\".\"Project\" p where p.id = '1';\n```\n\n---\n\n## PS\n\nInitially, it may produce non-working examples. However, it seems that a multi-step improvement process can enhance the results. By incorporating the additions mentioned above, you can achieve better final outcomes. While not perfect, it serves as a good starting point.\n\nSeems like the shorter the input the better the results. The models are seems to struggle with longer inputs.\n"
  },
  {
    "path": "packages/postgrest/supabase/tests/cleanup-builds.sql",
    "content": "BEGIN;\nSET LOCAL search_path = pgtap, public;\n-- Initialize the testing environment without planning any specific number of tests\n-- We are using SELECT no_plan() because we don't specify the exact number of tests upfront.\nSELECT no_plan();\n\n-- =========================================\n-- Setup: Insert initial data for Users, Projects, Domains, ProjectDomains, and Builds\n-- =========================================\n\n-- Insert a user into the \"User\" table\nINSERT INTO \"public\".\"User\" (\"id\", \"createdAt\", \"email\", \"username\")\nVALUES\n  ('user1', '2023-01-01 00:00:00+00', 'user1@517cce32-9af3-example.com', 'user1');\n\n-- Insert projects associated with the user into the \"Project\" table\nINSERT INTO \"public\".\"Project\" (\"id\", \"title\", \"domain\", \"userId\", \"isDeleted\", \"createdAt\")\nVALUES\n  ('project1', 'Project One', '517cce32-9af3-project1-domain1', 'user1', false, '2023-01-01 00:00:00+00'),\n  ('project2', 'Project Two', '517cce32-9af3-project2-domain1', 'user1', false, '2023-01-01 00:00:00+00');\n\n-- Insert custom domains into the \"Domain\" table\nINSERT INTO \"public\".\"Domain\" (\"id\", \"domain\", \"createdAt\", \"status\", \"updatedAt\")\nVALUES\n  ('project-1-custom-domain-1', '517cce32-9af3-project-1-custom-domain-1.com', '2023-01-01 00:00:00+00', 'INITIALIZING', '2023-01-01 00:00:00+00'),\n  ('project-1-custom-domain-2', '517cce32-9af3-project-1-custom-domain-2.com', '2023-01-01 00:00:00+00', 'INITIALIZING', '2023-01-01 00:00:00+00'),\n  ('project-2-custom-domain-1', '517cce32-9af3-project-2-custom-domain-1.com', '2023-01-01 00:00:00+00', 'INITIALIZING', '2023-01-01 00:00:00+00'),\n  ('project-2-custom-domain-2', '517cce32-9af3-project-2-custom-domain-2.com', '2023-01-01 00:00:00+00', 'INITIALIZING', '2023-01-01 00:00:00+00');\n\n-- Establish relationships between projects and custom domains in the \"ProjectDomain\" table\nINSERT INTO \"public\".\"ProjectDomain\" (\"projectId\", \"domainId\", \"createdAt\", \"txtRecord\", \"cname\")\nVALUES\n  ('project1', 'project-1-custom-domain-1', '2023-01-01 00:00:00+00', 'txtRecord1', 'cname1'),\n  ('project1', 'project-1-custom-domain-2', '2023-01-01 00:00:00+00', 'txtRecord2', 'cname2'),\n  ('project2', 'project-2-custom-domain-1', '2023-01-01 00:00:00+00', 'p2-txtRecord1', 'cname1'),\n  ('project2', 'project-2-custom-domain-2', '2023-01-01 00:00:00+00', 'p2-txtRecord2', 'cname2');\n\n-- Insert initial builds into the \"Build\" table\nINSERT INTO \"public\".\"Build\" (\n    \"id\",\n    \"createdAt\",\n    \"pages\",\n    \"projectId\",\n    \"deployment\",\n    \"updatedAt\",\n    \"publishStatus\"\n)\nVALUES\n  -- Development Build for Project1\n  (\n    'build1-development',\n    '1990-01-01 00:00:00+00',\n    'home',\n    'project1',\n    NULL,\n    '1990-01-01 00:00:00+00',\n    'PENDING'\n  ),\n  -- Development Build for Project2\n  (\n    'project2-build1-development',\n    '1990-01-01 00:00:00+00',\n    'home',\n    'project2',\n    NULL,\n    '1990-01-01 00:00:00+00',\n    'PENDING'\n  ),\n  -- Custom Domain Build for Project2\n  (\n    'project2-build1-for-custom-domain-1',\n    '1990-01-02 00:00:00+00',\n    'home',\n    'project2',\n    '{\"domains\": [\"517cce32-9af3-project-2-custom-domain-1.com\"]}'::text,\n    '1990-01-02 00:00:00+00',\n    'PUBLISHED'\n  ),\n  -- Project Domain Build for Project2\n  (\n    'project2-build1-for-project-domain-1',\n    '1990-01-01 00:00:00+00',\n    'home',\n    'project2',\n    '{\"domains\": [\"517cce32-9af3-project2-domain1\"]}'::text,\n    '1990-01-01 00:00:00+00',\n    'PUBLISHED'\n  ),\n  -- Custom Domain Build for Project1\n  (\n    'build1-for-custom-domain-1',\n    '1990-01-02 00:00:00+00',\n    'home',\n    'project1',\n    '{\"domains\": [\"517cce32-9af3-project-1-custom-domain-1.com\"]}'::text,\n    '1990-01-02 00:00:00+00',\n    'PUBLISHED'\n  ),\n  -- Project Domain Build for Project1\n  (\n    'build1-for-project-domain-1',\n    '1990-01-01 00:00:00+00',\n    'home',\n    'project1',\n    '{\"domains\": [\"517cce32-9af3-project1-domain1\"]}'::text,\n    '1990-01-01 00:00:00+00',\n    'PUBLISHED'\n  );\n\n-- =========================================\n-- Test 1: Verify that initial cleanup does not clean any builds\n-- =========================================\n\n-- Run the database cleanup function for builds created in 1990\nSELECT database_cleanup('1990-01-01 00:00:00', '1990-12-31 23:59:59');\n\n-- Assert that no builds have been cleaned up initially\nSELECT is(\n  (SELECT count(*)::integer FROM \"Build\" WHERE \"createdAt\" BETWEEN '1990-01-01' AND '1990-12-31' AND \"isCleaned\" = TRUE),\n  0,\n  'Test 1: No builds should be cleaned up on initial cleanup'\n);\n\n-- =========================================\n-- Test 2: Insert a new project domain build and verify cleanup\n-- =========================================\n\n-- Insert a new build for the project domain\nINSERT INTO \"public\".\"Build\" (\n    \"id\",\n    \"createdAt\",\n    \"pages\",\n    \"projectId\",\n    \"deployment\",\n    \"updatedAt\",\n    \"publishStatus\"\n)\nVALUES\n  -- New Project Domain Build for Project1\n  (\n    'build2-for-project-domain-1',\n    '1990-01-02 00:00:00+00',  -- A later date than the previous build\n    'home',\n    'project1',\n    '{\"domains\": [\"517cce32-9af3-project1-domain1\"]}'::text,\n    '1990-01-02 00:00:00+00',\n    'PUBLISHED'\n  );\n\n-- Run the database cleanup function again\nSELECT database_cleanup('1990-01-01 00:00:00', '1990-12-31 23:59:59');\n\n-- Assert that one build has been cleaned (the previous project domain build)\nSELECT is(\n  (SELECT count(*)::integer FROM \"Build\" WHERE \"createdAt\" BETWEEN '1990-01-01' AND '1990-12-31' AND \"isCleaned\" = TRUE),\n  1,\n  'Test 2: Previous project domain build should be cleaned up after new build is published'\n);\n\n-- Assert that the specific build cleaned is 'build1-for-project-domain-1'\nSELECT is(\n  (SELECT id FROM \"Build\" WHERE \"createdAt\" BETWEEN '1990-01-01' AND '1990-12-31' AND \"isCleaned\" = TRUE),\n  'build1-for-project-domain-1',\n  'Test 2: Build \"build1-for-project-domain-1\" should be marked as cleaned'\n);\n\n-- =========================================\n-- Test 3: Insert a new custom domain build and verify cleanup\n-- =========================================\n\n-- Insert a new build for the custom domain\nINSERT INTO \"public\".\"Build\" (\n    \"id\",\n    \"createdAt\",\n    \"pages\",\n    \"projectId\",\n    \"deployment\",\n    \"updatedAt\",\n    \"publishStatus\"\n)\nVALUES\n  -- New Custom Domain Build for Project1\n  (\n    'build2-for-custom-domain-1',\n    '1990-01-03 00:00:00+00',  -- A later date than the previous build\n    'home',\n    'project1',\n    '{\"domains\": [\"517cce32-9af3-project-1-custom-domain-1.com\"]}'::text,\n    '1990-01-03 00:00:00+00',\n    'PUBLISHED'\n  );\n\n-- Run the database cleanup function again\nSELECT database_cleanup('1990-01-01 00:00:00', '1990-12-31 23:59:59');\n\n-- Assert that two builds have been cleaned (the previous project domain and custom domain builds)\nSELECT is(\n  (SELECT count(*)::integer FROM \"Build\" WHERE \"createdAt\" BETWEEN '1990-01-01' AND '1990-12-31' AND \"isCleaned\" = TRUE),\n  2,\n  'Test 3: Previous custom domain build should be cleaned up after new build is published'\n);\n\n-- Assert that the builds cleaned are 'build1-for-custom-domain-1' and 'build1-for-project-domain-1'\nSELECT results_eq(\n  $$\n      SELECT id FROM \"Build\" WHERE \"createdAt\" BETWEEN '1990-01-01' AND '1990-12-31' AND \"isCleaned\" = TRUE ORDER BY id\n  $$,\n  $$\n    SELECT * FROM (\n        VALUES\n            ('build1-for-custom-domain-1'),\n            ('build1-for-project-domain-1')\n    ) AS expected(id)\n    ORDER BY \"id\"\n  $$,\n  'Test 3: Builds \"build1-for-custom-domain-1\" and \"build1-for-project-domain-1\" should be marked as cleaned'\n);\n\n-- =========================================\n-- Finish the test\n-- =========================================\n\n-- Finish the test by calling the finish() function, which outputs the test summary\nSELECT finish();\n\n-- Rollback the transaction to ensure no changes are persisted in the database\nROLLBACK;\n"
  },
  {
    "path": "packages/postgrest/supabase/tests/latest-builds-domains.sql",
    "content": "BEGIN;\n\nSET\n    LOCAL search_path = pgtap,\n    public;\n\n-- SET LOCAL search_path = pgtap,public;\n-- Initialize the testing environment without planning any specific number of tests\nSELECT\n    no_plan();\n\n-- Insert a new user into the User table\nINSERT INTO\n    \"public\".\"User\" (\"id\", \"createdAt\", \"email\", \"username\")\nVALUES\n    (\n        'user1',\n        '2023-01-01 00:00:00+00',\n        'user1@517cce32-9af3-example.com',\n        'user1'\n    );\n\n-- Insert projects associated with the user\nINSERT INTO\n    \"public\".\"Project\" (\n        \"id\",\n        \"title\",\n        \"domain\",\n        \"userId\",\n        \"isDeleted\",\n        \"createdAt\"\n    )\nVALUES\n    (\n        'project1',\n        'Project One',\n        '517cce32-9af3-project1-domain1',\n        'user1',\n        false,\n        '2023-01-01 00:00:00+00'\n    ),\n    (\n        'project2',\n        'Project Two',\n        '517cce32-9af3-project2-domain1',\n        'user1',\n        false,\n        '2023-01-01 00:00:00+00'\n    );\n\n-- Insert custom domains into the Domain table\nINSERT INTO\n    \"public\".\"Domain\" (\n        \"id\",\n        \"domain\",\n        \"createdAt\",\n        \"status\",\n        \"updatedAt\"\n    )\nVALUES\n    (\n        'project-1-custom-domain-1',\n        '517cce32-9af3-project-1-custom-domain-1.com',\n        '2023-01-01 00:00:00+00',\n        'INITIALIZING',\n        '2023-01-01 00:00:00+00'\n    ),\n    (\n        'project-1-custom-domain-2',\n        '517cce32-9af3-project-1-custom-domain-2.com',\n        '2023-01-01 00:00:00+00',\n        'INITIALIZING',\n        '2023-01-01 00:00:00+00'\n    );\n\n-- Establish relationships between projects and custom domains\nINSERT INTO\n    \"public\".\"ProjectDomain\" (\n        \"projectId\",\n        \"domainId\",\n        \"createdAt\",\n        \"txtRecord\",\n        \"cname\"\n    )\nVALUES\n    (\n        'project1',\n        'project-1-custom-domain-1',\n        '2023-01-01 00:00:00+00',\n        'txtRecord1',\n        'cname1'\n    ),\n    (\n        'project1',\n        'project-1-custom-domain-2',\n        '2023-01-01 00:00:00+00',\n        'txtRecord2',\n        'cname2'\n    );\n\n-- Create a view to encapsulate the repetitive SELECT query for testing\nCREATE\nOR REPLACE VIEW \"public\".\"TestProjectDomains\" AS\nSELECT\n    pd.\"projectId\",\n    pd.\"domainId\",\n    lbv.\"buildId\"\nFROM\n    \"public\".\"ProjectDomain\" pd\n    LEFT JOIN LATERAL (\n        SELECT\n            *\n        FROM\n            \"latestBuildVirtual\"(\n                ROW(\n                    '',\n                    pd.\"domainId\",\n                    pd.\"projectId\",\n                    '',\n                    'INITIALIZING' :: \"DomainStatus\",\n                    NULL,\n                    'txt',\n                    'expectedTxt',\n                    'cname',\n                    TRUE,\n                    NOW(),\n                    NOW()\n                ) :: \"domainsVirtual\"\n            )\n    ) lbv ON TRUE\nORDER BY\n    pd.\"domainId\";\n\n--------------------------------------------------------------------------------\n-- Test Case 1: Initial State Without Builds\n--------------------------------------------------------------------------------\nSELECT\n    results_eq(\n        'SELECT * FROM \"public\".\"TestProjectDomains\" WHERE \"projectId\" = ''project1''',\n        $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    ('project1', 'project-1-custom-domain-1', NULL),\n                    ('project1', 'project-1-custom-domain-2', NULL)\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'Initial state without builds'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 2: After Inserting Build1 Associated with Custom Domain 1\n--------------------------------------------------------------------------------\n-- Insert a build associated with a custom domain\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build1-for-custom-domain-1',\n        '2023-01-02 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2023-01-02 00:00:00+00',\n        'PUBLISHED'\n    );\n\nSELECT\n    results_eq(\n        'SELECT * FROM \"public\".\"TestProjectDomains\" WHERE \"projectId\" = ''project1''',\n        $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    (\n                        'project1',\n                        'project-1-custom-domain-1',\n                        'build1-for-custom-domain-1'\n                    ),\n                    ('project1', 'project-1-custom-domain-2', NULL)\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'After inserting build1 associated with custom domain 1'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 3: After Inserting Build2 Associated with Custom Domain 1\n--------------------------------------------------------------------------------\n-- Insert a new build associated with the same custom domain\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build2-for-custom-domain-1',\n        '2024-01-02 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2024-01-02 00:00:00+00',\n        'PUBLISHED'\n    );\n\nSELECT\n    results_eq(\n        'SELECT * FROM \"public\".\"TestProjectDomains\" WHERE \"projectId\" = ''project1''',\n        $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    (\n                        'project1',\n                        'project-1-custom-domain-1',\n                        'build2-for-custom-domain-1'\n                    ),\n                    ('project1', 'project-1-custom-domain-2', NULL)\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'After inserting build2 associated with custom domain 1'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 4: After Inserting Build3 Associated with Custom Domain 2\n--------------------------------------------------------------------------------\n-- Insert a new build associated with another custom domain\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build3-for-custom-domain-2',\n        '2024-01-03 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-2.com\"]}' :: text,\n        '2024-01-03 00:00:00+00',\n        'PUBLISHED'\n    );\n\nSELECT\n    results_eq(\n        'SELECT * FROM \"public\".\"TestProjectDomains\" WHERE \"projectId\" = ''project1''',\n        $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    (\n                        'project1',\n                        'project-1-custom-domain-1',\n                        'build2-for-custom-domain-1'\n                    ),\n                    (\n                        'project1',\n                        'project-1-custom-domain-2',\n                        'build3-for-custom-domain-2'\n                    )\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'After inserting build3 associated with custom domain 2'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 5: After Inserting Build4 Associated with Both Domains\n--------------------------------------------------------------------------------\n-- Insert a new build associated with both custom domains\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build4-for-both-domains',\n        '2024-01-04 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-2.com\", \"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2024-01-04 00:00:00+00',\n        'PUBLISHED'\n    );\n\nSELECT\n    results_eq(\n        'SELECT * FROM \"public\".\"TestProjectDomains\" WHERE \"projectId\" = ''project1''',\n        $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    (\n                        'project1',\n                        'project-1-custom-domain-1',\n                        'build4-for-both-domains'\n                    ),\n                    (\n                        'project1',\n                        'project-1-custom-domain-2',\n                        'build4-for-both-domains'\n                    )\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'After inserting build4 associated with both domains'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 6: Insert Builds and Relationships for Project2\n--------------------------------------------------------------------------------\n-- Establish relationships between project2 and the same custom domains\nINSERT INTO\n    \"public\".\"ProjectDomain\" (\n        \"projectId\",\n        \"domainId\",\n        \"createdAt\",\n        \"txtRecord\",\n        \"cname\"\n    )\nVALUES\n    (\n        'project2',\n        'project-1-custom-domain-1',\n        '2023-01-01 00:00:00+00',\n        'txtRecord21',\n        'cname21'\n    ),\n    (\n        'project2',\n        'project-1-custom-domain-2',\n        '2023-01-01 00:00:00+00',\n        'txtRecord22',\n        'cname22'\n    );\n\n-- Insert a new build for project2 associated with both domains\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build5-project2-for-both-domains',\n        '2025-01-04 00:00:00+00',\n        'home',\n        'project2',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-2.com\", \"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2025-01-04 00:00:00+00',\n        'PUBLISHED'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 7: Verify Results for Project1 Remain Unchanged After Project2 Updates\n--------------------------------------------------------------------------------\nSELECT\n    results_eq(\n        'SELECT * FROM \"public\".\"TestProjectDomains\" WHERE \"projectId\" = ''project1''',\n        $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    (\n                        'project1',\n                        'project-1-custom-domain-1',\n                        'build4-for-both-domains'\n                    ),\n                    (\n                        'project1',\n                        'project-1-custom-domain-2',\n                        'build4-for-both-domains'\n                    )\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'Verify Project1 results remain unchanged after Project2 updates'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 8: Verify Results for Project2 Reflect Latest Build\n--------------------------------------------------------------------------------\nSELECT\n    results_eq(\n        $$\n        SELECT\n            *\n        FROM\n            \"public\".\"TestProjectDomains\"\n        WHERE\n            \"projectId\" = 'project2' $$,\n            $$\n        SELECT\n            *\n        FROM\n            (\n                VALUES\n                    (\n                        'project2',\n                        'project-1-custom-domain-1',\n                        'build5-project2-for-both-domains'\n                    ),\n                    (\n                        'project2',\n                        'project-1-custom-domain-2',\n                        'build5-project2-for-both-domains'\n                    )\n            ) AS expected(projectId, domainId, buildId)\n        ORDER BY\n            domainId $$,\n            'Verify Project2 results reflect the latest build'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 9: Verify updatedAt Field for domainsVirtual Function\n--------------------------------------------------------------------------------\n-- Insert a build with specific updatedAt value for project1 custom domain 1\n-- This build should be selected as latest because it has the newest createdAt\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build-with-updatedAt-test',\n        '2025-01-10 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2024-06-15 12:30:00+00',\n        'PUBLISHED'\n    );\n\n-- Verify updatedAt is returned correctly from the latest build\nSELECT\n    is (\n        (\n            SELECT\n                \"updatedAt\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    ROW(\n                        '',\n                        'project-1-custom-domain-1',\n                        'project1',\n                        '',\n                        'INITIALIZING' :: \"DomainStatus\",\n                        NULL,\n                        'txt',\n                        'expectedTxt',\n                        'cname',\n                        TRUE,\n                        NOW(),\n                        NOW()\n                    ) :: \"domainsVirtual\"\n                )\n        ),\n        '2024-06-15 12:30:00+00' :: timestamp with time zone,\n        'Test Case 9: updatedAt field should be returned correctly from latestBuildVirtual(domainsVirtual).'\n    );\n\n-- Finalize the tests\nSELECT\n    finish();\n\nROLLBACK;"
  },
  {
    "path": "packages/postgrest/supabase/tests/latest-builds-projects.sql",
    "content": "BEGIN;\n\nSET\n    LOCAL search_path = pgtap,\n    public;\n\n-- Initialize the testing environment without planning any specific number of tests\nSELECT\n    no_plan();\n\n--------------------------------------------------------------------------------\n-- Setup: Insert Initial Data\n--------------------------------------------------------------------------------\n-- Insert a new user into the User table\nINSERT INTO\n    \"public\".\"User\" (\"id\", \"createdAt\", \"email\", \"username\")\nVALUES\n    (\n        'user1',\n        '2023-01-01 00:00:00+00',\n        'user1@example.com',\n        'user1'\n    );\n\n-- Insert projects associated with the user\nINSERT INTO\n    \"public\".\"Project\" (\n        \"id\",\n        \"title\",\n        \"domain\",\n        \"userId\",\n        \"isDeleted\",\n        \"createdAt\"\n    )\nVALUES\n    (\n        'project1',\n        'Project One',\n        '517cce32-9af3-project1-domain1',\n        'user1',\n        false,\n        '2023-01-01 00:00:00+00'\n    ),\n    (\n        'project2',\n        'Project Two',\n        '517cce32-9af3-project2-domain1',\n        'user1',\n        false,\n        '2023-01-01 00:00:00+00'\n    );\n\n-- Insert builds with different deployment formats\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    -- Old deployment format: includes projectDomain\n    (\n        'build1',\n        '2023-01-01 00:00:00+00',\n        'home',\n        'project1',\n        '{\"projectDomain\": \"517cce32-9af3-project1-domain1\", \"domains\": [\"\"]}' :: text,\n        '2023-01-01 00:00:00+00',\n        'PUBLISHED'\n    ),\n    (\n        'build1-old',\n        '2022-01-01 00:00:00+00',\n        'home',\n        'project1',\n        '{\"projectDomain\": \"517cce32-9af3-project1-domain1\", \"domains\": [\"\"]}' :: text,\n        '2022-01-01 00:00:00+00',\n        'PUBLISHED'\n    ),\n    (\n        'build1-newest-wrong-domain',\n        '2024-01-01 00:00:00+00',\n        'home',\n        'project1',\n        '{\"projectDomain\": \"project-wrong\", \"domains\": [\"\"]}' :: text,\n        '2024-01-01 00:00:00+00',\n        'PUBLISHED'\n    ),\n    -- New deployment format: domains array only\n    (\n        'build2',\n        '2023-01-02 00:00:00+00',\n        'home',\n        'project2',\n        '{\"domains\": [\"517cce32-9af3-project2-domain1\"]}' :: text,\n        '2023-01-02 00:00:00+00',\n        'PENDING'\n    ),\n    (\n        'build2-old',\n        '2022-01-02 00:00:00+00',\n        'home',\n        'project2',\n        '{\"domains\": [\"517cce32-9af3-project2-domain1\"]}' :: text,\n        '2022-01-02 00:00:00+00',\n        'PENDING'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 1: Verify Latest Build Retrieval Using Old Deployment Format (projectDomain)\n--------------------------------------------------------------------------------\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1', '517cce32-9af3-project1-domain1'],\n        'Test Case 1.1: Should return the latest build for project1 with domain matching projectDomain.'\n    );\n\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1', '517cce32-9af3-project1-domain1'],\n        'Test Case 1.2: Should return the latest build for project1 with domain matching projectDomain.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 2: Verify Latest Build Retrieval Using New Deployment Format (domains array)\n--------------------------------------------------------------------------------\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project2'\n                    )\n                )\n        ),\n        'build2',\n        'Test Case 2.1: Should return the latest build for project2 with domain present in domains array.'\n    );\n\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project2'\n                    )\n                )\n        ),\n        'build2',\n        'Test Case 2.2: Should return the latest build for project2 domain with domain present in domains array.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 3: Update Project Domain and Verify No Build Exists for the New Domain\n--------------------------------------------------------------------------------\n-- Update project1's domain to a new domain\nUPDATE\n    \"public\".\"Project\"\nSET\n    \"domain\" = 'project1-domain2'\nWHERE\n    \"id\" = 'project1';\n\n-- Verify that no build exists for the updated domain\nSELECT\n    is (\n        (\n            SELECT\n                COUNT(*) :: integer\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        0,\n        'Test Case 3.1: Should return 0 as no build exists for the updated domain project1-domain2.'\n    );\n\nSELECT\n    is (\n        (\n            SELECT\n                COUNT(*) :: integer\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        0,\n        'Test Case 3.2: Should return 0 as no build exists for the updated domain project1-domain2.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 4: Insert a New Build with the Updated Domain and Verify Retrieval\n--------------------------------------------------------------------------------\n-- Insert a new build associated with the updated domain\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build1-for-domain2',\n        '2023-01-01 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"project1-domain2\"]}' :: text,\n        '2023-01-01 00:00:00+00',\n        'PUBLISHED'\n    );\n\n-- Verify that the latest build now reflects the updated domain\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1-for-domain2','project1-domain2'],\n        'Test Case 4.1: Should return the latest build for project1 with the updated domain in domains array.'\n    );\n\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1-for-domain2','project1-domain2'],\n        'Test Case 4.2: Should return the latest build for project1 domain with the updated domain in domains array.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 5: Register Custom Domains and Verify Latest Build for a Custom Domain\n--------------------------------------------------------------------------------\n-- Insert custom domains\nINSERT INTO\n    \"public\".\"Domain\" (\n        \"id\",\n        \"domain\",\n        \"createdAt\",\n        \"status\",\n        \"updatedAt\"\n    )\nVALUES\n    (\n        'project-1-custom-domain-1',\n        '517cce32-9af3-project-1-custom-domain-1.com',\n        '2023-01-01 00:00:00+00',\n        'INITIALIZING',\n        '2023-01-01 00:00:00+00'\n    ),\n    (\n        'project-1-custom-domain-2',\n        '517cce32-9af3-project-1-custom-domain-2.com',\n        '2023-01-01 00:00:00+00',\n        'INITIALIZING',\n        '2023-01-01 00:00:00+00'\n    );\n\n-- Establish relationships between project1 and custom domains\nINSERT INTO\n    \"public\".\"ProjectDomain\" (\n        \"projectId\",\n        \"domainId\",\n        \"createdAt\",\n        \"txtRecord\",\n        \"cname\"\n    )\nVALUES\n    (\n        'project1',\n        'project-1-custom-domain-1',\n        '2023-01-01 00:00:00+00',\n        'txtRecord1',\n        'cname1'\n    ),\n    (\n        'project1',\n        'project-1-custom-domain-2',\n        '2023-01-01 00:00:00+00',\n        'txtRecord2',\n        'cname2'\n    );\n\n-- Insert a build associated with a custom domain\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build1-for-custom-domain-1',\n        '2023-01-02 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2023-01-02 00:00:00+00',\n        'PUBLISHED'\n    );\n\n-- Verify that the latest build reflects the custom domain association\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1-for-custom-domain-1','517cce32-9af3-project-1-custom-domain-1.com'],\n        'Test Case 5.1: Should return the latest build for project1 with a registered custom domain in domains array.'\n    );\n\n-- Ensure the latest project domain build has not changed\n-- The difference between latestProjectDomainBuildVirtual and latestBuildVirtual is that the first returns data only for the project domain\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1-for-domain2','project1-domain2'],\n        'Test Case 5.2: Should return the latest build for project1 domain and not affected by custom domains'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 6: Publish a Preview Domain and Verify Latest Build Retrieval\n--------------------------------------------------------------------------------\n-- Insert a build for the preview domain using the new deployment format\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build1-for-domain2-new',\n        '2023-01-03 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"517cce32-9af3-project-1-custom-domain-1.com\", \"project1-domain2\"]}' :: text,\n        '2023-01-03 00:00:00+00',\n        'PUBLISHED'\n    );\n\n-- Verify that the latest build reflects the preview domain\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1-for-domain2-new', 'project1-domain2'],\n        'Test Case 6.1: Should return the latest build for project1 with the preview domain in domains array.'\n    );\n\nSELECT\n    is (\n        (\n            SELECT\n                ARRAY [\"buildId\", \"domain\"]\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        ARRAY ['build1-for-domain2-new', 'project1-domain2'],\n        'Test Case 6.2: Should return the latest build for project1 with the preview domain in domains array.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 7: Publish a New Build for a Custom Domain, Delete the Custom Domain, and Verify Latest Build Update\n--------------------------------------------------------------------------------\n-- Insert a new build for the custom domain\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    (\n        'build1-for-custom-domain-1-new',\n        '2023-01-04 00:00:00+00',\n        'home',\n        'project1',\n        '{\"domains\": [\"some-other-domain.com\", \"517cce32-9af3-project-1-custom-domain-1.com\"]}' :: text,\n        '2023-01-04 00:00:00+00',\n        'PUBLISHED'\n    );\n\n-- Verify that the latest build reflects the newly published build for the custom domain\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        'build1-for-custom-domain-1-new',\n        'Test Case 7a: Should return the latest build after publishing a new build for a custom domain.'\n    );\n\n-- Delete the custom domain association\nDELETE FROM\n    \"public\".\"ProjectDomain\"\nWHERE\n    \"projectId\" = 'project1'\n    AND \"domainId\" = 'project-1-custom-domain-1';\n\n-- Verify that the latest build reverts to the previous latest build after deletion\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        'build1-for-domain2-new',\n        'Test Case 7b: Should return the latest build after deleting the custom domain, reverting to the previous latest build.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 8: Publish a New Build for a Custom Domain, Move the Custom Domain, and Verify Latest Build Update\n--------------------------------------------------------------------------------\n-- Re-establish the custom domain association to revert Test Case 7\nINSERT INTO\n    \"public\".\"ProjectDomain\" (\n        \"projectId\",\n        \"domainId\",\n        \"createdAt\",\n        \"txtRecord\",\n        \"cname\"\n    )\nVALUES\n    (\n        'project1',\n        'project-1-custom-domain-1',\n        '2023-01-01 00:00:00+00',\n        'txtRecord1',\n        'cname1'\n    );\n\n-- Verify that the latest build is updated after re-establishing the custom domain\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        'build1-for-custom-domain-1-new',\n        'Test Case 8a: Should return the latest build after re-publishing a new build for a custom domain.'\n    );\n\n-- Move the custom domain association from project1 to project2\nUPDATE\n    \"public\".\"ProjectDomain\"\nSET\n    \"projectId\" = 'project2'\nWHERE\n    \"projectId\" = 'project1'\n    AND \"domainId\" = 'project-1-custom-domain-1';\n\n-- Verify that the latest build reverts to the previous latest build after moving the custom domain\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project1'\n                    )\n                )\n        ),\n        'build1-for-domain2-new',\n        'Test Case 8b: Should return the latest build after moving the custom domain, reverting to the previous latest build.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 9: Verify updatedAt Field is Returned (Selection is Still by createdAt)\n--------------------------------------------------------------------------------\n-- Insert builds with different createdAt and updatedAt\n-- NOTE: \"Latest build\" is determined by createdAt (when published), not updatedAt\nINSERT INTO\n    \"public\".\"Build\" (\n        \"id\",\n        \"createdAt\",\n        \"pages\",\n        \"projectId\",\n        \"deployment\",\n        \"updatedAt\",\n        \"publishStatus\"\n    )\nVALUES\n    -- Older createdAt but newer updatedAt\n    (\n        'build-updated-recently',\n        '2023-01-01 00:00:00+00',\n        'home',\n        'project2',\n        '{\"domains\": [\"517cce32-9af3-project2-domain1\"]}' :: text,\n        '2024-01-01 00:00:00+00',\n        'PUBLISHED'\n    ),\n    -- Newer createdAt but older updatedAt - SHOULD be selected as \"latest\"\n    (\n        'build-created-recently',\n        '2023-06-01 00:00:00+00',\n        'home',\n        'project2',\n        '{\"domains\": [\"517cce32-9af3-project2-domain1\"]}' :: text,\n        '2023-01-15 00:00:00+00',\n        'PUBLISHED'\n    );\n\n-- Verify that the build with newest createdAt is selected (not newest updatedAt)\nSELECT\n    is (\n        (\n            SELECT\n                \"buildId\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project2'\n                    )\n                )\n        ),\n        'build-created-recently',\n        'Test Case 9.1: Should select build with newest createdAt (selection is by createdAt, not updatedAt).'\n    );\n\n-- Verify that updatedAt field is returned (even though it is not the newest)\nSELECT\n    is (\n        (\n            SELECT\n                \"updatedAt\"\n            FROM\n                \"public\".\"latestBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project2'\n                    )\n                )\n        ),\n        '2023-01-15 00:00:00+00' :: timestamp with time zone,\n        'Test Case 9.2: updatedAt field from the selected build should be returned in latestBuildVirtual result.'\n    );\n\nSELECT\n    is (\n        (\n            SELECT\n                \"updatedAt\"\n            FROM\n                \"public\".\"latestProjectDomainBuildVirtual\"(\n                    (\n                        SELECT\n                            (p.*) :: \"Project\"\n                        FROM\n                            \"public\".\"Project\" p\n                        WHERE\n                            p.\"id\" = 'project2'\n                    )\n                )\n        ),\n        '2023-01-15 00:00:00+00' :: timestamp with time zone,\n        'Test Case 9.3: updatedAt field from the selected build should be returned in latestProjectDomainBuildVirtual result.'\n    );\n\n--------------------------------------------------------------------------------\n-- Test Case 10: Verify All Columns Are Returned in Correct Order\n--------------------------------------------------------------------------------\n-- Check that all 7 columns are present in the result\nSELECT\n    ok(\n        (\n            SELECT\n                COUNT(*) = 7\n            FROM\n                information_schema.columns\n            WHERE\n                table_name = 'latestBuildVirtual'\n        ),\n        'Test Case 10: latestBuildVirtual should have exactly 7 columns (buildId, projectId, domainsVirtualId, domain, createdAt, publishStatus, updatedAt).'\n    );\n\n-- Verify column names match expected structure  \nSELECT\n    bag_eq(\n        $$\n        SELECT\n            column_name :: text\n        FROM\n            information_schema.columns\n        WHERE\n            table_name = 'latestBuildVirtual' $$,\n            $$\n        VALUES\n            ('buildId'),\n            ('projectId'),\n            ('domainsVirtualId'),\n            ('domain'),\n            ('createdAt'),\n            ('publishStatus'),\n            ('updatedAt') $$,\n            'Test Case 10.1: All expected columns should be present in latestBuildVirtual table.'\n    );\n\n-- Finalize the tests\nSELECT\n    finish();\n\nROLLBACK;"
  },
  {
    "path": "packages/postgrest/supabase/tests/project-domains.sql",
    "content": "BEGIN;\nSET LOCAL search_path = pgtap, public;\n-- Initialize the testing environment without planning any specific number of tests\n-- We are using SELECT no_plan() because we don't specify the exact number of tests upfront.\nSELECT no_plan();\n\n-- Insert a new user into the User table for the project ownership\n-- We're inserting user_1 as the user for the test projects\nINSERT INTO \"public\".\"User\" (\"id\", \"createdAt\", \"email\", \"username\")\nVALUES\n  ('user_1', '2023-01-01 00:00:00+00', 'user1@517cce32-9af3-example.com', 'user1');\n\n-- Insert test projects into the Project table\n-- project_1 and project_2 belong to user_1 and are not deleted (isDeleted = false)\nINSERT INTO \"Project\" (id, title, domain, \"userId\", \"isDeleted\") VALUES\n('project_1', 'Test Project 1', '517cce32-9af3-testproject1.com', 'user_1', false),\n('project_2', 'Test Project 1', '517cce32-9af3-testproject2.com', 'user_1', false);\n\n-- Insert test domains into the Domain table\n-- We are inserting two domains: 517cce32-9af3-example.com and 517cce32-9af3-example.org with different statuses\nINSERT INTO \"Domain\" (id, domain, status, \"txtRecord\") VALUES\n('domain_1', '517cce32-9af3-example.com', 'INITIALIZING', 'txtRecord1'),\n('domain_2', '517cce32-9af3-example.org', 'ACTIVE', 'txtRecord21');\n\n-- Insert test data into the ProjectDomain table\n-- Mapping domains to projects, project_1 has two domains, project_2 has one domain\n-- Note the different TXT records in ProjectDomain\nINSERT INTO \"ProjectDomain\" (\"projectId\", \"domainId\", \"txtRecord\", \"cname\") VALUES\n('project_1', 'domain_1', 'txtRecord1', 'cname1'),\n('project_1', 'domain_2', 'txtRecord22', 'cname2'),\n('project_2', 'domain_1', 'txtRecord3', 'cname3');\n\n-- Test case 1: Verify that domainsVirtual returns correct values for project_1\n-- Compare the result of the domainsVirtual function with an expected result set.\n-- Project_1 is expected to return two domains with statuses, TXT records, and verification status\nSELECT results_eq(\n  $$\n      SELECT domain, status, error, \"domainTxtRecord\", \"expectedTxtRecord\", verified\n      FROM \"domainsVirtual\"(\n        (SELECT (p.*)::\"Project\" FROM \"Project\" p WHERE p.id = 'project_1' ORDER BY p.id)\n      )\n  $$,\n  $$\n    SELECT * FROM (\n        VALUES\n            ('517cce32-9af3-example.com','INITIALIZING'::\"DomainStatus\",NULL,E'txtRecord1',E'txtRecord1',TRUE), -- Verified domain (TXT records match)\n            ('517cce32-9af3-example.org','ACTIVE'::\"DomainStatus\",NULL,E'txtRecord21',E'txtRecord22',FALSE) -- Not verified domain (TXT records do not match)\n    ) AS expected(domain, status, error, \"domainTxtRecord\", \"expectedTxtRecord\", verified)\n    ORDER BY \"domain\"\n  $$,\n  'Test case 1: domainsVirtual should return correct results for project_1'\n);\n\n-- Test case 2: Verify that domainsVirtual returns correct values for project_2\n-- Project_2 is expected to return one domain with its corresponding status, TXT records, and verification status\nSELECT results_eq(\n  $$\n      SELECT domain, status, error, \"domainTxtRecord\", \"expectedTxtRecord\", verified\n      FROM \"domainsVirtual\"(\n        (SELECT (p.*)::\"Project\" FROM \"Project\" p WHERE p.id = 'project_2' ORDER BY p.id)\n      )\n  $$,\n  $$\n    SELECT * FROM (\n        VALUES\n            ('517cce32-9af3-example.com','INITIALIZING'::\"DomainStatus\",NULL,E'txtRecord1',E'txtRecord3',FALSE) -- Not verified domain (TXT records do not match)\n    ) AS expected(domain, status, error, \"domainTxtRecord\", \"expectedTxtRecord\", verified)\n    ORDER BY \"domain\"\n  $$,\n  'Test case 2: domainsVirtual should return correct results for project_2'\n);\n\n-- Finish the test by calling the finish() function, which outputs the test summary\nSELECT finish();\n\n-- Rollback the transaction to ensure no changes are persisted in the database\nROLLBACK;"
  },
  {
    "path": "packages/postgrest/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/prisma-client/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/prisma-client/README.md",
    "content": "# Webstudio Prisma client\n\nPackage conatining our API to interact with prisma\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/README.md",
    "content": "## Intro\n\nThis is a drop-in replacement for Prisma's migration engine. It adds:\n\n- an ability to write a data migration in TypeScript,\n- an ability to write a rollback for a data migration or schema migration (to be implemented).\n\nA regular Prisma's migrations directory looks like this:\n\n```\nmigrations/\n  20220601192603_start/migration.sql\n  20220608130959_adduser/migration.sql\n```\n\nWith the new engine it will look something like this:\n\n```\nmigrations/\n  20220601192603_start/\n    migration.sql\n  20220608130959_adduser/\n    migration.sql\n    rollback.sql\n  20220608130959_movedata/\n    client/\n      ...\n    migration.ts\n    rollback.ts\n```\n\nIn a TypeScript migration file, you can use a Prisma client generated specifically for this migration. This client is frozen in time at the moment when you create a migration. So if at a later point a model will be removed from the main Prisma schema, in the migration you'll still have access to the old model. In other words, the Client in a migration corresponds to a state of the database where the preceding migrations have been applied, but the succeeding haven't.\n\n## Use cases\n\n### How do I start using the new engine?\n\n- Stop using `prisma migrate *` commands\n- Start using `migrations *` commands instead\n\nNOTE: If this becomes a library there should be more istructions here.\n\n### How do I setup a new database for development?\n\n- Install Postgres and create a new database.\n- Make sure your `schema.prisma` file points to the correct database.\n- Apply migrations by running `migrations migrate --dev`.\n\n### I've pulled in migrations created by someone else. How do I apply them?\n\n- Run `migrations migrate --dev`\n\n### I've changed schema only, no data changes. How do I migrate database schema?\n\n- Make changes to `schema.prisma`.\n- Create a schema migration by running `migrations create-schema <name> --dev`.\n- Apply the migration by running `migrations migrate --dev`.\n\n### I want to change the data, without schema changes. How do I migrate data only?\n\n- Create a data migration by running `migrations create-data <name>`.\n- Edit the data migration file.\n- Apply the migration using `migrations migrate --dev`.\n\n### I need to change the schema and migrate the data.\n\n- Make changes to `schema.prisma` in a way that **both** the old and the new locations for the data are defined.\n- Create a schema migration by running `migrations create-schema <name>`.\n- And the new schema by running `migrations migrate --dev`.\n- Create a data migration by running `migrations create-data <name>`.\n- Edit the data migration file to move the data to the new location.\n- Apply the migration for data using the same command `migrations migrate --dev`.\n- Make changes to `schema.prisma` to remove the old models or fields that are no longer needed.\n- Create a schema migration by running `migrations create-schema <name>`.\n- Apply schema migration `migrations migrate --dev`.\n\n### A migration has failed. What do I do?\n\nYou have several options:\n\n1. If you have a backup, you can restore it.\n1. If you don't care about the data in the database, you can reset the database by running `migrations reset [--dev]`.\n1. Fix the issues manually.\n   - Figure out what changes the migration managed to make to the database.\n   - Revert the changes manually by any means you like. E.g. using a Postgres client.\n   - Alternatively, manually perform the remaining steps of the migration.\n   - Run `migrations resolve applied <name>` or `migrations resolve rolled-back <name>` to mark the migration as applied or rolled-back.\n\n### How do I apply migrations in a deployment environment?\n\n- Make sure your `schema.prisma` file points to the correct database.\n- Add `migrations migrate` to your deploy script.\n\n## CLI Reference\n\n### `--dev`\n\nThis flag can be added to any command. It informs the CLI that it’s used in a development environment, which slightly changes its behaviour:\n\n- automatically loads variables from `.env` files,\n- uses a lockfile to avoid running more than one migration process at the same time.\n\n### `create-schema <name>`\n\nExample: `$ migrations create-schema add_projects_table`\n\nCreates a schema migration. Compares the `schema.prisma` with the actual tables in the database, and creates a migration that changes the database to match the schema.\n\nNote: this may cause a loss of data if the migration removes tables or columns. Open the generated `migration.sql` file to see the warnings about potential data losses. Unless the data is not needed, you should first move it to a new location using a data-migration, and only then delete the old tables or columns.\n\n### `create-data <name>`\n\nExample: `$ migrations create-data move_projects_to_new_table`\n\nCreates a data migration. Creates a migration with an empty `migration.ts` file, which you can open in an editor and write the actual migration code.\n\n### `migrate`\n\nExample: `$ migrations migrate`\n\nApplies all pending migrations. Looks for migrations in the migrations directory that have not been applied to the database yet, and applies them.\n\n### `reset`\n\nExample: `$ migrations reset`\n\nDeletes all data from the database, and applies all migrations again.\n\n### `status`\n\nExample: `$ migrations status`\n\nDisplays the status of the migrations. Such as which migrations have been applied, which are pending, etc.\n\n### `resolve <applied|rolled-back> <name>`\n\nExample: `$ migrations resolve applied 20220905153337_move_projects_to_new_table`\n\nMarks a failed migration as applied or rolled back. You can see information about failed migrations using the `status` command.\n\nNote: this does not fix any issues that might have been caused by the failed run of the migration. You need to investigate and fix them manually before running the `resolve` command.\n\n## Comparison to Prisma (v4.x)\n\n<!-- prettier-ignore-start -->\n| Action | Prisma command | Our command | Notable differences |\n| -- | -- | -- | -- |\n| Creating a schema migration | `prisma migrate dev --create-only` | `migrations create-schema` | If there are pending migrations, Prisma will apply them. We will ask the user to apply. |\n| Creating a data migration | N/a | `migrations create-data`  | |\n| Applying migrations in dev | `prisma migrate dev` | `migrations migrate --dev` | We don't do [schema drift detection](https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#detecting-schema-drift) |\n| Applying migrations in prod | `prisma migrate deploy` | `migrations migrate`  | |\n| Resolving failed migrations | `prisma migrate resolve --<applied\\|rolled-back> <name>` | `migrations resolve <applied\\|rolled-back> <name>` | |\n| Status of migrations | `prisma migrate status` | `migrations status` | |\n| Resetting database | `prisma migrate reset` | `migrations reset --dev` | |\n<!-- prettier-ignore-end -->\n\nAlso, if a migration file of an applied migration is missing or have been modified, Prisma may treats this as a fatal issue. We also detect these issues and they appear in the `status` output, but we don't do anything beyond that.\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/TROUBLESHOOTING.md",
    "content": "## A migration failed in PREVIEW, how do I fix the PREVIEW database\n\n1. Open a new PR where you'll do the fix\n1. Go to https://vercel.com/webstudio-is/webstudio/settings/environment-variables\n1. Set APPLY_MIGRATIONS to \"true\" for your PR's branch\n1. Fix the failed migration code\n1. Fix the database mannually if the migration have been applied half-way\n1. Temporarily change `ci:migrate` script to run `resolve` before applying migrations: `\"ci:migrate\": \"migrations resolve rolled-back <name> --force && migrations migrate\"`. Use `applied` instead of `rolled-back` if you manually applied the migration at previous step.\n1. Commit the changes and check Vercel logs to see if the migration is applied\n1. Change `ci:migrate` script back to normal\n1. Merge the PR\n1. Remove APPLY_MIGRATIONS from Vercel settings for your branch\n\n## Transacation failed with a error \"Transaction API error: Transaction already closed: Could not perform operation.\"\n\nMost likely reason Prisma has thrown this error is because transaction has timed out.\nIf that's the case, you have the following options:\n\n1. Optimize your migration to make it faster\n2. Increase the timeout:\n\n```js\nexport default () => {\n  const client = new PrismaClient();\n  return client.$transaction(\n    async (prisma) => {\n      // migration code...\n    },\n    {\n      timeout: 1000 * 60 * 2, // in milliseconds, default 5000\n    }\n  );\n};\n```\n\n3. Do not wrap the migration body into a transaction:\n\n```js\nexport default async () => {\n  const prisma = new PrismaClient();\n  // use `prisma` here without wrapping it into a transaction\n};\n```\n\nNote that in this case if the migration fails again, it will be harder to fix the database,\nbecause the migration can end up half-way applied.\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/args.ts",
    "content": "import { parseArgs } from \"node:util\";\n\nexport const { values, positionals } = parseArgs({\n  args: process.argv.slice(2),\n  allowPositionals: true,\n  options: {\n    cwd: {\n      type: \"string\",\n      default: \"./\",\n    },\n    dev: {\n      type: \"boolean\",\n    },\n  },\n});\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/cli.ts",
    "content": "#!/usr/bin/env tsx\n\nimport { chdir, loadEnvFile } from \"node:process\";\nimport * as commands from \"./commands\";\nimport * as logger from \"./logger\";\nimport * as args from \"./args\";\nimport { UserError } from \"./errors\";\n\nchdir(args.values.cwd);\n\nconst USAGE = `Usage: migrations <command> [--dev]\n\nCommands:\n  create-schema <name> — Create a migration based on the changes in schema.prisma\n  create-data <name>   — Create a migration that will change data rather than schema\n  migrate              — Apply all pending migrations\n  status               — Information about the state of the migrations\n  pending-count        — Get the number of pending migrations\n  resolve <applied|rolled-back> <name> — Mark a failed migration as applied or rolled-back\n\nArguments\n  --dev                — Lets the CLI know that it's running in a development environment\n`;\n\nconst main = async () => {\n  if (args.values.dev) {\n    try {\n      loadEnvFile(\".env.development\");\n    } catch {\n      // empty block\n    }\n    try {\n      loadEnvFile(\".env\");\n    } catch {\n      // empty block\n    }\n  }\n\n  const command = args.positionals[0];\n\n  if (command === undefined) {\n    logger.info(USAGE);\n    return;\n  }\n\n  if (command === \"create-schema\") {\n    const name = args.positionals[1];\n    if (name === undefined) {\n      throw new UserError(\n        \"Missing name for migration.\\nUsage: migrations create-schema <name>\"\n      );\n    }\n    await commands.createSchema({ name });\n    return;\n  }\n\n  if (command === \"create-data\") {\n    const name = args.positionals[1];\n    if (name === undefined) {\n      throw new UserError(\n        \"Missing name for migration.\\nUsage: migrations create-data <name>\"\n      );\n    }\n    await commands.createData({ name });\n    return;\n  }\n\n  if (command === \"migrate\") {\n    await commands.migrate();\n    return;\n  }\n\n  if (command === \"status\") {\n    await commands.status();\n    return;\n  }\n\n  if (command === \"pending-count\") {\n    const count = await commands.pendingCount();\n    console.info(\"::pending-count::\", count);\n    return;\n  }\n\n  if (command === \"resolve\") {\n    const type = args.positionals[1];\n\n    if (type === undefined || (type !== \"applied\" && type !== \"rolled-back\")) {\n      throw new UserError(\n        \"Missing type of resolve.\\nUsage: migrations resolve <applied|rolled-back> <migration-name>\"\n      );\n    }\n\n    const name = args.positionals[2];\n    if (name === undefined) {\n      throw new UserError(\n        \"Missing name of migration.\\nUsage: migrations resolve <applied|rolled-back> <migration-name>\"\n      );\n    }\n\n    await commands.resolve({ migrationName: name, resolveAs: type });\n    return;\n  }\n\n  throw new UserError(`Unknown command: ${command}`);\n};\n\nconst runMain = async () => {\n  try {\n    await main();\n  } catch (error) {\n    if (error instanceof UserError) {\n      logger.error(error.message);\n      logger.error(\"\");\n      process.exit(1);\n    } else {\n      throw error;\n    }\n  }\n};\n\nrunMain();\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/commands.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { FileLocker, MigrationMeta } from \"umzug\";\nimport { inspect } from \"node:util\";\nimport * as prismaMigrations from \"./prisma-migrations\";\nimport { umzug } from \"./umzug\";\nimport * as logger from \"./logger\";\nimport * as args from \"./args\";\nimport { UserError } from \"./errors\";\n\nconst templateFilePath = path.join(\n  prismaMigrations.migrationsDir,\n  \"template.txt\"\n);\nconst lockfilePath = path.join(prismaMigrations.migrationsDir, \"lockfile\");\n\nconst writeFile = (filePath: string, content: string) => {\n  const dir = path.dirname(filePath);\n  if (!fs.existsSync(dir)) {\n    fs.mkdirSync(dir, { recursive: true });\n  }\n  fs.writeFileSync(filePath, content);\n};\n\ntype StartedMigration = {\n  migration: prismaMigrations.PrismaMigration;\n  state: \"applied\" | \"failed\" | \"rolled-back\";\n  fileState: \"ok\" | \"changed\" | \"deleted\";\n};\n\ntype Status = {\n  started: StartedMigration[];\n  pending: MigrationMeta[];\n};\n\nconst getStatus = async (): Promise<Status> => {\n  const dbMigrations = await prismaMigrations.getMigrations();\n\n  const started = dbMigrations.map((migration): StartedMigration => {\n    const filePathTS = prismaMigrations.getMigrationFilePath(\n      migration.migration_name,\n      \"ts\"\n    );\n    const filePathSQL = prismaMigrations.getMigrationFilePath(\n      migration.migration_name,\n      \"sql\"\n    );\n    const filePath = fs.existsSync(filePathTS) ? filePathTS : filePathSQL;\n\n    const fileState = fs.existsSync(filePath)\n      ? prismaMigrations.getFileChecksum(filePath) === migration.checksum\n        ? \"ok\"\n        : \"changed\"\n      : \"deleted\";\n\n    const state = prismaMigrations.isAppliedMigration(migration)\n      ? \"applied\"\n      : prismaMigrations.isFailedMigration(migration)\n        ? \"failed\"\n        : \"rolled-back\";\n\n    return { migration, state, fileState };\n  });\n\n  return {\n    started,\n    pending: await umzug.pending(),\n  };\n};\n\nconst ensureNoFailed = (status: Status) => {\n  const failed = status.started.filter(\n    (migration) => migration.state === \"failed\"\n  );\n  if (failed.length > 0) {\n    throw new UserError(\n      `There are failed migrations:\\n${failed\n        .map((item) => {\n          let text = `  - ${item.migration.migration_name}`;\n          const logs = (item.migration.logs || \"\").trim();\n          if (logs.length > 0) {\n            text += `\\n\\n${logs}\\n`;\n          }\n          return text;\n        })\n        .join(\n          \"\\n\"\n        )}\\n\\nPlease mark them as resolved or reset the database before you can proceed.`\n    );\n  }\n};\n\nconst ensureNoPending = (status: Status) => {\n  if (status.pending.length > 0) {\n    throw new UserError(\n      `There are pending migrations:\\n${status.pending\n        .map((migration) => `  - ${migration.name}`)\n        .join(\n          \"\\n\"\n        )}\\n\\nPlease apply them first before you can create a new schema migration.\\nIf this is a migration you created locally, you can also delete it if you want to start over.`\n    );\n  }\n};\n\nconst ensureLastPending = async (migrationsName: string) => {\n  const pending = await umzug.pending();\n  const last = pending[pending.length - 1];\n\n  if (last === undefined || last.name !== migrationsName) {\n    throw new UserError(\n      \"The migrations you've created did not appear as the last pending migration. This isn't supposed to happen. Please investigate / report this issue.\"\n    );\n  }\n};\n\nconst isNoopSql = (sqlScript: string) => {\n  return (\n    sqlScript\n      .split(\"\\n\")\n      // When there are no changes, prisma generates \"-- This is an empty migration\"\n      .filter((line) => line.startsWith(\"--\") === false)\n      .join(\"\\n\")\n      .trim() === \"\"\n  );\n};\n\nconst ensureNoChangesInPrismaSchema = async () => {\n  const sqlScript = await prismaMigrations.cliDiff();\n  if (isNoopSql(sqlScript ?? \"\") === false) {\n    throw new UserError(\n      \"There are changes in schema.prisma. Please create a schema migration first.\"\n    );\n  }\n};\n\nexport const createSchema = async ({ name }: { name: string }) => {\n  const status = await getStatus();\n\n  ensureNoFailed(status);\n\n  // Can't proceed if there are pending migrations.\n  // We need the database to be up to date before we can do a diff.\n  ensureNoPending(status);\n\n  const sqlScript = await prismaMigrations.cliDiff();\n\n  const migrationName = prismaMigrations.generateMigrationName(name);\n\n  const filePath = prismaMigrations.getMigrationFilePath(migrationName, \"sql\");\n\n  writeFile(filePath, sqlScript);\n  logger.info(`Created: ${filePath}`);\n\n  logger.info(\"\");\n\n  await ensureLastPending(migrationName);\n\n  logger.info(\"The migration is ready. You can now apply it to a database.\");\n  logger.info(\"\");\n};\n\nexport const createData = async ({ name }: { name: string }) => {\n  const status = await getStatus();\n\n  ensureNoFailed(status);\n  ensureNoPending(status);\n  await ensureNoChangesInPrismaSchema();\n\n  const migrationName = prismaMigrations.generateMigrationName(name);\n\n  const filePath = prismaMigrations.getMigrationFilePath(migrationName, \"ts\");\n  writeFile(filePath, fs.readFileSync(templateFilePath, \"utf8\"));\n  logger.info(`Created: ${filePath}`);\n\n  let schemaContent = fs.readFileSync(prismaMigrations.schemaFilePath, \"utf8\");\n\n  schemaContent = `// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n${schemaContent}`;\n\n  schemaContent = schemaContent.replace(\n    /\\/\\/[\\s]*<output-placeholder-for-migrations>[\\s\\S]*?\\/\\/[\\s]*<\\/output-placeholder-for-migrations>/g,\n    `output = \"client\"`\n  );\n\n  const schemaFilePath = path.join(\n    prismaMigrations.migrationsDir,\n    migrationName,\n    \"schema.prisma\"\n  );\n  writeFile(schemaFilePath, schemaContent);\n  logger.info(`Created: ${schemaFilePath}`);\n\n  await prismaMigrations.generateMigrationClient(migrationName);\n  logger.info(\n    `Created: ${path.join(\n      prismaMigrations.migrationsDir,\n      migrationName,\n      \"client\"\n    )}`\n  );\n\n  logger.info(\"\");\n\n  await ensureLastPending(migrationName);\n\n  logger.info(\n    \"The migrations templete is ready. You can now edit the migration.ts file and apply it to a database.\"\n  );\n  logger.info(\"\");\n};\n\nconst up = async () => {\n  let locker: FileLocker | undefined;\n  if (args.values.dev) {\n    locker = new FileLocker({ path: lockfilePath });\n  }\n\n  if (locker) {\n    try {\n      await locker.getLock();\n    } catch (error) {\n      throw new UserError(\n        `Could not acquire lock!\nThis means that another process is already running migrations.\nIf you're sure no other process is running, please delete the lockfile:\n  $ rm ${lockfilePath}`\n      );\n    }\n  }\n\n  try {\n    await umzug.up();\n  } catch (error) {\n    const originalError: unknown = error.cause || error;\n    const originalErrorString =\n      (originalError instanceof Error && originalError.stack) ||\n      inspect(originalError);\n\n    logger.error(\"\");\n    logger.error(originalErrorString);\n    logger.error(\"\");\n\n    const migrationName = (error.migration || undefined)?.name;\n    if (typeof migrationName === \"string\") {\n      prismaMigrations.setFailed(migrationName, originalErrorString);\n    }\n\n    process.exitCode = 1;\n  } finally {\n    if (locker) {\n      await locker.releaseLock();\n    }\n  }\n};\n\nexport const migrate = async () => {\n  const status = await getStatus();\n\n  ensureNoFailed(status);\n\n  if (status.pending.length === 0) {\n    logger.info(\"No pending migrations\\n\");\n    process.exit(0);\n  }\n\n  await up();\n};\n\nexport const status = async () => {\n  const status = await getStatus();\n\n  logger.info(\n    `Applied or failed: ${status.started.length === 0 ? \"none\" : \"\"}`\n  );\n  for (const migration of status.started) {\n    const fileState =\n      migration.fileState === \"ok\" ? \"\" : `, file ${migration.fileState}`;\n    logger.info(\n      `  - ${migration.migration.migration_name} (${migration.state}${fileState})`\n    );\n    const logs = (migration.migration.logs || \"\").trim();\n    if (migration.state === \"failed\" && logs.length > 0) {\n      logger.info(\"\");\n      logger.info(logs);\n      logger.info(\"\");\n    }\n  }\n\n  logger.info(\"\");\n  logger.info(`Pending: ${status.pending.length === 0 ? \"none\" : \"\"}`);\n  for (const migration of status.pending) {\n    logger.info(`  - ${migration.name}`);\n  }\n\n  logger.info(\"\");\n};\n\nexport const pendingCount = async () => {\n  const status = await getStatus();\n\n  return status.pending.length;\n};\n\n// Silimar to https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-resolve\nexport const resolve = async ({\n  migrationName,\n  resolveAs,\n}: {\n  migrationName: string;\n  resolveAs: \"applied\" | \"rolled-back\";\n}) => {\n  const status = await getStatus();\n\n  const failed = status.started.filter((m) => m.state === \"failed\");\n\n  if (\n    failed.some(\n      ({ migration }) => migration.migration_name === migrationName\n    ) === false\n  ) {\n    throw new UserError(\n      `Migration ${migrationName} is not failed. You can resolve only a failed migration.`\n    );\n  }\n\n  logger.info(`You're about to mark ${migrationName} as ${resolveAs}.`);\n  logger.info(\"This will NOT automatically resolve any issues\");\n  logger.info(\"that may have been caused by the failed run.\");\n  logger.info(\n    \"You should continue only if you're sure the issues have been resolved.\"\n  );\n  logger.info(\"\");\n\n  if (resolveAs === \"applied\") {\n    await prismaMigrations.setApplied(migrationName);\n    logger.info(`Resolved ${migrationName} as applied`);\n    logger.info(\"\");\n    return;\n  }\n\n  await prismaMigrations.setRolledBack(migrationName);\n  logger.info(`Resolved ${migrationName} as rolled back`);\n  logger.info(\"\");\n};\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/errors.ts",
    "content": "// Throw this error to indicate to CLI that this a error caused by user actions\n// as opposed to a bug, so it won't print the stacktrace\nexport class UserError extends Error {}\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/logger.ts",
    "content": "// When you're adding a log for debugging use console.info as normal.\n// Use logger only for CLI output.\n\nexport const info = (message: unknown) => {\n  console.info(message);\n};\n\nexport const error = (message: unknown) => {\n  console.error(message);\n};\n\nexport const warn = (message: unknown) => {\n  console.warn(message);\n};\n\nexport const debug = (message: unknown) => {\n  console.info(message);\n};\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/prisma-migrations.ts",
    "content": "// Code for interacting with the Prisma's migration engine.\n// We want to preserve semantics of the migrations folder and the _prisma_migrations table.\n// https://github.com/prisma/prisma-engines/blob/4.3.0/migration-engine/ARCHITECTURE.md\n\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { createHash } from \"node:crypto\";\nimport { x } from \"tinyexec\";\nimport { createPrisma } from \"../src/prisma\";\nimport { UserError } from \"./errors\";\nimport { PrismaClient } from \"../src/__generated__\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport const prismaDir = path.resolve(__dirname, \"..\", \"prisma\");\nexport const schemaFilePath = path.join(prismaDir, \"schema.prisma\");\nexport const migrationsDir = path.join(prismaDir, \"migrations\");\n\nlet prisma_: PrismaClient | undefined;\n\nconst context = {\n  // delay prisma initialization until it's actually needed\n  // this is needed as we read dotenv in the main file\n  get prisma() {\n    if (process.env.DIRECT_URL === undefined) {\n      throw new Error(\"DIRECT_URL is not set\");\n    }\n\n    prisma_ =\n      prisma_ ??\n      createPrisma({\n        datasourceUrl: process.env.DIRECT_URL,\n        // 10 minutes\n        timeout: 10 * 60 * 1000,\n        maxWait: 5000,\n      });\n\n    return prisma_;\n  },\n};\n\nexport const getMigrationFilePath = (\n  migrationName: string,\n  type: \"ts\" | \"sql\"\n) => path.join(migrationsDir, migrationName, `migration.${type}`);\n\nexport const ensureMigrationTable = async () => {\n  // https://github.com/prisma/prisma-engines/blob/4.3.0/migration-engine/ARCHITECTURE.md#the-_prisma_migrations-table\n  // https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/connectors/sql-migration-connector/src/flavour/postgres.rs#L211-L226\n  await context.prisma\n    .$executeRaw`CREATE TABLE IF NOT EXISTS _prisma_migrations (\n    id                      VARCHAR(36) PRIMARY KEY NOT NULL,\n    checksum                VARCHAR(64) NOT NULL,\n    finished_at             TIMESTAMPTZ,\n    migration_name          VARCHAR(255) NOT NULL,\n    logs                    TEXT,\n    rolled_back_at          TIMESTAMPTZ,\n    started_at              TIMESTAMPTZ NOT NULL DEFAULT now(),\n    applied_steps_count     INTEGER NOT NULL DEFAULT 0\n  )`;\n};\n\n// Fields' descriptions are quoted from ARCHITECTURE.md\n// (\"we\" refers to the Prisma team)\nexport type PrismaMigration = {\n  // A random unique identifier. In practice, a v4 UUID.\n  id: string;\n\n  // The sha256 checksum of the migration file.\n  // We never ovewrite this once it has been written.\n  checksum: string;\n\n  // The timestamp at which the migration completed.\n  // We only write this at the end of a successful migration,\n  // so this column being not null means the migration completed without error.\n  finished_at: Date | null;\n\n  // The complete name of the migration directory (without path prefix).\n  migration_name: string;\n\n  // Where we record the error, in case of error.\n  logs: string | null;\n\n  // Written by prisma migrate resolve,\n  // and causes the row to be ignored by migrate when not null.\n  rolled_back_at: Date | null;\n\n  // The creation timestamp of the row in the migrations table.\n  // We write this before starting to apply the migration.\n  started_at: Date;\n\n  // Should be considered deprecated.\n  applied_steps_count: number;\n};\n\nexport const getMigrations = async () => {\n  await ensureMigrationTable();\n\n  return context.prisma.$queryRaw<\n    PrismaMigration[]\n  >`select * from _prisma_migrations order by migration_name`;\n};\n\nconst getByName = async (\n  migrationName: string\n): Promise<PrismaMigration | undefined> => {\n  await ensureMigrationTable();\n\n  const migrations = await context.prisma.$queryRaw<\n    PrismaMigration[]\n  >`select * from _prisma_migrations where migration_name = ${migrationName}`;\n  return migrations[0];\n};\n\n// https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/connectors/migration-connector/src/checksum.rs\nexport const getFileChecksum = (filePath: string) => {\n  const input = fs.readFileSync(filePath);\n  return createHash(\"sha256\").update(input).digest(\"hex\");\n};\n\nexport const setStarted = async (migrationName: string, filePath: string) => {\n  await ensureMigrationTable();\n\n  await context.prisma.$executeRaw`delete from _prisma_migrations\n                            where id in (\n                              select id from _prisma_migrations\n                              where migration_name = ${migrationName}\n                              and rolled_back_at is not null\n                              limit 1\n                            )`;\n\n  const existingMigration = await getByName(migrationName);\n\n  // This shouldn't happen normally, checking just in case\n  if (existingMigration !== undefined) {\n    throw new Error(\n      `Can't start ${migrationName}. It has been already started before.`\n    );\n  }\n\n  const checksum = getFileChecksum(filePath);\n\n  await context.prisma.$executeRaw`insert into _prisma_migrations (\n                            id,\n                            checksum,\n                            migration_name\n                          ) values (\n                            gen_random_uuid()::text,\n                            ${checksum},\n                            ${migrationName}\n                          )`;\n};\n\nexport const setFailed = async (migrationName: string, error: string) => {\n  await ensureMigrationTable();\n\n  const existingMigration = await getByName(migrationName);\n\n  // The error accured before we started the migration\n  // No need to do anything\n  if (existingMigration === undefined) {\n    return;\n  }\n\n  // This shouldn't happen normally, checking just in case\n  if (isAppliedMigration(existingMigration)) {\n    throw new Error(\n      `Can't set ${migrationName} as failed. It has already been applied.`\n    );\n  }\n\n  await context.prisma.$executeRaw`update _prisma_migrations set logs = ${error}\n                            where migration_name = ${migrationName}`;\n};\n\nexport const setApplied = async (migrationName: string) => {\n  await ensureMigrationTable();\n\n  const existingMigration = await getByName(migrationName);\n\n  // This shouldn't happen normally, checking just in case\n  if (existingMigration === undefined) {\n    throw new Error(\n      `Can't set ${migrationName} as applied. It hasn't been started yet.`\n    );\n  }\n\n  // This shouldn't happen normally, checking just in case\n  if (isAppliedMigration(existingMigration)) {\n    throw new Error(\n      `Can't set ${migrationName} as applied. It has already been applied.`\n    );\n  }\n\n  // https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/core/src/commands/apply_migrations.rs#L72-L73\n  await context.prisma.$executeRaw`update _prisma_migrations set\n                            finished_at = now(),\n                            applied_steps_count = applied_steps_count + 1\n                          where migration_name = ${migrationName}`;\n};\n\nexport const setRolledBack = async (migrationName: string) => {\n  await ensureMigrationTable();\n\n  const existingMigration = await getByName(migrationName);\n\n  // This shouldn't happen normally, checking just in case\n  if (existingMigration === undefined) {\n    throw new Error(\n      `Can't set ${migrationName} as rolled back. It hasn't been started yet.`\n    );\n  }\n\n  await context.prisma\n    .$executeRaw`update _prisma_migrations set rolled_back_at = now()\n                            where migration_name = ${migrationName}`;\n};\n\n// https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/core/src/commands/diagnose_migration_history.rs#L109-L111\n// https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/core/src/commands/apply_migrations.rs#L108-L111\nexport const isFailedMigration = (migration: PrismaMigration) =>\n  migration.rolled_back_at === null && migration.finished_at === null;\n\n// https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/core/src/commands/diagnose_migration_history.rs#L119-L128\nexport const isAppliedMigration = (migration: PrismaMigration) =>\n  migration.finished_at !== null && migration.rolled_back_at === null;\n\nexport const isRolledBackMigration = (migration: PrismaMigration) =>\n  migration.rolled_back_at !== null;\n\n// https://github.com/prisma/prisma-engines/blob/88f6ab88e559ef52ab26bc98f1da15200e0c25b4/migration-engine/connectors/migration-connector/src/migrations_directory.rs#L30-L35\nexport const generateMigrationName = (baseName: string) => {\n  const date = new Date();\n  const prefix =\n    date.getFullYear() +\n    [\n      date.getMonth() + 1,\n      date.getDate(),\n      date.getHours(),\n      date.getMinutes(),\n      date.getSeconds(),\n    ]\n      .map((item) => item.toString().padStart(2, \"0\"))\n      .join(\"\");\n\n  // it's VARCHAR(255) in _prisma_migrations\n  return `${prefix}_${baseName}`.slice(0, 254);\n};\n\n// https://www.prisma.io/docs/reference/api-reference/command-reference#migrate-diff\nexport const cliDiff = async () => {\n  const { stdout } = await x(\n    \"prisma\",\n    [\n      \"migrate\",\n      \"diff\",\n      `--from-schema-datasource=${schemaFilePath}`,\n      `--to-schema-datamodel=${schemaFilePath}`,\n      \"--script\",\n    ],\n    {\n      nodeOptions: { cwd: prismaDir },\n    }\n  );\n  return stdout;\n};\n\n// https://www.prisma.io/docs/reference/api-reference/command-reference#db-execute\nexport const cliExecute = async (filePath: string) => {\n  await x(\n    \"prisma\",\n    [\"db\", \"execute\", `--file=${filePath}`, `--schema=${schemaFilePath}`],\n    {\n      nodeOptions: { cwd: prismaDir },\n    }\n  );\n};\n\nexport const generateMigrationClient = async (migrationName: string) => {\n  const migrationDir = path.join(migrationsDir, migrationName);\n\n  const schemaPath = path.join(migrationDir, \"schema.prisma\");\n  const clientPath = path.join(migrationDir, \"client\");\n\n  if (fs.existsSync(schemaPath) === false) {\n    const tsFilePath = getMigrationFilePath(migrationName, \"ts\");\n    if (fs.existsSync(tsFilePath)) {\n      throw new UserError(\n        `Can't generate client for ${migrationName} because ${migrationName}/schema.prisma is missing`\n      );\n    }\n    return;\n  }\n\n  if (fs.existsSync(clientPath)) {\n    fs.rmSync(clientPath, { recursive: true });\n  }\n\n  // https://www.prisma.io/docs/reference/api-reference/command-reference#generate\n  await x(\"prisma\", [\"generate\", `--schema=${schemaPath}`], {\n    nodeOptions: { cwd: prismaDir },\n  });\n};\n"
  },
  {
    "path": "packages/prisma-client/migrations-cli/umzug.ts",
    "content": "import { Umzug } from \"umzug\";\nimport fs from \"node:fs\";\nimport * as prismaMigrations from \"./prisma-migrations\";\nimport * as logger from \"./logger\";\nimport { UserError } from \"./errors\";\n\nexport const umzug = new Umzug({\n  migrations: {\n    glob: [\n      `./${\"[0-9]\".repeat(14)}_*`,\n      { cwd: prismaMigrations.migrationsDir },\n    ],\n    resolve(params) {\n      const sqlFilePath = prismaMigrations.getMigrationFilePath(\n        params.name,\n        \"sql\"\n      );\n      const tsFilePath = prismaMigrations.getMigrationFilePath(\n        params.name,\n        \"ts\"\n      );\n\n      if (fs.existsSync(sqlFilePath)) {\n        return {\n          ...params,\n          up: async () => {\n            await prismaMigrations.setStarted(params.name, sqlFilePath);\n            await prismaMigrations.cliExecute(sqlFilePath);\n          },\n        };\n      }\n\n      if (fs.existsSync(tsFilePath)) {\n        return {\n          ...params,\n          up: async () => {\n            await prismaMigrations.generateMigrationClient(params.name);\n\n            const migration = await import(tsFilePath);\n\n            if (typeof migration.default !== \"function\") {\n              throw new UserError(\n                `Migration file's ${tsFilePath} default export must be a function`\n              );\n            }\n\n            await prismaMigrations.setStarted(params.name, tsFilePath);\n            await migration.default();\n          },\n        };\n      }\n\n      throw new UserError(\n        `Couldn't find a migration.ts or migration.sql file in migrations/${params.name}`\n      );\n    },\n  },\n  context: {},\n  storage: {\n    logMigration(params) {\n      return prismaMigrations.setApplied(params.name);\n    },\n    unlogMigration() {\n      // this is for rollbacks, which we don't currently support\n      throw new Error(\"unlogMigration is not implemented\");\n    },\n    async executed() {\n      const migrations = await prismaMigrations.getMigrations();\n      return migrations\n        .filter(prismaMigrations.isAppliedMigration)\n        .map((row) => row.migration_name);\n    },\n  },\n  logger: {\n    ...logger,\n    info: (event) => {\n      if (event.event === \"migrating\") {\n        logger.info(`Starting ${event.name}`);\n        return;\n      }\n      if (event.event === \"migrated\") {\n        logger.info(`${event.name} done in ${event.durationSeconds}s`);\n        return;\n      }\n      if (event.event === \"up\") {\n        logger.info(event.message);\n        return;\n      }\n      if (event.event === \"created\") {\n        logger.info(`Created: ${event.path}`);\n        return;\n      }\n      logger.info(event);\n    },\n  },\n});\n"
  },
  {
    "path": "packages/prisma-client/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/prisma-client\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Prisma layer\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"generate\": \"PRISMA_BINARY_TARGET=${PRISMA_BINARY_TARGET:-'[\\\"native\\\"]'} prisma generate\",\n    \"migrations\": \"./migrations-cli/cli.ts\"\n  },\n  \"devDependencies\": {\n    \"@prisma/client\": \"^5.12.1\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"prisma\": \"5.12.1\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"exports\": {\n    \"types\": \"./lib/prisma.d.ts\",\n    \"import\": \"./prisma.mjs\",\n    \"require\": \"./prisma.cjs\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"dependencies\": {\n    \"nanoid\": \"^5.1.5\",\n    \"tinyexec\": \"^0.3.2\",\n    \"umzug\": \"^3.2.1\"\n  },\n  \"peerDependencies\": {\n    \"zod\": \"^3.19.1\"\n  },\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220601192603_start/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Project\" (\n    \"id\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"domain\" TEXT NOT NULL,\n    \"prodTreeId\" TEXT,\n    \"devTreeId\" TEXT NOT NULL,\n    \"prodTreeIdHistory\" TEXT NOT NULL DEFAULT E'[]',\n\n    CONSTRAINT \"Project_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" TEXT NOT NULL,\n\n    CONSTRAINT \"User_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Tree\" (\n    \"id\" TEXT NOT NULL,\n    \"root\" TEXT NOT NULL,\n\n    CONSTRAINT \"Tree_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"InstanceProps\" (\n    \"id\" TEXT NOT NULL,\n    \"instanceId\" TEXT NOT NULL,\n    \"treeId\" TEXT NOT NULL,\n    \"props\" TEXT NOT NULL DEFAULT E'[]',\n\n    CONSTRAINT \"InstanceProps_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Breakpoints\" (\n    \"treeId\" TEXT NOT NULL,\n    \"values\" TEXT NOT NULL DEFAULT E'[]',\n\n    CONSTRAINT \"Breakpoints_pkey\" PRIMARY KEY (\"treeId\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Project_domain_key\" ON \"Project\"(\"domain\");\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220608130924_/migration.sql",
    "content": "/*\n  Warnings:\n\n  - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.\n\n*/\n-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\nADD COLUMN     \"email\" TEXT,\nADD COLUMN     \"image\" TEXT,\nADD COLUMN     \"provider\" TEXT,\nADD COLUMN     \"username\" TEXT;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220608130959_adduser/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `createdAt` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `email` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `image` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `provider` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.\n\n*/\n-- DropIndex\nDROP INDEX \"User_email_key\";\n\n-- AlterTable\nALTER TABLE \"User\" DROP COLUMN \"createdAt\",\nDROP COLUMN \"email\",\nDROP COLUMN \"image\",\nDROP COLUMN \"provider\",\nDROP COLUMN \"username\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220608131719_add_user/migration.sql",
    "content": "/*\n  Warnings:\n\n  - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.\n\n*/\n-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\nADD COLUMN     \"email\" TEXT,\nADD COLUMN     \"image\" TEXT,\nADD COLUMN     \"provider\" TEXT,\nADD COLUMN     \"username\" TEXT;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220611090740_/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `createdAt` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `email` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `image` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `provider` on the `User` table. All the data in the column will be lost.\n  - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.\n\n*/\n-- DropIndex\nDROP INDEX \"User_email_key\";\n\n-- AlterTable\nALTER TABLE \"User\" DROP COLUMN \"createdAt\",\nDROP COLUMN \"email\",\nDROP COLUMN \"image\",\nDROP COLUMN \"provider\",\nDROP COLUMN \"username\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220611091346_add_email/migration.sql",
    "content": "/*\n  Warnings:\n\n  - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.\n\n*/\n-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\nADD COLUMN     \"email\" TEXT,\nADD COLUMN     \"image\" TEXT,\nADD COLUMN     \"provider\" TEXT,\nADD COLUMN     \"username\" TEXT;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User_email_key\" ON \"User\"(\"email\");\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220616143541_add_projects/migration.sql",
    "content": "-- AddForeignKey\nALTER TABLE \"Project\" ADD CONSTRAINT \"Project_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220616143902_userid_not_mandatory/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Project\" DROP CONSTRAINT \"Project_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Project\" ALTER COLUMN \"userId\" DROP NOT NULL;\n\n-- AddForeignKey\nALTER TABLE \"Project\" ADD CONSTRAINT \"Project_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220619163536_userid_mandatory/migration.sql",
    "content": "/*\n  Warnings:\n\n  - Made the column `userId` on table `Project` required. This step will fail if there are existing NULL values in that column.\n\n*/\n-- DropForeignKey\nALTER TABLE \"Project\" DROP CONSTRAINT \"Project_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Project\" ALTER COLUMN \"userId\" SET NOT NULL;\n\n-- AddForeignKey\nALTER TABLE \"Project\" ADD CONSTRAINT \"Project_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220624214305_teams/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Project\" DROP CONSTRAINT \"Project_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Project\" ADD COLUMN     \"teamId\" TEXT;\n\n-- AlterTable\nALTER TABLE \"User\" ADD COLUMN     \"teamId\" TEXT;\n\n-- CreateTable\nCREATE TABLE \"Team\" (\n    \"id\" TEXT NOT NULL,\n\n    CONSTRAINT \"Team_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"User\" ADD CONSTRAINT \"User_teamId_fkey\" FOREIGN KEY (\"teamId\") REFERENCES \"Team\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Project\" ADD CONSTRAINT \"Project_teamId_fkey\" FOREIGN KEY (\"teamId\") REFERENCES \"Team\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220624215036_remove_userid/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `userId` on the `Project` table. All the data in the column will be lost.\n\n*/\n-- AlterTable\nALTER TABLE \"Project\" DROP COLUMN \"userId\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220624235138_users_have_projects/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `teamId` on the `Project` table. All the data in the column will be lost.\n\n*/\n-- DropForeignKey\nALTER TABLE \"Project\" DROP CONSTRAINT \"Project_teamId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Project\" DROP COLUMN \"teamId\",\nADD COLUMN     \"userId\" TEXT;\n\n-- AddForeignKey\nALTER TABLE \"Project\" ADD CONSTRAINT \"Project_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220714112221_add_assets/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Asset\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"type\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"path\" TEXT NOT NULL,\n    \"size\" DOUBLE PRECISION NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"Asset_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"Asset\" ADD CONSTRAINT \"Asset_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220714114102_remove_size/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `size` on the `Asset` table. All the data in the column will be lost.\n\n*/\n-- AlterTable\nALTER TABLE \"Asset\" DROP COLUMN \"size\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220715192633_add_alt/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" ADD COLUMN     \"alt\" TEXT;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220716191150_add_more_info_to_asset/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `type` on the `Asset` table. All the data in the column will be lost.\n  - Added the required column `format` to the `Asset` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `height` to the `Asset` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `size` to the `Asset` table without a default value. This is not possible if the table is not empty.\n  - Added the required column `width` to the `Asset` table without a default value. This is not possible if the table is not empty.\n\n*/\n-- AlterTable\nALTER TABLE \"Asset\" DROP COLUMN \"type\",\nADD COLUMN     \"format\" TEXT NOT NULL,\nADD COLUMN     \"height\" INTEGER NOT NULL,\nADD COLUMN     \"size\" INTEGER NOT NULL,\nADD COLUMN     \"width\" INTEGER NOT NULL;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220716192051_make_metadata_not_mandatory/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" ALTER COLUMN \"format\" DROP NOT NULL,\nALTER COLUMN \"height\" DROP NOT NULL,\nALTER COLUMN \"width\" DROP NOT NULL;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220717152939_make_width_and_height_float/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" ALTER COLUMN \"height\" SET DATA TYPE DOUBLE PRECISION,\nALTER COLUMN \"width\" SET DATA TYPE DOUBLE PRECISION;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220717193140_make_width_and_height_decimal/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to alter the column `height` on the `Asset` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.\n  - You are about to alter the column `width` on the `Asset` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`.\n\n*/\n-- AlterTable\nALTER TABLE \"Asset\" ALTER COLUMN \"height\" SET DATA TYPE DECIMAL(65,30),\nALTER COLUMN \"width\" SET DATA TYPE DECIMAL(65,30);\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220722131820_remove_path/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the column `path` on the `Asset` table. All the data in the column will be lost.\n\n*/\n-- AlterTable\nALTER TABLE \"Asset\" DROP COLUMN \"path\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220722132445_add_location/migration.sql",
    "content": "/*\n  Warnings:\n\n  - Added the required column `location` to the `Asset` table without a default value. This is not possible if the table is not empty.\n\n*/\n-- CreateEnum\nCREATE TYPE \"Location\" AS ENUM ('FS', 'REMOTE');\n\n-- AlterTable\nALTER TABLE \"Asset\" ADD COLUMN     \"location\" \"Location\" NOT NULL;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220905153337_noop/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\nexport default () => {\n  const client = new PrismaClient();\n  return client.$transaction(async (prisma) => {\n    const usersCount = await prisma.user.count();\n\n    console.info(\"Example noop migration test\", { usersCount });\n  });\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220905153337_noop/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id        String   @id @default(uuid())\n  project   Project  @relation(fields: [projectId], references: [id])\n  projectId String\n  format    String?\n  size      Int\n  width     Decimal?\n  height    Decimal?\n  name      String\n  alt       String?\n  location  Location\n  createdAt DateTime @default(now())\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id                String  @id @default(uuid())\n  title             String\n  domain            String  @unique\n  prodTreeId        String? // exists when published\n  devTreeId         String\n  prodTreeIdHistory String  @default(\"[]\")\n  user              User?   @relation(fields: [userId], references: [id])\n  userId            String?\n  assets            Asset[]\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  treeId String @id @default(uuid())\n  values String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220909124449_builds/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Build\" (\n    \"id\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"pages\" TEXT NOT NULL,\n    \"isDev\" BOOLEAN NOT NULL,\n    \"isProd\" BOOLEAN NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n\n    CONSTRAINT \"Build_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- AddForeignKey\nALTER TABLE \"Build\" ADD CONSTRAINT \"Build_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220909124542_builds-data/migration.ts",
    "content": "import { PrismaClient, Prisma } from \"./client\";\nimport { z } from \"zod\";\n\nconst TreeHistory = z.array(z.string());\n\nconst Page = z.object({\n  id: z.string(),\n  name: z.string(),\n  path: z.string(),\n  title: z.string(),\n  meta: z.record(z.string(), z.string()),\n  treeId: z.string(),\n});\n\nconst Pages = z.object({\n  homePage: Page,\n  pages: z.array(Page),\n});\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      const trees = await prisma.tree.findMany();\n      const projects = await prisma.project.findMany();\n\n      const builds: Prisma.BuildCreateManyInput[] = [];\n\n      for (const tree of trees) {\n        const project = projects.find(\n          (project) =>\n            project.devTreeId === tree.id ||\n            project.prodTreeId === tree.id ||\n            TreeHistory.parse(JSON.parse(project.prodTreeIdHistory)).includes(\n              tree.id\n            )\n        );\n\n        if (project === undefined) {\n          continue;\n        }\n\n        const pages = Pages.parse({\n          homePage: {\n            id: crypto.randomUUID(),\n            name: \"Home\",\n            path: \"\",\n            title: \"Home\",\n            meta: {},\n            treeId: tree.id,\n          },\n          pages: [],\n        });\n\n        builds.push({\n          pages: JSON.stringify(pages),\n          isDev: project.devTreeId === tree.id,\n          isProd: project.prodTreeId === tree.id,\n          projectId: project.id,\n        });\n      }\n\n      await prisma.build.createMany({ data: builds });\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220909124542_builds-data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id        String   @id @default(uuid())\n  project   Project  @relation(fields: [projectId], references: [id])\n  projectId String\n  format    String?\n  size      Int\n  width     Decimal?\n  height    Decimal?\n  name      String\n  alt       String?\n  location  Location\n  createdAt DateTime @default(now())\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  assets Asset[]\n  build  Build[]\n\n  // tmp\n  prodTreeId        String? // exists when published\n  devTreeId         String\n  prodTreeIdHistory String  @default(\"[]\")\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n  isDev     Boolean // exctly one is true per project\n  isProd    Boolean // at most one is true per project (none if not published)\n  project   Project  @relation(fields: [projectId], references: [id])\n  projectId String\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  treeId String @id @default(uuid())\n  values String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220909131750_builds-cleanup/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Project\" DROP COLUMN \"devTreeId\",\nDROP COLUMN \"prodTreeId\",\nDROP COLUMN \"prodTreeIdHistory\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220912141854_assets-meta/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" ADD COLUMN     \"description\" TEXT,\nADD COLUMN     \"meta\" TEXT NOT NULL DEFAULT '{}';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220912142938_assets-meta-data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\n// NOTE ON IMPORTS:\n//\n//   We want to be able to run old migrations at any point.\n//   For example, when we setting up a fresh database or making a reset.\n//\n//   You shouldn't import code that may change later\n//   and become incompatible with the migration.\n//   It's better to copy it to the migration directory.\n\nexport default () => {\n  const client = new PrismaClient();\n\n  return client.$transaction(\n    async (prisma) => {\n      const previousAssets = await prisma.asset.findMany();\n      const update = previousAssets.map((asset) => {\n        return {\n          id: asset.id,\n          description: asset.alt,\n          meta: JSON.stringify({\n            width: Number(asset.width),\n            height: Number(asset.height),\n          }),\n        };\n      });\n      await Promise.all(\n        update.map(({ id, ...data }) =>\n          prisma.asset.update({ where: { id }, data })\n        )\n      );\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220912142938_assets-meta-data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id          String   @id @default(uuid())\n  project     Project  @relation(fields: [projectId], references: [id])\n  projectId   String\n  format      String?\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime @default(now())\n  meta        String   @default(\"{}\")\n\n  // Migrating\n  width     Decimal?\n  height    Decimal?\n  alt       String?\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id                String  @id @default(uuid())\n  title             String\n  domain            String  @unique\n  prodTreeId        String? // exists when published\n  devTreeId         String\n  prodTreeIdHistory String  @default(\"[]\")\n  user              User?   @relation(fields: [userId], references: [id])\n  userId            String?\n  assets            Asset[]\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  treeId String @id @default(uuid())\n  values String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220912150542_assets-meta-cleanup/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" DROP COLUMN \"alt\",\nDROP COLUMN \"height\",\nDROP COLUMN \"width\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220915143947_breakpoints-build/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Breakpoints\" ADD COLUMN     \"buildId\" TEXT;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220915144008_breakpoints-build_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\nimport { z } from \"zod\";\n\nconst Page = z.object({\n  id: z.string(),\n  name: z.string(),\n  path: z.string(),\n  title: z.string(),\n  meta: z.record(z.string(), z.string()),\n  treeId: z.string(),\n});\n\nconst Pages = z.object({\n  homePage: Page,\n  pages: z.array(Page),\n});\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      const builds = await prisma.build.findMany();\n      const breakpoints = await prisma.breakpoints.findMany();\n\n      const buildsParsed = builds.map((build) => ({\n        id: build.id,\n        pages: Pages.parse(JSON.parse(build.pages)),\n      }));\n\n      // await Promise.all(\n      //   breakpoints.map((breakpoint) => {\n      //     const build = buildsParsed.find(\n      //       (build) => build.pages.homePage.treeId === breakpoint.treeId\n      //     );\n\n      //     if (build === undefined) {\n      //\n      //       console.warn(\n      //         `Build not found for breakpoint ${breakpoint.treeId}. Deleting!`\n      //       );\n\n      //       return prisma.breakpoints.delete({\n      //         where: { treeId: breakpoint.treeId },\n      //       });\n      //     }\n\n      //     return prisma.breakpoints.update({\n      //       where: { treeId: breakpoint.treeId },\n      //       data: {\n      //         buildId: build.id,\n      //       },\n      //     });\n      //   })\n      // );\n\n      for (const breakpoint of breakpoints) {\n        const build = buildsParsed.find(\n          (build) => build.pages.homePage.treeId === breakpoint.treeId\n        );\n\n        if (build === undefined) {\n          console.warn(\n            `Build not found for breakpoint ${breakpoint.treeId}. Deleting!`\n          );\n\n          await prisma.breakpoints.delete({\n            where: { treeId: breakpoint.treeId },\n          });\n        } else {\n          console.info(`Updating breakpoint ${breakpoint.treeId}`);\n          await prisma.breakpoints.update({\n            where: { treeId: breakpoint.treeId },\n            data: {\n              buildId: build.id,\n            },\n          });\n        }\n      }\n    },\n    { timeout: 1000 * 60 * 15 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220915144008_breakpoints-build_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id          String   @id @default(uuid())\n  project     Project  @relation(fields: [projectId], references: [id])\n  projectId   String\n  format      String?\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime @default(now())\n  meta        String   @default(\"{}\")\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  assets Asset[]\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  // breakpoints Breakpoints?\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  // build   Build  @relation(fields: [buildId], references: [id])\n  buildId String? // @id\n  values  String  @default(\"[]\")\n\n  // to be deleted\n  treeId String @id @default(uuid())\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20220915145316_breakpoints-build_cleanup/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Breakpoints\" DROP CONSTRAINT \"Breakpoints_pkey\",\nDROP COLUMN \"treeId\",\nALTER COLUMN \"buildId\" SET NOT NULL,\nADD CONSTRAINT \"Breakpoints_pkey\" PRIMARY KEY (\"buildId\");\n\n-- AddForeignKey\nALTER TABLE \"Breakpoints\" ADD CONSTRAINT \"Breakpoints_buildId_fkey\" FOREIGN KEY (\"buildId\") REFERENCES \"Build\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221021172622_asset_format_notnull/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" ALTER COLUMN \"format\" SET NOT NULL;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221021172647_lowercase_domains/migration.ts",
    "content": "import { PrismaClient, Project } from \"./client\";\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      const projects = await prisma.project.findMany();\n\n      const projectsByDomain = new Map<string, Project>();\n\n      for (const project of projects) {\n        const domain = project.domain.toLowerCase();\n        if (projectsByDomain.has(domain) === false) {\n          projectsByDomain.set(domain, project);\n        } else {\n          let suffix = 1;\n          while (projectsByDomain.has(`${domain}${suffix}`)) {\n            suffix++;\n          }\n          projectsByDomain.set(`${domain}${suffix}`, project);\n        }\n      }\n\n      for (const [domain, project] of projectsByDomain) {\n        await prisma.project.update({\n          where: { id: project.id },\n          data: { domain },\n        });\n      }\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221021172647_lowercase_domains/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id          String   @id @default(uuid())\n  project     Project  @relation(fields: [projectId], references: [id])\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime @default(now())\n  meta        String   @default(\"{}\")\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  assets Asset[]\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints Breakpoints?\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221126165439_design-tokens/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"DesignTokens\" (\n    \"buildId\" TEXT NOT NULL,\n    \"value\" TEXT NOT NULL DEFAULT '[]',\n\n    CONSTRAINT \"DesignTokens_pkey\" PRIMARY KEY (\"buildId\")\n);\n\n-- AddForeignKey\nALTER TABLE \"DesignTokens\" ADD CONSTRAINT \"DesignTokens_buildId_fkey\" FOREIGN KEY (\"buildId\") REFERENCES \"Build\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221201075120_user-props/migration.ts",
    "content": "import { z } from \"zod\";\nimport { PrismaClient } from \"./client\";\n\nconst baseUserProps = {\n  id: z.string(),\n  prop: z.string(),\n  required: z.optional(z.boolean()),\n};\n\nexport const UserDbProp = z.discriminatedUnion(\"type\", [\n  z.object({\n    ...baseUserProps,\n    type: z.literal(\"number\"),\n    value: z.number(),\n  }),\n  z.object({\n    ...baseUserProps,\n    type: z.literal(\"string\"),\n    value: z.string(),\n  }),\n  z.object({\n    ...baseUserProps,\n    type: z.literal(\"boolean\"),\n    value: z.boolean(),\n  }),\n  z.object({\n    ...baseUserProps,\n    type: z.literal(\"asset\"),\n    // In database we hold asset.id\n    value: z.string(),\n  }),\n]);\n\nconst UserDbProps = z.array(UserDbProp);\ntype UserDbProp = z.infer<typeof UserDbProp>;\n\nconst OutdatedProp = z.object({\n  ...baseUserProps,\n  value: z.optional(z.union([z.string(), z.number(), z.boolean()])),\n  asset_id: z.optional(z.string()),\n  assetId: z.optional(z.string()),\n  type: z.optional(z.string()),\n});\n\nconst OutdatedProps = z.array(OutdatedProp);\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      const allInstanceProps = await prisma.instanceProps.findMany({\n        where: {\n          props: {\n            not: {\n              equals: \"[]\",\n            },\n          },\n        },\n      });\n\n      for (const instanceProps of allInstanceProps) {\n        const rawProps = JSON.parse(instanceProps.props);\n        const props = OutdatedProps.parse(rawProps);\n\n        const newProps: UserDbProp[] = [];\n\n        let need_update = false;\n\n        for (const prop of props) {\n          if (prop.type != null) {\n            const dbProp = UserDbProp.parse(prop);\n            newProps.push(dbProp);\n            continue;\n          }\n\n          need_update = true;\n\n          if (prop.assetId != null) {\n            newProps.push({\n              id: prop.id,\n              prop: prop.prop,\n              required: prop.required,\n              type: \"asset\",\n              value: prop.assetId,\n            });\n            continue;\n          }\n\n          if (prop.asset_id != null) {\n            newProps.push({\n              id: prop.id,\n              prop: prop.prop,\n              required: prop.required,\n              type: \"asset\",\n              value: prop.asset_id,\n            });\n            continue;\n          }\n\n          if (prop.value == null) {\n            continue;\n          }\n\n          if (typeof prop.value === \"string\") {\n            newProps.push({\n              id: prop.id,\n              prop: prop.prop,\n              required: prop.required,\n              type: \"string\",\n              value: prop.value,\n            });\n            continue;\n          }\n\n          if (typeof prop.value === \"boolean\") {\n            newProps.push({\n              id: prop.id,\n              prop: prop.prop,\n              required: prop.required,\n              type: \"boolean\",\n              value: prop.value,\n            });\n            continue;\n          }\n\n          if (typeof prop.value === \"number\") {\n            newProps.push({\n              id: prop.id,\n              prop: prop.prop,\n              required: prop.required,\n              type: \"number\",\n              value: prop.value,\n            });\n            continue;\n          }\n\n          throw new Error(`Unexpected prop type ${typeof prop.value}`);\n        }\n\n        if (need_update) {\n          await prisma.instanceProps.update({\n            where: { id: instanceProps.id },\n            data: { props: JSON.stringify(UserDbProps.parse(newProps)) },\n          });\n        }\n      }\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221201075120_user-props/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id          String   @id @default(uuid())\n  project     Project  @relation(fields: [projectId], references: [id])\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime @default(now())\n  meta        String   @default(\"{}\")\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  assets Asset[]\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints Breakpoints?\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221208123312_remove-assets-from-project/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Asset\" DROP CONSTRAINT \"Asset_projectId_fkey\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221218211129_tree_text/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype PrevInstance = {\n  id: string;\n  component: any;\n  cssRules: Array<any>;\n  children: Array<PrevInstance | string>;\n};\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Instance = {\n  type: \"instance\";\n  id: string;\n  component: any;\n  cssRules: Array<any>;\n  children: Array<Instance | Text>;\n};\n\nconst convertTree = (prevInstance: PrevInstance): Instance => {\n  const instance: Instance = {\n    type: \"instance\",\n    id: prevInstance.id,\n    component: prevInstance.component,\n    cssRules: prevInstance.cssRules,\n    children: [],\n  };\n\n  for (const child of prevInstance.children) {\n    if (typeof child === \"string\") {\n      instance.children.push({ type: \"text\", value: child });\n    } else {\n      instance.children.push(convertTree(child));\n    }\n  }\n  return instance;\n};\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      const previousTrees = await prisma.tree.findMany();\n      const update = previousTrees.map((tree) => {\n        let newRoot = tree.root;\n        try {\n          newRoot = JSON.stringify(convertTree(JSON.parse(tree.root)));\n        } catch {\n          console.info(`Tree ${tree.id} cannot be converted`);\n        }\n        return {\n          id: tree.id,\n          root: newRoot,\n        };\n      });\n      await Promise.all(\n        update.map(({ id, ...data }) =>\n          prisma.tree.update({ where: { id }, data })\n        )\n      );\n    },\n    { timeout: 1000 * 60 * 5 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221218211129_tree_text/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nmodel Asset {\n  id          String   @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime @default(now())\n  meta        String   @default(\"{}\")\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints  Breakpoints?\n  designTokens DesignTokens?\n}\n\nmodel Tree {\n  id   String @id @default(uuid())\n  root String\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nmodel DesignTokens {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  value   String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221227220622_assets-status/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"UploadStatus\" AS ENUM ('UPLOADING', 'UPLOADED');\n\n-- This field is necessary to be able to limit the number of Assets in the project.\nALTER TABLE \"Asset\" ADD COLUMN     \"status\" \"UploadStatus\" NOT NULL DEFAULT 'UPLOADED';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20221230120125_tree_preset_styles/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Tree\" ADD COLUMN     \"presetStyles\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230106000103_tree_styles/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Tree\" ADD COLUMN     \"styles\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230106000143_tree_styles_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype StylesItem = {\n  breakpointId: string;\n  instanceId: string;\n  property: string;\n  value: unknown;\n};\n\ntype Styles = StylesItem[];\n\ntype CssRule = {\n  breakpoint: undefined | string;\n  style: Record<string, unknown>;\n};\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  cssRules: Array<CssRule>;\n  children: Array<Instance | Text>;\n};\n\nconst convertTree = (instance: Instance, styles: Styles) => {\n  for (const rule of instance.cssRules) {\n    if (rule.breakpoint === undefined) {\n      continue;\n    }\n    for (const [property, value] of Object.entries(rule.style)) {\n      styles.push({\n        breakpointId: rule.breakpoint,\n        instanceId: instance.id,\n        property,\n        value,\n      });\n    }\n  }\n  instance.cssRules = [];\n  for (const child of instance.children) {\n    if (child.type === \"instance\") {\n      convertTree(child, styles);\n    }\n  }\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        const chunkSize = 1000;\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = trees.at(-1)?.id;\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          try {\n            const root = JSON.parse(tree.root);\n            const styles: Styles = [];\n            convertTree(root, styles);\n            tree.root = JSON.stringify(root);\n            tree.styles = JSON.stringify(styles);\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          trees.map(({ id, root, styles }) =>\n            prisma.tree.update({ where: { id }, data: { root, styles } })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 5 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230106000143_tree_styles_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints  Breakpoints?\n  designTokens DesignTokens?\n}\n\nmodel Tree {\n  id           String @id @default(uuid())\n  root         String\n  presetStyles String @default(\"[]\")\n  styles       String @default(\"[]\")\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nmodel DesignTokens {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  value   String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230115165217_tree_instances/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Tree\" ADD COLUMN     \"instances\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230115165314_tree_instances_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Id = {\n  type: \"id\";\n  value: string;\n};\n\ntype Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  cssRules: Array<any>;\n  children: Array<Instance | Text>;\n};\n\ntype InstancesItem = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  children: Array<Id | Text>;\n};\n\nconst convertTree = (instance: Instance, instances: InstancesItem[]) => {\n  const instancesItem: InstancesItem = {\n    type: \"instance\",\n    id: instance.id,\n    component: instance.component,\n    children: [],\n  };\n  instances.push(instancesItem);\n  for (const child of instance.children) {\n    if (child.type === \"instance\") {\n      convertTree(child, instances);\n      instancesItem.children.push({ type: \"id\", value: child.id });\n    } else {\n      instancesItem.children.push(child);\n    }\n  }\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        const chunkSize = 1000;\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = trees.at(-1)?.id;\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          try {\n            const root = JSON.parse(tree.root);\n            const instances: InstancesItem[] = [];\n            convertTree(root, instances);\n            tree.instances = JSON.stringify(instances);\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          trees.map(({ id, instances }) =>\n            prisma.tree.update({ where: { id }, data: { instances } })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 5 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230115165314_tree_instances_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints  Breakpoints?\n  designTokens DesignTokens?\n}\n\nmodel Tree {\n  id           String @id @default(uuid())\n  root         String\n  instances    String @default(\"[]\")\n  presetStyles String @default(\"[]\")\n  styles       String @default(\"[]\")\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nmodel DesignTokens {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  value   String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230119013820_instance-props-uniq/migration.sql",
    "content": "-- CreateIndex\nCREATE UNIQUE INDEX \"InstanceProps_instanceId_treeId_key\" ON \"InstanceProps\"(\"instanceId\", \"treeId\");\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230119181836_tree_props/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Tree\" ADD COLUMN     \"props\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230119181858_tree_props_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype PropsItem = {\n  instanceId: string;\n  id: string;\n  name: string;\n  required?: boolean;\n  type: string;\n  value: unknown;\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        const chunkSize = 1000;\n        const trees: any[] = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = trees.at(-1)?.id;\n        hasNext = trees.length === chunkSize;\n\n        const treesProps = await prisma.instanceProps.findMany({\n          where: {\n            treeId: {\n              in: trees.map((tree) => tree.id),\n            },\n          },\n        });\n\n        for (const tree of trees) {\n          const instanceProps = treesProps.filter(\n            (props) => props.treeId === tree.id\n          );\n          try {\n            const list: PropsItem[] = [];\n            for (const { instanceId, props } of instanceProps) {\n              const parsed = JSON.parse(props);\n              for (const prop of parsed) {\n                list.push({\n                  instanceId,\n                  id: prop.id,\n                  name: prop.prop,\n                  required: prop.required,\n                  type: prop.type,\n                  value: prop.value,\n                });\n              }\n            }\n\n            tree.props = JSON.stringify(list);\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          trees.map(({ id, props }) =>\n            prisma.tree.update({ where: { id }, data: { props } })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 5 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230119181858_tree_props_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"interactiveTransactions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id     String  @id @default(uuid())\n  title  String\n  domain String  @unique\n  user   User?   @relation(fields: [userId], references: [id])\n  userId String?\n  build  Build[]\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints  Breakpoints?\n  designTokens DesignTokens?\n}\n\nmodel Tree {\n  id           String @id @default(uuid())\n  root         String\n  instances    String @default(\"[]\")\n  props        String @default(\"[]\")\n  presetStyles String @default(\"[]\")\n  styles       String @default(\"[]\")\n}\n\nmodel InstanceProps {\n  id         String @id @default(uuid())\n  instanceId String\n  treeId     String\n  props      String @default(\"[]\")\n\n  @@unique([instanceId, treeId])\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nmodel DesignTokens {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  value   String @default(\"[]\")\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230120130130_dashboard-project/migration.sql",
    "content": "-- AlterTable\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230120130131_dashboard-project-is-prod/migration.sql",
    "content": "-- AlterTable\nCREATE\nOR REPLACE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"isProd\" = true\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230123225816_dashboard-project-is-deleted/migration.sql",
    "content": "-- AlterTable\nALTER TABLE\n  \"Project\"\nADD\n  COLUMN \"isDeleted\" BOOLEAN NOT NULL DEFAULT false;\n\n-- Drop DashboardProject View\nDROP VIEW IF EXISTS \"DashboardProject\";\n\n-- Update DashboardProject View\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"isProd\" = true\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230124131218_authorization-tokens/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"AuthorizationPermit\" AS ENUM ('VIEW', 'EDIT');\n\n-- CreateTable\nCREATE TABLE \"AuthorizationTokens\" (\n    \"id\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"token\" TEXT NOT NULL,\n    \"permit\" \"AuthorizationPermit\" NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"AuthorizationTokens_pkey\" PRIMARY KEY (\"id\")\n);\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230127120101_style_sources/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\"\n  ADD COLUMN \"styleSources\" TEXT NOT NULL DEFAULT '[]',\n  ADD COLUMN \"styles\" TEXT NOT NULL DEFAULT '[]';\n\n-- AlterTable\nALTER TABLE \"Tree\"\n  ADD COLUMN \"styleSelections\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230129141714_unused_schema/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"DesignTokens\" DROP CONSTRAINT \"DesignTokens_buildId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Tree\" DROP COLUMN \"presetStyles\";\n\n-- DropTable\nDROP TABLE \"DesignTokens\";\n\n-- DropTable\nDROP TABLE \"InstanceProps\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230129174218_fill-auth-view-tokens/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\n// NOTE ON IMPORTS:\n//\n//   We want to be able to run old migrations at any point.\n//   For example, when we setting up a fresh database or making a reset.\n//\n//   You shouldn't import code that may change later\n//   and become incompatible with the migration.\n//   It's better to copy it to the migration directory.\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      await prisma.$executeRaw`\n      INSERT INTO \"AuthorizationTokens\" (id, \"projectId\", token, permit)\n        SELECT\n          gen_random_uuid () AS id,\n          id AS projectId,\n          gen_random_uuid () AS token,\n          'VIEW' AS permit\n        FROM\n        \"Project\";\n    `;\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230129174218_fill-auth-view-tokens/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String  @id @default(uuid())\n  title     String\n  domain    String  @unique\n  user      User?   @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  isDeleted Boolean @default(false)\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints Breakpoints?\n\n  styles       String @default(\"[]\")\n  styleSources String @default(\"[]\")\n}\n\nmodel Tree {\n  id              String @id @default(uuid())\n  root            String\n  instances       String @default(\"[]\")\n  props           String @default(\"[]\")\n  styles          String @default(\"[]\")\n  styleSelections String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationPermit {\n  VIEW\n  EDIT\n}\n\nmodel AuthorizationTokens {\n  id        String              @id @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  token     String\n  permit    AuthorizationPermit\n  createdAt DateTime            @default(now())\n}\n\n// Dashboard\nview DashboardProject {\n  id          String  @id @default(uuid())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130121014_tree_relations/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Tree\" ADD COLUMN     \"buildId\" TEXT,\nADD COLUMN     \"projectId\" TEXT;\n\n-- AddForeignKey\nALTER TABLE \"Tree\" ADD CONSTRAINT \"Tree_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Tree\" ADD CONSTRAINT \"Tree_buildId_fkey\" FOREIGN KEY (\"buildId\") REFERENCES \"Build\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130121041_tree_relations_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype TreeId = string;\ntype BuildId = string;\ntype ProjectId = string;\n\ntype Page = {\n  id: string;\n  name: string;\n  path: string;\n  title: string;\n  meta: any;\n  treeId: TreeId;\n};\n\ntype Pages = {\n  homePage: Page;\n  pages: Page[];\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const buildIdPerTreeId = new Map<TreeId, BuildId>();\n      const projectIdPerTreeId = new Map<TreeId, ProjectId>();\n\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const chunkSize = 1000;\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          try {\n            const pages: Pages = JSON.parse(build.pages);\n            buildIdPerTreeId.set(pages.homePage.treeId, build.id);\n            projectIdPerTreeId.set(pages.homePage.treeId, build.projectId);\n            for (const page of pages.pages) {\n              buildIdPerTreeId.set(page.treeId, build.id);\n              projectIdPerTreeId.set(page.treeId, build.projectId);\n            }\n          } catch {\n            console.info(`Build ${build.id} cannot be parsed`);\n          }\n        }\n      }\n\n      cursor = undefined;\n      hasNext = true;\n      while (hasNext) {\n        const chunkSize = 1000;\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = trees.at(-1)?.id;\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          const buildId = buildIdPerTreeId.get(tree.id);\n          const projectId = projectIdPerTreeId.get(tree.id);\n          if (buildId && projectId) {\n            tree.buildId = buildId;\n            tree.projectId = projectId;\n          }\n        }\n        await Promise.all(\n          trees.map(({ id, buildId, projectId }) =>\n            prisma.tree.update({ where: { id }, data: { buildId, projectId } })\n          )\n        );\n      }\n\n      await prisma.tree.deleteMany({\n        where: {\n          OR: [\n            {\n              buildId: { equals: null },\n            },\n            {\n              projectId: { equals: null },\n            },\n          ],\n        },\n      });\n    },\n    { timeout: 1000 * 60 * 5 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130121041_tree_relations_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String  @id @default(uuid())\n  title     String\n  domain    String  @unique\n  user      User?   @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean @default(false)\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints Breakpoints?\n\n  styles       String @default(\"[]\")\n  styleSources String @default(\"[]\")\n}\n\nmodel Tree {\n  id              String @id @default(uuid())\n  project         Project? @relation(fields: [projectId], references: [id])\n  projectId       String?\n  build           Build? @relation(fields: [buildId], references: [id])\n  buildId         String?\n  root            String\n  instances       String @default(\"[]\")\n  props           String @default(\"[]\")\n  styles          String @default(\"[]\")\n  styleSelections String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationPermit {\n  VIEW\n  EDIT\n}\n\nmodel AuthorizationTokens {\n  id        String              @id @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  token     String\n  permit    AuthorizationPermit\n  createdAt DateTime            @default(now())\n}\n\n// Dashboard\nview DashboardProject {\n  id          String  @id @default(uuid())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130124937_tree_relations_not_null/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Tree\" DROP CONSTRAINT \"Tree_buildId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"Tree\" DROP CONSTRAINT \"Tree_projectId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Tree\" ALTER COLUMN \"buildId\" SET NOT NULL,\nALTER COLUMN \"projectId\" SET NOT NULL;\n\n-- AddForeignKey\nALTER TABLE \"Tree\" ADD CONSTRAINT \"Tree_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Tree\" ADD CONSTRAINT \"Tree_buildId_fkey\" FOREIGN KEY (\"buildId\") REFERENCES \"Build\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130153140_authorization-tokens-fix/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"AuthorizationRelation\" AS ENUM ('viewers', 'editors', 'builders');\n\n-- DropTable\nDROP TABLE \"AuthorizationTokens\";\n\n-- DropEnum\nDROP TYPE \"AuthorizationPermit\";\n\n-- CreateTable\nCREATE TABLE \"AuthorizationToken\" (\n    \"token\" TEXT NOT NULL,\n    \"projectId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL DEFAULT '',\n    \"relation\" \"AuthorizationRelation\" NOT NULL DEFAULT 'viewers',\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"AuthorizationToken_pkey\" PRIMARY KEY (\"token\",\"projectId\")\n);\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130160827_build_styles/migration.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { PrismaClient } from \"./client\";\n\ntype InstanceId = string;\ntype StyleSourceId = string;\ntype BuildId = string;\ntype TreeId = string;\n\ntype TreeStyleDecl = {\n  breakpointId: string;\n  instanceId: InstanceId;\n  property: string;\n  value: unknown;\n};\n\ntype TreeStyles = TreeStyleDecl[];\n\ntype StyleDecl = {\n  styleSourceId: StyleSourceId;\n  breakpointId: string;\n  property: string;\n  value: unknown;\n};\n\ntype Styles = StyleDecl[];\n\ntype StyleSource = {\n  type: \"local\";\n  id: StyleSourceId;\n  treeId: TreeId;\n};\n\ntype StyleSources = StyleSource[];\n\ntype StyleSourceSelection = {\n  instanceId: InstanceId;\n  values: StyleSourceId[];\n};\n\ntype StyleSourceSelections = StyleSourceSelection[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let stylesPerBuild = new Map<BuildId, Styles>();\n      let styleSourcesPerBuild = new Map<BuildId, StyleSources>();\n\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = trees.at(-1)?.id;\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          const treeId = tree.id;\n          try {\n            const treeStyles: TreeStyles = JSON.parse(tree.styles);\n            const styles: Styles = [];\n            const styleSources: StyleSources = [];\n            const styleSelections: StyleSourceSelections = [];\n            const styleSourceIdPerInstanceId = new Map<\n              InstanceId,\n              StyleSourceId\n            >();\n\n            for (const styleDecl of treeStyles) {\n              const { instanceId, breakpointId, property, value } = styleDecl;\n              let styleSourceId = styleSourceIdPerInstanceId.get(instanceId);\n              if (styleSourceId === undefined) {\n                styleSourceId = nanoid();\n                styleSourceIdPerInstanceId.set(instanceId, styleSourceId);\n              }\n\n              styles.push({\n                styleSourceId,\n                breakpointId,\n                property,\n                value,\n              });\n              styleSources.push({\n                id: styleSourceId,\n                treeId,\n                type: \"local\",\n              });\n              styleSelections.push({\n                instanceId,\n                values: [styleSourceId],\n              });\n            }\n\n            stylesPerBuild.set(tree.buildId, styles);\n            styleSourcesPerBuild.set(tree.buildId, styleSources);\n            tree.styleSelections = JSON.stringify(styleSelections);\n          } catch {\n            console.info(`Tree ${treeId} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          trees.map(({ id, styleSelections }) =>\n            prisma.tree.update({ where: { id }, data: { styleSelections } })\n          )\n        );\n      }\n\n      cursor = undefined;\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const buildId = build.id;\n          const styles = stylesPerBuild.get(buildId) ?? [];\n          const styleSources = styleSourcesPerBuild.get(buildId) ?? [];\n          build.styles = JSON.stringify(styles);\n          build.styleSources = JSON.stringify(styleSources);\n        }\n        await Promise.all(\n          builds.map(({ id, styles, styleSources }) =>\n            prisma.build.update({\n              where: { id },\n              data: { styles, styleSources },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230130160827_build_styles/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String  @id @default(uuid())\n  title     String\n  domain    String  @unique\n  user      User?   @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean @default(false)\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints Breakpoints?\n\n  styles       String @default(\"[]\")\n  styleSources String @default(\"[]\")\n}\n\nmodel Tree {\n  id              String @id @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build @relation(fields: [buildId], references: [id])\n  buildId         String\n  root            String\n  instances       String @default(\"[]\")\n  props           String @default(\"[]\")\n  styles          String @default(\"[]\")\n  styleSelections String @default(\"[]\")\n}\n\nmodel Breakpoints {\n  build   Build  @relation(fields: [buildId], references: [id])\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationPermit {\n  VIEW\n  EDIT\n}\n\nmodel AuthorizationTokens {\n  id        String              @id @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  token     String\n  permit    AuthorizationPermit\n  createdAt DateTime            @default(now())\n}\n\n// Dashboard\nview DashboardProject {\n  id          String  @id @default(uuid())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230202131409_project-created-at/migration.sql",
    "content": "-- AlterTable\nALTER TABLE\n  \"Project\"\nADD\n  COLUMN \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;\n\n-- Drop DashboardProject View\nDROP VIEW IF EXISTS \"DashboardProject\";\n\n-- Update DashboardProject View\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"isProd\" = true\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230202174408_build_breakpoints/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Breakpoints\" DROP CONSTRAINT \"Breakpoints_buildId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"breakpoints\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230202174456_build_breakpoints_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      await prisma.$executeRaw`\n        UPDATE \"Build\"\n        SET breakpoints=\"Breakpoints\".values\n        FROM \"Breakpoints\"\n        WHERE \"Build\".id=\"Breakpoints\".\"buildId\";\n      `;\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230202174456_build_breakpoints_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @id @default(uuid())\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String  @id @default(uuid())\n  title     String\n  domain    String  @unique\n  user      User?   @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean @default(false)\n}\n\nmodel Build {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints  String @default(\"[]\")\n  styles       String @default(\"[]\")\n  styleSources String @default(\"[]\")\n}\n\nmodel Tree {\n  id              String  @id @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId], references: [id])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String  @id @default(uuid())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230202192437_composite_ids/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Tree\" DROP CONSTRAINT \"Tree_buildId_fkey\";\n\n-- AlterTable\nALTER TABLE \"Asset\" DROP CONSTRAINT \"Asset_pkey\",\nADD CONSTRAINT \"Asset_pkey\" PRIMARY KEY (\"id\", \"projectId\");\n\n-- AlterTable\nALTER TABLE \"Build\" DROP CONSTRAINT \"Build_pkey\",\nADD CONSTRAINT \"Build_pkey\" PRIMARY KEY (\"id\", \"projectId\");\n\n-- AlterTable\nALTER TABLE \"Tree\" DROP CONSTRAINT \"Tree_pkey\",\nADD CONSTRAINT \"Tree_pkey\" PRIMARY KEY (\"id\", \"projectId\");\n\n-- AddForeignKey\nALTER TABLE \"Tree\" ADD CONSTRAINT \"Tree_buildId_projectId_fkey\" FOREIGN KEY (\"buildId\", \"projectId\") REFERENCES \"Build\"(\"id\", \"projectId\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230213220858_is-deleted-uniq/migration.sql",
    "content": "-- CreateIndex\nCREATE UNIQUE INDEX \"Project_id_isDeleted_key\" ON \"Project\"(\"id\", \"isDeleted\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Project_domain_isDeleted_key\" ON \"Project\"(\"domain\", \"isDeleted\");\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230227142607_build_style_source_selections/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"styleSourceSelections\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230227142622_build_style_source_selections_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype InstanceId = string;\ntype StyleSourceId = string;\ntype BuildId = string;\n\ntype StyleSourceSelection = {\n  instanceId: InstanceId;\n  values: StyleSourceId[];\n};\n\ntype StyleSourceSelections = StyleSourceSelection[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let styleSourceSelectionsByBuildId = new Map<\n        BuildId,\n        StyleSourceSelections\n      >();\n\n      const chunkSize = 1000;\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastTree = trees.at(-1);\n        if (lastTree) {\n          cursor = { id: lastTree.id, projectId: lastTree?.projectId };\n        }\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          try {\n            const treeStyleSourceSelections: StyleSourceSelections = JSON.parse(\n              tree.styleSelections\n            );\n            const buildStyleSourceSelections =\n              styleSourceSelectionsByBuildId.get(tree.buildId) ?? [];\n            styleSourceSelectionsByBuildId.set(tree.buildId, [\n              ...buildStyleSourceSelections,\n              ...treeStyleSourceSelections,\n            ]);\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n      }\n\n      cursor = undefined;\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          // deduplicate badly migrated data\n          const styleSourceSelections = Array.from(\n            new Map(\n              (styleSourceSelectionsByBuildId.get(build.id) ?? []).map(\n                (item) => [item.instanceId, item]\n              )\n            ).values()\n          );\n          build.styleSourceSelections = JSON.stringify(styleSourceSelections);\n        }\n        await Promise.all(\n          builds.map(({ id, projectId, styleSourceSelections }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { styleSourceSelections },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230227142622_build_style_source_selections_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230227180214_build_props/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"props\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230227180250_build_props_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype PropId = string;\ntype InstanceId = string;\ntype BuildId = string;\n\ntype Prop = {\n  id: PropId;\n  instanceId: InstanceId;\n};\n\ntype Props = Prop[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let propsByBuildId = new Map<BuildId, Props>();\n\n      const chunkSize = 1000;\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastTree = trees.at(-1);\n        if (lastTree) {\n          cursor = { id: lastTree.id, projectId: lastTree?.projectId };\n        }\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          try {\n            const treeProps: Props = JSON.parse(tree.props);\n            const buildProps = propsByBuildId.get(tree.buildId) ?? [];\n            propsByBuildId.set(tree.buildId, [...buildProps, ...treeProps]);\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n      }\n\n      cursor = undefined;\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const props = propsByBuildId.get(build.id) ?? [];\n          build.props = JSON.stringify(props);\n        }\n        await Promise.all(\n          builds.map(({ id, projectId, props }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { props },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230227180250_build_props_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228132402_drop_style_source_tree_id/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype StyleSource = {\n  type: \"local\" | \"token\";\n  id: string;\n  treeId?: string;\n  name?: string;\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        const chunkSize = 1000;\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          try {\n            const styleSources: StyleSource[] = JSON.parse(build.styleSources);\n            for (const styleSource of styleSources) {\n              delete styleSource.treeId;\n            }\n            build.styleSources = JSON.stringify(styleSources);\n          } catch {\n            console.info(`Build ${build.id} cannot be converted`);\n          }\n        }\n\n        await Promise.all(\n          builds.map(({ id, projectId, styleSources }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { styleSources },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 5 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228132402_drop_style_source_tree_id/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228161419_page_root_instance_id/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype InstanceId = string;\ntype BuildId = string;\ntype TreeId = string;\n\ntype Instance = {\n  id: InstanceId;\n};\n\ntype Page = {\n  id: string;\n  treeId: TreeId;\n  rootInstanceId: InstanceId;\n};\n\ntype Pages = {\n  homePage: Page;\n  pages: Page[];\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let rootInstanceIdByBuildId = new Map<\n        `${BuildId}:${TreeId}`,\n        InstanceId\n      >();\n\n      const chunkSize = 1000;\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastTree = trees.at(-1);\n        if (lastTree) {\n          cursor = { id: lastTree.id, projectId: lastTree?.projectId };\n        }\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          try {\n            const instances: Instance[] = JSON.parse(tree.instances);\n            const rootInstanceId = instances[0].id;\n            rootInstanceIdByBuildId.set(\n              `${tree.buildId}:${tree.id}`,\n              rootInstanceId\n            );\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n      }\n\n      cursor = undefined;\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const { homePage, pages }: Pages = JSON.parse(build.pages);\n          homePage.rootInstanceId = rootInstanceIdByBuildId.get(\n            `${build.id}:${homePage.treeId}`\n          ) as string;\n          for (const page of pages) {\n            page.rootInstanceId = rootInstanceIdByBuildId.get(\n              `${build.id}:${page.treeId}`\n            ) as string;\n          }\n          build.pages = JSON.stringify({ homePage, pages });\n        }\n\n        await Promise.all(\n          builds.map(({ id, projectId, pages }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { pages },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228161419_page_root_instance_id/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228194425_build_instances/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"instances\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228194553_build_instances_data/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype InstanceId = string;\ntype BuildId = string;\n\ntype Child =\n  | { type: \"text\"; value: string }\n  | { type: \"id\"; value: InstanceId };\n\ntype Instance = {\n  id: InstanceId;\n  children: Child[];\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      let instancesByBuildId = new Map<BuildId, Instance[]>();\n\n      const chunkSize = 1000;\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const trees = await prisma.tree.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastTree = trees.at(-1);\n        if (lastTree) {\n          cursor = { id: lastTree.id, projectId: lastTree?.projectId };\n        }\n        hasNext = trees.length === chunkSize;\n\n        for (const tree of trees) {\n          try {\n            const treeInstances: Instance[] = JSON.parse(tree.instances);\n            const buildInstances = instancesByBuildId.get(tree.buildId) ?? [];\n            instancesByBuildId.set(tree.buildId, [\n              ...buildInstances,\n              ...treeInstances,\n            ]);\n          } catch {\n            console.info(`Tree ${tree.id} cannot be converted`);\n          }\n        }\n      }\n\n      cursor = undefined;\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const instances = instancesByBuildId.get(build.id) ?? [];\n          build.instances = JSON.stringify(instances);\n        }\n\n        await Promise.all(\n          builds.map(({ id, projectId, instances }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { instances },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228194553_build_instances_data/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228222541_convert-image-style/migration.ts",
    "content": "import { z } from \"zod\";\nimport { PrismaClient } from \"./client\";\n\nconst Unit = z.string();\n\ntype Unit = z.infer<typeof Unit>;\n\nconst UnitValue = z.object({\n  type: z.literal(\"unit\"),\n  unit: Unit,\n  value: z.number(),\n});\n\ntype UnitValue = z.infer<typeof UnitValue>;\n\nconst KeywordValue = z.object({\n  type: z.literal(\"keyword\"),\n  // @todo use exact type\n  value: z.string(),\n});\ntype KeywordValue = z.infer<typeof KeywordValue>;\n\n/**\n * Valid unparsed css value\n **/\nconst UnparsedValue = z.object({\n  type: z.literal(\"unparsed\"),\n  value: z.string(),\n});\n\nconst FontFamilyValue = z.object({\n  type: z.literal(\"fontFamily\"),\n  value: z.array(z.string()),\n});\ntype FontFamilyValue = z.infer<typeof FontFamilyValue>;\n\nconst RgbValue = z.object({\n  type: z.literal(\"rgb\"),\n  r: z.number(),\n  g: z.number(),\n  b: z.number(),\n  alpha: z.number(),\n});\ntype RgbValue = z.infer<typeof RgbValue>;\n\nconst ImageValue = z.object({\n  type: z.literal(\"image\"),\n  value: z.array(z.object({ type: z.literal(\"asset\"), value: z.unknown() })),\n});\n\ntype ImageValue = z.infer<typeof ImageValue>;\n\n// We want to be able to render the invalid value\n// and show it is invalid visually, without saving it to the db\nconst InvalidValue = z.object({\n  type: z.literal(\"invalid\"),\n  value: z.string(),\n});\ntype InvalidValue = z.infer<typeof InvalidValue>;\n\nconst UnsetValue = z.object({\n  type: z.literal(\"unset\"),\n  value: z.literal(\"\"),\n});\ntype UnsetValue = z.infer<typeof UnsetValue>;\n\nconst ArrayValue = z.object({\n  type: z.literal(\"array\"),\n  value: z.array(z.union([UnitValue, KeywordValue, UnparsedValue, ImageValue])),\n});\ntype ArrayValue = z.infer<typeof ArrayValue>;\n\nconst validStaticValueTypes = [\n  \"unit\",\n  \"keyword\",\n  \"fontFamily\",\n  \"rgb\",\n  \"image\",\n  \"unparsed\",\n  \"array\",\n] as const;\n\n/**\n * Shared zod types with DB types.\n * ImageValue in DB has a different type\n */\nconst SharedStaticStyleValue = z.union([\n  UnitValue,\n  KeywordValue,\n  FontFamilyValue,\n  RgbValue,\n  UnparsedValue,\n  ArrayValue,\n]);\n\nconst ValidStaticStyleValue = z.union([ImageValue, SharedStaticStyleValue]);\n\ntype ValidStaticStyleValue = z.infer<typeof ValidStaticStyleValue>;\n\nconst VarValue = z.object({\n  type: z.literal(\"var\"),\n  value: z.string(),\n  fallbacks: z.array(ValidStaticStyleValue),\n});\ntype VarValue = z.infer<typeof VarValue>;\n\nconst StyleValue = z.union([\n  ValidStaticStyleValue,\n  InvalidValue,\n  UnsetValue,\n  VarValue,\n]);\n\n/**\n * Shared types with DB types\n */\nconst SharedStyleValue = z.union([\n  SharedStaticStyleValue,\n  InvalidValue,\n  UnsetValue,\n  VarValue,\n]);\n\nconst StoredImageValue = z.object({\n  type: z.literal(\"image\"),\n  value: z.union([\n    z.object({ type: z.literal(\"asset\"), value: z.string() }),\n    z.array(z.object({ type: z.literal(\"asset\"), value: z.string() })),\n  ]),\n});\n\nconst StoredStyleDecl = z.object({\n  styleSourceId: z.string(),\n  breakpointId: z.string(),\n  // @todo can't figure out how to make property to be enum\n  property: z.string(),\n  value: z.union([StoredImageValue, SharedStyleValue]),\n});\n\ntype StoredStyleDecl = z.infer<typeof StoredStyleDecl>;\n\n// NOTE ON IMPORTS:\n//\n//   We want to be able to run old migrations at any point.\n//   For example, when we setting up a fresh database or making a reset.\n//\n//   You shouldn't import code that may change later\n//   and become incompatible with the migration.\n//   It's better to copy it to the migration directory.\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      const chunkSize = 1000;\n\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        const buildsToUpdate = [];\n\n        for (const build of builds) {\n          let hasConversion = false;\n\n          const styles: StoredStyleDecl[] = JSON.parse(build.styles);\n          for (const style of styles) {\n            if (style.value.type === \"image\") {\n              const value = style.value.value;\n              if (Array.isArray(value)) {\n                style.value.value = style.value.value[0];\n                hasConversion = true;\n              }\n            }\n          }\n          if (hasConversion) {\n            build.styles = JSON.stringify(styles);\n            buildsToUpdate.push(build as never);\n          }\n        }\n\n        await Promise.all(\n          buildsToUpdate.map(({ id, projectId, styles }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { styles },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230228222541_convert-image-style/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230301101527_drop_page_tree_id/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype TreeId = string;\n\ntype Page = {\n  id: string;\n  treeId?: TreeId;\n};\n\ntype Pages = {\n  homePage: Page;\n  pages: Page[];\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const { homePage, pages }: Pages = JSON.parse(build.pages);\n          delete homePage.treeId;\n          for (const page of pages) {\n            delete page.treeId;\n          }\n          build.pages = JSON.stringify({ homePage, pages });\n        }\n\n        await Promise.all(\n          builds.map(({ id, projectId, pages }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { pages },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230301101527_drop_page_tree_id/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  trees     Tree[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n  trees     Tree[]\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Tree {\n  id              String  @default(uuid())\n  project         Project @relation(fields: [projectId], references: [id])\n  projectId       String\n  build           Build   @relation(fields: [buildId, projectId], references: [id, projectId])\n  buildId         String\n  root            String\n  instances       String  @default(\"[]\")\n  props           String  @default(\"[]\")\n  styles          String  @default(\"[]\")\n  styleSelections String  @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230301101856_drop_tree/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Tree\" DROP CONSTRAINT \"Tree_buildId_projectId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"Tree\" DROP CONSTRAINT \"Tree_projectId_fkey\";\n\n-- DropTable\nDROP TABLE \"Tree\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230301134408_convert-background-to-layers/migration.ts",
    "content": "import { z } from \"zod\";\nimport { PrismaClient } from \"./client\";\n\nconst Unit = z.string();\n\ntype Unit = z.infer<typeof Unit>;\n\nconst UnitValue = z.object({\n  type: z.literal(\"unit\"),\n  unit: Unit,\n  value: z.number(),\n});\n\ntype UnitValue = z.infer<typeof UnitValue>;\n\nconst KeywordValue = z.object({\n  type: z.literal(\"keyword\"),\n  // @todo use exact type\n  value: z.string(),\n});\ntype KeywordValue = z.infer<typeof KeywordValue>;\n\n/**\n * Valid unparsed css value\n **/\nconst UnparsedValue = z.object({\n  type: z.literal(\"unparsed\"),\n  value: z.string(),\n});\n\nconst FontFamilyValue = z.object({\n  type: z.literal(\"fontFamily\"),\n  value: z.array(z.string()),\n});\ntype FontFamilyValue = z.infer<typeof FontFamilyValue>;\n\nconst RgbValue = z.object({\n  type: z.literal(\"rgb\"),\n  r: z.number(),\n  g: z.number(),\n  b: z.number(),\n  alpha: z.number(),\n});\ntype RgbValue = z.infer<typeof RgbValue>;\n\nconst ImageValue = z.object({\n  type: z.literal(\"image\"),\n  value: z.object({ type: z.literal(\"asset\"), value: z.unknown() }),\n});\n\ntype ImageValue = z.infer<typeof ImageValue>;\n\n// We want to be able to render the invalid value\n// and show it is invalid visually, without saving it to the db\nconst InvalidValue = z.object({\n  type: z.literal(\"invalid\"),\n  value: z.string(),\n});\ntype InvalidValue = z.infer<typeof InvalidValue>;\n\nconst UnsetValue = z.object({\n  type: z.literal(\"unset\"),\n  value: z.literal(\"\"),\n});\ntype UnsetValue = z.infer<typeof UnsetValue>;\n\n/**\n * Shared zod types with DB types.\n * ImageValue in DB has a different type\n */\nconst SharedStaticStyleValue = z.union([\n  UnitValue,\n  KeywordValue,\n  FontFamilyValue,\n  RgbValue,\n  UnparsedValue,\n]);\n\nconst ValidStaticStyleValue = z.union([ImageValue, SharedStaticStyleValue]);\n\ntype ValidStaticStyleValue = z.infer<typeof ValidStaticStyleValue>;\n\nconst VarValue = z.object({\n  type: z.literal(\"var\"),\n  value: z.string(),\n  fallbacks: z.array(ValidStaticStyleValue),\n});\ntype VarValue = z.infer<typeof VarValue>;\n\n/**\n * Shared types with DB types\n */\nconst SharedStyleValue = z.union([\n  SharedStaticStyleValue,\n  InvalidValue,\n  UnsetValue,\n  VarValue,\n]);\n\nconst StoredImageValue = z.object({\n  type: z.literal(\"image\"),\n  value: z.object({ type: z.literal(\"asset\"), value: z.string() }),\n});\n\nconst StoredLayersValue = z.object({\n  type: z.literal(\"layers\"),\n  value: z.array(\n    z.union([\n      UnitValue,\n      KeywordValue,\n      UnparsedValue,\n      StoredImageValue,\n      InvalidValue,\n    ])\n  ),\n});\n\nexport const StoredStyleDecl = z.object({\n  styleSourceId: z.string(),\n  breakpointId: z.string(),\n  // @todo can't figure out how to make property to be enum\n  property: z.string(),\n  value: z.union([StoredImageValue, StoredLayersValue, SharedStyleValue]),\n});\n\ntype StoredStyleDecl = z.infer<typeof StoredStyleDecl>;\n\nconst layeredBackgroundProps = [\n  \"backgroundAttachment\",\n  \"backgroundClip\",\n  \"backgroundBlendMode\",\n  \"backgroundImage\",\n  \"backgroundOrigin\",\n  \"backgroundPositionX\",\n  \"backgroundPositionY\",\n  \"backgroundRepeat\",\n  \"backgroundSize\",\n];\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      const chunkSize = 1000;\n\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          where: {\n            OR: [{ isDev: true }, { isProd: true }],\n          },\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        const buildsToUpdate = [];\n\n        for (const build of builds) {\n          let hasConversion = false;\n\n          const styles: StoredStyleDecl[] = JSON.parse(build.styles);\n\n          for (const style of styles) {\n            if (layeredBackgroundProps.includes(style.property)) {\n              if (\n                style.value.type === \"image\" ||\n                style.value.type === \"unit\" ||\n                style.value.type === \"unparsed\" ||\n                style.value.type === \"keyword\"\n              ) {\n                hasConversion = true;\n\n                style.value = {\n                  type: \"layers\",\n                  value: [style.value],\n                };\n              } else {\n                console.warn(\"unsupported style value\", style.value);\n              }\n            }\n          }\n\n          if (hasConversion) {\n            build.styles = JSON.stringify(styles);\n            buildsToUpdate.push(build as never);\n          }\n        }\n\n        await Promise.all(\n          buildsToUpdate.map(({ id, projectId, styles }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { styles },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230301134408_convert-background-to-layers/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230309142820_link-target/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      let cursor: undefined | { id: string; projectId: string } = undefined;\n      let hasNext = true;\n      const chunkSize = 1000;\n\n      hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          where: {\n            OR: [{ isDev: true }, { isProd: true }],\n          },\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id_projectId: cursor },\n              }\n            : null),\n        });\n        const lastBuild = builds.at(-1);\n        if (lastBuild) {\n          cursor = { id: lastBuild.id, projectId: lastBuild.projectId };\n        }\n        hasNext = builds.length === chunkSize;\n\n        const buildsToUpdate = [];\n\n        for (const build of builds) {\n          const props = JSON.parse(build.props);\n          let hasConversion = false;\n\n          // prop.value === 'self' > prop.value === '_self'\n          for (const prop of props) {\n            if (\n              prop.name === \"target\" &&\n              typeof prop.value === \"string\" &&\n              prop.value[0] !== \"_\"\n            ) {\n              prop.value = `_${prop.value}`;\n              hasConversion = true;\n            }\n          }\n\n          if (hasConversion) {\n            build.props = JSON.stringify(props);\n            buildsToUpdate.push(build as never);\n          }\n        }\n\n        await Promise.all(\n          buildsToUpdate.map(({ id, projectId, props }) =>\n            prisma.build.update({\n              where: { id_projectId: { id, projectId } },\n              data: { props },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230309142820_link-target/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database \n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230412160008_min-width-remove/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\n// NOTE ON IMPORTS:\n//\n//   We want to be able to run old migrations at any point.\n//   For example, when we setting up a fresh database or making a reset.\n//\n//   You shouldn't import code that may change later\n//   and become incompatible with the migration.\n//   It's better to copy it to the migration directory.\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      /*\n      There are no minWidth: 0 inside other breakpoints anymore. Manually tested with\n      select distinct((breakpoints::jsonb #> '{1, minWidth}')::int) from \"Build\";\n\n      Below in jsonb syntax:\n      #> means select by path\n      #- remove at path\n      */\n      await prisma.$executeRaw`\n        UPDATE \"Build\"\n        SET breakpoints = (breakpoints::jsonb #- '{0, minWidth}')::text\n        WHERE (breakpoints::jsonb #> '{0, minWidth}')::int = 0;\n      `;\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230412160008_min-width-remove/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230501151815_file/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"File\" (\n    \"name\" TEXT NOT NULL,\n    \"format\" TEXT NOT NULL,\n    \"size\" INTEGER NOT NULL,\n    \"description\" TEXT,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"meta\" TEXT NOT NULL DEFAULT '{}',\n    \"status\" \"UploadStatus\" NOT NULL DEFAULT 'UPLOADING',\n\n    CONSTRAINT \"File_pkey\" PRIMARY KEY (\"name\")\n);\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230501151941_asset-to-file/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\nexport default () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n  return client.$transaction(\n    async (prisma) => {\n      const result = await prisma.$executeRaw`\n        INSERT INTO \"File\"\n        (\n          \"name\",\n          \"format\",\n          \"size\",\n          \"description\",\n          \"createdAt\",\n          \"meta\",\n          \"status\"\n        )\n        SELECT\n          \"name\",\n          \"format\",\n          \"size\",\n          \"description\",\n          \"createdAt\",\n          \"meta\",\n          \"status\"\n        FROM \"Asset\"\n        GROUP BY\n          \"name\",\n          \"format\",\n          \"size\",\n          \"description\",\n          \"createdAt\",\n          \"meta\",\n          \"status\";\n      `;\n      console.info(`${result} files migrated from assets`);\n    },\n    { timeout: 1000 * 60 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230501151941_asset-to-file/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum Location {\n  FS\n  REMOTE\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name        String       @id\n  user        User?        @relation(fields: [userId], references: [id])\n  userId      String?\n  format      String\n  size        Int\n  description String?\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADING)\n}\n\nmodel Asset {\n  id          String       @default(uuid()) // not unique!\n  projectId   String\n  format      String\n  size        Int\n  name        String\n  description String?\n  location    Location\n  createdAt   DateTime     @default(now())\n  meta        String       @default(\"{}\")\n  status      UploadStatus @default(UPLOADED)\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n  files     File[]\n}\n\nmodel Project {\n  id        String   @id @default(uuid())\n  createdAt DateTime @default(now())\n  title     String\n  domain    String   @unique\n  user      User?    @relation(fields: [userId], references: [id])\n  userId    String?\n  build     Build[]\n  isDeleted Boolean  @default(false)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n}\n\nmodel Build {\n  id        String   @default(uuid())\n  createdAt DateTime @default(now())\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  isDev  Boolean // exctly one is true per project\n  isProd Boolean // at most one is true per project (none if not published)\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  @@id([id, projectId])\n}\n\nmodel Breakpoints {\n  buildId String @id\n  values  String @default(\"[]\")\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230501153024_asset-file-relation/migration.sql",
    "content": "-- AddForeignKey\nALTER TABLE \"Asset\" ADD CONSTRAINT \"Asset_name_fkey\" FOREIGN KEY (\"name\") REFERENCES \"File\"(\"name\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230515112405_file_uploader_project/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"File\" ADD COLUMN     \"uploaderProjectId\" TEXT;\n\n-- AddForeignKey\nALTER TABLE \"File\" ADD CONSTRAINT \"File_uploaderProjectId_fkey\" FOREIGN KEY (\"uploaderProjectId\") REFERENCES \"Project\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230517133730_file_is_deleted/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"File\" ADD COLUMN     \"isDeleted\" BOOLEAN NOT NULL DEFAULT false,\nADD COLUMN     \"updatedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230517150043_domain/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"DomainStatus\" AS ENUM ('INITIALIZING', 'ACTIVE', 'ERROR', 'PENDING');\n\n-- CreateTable\nCREATE TABLE \"Domain\" (\n    \"id\" TEXT NOT NULL,\n    \"domain\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"txtRecord\" TEXT,\n    \"status\" \"DomainStatus\" NOT NULL DEFAULT 'INITIALIZING',\n    \"error\" TEXT,\n\n    CONSTRAINT \"Domain_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"ProjectDomain\" (\n    \"projectId\" TEXT NOT NULL,\n    \"domainId\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"txtRecord\" TEXT NOT NULL,\n    \"cname\" TEXT NOT NULL,\n\n    CONSTRAINT \"ProjectDomain_pkey\" PRIMARY KEY (\"projectId\",\"domainId\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Domain_domain_key\" ON \"Domain\"(\"domain\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ProjectDomain_txtRecord_key\" ON \"ProjectDomain\"(\"txtRecord\");\n\n-- CreateIndex\nCREATE INDEX \"ProjectDomain_domainId_idx\" ON \"ProjectDomain\"(\"domainId\");\n\n-- AddForeignKey\nALTER TABLE \"ProjectDomain\" ADD CONSTRAINT \"ProjectDomain_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ProjectDomain\" ADD CONSTRAINT \"ProjectDomain_domainId_fkey\" FOREIGN KEY (\"domainId\") REFERENCES \"Domain\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n\n-- CreateView\nCREATE VIEW \"ProjectWithDomain\" AS\nSELECT\n  pd.\"projectId\",\n  pd.\"domainId\",\n  pd.\"txtRecord\",\n  pd.\"cname\",\n  pd.\"createdAt\",\n  -- any DNS txt record change would cause verified to be changed immediately\n  coalesce(pd.\"txtRecord\" = d.\"txtRecord\", false) AS verified,\n  -- domains count per user\n  p.\"userId\"\nFROM\n  \"ProjectDomain\" pd\n  LEFT JOIN \"Domain\" d ON pd.\"domainId\" = d.id\n  LEFT JOIN \"Project\" p ON pd.\"projectId\" = p.id;\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230520112258_drop_breakpoints/migration.sql",
    "content": "-- DropTable\nDROP TABLE \"Breakpoints\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230529133454_build_version/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"updatedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\nADD COLUMN     \"version\" INTEGER NOT NULL DEFAULT 0;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230530132921_deployment/migration.sql",
    "content": "-- Drop DashboardProject View\nDROP VIEW IF EXISTS \"DashboardProject\";\n\n-- AlterTable\n\nALTER TABLE \"Build\"\n  ADD COLUMN     \"deployment\" TEXT;\n\nUPDATE \"Build\"\n  SET \"deployment\" = '{\"domains\":[]}'\n  WHERE \"isDev\" = false;\n\n\nALTER TABLE \"Build\"\n  DROP COLUMN \"isDev\",\n  DROP COLUMN \"isProd\";\n\n-- CreateIndexes\nCREATE UNIQUE INDEX \"Build_id_key\" ON \"Build\"(\"id\");\n-- Ensure that only single \"dev\" build per project exists\nCREATE UNIQUE INDEX \"Build_dev_index\" ON \"Build\" (\"projectId\") WHERE \"deployment\" IS NULL;\n\n-- Update DashboardProject View\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"deployment\" IS NOT NULL\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230530155049_drop_unused_asset_fields/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\" DROP COLUMN \"createdAt\",\nDROP COLUMN \"description\",\nDROP COLUMN \"format\",\nDROP COLUMN \"location\",\nDROP COLUMN \"meta\",\nDROP COLUMN \"size\",\nDROP COLUMN \"status\";\n\n-- DropEnum\nDROP TYPE \"Location\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230605174851_domain-updated-at/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Domain\" ADD COLUMN     \"updatedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;\n\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230606165920_deployment-project-domain/migration.sql",
    "content": "UPDATE\n  \"Build\"\nSET\n  deployment = deployment::jsonb ||('{\"projectDomain\": \"' || \"Project\".domain || '\" }')::jsonb\nFROM\n  \"Project\"\nWHERE\n  \"Project\".id = \"Build\".\"projectId\"\n  AND \"Build\".deployment IS NOT NULL;\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230606234538_latest-build/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"PublishStatus\" AS ENUM ('PENDING', 'PUBLISHED', 'FAILED');\n\n-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"publishStatus\" \"PublishStatus\" NOT NULL DEFAULT 'PENDING';\n\n-- Update existing and published\nUPDATE \"Build\" SET \"publishStatus\" = 'PUBLISHED' WHERE deployment IS NOT NULL;\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Project_id_domain_key\" ON \"Project\"(\"id\", \"domain\");\n\n\nCREATE OR REPLACE VIEW \"LatestBuildPerProjectDomain\" AS\nWITH lbd AS (\n  SELECT DISTINCT ON (\"projectId\",\n    \"domain\")\n    jsonb_array_elements_text(deployment::jsonb -> 'domains') AS \"domain\",\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\",\n    bld.\"publishStatus\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    \"domain\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n),\nlb AS (\n  SELECT DISTINCT ON (\"projectId\")\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\",\n    bld.\"publishStatus\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n)\nSELECT\n  d.id AS \"domainId\",\n  lbd.\"projectId\",\n  lbd.\"buildId\",\n  coalesce(lbd.\"updatedAt\" = lb.\"updatedAt\", FALSE) AS \"isLatestBuild\",\n  lbd.\"publishStatus\",\n  lbd.\"updatedAt\"\nFROM\n  lbd,\n  lb,\n  \"Domain\" d\nWHERE\n  lbd.domain = d.domain\n  AND lb.\"projectId\" = lbd.\"projectId\";\n\nCREATE OR REPLACE VIEW \"LatestBuildPerProject\" AS\nWITH lb AS (\n  SELECT DISTINCT ON (\"projectId\")\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n)\nSELECT DISTINCT ON (\"projectId\", \"domain\")\n  bld.id AS \"buildId\",\n  bld.\"projectId\",\n  deployment::jsonb ->> 'projectDomain' AS domain,\n  coalesce(bld.\"updatedAt\" = lb.\"updatedAt\", FALSE) AS \"isLatestBuild\",\n  bld.\"updatedAt\",\n  bld.\"publishStatus\"\nFROM\n  \"Build\" bld,\n  lb\nWHERE\n  bld.deployment IS NOT NULL\n  AND lb.\"projectId\" = bld.\"projectId\"\nORDER BY\n  bld.\"projectId\",\n  \"domain\",\n  bld.\"createdAt\" DESC,\n  \"buildId\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230610111903_button_children/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Id = {\n  type: \"id\";\n  value: string;\n};\n\nexport type Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  label?: string;\n  children: Array<Id | Text>;\n};\n\ntype InstancesList = Instance[];\n\ntype BaseProp = {\n  id: string;\n  instanceId: string;\n  name: string;\n  required?: boolean;\n};\n\ntype Prop = BaseProp &\n  (\n    | { type: \"number\"; value: number }\n    | { type: \"string\"; value: string }\n    | { type: \"boolean\"; value: boolean }\n    | { type: \"asset\"; value: string }\n    | {\n        type: \"page\";\n        value: string | { pageId: string; instanceId: string };\n      }\n    | { type: \"string[]\"; value: string[] }\n  );\n\ntype PropsList = Prop[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            const instancesList: InstancesList = JSON.parse(build.instances);\n            const propsList: PropsList = JSON.parse(build.props);\n            const newPropsList: PropsList = [];\n            const buttonInstanceIds = new Set<string>();\n            const buttonInnerTexts = new Map<string, string>();\n\n            for (const instance of instancesList) {\n              if (\n                instance.component === \"Button\" ||\n                instance.component === \"VimeoPlayButton\"\n              ) {\n                buttonInstanceIds.add(instance.id);\n              }\n            }\n\n            for (const prop of propsList) {\n              if (\n                buttonInstanceIds.has(prop.instanceId) &&\n                prop.name === \"innerText\"\n              ) {\n                if (prop.type === \"string\") {\n                  buttonInnerTexts.set(prop.instanceId, prop.value);\n                }\n                continue;\n              }\n              newPropsList.push(prop);\n            }\n\n            for (const instance of instancesList) {\n              if (\n                instance.component === \"Button\" ||\n                instance.component === \"VimeoPlayButton\"\n              ) {\n                const innerText = buttonInnerTexts.get(instance.id);\n                if (instance.children.length !== 0 || innerText === undefined) {\n                  continue;\n                }\n                instance.children.push({ type: \"text\", value: innerText });\n              }\n            }\n\n            build.instances = JSON.stringify(instancesList);\n            build.props = JSON.stringify(newPropsList);\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          builds.map(({ id, instances, props }) =>\n            prisma.build.update({ where: { id }, data: { instances, props } })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230610111903_button_children/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id            String                 @id @default(uuid())\n  createdAt     DateTime               @default(now())\n  title         String\n  domain        String                 @unique\n  user          User?                  @relation(fields: [userId], references: [id])\n  userId        String?\n  build         Build[]\n  isDeleted     Boolean                @default(false)\n  files         File[]\n  projectDomain ProjectDomain[]\n  latestBuild   LatestBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id        String   @unique @default(uuid())\n  version   Int      @default(0)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230611121710_merge_block_text_components/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Id = {\n  type: \"id\";\n  value: string;\n};\n\nexport type Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  label?: string;\n  children: Array<Id | Text>;\n};\n\ntype InstancesList = Instance[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            const instancesList: InstancesList = JSON.parse(build.instances);\n\n            for (const instance of instancesList) {\n              if (instance.component === \"TextBlock\") {\n                instance.component = \"Text\";\n              }\n              if (instance.component === \"LinkBlock\") {\n                instance.component = \"Link\";\n              }\n            }\n\n            build.instances = JSON.stringify(instancesList);\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          builds.map(({ id, instances, props }) =>\n            prisma.build.update({ where: { id }, data: { instances, props } })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230611121710_merge_block_text_components/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id            String                 @id @default(uuid())\n  createdAt     DateTime               @default(now())\n  title         String\n  domain        String                 @unique\n  user          User?                  @relation(fields: [userId], references: [id])\n  userId        String?\n  build         Build[]\n  isDeleted     Boolean                @default(false)\n  files         File[]\n  projectDomain ProjectDomain[]\n  latestBuild   LatestBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id        String   @unique @default(uuid())\n  version   Int      @default(0)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230611181439_control_labels/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Id = {\n  type: \"id\";\n  value: string;\n};\n\nexport type Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  label?: string;\n  children: Array<Id | Text>;\n};\n\ntype InstancesList = Instance[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            const instancesList: InstancesList = JSON.parse(build.instances);\n            let changed = false;\n\n            for (const instance of instancesList) {\n              if (instance.component === \"RadioButtonField\") {\n                instance.component = \"Label\";\n                instance.label = \"Radio Field\";\n                changed = true;\n              }\n              if (instance.component === \"CheckboxField\") {\n                instance.component = \"Label\";\n                instance.label = \"Checkbox Field\";\n                changed = true;\n              }\n            }\n\n            if (changed) {\n              build.instances = JSON.stringify(instancesList);\n              changedBuilds.push(build);\n            }\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          changedBuilds.map(({ id, instances }) =>\n            prisma.build.update({ where: { id }, data: { instances } })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230611181439_control_labels/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id            String                 @id @default(uuid())\n  createdAt     DateTime               @default(now())\n  title         String\n  domain        String                 @unique\n  user          User?                  @relation(fields: [userId], references: [id])\n  userId        String?\n  build         Build[]\n  isDeleted     Boolean                @default(false)\n  files         File[]\n  projectDomain ProjectDomain[]\n  latestBuild   LatestBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id        String   @unique @default(uuid())\n  version   Int      @default(0)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230619131628_build_data_sources/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"dataSources\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230702002752_form_data_sources/migration.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { PrismaClient } from \"./client\";\n\nconst showAttribute = \"data-ws-show\";\n\nconst dataSourceVariablePrefix = \"$ws$dataSource$\";\n\nconst encodeDataSourceVariable = (id: string) => {\n  const encoded = id.replaceAll(\"-\", \"__DASH__\");\n  return `${dataSourceVariablePrefix}${encoded}`;\n};\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Id = {\n  type: \"id\";\n  value: string;\n};\n\nexport type Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  label?: string;\n  children: Array<Id | Text>;\n};\n\ntype InstancesList = Instance[];\n\ntype BaseProp = {\n  id: string;\n  instanceId: string;\n  name: string;\n  required?: boolean;\n};\n\ntype Prop = BaseProp &\n  (\n    | { type: \"number\"; value: number }\n    | { type: \"string\"; value: string }\n    | { type: \"boolean\"; value: boolean }\n    | { type: \"asset\"; value: string }\n    | { type: \"page\"; value: string | { pageId: string; instanceId: string } }\n    | { type: \"string[]\"; value: string[] }\n    | { type: \"dataSource\"; value: string }\n  );\n\ntype PropsList = Prop[];\n\ntype DataSourceVariableValue =\n  | { type: \"number\"; value: number }\n  | { type: \"string\"; value: string }\n  | { type: \"boolean\"; value: boolean }\n  | { type: \"string[]\"; value: string[] };\n\ntype DataSource =\n  | {\n      type: \"variable\";\n      id: string;\n      scopeInstanceId?: string;\n      name: string;\n      value: DataSourceVariableValue;\n    }\n  | {\n      type: \"expression\";\n      id: string;\n      scopeInstanceId?: string;\n      name: string;\n      code: string;\n    };\n\ntype DataSourcesList = DataSource[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            const instancesList: InstancesList = JSON.parse(build.instances);\n            const propsList: PropsList = JSON.parse(build.props);\n            const dataSourcesList: DataSourcesList = JSON.parse(\n              build.dataSources\n            );\n\n            const instances = new Map(\n              instancesList.map((instance) => [instance.id, instance])\n            );\n            let changed = false;\n\n            const formInstanceIds = new Set<string>();\n\n            for (const instance of instancesList) {\n              if (instance.component === \"Form\") {\n                formInstanceIds.add(instance.id);\n                const formDataSourceId = nanoid();\n                const formState = encodeDataSourceVariable(formDataSourceId);\n                dataSourcesList.push({\n                  type: \"variable\",\n                  id: formDataSourceId,\n                  scopeInstanceId: instance.id,\n                  name: \"formState\",\n                  value: {\n                    type: \"string\",\n                    value: \"initial\",\n                  },\n                });\n                propsList.push({\n                  id: nanoid(),\n                  instanceId: instance.id,\n                  type: \"dataSource\",\n                  name: \"state\",\n                  value: formDataSourceId,\n                });\n\n                // add show bindings to all direct children\n                for (const child of instance.children) {\n                  if (child.type !== \"id\") {\n                    continue;\n                  }\n                  const chillInstance = instances.get(child.value);\n                  if (chillInstance === undefined) {\n                    continue;\n                  }\n                  const childDataSourceId = nanoid();\n                  propsList.push({\n                    id: nanoid(),\n                    instanceId: chillInstance.id,\n                    type: \"dataSource\",\n                    name: showAttribute,\n                    value: childDataSourceId,\n                  });\n                  if (chillInstance.component === \"ErrorMessage\") {\n                    dataSourcesList.push({\n                      type: \"expression\",\n                      id: childDataSourceId,\n                      scopeInstanceId: chillInstance.id,\n                      name: \"formError\",\n                      code: `${formState} === 'error'`,\n                    });\n                  } else if (chillInstance.component === \"SuccessMessage\") {\n                    dataSourcesList.push({\n                      type: \"expression\",\n                      id: childDataSourceId,\n                      scopeInstanceId: chillInstance.id,\n                      name: \"formSuccess\",\n                      code: `${formState} === 'success'`,\n                    });\n                  } else {\n                    dataSourcesList.push({\n                      type: \"expression\",\n                      id: childDataSourceId,\n                      scopeInstanceId: chillInstance.id,\n                      name: \"formInitial\",\n                      code: `${formState} === 'initial' || ${formState} === 'error'`,\n                    });\n                  }\n                }\n\n                changed = true;\n              }\n              if (instance.component === \"ErrorMessage\") {\n                instance.component = \"Box\";\n                instance.label = \"Error Message\";\n                changed = true;\n              }\n              if (instance.component === \"SuccessMessage\") {\n                instance.component = \"Box\";\n                instance.label = \"Success Message\";\n                changed = true;\n              }\n            }\n\n            const newPropsList: PropsList = [];\n\n            for (const prop of propsList) {\n              if (\n                prop.name === \"initialState\" &&\n                formInstanceIds.has(prop.instanceId)\n              ) {\n                changed = true;\n                continue;\n              }\n              newPropsList.push(prop);\n            }\n\n            if (changed) {\n              build.instances = JSON.stringify(instancesList);\n              build.props = JSON.stringify(newPropsList);\n              build.dataSources = JSON.stringify(dataSourcesList);\n              changedBuilds.push(build);\n            }\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          changedBuilds.map(({ id, instances, props, dataSources }) =>\n            prisma.build.update({\n              where: { id },\n              data: { instances, props, dataSources },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230702002752_form_data_sources/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id            String                 @id @default(uuid())\n  createdAt     DateTime               @default(now())\n  title         String\n  domain        String                 @unique\n  user          User?                  @relation(fields: [userId], references: [id])\n  userId        String?\n  build         Build[]\n  isDeleted     Boolean                @default(false)\n  files         File[]\n  projectDomain ProjectDomain[]\n  latestBuild   LatestBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id        String   @unique @default(uuid())\n  version   Int      @default(0)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230831150459_add-administrators/migration.sql",
    "content": "-- AlterEnum\nALTER TYPE \"AuthorizationRelation\" ADD VALUE 'administrators';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230911125308_form_action/migration.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { PrismaClient } from \"./client\";\n\nconst dataSourceVariablePrefix = \"$ws$dataSource$\";\n\nconst encodeDataSourceVariable = (id: string) => {\n  const encoded = id.replaceAll(\"-\", \"__DASH__\");\n  return `${dataSourceVariablePrefix}${encoded}`;\n};\n\ntype Text = {\n  type: \"text\";\n  value: string;\n};\n\ntype Id = {\n  type: \"id\";\n  value: string;\n};\n\nexport type Instance = {\n  type: \"instance\";\n  id: string;\n  component: string;\n  label?: string;\n  children: Array<Id | Text>;\n};\n\ntype InstancesList = Instance[];\n\ntype BaseProp = {\n  id: string;\n  instanceId: string;\n  name: string;\n  required?: boolean;\n};\n\ntype Prop = BaseProp &\n  (\n    | { type: \"number\"; value: number }\n    | { type: \"string\"; value: string }\n    | { type: \"boolean\"; value: boolean }\n    | { type: \"asset\"; value: string }\n    | { type: \"page\"; value: string | { pageId: string; instanceId: string } }\n    | { type: \"string[]\"; value: string[] }\n    | { type: \"dataSource\"; value: string }\n    | {\n        type: \"action\";\n        value: Array<{ type: \"execute\"; args: string[]; code: string }>;\n      }\n  );\n\ntype PropsList = Prop[];\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n      while (hasNext) {\n        const builds = await prisma.build.findMany({\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          ...(cursor\n            ? {\n                skip: 1, // Skip the cursor\n                cursor: { id: cursor },\n              }\n            : null),\n        });\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            const instancesList: InstancesList = JSON.parse(build.instances);\n            const propsList: PropsList = JSON.parse(build.props);\n\n            let changed = false;\n\n            const formInstanceIds = new Set<string>();\n            for (const instance of instancesList) {\n              if (instance.component === \"Form\") {\n                formInstanceIds.add(instance.id);\n                changed = true;\n              }\n            }\n\n            for (const prop of propsList) {\n              if (\n                prop.name === \"state\" &&\n                formInstanceIds.has(prop.instanceId) &&\n                prop.type === \"dataSource\"\n              ) {\n                const dataSourceId = prop.value;\n                const formState = encodeDataSourceVariable(dataSourceId);\n                propsList.push({\n                  id: nanoid(),\n                  instanceId: prop.instanceId,\n                  type: \"action\",\n                  name: \"onStateChange\",\n                  value: [\n                    {\n                      type: \"execute\",\n                      args: [\"state\"],\n                      code: `${formState} = state`,\n                    },\n                  ],\n                });\n              }\n            }\n\n            if (changed) {\n              build.instances = JSON.stringify(instancesList);\n              build.props = JSON.stringify(propsList);\n              changedBuilds.push(build);\n            }\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        await Promise.all(\n          changedBuilds.map(({ id, instances, props, dataSources }) =>\n            prisma.build.update({\n              where: { id },\n              data: { instances, props, dataSources },\n            })\n          )\n        );\n      }\n    },\n    { timeout: 1000 * 60 * 8 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20230911125308_form_action/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id            String                 @id @default(uuid())\n  createdAt     DateTime               @default(now())\n  title         String\n  domain        String                 @unique\n  user          User?                  @relation(fields: [userId], references: [id])\n  userId        String?\n  build         Build[]\n  isDeleted     Boolean                @default(false)\n  files         File[]\n  projectDomain ProjectDomain[]\n  latestBuild   LatestBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id        String   @unique @default(uuid())\n  version   Int      @default(0)\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n  pages     String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n  administrators\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231105075338_add-last-transaction-id/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"lastTransactionId\" TEXT;\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231108172804_prop_expression/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype BaseProp = {\n  id: string;\n  instanceId: string;\n  name: string;\n  required?: boolean;\n};\n\ntype Prop = BaseProp &\n  (\n    | { type: \"number\"; value: number }\n    | { type: \"string\"; value: string }\n    | { type: \"boolean\"; value: boolean }\n    | { type: \"asset\"; value: string }\n    | { type: \"page\"; value: string | { pageId: string; instanceId: string } }\n    | { type: \"string[]\"; value: string[] }\n    | { type: \"dataSource\"; value: string }\n    | { type: \"expression\"; value: string }\n    | {\n        type: \"action\";\n        value: Array<{ type: \"execute\"; args: string[]; code: string }>;\n      }\n  );\n\ntype PropsList = Prop[];\n\ntype DataSourceVariableValue =\n  | { type: \"number\"; value: number }\n  | { type: \"string\"; value: string }\n  | { type: \"boolean\"; value: boolean }\n  | { type: \"string[]\"; value: string[] }\n  | { type: \"json\"; value: unknown };\n\ntype DataSource =\n  | {\n      type: \"variable\";\n      id: string;\n      scopeInstanceId?: string;\n      name: string;\n      value: DataSourceVariableValue;\n    }\n  | {\n      type: \"expression\";\n      id: string;\n      scopeInstanceId?: string;\n      name: string;\n      code: string;\n    };\n\ntype DataSourcesList = DataSource[];\n\nconst dataSourceVariablePrefix = \"$ws$dataSource$\";\n\nconst encodeDataSourceVariable = (id: string) => {\n  const encoded = id.replaceAll(\"-\", \"__DASH__\");\n  return `${dataSourceVariablePrefix}${encoded}`;\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        console.info(\"CHUNK\", cursor);\n        console.time(\"read\");\n\n        const cursorOptions: {} = cursor\n          ? {\n              skip: 1, // Skip the cursor\n              cursor: { id: cursor },\n            }\n          : {};\n\n        const builds = await prisma.build.findMany({\n          select: {\n            id: true,\n            props: true,\n            dataSources: true,\n          },\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          where: {\n            AND: [\n              {\n                OR: [\n                  {\n                    props: {\n                      not: \"[]\",\n                    },\n                  },\n                  {\n                    dataSources: {\n                      not: \"[]\",\n                    },\n                  },\n                ],\n              },\n              {\n                deployment: null,\n              },\n            ],\n          },\n\n          ...cursorOptions,\n        });\n        console.timeEnd(\"read\");\n\n        console.time(\"parse-change\");\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            let dataSourcesList: DataSourcesList = JSON.parse(\n              build.dataSources\n            );\n            let propsList: PropsList = JSON.parse(build.props);\n\n            let changed = false;\n\n            const dataSources = new Map(\n              dataSourcesList.map((dataSource) => [dataSource.id, dataSource])\n            );\n\n            // convert type=dataSource to type=expression\n            propsList = propsList.map((prop) => {\n              if (prop.type === \"dataSource\") {\n                changed = true;\n\n                const dataSourceId = prop.value;\n                const dataSource = dataSources.get(dataSourceId);\n                if (dataSource?.type === \"variable\") {\n                  return {\n                    ...prop,\n                    type: \"expression\",\n                    value: encodeDataSourceVariable(dataSourceId),\n                  };\n                }\n                if (dataSource?.type === \"expression\") {\n                  return {\n                    ...prop,\n                    type: \"expression\",\n                    value: dataSource.code,\n                  };\n                }\n              }\n              return prop;\n            });\n\n            // delete all expression data sources\n            dataSourcesList = dataSourcesList.filter((dataSource) => {\n              if (dataSource.type === \"variable\") {\n                changed = true;\n                return true;\n              }\n              return false;\n            });\n\n            if (changed) {\n              build.dataSources = JSON.stringify(dataSourcesList);\n              build.props = JSON.stringify(propsList);\n              changedBuilds.push(build);\n            }\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        console.timeEnd(\"parse-change\");\n        console.info(\"changedBuilds.length\", changedBuilds.length);\n        console.time(\"update\");\n\n        const sql = `\n          UPDATE \"Build\"\n          SET\n            \"props\" = data.\"props\",\n            \"dataSources\" = data.\"dataSources\"\n          FROM unnest(ARRAY[$1], ARRAY[$2], ARRAY[$3]) as data(id, props, \"dataSources\")\n          WHERE \"Build\".\"id\" = data.\"id\"\n        `;\n\n        if (changedBuilds.length === 0) {\n          return;\n        }\n        const res = await prisma.$executeRawUnsafe(\n          sql,\n          changedBuilds.map((changedBuild) => changedBuild.id),\n          changedBuilds.map((changedBuild) => changedBuild.props),\n          changedBuilds.map((changedBuild) => changedBuild.dataSources)\n        );\n\n        console.timeEnd(\"update\");\n        console.info(\"res\", res);\n      }\n    },\n    { timeout: 3600000 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231108172804_prop_expression/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id        String    @id @default(uuid())\n  email     String?   @unique\n  provider  String?\n  image     String?\n  username  String?\n  createdAt DateTime  @default(now())\n  team      Team?     @relation(fields: [teamId], references: [id])\n  teamId    String?\n  projects  Project[]\n}\n\nmodel Project {\n  id            String                 @id @default(uuid())\n  createdAt     DateTime               @default(now())\n  title         String\n  domain        String                 @unique\n  user          User?                  @relation(fields: [userId], references: [id])\n  userId        String?\n  build         Build[]\n  isDeleted     Boolean                @default(false)\n  files         File[]\n  projectDomain ProjectDomain[]\n  latestBuild   LatestBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id                String   @unique @default(uuid())\n  version           Int      @default(0)\n  lastTransactionId String?\n  createdAt         DateTime @default(now())\n  updatedAt         DateTime @default(now()) @updatedAt\n  pages             String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n  administrators\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  // No relation to Project, as the Authorization system is not tied to a project\n  projectId String\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231115205820_client-references/migration.sql",
    "content": "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";\n\n-- CreateTable\nCREATE TABLE \"ClientReferences\" (\n    \"id\" TEXT NOT NULL DEFAULT uuid_generate_v4(),\n    \"reference\" TEXT NOT NULL DEFAULT uuid_generate_v4(),\n    \"service\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"userId\" TEXT NOT NULL,\n\n    CONSTRAINT \"ClientReferences_pkey\" PRIMARY KEY (\"id\")\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ClientReferences_userId_service_key\" ON \"ClientReferences\"(\"userId\", \"service\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"ClientReferences_reference_service_key\" ON \"ClientReferences\"(\"reference\", \"service\");\n\n-- AddForeignKey\nALTER TABLE \"ClientReferences\" ADD CONSTRAINT \"ClientReferences_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231116173417_product/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Product\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"description\" TEXT,\n    \"features\" TEXT[],\n    \"images\" TEXT[],\n    \"meta\" JSONB NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n\n    CONSTRAINT \"Product_pkey\" PRIMARY KEY (\"id\")\n);\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231117095612_transaction/migration.sql",
    "content": "-- DropIndex\nDROP INDEX \"ClientReferences_userId_service_key\";\n\n-- AlterTable\nALTER TABLE \"ClientReferences\" DROP CONSTRAINT \"ClientReferences_pkey\",\nDROP COLUMN \"id\",\nADD CONSTRAINT \"ClientReferences_pkey\" PRIMARY KEY (\"userId\", \"service\");\n\n-- CreateTable\nCREATE TABLE \"TransactionLog\" (\n    \"eventId\" TEXT NOT NULL,\n    \"sessionId\" TEXT NOT NULL,\n    \"productId\" TEXT NOT NULL,\n    \"userId\" TEXT NOT NULL,\n    \"status\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"TransactionLog_eventId_productId_key\" ON \"TransactionLog\"(\"eventId\", \"productId\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"TransactionLog_sessionId_productId_key\" ON \"TransactionLog\"(\"sessionId\", \"productId\");\n\n-- AddForeignKey\nALTER TABLE \"TransactionLog\" ADD CONSTRAINT \"TransactionLog_productId_fkey\" FOREIGN KEY (\"productId\") REFERENCES \"Product\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"TransactionLog\" ADD CONSTRAINT \"TransactionLog_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231119153806_transaction-customer-subscription/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"TransactionLog\" ADD COLUMN     \"customerId\" TEXT,\nADD COLUMN     \"subscriptionId\" TEXT,\nADD COLUMN     \"customerEmail\" TEXT;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231120172840_add-event-type/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"TransactionLog\" ADD COLUMN     \"eventType\" TEXT NOT NULL DEFAULT 'checkout.session.completed';\n\nALTER TABLE \"TransactionLog\"\nALTER COLUMN \"eventType\" DROP DEFAULT;\n\n-- AlterTable\nALTER TABLE \"TransactionLog\" ADD COLUMN     \"eventCreated\" INTEGER NOT NULL DEFAULT 0;\n-- Fill Initally\nUPDATE \"TransactionLog\" SET \"eventCreated\" = EXTRACT(EPOCH FROM \"createdAt\");\n\nALTER TABLE \"TransactionLog\"\nALTER COLUMN \"eventCreated\" DROP DEFAULT;\n\n-- AlterTable\nALTER TABLE \"TransactionLog\" ADD CONSTRAINT \"TransactionLog_pkey\" PRIMARY KEY (\"eventId\");\n\n-- DropIndex\nDROP INDEX \"TransactionLog_sessionId_productId_key\";\n\n\nCREATE VIEW \"UserProduct\" AS\nSELECT \"userId\", \"subscriptionId\", \"productId\", \"customerId\", \"customerEmail\"\nFROM \"TransactionLog\" AS tl\nWHERE \"status\" = 'paid'\n  AND NOT EXISTS (\n    SELECT 1\n    FROM \"TransactionLog\" AS tlexsists\n    WHERE tlexsists.\"subscriptionId\" = tl.\"subscriptionId\"\n      AND tlexsists.\"status\" = 'canceled'\n      AND tlexsists.\"eventCreated\" > tl.\"eventCreated\"\n  )\nORDER BY \"userId\", \"eventCreated\" DESC;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231121125755_token-uniq/migration.sql",
    "content": "-- CreateIndex\nCREATE UNIQUE INDEX \"AuthorizationToken_token_key\" ON \"AuthorizationToken\"(\"token\");\n-- AddForeignKey\nALTER TABLE \"AuthorizationToken\" ADD CONSTRAINT \"AuthorizationToken_projectId_fkey\" FOREIGN KEY (\"projectId\") REFERENCES \"Project\"(\"id\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231129164239_event-data/migration.sql",
    "content": "DROP VIEW \"UserProduct\";\n\nALTER TABLE \"public\".\"TransactionLog\" ALTER COLUMN \"productId\" DROP NOT NULL;\n\nALTER TABLE \"TransactionLog\" DROP CONSTRAINT \"TransactionLog_productId_fkey\";\n\n-- AddForeignKey\nALTER TABLE \"TransactionLog\" ADD CONSTRAINT \"TransactionLog_productId_fkey\" FOREIGN KEY (\"productId\") REFERENCES \"Product\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n\n\nALTER TABLE \"TransactionLog\"\nDROP COLUMN \"eventCreated\",\nDROP COLUMN \"eventType\",\nDROP COLUMN \"status\",\nDROP COLUMN \"customerEmail\",\nDROP COLUMN \"subscriptionId\",\nDROP COLUMN \"customerId\",\nDROP COLUMN \"sessionId\";\n\nALTER TABLE \"TransactionLog\"\nADD COLUMN     \"eventData\" JSONB,\nADD COLUMN     \"eventCreated\" INT GENERATED ALWAYS AS ((\"eventData\"#>>'{created}')::INTEGER) STORED,\nADD COLUMN     \"eventType\" TEXT GENERATED ALWAYS AS (\"eventData\"#>>'{type}') STORED,\nADD COLUMN     \"status\" TEXT GENERATED ALWAYS AS (\"eventData\"#>>'{data,object,status}') STORED,\nADD COLUMN     \"customerId\" TEXT GENERATED ALWAYS AS (\"eventData\"#>>'{data,object,customer}') STORED,\nADD COLUMN     \"customerEmail\" TEXT GENERATED ALWAYS AS (\"eventData\"#>>'{data,object,customer_details,email}') STORED,\nADD COLUMN     \"subscriptionId\" TEXT GENERATED ALWAYS AS (\n  CASE\n    WHEN \"eventData\"#>>'{data,object,object}' = 'subscription'\n    THEN \"eventData\"#>>'{data,object,id}'\n    ELSE \"eventData\"#>>'{data,object,subscription}'\n  END\n) STORED,\nADD COLUMN     \"paymentIntent\" TEXT GENERATED ALWAYS AS (\"eventData\"#>>'{data,object,payment_intent}') STORED;\n\n\nCREATE VIEW \"UserProduct\" AS\nSELECT \"userId\", \"subscriptionId\", \"productId\", \"customerId\", \"customerEmail\"\nFROM \"TransactionLog\" AS tl\nWHERE \"status\" = 'complete' AND \"eventType\" = 'checkout.session.completed'\n  AND NOT EXISTS (\n    SELECT 1\n    FROM \"TransactionLog\" AS tlexsists\n    WHERE tlexsists.\"subscriptionId\" = tl.\"subscriptionId\"\n      AND tlexsists.\"eventType\" = 'customer.subscription.deleted'\n      AND tlexsists.\"status\" = 'canceled'\n      AND tlexsists.\"eventCreated\" > tl.\"eventCreated\"\n  )\n  AND NOT EXISTS (\n    SELECT 1\n    FROM \"TransactionLog\" AS tlexsists\n    WHERE tlexsists.\"paymentIntent\" = tl.\"paymentIntent\"\n      AND tlexsists.\"eventType\" = 'charge.refunded'\n      AND tlexsists.\"status\" = 'succeeded'\n      AND tlexsists.\"eventCreated\" > tl.\"eventCreated\"\n  )\nORDER BY \"userId\", \"eventCreated\" DESC;\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20231211152313_build_resources/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Build\" ADD COLUMN     \"resources\" TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240112011509_folders/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        console.info(\"CHUNK\", cursor);\n        console.time(\"read\");\n\n        const cursorOptions: {} = cursor\n          ? {\n              skip: 1, // Skip the cursor\n              cursor: { id: cursor },\n            }\n          : {};\n\n        const builds = await prisma.build.findMany({\n          select: {\n            id: true,\n            pages: true,\n          },\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          //where: { id: \"d55ce7ea-6853-40af-8a65-b63b79dde0f9\" },\n          ...cursorOptions,\n        });\n        console.timeEnd(\"read\");\n\n        console.time(\"parse-change\");\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const pages = JSON.parse(build.pages);\n          // Already migrated\n          if (pages.folders) {\n            continue;\n          }\n\n          try {\n            const pageIds = [pages.homePage, ...pages.pages].map(\n              (page) => page.id\n            );\n\n            pages.folders = [\n              {\n                id: \"root\",\n                name: \"Root\",\n                slug: \"\",\n                children: pageIds,\n              },\n            ];\n\n            build.pages = JSON.stringify(pages);\n            changedBuilds.push(build);\n          } catch {\n            console.info(`build ${build.id} cannot be converted`);\n          }\n        }\n\n        if (changedBuilds.length === 0) {\n          return;\n        }\n\n        console.timeEnd(\"parse-change\");\n        console.info(\"changedBuilds.length\", changedBuilds.length);\n        console.time(\"update\");\n\n        const sql = `\n          UPDATE \"Build\"\n          SET\n            \"pages\" = data.\"pages\"\n          FROM unnest(ARRAY[$1], ARRAY[$2]) as data(id, pages)\n          WHERE \"Build\".\"id\" = data.\"id\"\n        `;\n\n        const res = await prisma.$executeRawUnsafe(\n          sql,\n          changedBuilds.map((changedBuild) => changedBuild.id),\n          changedBuilds.map((changedBuild) => changedBuild.pages)\n        );\n\n        console.timeEnd(\"update\");\n        console.info(\"res\", res);\n      }\n    },\n    { timeout: 3600000 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240112011509_folders/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\", \"clientExtensions\", \"jsonProtocol\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider = \"postgres\"\n  url      = env(\"DATABASE_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id        String @default(uuid()) // not unique!\n  projectId String\n  file      File   @relation(fields: [name], references: [name])\n  name      String\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id               String             @id @default(uuid())\n  email            String?            @unique\n  provider         String?\n  image            String?\n  username         String?\n  createdAt        DateTime           @default(now())\n  team             Team?              @relation(fields: [teamId], references: [id])\n  teamId           String?\n  projects         Project[]\n  clientReferences ClientReferences[]\n  checkout         TransactionLog[]\n  products         UserProduct[]\n}\n\n// User client references in external services like stripe etc\nmodel ClientReferences {\n  reference String   @default(dbgenerated())\n  service   String\n  createdAt DateTime @default(now())\n  user      User     @relation(fields: [userId], references: [id])\n  userId    String\n\n  @@id([userId, service])\n  @@unique([reference, service])\n}\n\nmodel Product {\n  id          String   @id\n  name        String\n  description String?\n  features    String[]\n  images      String[]\n  meta        Json\n\n  createdAt    DateTime         @default(now())\n  checkout     TransactionLog[]\n  userProducts UserProduct[]\n}\n\nmodel TransactionLog {\n  // Stripe event id (debug and idempotency purposes)\n  eventId String @id\n  userId  String\n  user    User   @relation(fields: [userId], references: [id])\n\n  productId String?\n  product   Product? @relation(fields: [productId], references: [id])\n\n  createdAt DateTime @default(now())\n\n  eventData Json? @default(dbgenerated())\n\n  eventType    String? @default(dbgenerated())\n  eventCreated Int?    @default(dbgenerated())\n\n  // paid\n  status String? @default(dbgenerated())\n\n  customerId    String? @default(dbgenerated())\n  customerEmail String? @default(dbgenerated())\n\n  subscriptionId String? @default(dbgenerated())\n\n  // Used for Refund\n  paymentIntent String? @default(dbgenerated())\n\n  @@unique([eventId, productId])\n}\n\nview UserProduct {\n  userId         String\n  user           User    @relation(fields: [userId], references: [id])\n  productId      String\n  product        Product @relation(fields: [productId], references: [id])\n  // subscriptionId and customerId not null for subscriptions\n  subscriptionId String?\n  customerId     String?\n  // Easier to debug\n  customerEmail  String?\n\n  @@unique([userId, productId])\n}\n\nmodel Project {\n  id                 String                 @id @default(uuid())\n  createdAt          DateTime               @default(now())\n  title              String\n  domain             String                 @unique\n  user               User?                  @relation(fields: [userId], references: [id])\n  userId             String?\n  build              Build[]\n  isDeleted          Boolean                @default(false)\n  files              File[]\n  projectDomain      ProjectDomain[]\n  latestBuild        LatestBuildPerProject?\n  authorizationToken AuthorizationToken[]\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id                String   @unique @default(uuid())\n  version           Int      @default(0)\n  lastTransactionId String?\n  createdAt         DateTime @default(now())\n  updatedAt         DateTime @default(now()) @updatedAt\n  pages             String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  resources             String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n  administrators\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  projectId String\n  project   Project               @relation(fields: [projectId], references: [id])\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n  @@unique([token])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id          String   @id @default(uuid())\n  createdAt   DateTime @default(now())\n  title       String\n  domain      String\n  userId      String?\n  isDeleted   Boolean  @default(false)\n  isPublished Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240112155724_nullable-user/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"TransactionLog\" DROP CONSTRAINT \"TransactionLog_userId_fkey\";\n\n-- AlterTable\nALTER TABLE \"TransactionLog\" ALTER COLUMN \"userId\" DROP NOT NULL;\n\n-- AddForeignKey\nALTER TABLE \"TransactionLog\" ADD CONSTRAINT \"TransactionLog_userId_fkey\" FOREIGN KEY (\"userId\") REFERENCES \"User\"(\"id\") ON DELETE SET NULL ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240112155725_user_product_view/migration.sql",
    "content": "CREATE OR REPLACE VIEW \"UserProduct\" AS (\n  SELECT\n    \"userId\",\n    \"subscriptionId\",\n    \"productId\",\n    \"customerId\",\n    \"customerEmail\"\n  FROM\n    \"TransactionLog\" AS tl\n  WHERE\n    \"status\" = 'complete'\n    AND \"eventType\" = 'checkout.session.completed'\n    AND NOT EXISTS (\n      SELECT\n        1\n      FROM\n        \"TransactionLog\" AS tlexsists\n      WHERE\n        tlexsists.\"subscriptionId\" = tl.\"subscriptionId\"\n        AND tlexsists.\"eventType\" = 'customer.subscription.deleted'\n        AND tlexsists.\"status\" = 'canceled'\n        AND tlexsists.\"eventCreated\" > tl.\"eventCreated\")\n      AND NOT EXISTS (\n        SELECT\n          1\n        FROM\n          \"TransactionLog\" AS tlexsists\n        WHERE\n          tlexsists.\"paymentIntent\" = tl.\"paymentIntent\"\n          AND tlexsists.\"eventType\" = 'charge.refunded'\n          AND tlexsists.\"status\" = 'succeeded'\n          AND tlexsists.\"eventCreated\" > tl.\"eventCreated\")\n      ORDER BY\n        \"userId\",\n        \"eventCreated\" DESC)\n    UNION ALL (\n      SELECT\n        \"userId\",\n        \"subscriptionId\",\n        \"productId\",\n        \"customerId\",\n        \"customerEmail\"\n      FROM\n        \"TransactionLog\"\n      WHERE\n        \"eventType\" = 'appsumo.activate');\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240125182656_calc-domains/migration.sql",
    "content": "CREATE OR REPLACE VIEW \"ProjectWithDomain\" AS\nSELECT\n  pd.\"projectId\",\n  pd.\"domainId\",\n  pd.\"txtRecord\",\n  pd.\"cname\",\n  pd.\"createdAt\",\n  -- any DNS txt record change would cause verified to be changed immediately\n  coalesce(pd.\"txtRecord\" = d.\"txtRecord\", false) AS verified,\n  -- domains count per user\n  p.\"userId\"\nFROM\n  \"ProjectDomain\" pd\n  LEFT JOIN \"Domain\" d ON pd.\"domainId\" = d.id\n  LEFT JOIN \"Project\" p ON pd.\"projectId\" = p.id\n  WHERE p.\"isDeleted\" = FALSE;\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240127230238_project-preview/migration.sql",
    "content": "ALTER TABLE\n  \"public\".\"Project\"\nADD\n  COLUMN \"previewImageAssetId\" text;\n\n-- AddForeignKey\nALTER TABLE\n  \"Project\"\nADD\n  CONSTRAINT \"Project_previewImageAssetId_id_fkey\" FOREIGN KEY (\"previewImageAssetId\", \"id\") REFERENCES \"Asset\"(\"id\", \"projectId\") ON DELETE\nSET\n  NULL;\n\n-- Drop DashboardProject View\nDROP VIEW IF EXISTS \"DashboardProject\";\n\n-- Update DashboardProject View\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"deployment\" IS NOT NULL\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240131193159_new_constraints/migration.sql",
    "content": "-- DropForeignKey\nALTER TABLE \"Project\" DROP CONSTRAINT \"Project_previewImageAssetId_id_fkey\";\n\n-- AddForeignKey\nALTER TABLE \"Project\" ADD CONSTRAINT \"Project_previewImageAssetId_id_fkey\" FOREIGN KEY (\"previewImageAssetId\", \"id\") REFERENCES \"Asset\"(\"id\", \"projectId\") ON DELETE RESTRICT ON UPDATE CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240131200102_page_meta_expressions/migration.ts",
    "content": "import { PrismaClient } from \"./client\";\n\ntype Folder = {\n  id: string;\n  name: string;\n  slug: string;\n  children: string[];\n};\n\ntype Page = {\n  path: string;\n  id: string;\n  name: string;\n  title: string;\n  meta: {\n    description?: string;\n    title?: string;\n    // convert boolean to expression\n    excludePageFromSearch?: any;\n    socialImageAssetId?: string;\n    socialImageUrl?: string;\n    custom?: Array<{\n      property: string;\n      content: string;\n    }>;\n  };\n  rootInstanceId: string;\n  pathVariableId?: string;\n};\n\ntype ProjectMeta = {\n  siteName?: string;\n  faviconAssetId?: string;\n  code?: string;\n};\n\ntype PageRedirect = {\n  old: string;\n  new: string;\n};\n\ntype ProjectSettings = {\n  atomicStyles?: boolean;\n  redirects?: PageRedirect[];\n};\n\ntype Pages = {\n  meta?: ProjectMeta;\n  settings?: ProjectSettings;\n  homePage: Page;\n  pages: Page[];\n  folders: Folder[];\n};\n\n/**\n * convert all fields to js expression\n * string -> `\"string\"`\n * false -> `false`\n * undefined -> undefined\n */\nconst mutatePageMeta = (page: Page) => {\n  page.title = JSON.stringify(page.title);\n  page.meta.description = JSON.stringify(page.meta.description);\n  page.meta.excludePageFromSearch = JSON.stringify(\n    page.meta.excludePageFromSearch\n  );\n  page.meta.socialImageUrl = JSON.stringify(page.meta.socialImageUrl);\n  if (page.meta.custom) {\n    for (const item of page.meta.custom) {\n      item.content = JSON.stringify(item.content);\n    }\n  }\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        console.info(\"CHUNK\", cursor);\n        console.time(\"read\");\n\n        const cursorOptions: {} = cursor\n          ? {\n              skip: 1, // Skip the cursor\n              cursor: { id: cursor },\n            }\n          : {};\n\n        const builds = await prisma.build.findMany({\n          select: {\n            id: true,\n            pages: true,\n          },\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          where: {\n            AND: [\n              {\n                deployment: null,\n              },\n            ],\n          },\n\n          ...cursorOptions,\n        });\n        console.timeEnd(\"read\");\n\n        console.time(\"parse-change\");\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const buildId = build.id;\n          try {\n            const pages: Pages = JSON.parse(build.pages);\n\n            mutatePageMeta(pages.homePage);\n            for (const page of pages.pages) {\n              mutatePageMeta(page);\n            }\n\n            build.pages = JSON.stringify(pages);\n            changedBuilds.push(build);\n          } catch {\n            console.info(`build ${buildId} cannot be converted`);\n          }\n        }\n        console.timeEnd(\"parse-change\");\n        console.info(\"changedBuilds.length\", changedBuilds.length);\n        console.time(\"update\");\n\n        const sql = `\n          UPDATE \"Build\"\n          SET\n            \"pages\" = data.\"pages\"\n          FROM unnest(ARRAY[$1], ARRAY[$2]) as data(id, pages)\n          WHERE \"Build\".\"id\" = data.\"id\"\n        `;\n\n        if (changedBuilds.length === 0) {\n          return;\n        }\n        const res = await prisma.$executeRawUnsafe(\n          sql,\n          changedBuilds.map((changedBuild) => changedBuild.id),\n          changedBuilds.map((changedBuild) => changedBuild.pages)\n        );\n\n        console.timeEnd(\"update\");\n        console.info(\"res\", res);\n      }\n    },\n    { timeout: 3600000 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240131200102_page_meta_expressions/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider  = \"postgres\"\n  url       = env(\"DATABASE_URL\")\n  directUrl = env(\"DIRECT_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id               String             @default(uuid()) // not unique!\n  projectId        String\n  file             File               @relation(fields: [name], references: [name])\n  name             String\n  Project          Project[]\n  DashboardProject DashboardProject[]\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id               String             @id @default(uuid())\n  email            String?            @unique\n  provider         String?\n  image            String?\n  username         String?\n  createdAt        DateTime           @default(now())\n  team             Team?              @relation(fields: [teamId], references: [id])\n  teamId           String?\n  projects         Project[]\n  clientReferences ClientReferences[]\n  checkout         TransactionLog[]\n  products         UserProduct[]\n}\n\n// User client references in external services like stripe etc\nmodel ClientReferences {\n  reference String   @default(dbgenerated())\n  service   String\n  createdAt DateTime @default(now())\n  user      User     @relation(fields: [userId], references: [id])\n  userId    String\n\n  @@id([userId, service])\n  @@unique([reference, service])\n}\n\nmodel Product {\n  id          String   @id\n  name        String\n  description String?\n  features    String[]\n  images      String[]\n  meta        Json\n\n  createdAt    DateTime         @default(now())\n  checkout     TransactionLog[]\n  userProducts UserProduct[]\n}\n\nmodel TransactionLog {\n  // Stripe event id (debug and idempotency purposes)\n  eventId String  @id\n  userId  String?\n  user    User?   @relation(fields: [userId], references: [id])\n\n  productId String?\n  product   Product? @relation(fields: [productId], references: [id])\n\n  createdAt DateTime @default(now())\n\n  eventData Json? @default(dbgenerated())\n\n  eventType    String? @default(dbgenerated())\n  eventCreated Int?    @default(dbgenerated())\n\n  // paid\n  status String? @default(dbgenerated())\n\n  customerId    String? @default(dbgenerated())\n  customerEmail String? @default(dbgenerated())\n\n  subscriptionId String? @default(dbgenerated())\n\n  // Used for Refund\n  paymentIntent String? @default(dbgenerated())\n\n  @@unique([eventId, productId])\n}\n\nview UserProduct {\n  userId         String\n  user           User    @relation(fields: [userId], references: [id])\n  productId      String\n  product        Product @relation(fields: [productId], references: [id])\n  // subscriptionId and customerId not null for subscriptions\n  subscriptionId String?\n  customerId     String?\n  // Easier to debug\n  customerEmail  String?\n\n  @@unique([userId, productId])\n}\n\nmodel Project {\n  id                  String                 @id @default(uuid())\n  createdAt           DateTime               @default(now())\n  title               String\n  domain              String                 @unique\n  user                User?                  @relation(fields: [userId], references: [id])\n  userId              String?\n  build               Build[]\n  isDeleted           Boolean                @default(false)\n  files               File[]\n  projectDomain       ProjectDomain[]\n  latestBuild         LatestBuildPerProject?\n  authorizationToken  AuthorizationToken[]\n  previewImageAsset   Asset?                 @relation(fields: [previewImageAssetId, id], references: [id, projectId])\n  previewImageAssetId String?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nmodel Build {\n  id                String   @unique @default(uuid())\n  version           Int      @default(0)\n  lastTransactionId String?\n  createdAt         DateTime @default(now())\n  updatedAt         DateTime @default(now()) @updatedAt\n  pages             String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  resources             String @default(\"[]\")\n  instances             String @default(\"[]\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n  administrators\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  projectId String\n  project   Project               @relation(fields: [projectId], references: [id])\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n  @@unique([token])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n}\n\n// Dashboard\nview DashboardProject {\n  id                  String   @id @default(uuid())\n  createdAt           DateTime @default(now())\n  title               String\n  domain              String\n  userId              String?\n  previewImageAsset   Asset?   @relation(fields: [previewImageAssetId, id], references: [id, projectId])\n  previewImageAssetId String?\n  isDeleted           Boolean  @default(false)\n  isPublished         Boolean\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240227150630_marketplace/migration.sql",
    "content": "-- CreateEnum\nCREATE TYPE \"MarketplaceApprovalStatus\" AS ENUM ('UNLISTED', 'PENDING', 'APPROVED', 'REJECTED');\n\n-- AlterTable\nALTER TABLE\n  \"Build\"\nADD\n  COLUMN \"marketplaceProduct\" TEXT NOT NULL DEFAULT '{}';\n\n-- AlterTable\nALTER TABLE\n  \"Project\"\nADD\n  COLUMN \"marketplaceApprovalStatus\" \"MarketplaceApprovalStatus\" NOT NULL DEFAULT 'UNLISTED';"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240229133316_add-approval-status/migration.sql",
    "content": "-- Drop DashboardProject View\nDROP VIEW IF EXISTS \"DashboardProject\";\n\n-- Update DashboardProject View\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"deployment\" IS NOT NULL\n  ) AS \"isPublished\"\nFROM\n  \"Project\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240308131249_add-token-rights/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"AuthorizationToken\" ADD COLUMN     \"canClone\" BOOLEAN NOT NULL DEFAULT true,\nADD COLUMN     \"canCopy\" BOOLEAN NOT NULL DEFAULT true;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240309212641_page_system_variable/migration.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport { PrismaClient } from \"./client\";\n\ntype DataSource = {\n  type: \"parameter\";\n  id: string;\n  scopeInstanceId?: string;\n  name: string;\n};\n\ntype Page = {\n  id: string;\n  name: string;\n  path: string;\n  title: string;\n  meta: {\n    description?: string;\n    title?: string;\n    excludePageFromSearch?: string;\n    language?: string;\n    socialImageAssetId?: string;\n    socialImageUrl?: string;\n    status?: string;\n    redirect?: string;\n    custom?: Array<{\n      property: string;\n      content: string;\n    }>;\n  };\n  rootInstanceId: string;\n  pathParamsDataSourceId?: string;\n  systemDataSourceId?: string;\n};\n\ntype Pages = {\n  homePage: Page;\n  pages: Page[];\n};\n\nexport default async () => {\n  const client = new PrismaClient({\n    datasources: {\n      db: {\n        url: process.env.DIRECT_URL ?? process.env.DATABASE_URL,\n      },\n    },\n    // Uncomment to see the queries in console as the migration runs\n    // log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  await client.$transaction(\n    async (prisma) => {\n      const chunkSize = 1000;\n      let cursor: undefined | string = undefined;\n      let hasNext = true;\n\n      while (hasNext) {\n        console.info(\"CHUNK\", cursor);\n        console.time(\"read\");\n\n        const cursorOptions: {} = cursor\n          ? {\n              skip: 1, // Skip the cursor\n              cursor: { id: cursor },\n            }\n          : {};\n\n        const builds = await prisma.build.findMany({\n          select: {\n            id: true,\n            pages: true,\n            dataSources: true,\n          },\n          take: chunkSize,\n          orderBy: {\n            id: \"asc\",\n          },\n          //where: { id: \"d55ce7ea-6853-40af-8a65-b63b79dde0f9\" },\n          ...cursorOptions,\n        });\n        console.timeEnd(\"read\");\n\n        console.time(\"parse-change\");\n        cursor = builds.at(-1)?.id;\n        hasNext = builds.length === chunkSize;\n        const changedBuilds: typeof builds = [];\n\n        for (const build of builds) {\n          const pages: Pages = JSON.parse(build.pages);\n          const dataSources: DataSource[] = JSON.parse(build.dataSources);\n          // Already migrated\n          if (pages.homePage.systemDataSourceId) {\n            continue;\n          }\n          const updatePage = (page: Page) => {\n            const dataSource: DataSource = {\n              id: nanoid(),\n              scopeInstanceId: page.rootInstanceId,\n              name: \"system\",\n              type: \"parameter\",\n            };\n            dataSources.push(dataSource);\n            page.systemDataSourceId = dataSource.id;\n          };\n          try {\n            updatePage(pages.homePage);\n            for (const page of pages.pages) {\n              updatePage(page);\n            }\n            build.pages = JSON.stringify(pages);\n            build.dataSources = JSON.stringify(dataSources);\n            changedBuilds.push(build);\n          } catch {\n            console.info(`build ${build.id} cannot be converted`);\n          }\n        }\n\n        if (changedBuilds.length === 0) {\n          return;\n        }\n\n        console.timeEnd(\"parse-change\");\n        console.info(\"changedBuilds.length\", changedBuilds.length);\n        console.time(\"update\");\n\n        const sql = `\n          UPDATE \"Build\"\n          SET\n            \"pages\" = data.\"pages\",\n            \"dataSources\" = data.\"dataSources\"\n          FROM unnest(ARRAY[$1], ARRAY[$2], ARRAY[$3]) as data(id, pages, \"dataSources\")\n          WHERE \"Build\".\"id\" = data.\"id\"\n        `;\n\n        const res = await prisma.$executeRawUnsafe(\n          sql,\n          changedBuilds.map((changedBuild) => changedBuild.id),\n          changedBuilds.map((changedBuild) => changedBuild.pages),\n          changedBuilds.map((changedBuild) => changedBuild.dataSources)\n        );\n\n        console.timeEnd(\"update\");\n        console.info(\"res\", res);\n      }\n    },\n    { timeout: 3600000 }\n  );\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240309212641_page_system_variable/schema.prisma",
    "content": "// DO NOT EDIT THIS FILE!\n// This is a copy of your schema.prisma that corresponds to the state of the database\n// when all migrations up until this one are applied.\n// It's used to generate a Prisma Client for the migration.\n\n// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\"]\n  // see commands.ts\n  output = \"client\"\n}\n\ndatasource db {\n  provider  = \"postgres\"\n  url       = env(\"DATABASE_URL\")\n  directUrl = env(\"DIRECT_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now())\n  updatedAt         DateTime     @default(now()) @updatedAt\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id               String             @default(uuid()) // not unique!\n  projectId        String\n  file             File               @relation(fields: [name], references: [name])\n  name             String\n  Project          Project[]\n  DashboardProject DashboardProject[]\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id               String             @id @default(uuid())\n  email            String?            @unique\n  provider         String?\n  image            String?\n  username         String?\n  createdAt        DateTime           @default(now())\n  team             Team?              @relation(fields: [teamId], references: [id])\n  teamId           String?\n  projects         Project[]\n  clientReferences ClientReferences[]\n  checkout         TransactionLog[]\n  products         UserProduct[]\n}\n\n// User client references in external services like stripe etc\nmodel ClientReferences {\n  reference String   @default(dbgenerated())\n  service   String\n  createdAt DateTime @default(now())\n  user      User     @relation(fields: [userId], references: [id])\n  userId    String\n\n  @@id([userId, service])\n  @@unique([reference, service])\n}\n\nmodel Product {\n  id          String   @id\n  name        String\n  description String?\n  features    String[]\n  images      String[]\n  meta        Json\n\n  createdAt    DateTime         @default(now())\n  checkout     TransactionLog[]\n  userProducts UserProduct[]\n}\n\nmodel TransactionLog {\n  // Stripe event id (debug and idempotency purposes)\n  eventId String  @id\n  userId  String?\n  user    User?   @relation(fields: [userId], references: [id])\n\n  productId String?\n  product   Product? @relation(fields: [productId], references: [id])\n\n  createdAt DateTime @default(now())\n\n  eventData Json? @default(dbgenerated())\n\n  eventType    String? @default(dbgenerated())\n  eventCreated Int?    @default(dbgenerated())\n\n  // paid\n  status String? @default(dbgenerated())\n\n  customerId    String? @default(dbgenerated())\n  customerEmail String? @default(dbgenerated())\n\n  subscriptionId String? @default(dbgenerated())\n\n  // Used for Refund\n  paymentIntent String? @default(dbgenerated())\n\n  @@unique([eventId, productId])\n}\n\nview UserProduct {\n  userId         String\n  user           User    @relation(fields: [userId], references: [id])\n  productId      String\n  product        Product @relation(fields: [productId], references: [id])\n  // subscriptionId and customerId not null for subscriptions\n  subscriptionId String?\n  customerId     String?\n  // Easier to debug\n  customerEmail  String?\n\n  @@unique([userId, productId])\n}\n\nmodel Project {\n  id                        String                    @id @default(uuid())\n  createdAt                 DateTime                  @default(now())\n  title                     String\n  domain                    String                    @unique\n  user                      User?                     @relation(fields: [userId], references: [id])\n  userId                    String?\n  build                     Build[]\n  isDeleted                 Boolean                   @default(false)\n  files                     File[]\n  projectDomain             ProjectDomain[]\n  latestBuild               LatestBuildPerProject?\n  authorizationToken        AuthorizationToken[]\n  previewImageAsset         Asset?                    @relation(fields: [previewImageAssetId, id], references: [id, projectId])\n  previewImageAssetId       String?\n  marketplaceApprovalStatus MarketplaceApprovalStatus @default(UNLISTED)\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nenum MarketplaceApprovalStatus {\n  UNLISTED\n  PENDING\n  APPROVED\n  REJECTED\n}\n\nmodel Build {\n  id                String   @unique @default(uuid())\n  version           Int      @default(0)\n  lastTransactionId String?\n  createdAt         DateTime @default(now())\n  updatedAt         DateTime @default(now()) @updatedAt\n  pages             String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  resources             String @default(\"[]\")\n  instances             String @default(\"[]\")\n  marketplaceProduct    String @default(\"{}\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  latestBuildPerProject LatestBuildPerProject?\n\n  @@id([id, projectId])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n  administrators\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  projectId String\n  project   Project               @relation(fields: [projectId], references: [id])\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now())\n\n  @@id([token, projectId])\n  @@unique([token])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now()) @updatedAt\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n\n  projectWithDomain ProjectWithDomain[]\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now())\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview ProjectWithDomain {\n  projectId String\n\n  domainId  String\n  domain    Domain @relation(fields: [domainId], references: [id])\n  txtRecord String\n\n  createdAt DateTime\n\n  // CNAME record to point to the domain\n  cname String\n\n  verified Boolean\n  // To count statistics per user\n  userId   String?\n\n  // We can deploy on per domain basis, here for each project domain we have latest build\n  latestBuid LatestBuildPerProjectDomain?\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProjectDomain {\n  domainId          String\n  buildId           String\n  projectId         String\n  projectWithDomain ProjectWithDomain @relation(fields: [projectId, domainId], references: [projectId, domainId])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domainId])\n}\n\nview LatestBuildPerProject {\n  buildId String\n  build   Build  @relation(fields: [buildId, projectId], references: [id, projectId])\n\n  projectId String\n  domain    String\n  project   Project @relation(fields: [projectId, domain], references: [id, domain])\n\n  isLatestBuild Boolean\n  publishStatus PublishStatus\n  updatedAt     DateTime\n\n  @@id([projectId, domain])\n  @@unique([buildId, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id                        String                    @id @default(uuid())\n  createdAt                 DateTime                  @default(now())\n  title                     String\n  domain                    String\n  userId                    String?\n  previewImageAsset         Asset?                    @relation(fields: [previewImageAssetId, id], references: [id, projectId])\n  previewImageAssetId       String?\n  isDeleted                 Boolean                   @default(false)\n  isPublished               Boolean\n  marketplaceApprovalStatus MarketplaceApprovalStatus @default(UNLISTED)\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240315173349_add-approved-marketplace-product/migration.sql",
    "content": "-- CreateIndex\nCREATE INDEX \"Build_projectId_createdAt_idx\" ON \"Build\"(\"projectId\", \"createdAt\" DESC);\nCOMMENT ON INDEX \"Build_projectId_createdAt_idx\" IS 'Used to speedup ApprovedMarketplaceProduct view';\n\n-- CreateIndex\nCREATE INDEX \"Project_isDeleted_marketplaceApprovalStatus_idx\" ON \"Project\"(\"isDeleted\", \"marketplaceApprovalStatus\");\nCOMMENT ON INDEX \"Project_isDeleted_marketplaceApprovalStatus_idx\" IS 'Used to speedup ApprovedMarketplaceProduct view';\n\nCREATE VIEW \"ApprovedMarketplaceProduct\" AS\nSELECT DISTINCT ON (build.\"projectId\")\n  build.\"projectId\",\n  build.\"marketplaceProduct\"\nFROM\n  \"Build\" build\nWHERE\n  build.deployment IS NOT NULL -- published\n  AND build.\"projectId\" IN (\n    SELECT\n      \"id\"\n    FROM\n      \"Project\"\n    WHERE (\"isDeleted\" = FALSE\n      AND \"marketplaceApprovalStatus\" = CAST('APPROVED'::text AS \"MarketplaceApprovalStatus\")))\nORDER BY\n  build.\"projectId\",\n  build.\"createdAt\" DESC,\n  build.id;\n\nCOMMENT ON VIEW \"ApprovedMarketplaceProduct\" IS '\nGet latest published build.marketplaceProduct\nfor a non deleted projects with marketplaceApprovalStatus=APPROVED\n';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240530162819_marketplace-token/migration.sql",
    "content": "-- This is an empty migration.\nDROP VIEW \"ApprovedMarketplaceProduct\";\n\nCREATE VIEW \"ApprovedMarketplaceProduct\" AS\nSELECT DISTINCT ON (build.\"projectId\")\n  build.\"projectId\",\n  build.\"marketplaceProduct\",\n  (\n    SELECT\n      token\n    FROM\n      \"AuthorizationToken\" auth\n    WHERE\n      auth.\"projectId\" = build.\"projectId\" AND\n      auth.relation = 'viewers'\n    ORDER BY\n      auth.\"token\"\n    LIMIT 1\n  ) AS \"authorizationToken\"\nFROM\n  \"Build\" build\nWHERE\n  build.deployment IS NOT NULL -- published\n  AND build.\"projectId\" IN (\n    SELECT\n      \"id\"\n    FROM\n      \"Project\"\n    WHERE (\"isDeleted\" = FALSE\n      AND \"marketplaceApprovalStatus\" = CAST('APPROVED'::text AS \"MarketplaceApprovalStatus\")))\nORDER BY\n  build.\"projectId\",\n  build.\"createdAt\" DESC,\n  build.id;"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240723144019_latest-build/migration.sql",
    "content": "CREATE OR REPLACE VIEW \"LatestBuildPerProject\" AS\nWITH lb AS (\n  SELECT DISTINCT ON (\"projectId\")\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n    AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n)\nSELECT DISTINCT ON (\"projectId\", \"domain\")\n  bld.id AS \"buildId\",\n  bld.\"projectId\",\n  deployment::jsonb ->> 'projectDomain' AS domain,\n  coalesce(bld.\"updatedAt\" = lb.\"updatedAt\", FALSE) AS \"isLatestBuild\",\n  bld.\"updatedAt\",\n  bld.\"publishStatus\"\nFROM\n  \"Build\" bld,\n  lb\nWHERE\n  bld.deployment IS NOT NULL\n  AND lb.\"projectId\" = bld.\"projectId\"\n  AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL\nORDER BY\n  bld.\"projectId\",\n  \"domain\",\n  bld.\"createdAt\" DESC,\n  \"buildId\";\n\n\nCREATE OR REPLACE VIEW \"LatestBuildPerProjectDomain\" AS\nWITH lbd AS (\n  SELECT DISTINCT ON (\"projectId\",\n    \"domain\")\n    jsonb_array_elements_text(deployment::jsonb -> 'domains') AS \"domain\",\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\",\n    bld.\"publishStatus\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    \"domain\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n),\nlb AS (\n  SELECT DISTINCT ON (\"projectId\")\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\",\n    bld.\"publishStatus\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n    AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n)\nSELECT\n  d.id AS \"domainId\",\n  lbd.\"projectId\",\n  lbd.\"buildId\",\n  coalesce(lbd.\"updatedAt\" = lb.\"updatedAt\", FALSE) AS \"isLatestBuild\",\n  lbd.\"publishStatus\",\n  lbd.\"updatedAt\"\nFROM\n  lbd,\n  lb,\n  \"Domain\" d\nWHERE\n  lbd.domain = d.domain\n  AND lb.\"projectId\" = lbd.\"projectId\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240723150501_latest-static-build/migration.sql",
    "content": "\nCREATE OR REPLACE VIEW \"LatestStaticBuildPerProject\" AS\nSELECT DISTINCT ON (\"projectId\")\n  bld.id AS \"buildId\",\n  bld.\"projectId\",\n  bld.\"updatedAt\",\n  bld.\"publishStatus\"\nFROM\n  \"Build\" bld\nWHERE\n  bld.deployment IS NOT NULL\n  AND bld.deployment::jsonb ->> 'destination'::text = 'static'\nORDER BY\n  bld.\"projectId\",\n  bld.\"createdAt\" DESC,\n  \"buildId\";\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240725003228_clone_project/migration.sql",
    "content": "DROP FUNCTION IF EXISTS clone_project;\nCREATE FUNCTION clone_project(\n  project_id text,\n  user_id text,\n  title text,\n  domain text\n) RETURNS \"Project\" AS $$\nDECLARE\n  old_project \"Project\";\n  new_project \"Project\";\nBEGIN\n  SELECT * FROM \"Project\" WHERE id=project_id INTO old_project;\n\n  INSERT INTO \"Project\" (\n    id,\n    \"userId\",\n    title,\n    domain,\n    \"previewImageAssetId\"\n  )\n  VALUES (\n    extensions.uuid_generate_v4(),\n    user_id,\n    title,\n    domain,\n    old_project.\"previewImageAssetId\"\n  )\n  RETURNING * INTO new_project;\n  \n  INSERT INTO \"Asset\" (id, name, \"projectId\")\n  SELECT asset.id, asset.name, new_project.id AS \"projectId\"\n  FROM \"Asset\" AS asset, \"File\" AS file\n  WHERE\n    asset.name = file.name AND\n    file.status = 'UPLOADED' AND\n    asset.\"projectId\" = old_project.id;\n\n  INSERT INTO \"Build\" (\n    id,\n    \"projectId\",\n    pages,\n    \"styleSources\",\n    \"styleSourceSelections\",\n    styles,\n    breakpoints,\n    props,\n    instances,\n    \"dataSources\",\n    resources\n  )\n  SELECT\n    extensions.uuid_generate_v4() AS id,\n    new_project.id AS \"projectId\",\n    pages,\n    \"styleSources\",\n    \"styleSourceSelections\",\n    styles,\n    breakpoints,\n    props,\n    instances,\n    \"dataSources\",\n    resources\n  FROM \"Build\"\n  WHERE \"projectId\" = old_project.id AND deployment IS NULL;\n\n  RETURN new_project;\nEND;\n$$ LANGUAGE plpgsql;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240730131207_clone_project_preview_imagea/migration.sql",
    "content": "DROP FUNCTION IF EXISTS clone_project;\nCREATE FUNCTION clone_project(\n  project_id text,\n  user_id text,\n  title text,\n  domain text\n) RETURNS \"Project\" AS $$\nDECLARE\n  old_project \"Project\";\n  new_project \"Project\";\nBEGIN\n  SELECT * FROM \"Project\" WHERE id=project_id INTO old_project;\n\n  INSERT INTO \"Project\" (\n    id,\n    \"userId\",\n    title,\n    domain\n  )\n  VALUES (\n    extensions.uuid_generate_v4(),\n    user_id,\n    title,\n    domain\n  )\n  RETURNING * INTO new_project;\n\n  INSERT INTO \"Asset\" (id, name, \"projectId\")\n  SELECT asset.id, asset.name, new_project.id AS \"projectId\"\n  FROM \"Asset\" AS asset, \"File\" AS file\n  WHERE\n    asset.name = file.name AND\n    file.status = 'UPLOADED' AND\n    asset.\"projectId\" = old_project.id;\n\n  -- set preview image asset after copying assets to the project\n  UPDATE \"Project\"\n  SET \"previewImageAssetId\"= old_project.\"previewImageAssetId\"\n  WHERE id=new_project.id;\n\n  INSERT INTO \"Build\" (\n    id,\n    \"projectId\",\n    pages,\n    \"styleSources\",\n    \"styleSourceSelections\",\n    styles,\n    breakpoints,\n    props,\n    instances,\n    \"dataSources\",\n    resources\n  )\n  SELECT\n    extensions.uuid_generate_v4() AS id,\n    new_project.id AS \"projectId\",\n    pages,\n    \"styleSources\",\n    \"styleSourceSelections\",\n    styles,\n    breakpoints,\n    props,\n    instances,\n    \"dataSources\",\n    resources\n  FROM \"Build\"\n  WHERE \"projectId\" = old_project.id AND deployment IS NULL;\n\n  RETURN new_project;\nEND;\n$$ LANGUAGE plpgsql;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240731170412_create_production_build/migration.sql",
    "content": "DROP FUNCTION IF EXISTS create_production_build;\nCREATE FUNCTION create_production_build(\n  project_id text,\n  deployment text\n) RETURNS text AS $$\nDECLARE\n  new_build_id text;\nBEGIN\n  INSERT INTO \"Build\" (\n    version,\n    \"lastTransactionId\",\n    pages,\n    breakpoints,\n    styles,\n    \"styleSources\",\n    \"styleSourceSelections\",\n    props,\n    \"dataSources\",\n    resources,\n    instances,\n    \"marketplaceProduct\",\n    \"publishStatus\",\n    \"projectId\",\n    id,\n    deployment\n  )\n  SELECT\n    version,\n    \"lastTransactionId\",\n    pages,\n    breakpoints,\n    styles,\n    \"styleSources\",\n    \"styleSourceSelections\",\n    props,\n    \"dataSources\",\n    resources,\n    instances,\n    \"marketplaceProduct\",\n    \"publishStatus\",\n    \"projectId\",\n    extensions.uuid_generate_v4() as id,\n    create_production_build.deployment as deployment\n  FROM \"Build\"\n  WHERE \"projectId\" = project_id AND \"Build\".\"deployment\" IS NULL\n  RETURNING \"id\" INTO new_build_id;\n\n  RETURN new_build_id;\nEND;\n$$ LANGUAGE plpgsql;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240807000548_deleted-project-free-domain/migration.sql",
    "content": "-- to avoid some issues with backups we are using explicit schemas for \"extensions\" functions\n\nUPDATE \"Project\" SET \"domain\"=extensions.uuid_generate_v4() WHERE \"isDeleted\" = true;"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240809220753_tz/migration.sql",
    "content": "DROP VIEW \"LatestStaticBuildPerProject\";\nDROP VIEW \"LatestBuildPerProject\";\n\nDROP VIEW \"LatestBuildPerProjectDomain\";\n\n-- AlterTable\nALTER TABLE \"Build\" ALTER COLUMN \"updatedAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\nCREATE OR REPLACE VIEW \"LatestStaticBuildPerProject\" AS\nSELECT DISTINCT ON (\"projectId\")\n  bld.id AS \"buildId\",\n  bld.\"projectId\",\n  bld.\"updatedAt\",\n  bld.\"publishStatus\"\nFROM\n  \"Build\" bld\nWHERE\n  bld.deployment IS NOT NULL\n  AND bld.deployment::jsonb ->> 'destination'::text = 'static'\nORDER BY\n  bld.\"projectId\",\n  bld.\"createdAt\" DESC,\n  \"buildId\";\n\nCREATE OR REPLACE VIEW \"LatestBuildPerProject\" AS\nWITH lb AS (\n  SELECT DISTINCT ON (\"projectId\")\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n    AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n)\nSELECT DISTINCT ON (\"projectId\", \"domain\")\n  bld.id AS \"buildId\",\n  bld.\"projectId\",\n  deployment::jsonb ->> 'projectDomain' AS domain,\n  coalesce(bld.\"updatedAt\" = lb.\"updatedAt\", FALSE) AS \"isLatestBuild\",\n  bld.\"updatedAt\",\n  bld.\"publishStatus\"\nFROM\n  \"Build\" bld,\n  lb\nWHERE\n  bld.deployment IS NOT NULL\n  AND lb.\"projectId\" = bld.\"projectId\"\n  AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL\nORDER BY\n  bld.\"projectId\",\n  \"domain\",\n  bld.\"createdAt\" DESC,\n  \"buildId\";\n\n\nCREATE OR REPLACE VIEW \"LatestBuildPerProjectDomain\" AS\nWITH lbd AS (\n  SELECT DISTINCT ON (\"projectId\",\n    \"domain\")\n    jsonb_array_elements_text(deployment::jsonb -> 'domains') AS \"domain\",\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\",\n    bld.\"publishStatus\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    \"domain\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n),\nlb AS (\n  SELECT DISTINCT ON (\"projectId\")\n    bld.id AS \"buildId\",\n    bld.\"projectId\",\n    bld.\"updatedAt\",\n    bld.\"publishStatus\"\n  FROM\n    \"Build\" bld\n  WHERE\n    bld.deployment IS NOT NULL\n    AND bld.deployment::jsonb ->> 'projectDomain'::text IS NOT NULL\n  ORDER BY\n    bld.\"projectId\",\n    bld.\"createdAt\" DESC,\n    \"buildId\"\n)\nSELECT\n  d.id AS \"domainId\",\n  lbd.\"projectId\",\n  lbd.\"buildId\",\n  coalesce(lbd.\"updatedAt\" = lb.\"updatedAt\", FALSE) AS \"isLatestBuild\",\n  lbd.\"publishStatus\",\n  lbd.\"updatedAt\"\nFROM\n  lbd,\n  lb,\n  \"Domain\" d\nWHERE\n  lbd.domain = d.domain\n  AND lb.\"projectId\" = lbd.\"projectId\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240916143551_time/migration.sql",
    "content": "DROP VIEW IF EXISTS \"LatestBuildPerProject\" CASCADE;\nDROP VIEW IF EXISTS \"LatestBuildPerProjectDomain\" CASCADE;\nDROP VIEW IF EXISTS  \"LatestStaticBuildPerProject\" CASCADE;\nDROP VIEW \"ApprovedMarketplaceProduct\";\nDROP VIEW IF EXISTS \"DashboardProject\";\nDROP VIEW IF EXISTS \"ProjectWithDomain\";\n\n-- AlterTable\nALTER TABLE \"AuthorizationToken\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"Build\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"ClientReferences\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"Domain\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3),\nALTER COLUMN \"updatedAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"File\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3),\nALTER COLUMN \"updatedAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"Product\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"Project\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"ProjectDomain\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"TransactionLog\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n-- AlterTable\nALTER TABLE \"User\" ALTER COLUMN \"createdAt\" SET DATA TYPE TIMESTAMPTZ(3);\n\n\nCREATE OR REPLACE VIEW \"LatestStaticBuildPerProject\" AS\nSELECT DISTINCT ON (\"projectId\")\n  bld.id AS \"buildId\",\n  bld.\"projectId\",\n  bld.\"updatedAt\",\n  bld.\"publishStatus\"\nFROM\n  \"Build\" bld\nWHERE\n  bld.deployment IS NOT NULL\n  AND bld.deployment::jsonb ->> 'destination'::text = 'static'\nORDER BY\n  bld.\"projectId\",\n  bld.\"createdAt\" DESC,\n  \"buildId\";\n\n\n\nCREATE VIEW \"ApprovedMarketplaceProduct\" AS\nSELECT DISTINCT ON (build.\"projectId\")\n  build.\"projectId\",\n  build.\"marketplaceProduct\",\n  (\n    SELECT\n      token\n    FROM\n      \"AuthorizationToken\" auth\n    WHERE\n      auth.\"projectId\" = build.\"projectId\" AND\n      auth.relation = 'viewers'\n    ORDER BY\n      auth.\"token\"\n    LIMIT 1\n  ) AS \"authorizationToken\"\nFROM\n  \"Build\" build\nWHERE\n  build.deployment IS NOT NULL -- published\n  AND build.\"projectId\" IN (\n    SELECT\n      \"id\"\n    FROM\n      \"Project\"\n    WHERE (\"isDeleted\" = FALSE\n      AND \"marketplaceApprovalStatus\" = CAST('APPROVED'::text AS \"MarketplaceApprovalStatus\")))\nORDER BY\n  build.\"projectId\",\n  build.\"createdAt\" DESC,\n  build.id;\n\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  *,\n  EXISTS (\n    SELECT\n      1\n    from\n      \"Build\"\n    WHERE\n      \"Build\".\"projectId\" = \"Project\".id\n      AND \"Build\".\"deployment\" IS NOT NULL\n  ) AS \"isPublished\"\nFROM\n  \"Project\";\n\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240917152817_pgtap/migration.sql",
    "content": "CREATE SCHEMA IF NOT EXISTS pgtap;\n\n-- Enable pg_tap\nCREATE EXTENSION IF NOT EXISTS pgtap WITH SCHEMA pgtap;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240918100751_latest-virtual-build/migration.sql",
    "content": "-- Drop the \"domainsVirtual\" table if it already exists, cascading to any dependent objects.\nDROP TABLE IF EXISTS \"domainsVirtual\" CASCADE;\n\n-- Create the \"domainsVirtual\" table to represent a virtual view of the relationship between domains and projects.\n-- This table enforces a 1-1 relationship between Project and Domain, used primarily for postgrest types and API purposes.\nCREATE TABLE \"domainsVirtual\" (\n    \"id\" text PRIMARY KEY NOT NULL, -- Primary key for the virtual table.\n    \"domainId\" text REFERENCES \"Domain\" (id) NOT NULL, -- Foreign key referencing the Domain table's ID.\n    \"projectId\" text REFERENCES \"Project\" (id) NOT NULL, -- Foreign key referencing the Project table's ID.\n    \"domain\" text NOT NULL, -- Domain name (must not be null).\n    \"status\" \"DomainStatus\" NOT NULL DEFAULT 'INITIALIZING'::\"DomainStatus\", -- Status of the domain, defaulting to 'INITIALIZING'.\n    \"error\" text, -- Optional error message, populated if status is 'ERROR'.\n    \"domainTxtRecord\" text, -- Current TXT record for the domain, retrieved from the Domain table.\n    \"expectedTxtRecord\" text NOT NULL, -- Expected TXT record, coming from the ProjectDomain table.\n    \"cname\" text NOT NULL, -- CNAME record for the domain, coming from the ProjectDomain table.\n    \"verified\" boolean NOT NULL DEFAULT false, -- Boolean flag indicating if the domain is verified (if TXT records match).\n    \"createdAt\" timestamp(3) with time zone NOT NULL, -- Creation timestamp from ProjectDomain.\n    \"updatedAt\" timestamp(3) with time zone NOT NULL -- Last update timestamp from Domain.\n);\n\n-- Add detailed comments for the table and each relevant column.\nCOMMENT ON TABLE \"domainsVirtual\" IS 'Virtual table representing domains related to each project. This table enforces a 1-1 relationship with the Project table and is used for API interaction (postgrest types).';\n\nCOMMENT ON COLUMN \"domainsVirtual\".\"domainId\" IS 'Unique identifier for the domain (Domain table reference).';\nCOMMENT ON COLUMN \"domainsVirtual\".\"projectId\" IS 'Unique identifier for the project, acting as the primary key (1-1 relationship with Project table).';\nCOMMENT ON COLUMN \"domainsVirtual\".\"domain\" IS 'The domain name (must not be NULL).';\nCOMMENT ON COLUMN \"domainsVirtual\".\"status\" IS 'Current status of the domain, with a default of INITIALIZING.';\nCOMMENT ON COLUMN \"domainsVirtual\".\"error\" IS 'Optional error message field populated if the domain status is ERROR.';\nCOMMENT ON COLUMN \"domainsVirtual\".\"domainTxtRecord\" IS 'Current TXT record for the domain, coming from the Domain table.';\nCOMMENT ON COLUMN \"domainsVirtual\".\"expectedTxtRecord\" IS 'Expected TXT record for the domain, pulled from the ProjectDomain table.';\nCOMMENT ON COLUMN \"domainsVirtual\".\"verified\" IS 'Boolean flag indicating whether the domain is verified (true if TXT records match, false otherwise).';\nCOMMENT ON COLUMN \"domainsVirtual\".\"createdAt\" IS 'Timestamp indicating when the ProjectDomain entry was created.';\nCOMMENT ON COLUMN \"domainsVirtual\".\"updatedAt\" IS 'Timestamp indicating when the Domain was last updated.';\n\n-- Create the \"domainsVirtual\" function to return all domain-related data for a specific project.\nCREATE OR REPLACE FUNCTION \"domainsVirtual\"(\"Project\")\nRETURNS SETOF \"domainsVirtual\" AS $$\n    -- This function retrieves all the domain information associated with a specific project by joining the Domain and ProjectDomain tables.\n    -- It returns a result set conforming to the \"domainsVirtual\" structure, including fields such as domain status, error, verification status, etc.\n    SELECT\n        \"Domain\".id || '-' || \"ProjectDomain\".\"projectId\" as id,\n        \"Domain\".id AS \"domainId\", -- Domain ID from Domain table\n        \"ProjectDomain\".\"projectId\", -- Project ID from ProjectDomain table\n        \"Domain\".domain, -- Domain name\n        \"Domain\".status, -- Current domain status\n        \"Domain\".error, -- Error message, if any\n        \"Domain\".\"txtRecord\" AS \"domainTxtRecord\", -- Current TXT record from Domain table\n        \"ProjectDomain\".\"txtRecord\" AS \"expectedTxtRecord\", -- Expected TXT record from ProjectDomain table\n        \"ProjectDomain\".\"cname\" AS \"cname\",\n        CASE\n            WHEN \"Domain\".\"txtRecord\" = \"ProjectDomain\".\"txtRecord\" THEN true -- If TXT records match, domain is verified\n            ELSE false\n        END AS \"verified\", -- Boolean flag for verification status\n        \"ProjectDomain\".\"createdAt\", -- Creation timestamp from ProjectDomain table\n        \"Domain\".\"updatedAt\" -- Last updated timestamp from Domain table\n    FROM\n        \"Domain\"\n    JOIN\n        \"ProjectDomain\" ON \"Domain\".id = \"ProjectDomain\".\"domainId\" -- Joining Domain and ProjectDomain on domainId\n    WHERE\n        \"ProjectDomain\".\"projectId\" = $1.id; -- Filtering by projectId passed as an argument to the function\n$$\nSTABLE\nLANGUAGE sql;\n\n-- Add function-specific comments to explain its behavior.\nCOMMENT ON FUNCTION \"domainsVirtual\"(\"Project\") IS 'Function that retrieves domain-related data for a given project by joining the Domain and ProjectDomain tables. It returns a result set that conforms to the structure defined in the domainsVirtual virtual table.';\n\n\n\nDROP TABLE IF EXISTS \"latestBuildVirtual\" CASCADE;\n\n-- In the postgrest-js type system, it appears that Project has a 1-1 relationship with latestBuild\nCREATE TABLE \"latestBuildVirtual\" (\n    \"buildId\" text REFERENCES \"Build\" (id) unique NOT NULL,\n    \"projectId\" text PRIMARY KEY REFERENCES \"Project\" (id) NOT NULL, -- PRIMARY KEY indicates a 1-1 relationship https://docs.postgrest.org/en/v12/references/api/resource_embedding.html#one-to-one-relationships\n    \"domainsVirtualId\" text  REFERENCES \"domainsVirtual\" (\"id\") unique NOT NULL, -- UNIQUE KEY indicates a 1-1 relationship https://docs.postgrest.org/en/v12/references/api/resource_embedding.html#one-to-one-relationships\n    domain text NOT NULL,\n    \"createdAt\" timestamp(3) with time zone NOT NULL,\n    \"publishStatus\" \"PublishStatus\" NOT NULL\n);\n\n-- Adding comments for the table and specific column\nCOMMENT ON TABLE \"latestBuildVirtual\" IS 'Virtual table representing the latest build for each project, enforcing a 1-1 relationship with the Project table. Used ONLY for postgrest types';\n\nCOMMENT ON COLUMN \"latestBuildVirtual\".\"projectId\" IS 'Identifier for the project, enforcing a 1-1 relationship with the Project table as a primary key';\n\n\n-- PostgREST will use this function as a computed field\n-- See: https://docs.postgrest.org/en/v12/references/api/resource_embedding.html#computed-relationships\nCREATE OR REPLACE FUNCTION \"latestBuildVirtual\"(\"Project\")\nRETURNS SETOF \"latestBuildVirtual\"\nROWS 1 AS $$ -- The function is expected to return 1 row\n\n-- This function selects the latest build for a given project where:\n-- 1. The \"deployment\" field is not NULL, ensuring it is a production build.\n-- 2. The 'destination' field in the JSONB \"deployment\" is either NULL (for backward compatibility)\n--    or equal to 'saas', indicating a non-static build.\n-- 3. The selected \"domain\" must exist in the \"Domain\" table (many-to-many relation with \"Project\" via \"ProjectDomain\")\n--    or it must match the \"Project.domain\" field directly.\n-- 4. If 'projectDomain' exists in the JSONB \"deployment\", it is used as the \"domain\".\n--    If not, the first element of 'domains' in the JSONB \"deployment\" array is used as the \"domain\".\n-- The function returns the most recent (by \"createdAt\") valid build.\n\nSELECT\n    b.id AS \"buildId\",\n    b.\"projectId\",\n    '' as \"domainsVirtualId\",\n    -- Use CASE to determine which domain to select based on conditions\n    CASE\n        WHEN (b.deployment::jsonb ->> 'projectDomain') = p.domain\n             OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[p.domain])\n        THEN p.domain\n        ELSE d.domain\n    END AS \"domain\",\n    b.\"createdAt\",\n    b.\"publishStatus\"\nFROM \"Build\" b\nJOIN \"Project\" p ON b.\"projectId\" = p.id\nLEFT JOIN \"ProjectDomain\" pd ON pd.\"projectId\" = p.id\nLEFT JOIN \"Domain\" d ON d.id = pd.\"domainId\"\nWHERE b.\"projectId\" = $1.id\n  AND b.deployment IS NOT NULL\n  -- 'destination' IS NULL for backward compatibility; 'destination' = 'saas' for non-static builds\n  AND ((b.deployment::jsonb ->> 'destination') IS NULL OR (b.deployment::jsonb ->> 'destination') = 'saas')\n  AND (\n      -- Check if 'projectDomain' matches p.domain\n      (b.deployment::jsonb ->> 'projectDomain') = p.domain\n      -- Check if 'domains' contains p.domain or d.domain\n      OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[p.domain])\n      OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[d.domain])\n  )\nORDER BY b.\"createdAt\" DESC\nLIMIT 1;\n\n$$\nSTABLE\nLANGUAGE sql;\n\n-- Comment on the function to provide additional context\nCOMMENT ON FUNCTION \"latestBuildVirtual\"(\"Project\") IS 'This function computes the latest build for a project, ensuring it is a production (non-static) build, where the domain matches either the Project.domain field or exists in the related Domain table. It provides backward compatibility for older records with a missing \"destination\" field.';\n\n-- Example:\n-- select p.id, p.domain, lbv.* from \"Project\" p\n-- LEFT JOIN LATERAL (\n--    SELECT * FROM \"latestBuildVirtual\"(p)\n-- ) lbv ON TRUE;\n\n\n\n\n\n\n-- This function defines a computed field \"latestBuildVirtual\" for PostgREST\n-- It returns the latest build associated with a given Project and Domain.\n-- Reference: https://docs.postgrest.org/en/v12/references/api/resource_embedding.html#computed-relationships\n\nCREATE OR REPLACE FUNCTION \"latestBuildVirtual\"(\"domainsVirtual\")\nRETURNS SETOF \"latestBuildVirtual\"\nROWS 1 AS $$  -- The function is expected to return at most 1 row since it fetches the latest build\n\nSELECT\n    b.id AS \"buildId\",         -- ID of the Build\n    b.\"projectId\",             -- ID of the Project to which the build belongs\n    '' as \"domainsVirtualId\",  -- Placeholder for the domainsVirtual ID (not used in this context)\n    d.\"domain\",                -- Domain associated with the build\n    b.\"createdAt\",             -- Timestamp of when the build was created\n    b.\"publishStatus\"          -- Status of the build (e.g., published, draft, etc.)\nFROM \"Build\" b\nJOIN \"Domain\" d ON d.id = $1.\"domainId\"  -- Join the \"Build\" and \"Domain\" tables using the domain ID\nWHERE\n    b.\"projectId\" = $1.\"projectId\"  -- Ensure the Build belongs to the specified project\n    AND b.deployment IS NOT NULL    -- Ensure the Build has a non-null deployment field\n    AND (b.deployment::jsonb -> 'domains') @> to_jsonb(array[d.domain])  -- Check if the domain exists in the deployment JSON array\nORDER BY b.\"createdAt\" DESC  -- Order builds by creation date in descending order to get the latest one\nLIMIT 1;  -- Limit the result to the most recent build\n\n$$\nSTABLE  -- Declares that the function always returns the same result for the same input parameters\nLANGUAGE sql;\n\n-- Adding a comment to provide more context about the function's purpose\nCOMMENT ON FUNCTION \"latestBuildVirtual\"(\"domainsVirtual\") IS 'Returns the latest build for a given project and domain as a computed field for PostgREST.';\n\n\n\nDROP VIEW IF EXISTS \"LatestBuildPerProject\" CASCADE;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240920091253_domain-ordering/migration.sql",
    "content": "-- Create the \"domainsVirtual\" function to return all domain-related data for a specific project.\nCREATE OR REPLACE FUNCTION \"domainsVirtual\"(\"Project\")\nRETURNS SETOF \"domainsVirtual\" AS $$\n    -- This function retrieves all the domain information associated with a specific project by joining the Domain and ProjectDomain tables.\n    -- It returns a result set conforming to the \"domainsVirtual\" structure, including fields such as domain status, error, verification status, etc.\n    SELECT\n        \"Domain\".id || '-' || \"ProjectDomain\".\"projectId\" as id,\n        \"Domain\".id AS \"domainId\", -- Domain ID from Domain table\n        \"ProjectDomain\".\"projectId\", -- Project ID from ProjectDomain table\n        \"Domain\".domain, -- Domain name\n        \"Domain\".status, -- Current domain status\n        \"Domain\".error, -- Error message, if any\n        \"Domain\".\"txtRecord\" AS \"domainTxtRecord\", -- Current TXT record from Domain table\n        \"ProjectDomain\".\"txtRecord\" AS \"expectedTxtRecord\", -- Expected TXT record from ProjectDomain table\n        \"ProjectDomain\".\"cname\" AS \"cname\",\n        CASE\n            WHEN \"Domain\".\"txtRecord\" = \"ProjectDomain\".\"txtRecord\" THEN true -- If TXT records match, domain is verified\n            ELSE false\n        END AS \"verified\", -- Boolean flag for verification status\n        \"ProjectDomain\".\"createdAt\", -- Creation timestamp from ProjectDomain table\n        \"Domain\".\"updatedAt\" -- Last updated timestamp from Domain table\n    FROM\n        \"Domain\"\n    JOIN\n        \"ProjectDomain\" ON \"Domain\".id = \"ProjectDomain\".\"domainId\" -- Joining Domain and ProjectDomain on domainId\n    WHERE\n        \"ProjectDomain\".\"projectId\" = $1.id -- Filtering by projectId passed as an argument to the function\n    ORDER BY \"ProjectDomain\".\"createdAt\", \"Domain\".id; -- Stable sort\n$$\nSTABLE\nLANGUAGE sql;\n\n-- Add function-specific comments to explain its behavior.\nCOMMENT ON FUNCTION \"domainsVirtual\"(\"Project\") IS 'Function that retrieves domain-related data for a given project by joining the Domain and ProjectDomain tables. It returns a result set that conforms to the structure defined in the domainsVirtual virtual table.';\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20240924174536_cleanup/migration.sql",
    "content": "ALTER TABLE \"Build\" ADD COLUMN \"isCleaned\" BOOLEAN DEFAULT FALSE;\n\n-- PostgREST will use this function as a computed field\n-- See: https://docs.postgrest.org/en/v12/references/api/resource_embedding.html#computed-relationships\nCREATE OR REPLACE FUNCTION \"latestProjectDomainBuildVirtual\"(\"Project\")\nRETURNS SETOF \"latestBuildVirtual\"\nROWS 1 AS $$ -- The function is expected to return 1 row\n\n-- This function selects the latest build for a given project domain where:\n-- 1. The \"deployment\" field is not NULL, ensuring it is a production build.\n-- 2. The 'destination' field in the JSONB \"deployment\" is either NULL (for backward compatibility)\n--    or equal to 'saas', indicating a non-static build.\n-- 3. The selected \"domain\" must exist in the \"Domain\" table (many-to-many relation with \"Project\" via \"ProjectDomain\")\n--    or it must match the \"Project.domain\" field directly.\n-- 4. If 'projectDomain' exists in the JSONB \"deployment\", it is used as the \"domain\".\n--    If not, the first element of 'domains' in the JSONB \"deployment\" array is used as the \"domain\".\n-- The function returns the most recent (by \"createdAt\") valid build.\nSELECT\n    b.id AS \"buildId\",\n    b.\"projectId\",\n    '' as \"domainsVirtualId\",\n    p.domain AS \"domain\",\n    b.\"createdAt\",\n    b.\"publishStatus\"\nFROM \"Build\" b\nJOIN \"Project\" p ON b.\"projectId\" = p.id\nLEFT JOIN \"ProjectDomain\" pd ON pd.\"projectId\" = p.id\nWHERE b.\"projectId\" = $1.id\n  AND b.deployment IS NOT NULL\n  -- 'destination' IS NULL for backward compatibility; 'destination' = 'saas' for non-static builds\n  AND ((b.deployment::jsonb ->> 'destination') IS NULL OR (b.deployment::jsonb ->> 'destination') = 'saas')\n  AND (\n      -- Check if 'projectDomain' matches p.domain\n      (b.deployment::jsonb ->> 'projectDomain') = p.domain\n      -- Check if 'domains' contains p.domain or d.domain\n      OR (b.deployment::jsonb -> 'domains') @> to_jsonb(array[p.domain])\n  )\nORDER BY b.\"createdAt\" DESC\nLIMIT 1;\n$$\nSTABLE\nLANGUAGE sql;\n\n-- Comment on the function to provide additional context\nCOMMENT ON FUNCTION \"latestProjectDomainBuildVirtual\"(\"Project\") IS 'This function computes the latest build for a project domain, ensuring it is a production (non-static) build, where the domain matches either the Project.domain field or exists in the related Domain table. It provides backward compatibility for older records with a missing \"destination\" field.';\n\n\n\nCREATE OR REPLACE FUNCTION database_cleanup(\n  from_date timestamp DEFAULT '2020-01-01 00:00:00',\n  to_date timestamp DEFAULT '2099-12-31 23:59:59' -- SQL should die long before this date!\n) RETURNS VOID AS $$\nBEGIN\n  WITH latest_builds AS (\n    SELECT \"buildId\" FROM \"Project\" p, LATERAL \"latestProjectDomainBuildVirtual\"(p)\n    UNION\n    SELECT \"buildId\" FROM \"Project\" p, LATERAL \"latestBuildVirtual\"(p)\n    UNION\n    SELECT lb.\"buildId\"\n    FROM \"Project\" p, LATERAL \"domainsVirtual\"(p) dv, LATERAL \"latestBuildVirtual\"(dv) lb\n  )\n  UPDATE \"Build\"\n  SET\n    \"styleSources\" = '[]'::text,\n    styles = '[]'::text,\n    breakpoints = '[]'::text,\n    \"styleSourceSelections\" = '[]'::text,\n    props = '[]'::text,\n    instances = '[]'::text,\n    \"dataSources\" = '[]'::text,\n    resources = '[]'::text,\n    \"marketplaceProduct\" = '{}'::text,\n    \"isCleaned\" = TRUE\n  WHERE deployment IS NOT NULL\n  AND id NOT IN (SELECT \"buildId\" FROM latest_builds)\n  AND \"isCleaned\" = FALSE\n  AND \"createdAt\" BETWEEN from_date AND to_date;  -- Filter by date range (for testing purposes)\nEND;\n$$ LANGUAGE plpgsql;\n\n-- SELECT database_cleanup();\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20241207052014_can-publish/migration.sql",
    "content": "\nALTER TABLE \"AuthorizationToken\" ADD COLUMN \"canPublish\" boolean NOT NULL DEFAULT false;\n\nUPDATE \"AuthorizationToken\"\nSET \"canPublish\" =\nCASE\n    WHEN \"relation\" IN ('viewers', 'builders') THEN FALSE\n    ELSE TRUE\nEND;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20250322141808_user_publish_count/migration.sql",
    "content": "CREATE OR REPLACE VIEW user_publish_count AS\nSELECT DISTINCT \"userId\" AS user_id, count(*)\nFROM \"Build\"\nLEFT JOIN \"Project\" ON \"Project\".id=\"Build\".\"projectId\"\nWHERE DATE_TRUNC('day', \"Build\".\"createdAt\") = DATE_TRUNC('day', NOW())\nAND \"Build\".deployment IS NOT NULL\nGROUP BY \"userId\";\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20250710163439_restore_development_build/migration.sql",
    "content": "DROP FUNCTION IF EXISTS restore_development_build;\nCREATE FUNCTION restore_development_build(\n  project_id  text,\n  from_build_id text\n) RETURNS text AS $$\nBEGIN\n  UPDATE \"Build\"\n  SET\n    \"version\" = source.\"version\",\n    \"lastTransactionId\" = source.\"lastTransactionId\",\n    \"pages\" = source.\"pages\",\n    \"breakpoints\" = source.\"breakpoints\",\n    \"styles\" = source.\"styles\",\n    \"styleSources\" = source.\"styleSources\",\n    \"styleSourceSelections\" = source.\"styleSourceSelections\",\n    \"props\" = source.\"props\",\n    \"dataSources\" = source.\"dataSources\",\n    \"resources\" = source.\"resources\",\n    \"instances\" = source.\"instances\",\n    \"marketplaceProduct\" = source.\"marketplaceProduct\"\n  FROM (\n    SELECT * FROM \"Build\"\n    WHERE \"projectId\" = project_id AND \"id\" = from_build_id\n  ) as source\n  WHERE \"Build\".\"projectId\" = project_id AND \"Build\".\"deployment\" IS NULL;\n  RETURN 'OK';\nEND;\n$$ LANGUAGE plpgsql;\n\nDROP VIEW IF EXISTS published_builds;\nCREATE VIEW published_builds AS\nSELECT\n  build.id AS \"buildId\",\n  build.\"projectId\",\n  build.\"createdAt\",\n  (\n    SELECT string_agg(list.value, ', ')\n    FROM jsonb_array_elements_text(build.deployment::jsonb -> 'domains') AS list(value)\n  ) AS domains\nFROM \"Build\" as build\nWHERE build.deployment IS NOT NULL\n  AND build.\"isCleaned\"=FALSE\nORDER BY build.\"projectId\", build.\"createdAt\" DESC;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20250810123401_asset_filename_description/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"Asset\"\n  ADD COLUMN \"description\" TEXT,\n  ADD COLUMN \"filename\" TEXT;\n"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20250913204036_project_tags/migration.sql",
    "content": "ALTER TABLE\n  \"Project\"\nADD\n  COLUMN IF NOT EXISTS \"tags\" TEXT [];\n\nALTER TABLE\n  \"User\"\nADD\n  COLUMN IF NOT EXISTS \"projectsTags\" JSONB NOT NULL DEFAULT '[]';\n\nDROP VIEW IF EXISTS \"DashboardProject\";\n\nCREATE VIEW \"DashboardProject\" AS\nSELECT\n  id,\n  title,\n  tags,\n  domain,\n  \"userId\",\n  \"isDeleted\",\n  \"createdAt\",\n  \"previewImageAssetId\",\n  \"marketplaceApprovalStatus\",\n  (\n    EXISTS (\n      SELECT\n        1\n      FROM\n        \"Build\"\n      WHERE\n        \"Build\".\"projectId\" = \"Project\".id\n        AND \"Build\".deployment IS NOT NULL\n    )\n  ) AS \"isPublished\"\nFROM\n  \"Project\";"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/20251129093846_add_updated_at_to_latest_build_virtual/migration.sql",
    "content": "-- Add updatedAt field to latestBuildVirtual virtual table (type definition)\n-- and update both functions to include Build's updatedAt timestamp\n\n-- First, drop existing functions that depend on the old table structure\nDROP FUNCTION IF EXISTS \"latestBuildVirtual\"(\"Project\");\nDROP FUNCTION IF EXISTS \"latestBuildVirtual\"(\"domainsVirtual\");\nDROP FUNCTION IF EXISTS \"latestProjectDomainBuildVirtual\"(\"Project\");\nDROP FUNCTION IF EXISTS \"latestBuildVirtual\"(\"DashboardProject\");\n\n-- Add updatedAt column to the virtual table type definition\nALTER TABLE \"latestBuildVirtual\"\n  ADD COLUMN \"updatedAt\" timestamp(3) with time zone NOT NULL DEFAULT NOW();\n\nCOMMENT ON COLUMN \"public\".\"latestBuildVirtual\".\"updatedAt\" IS 'Timestamp indicating when the Build was last updated';\n\n-- Recreate the function for Project with updatedAt field\nCREATE\nOR REPLACE FUNCTION \"latestBuildVirtual\"(\"Project\") RETURNS SETOF \"latestBuildVirtual\" ROWS 1 AS $$\nSELECT\n  b.id AS \"buildId\",\n  b.\"projectId\",\n  '' as \"domainsVirtualId\",\n  -- Use CASE to determine which domain to select based on conditions\n  CASE\n    WHEN (b.deployment :: jsonb ->> 'projectDomain') = p.domain\n    OR (b.deployment :: jsonb -> 'domains') @> to_jsonb(array [p.domain]) THEN p.domain\n    ELSE d.domain\n  END AS \"domain\",\n  b.\"createdAt\",\n  b.\"publishStatus\",\n  b.\"updatedAt\"\nFROM\n  \"Build\" b\n  JOIN \"Project\" p ON b.\"projectId\" = p.id\n  LEFT JOIN \"ProjectDomain\" pd ON pd.\"projectId\" = p.id\n  LEFT JOIN \"Domain\" d ON d.id = pd.\"domainId\"\nWHERE\n  b.\"projectId\" = $1.id\n  AND b.deployment IS NOT NULL -- 'destination' IS NULL for backward compatibility; 'destination' = 'saas' for non-static builds\n  AND (\n    (b.deployment :: jsonb ->> 'destination') IS NULL\n    OR (b.deployment :: jsonb ->> 'destination') = 'saas'\n  )\n  AND (\n    -- Check if 'projectDomain' matches p.domain\n    (b.deployment :: jsonb ->> 'projectDomain') = p.domain -- Check if 'domains' contains p.domain or d.domain\n    OR (b.deployment :: jsonb -> 'domains') @> to_jsonb(array [p.domain])\n    OR (b.deployment :: jsonb -> 'domains') @> to_jsonb(array [d.domain])\n  )\nORDER BY\n  b.\"createdAt\" DESC\nLIMIT\n  1;\n\n$$ STABLE LANGUAGE sql;\n\n-- Comment on the function\nCOMMENT ON FUNCTION \"latestBuildVirtual\"(\"Project\") IS 'This function computes the latest build for a project, ensuring it is a production (non-static) build, where the domain matches either the Project.domain field or exists in the related Domain table. It provides backward compatibility for older records with a missing \"destination\" field.';\n\n-- Recreate the function for domainsVirtual with updatedAt field\nCREATE\nOR REPLACE FUNCTION \"latestBuildVirtual\"(\"domainsVirtual\") RETURNS SETOF \"latestBuildVirtual\" ROWS 1 AS $$\nSELECT\n  b.id AS \"buildId\",\n  b.\"projectId\",\n  '' as \"domainsVirtualId\",\n  d.\"domain\",\n  b.\"createdAt\",\n  b.\"publishStatus\",\n  b.\"updatedAt\"\nFROM\n  \"Build\" b\n  JOIN \"Domain\" d ON d.id = $1.\"domainId\"\nWHERE\n  b.\"projectId\" = $1.\"projectId\"\n  AND b.deployment IS NOT NULL\n  AND (b.deployment :: jsonb -> 'domains') @> to_jsonb(array [d.domain])\nORDER BY\n  b.\"createdAt\" DESC\nLIMIT\n  1;\n\n$$ STABLE LANGUAGE sql;\n\n-- Adding a comment to provide more context\nCOMMENT ON FUNCTION \"latestBuildVirtual\"(\"domainsVirtual\") IS 'Returns the latest build for a given project and domain as a computed field for PostgREST.';\n\n-- Update latestProjectDomainBuildVirtual function to include updatedAt\nCREATE\nOR REPLACE FUNCTION \"latestProjectDomainBuildVirtual\"(\"Project\") RETURNS SETOF \"latestBuildVirtual\" ROWS 1 AS $$\nSELECT\n  b.id AS \"buildId\",\n  b.\"projectId\",\n  '' as \"domainsVirtualId\",\n  p.domain AS \"domain\",\n  b.\"createdAt\",\n  b.\"publishStatus\",\n  b.\"updatedAt\"\nFROM\n  \"Build\" b\n  JOIN \"Project\" p ON b.\"projectId\" = p.id\n  LEFT JOIN \"ProjectDomain\" pd ON pd.\"projectId\" = p.id\nWHERE\n  b.\"projectId\" = $1.id\n  AND b.deployment IS NOT NULL\n  AND (\n    (b.deployment :: jsonb ->> 'destination') IS NULL\n    OR (b.deployment :: jsonb ->> 'destination') = 'saas'\n  )\n  AND (\n    (b.deployment :: jsonb ->> 'projectDomain') = p.domain\n    OR (b.deployment :: jsonb -> 'domains') @> to_jsonb(array [p.domain])\n  )\nORDER BY\n  b.\"createdAt\" DESC\nLIMIT\n  1;\n\n$$ STABLE LANGUAGE sql;\n\nCOMMENT ON FUNCTION \"latestProjectDomainBuildVirtual\"(\"Project\") IS 'This function computes the latest build for a project domain, ensuring it is a production (non-static) build, where the domain matches either the Project.domain field or exists in the related Domain table. It provides backward compatibility for older records with a missing \"destination\" field.';\n\n-- Add latestBuildVirtual function overload for DashboardProject view\n-- This is needed because PostgREST computed fields require a function\n-- that matches the source table/view type. DashboardProject is a view\n-- over Project, so we need this wrapper function that casts to Project type.\nCREATE\nOR REPLACE FUNCTION \"latestBuildVirtual\"(\"DashboardProject\") RETURNS SETOF \"latestBuildVirtual\" ROWS 1 AS $$\nSELECT\n  *\nFROM\n  \"latestBuildVirtual\"(ROW($1.id, $1.title, $1.domain, $1.\"userId\", $1.\"isDeleted\", $1.\"createdAt\", $1.\"previewImageAssetId\", $1.\"marketplaceApprovalStatus\", $1.tags)::\"Project\");\n\n$$ STABLE LANGUAGE sql;\n\nCOMMENT ON FUNCTION \"latestBuildVirtual\"(\"DashboardProject\") IS 'Wrapper function to make latestBuildVirtual work with DashboardProject view for PostgREST computed fields.';\n\n-- Grant execute permissions to all PostgREST roles\n-- Uses DO block to check if roles exist before granting (prevents errors if roles are missing)\nDO $$\nDECLARE\n  role_name TEXT;\nBEGIN\n  FOREACH role_name IN ARRAY ARRAY['anon', 'authenticated', 'service_role']\n  LOOP\n    IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = role_name) THEN\n      EXECUTE format('GRANT EXECUTE ON FUNCTION \"latestBuildVirtual\"(\"DashboardProject\") TO %I', role_name);\n    END IF;\n  END LOOP;\nEND $$;"
  },
  {
    "path": "packages/prisma-client/prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (i.e. Git)\nprovider = \"postgresql\""
  },
  {
    "path": "packages/prisma-client/prisma/migrations/template.txt",
    "content": "import { PrismaClient } from \"./client\";\n\n// NOTE ON IMPORTS:\n//\n//   We want to be able to run old migrations at any point.\n//   For example, when we setting up a fresh database or making a reset.\n//\n//   You shouldn't import code that may change later\n//   and become incompatible with the migration.\n//   It's better to copy it to the migration directory.\n\nexport default () => {\n  if (process.env.DIRECT_URL === undefined) {\n    throw new Error(\"env variable DIRECT_URL is not defined\");\n  }\n\n  const client = new PrismaClient({\n    datasources: {\n      db: {\n        url: process.env.DIRECT_URL,\n      },\n    },\n\n    transactionOptions: {\n      timeout: 10 * 60 * 1000,\n      maxWait: 5000,\n    },\n\n    // Uncomment to see the queries in console as the migration runs\n    log: [\"query\", \"info\", \"warn\", \"error\"],\n  });\n\n  return client.$transaction(async (prisma) => {\n    // This is the body of your migration.\n    // Use the `prisma` client to perform database operations.\n    //\n    // Example:\n    //\n    // await prisma.user.updateMany({\n    //   where: { username: null },\n    //   data: { username: \"Anonymous\" },\n    // });\n    //\n    // If data models' API is not enough, consider using a raw query:\n    //\n    // await prisma.$executeRaw`UPDATE \"User\" SET \"username\" = \"email\" WHERE \"username\" IS NULL`;\n    //\n    //\n    // NOTE:\n    //   Don't do any schema modifications here like adding a column to a table!\n    //   If you need to change schema, edit schema.prisma and create a schema migration.\n  }, { timeout: 1000 * 60 });\n};\n"
  },
  {
    "path": "packages/prisma-client/prisma/schema.prisma",
    "content": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"views\"]\n  // see commands.ts\n  // <output-placeholder-for-migrations>\n  output          = \"../src/__generated__\"\n  // </output-placeholder-for-migrations>\n\n  binaryTargets = env(\"PRISMA_BINARY_TARGET\")\n}\n\ndatasource db {\n  provider  = \"postgres\"\n  url       = env(\"DATABASE_URL\")\n  directUrl = env(\"DIRECT_URL\")\n}\n\nmodel Team {\n  id    String @id @default(uuid())\n  users User[]\n}\n\nenum UploadStatus {\n  UPLOADING\n  UPLOADED\n}\n\nmodel File {\n  name              String       @id\n  format            String\n  size              Int\n  description       String?\n  createdAt         DateTime     @default(now()) @db.Timestamptz(3)\n  updatedAt         DateTime     @default(now()) @updatedAt @db.Timestamptz(3)\n  meta              String       @default(\"{}\")\n  status            UploadStatus @default(UPLOADING)\n  isDeleted         Boolean      @default(false)\n  uploaderProject   Project?     @relation(fields: [uploaderProjectId], references: [id])\n  uploaderProjectId String?\n  assets            Asset[]\n}\n\nmodel Asset {\n  id               String             @default(uuid()) // not unique!\n  projectId        String\n  file             File               @relation(fields: [name], references: [name])\n  name             String\n  filename         String?\n  description      String?\n  Project          Project[]\n  DashboardProject DashboardProject[]\n\n  @@id([id, projectId])\n}\n\nmodel User {\n  id               String             @id @default(uuid())\n  email            String?            @unique\n  provider         String?\n  image            String?\n  username         String?\n  createdAt        DateTime           @default(now()) @db.Timestamptz(3)\n  team             Team?              @relation(fields: [teamId], references: [id])\n  teamId           String?\n  projects         Project[]\n  projectsTags     Json               @default(\"[]\")\n  clientReferences ClientReferences[]\n  checkout         TransactionLog[]\n  products         UserProduct[]\n}\n\n// User client references in external services like stripe etc\nmodel ClientReferences {\n  reference String   @default(dbgenerated())\n  service   String\n  createdAt DateTime @default(now()) @db.Timestamptz(3)\n  user      User     @relation(fields: [userId], references: [id])\n  userId    String\n\n  @@id([userId, service])\n  @@unique([reference, service])\n}\n\nmodel Product {\n  id          String   @id\n  name        String\n  description String?\n  features    String[]\n  images      String[]\n  meta        Json\n\n  createdAt    DateTime         @default(now()) @db.Timestamptz(3)\n  checkout     TransactionLog[]\n  userProducts UserProduct[]\n}\n\nmodel TransactionLog {\n  // Stripe event id (debug and idempotency purposes)\n  eventId String  @id\n  userId  String?\n  user    User?   @relation(fields: [userId], references: [id])\n\n  productId String?\n  product   Product? @relation(fields: [productId], references: [id])\n\n  createdAt DateTime @default(now()) @db.Timestamptz(3)\n\n  eventData Json? @default(dbgenerated())\n\n  eventType    String? @default(dbgenerated())\n  eventCreated Int?    @default(dbgenerated())\n\n  // paid\n  status String? @default(dbgenerated())\n\n  customerId    String? @default(dbgenerated())\n  customerEmail String? @default(dbgenerated())\n\n  subscriptionId String? @default(dbgenerated())\n\n  // Used for Refund\n  paymentIntent String? @default(dbgenerated())\n\n  @@unique([eventId, productId])\n}\n\nview UserProduct {\n  userId         String\n  user           User    @relation(fields: [userId], references: [id])\n  productId      String\n  product        Product @relation(fields: [productId], references: [id])\n  // subscriptionId and customerId not null for subscriptions\n  subscriptionId String?\n  customerId     String?\n  // Easier to debug\n  customerEmail  String?\n\n  @@unique([userId, productId])\n}\n\nmodel Project {\n  id                        String                       @id @default(uuid())\n  createdAt                 DateTime                     @default(now()) @db.Timestamptz(3)\n  title                     String\n  // Tag ids used in User.projectTags\n  tags                      String[]\n  domain                    String                       @unique\n  user                      User?                        @relation(fields: [userId], references: [id])\n  userId                    String?\n  build                     Build[]\n  isDeleted                 Boolean                      @default(false)\n  files                     File[]\n  projectDomain             ProjectDomain[]\n  authorizationToken        AuthorizationToken[]\n  previewImageAsset         Asset?                       @relation(fields: [previewImageAssetId, id], references: [id, projectId])\n  previewImageAssetId       String?\n  marketplaceApprovalStatus MarketplaceApprovalStatus    @default(UNLISTED)\n  latestStaticBuild         LatestStaticBuildPerProject?\n\n  @@unique([id, isDeleted])\n  @@unique([domain, isDeleted])\n  @@unique([id, domain])\n  // ApprovedMarketplaceProduct view performance index\n  @@index([isDeleted, marketplaceApprovalStatus])\n}\n\nenum PublishStatus {\n  PENDING\n  PUBLISHED\n  FAILED\n}\n\nenum MarketplaceApprovalStatus {\n  UNLISTED\n  PENDING\n  APPROVED\n  REJECTED\n}\n\nmodel Build {\n  id                String   @unique @default(uuid())\n  version           Int      @default(0)\n  lastTransactionId String?\n  createdAt         DateTime @default(now()) @db.Timestamptz(3)\n  updatedAt         DateTime @default(now()) @updatedAt @db.Timestamptz(3)\n\n  pages String\n\n  project   Project @relation(fields: [projectId], references: [id])\n  projectId String\n\n  breakpoints           String @default(\"[]\")\n  styles                String @default(\"[]\")\n  styleSources          String @default(\"[]\")\n  styleSourceSelections String @default(\"[]\")\n  props                 String @default(\"[]\")\n  dataSources           String @default(\"[]\")\n  resources             String @default(\"[]\")\n  instances             String @default(\"[]\")\n  marketplaceProduct    String @default(\"{}\")\n\n  deployment    String?\n  publishStatus PublishStatus @default(PENDING)\n\n  latestStaticBuildPerProject LatestStaticBuildPerProject?\n\n  @@id([id, projectId])\n  // ApprovedMarketplaceProduct view performance index\n  @@index([projectId, createdAt(sort: Desc)])\n}\n\nenum AuthorizationRelation {\n  viewers\n  editors\n  builders\n  administrators\n}\n\nmodel AuthorizationToken {\n  token     String                @default(uuid())\n  projectId String\n  project   Project               @relation(fields: [projectId], references: [id])\n  name      String                @default(\"\")\n  relation  AuthorizationRelation @default(viewers)\n  createdAt DateTime              @default(now()) @db.Timestamptz(3)\n\n  canClone Boolean @default(true)\n  canCopy  Boolean @default(true)\n\n  @@id([token, projectId])\n  @@unique([token])\n}\n\nenum DomainStatus {\n  INITIALIZING\n  ACTIVE\n  ERROR\n  PENDING\n}\n\n// Domains  + last known status and last known txtRecord\n// In the future we can update this table using queue, n8n or temporal workflows.\n// As of now updates are done during UI interactions\nmodel Domain {\n  id        String   @id @default(uuid())\n  domain    String   @unique\n  createdAt DateTime @default(now()) @db.Timestamptz(3)\n  updatedAt DateTime @default(now()) @updatedAt @db.Timestamptz(3)\n\n  ProjectDomain ProjectDomain[]\n  // Last known txtRecord of the domain (to check domain ownership)\n  txtRecord     String?\n  // create, init, pending, active, error\n  status        DomainStatus    @default(INITIALIZING)\n  // In case of status=\"error\", this will contain the error message\n  error         String?\n}\n\nmodel ProjectDomain {\n  projectId String\n  project   Project  @relation(fields: [projectId], references: [id])\n  domainId  String\n  domain    Domain   @relation(fields: [domainId], references: [id])\n  createdAt DateTime @default(now()) @db.Timestamptz(3)\n  // Generated txt record to check domain ownership\n  txtRecord String   @unique @default(uuid())\n\n  // CNAME record to point to the domain\n  cname String\n\n  @@id([projectId, domainId])\n  @@index([domainId])\n}\n\nview LatestStaticBuildPerProject {\n  buildId String\n  build   Build  @relation(fields: [buildId, projectId], references: [id, projectId])\n\n  projectId String\n  project   Project @relation(fields: [projectId], references: [id])\n\n  publishStatus PublishStatus\n  updatedAt     DateTime      @db.Timestamptz(3)\n\n  @@id([projectId])\n  @@unique([buildId, projectId])\n}\n\n// Dashboard\nview DashboardProject {\n  id                        String                    @id @default(uuid())\n  createdAt                 DateTime                  @default(now()) @db.Timestamptz(3)\n  title                     String\n  domain                    String\n  userId                    String?\n  previewImageAsset         Asset?                    @relation(fields: [previewImageAssetId, id], references: [id, projectId])\n  previewImageAssetId       String?\n  isDeleted                 Boolean                   @default(false)\n  isPublished               Boolean\n  marketplaceApprovalStatus MarketplaceApprovalStatus @default(UNLISTED)\n}\n\nview ApprovedMarketplaceProduct {\n  projectId          String  @id\n  marketplaceProduct String\n  authorizationToken String?\n}\n"
  },
  {
    "path": "packages/prisma-client/prisma.cjs",
    "content": "const pkg = require(\"./lib/prisma.js\");\n\nexports.prisma = pkg.prisma;\nexports.Prisma = pkg.Prisma;\nexports.PrismaClientKnownRequestError = pkg.PrismaClientKnownRequestError;\nexports.Decimal = pkg.Decimal;\n"
  },
  {
    "path": "packages/prisma-client/prisma.mjs",
    "content": "import pkg from \"./lib/prisma.js\";\n\nexport const { prisma, Prisma, PrismaClientKnownRequestError, Decimal } = pkg;\n"
  },
  {
    "path": "packages/prisma-client/src/cjs/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "packages/prisma-client/src/prisma.ts",
    "content": "import { PrismaClient, Prisma } from \"./__generated__\";\n\nexport type {\n  User,\n  Build,\n  Project,\n  Asset,\n  File,\n  DashboardProject,\n  AuthorizationToken,\n  DomainStatus,\n  Domain,\n  PublishStatus,\n  Product,\n  $Enums,\n} from \"./__generated__\";\n\nexport { Prisma };\nexport const { PrismaClientKnownRequestError, Decimal } = Prisma;\n\ndeclare global {\n  // allow global `var` declarations\n  // eslint-disable-next-line no-var\n  var prisma:\n    | PrismaClient<\n        {\n          log: \"query\"[];\n        },\n        \"query\"\n      >\n    | undefined;\n}\n\nconst logPrisma = process.env.NODE_ENV === \"production\";\n\ntype PrismaClientOptions = {\n  datasourceUrl: string;\n  timeout?: number;\n  maxWait?: number;\n};\n\nexport const createPrisma = ({\n  datasourceUrl,\n  timeout = 5000,\n  maxWait = 2000,\n}: PrismaClientOptions) => {\n  return new PrismaClient({\n    datasourceUrl,\n    transactionOptions: {\n      timeout,\n      maxWait,\n    },\n  });\n};\n\n// this fixes the issue with `warn(prisma-client) There are already 10 instances of Prisma Client actively running.`\n// explanation here\n// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices\nexport const prisma =\n  global.prisma ||\n  new PrismaClient({\n    ...(logPrisma\n      ? {\n          log: [\n            { emit: \"event\", level: \"query\" },\n\n            {\n              emit: \"stdout\",\n              level: \"error\",\n            },\n            {\n              emit: \"stdout\",\n              level: \"info\",\n            },\n            {\n              emit: \"stdout\",\n              level: \"warn\",\n            },\n          ],\n        }\n      : {\n          log: [\n            {\n              emit: \"stdout\",\n              level: \"error\",\n            },\n            {\n              emit: \"stdout\",\n              level: \"info\",\n            },\n            {\n              emit: \"stdout\",\n              level: \"warn\",\n            },\n          ],\n        }),\n  });\n\nprisma.$on(\"query\", (error) => {\n  // Try to minify the query as vercel/new relic log size is limited\n\n  console.info(\n    \"Query: \" +\n      error.query\n        .replace(/\"public\"\\./g, \"\")\n        .replace(/\"Project\"\\./g, \"\")\n        .replace(/\"Build\"\\./g, \"\")\n        .replace(/\"AuthorizationToken\"\\./g, \"\")\n        .replace(/\"Asset\"\\./g, \"\")\n  );\n\n  console.info(\"Params: \" + error.params.slice(0, 200));\n\n  console.info(\"Duration: \" + error.duration + \"ms\");\n});\n\nif (process.env.NODE_ENV !== \"production\") {\n  global.prisma = prisma;\n}\n"
  },
  {
    "path": "packages/prisma-client/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./lib\"\n  }\n}\n"
  },
  {
    "path": "packages/project/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/project/README.md",
    "content": "# Webstudio Project\n\nProject related functionalities. This was temporarily placed here to reuse between other packages, but we need to split this into separate packages and probably remove this package entirely or keep it only for \"project\" specific functionality.\n"
  },
  {
    "path": "packages/project/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/project\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Project\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"dependencies\": {\n    \"@trpc/server\": \"^10.45.2\",\n    \"@webstudio-is/asset-uploader\": \"workspace:*\",\n    \"@webstudio-is/authorization-token\": \"workspace:*\",\n    \"@webstudio-is/project-build\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"nanoid\": \"^5.1.5\",\n    \"slugify\": \"^1.6.6\",\n    \"type-fest\": \"^4.37.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\"\n    },\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/project/src/db/project-domain.ts",
    "content": "import { customAlphabet } from \"nanoid\";\nimport slugify from \"slugify\";\n\nconst nanoid = customAlphabet(\"1234567890abcdefghijklmnopqrstuvwxyz\");\n\nconst slugifyOptions = { lower: true, strict: true };\nconst MIN_DOMAIN_LENGTH = 10;\n\n// MIN_DOMAIN_LENGTH doesn't allow to use \"assets\" as a domain,\n// but in case it will be changed in the future, we reserve even short names\nconst reservedDomains = [\n  // Reserved for SaaS support\n  \"customer\",\n  \"customers\",\n  \"proxy-fallback\",\n  // Reserved for local development\n  \"local\",\n  // Reserved for image transforms\n  \"image-transform\",\n  \"image-transforms\",\n  \"images-transform\",\n  \"images-transforms\",\n  // Assets\n  \"assets\",\n  \"static-assets\",\n  \"fonts\",\n  \"images\",\n];\n\n// For the future use, reserve prefixes we can use for internal services\nconst reservedPrefixes = [\"wstd_sys_\", \"wstd-sys-\"];\n\nexport const validateProjectDomain = (\n  domainInput: string\n): { success: false; error: string } | { success: true; domain: string } => {\n  try {\n    const domain = slugify(domainInput, slugifyOptions);\n\n    if (domain.length < MIN_DOMAIN_LENGTH) {\n      return {\n        success: false,\n        error: `Minimum ${MIN_DOMAIN_LENGTH} characters required`,\n      };\n    }\n\n    if (reservedDomains.includes(domain)) {\n      return {\n        success: false,\n        error: `Domain ${domain} is reserved`,\n      };\n    }\n\n    if (reservedPrefixes.some((prefix) => domain.startsWith(prefix))) {\n      return {\n        success: false,\n        error: `Domain ${domain} is reserved`,\n      };\n    }\n\n    return {\n      success: true,\n      domain,\n    };\n  } catch {\n    return {\n      success: false,\n      error: `Invalid domain ${domainInput}`,\n    };\n  }\n};\n\nexport const generateDomain = (title: string) => {\n  const slugifiedTitle = slugify(title, slugifyOptions);\n  const domain = `${slugifiedTitle}-${nanoid(\n    // If user entered a long title already, we just add 5 chars generated id\n    // Otherwise we add the amount of chars to satisfy min length\n    Math.max(MIN_DOMAIN_LENGTH - slugifiedTitle.length - 1, 5)\n  )}`;\n  return domain;\n};\n"
  },
  {
    "path": "packages/project/src/db/project.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport {\n  authorizeProject,\n  type AppContext,\n  AuthorizationError,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { createBuild } from \"@webstudio-is/project-build/index.server\";\nimport { MarketplaceApprovalStatus, Title } from \"../shared/schema\";\nimport { generateDomain, validateProjectDomain } from \"./project-domain\";\nimport type { SetNonNullable } from \"type-fest\";\n\nexport const findProjectIdsByUserId = async (\n  userId: string,\n  context: AppContext\n) => {\n  if (context.authorization.type !== \"user\") {\n    throw new AuthorizationError(\n      \"Only logged in users can view the project list\"\n    );\n  }\n\n  if (userId !== context.authorization.userId) {\n    throw new AuthorizationError(\n      \"Only the project owner can view the project list\"\n    );\n  }\n\n  const result = await context.postgrest.client\n    .from(\"Project\")\n    .select(\"id\")\n    .eq(\"userId\", userId)\n    .eq(\"isDeleted\", false)\n    .order(\"id\");\n\n  if (result.error) {\n    throw result.error;\n  }\n\n  return result.data;\n};\n\nexport type Project = Awaited<ReturnType<typeof loadById>>;\n\nexport const loadById = async (projectId: string, context: AppContext) => {\n  const canRead = await authorizeProject.hasProjectPermit(\n    { projectId, permit: \"view\" },\n    context\n  );\n\n  if (canRead === false) {\n    throw new AuthorizationError(\"You don't have access to this project\");\n  }\n\n  const data = await context.postgrest.client\n    .from(\"Project\")\n    .select(\n      `\n        *,\n        previewImageAsset:Asset (*),\n        latestBuildVirtual(*),\n        latestStaticBuild:LatestStaticBuildPerProject (*),\n        domainsVirtual(*, latestBuildVirtual(*))\n      `\n    )\n    .eq(\"id\", projectId)\n    .eq(\"isDeleted\", false)\n    .single();\n\n  if (data.error) {\n    throw data.error;\n  }\n  const { latestStaticBuild, ...project } = data.data;\n\n  return {\n    ...project,\n    // postgres marks all view fields as nullable\n    // workaround this by casting to non nullable\n    latestStaticBuild: (latestStaticBuild[0] ?? null) as null | SetNonNullable<\n      NonNullable<(typeof latestStaticBuild)[0]>\n    >,\n  };\n};\n\nexport const create = async (\n  { title }: { title: string },\n  context: AppContext\n) => {\n  Title.parse(title);\n\n  if (context.authorization.type !== \"user\") {\n    throw new AuthorizationError(\"Only logged in users can create a project\");\n  }\n\n  const userId = context.authorization.userId;\n\n  if (userId === undefined) {\n    throw new Error(\"The user must be authenticated to create a project\");\n  }\n\n  const projectId = crypto.randomUUID();\n\n  // create project without user first\n  // and set user only after build is successfully created\n  // this way to make project creation transactional\n  // for user\n  const newProject = await context.postgrest.client.from(\"Project\").insert({\n    id: projectId,\n    title,\n    domain: generateDomain(title),\n  });\n  if (newProject.error) {\n    throw newProject.error;\n  }\n\n  await createBuild({ projectId }, context);\n\n  const updatedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({ userId })\n    .eq(\"id\", projectId)\n    .select(\"*\")\n    .single();\n  if (updatedProject.error) {\n    throw updatedProject.error;\n  }\n  return updatedProject.data;\n};\n\nexport const markAsDeleted = async (\n  projectId: Project[\"id\"],\n  context: AppContext\n) => {\n  const canDelete = await authorizeProject.hasProjectPermit(\n    { projectId, permit: \"own\" },\n    context\n  );\n\n  if (canDelete === false) {\n    return { errors: \"Only the owner can delete the project\" };\n  }\n\n  const deletedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({\n      isDeleted: true,\n      // Free up the subdomain\n      domain: nanoid(),\n    })\n    .eq(\"id\", projectId);\n  if (deletedProject.error) {\n    throw deletedProject.error;\n  }\n};\n\nconst assertEditPermission = async (projectId: string, context: AppContext) => {\n  const canEdit = await authorizeProject.hasProjectPermit(\n    { projectId, permit: \"edit\" },\n    context\n  );\n\n  if (canEdit === false) {\n    throw new Error(\n      \"Only a token or user with edit permission can edit the project.\"\n    );\n  }\n};\n\nexport const rename = async (\n  {\n    projectId,\n    title,\n  }: {\n    projectId: Project[\"id\"];\n    title: string;\n  },\n  context: AppContext\n) => {\n  Title.parse(title);\n\n  await assertEditPermission(projectId, context);\n\n  const renamedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({ title })\n    .eq(\"id\", projectId);\n  if (renamedProject.error) {\n    throw renamedProject.error;\n  }\n};\n\nexport const updatePreviewImage = async (\n  {\n    projectId,\n    assetId,\n  }: {\n    projectId: Project[\"id\"];\n    assetId: string | null;\n  },\n  context: AppContext\n) => {\n  await assertEditPermission(projectId, context);\n\n  const updatedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({ previewImageAssetId: assetId })\n    .eq(\"id\", projectId);\n  if (updatedProject.error) {\n    throw updatedProject.error;\n  }\n};\n\nexport const clone = async (\n  {\n    projectId,\n    title,\n  }: {\n    projectId: string;\n    title?: string | undefined;\n  },\n  destinationContext: AppContext,\n  sourceContext: AppContext\n) => {\n  const project = await loadById(projectId, sourceContext);\n  if (project === null) {\n    throw new Error(`Not found project \"${projectId}\"`);\n  }\n\n  if (destinationContext.authorization.type !== \"user\") {\n    throw new AuthorizationError(\"Only logged in users can clone a project\");\n  }\n\n  const { userId } = destinationContext.authorization;\n  if (userId === undefined) {\n    throw new Error(\"The user must be authenticated to clone the project\");\n  }\n\n  // Should be some mixed context in case of RLS\n  const clonedProject = await destinationContext.postgrest.client.rpc(\n    \"clone_project\",\n    {\n      project_id: projectId,\n      user_id: userId,\n      title: title ?? `${project.title} (copy)`,\n      domain: generateDomain(project.title),\n    }\n  );\n\n  if (clonedProject.error) {\n    throw clonedProject.error;\n  }\n\n  return { id: clonedProject.data.id };\n};\n\nexport const updateDomain = async (\n  input: {\n    id: string;\n    domain: string;\n  },\n  context: AppContext\n) => {\n  const domainValidation = validateProjectDomain(input.domain);\n\n  if (domainValidation.success === false) {\n    throw new Error(domainValidation.error);\n  }\n\n  const { domain } = domainValidation;\n\n  await assertEditPermission(input.id, context);\n\n  // Check if the current wstd domain is published - forbid renaming while published\n  // Get current domain first\n  const currentProject = await context.postgrest.client\n    .from(\"Project\")\n    .select(\"domain\")\n    .eq(\"id\", input.id)\n    .single();\n\n  if (currentProject.error) {\n    throw currentProject.error;\n  }\n\n  // Check if any build has this domain in deployment.domains\n  const buildsWithDomain = await context.postgrest.client\n    .from(\"Build\")\n    .select(\"id, deployment\")\n    .eq(\"projectId\", input.id)\n    .not(\"deployment\", \"is\", null);\n\n  if (buildsWithDomain.error) {\n    throw buildsWithDomain.error;\n  }\n\n  const isDomainPublished = buildsWithDomain.data.some((build) => {\n    const deployment = build.deployment as {\n      destination?: string;\n      domains?: string[];\n    } | null;\n    if (deployment === null) {\n      return false;\n    }\n    if (deployment.destination === \"static\") {\n      return false;\n    }\n    return deployment.domains?.includes(currentProject.data.domain) ?? false;\n  });\n\n  if (isDomainPublished) {\n    throw new Error(\n      \"Cannot change domain while it is published. Unpublish first.\"\n    );\n  }\n\n  const updatedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({ domain })\n    .eq(\"id\", input.id);\n  if (updatedProject.error) {\n    if (updatedProject.error.code === \"23505\") {\n      throw new Error(`Domain \"${domain}\" is already used`);\n    }\n    throw updatedProject.error;\n  }\n};\n\nexport const setMarketplaceApprovalStatus = async (\n  {\n    projectId,\n    marketplaceApprovalStatus,\n  }: {\n    projectId: Project[\"id\"];\n    marketplaceApprovalStatus: MarketplaceApprovalStatus;\n  },\n  context: AppContext\n) => {\n  if (\n    marketplaceApprovalStatus === \"APPROVED\" ||\n    marketplaceApprovalStatus === \"REJECTED\"\n  ) {\n    throw new Error(\"User can't approve or reject\");\n  }\n  await assertEditPermission(projectId, context);\n\n  const updatedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({ marketplaceApprovalStatus })\n    .eq(\"id\", projectId)\n    .select()\n    .single();\n  if (updatedProject.error) {\n    throw updatedProject.error;\n  }\n  return updatedProject.data;\n};\n\nexport const updateProjectTags = async (\n  { projectId, tags }: { projectId: Project[\"id\"]; tags: string[] },\n  context: AppContext\n) => {\n  await assertEditPermission(projectId, context);\n  const updatedProject = await context.postgrest.client\n    .from(\"Project\")\n    .update({ tags })\n    .eq(\"id\", projectId)\n    .select()\n    .single();\n  if (updatedProject.error) {\n    throw updatedProject.error;\n  }\n  return updatedProject.data;\n};\n"
  },
  {
    "path": "packages/project/src/index.server.ts",
    "content": "export * from \"./db/project\";\nexport * from \"./trpc/project-router\";\n"
  },
  {
    "path": "packages/project/src/index.ts",
    "content": "export * from \"./shared/schema\";\nexport type { Project } from \"./db/project\";\nexport type { ProjectRouter } from \"./trpc/project-router\";\nexport { validateProjectDomain } from \"./db/project-domain\";\n"
  },
  {
    "path": "packages/project/src/shared/schema.ts",
    "content": "import { z } from \"zod\";\n\nconst MIN_TITLE_LENGTH = 2;\n\nexport const Title = z\n  .string()\n  .refine(\n    (val) => val.length >= MIN_TITLE_LENGTH,\n    `Minimum ${MIN_TITLE_LENGTH} characters required`\n  );\n\nexport const MarketplaceApprovalStatus = z.enum([\n  \"UNLISTED\",\n  \"PENDING\",\n  \"APPROVED\",\n  \"REJECTED\",\n]);\n\nexport type MarketplaceApprovalStatus = z.infer<\n  typeof MarketplaceApprovalStatus\n>;\n"
  },
  {
    "path": "packages/project/src/trpc/project-router.ts",
    "content": "import * as projectApi from \"../db/project\";\nimport { z } from \"zod\";\nimport {\n  router,\n  procedure,\n  AuthorizationError,\n  authorizeProject,\n  createErrorResponse,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { MarketplaceApprovalStatus, Title } from \"../shared/schema\";\n\nexport const projectRouter = router({\n  rename: procedure\n    .input(\n      z.object({\n        title: Title,\n        projectId: z.string(),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      // @todo pass ctx for authorization\n      return await projectApi.rename(input, ctx);\n    }),\n\n  delete: procedure\n    .input(z.object({ projectId: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      // @todo pass ctx for authorization\n      return await projectApi.markAsDeleted(input.projectId, ctx);\n    }),\n\n  clone: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        title: z.optional(z.string()),\n        authToken: z.optional(z.string()),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      const sourceContext = input.authToken\n        ? await ctx.createTokenContext(input.authToken)\n        : ctx;\n      return await projectApi.clone(input, ctx, sourceContext);\n    }),\n\n  create: procedure\n    .input(z.object({ title: Title }))\n    .mutation(async ({ input, ctx }) => {\n      return await projectApi.create(input, ctx);\n    }),\n\n  setMarketplaceApprovalStatus: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        marketplaceApprovalStatus: MarketplaceApprovalStatus,\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      return await projectApi.setMarketplaceApprovalStatus(input, ctx);\n    }),\n\n  updateTags: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        tags: z.array(z.string()),\n      })\n    )\n    .mutation(async ({ input, ctx }) => {\n      await projectApi.updateProjectTags(input, ctx);\n    }),\n\n  findCurrentUserProjectIds: procedure.query(async ({ ctx }) => {\n    if (ctx.authorization.type !== \"user\") {\n      return [];\n    }\n    const projectIds = await projectApi.findProjectIdsByUserId(\n      ctx.authorization.userId,\n      ctx\n    );\n    return projectIds.map((project) => project.id);\n  }),\n\n  userPublishCount: procedure.query(async ({ ctx }) => {\n    try {\n      if (\n        ctx.authorization.type !== \"user\" &&\n        ctx.authorization.type !== \"token\"\n      ) {\n        throw new Error(\"Not authorized\");\n      }\n      const userId =\n        ctx.authorization.type === \"user\"\n          ? ctx.authorization.userId\n          : ctx.authorization.ownerId;\n      const result = await ctx.postgrest.client\n        .from(\"user_publish_count\")\n        .select(\"count\")\n        .eq(\"user_id\", userId)\n        .maybeSingle();\n      if (result.error) {\n        throw result.error;\n      }\n      return {\n        success: true,\n        data: result.data?.count ?? 0,\n      };\n    } catch (error) {\n      return createErrorResponse(error);\n    }\n  }),\n\n  publishedBuilds: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n      })\n    )\n    .query(async ({ input, ctx }) => {\n      try {\n        if (\n          ctx.authorization.type !== \"user\" &&\n          ctx.authorization.type !== \"token\"\n        ) {\n          throw new AuthorizationError(\"Not authorized\");\n        }\n        const permit = await authorizeProject.getProjectPermit(\n          {\n            projectId: input.projectId,\n            permits: [\"view\", \"build\", \"admin\"],\n          },\n          ctx\n        );\n        if (permit === undefined) {\n          throw new AuthorizationError(\"Not authorized to access this project\");\n        }\n        const publishedBuilds = await ctx.postgrest.client\n          .from(\"published_builds\")\n          .select(\"*\")\n          .eq(\"projectId\", input.projectId);\n        if (publishedBuilds.error) {\n          throw publishedBuilds.error;\n        }\n        return {\n          success: true,\n          data: publishedBuilds.data,\n        };\n      } catch (error) {\n        return createErrorResponse(error);\n      }\n    }),\n\n  restoreDevelopmentBuild: procedure\n    .input(\n      z.object({\n        projectId: z.string(),\n        fromBuildId: z.string(),\n      })\n    )\n    .mutation(\n      async ({\n        input,\n        ctx,\n      }): Promise<{ success: true } | { success: false; error: string }> => {\n        try {\n          const permit = await authorizeProject.getProjectPermit(\n            {\n              projectId: input.projectId,\n              permits: [\"build\", \"admin\"],\n            },\n            ctx\n          );\n          if (!permit) {\n            throw new AuthorizationError(\"Not authorized\");\n          }\n          const build = await ctx.postgrest.client.rpc(\n            \"restore_development_build\",\n            {\n              project_id: input.projectId,\n              from_build_id: input.fromBuildId,\n            }\n          );\n          if (build.error) {\n            throw build.error;\n          }\n          return {\n            success: true,\n          };\n        } catch (error) {\n          return createErrorResponse(error);\n        }\n      }\n    ),\n});\n\nexport type ProjectRouter = typeof projectRouter;\n"
  },
  {
    "path": "packages/project/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/project-build/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/project-build\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio Project Build\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\"\n    },\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/authorization-token\": \"workspace:*\",\n    \"@webstudio-is/postgrest\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/trpc-interface\": \"workspace:*\",\n    \"nanoid\": \"^5.1.5\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"vitest\": \"^3.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/project-build/src/db/build.ts",
    "content": "/* eslint no-console: [\"error\", { allow: [\"time\", \"timeEnd\"] }] */\n\nimport type { Database } from \"@webstudio-is/postgrest/index.server\";\nimport {\n  AuthorizationError,\n  authorizeProject,\n  type AppContext,\n} from \"@webstudio-is/trpc-interface/index.server\";\nimport { db as authDb } from \"@webstudio-is/authorization-token/index.server\";\nimport type {\n  Deployment,\n  Resource,\n  StyleSource,\n  Prop,\n  DataSource,\n  Instance,\n  Breakpoint,\n  StyleSourceSelection,\n  StyleDecl,\n  Pages,\n} from \"@webstudio-is/sdk\";\nimport type { Build, CompactBuild } from \"../types\";\nimport { parseDeployment } from \"./deployment\";\nimport type { MarketplaceProduct } from \"../shared//marketplace\";\nimport { breakCyclesMutable } from \"../shared/graph-utils\";\nimport { createPages } from \"../template\";\nimport { serializeStyles } from \"./styles\";\nimport { serializeStyleSourceSelections } from \"./style-source-selections\";\n\nconst parseCompactData = <Item>(serialized: string) =>\n  JSON.parse(serialized) as Item[];\n\nconst parseCompactInstanceData = (serialized: string) => {\n  const instances = JSON.parse(serialized) as Instance[];\n\n  // @todo: Remove after measurements on real data\n  console.time(\"breakCyclesMutable\");\n  breakCyclesMutable(instances, (node) => node.component === \"Slot\");\n  console.timeEnd(\"breakCyclesMutable\");\n\n  return instances;\n};\n\nexport const parseData = <Type extends { id: string }>(\n  string: string\n): Map<Type[\"id\"], Type> => {\n  const list = JSON.parse(string) as Type[];\n  return new Map(list.map((item) => [item.id, item]));\n};\n\nexport const parseInstanceData = (\n  string: string\n): Map<Instance[\"id\"], Instance> => {\n  const list = parseCompactInstanceData(string);\n  return new Map(list.map((item) => [item.id, item]));\n};\n\nexport const serializeData = <Type extends { id: string }>(\n  data: Map<Type[\"id\"], Type>\n) => {\n  const dataSourcesList: Type[] = Array.from(data.values());\n  return JSON.stringify(dataSourcesList);\n};\n\nexport const parseConfig = <Type>(string: string): Type => {\n  return JSON.parse(string);\n};\n\nexport const serializeConfig = <Type>(data: Type) => {\n  return JSON.stringify(data);\n};\n\nconst parseCompactBuild = async (\n  build: Database[\"public\"][\"Tables\"][\"Build\"][\"Row\"]\n) => {\n  try {\n    return {\n      id: build.id,\n      projectId: build.projectId,\n      version: build.version,\n      createdAt: build.createdAt,\n      updatedAt: build.updatedAt,\n      pages: parseConfig<Pages>(build.pages),\n      breakpoints: parseCompactData<Breakpoint>(build.breakpoints),\n      styles: parseCompactData<StyleDecl>(build.styles),\n      styleSources: parseCompactData<StyleSource>(build.styleSources),\n      styleSourceSelections: parseCompactData<StyleSourceSelection>(\n        build.styleSourceSelections\n      ),\n      props: parseCompactData<Prop>(build.props),\n      dataSources: parseCompactData<DataSource>(build.dataSources),\n      resources: parseCompactData<Resource>(build.resources),\n      instances: parseCompactInstanceData(build.instances),\n      deployment: parseDeployment(build.deployment),\n      marketplaceProduct: parseConfig<MarketplaceProduct>(\n        build.marketplaceProduct\n      ),\n    } satisfies CompactBuild;\n  } finally {\n    // empty block\n  }\n};\n\nexport const loadRawBuildById = async (\n  context: AppContext,\n  id: Build[\"id\"]\n) => {\n  const build = await context.postgrest.client\n    .from(\"Build\")\n    .select(\"*\")\n    .eq(\"id\", id);\n  // .single(); Note: Single response is not compressed. Uncomment the following line once the issue is resolved: https://github.com/orgs/supabase/discussions/28757\n\n  if (build.error) {\n    throw build.error;\n  }\n\n  if (build.data.length !== 1) {\n    throw new Error(\n      `Results contain ${build.data.length} row(s) requires 1 row`\n    );\n  }\n\n  return build.data[0];\n};\n\nexport const loadBuildById = async (context: AppContext, id: Build[\"id\"]) => {\n  const build = await loadRawBuildById(context, id);\n\n  return parseCompactBuild(build);\n};\n\nexport const loadDevBuildByProjectId = async (\n  context: AppContext,\n  projectId: Build[\"projectId\"]\n) => {\n  const build = await context.postgrest.client\n    .from(\"Build\")\n    .select(\"*\")\n    .eq(\"projectId\", projectId)\n    .is(\"deployment\", null)\n    .order(\"createdAt\", { ascending: false })\n    .limit(1);\n  // .single(); Note: Single response is not compressed. Uncomment the following line once the issue is resolved: https://github.com/orgs/supabase/discussions/28757\n\n  if (build.error) {\n    throw build.error;\n  }\n\n  if (build.data.length === 0) {\n    throw new Error(\"No dev build found\");\n  }\n\n  return parseCompactBuild(build.data[0]);\n};\n\nexport const loadApprovedProdBuildByProjectId = async (\n  context: AppContext,\n  projectId: Build[\"projectId\"]\n) => {\n  const project = await context.postgrest.client\n    .from(\"Project\")\n    .select(\n      `\n        id,\n        latestBuildVirtual(buildId)\n      `\n    )\n    .eq(\"id\", projectId)\n    .eq(\"isDeleted\", false)\n    .eq(\"marketplaceApprovalStatus\", \"APPROVED\")\n    .single();\n  if (project.error) {\n    throw project.error;\n  }\n  if (project.data.latestBuildVirtual === null) {\n    throw Error(\"Build not found\");\n  }\n\n  const build = await context.postgrest.client\n    .from(\"Build\")\n    .select()\n    .eq(\"id\", project.data.latestBuildVirtual.buildId);\n  // .single(); Note: Single response is not compressed. Uncomment the following line once the issue is resolved: https://github.com/orgs/supabase/discussions/28757\n\n  if (build.error) {\n    throw build.error;\n  }\n\n  if (build.data.length !== 1) {\n    throw new Error(\n      `Results contain ${build.data.length} row(s) requires 1 row`\n    );\n  }\n\n  return parseCompactBuild(build.data[0]);\n};\n\n/*\n * We create \"dev\" build in two cases:\n *   1. when we create a new project\n *   2. when we clone a project\n * We create \"prod\" build when we publish a dev build.\n */\nexport const createBuild = async (\n  props: {\n    projectId: Build[\"projectId\"];\n  },\n  context: AppContext\n): Promise<void> => {\n  const data = createPages();\n  const newBuild = await context.postgrest.client.from(\"Build\").insert({\n    id: crypto.randomUUID(),\n    projectId: props.projectId,\n    pages: serializeConfig<Pages>(data.pages),\n    breakpoints: serializeData<Breakpoint>(data.breakpoints),\n    styles: serializeStyles(data.styles),\n    styleSources: serializeData<StyleSource>(data.styleSources),\n    styleSourceSelections: serializeStyleSourceSelections(\n      data.styleSourceSelections\n    ),\n    props: serializeData<Prop>(data.props),\n    dataSources: serializeData<DataSource>(data.dataSources),\n    resources: serializeData<Resource>(data.resources),\n    instances: serializeData<Instance>(data.instances),\n  });\n  if (newBuild.error) {\n    throw newBuild.error;\n  }\n};\n\nexport const unpublishBuild = async (\n  props: {\n    projectId: Build[\"projectId\"];\n    domain: string;\n  },\n  context: AppContext\n) => {\n  const canEdit = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"edit\" },\n    context\n  );\n\n  if (canEdit === false) {\n    throw new AuthorizationError(\n      \"You don't have access to unpublish this project\"\n    );\n  }\n\n  // Find all builds that have this domain in their deployment\n  const buildsResult = await context.postgrest.client\n    .from(\"Build\")\n    .select(\"id, deployment\")\n    .eq(\"projectId\", props.projectId)\n    .not(\"deployment\", \"is\", null)\n    .order(\"createdAt\", { ascending: false });\n\n  if (buildsResult.error) {\n    throw buildsResult.error;\n  }\n\n  // Find all builds with this specific domain in deployment.domains\n  const targetBuilds = buildsResult.data.filter((build) => {\n    const deployment = parseDeployment(build.deployment);\n    if (deployment === undefined) {\n      return false;\n    }\n    if (deployment.destination === \"static\") {\n      return false;\n    }\n    return deployment.domains.includes(props.domain);\n  });\n\n  if (targetBuilds.length === 0) {\n    throw new Error(`Domain ${props.domain} is not published`);\n  }\n\n  // Process all builds that contain this domain\n  for (const targetBuild of targetBuilds) {\n    const deployment = parseDeployment(targetBuild.deployment);\n\n    if (deployment === undefined || deployment.destination !== \"saas\") {\n      continue;\n    }\n\n    // Remove the domain from the deployment\n    const remainingDomains = deployment.domains.filter(\n      (d) => d !== props.domain\n    );\n\n    if (remainingDomains.length === 0) {\n      // Delete the production build entirely when no domains remain\n      // Don't set deployment=null as that would create a duplicate \"dev build\"\n      const result = await context.postgrest.client\n        .from(\"Build\")\n        .delete()\n        .eq(\"id\", targetBuild.id);\n\n      if (result.error) {\n        throw result.error;\n      }\n    } else {\n      // Update with remaining domains\n      const newDeployment = JSON.stringify({\n        ...deployment,\n        domains: remainingDomains,\n      });\n\n      const result = await context.postgrest.client\n        .from(\"Build\")\n        .update({ deployment: newDeployment })\n        .eq(\"id\", targetBuild.id);\n\n      if (result.error) {\n        throw result.error;\n      }\n    }\n  }\n};\n\nexport const createProductionBuild = async (\n  props: {\n    projectId: Build[\"projectId\"];\n    deployment: Deployment;\n  },\n  context: AppContext\n) => {\n  const canBuild = await authorizeProject.hasProjectPermit(\n    { projectId: props.projectId, permit: \"edit\" },\n    context\n  );\n\n  if (canBuild === false) {\n    throw new AuthorizationError(\"You don't have access to build this project\");\n  }\n\n  // Get token permissions\n  if (context.authorization.type === \"token\") {\n    const permissions = await authDb.getTokenPermissions(\n      {\n        projectId: props.projectId,\n        token: context.authorization.authToken,\n      },\n      context\n    );\n\n    if (!permissions.canPublish) {\n      throw new AuthorizationError(\n        \"The token does not have permission to build this project.\"\n      );\n    }\n  }\n\n  const build = await context.postgrest.client.rpc(\"create_production_build\", {\n    project_id: props.projectId,\n    deployment: JSON.stringify(props.deployment),\n  });\n  const buildId = build.data;\n  if (build.error) {\n    throw build.error;\n  }\n  if (buildId === null) {\n    throw Error(`Project ${props.projectId} not found`);\n  }\n\n  return {\n    id: build.data,\n  };\n};\n"
  },
  {
    "path": "packages/project-build/src/db/deployment.ts",
    "content": "import type { Deployment } from \"@webstudio-is/sdk\";\n\nexport const parseDeployment = (deployment: string | null) => {\n  if (deployment === null) {\n    return;\n  }\n\n  return JSON.parse(deployment) as Deployment;\n};\n"
  },
  {
    "path": "packages/project-build/src/db/pages.ts",
    "content": "// DEPRECATED: use parseData and serializeData from build.ts\nimport type { Pages } from \"@webstudio-is/sdk\";\n\nexport const parsePages = (pagesString: string): Pages => {\n  return JSON.parse(pagesString) as Pages;\n};\n\nexport const serializePages = (pages: Pages) => {\n  return JSON.stringify(pages);\n};\n"
  },
  {
    "path": "packages/project-build/src/db/style-source-selections.ts",
    "content": "// DEPRECATED: use parseData and serializeData from build.ts\nimport type {\n  StyleSourceSelections,\n  StyleSourceSelection,\n} from \"@webstudio-is/sdk\";\n\nexport const parseStyleSourceSelections = (\n  styleSourceSelectionsString: string\n): StyleSourceSelections => {\n  const styleSourceSelectionsList = JSON.parse(\n    styleSourceSelectionsString\n  ) as StyleSourceSelection[];\n\n  return new Map(\n    styleSourceSelectionsList.map((item) => [item.instanceId, item])\n  );\n};\n\nexport const serializeStyleSourceSelections = (\n  styleSourceSelectionsMap: StyleSourceSelections\n) => {\n  const styleSourceSelectionsList: StyleSourceSelection[] = Array.from(\n    styleSourceSelectionsMap.values()\n  );\n  return JSON.stringify(styleSourceSelectionsList);\n};\n"
  },
  {
    "path": "packages/project-build/src/db/styles.ts",
    "content": "import {\n  type Styles,\n  type StyleDecl,\n  getStyleDeclKey,\n} from \"@webstudio-is/sdk\";\n\nexport const parseStyles = (stylesString: string): Styles => {\n  const stylesList = JSON.parse(stylesString) as StyleDecl[];\n  return new Map(stylesList.map((item) => [getStyleDeclKey(item), item]));\n};\n\nexport const serializeStyles = (stylesMap: Styles) => {\n  const stylesList: StyleDecl[] = Array.from(stylesMap.values());\n  return JSON.stringify(stylesList);\n};\n"
  },
  {
    "path": "packages/project-build/src/index.server.ts",
    "content": "export * from \"./db/build\";\nexport * from \"./db/pages\";\nexport * from \"./db/styles\";\nexport * from \"./db/style-source-selections\";\n"
  },
  {
    "path": "packages/project-build/src/index.ts",
    "content": "export * from \"./types\";\nexport * from \"./shared/pages-utils\";\nexport * from \"./shared/marketplace\";\nexport * from \"./shared/graph-utils\";\n"
  },
  {
    "path": "packages/project-build/src/shared/graph-utils.test.ts",
    "content": "import { test, expect, describe } from \"vitest\";\nimport { findCycles, breakCyclesMutable } from \"./graph-utils\";\nimport type { Instance } from \"@webstudio-is/sdk\";\n\nconst typeId = \"id\" as const;\n\ndescribe(\"findCycles\", () => {\n  test(\"should return an empty array for an empty graph\", () => {\n    const graph: Instance[] = [];\n    const result = findCycles(graph);\n    expect(result).toEqual([]);\n  });\n\n  test(\"should return an empty array for a graph with no cycles\", () => {\n    const graph = [\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      { id: \"2\", children: [{ type: typeId, value: \"3\" }] },\n      { id: \"3\", children: [] },\n    ];\n    const result = findCycles(graph);\n    expect(result).toEqual([]);\n  });\n\n  test(\"should return a single cycle for a graph with one cycle\", () => {\n    const graph = [\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      { id: \"2\", children: [{ type: typeId, value: \"3\" }] },\n      { id: \"3\", children: [{ type: typeId, value: \"1\" }] },\n    ];\n    const result = findCycles(graph);\n    expect(result).toEqual([[\"1\", \"2\", \"3\", \"1\"]]);\n  });\n\n  test(\"should return multiple cycles for a graph with multiple cycles\", () => {\n    const graph = [\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      {\n        id: \"2\",\n        children: [\n          { type: typeId, value: \"3\" },\n          { type: typeId, value: \"4\" },\n        ],\n      },\n      { id: \"3\", children: [{ type: typeId, value: \"1\" }] },\n      { id: \"4\", children: [{ type: typeId, value: \"2\" }] },\n    ];\n    const result = findCycles(graph);\n    expect(result).toEqual([\n      [\"1\", \"2\", \"3\", \"1\"],\n      [\"2\", \"4\", \"2\"],\n    ]);\n  });\n\n  test(\"should return multiple cycles for a graph with multiple inline cycles\", () => {\n    const graph = [\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      {\n        id: \"2\",\n        children: [{ type: typeId, value: \"3\" }],\n      },\n      {\n        id: \"3\",\n        children: [\n          { type: typeId, value: \"4\" },\n          { type: typeId, value: \"2\" },\n        ],\n      },\n      { id: \"4\", children: [{ type: typeId, value: \"1\" }] },\n    ];\n\n    const result = findCycles(graph);\n\n    expect(result).toEqual([\n      [\"1\", \"2\", \"3\", \"4\", \"1\"],\n      [\"2\", \"3\", \"2\"],\n    ]);\n  });\n});\n\ndescribe(\"breakCyclesMutable\", () => {\n  test(\"should return the same instances for an empty graph\", () => {\n    const result = breakCyclesMutable([], () => false);\n\n    expect(result).toEqual([]);\n  });\n\n  test(\"should return the same instances for a graph with no cycles\", () => {\n    const instances = [\n      { id: \"1\", component: \"Slot\", children: [{ type: typeId, value: \"2\" }] },\n      { id: \"2\", children: [{ type: typeId, value: \"3\" }] },\n      { id: \"3\", children: [] },\n    ];\n    const result = breakCyclesMutable(\n      instances,\n      (node) => node?.component === \"Slot\"\n    );\n    expect(result).toEqual(instances);\n  });\n\n  test(\"should break a single cycle in the graph\", () => {\n    const instances = [\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      { id: \"2\", children: [{ type: typeId, value: \"3\" }] },\n      { id: \"3\", component: \"Slot\", children: [{ type: typeId, value: \"1\" }] },\n    ];\n\n    const result = breakCyclesMutable(\n      instances,\n      (node) => node?.component === \"Slot\"\n    );\n\n    expect(result).toEqual([\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      { id: \"2\", children: [] },\n      { id: \"3\", component: \"Slot\", children: [{ type: typeId, value: \"1\" }] },\n    ]);\n  });\n\n  test(\"should break multiple cycles in the graph\", () => {\n    const instances = [\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      {\n        id: \"2\",\n        children: [\n          { type: typeId, value: \"3\" },\n          { type: typeId, value: \"4\" },\n        ],\n      },\n      { id: \"3\", component: \"Slot\", children: [{ type: typeId, value: \"1\" }] },\n      { id: \"4\", component: \"Slot\", children: [{ type: typeId, value: \"2\" }] },\n    ];\n\n    const result = breakCyclesMutable(\n      instances,\n      (node) => node?.component === \"Slot\"\n    );\n    expect(result).toEqual([\n      { id: \"1\", children: [{ type: typeId, value: \"2\" }] },\n      {\n        id: \"2\",\n        children: [],\n      },\n      { id: \"3\", component: \"Slot\", children: [{ type: typeId, value: \"1\" }] },\n      { id: \"4\", component: \"Slot\", children: [{ type: typeId, value: \"2\" }] },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/project-build/src/shared/graph-utils.ts",
    "content": "import type { Instance } from \"@webstudio-is/sdk\";\n\ntype InstanceId = Instance[\"id\"];\n\n// Depth-First Search (DFS) algorithm to find cycles in a directed graph\nexport const findCycles = (\n  graph: Iterable<Pick<Instance, \"id\" | \"children\">>\n): InstanceId[][] => {\n  const adjacencyList: Record<InstanceId, InstanceId[]> = {};\n\n  // Build adjacency list\n  for (const node of graph) {\n    adjacencyList[node.id] = node.children\n      .filter((child) => child.type === \"id\")\n      .map((child) => child.value);\n  }\n\n  const visited = new Set<string>();\n  const path: InstanceId[] = [];\n  const cycles: InstanceId[][] = [];\n\n  const dfs = (nodeId: string): void => {\n    if (path.includes(nodeId)) {\n      const cycleStart = path.indexOf(nodeId);\n      cycles.push(path.slice(cycleStart).concat(nodeId));\n      return;\n    }\n\n    if (visited.has(nodeId)) {\n      return;\n    }\n\n    visited.add(nodeId);\n    path.push(nodeId);\n\n    for (const childId of adjacencyList[nodeId] || []) {\n      dfs(childId);\n    }\n\n    path.pop();\n  };\n\n  // Start DFS from each node\n  for (const node of graph) {\n    if (!visited.has(node.id)) {\n      dfs(node.id);\n    }\n  }\n\n  return cycles;\n};\n\nexport const breakCyclesMutable = <T extends Pick<Instance, \"id\" | \"children\">>(\n  instances: Iterable<T>,\n  breakOn: (node: T) => boolean\n) => {\n  const cycles = findCycles(instances);\n  if (cycles.length === 0) {\n    return instances;\n  }\n\n  const cycleInstances = new Map<T[\"id\"], T>();\n  const cycleInstanceIdSet = new Set<T[\"id\"]>(cycles.flat());\n\n  // Pick all instances that are part of the cycle\n  for (const instance of instances) {\n    if (cycleInstanceIdSet.has(instance.id)) {\n      cycleInstances.set(instance.id, instance);\n    }\n  }\n\n  for (const cycle of cycles) {\n    // Find slot or take last instance\n    const slotId =\n      cycle.find((id) => breakOn(cycleInstances.get(id)!)) ??\n      cycle[cycle.length - 1];\n\n    // Remove slot from children of all instances in the cycle\n    for (const id of cycle) {\n      const instance = cycleInstances.get(id);\n      if (instance === undefined) {\n        continue;\n      }\n\n      if (instance.children.find((child) => child.value === slotId)) {\n        instance.children = instance.children.filter(\n          (child) => child.value !== slotId\n        );\n      }\n    }\n  }\n  return instances;\n};\n"
  },
  {
    "path": "packages/project-build/src/shared/marketplace.ts",
    "content": "import { z } from \"zod\";\n\nexport const MarketplaceProduct = z.object({\n  category: z.union([\n    z.literal(\"sectionTemplates\"),\n    z.literal(\"pageTemplates\"),\n    z.literal(\"integrationTemplates\"),\n  ]),\n  name: z.string().min(2).max(200).trim(),\n  thumbnailAssetId: z.string(),\n  author: z.string().min(2).max(200).trim(),\n  email: z.string().email().max(200).trim(),\n  website: z.union([\n    z.string().max(200).url().trim().optional(),\n    z.literal(\"\"),\n  ]),\n  issues: z.union([z.string().max(200).url().trim().optional(), z.literal(\"\")]),\n  description: z.string().trim().min(10).max(1000),\n});\nexport type MarketplaceProduct = z.infer<typeof MarketplaceProduct>;\n\nexport const marketplaceCategories = new Map<\n  MarketplaceProduct[\"category\"],\n  { label: string; description: string }\n>([\n  [\n    \"sectionTemplates\",\n    {\n      label: \"Sections\",\n      description:\n        \"Section templates are pre-designed layouts for quickly creating pages using sections.\",\n    },\n  ],\n  [\n    \"pageTemplates\",\n    {\n      label: \"Pages\",\n      description:\n        \"Page templates are pre-designed single or multi-page layouts for entire pages.\",\n    },\n  ],\n  [\n    \"integrationTemplates\",\n    {\n      label: \"Integrations\",\n      description:\n        \"Integration templates demonstrate how to integrate external services.\",\n    },\n  ],\n]);\n"
  },
  {
    "path": "packages/project-build/src/shared/pages-utils.test.ts",
    "content": "import { test, expect } from \"vitest\";\nimport { createDefaultPages } from \"./pages-utils\";\n\ntest(\"createDefaultPages\", () => {\n  expect(\n    createDefaultPages({\n      rootInstanceId: \"rootInstanceId\",\n      systemDataSourceId: \"systemDataSourceId\",\n      homePageId: \"homePageId\",\n    })\n  ).toEqual({\n    meta: {},\n    homePage: {\n      id: \"homePageId\",\n      name: \"Home\",\n      path: \"\",\n      title: `\"Home\"`,\n      meta: {},\n      rootInstanceId: \"rootInstanceId\",\n      systemDataSourceId: \"systemDataSourceId\",\n    },\n    pages: [],\n    folders: [\n      {\n        id: \"root\",\n        name: \"Root\",\n        slug: \"\",\n        children: [\"homePageId\"],\n      },\n    ],\n  });\n});\n"
  },
  {
    "path": "packages/project-build/src/shared/pages-utils.ts",
    "content": "import { nanoid } from \"nanoid\";\nimport {\n  type Pages,\n  type Folder,\n  ROOT_FOLDER_ID,\n  Instance,\n  DataSource,\n} from \"@webstudio-is/sdk\";\n\nexport const createRootFolder = (\n  children: Folder[\"children\"] = []\n): Folder => ({\n  id: ROOT_FOLDER_ID,\n  name: \"Root\",\n  slug: \"\",\n  children,\n});\n\nexport const createDefaultPages = ({\n  rootInstanceId,\n  systemDataSourceId,\n  homePageId = nanoid(),\n}: {\n  rootInstanceId: Instance[\"id\"];\n  systemDataSourceId?: DataSource[\"id\"];\n  homePageId?: string;\n}): Pages => {\n  // This is a root folder that nobody can delete or going to be able to see.\n  const rootFolder = createRootFolder([homePageId]);\n  return {\n    meta: {},\n    homePage: {\n      id: homePageId,\n      name: \"Home\",\n      path: \"\",\n      title: `\"Home\"`,\n      meta: {},\n      rootInstanceId,\n      systemDataSourceId,\n    },\n    pages: [],\n    folders: [rootFolder],\n  };\n};\n"
  },
  {
    "path": "packages/project-build/src/template.tsx",
    "content": "import { nanoid } from \"nanoid\";\nimport {\n  initialBreakpoints,\n  type Pages,\n  type WebstudioData,\n} from \"@webstudio-is/sdk\";\nimport { coreTemplates } from \"@webstudio-is/sdk/core-templates\";\nimport { css, renderData, ws } from \"@webstudio-is/template\";\nimport { createRootFolder } from \"./shared/pages-utils\";\n\nexport const createPages = (): WebstudioData => {\n  const breakpoints = initialBreakpoints.map((breakpoint) => ({\n    ...breakpoint,\n    id: nanoid(),\n  }));\n  const homePageId = nanoid();\n  const homeBodyId = nanoid();\n  const notFoundPageId = nanoid();\n  const notFoundBodyId = nanoid();\n\n  const data = renderData(\n    <>\n      {/* home page body */}\n      <ws.element ws:tag=\"body\" ws:id={homeBodyId}></ws.element>\n      {/* not found page body */}\n      <ws.element\n        ws:tag=\"body\"\n        ws:id={notFoundBodyId}\n        ws:style={css`\n          display: flex;\n          justify-content: center;\n          align-items: center;\n          background-color: #fff;\n        `}\n      >\n        <ws.element ws:tag=\"div\">\n          <ws.element\n            ws:tag=\"div\"\n            ws:style={css`\n              position: relative;\n              text-align: center;\n              font-weight: 900;\n              font-size: 8rem;\n              line-height: 1;\n              letter-spacing: -0.05em;\n            `}\n          >\n            <ws.element ws:tag=\"div\">404</ws.element>\n            <ws.element\n              ws:tag=\"div\"\n              ws:style={css`\n                position: absolute;\n                inset: 0 -0.125rem 0 0.125rem;\n                opacity: 0.3;\n              `}\n            >\n              404\n            </ws.element>\n            <ws.element\n              ws:tag=\"div\"\n              ws:style={css`\n                position: absolute;\n                inset: 0 0.125rem 0 -0.125rem;\n                opacity: 0.3;\n              `}\n            >\n              404\n            </ws.element>\n            <ws.element\n              ws:tag=\"div\"\n              ws:style={css`\n                position: absolute;\n                top: 50%;\n                left: 0;\n                width: 100%;\n                background-color: #fff;\n                height: 0.375rem;\n              `}\n            ></ws.element>\n          </ws.element>\n          <ws.element\n            ws:tag=\"p\"\n            ws:style={css`\n              margin-top: 1.5rem;\n              font-weight: 700;\n              font-size: 1.5rem;\n              line-height: 2rem;\n              letter-spacing: 0.05em;\n            `}\n          >\n            PAGE NOT FOUND\n          </ws.element>\n        </ws.element>\n        {coreTemplates.builtWithWebstudio.template}\n      </ws.element>\n    </>,\n    nanoid,\n    breakpoints\n  );\n\n  const pages: Pages = {\n    homePage: {\n      id: homePageId,\n      name: \"Home\",\n      path: \"\",\n      title: `\"Home\"`,\n      meta: {},\n      rootInstanceId: homeBodyId,\n    },\n    pages: [\n      {\n        id: notFoundPageId,\n        name: \"404\",\n        path: \"/*\",\n        title: `\"Page not found\"`,\n        meta: {\n          status: `404`,\n          excludePageFromSearch: \"false\",\n        },\n        rootInstanceId: notFoundBodyId,\n      },\n    ],\n    folders: [createRootFolder([homePageId, notFoundPageId])],\n  };\n\n  return { ...data, pages };\n};\n\ncreatePages();\n"
  },
  {
    "path": "packages/project-build/src/types.ts",
    "content": "import type {\n  Pages,\n  Breakpoint,\n  StyleDecl,\n  StyleDeclKey,\n  StyleSource,\n  Instance,\n  Prop,\n  StyleSourceSelection,\n  Deployment,\n  DataSource,\n  Resource,\n} from \"@webstudio-is/sdk\";\nimport type { MarketplaceProduct } from \"./shared/marketplace\";\n\nexport type Build = {\n  id: string;\n  projectId: string;\n  version: number;\n  createdAt: string;\n  updatedAt: string;\n  pages: Pages;\n  breakpoints: [Breakpoint[\"id\"], Breakpoint][];\n  styles: [StyleDeclKey, StyleDecl][];\n  styleSources: [StyleSource[\"id\"], StyleSource][];\n  styleSourceSelections: [Instance[\"id\"], StyleSourceSelection][];\n  props: [Prop[\"id\"], Prop][];\n  instances: [Instance[\"id\"], Instance][];\n  dataSources: [DataSource[\"id\"], DataSource][];\n  resources: [Resource[\"id\"], Resource][];\n  deployment?: Deployment | undefined;\n  marketplaceProduct: MarketplaceProduct;\n};\n\nexport type CompactBuild = {\n  id: string;\n  projectId: string;\n  version: number;\n  createdAt: string;\n  updatedAt: string;\n  pages: Pages;\n  breakpoints: Breakpoint[];\n  styles: StyleDecl[];\n  styleSources: StyleSource[];\n  styleSourceSelections: StyleSourceSelection[];\n  props: Prop[];\n  dataSources: DataSource[];\n  resources: Resource[];\n  instances: Instance[];\n  deployment?: Deployment;\n  marketplaceProduct: MarketplaceProduct;\n};\n"
  },
  {
    "path": "packages/project-build/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/react-sdk/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/react-sdk/LICENSE-3RD-PARTY",
    "content": "The MIT License (MIT)\n\napplies to:\n\n- https://github.com/vercel/next.js, Copyright (c) 2022 Vercel, Inc.\n\nThe MIT License (MIT)\n\nCopyright (c) 2022 Vercel, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---\n\nMIT License\n\napplies to:\n\n- https://github.com/sindresorhus/modern-normalize\n\nCopyright (c) Nicolas Gallagher\nCopyright (c) Jonathan Neal\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\n"
  },
  {
    "path": "packages/react-sdk/README.md",
    "content": "# Webstudio SDK\n\nWebstudio SDK is a TypeScript API that lets you use your Webstudio project or some components in your custom codebase or just render a complete Remix Document.\nIt is currently under development, but feel free to play with the the current [landing site](https://github.com/webstudio-is/webstudio-landing)\n"
  },
  {
    "path": "packages/react-sdk/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/react-sdk\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio JavaScript / TypeScript API\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"rm -rf lib && esbuild src/index.ts src/runtime.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"strip-indent\": \"^4.0.0\",\n    \"type-fest\": \"^4.37.0\",\n    \"vitest\": \"^3.1.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"zod\": \"^3.19.1\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/fonts\": \"workspace:*\",\n    \"@webstudio-is/icons\": \"workspace:^\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"change-case\": \"^5.4.4\",\n    \"html-tags\": \"^4.0.0\",\n    \"nanoid\": \"^5.1.5\"\n  },\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\",\n      \"types\": \"./lib/types/index.d.ts\",\n      \"import\": \"./lib/index.js\"\n    },\n    \"./runtime\": {\n      \"webstudio\": \"./src/runtime.ts\",\n      \"types\": \"./lib/types/runtime.d.ts\",\n      \"import\": \"./lib/runtime.js\"\n    },\n    \"./placeholder\": {\n      \"types\": \"./placeholder.d.ts\"\n    }\n  },\n  \"files\": [\n    \"lib/*\",\n    \"./placeholder.d.ts\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/react-sdk/placeholder.d.ts",
    "content": "declare module \"__CONSTANTS__\" {\n  import type { ImageLoader, VideoLoader } from \"@webstudio-is/image\";\n  export const assetBaseUrl: string;\n  export const imageLoader: ImageLoader;\n  export const videoLoader: undefined | VideoLoader;\n}\n\ndeclare module \"__CLIENT__\" {\n  import type { ResourceRequest, System } from \"@webstudio-is/sdk\";\n\n  export const projectId: string;\n\n  export const lastPublished: string;\n\n  export const siteName: string;\n\n  export const favIconAsset: string | undefined;\n\n  export const breakpoints: {\n    id: string;\n    minWidth?: number;\n    maxWidth?: number;\n  }[];\n\n  // Font assets on current page (can be preloaded)\n  export const pageFontAssets: string[];\n\n  export const pageBackgroundImageAssets: string[];\n\n  export const CustomCode: () => ReactNode;\n\n  export const Page: (props: { system: System }) => ReactNode;\n}\n\ndeclare module \"__SERVER__\" {\n  import type { PageMeta, System, ResourceRequest } from \"@webstudio-is/sdk\";\n\n  export const getResources: (props: { system: System }) => {\n    data: Map<string, ResourceRequest>;\n    action: Map<string, ResourceRequest>;\n  };\n\n  export const getPageMeta: (props: {\n    system: System;\n    resources: Record<string, any>;\n  }) => PageMeta;\n\n  type Params = Record<string, string | undefined>;\n  export const getRemixParams: ({ ...params }: Params) => Params;\n\n  export const contactEmail: undefined | string;\n}\n\ndeclare module \"__SITEMAP__\" {\n  export const sitemap: Array<{\n    path: string;\n    lastModified: string;\n  }>;\n}\n\ndeclare module \"__ASSETS__\" {\n  import type { RuntimeAsset } from \"@webstudio-is/sdk\";\n  export const assets: Record<string, RuntimeAsset>;\n}\n\ndeclare module \"__REDIRECT__\" {\n  export const url: string;\n  export const status: number;\n}\n"
  },
  {
    "path": "packages/react-sdk/src/__generated__/standard-attributes.ts",
    "content": "export const standardAttributesToReactProps: Record<string, string> = {\n  \"accept-charset\": \"acceptCharset\",\n  accesskey: \"accessKey\",\n  allowfullscreen: \"allowFullScreen\",\n  autocapitalize: \"autoCapitalize\",\n  autocomplete: \"autoComplete\",\n  autocorrect: \"autoCorrect\",\n  autofocus: \"autoFocus\",\n  autoplay: \"autoPlay\",\n  charset: \"charSet\",\n  class: \"className\",\n  colspan: \"colSpan\",\n  contenteditable: \"contentEditable\",\n  crossorigin: \"crossOrigin\",\n  datetime: \"dateTime\",\n  enctype: \"encType\",\n  enterkeyhint: \"enterKeyHint\",\n  fetchpriority: \"fetchPriority\",\n  for: \"htmlFor\",\n  formmethod: \"formMethod\",\n  formaction: \"formAction\",\n  formenctype: \"formEncType\",\n  formnovalidate: \"formNoValidate\",\n  formtarget: \"formTarget\",\n  hreflang: \"hrefLang\",\n  \"http-equiv\": \"httpEquiv\",\n  imagesizes: \"imageSizes\",\n  imagesrcset: \"imageSrcSet\",\n  inputmode: \"inputMode\",\n  itemid: \"itemID\",\n  itemprop: \"itemProp\",\n  itemref: \"itemRef\",\n  itemscope: \"itemScope\",\n  itemtype: \"itemType\",\n  maxlength: \"maxLength\",\n  minlength: \"minLength\",\n  nomodule: \"noModule\",\n  novalidate: \"noValidate\",\n  playsinline: \"playsInline\",\n  readonly: \"readOnly\",\n  referrerpolicy: \"referrerPolicy\",\n  rowspan: \"rowSpan\",\n  spellcheck: \"spellCheck\",\n  srcdoc: \"srcDoc\",\n  srclang: \"srcLang\",\n  srcset: \"srcSet\",\n  tabindex: \"tabIndex\",\n  usemap: \"useMap\",\n  \"alignment-baseline\": \"alignmentBaseline\",\n  \"baseline-shift\": \"baselineShift\",\n  \"clip-path\": \"clipPath\",\n  \"clip-rule\": \"clipRule\",\n  \"color-interpolation\": \"colorInterpolation\",\n  \"color-interpolation-filters\": \"colorInterpolationFilters\",\n  \"color-profile\": \"colorProfile\",\n  \"color-rendering\": \"colorRendering\",\n  \"dominant-baseline\": \"dominantBaseline\",\n  \"enable-background\": \"enableBackground\",\n  \"fill-opacity\": \"fillOpacity\",\n  \"fill-rule\": \"fillRule\",\n  \"flood-opacity\": \"floodOpacity\",\n  \"flood-color\": \"floodColor\",\n  \"font-family\": \"fontFamily\",\n  \"font-size\": \"fontSize\",\n  \"font-size-adjust\": \"fontSizeAdjust\",\n  \"font-stretch\": \"fontStretch\",\n  \"font-style\": \"fontStyle\",\n  \"font-variant\": \"fontVariant\",\n  \"font-weight\": \"fontWeight\",\n  \"glyph-orientation-horizontal\": \"glyphOrientationHorizontal\",\n  \"glyph-orientation-vertical\": \"glyphOrientationVertical\",\n  \"image-rendering\": \"imageRendering\",\n  \"letter-spacing\": \"letterSpacing\",\n  \"lighting-color\": \"lightingColor\",\n  \"marker-end\": \"markerEnd\",\n  \"marker-mid\": \"markerMid\",\n  \"marker-start\": \"markerStart\",\n  \"pointer-events\": \"pointerEvents\",\n  popovertarget: \"popoverTarget\",\n  popovertargetaction: \"popoverTargetAction\",\n  \"shape-rendering\": \"shapeRendering\",\n  \"stop-color\": \"stopColor\",\n  \"stop-opacity\": \"stopOpacity\",\n  \"stroke-dasharray\": \"strokeDasharray\",\n  \"stroke-dashoffset\": \"strokeDashoffset\",\n  \"stroke-linecap\": \"strokeLinecap\",\n  \"stroke-linejoin\": \"strokeLinejoin\",\n  \"stroke-miterlimit\": \"strokeMiterlimit\",\n  \"stroke-width\": \"strokeWidth\",\n  \"stroke-opacity\": \"strokeOpacity\",\n  \"text-anchor\": \"textAnchor\",\n  \"text-decoration\": \"textDecoration\",\n  \"text-rendering\": \"textRendering\",\n  \"unicode-bidi\": \"unicodeBidi\",\n  \"word-spacing\": \"wordSpacing\",\n  \"writing-mode\": \"writingMode\",\n  \"xlink:actuate\": \"xlinkActuate\",\n  \"xlink:arcrole\": \"xlinkArcrole\",\n  \"xlink:href\": \"xlinkHref\",\n  \"xlink:role\": \"xlinkRole\",\n  \"xlink:show\": \"xlinkShow\",\n  \"xlink:title\": \"xlinkTitle\",\n  \"xlink:type\": \"xlinkType\",\n  \"xml:base\": \"xmlBase\",\n  \"xml:lang\": \"xmlLang\",\n  \"xml:space\": \"xmlSpace\",\n  dirname: \"dirName\",\n};\n\nexport const reactPropsToStandardAttributes: Record<string, string> = {\n  acceptCharset: \"accept-charset\",\n  accessKey: \"accesskey\",\n  allowFullScreen: \"allowfullscreen\",\n  autoCapitalize: \"autocapitalize\",\n  autoComplete: \"autocomplete\",\n  autoCorrect: \"autocorrect\",\n  autoFocus: \"autofocus\",\n  autoPlay: \"autoplay\",\n  charSet: \"charset\",\n  className: \"class\",\n  colSpan: \"colspan\",\n  contentEditable: \"contenteditable\",\n  crossOrigin: \"crossorigin\",\n  dateTime: \"datetime\",\n  encType: \"enctype\",\n  enterKeyHint: \"enterkeyhint\",\n  fetchPriority: \"fetchpriority\",\n  htmlFor: \"for\",\n  formMethod: \"formmethod\",\n  formAction: \"formaction\",\n  formEncType: \"formenctype\",\n  formNoValidate: \"formnovalidate\",\n  formTarget: \"formtarget\",\n  hrefLang: \"hreflang\",\n  httpEquiv: \"http-equiv\",\n  imageSizes: \"imagesizes\",\n  imageSrcSet: \"imagesrcset\",\n  inputMode: \"inputmode\",\n  itemID: \"itemid\",\n  itemProp: \"itemprop\",\n  itemRef: \"itemref\",\n  itemScope: \"itemscope\",\n  itemType: \"itemtype\",\n  maxLength: \"maxlength\",\n  minLength: \"minlength\",\n  noModule: \"nomodule\",\n  noValidate: \"novalidate\",\n  playsInline: \"playsinline\",\n  readOnly: \"readonly\",\n  referrerPolicy: \"referrerpolicy\",\n  rowSpan: \"rowspan\",\n  spellCheck: \"spellcheck\",\n  srcDoc: \"srcdoc\",\n  srcLang: \"srclang\",\n  srcSet: \"srcset\",\n  tabIndex: \"tabindex\",\n  useMap: \"usemap\",\n  alignmentBaseline: \"alignment-baseline\",\n  baselineShift: \"baseline-shift\",\n  clipPath: \"clip-path\",\n  clipRule: \"clip-rule\",\n  colorInterpolation: \"color-interpolation\",\n  colorInterpolationFilters: \"color-interpolation-filters\",\n  colorProfile: \"color-profile\",\n  colorRendering: \"color-rendering\",\n  dominantBaseline: \"dominant-baseline\",\n  enableBackground: \"enable-background\",\n  fillOpacity: \"fill-opacity\",\n  fillRule: \"fill-rule\",\n  floodOpacity: \"flood-opacity\",\n  floodColor: \"flood-color\",\n  fontFamily: \"font-family\",\n  fontSize: \"font-size\",\n  fontSizeAdjust: \"font-size-adjust\",\n  fontStretch: \"font-stretch\",\n  fontStyle: \"font-style\",\n  fontVariant: \"font-variant\",\n  fontWeight: \"font-weight\",\n  glyphOrientationHorizontal: \"glyph-orientation-horizontal\",\n  glyphOrientationVertical: \"glyph-orientation-vertical\",\n  imageRendering: \"image-rendering\",\n  letterSpacing: \"letter-spacing\",\n  lightingColor: \"lighting-color\",\n  markerEnd: \"marker-end\",\n  markerMid: \"marker-mid\",\n  markerStart: \"marker-start\",\n  pointerEvents: \"pointer-events\",\n  popoverTarget: \"popovertarget\",\n  popoverTargetAction: \"popovertargetaction\",\n  shapeRendering: \"shape-rendering\",\n  stopColor: \"stop-color\",\n  stopOpacity: \"stop-opacity\",\n  strokeDasharray: \"stroke-dasharray\",\n  strokeDashoffset: \"stroke-dashoffset\",\n  strokeLinecap: \"stroke-linecap\",\n  strokeLinejoin: \"stroke-linejoin\",\n  strokeMiterlimit: \"stroke-miterlimit\",\n  strokeWidth: \"stroke-width\",\n  strokeOpacity: \"stroke-opacity\",\n  textAnchor: \"text-anchor\",\n  textDecoration: \"text-decoration\",\n  textRendering: \"text-rendering\",\n  unicodeBidi: \"unicode-bidi\",\n  wordSpacing: \"word-spacing\",\n  writingMode: \"writing-mode\",\n  xlinkActuate: \"xlink:actuate\",\n  xlinkArcrole: \"xlink:arcrole\",\n  xlinkHref: \"xlink:href\",\n  xlinkRole: \"xlink:role\",\n  xlinkShow: \"xlink:show\",\n  xlinkTitle: \"xlink:title\",\n  xlinkType: \"xlink:type\",\n  xmlBase: \"xml:base\",\n  xmlLang: \"xml:lang\",\n  xmlSpace: \"xml:space\",\n  dirName: \"dirname\",\n};\n"
  },
  {
    "path": "packages/react-sdk/src/collection-utils.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport {\n  getCollectionEntries,\n  generateCollectionIterationCode,\n} from \"./collection-utils\";\n\ntest(\"getCollectionEntries handles arrays\", () => {\n  expect(getCollectionEntries([\"a\", \"b\", \"c\"])).toEqual([\n    [0, \"a\"],\n    [1, \"b\"],\n    [2, \"c\"],\n  ]);\n});\n\ntest(\"getCollectionEntries handles objects\", () => {\n  expect(getCollectionEntries({ first: \"a\", second: \"b\" })).toEqual([\n    [\"first\", \"a\"],\n    [\"second\", \"b\"],\n  ]);\n});\n\ntest(\"getCollectionEntries handles null/undefined\", () => {\n  expect(getCollectionEntries(null)).toEqual([]);\n  expect(getCollectionEntries(undefined)).toEqual([]);\n});\n\ntest(\"getCollectionEntries handles non-objects\", () => {\n  expect(getCollectionEntries(42)).toEqual([]);\n  expect(getCollectionEntries(\"string\")).toEqual([]);\n  expect(getCollectionEntries(true)).toEqual([]);\n});\n\ntest(\"generateCollectionIterationCode produces correct template\", () => {\n  const code = generateCollectionIterationCode({\n    dataExpression: \"myData\",\n    keyVariable: \"index\",\n    itemVariable: \"item\",\n  });\n\n  expect(code).toBe(`Object.entries(\n  // @ts-ignore\n  myData ?? {}\n).map(([_key, item]: any) => {\n  const index = Array.isArray(myData) ? Number(_key) : _key;\n  return`);\n});\n\ntest(\"generated code works with arrays at runtime\", () => {\n  // @ts-expect-error - used in eval below\n  const data = [\"apple\", \"banana\", \"orange\"];\n  const result: Array<[string, string]> = [];\n\n  // Simulate generated code\n  eval(`\n    Object.entries(data ?? {}).map(([index, item]) => {\n      result.push([index, item]);\n    });\n  `);\n\n  expect(result).toEqual([\n    [\"0\", \"apple\"],\n    [\"1\", \"banana\"],\n    [\"2\", \"orange\"],\n  ]);\n});\n\ntest(\"generated code works with objects at runtime\", () => {\n  // @ts-expect-error - used in eval below\n  const data = { first: \"apple\", second: \"banana\" };\n  const result: Array<[string, string]> = [];\n\n  // Simulate generated code\n  eval(`\n    Object.entries(data ?? {}).map(([index, item]) => {\n      result.push([index, item]);\n    });\n  `);\n\n  expect(result).toEqual([\n    [\"first\", \"apple\"],\n    [\"second\", \"banana\"],\n  ]);\n});\n\ntest(\"generated code handles null/undefined\", () => {\n  // @ts-expect-error - used in eval below\n  let data: null | undefined = null;\n  const resultNull: Array<[string, unknown]> = [];\n\n  eval(`\n    Object.entries(data ?? {}).map(([index, item]) => {\n      resultNull.push([index, item]);\n    });\n  `);\n\n  expect(resultNull).toEqual([]);\n\n  data = undefined;\n  const resultUndefined: Array<[string, unknown]> = [];\n\n  eval(`\n    Object.entries(data ?? {}).map(([index, item]) => {\n      resultUndefined.push([index, item]);\n    });\n  `);\n\n  expect(resultUndefined).toEqual([]);\n});\n"
  },
  {
    "path": "packages/react-sdk/src/collection-utils.ts",
    "content": "/**\n * Shared collection iteration logic for both builder and published sites.\n *\n * Collections support both arrays and objects:\n * - Arrays: [item1, item2] -> entries are [[\"0\", item1], [\"1\", item2]]\n * - Objects: {key1: val1, key2: val2} -> entries are [[\"key1\", val1], [\"key2\", val2]]\n *\n * Using Object.entries() unifies both cases since arrays are objects with numeric keys.\n */\n\n/**\n * Normalize collection data to entries format [key, value][].\n * Returns empty array if data is not iterable.\n * For arrays, keys are converted to numbers.\n */\nexport const getCollectionEntries = (\n  data: unknown\n): Array<[string | number, unknown]> => {\n  if (data === null || data === undefined) {\n    return [];\n  }\n  if (typeof data !== \"object\") {\n    return [];\n  }\n  const entries = Object.entries(data);\n  // Convert string indices to numbers for arrays\n  if (Array.isArray(data)) {\n    return entries.map(([key, value]) => [Number(key), value]);\n  }\n  return entries;\n};\n\n/**\n * Template for generated code that iterates over collections.\n * Used by component-generator.ts to ensure consistency.\n */\nexport const generateCollectionIterationCode = ({\n  dataExpression,\n  keyVariable,\n  itemVariable,\n}: {\n  dataExpression: string;\n  keyVariable: string;\n  itemVariable: string;\n}) => {\n  return `Object.entries(\n  // @ts-ignore\n  ${dataExpression} ?? {}\n).map(([_key, ${itemVariable}]: any) => {\n  const ${keyVariable} = Array.isArray(${dataExpression}) ? Number(_key) : _key;\n  return`;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/component-generator.test.tsx",
    "content": "import ts from \"typescript\";\nimport { expect, test } from \"vitest\";\nimport stripIndent from \"strip-indent\";\nimport {\n  createScope,\n  elementComponent,\n  ROOT_INSTANCE_ID,\n  SYSTEM_VARIABLE_ID,\n  WsComponentMeta,\n} from \"@webstudio-is/sdk\";\nimport {\n  $,\n  ActionValue,\n  AssetValue,\n  PageValue,\n  Parameter,\n  ResourceValue,\n  Variable,\n  createProxy,\n  expression,\n  renderData,\n  ws,\n} from \"@webstudio-is/template\";\nimport {\n  generateJsxChildren,\n  generateWebstudioComponent,\n} from \"./component-generator\";\n\nconst isValidJSX = (code: string): boolean => {\n  // Create a \"virtual\" TypeScript program\n  const compilerHost = ts.createCompilerHost({});\n  const fileName = \"virtual.tsx\";\n\n  compilerHost.getSourceFile = (filename) => {\n    if (filename === fileName) {\n      return ts.createSourceFile(\n        filename,\n        code,\n        ts.ScriptTarget.Latest,\n        true,\n        ts.ScriptKind.TSX\n      );\n    }\n    return;\n  };\n\n  const program = ts.createProgram(\n    [fileName],\n    {\n      jsx: ts.JsxEmit.React,\n      strict: true,\n    },\n    compilerHost\n  );\n\n  const sourceFile = program.getSourceFile(fileName);\n\n  if (!sourceFile) {\n    return false;\n  }\n\n  const diagnostics = [\n    ...program.getSyntacticDiagnostics(sourceFile),\n    // ...program.getSemanticDiagnostics(sourceFile),\n  ];\n\n  return diagnostics.length === 0;\n};\n\nconst validateJSX = (code: string) => {\n  expect(isValidJSX(code)).toBeTruthy();\n  return code;\n};\n\nconst clear = (input: string) =>\n  stripIndent(input).trimStart().replace(/ +$/, \"\");\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map((item) => [item.id, item] as const));\n\ntest(\"generate jsx element with children and without them\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"body\" }],\n      ...renderData(<$.Body ws:id=\"body\">Children</$.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Body>\n      {\"Children\"}\n      </Body>\n    `)\n    )\n  );\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"image\" }],\n      ...renderData(<$.Image ws:id=\"image\"></$.Image>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Image />\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx element with namespaces components\", () => {\n  const library = createProxy(\"@webstudio-is/library:\");\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"body\" }],\n      ...renderData(<library.Body ws:id=\"body\"></library.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Body />\n    `)\n    )\n  );\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"image\" }],\n      ...renderData(<library.Image ws:id=\"image\"></library.Image>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Image />\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx element with literal props\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"body\" }],\n      ...renderData(<$.Body ws:id=\"body\" string=\"string\" number={0}></$.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Body\n      string={\"string\"}\n      number={0} />\n    `)\n    )\n  );\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"image\" }],\n      ...renderData(\n        <$.Image\n          ws:id=\"image\"\n          boolean={true}\n          stringArray={[\"value1\", \"value2\"]}\n        ></$.Image>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Image\n      boolean={true}\n      stringArray={[\"value1\",\"value2\"]} />\n    `)\n    )\n  );\n});\n\ntest(\"ignore asset and page props\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"box\" }],\n      ...renderData(\n        <$.Box\n          ws:id=\"box\"\n          page={new PageValue(\"pageId\")}\n          asset={new AssetValue(\"assetId\")}\n        ></$.Box>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Box />\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx element with data sources and action\", () => {\n  const variable = new Variable(\"variable\", 0);\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"box\" }],\n      ...renderData(\n        <$.Box\n          ws:id=\"box\"\n          variable={expression`${variable}`}\n          expression={expression`${variable} + 1`}\n          onChange={new ActionValue([\"value\"], expression`${variable} = value`)}\n        ></$.Box>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Box\n      variable={variable}\n      expression={variable + 1}\n      onChange={(value: any) => {\n      variable = value\n      set$variable(variable)\n      }} />\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx element with condition based on show prop\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"box\" }],\n      ...renderData(<$.Box ws:id=\"box\" data-ws-show={true}></$.Box>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      <Box />\n    `)\n    )\n  );\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"box\" }],\n      ...renderData(<$.Box ws:id=\"box\" data-ws-show={false}></$.Box>),\n    })\n  ).toEqual(\"\");\n  const condition = new Variable(\"condition\", false);\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"box\" }],\n      ...renderData(\n        <$.Box ws:id=\"box\" data-ws-show={expression`${condition}`}></$.Box>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      {(condition) &&\n      <Box />\n      }\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx children with text\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [\n        { type: \"text\", value: \"Some\\ntext\" },\n        { type: \"text\", value: 'Escaped \"text\"' },\n      ],\n      instances: new Map(),\n      props: new Map(),\n      dataSources: new Map(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      {\"Some\"}\n      <br />\n      {\"text\"}\n      {\"Escaped \\\\\"text\\\\\"\"}\n    `)\n    )\n  );\n});\n\ntest(\"exclude text placeholders\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [\n        { type: \"text\", value: \"Text\" },\n        { type: \"text\", value: \"Placeholder text\", placeholder: true },\n      ],\n      instances: new Map(),\n      props: new Map(),\n      dataSources: new Map(),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      excludePlaceholders: true,\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      {\"Text\"}\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx children with expression\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [\n        { type: \"expression\", value: \"'Hello ' + $ws$dataSource$var\" },\n      ],\n      instances: new Map(),\n      props: new Map(),\n      dataSources: toMap([\n        {\n          id: \"var\",\n          scopeInstanceId: \"body\",\n          name: \"my var\",\n          type: \"variable\",\n          value: { type: \"string\", value: \"world\" },\n        },\n      ]),\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      {'Hello ' + myvar}\n    `)\n    )\n  );\n});\n\ntest(\"generate jsx children with nested instances\", () => {\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"form\" }],\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      ...renderData(\n        <$.Form ws:id=\"form\" prop=\"value\">\n          <$.Input></$.Input>\n          <$.Button></$.Button>\n        </$.Form>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n    <Form\n    prop={\"value\"}>\n    <Input />\n    <Button />\n    </Form>\n    `)\n    )\n  );\n});\n\ntest(\"deduplicate base and namespaced components with same short name\", () => {\n  const radix = createProxy(\"@webstudio-is/sdk-component-react-radix:\");\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [\n        { type: \"id\", value: \"button1\" },\n        { type: \"id\", value: \"button2\" },\n      ],\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      ...renderData(\n        <$.Fragment>\n          <$.Button ws:id=\"button1\"></$.Button>\n          <radix.Button ws:id=\"button2\"></radix.Button>\n        </$.Fragment>\n      ),\n    })\n  ).toEqual(\n    clear(`\n    <Button />\n    <Button_1 />\n    `)\n  );\n});\n\ntest(\"generate collection component as map\", () => {\n  const data = new Variable(\"data\", [\"apple\", \"orange\", \"mango\"]);\n  const element = new Parameter(\"element\");\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"list\" }],\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      ...renderData(\n        <ws.collection ws:id=\"list\" data={expression`${data}`} item={element}>\n          <$.Label></$.Label>\n          <$.Button aria-label={expression`${element}`}></$.Button>\n        </ws.collection>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n    {Object.entries(\n      // @ts-ignore\n      data ?? {}\n    ).map(([_key, element]: any) => {\n      const index = Array.isArray(data) ? Number(_key) : _key;\n      return (\n    <Fragment key={index}>\n    <Label />\n    <Button\n    aria-label={element} />\n    </Fragment>\n    )\n    })\n    }\n    `)\n    )\n  );\n});\n\ntest(\"generate collection component with itemKey\", () => {\n  const data = new Variable(\"data\", { a: \"apple\", b: \"orange\" });\n  const element = new Parameter(\"element\");\n  const key = new Parameter(\"key\");\n  expect(\n    generateJsxChildren({\n      scope: createScope(),\n      metas: new Map(),\n      children: [{ type: \"id\", value: \"list\" }],\n      usedDataSources: new Map(),\n      indexesWithinAncestors: new Map(),\n      ...renderData(\n        <ws.collection\n          ws:id=\"list\"\n          data={expression`${data}`}\n          item={element}\n          itemKey={key}\n        >\n          <$.Label>{expression`${key}`}</$.Label>\n          <$.Button aria-label={expression`${element}`}></$.Button>\n        </ws.collection>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n    {Object.entries(\n      // @ts-ignore\n      data ?? {}\n    ).map(([_key, element]: any) => {\n      const key = Array.isArray(data) ? Number(_key) : _key;\n      return (\n    <Fragment key={key}>\n    <Label>\n    {key}\n    </Label>\n    <Button\n    aria-label={element} />\n    </Fragment>\n    )\n    })\n    }\n    `)\n    )\n  );\n});\n\ntest(\"generate component with variables and actions\", () => {\n  const variable = new Variable(\"variable\", \"initial\");\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <$.Input\n            value={expression`${variable}`}\n            onChange={\n              new ActionValue([\"value\"], expression`${variable} = value`)\n            }\n          />\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      let [variable, set$variable] = useVariableState<any>(\"initial\")\n      return <Body>\n      <Input\n      value={variable}\n      onChange={(value: any) => {\n      variable = value\n      set$variable(variable)\n      }} />\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"merge classes if no className\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map([[\"body\", [\"cls1\"]]]),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(<$.Body ws:id=\"body\"></$.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n        const Page = () => {\n        return <Body\n        className={\\`cls1\\`} />\n        }\n    `)\n    )\n  );\n});\n\ntest(\"add classes and merge classes\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map([[\"body\", [\"cls1\"]]]),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(<$.Body ws:id=\"body\" className='cls2 \"cls3\"'></$.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n        const Page = () => {\n        return <Body\n        className={\\`cls1 \\${\"cls2 \\\\\"cls3\\\\\"\"}\\`} />\n        }\n    `)\n    )\n  );\n});\n\ntest(\"add classes\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(<$.Body ws:id=\"body\" className='cls2 \"cls3\"'></$.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n        const Page = () => {\n        return <Body\n        className={\\`\\${\"cls2 \\\\\"cls3\\\\\"\"}\\`} />\n        }\n    `)\n    )\n  );\n});\n\ntest(\"add bind classes and merge classes\", () => {\n  const hasClass2 = new Variable(\"variableName\", false);\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map([[\"body\", [\"cls1\"]]]),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body\n          ws:id=\"body\"\n          className={expression`${hasClass2} ? 'cls2' : ''`}\n        ></$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n        const Page = () => {\n        let [variableName, set$variableName] = useVariableState<any>(false)\n        return <Body\n        className={\\`cls1 \\${variableName ? 'cls2' : ''}\\`} />\n        }\n    `)\n    )\n  );\n});\n\ntest(\"avoid generating collection parameter variable as state\", () => {\n  const data = new Variable(\"data\", [\"apple\", \"orange\", \"mango\"]);\n  const element = new Parameter(\"element\");\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <ws.collection\n            ws:id=\"list\"\n            data={expression`${data}`}\n            item={element}\n          ></ws.collection>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n    const Page = () => {\n    let [data, set$data] = useVariableState<any>([\"apple\",\"orange\",\"mango\"])\n    return <Body>\n    {Object.entries(\n      // @ts-ignore\n      data ?? {}\n    ).map(([_key, element]: any) => {\n      const index = Array.isArray(data) ? Number(_key) : _key;\n      return (\n    <Fragment key={index}>\n    </Fragment>\n    )\n    })\n    }\n    </Body>\n    }\n    `)\n    )\n  );\n});\n\ntest(\"generate both page system and global system variables when present\", () => {\n  const system = new Parameter(\"system\");\n  const data = renderData(\n    <$.Body\n      ws:id=\"body\"\n      data-page={expression`${system}.params.slug`}\n      data-global={expression`$ws$system.params.slug`}\n    ></$.Body>\n  );\n  expect(data.dataSources.size).toEqual(1);\n  const [pageSystemVariableId] = data.dataSources.keys();\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope([\"system\"]),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [\n        {\n          id: \"pathSystemPropId1\",\n          type: \"parameter\",\n          instanceId: \"\",\n          name: \"system\",\n          value: pageSystemVariableId,\n        },\n        {\n          id: \"pathSystemPropId2\",\n          type: \"parameter\",\n          instanceId: \"\",\n          name: \"system\",\n          value: SYSTEM_VARIABLE_ID,\n        },\n      ],\n      metas: new Map(),\n      ...data,\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n    const Page = (_props: { system: any; }) => {\n    const system_1 = _props.system;\n    const system_2 = _props.system;\n    return <Body\n    data-page={system_1?.params?.slug}\n    data-global={system_2?.params?.slug} />\n    }\n    `)\n    )\n  );\n});\n\ntest(\"generate resources loading\", () => {\n  const dataVariable = new Variable(\"data\", \"data\");\n  const dataResource = new ResourceValue(\"data\", {\n    url: expression`\"\"`,\n    method: \"get\",\n    searchParams: [],\n    headers: [],\n  });\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body\n          ws:id=\"body\"\n          data-data={expression`${dataVariable}`}\n          data-resource={expression`${dataResource}`}\n        ></$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n    const Page = () => {\n    let [data, set$data] = useVariableState<any>(\"data\")\n    let data_1 = useResource(\"data_2\")\n    return <Body\n    data-data={data}\n    data-resource={data_1} />\n    }\n    `)\n    )\n  );\n});\n\ntest(\"avoid generating unused variables\", () => {\n  const usedVariable = new Variable(\"Used Variable Name\", \"initial\");\n  const unusedVariable = new Variable(\"Unused Variable Name\", \"initial\");\n  const unusedParameter = new Parameter(\"Unused Parameter Name\");\n  const unusedResource = new ResourceValue(\"Unused Resource Name\", {\n    url: expression`\"\"`,\n    method: \"get\",\n    searchParams: [],\n    headers: [],\n  });\n  const data = renderData(\n    <$.Body\n      ws:id=\"body\"\n      data-used={expression`${usedVariable}`}\n      data-unused={expression`${unusedVariable} ${unusedParameter} ${unusedResource}`}\n    ></$.Body>\n  );\n  expect(Array.from(data.props.values())).toEqual([\n    expect.objectContaining({ name: \"data-used\" }),\n    expect.objectContaining({ name: \"data-unused\" }),\n  ]);\n  // make variables unused\n  data.props.delete(Array.from(data.props.values())[1].id);\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [\n        {\n          id: \"systemPropId\",\n          type: \"parameter\",\n          instanceId: \"\",\n          name: \"system\",\n          value: \"unusedParameterId\",\n        },\n      ],\n      metas: new Map(),\n      ...data,\n    })\n  ).toMatchInlineSnapshot(`\n\"const Page = (_props: { system: any; }) => {\nlet [UsedVariableName, set$UsedVariableName] = useVariableState<any>(\"initial\")\nreturn <Body\ndata-used={UsedVariableName} />\n}\n\"\n`);\n});\n\ntest(\"avoid generating descendant component\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <ws.descendant></ws.descendant>\n        </$.Body>\n      ),\n    })\n  ).toMatchInlineSnapshot(`\n\"const Page = () => {\nreturn <Body>\n</Body>\n}\n\"\n`);\n});\n\ntest(\"generate conditional collection\", () => {\n  const condition = new Variable(\"condition\", false);\n  const collectionItem = new Parameter(\"collectionItem\");\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <ws.collection\n            ws:id=\"list\"\n            data-ws-show={expression`${condition}`}\n            data={[]}\n            item={collectionItem}\n          ></ws.collection>\n        </$.Body>\n      ),\n    })\n  ).toMatchInlineSnapshot(`\n    \"const Page = () => {\n    let [condition, set$condition] = useVariableState<any>(false)\n    return <Body>\n    {(condition) &&\n    <>\n    {Object.entries(\n      // @ts-ignore\n      [] ?? {}\n    ).map(([_key, collectionItem]: any) => {\n      const index = Array.isArray([]) ? Number(_key) : _key;\n      return (\n    <Fragment key={index}>\n    </Fragment>\n    )\n    })\n    }\n    </>\n    }\n    </Body>\n    }\n    \"\n  `);\n});\n\ntest(\"generate conditional body\", () => {\n  const condition = new Variable(\"condition\", false);\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\" data-ws-show={expression`${condition}`}></$.Body>\n      ),\n    })\n  ).toMatchInlineSnapshot(`\n\"const Page = () => {\nlet [condition, set$condition] = useVariableState<any>(false)\nreturn (condition) &&\n<Body />\n\n}\n\"\n`);\n});\n\ntest(\"generate resource prop\", () => {\n  const myResource = new ResourceValue(\"myResource\", {\n    url: expression`\"https://my-url.com?with-secret\"`,\n    method: \"get\",\n    searchParams: [],\n    headers: [],\n  });\n  const anotherResource = new ResourceValue(\"anotherResource\", {\n    url: expression`\"https://another-url.com?with-secret\"`,\n    method: \"get\",\n    searchParams: [],\n    headers: [],\n  });\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <$.Form ws:id=\"form1\" action={myResource}></$.Form>\n          <$.Form ws:id=\"form2\" action={anotherResource}></$.Form>\n        </$.Body>\n      ),\n    })\n  ).toMatchInlineSnapshot(`\n    \"const Page = () => {\n    return <Body>\n    <Form\n    action={\"action\"} />\n    <Form\n    action={\"action_1\"} />\n    </Body>\n    }\n    \"\n  `);\n});\n\ntest(\"skip unsafe properties\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body\n          ws:id=\"body\"\n          {...{\n            \"\": \"unsafe\",\n            \"1-numeric-unsafe\": \"unsafe\",\n            \"click.prevent\": \"unsafe\",\n          }}\n        ></$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n        const Page = () => {\n        return <Body />\n        }\n    `)\n    )\n  );\n});\n\ntest(\"variable names can be js identifiers\", () => {\n  const variable = new Variable(\"switch\", \"initial\");\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <$.Input\n            value={expression`${variable}`}\n            onChange={\n              new ActionValue([\"value\"], expression`${variable} = value`)\n            }\n          />\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      let [switch_, set$switch] = useVariableState<any>(\"initial\")\n      return <Body>\n      <Input\n      value={switch_}\n      onChange={(value: any) => {\n      switch_ = value\n      set$switch(switch_)\n      }} />\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"renders nothing if only templates are present in block\", () => {\n  const BlockTemplate = ws[\"block-template\"];\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <ws.block ws:id=\"block\">\n            <BlockTemplate>\n              <$.Box>Test</$.Box>\n            </BlockTemplate>\n          </ws.block>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"renders only block children\", () => {\n  const BlockTemplate = ws[\"block-template\"];\n\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <ws.block ws:id=\"block\">\n            <BlockTemplate>\n              <$.Box>Test</$.Box>\n            </BlockTemplate>\n            <$.Box>Child0</$.Box>\n          </ws.block>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      <Box>\n      {\"Child0\"}\n      </Box>\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"generate unset variables as undefined\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <$.Box>{expression`a + b`}</$.Box>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      <Box>\n      {undefined + undefined}\n      </Box>\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"generate global variables\", () => {\n  const rootVariable = new Variable(\"rootVariable\", \"root\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${rootVariable}`}>\n      <$.Body ws:id=\"body\">\n        <$.Box>{expression`${rootVariable}`}</$.Box>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...data,\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      let [rootVariable, set$rootVariable] = useVariableState<any>(\"root\")\n      return <Body>\n      <Box>\n      {rootVariable}\n      </Box>\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"ignore unused global variables\", () => {\n  const rootVariable = new Variable(\"rootVariable\", \"root\");\n  const data = renderData(\n    <ws.root ws:id={ROOT_INSTANCE_ID} vars={expression`${rootVariable}`}>\n      <$.Body ws:id=\"body\">\n        <$.Box></$.Box>\n      </$.Body>\n    </ws.root>\n  );\n  data.instances.delete(ROOT_INSTANCE_ID);\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map(),\n      ...data,\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      <Box />\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"generate prop with index within ancestor\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"body\",\n      parameters: [],\n      metas: new Map<string, WsComponentMeta>([\n        [\"TabsTrigger\", { indexWithinAncestor: \"Tabs\" }],\n        [\"TabsContent\", { indexWithinAncestor: \"Tabs\" }],\n      ]),\n      ...renderData(\n        <$.Body ws:id=\"body\">\n          <$.Tabs>\n            <$.TabsList>\n              <$.TabsTrigger></$.TabsTrigger>\n              <$.Box>\n                <$.TabsTrigger></$.TabsTrigger>\n              </$.Box>\n            </$.TabsList>\n            <$.Box>\n              <$.TabsContent></$.TabsContent>\n            </$.Box>\n            <$.TabsContent></$.TabsContent>\n          </$.Tabs>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      <Tabs>\n      <TabsList>\n      <TabsTrigger\n      data-ws-index=\"0\" />\n      <Box>\n      <TabsTrigger\n      data-ws-index=\"1\" />\n      </Box>\n      </TabsList>\n      <Box>\n      <TabsContent\n      data-ws-index=\"0\" />\n      </Box>\n      <TabsContent\n      data-ws-index=\"1\" />\n      </Tabs>\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"ignore ws:block-template when generate index attribute\", () => {\n  const BlockTemplate = ws[\"block-template\"];\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map<string, WsComponentMeta>([\n        [\"TabsTrigger\", { indexWithinAncestor: \"Tabs\" }],\n      ]),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Tabs>\n            <BlockTemplate>\n              <$.TabsTrigger></$.TabsTrigger>\n            </BlockTemplate>\n            <$.Box>\n              <$.TabsTrigger></$.TabsTrigger>\n            </$.Box>\n            <$.TabsTrigger></$.TabsTrigger>\n          </$.Tabs>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      <Tabs>\n      <Box>\n      <TabsTrigger\n      data-ws-index=\"0\" />\n      </Box>\n      <TabsTrigger\n      data-ws-index=\"1\" />\n      </Tabs>\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"render empty component when no instances found\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(<$.Body ws:id=\"bodyId\"></$.Body>),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <></>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"render tag property on components\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Box ws:id=\"spanId\" ws:tag=\"span\"></$.Box>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n      const Page = () => {\n      return <Body>\n      <Box\n      data-ws-tag=\"span\" />\n      </Body>\n      }\n    `)\n    )\n  );\n});\n\ntest(\"render ws:element component with div tag by default\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element id=\"element1\">\n            <ws.element id=\"element2\"></ws.element>\n          </ws.element>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <div\n       id={\"element1\"}>\n       <div\n       id={\"element2\"} />\n       </div>\n       </Body>\n       }\n     `)\n    )\n  );\n});\n\ntest(\"render ws:element component with ws:tag\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map(),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element ws:tag=\"p\" id=\"paragraph\">\n            <ws.element ws:tag=\"span\" id=\"span\"></ws.element>\n          </ws.element>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <p\n       id={\"paragraph\"}>\n       <span\n       id={\"span\"} />\n       </p>\n       </Body>\n       }\n     `)\n    )\n  );\n});\n\ntest(\"convert attributes to react compatible when render ws:element\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map([[elementComponent, { presetStyle: { div: [] } }]]),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <ws.element\n            class=\"my-class\"\n            for=\"my-id\"\n            autocomplete=\"off\"\n          ></ws.element>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <div\n       htmlFor={\"my-id\"}\n       autoComplete={\"off\"}\n       className={\\`\\${\"my-class\"}\\`} />\n       </Body>\n       }\n     `)\n    )\n  );\n});\n\ntest(\"convert attributes to react compatible when render components with tags\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map([[\"Box\", { presetStyle: { div: [] } }]]),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Box class=\"my-class\" for=\"my-id\" autocomplete=\"off\"></$.Box>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <Box\n       htmlFor={\"my-id\"}\n       autoComplete={\"off\"}\n       className={\\`\\${\"my-class\"}\\`} />\n       </Body>\n       }\n     `)\n    )\n  );\n});\n\ntest(\"ignore props similar to standard attributes when react components defines them\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map([\n        [\n          \"Vimeo\",\n          {\n            presetStyle: { div: [] },\n            props: {\n              autoplay: { type: \"boolean\", control: \"boolean\", required: true },\n            },\n          },\n        ],\n      ]),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.Vimeo autoplay={true}></$.Vimeo>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <Vimeo\n       autoplay={true} />\n       </Body>\n       }\n     `)\n    )\n  );\n});\n\ntest(\"ignore props similar to standard attributes on react components without tags\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map([[\"HeadSlot\", {}]]),\n      ...renderData(\n        <$.Body ws:id=\"bodyId\">\n          <$.HeadSlot\n            class=\"my-class\"\n            for=\"my-id\"\n            autocomplete=\"off\"\n          ></$.HeadSlot>\n        </$.Body>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <HeadSlot\n       class={\"my-class\"}\n       for={\"my-id\"}\n       autocomplete={\"off\"} />\n       </Body>\n       }\n     `)\n    )\n  );\n});\n\ntest(\"overrides some element tags with provided components\", () => {\n  expect(\n    generateWebstudioComponent({\n      classesMap: new Map(),\n      scope: createScope(),\n      name: \"Page\",\n      rootInstanceId: \"bodyId\",\n      parameters: [],\n      metas: new Map([[\"HeadSlot\", { icon: \"\" }]]),\n      tagsOverrides: {\n        body: \"namespace:Body\",\n        a: \"namespace:Link\",\n      },\n      ...renderData(\n        <ws.element ws:tag=\"body\" ws:id=\"bodyId\">\n          <ws.element ws:tag=\"a\"></ws.element>\n          <ws.element ws:tag=\"div\"></ws.element>\n        </ws.element>\n      ),\n    })\n  ).toEqual(\n    validateJSX(\n      clear(`\n       const Page = () => {\n       return <Body>\n       <Link />\n       <div />\n       </Body>\n       }\n     `)\n    )\n  );\n});\n"
  },
  {
    "path": "packages/react-sdk/src/component-generator.ts",
    "content": "import type {\n  Instances,\n  Instance,\n  Props,\n  Scope,\n  DataSources,\n  Prop,\n  DataSource,\n  WsComponentMeta,\n  IndexesWithinAncestors,\n} from \"@webstudio-is/sdk\";\nimport {\n  parseComponentName,\n  generateExpression,\n  decodeDataSourceVariable,\n  transpileExpression,\n  blockComponent,\n  blockTemplateComponent,\n  collectionComponent,\n  descendantComponent,\n  getIndexesWithinAncestors,\n  elementComponent,\n} from \"@webstudio-is/sdk\";\nimport { indexProperty, tagProperty } from \"@webstudio-is/sdk/runtime\";\nimport { isAttributeNameSafe, showAttribute } from \"./props\";\nimport { standardAttributesToReactProps } from \"./__generated__/standard-attributes\";\nimport { generateCollectionIterationCode } from \"./collection-utils\";\n\n/**\n * (arg1) => {\n * myVar = myVar + arg1\n * set$myVar(myVar)\n * }\n */\nconst generateAction = ({\n  scope,\n  prop,\n  dataSources,\n  usedDataSources,\n}: {\n  scope: Scope;\n  prop: Extract<Prop, { type: \"action\" }>;\n  dataSources: DataSources;\n  usedDataSources: DataSources;\n}) => {\n  const setters = new Set<DataSource>();\n  // important to fallback to empty argumets to render empty function\n  let args: string[] = [];\n  let assignersCode = \"\";\n  for (const value of prop.value) {\n    args = value.args;\n    assignersCode += transpileExpression({\n      expression: value.code,\n      executable: true,\n      replaceVariable: (identifier, assignee) => {\n        if (args?.includes(identifier)) {\n          return;\n        }\n        const depId = decodeDataSourceVariable(identifier);\n        const dep = depId ? dataSources.get(depId) : undefined;\n        if (dep) {\n          usedDataSources.set(dep.id, dep);\n          if (assignee) {\n            setters.add(dep);\n          }\n          const valueName = scope.getName(dep.id, dep.name);\n          return valueName;\n        }\n\n        console.error(`Unknown dependency \"${identifier}\"`);\n      },\n    });\n    assignersCode += `\\n`;\n  }\n  let settersCode = \"\";\n  for (const dataSource of setters) {\n    const valueName = scope.getName(dataSource.id, dataSource.name);\n    const setterName = scope.getName(\n      `set$${dataSource.id}`,\n      `set$${dataSource.name}`\n    );\n    settersCode += `${setterName}(${valueName})\\n`;\n  }\n  const argsList = args.map((arg) => `${arg}: any`).join(\", \");\n  let generated = \"\";\n  generated += `(${argsList}) => {\\n`;\n  generated += assignersCode;\n  generated += settersCode;\n  generated += `}`;\n  return generated;\n};\n\nconst generatePropValue = ({\n  scope,\n  prop,\n  dataSources,\n  usedDataSources,\n}: {\n  scope: Scope;\n  prop: Prop;\n  dataSources: DataSources;\n  usedDataSources: DataSources;\n}) => {\n  // ignore asset and page props which are handled by components internally\n  if (prop.type === \"asset\" || prop.type === \"page\") {\n    return;\n  }\n  if (\n    prop.type === \"string\" ||\n    prop.type === \"number\" ||\n    prop.type === \"boolean\" ||\n    prop.type === \"string[]\" ||\n    prop.type === \"json\" ||\n    prop.type === \"animationAction\"\n  ) {\n    return JSON.stringify(prop.value);\n  }\n  // generate variable name for parameter\n  if (prop.type === \"parameter\") {\n    const dataSource = dataSources.get(prop.value);\n    if (dataSource === undefined) {\n      return;\n    }\n    usedDataSources.set(dataSource.id, dataSource);\n    return scope.getName(dataSource.id, dataSource.name);\n  }\n  // inline expression to safely use collection item\n  if (prop.type === \"expression\") {\n    return generateExpression({\n      expression: prop.value,\n      dataSources,\n      usedDataSources,\n      scope,\n    });\n  }\n  if (prop.type === \"action\") {\n    return generateAction({ scope, prop, dataSources, usedDataSources });\n  }\n  if (prop.type === \"resource\") {\n    return JSON.stringify(scope.getName(prop.value, prop.name));\n  }\n  prop satisfies never;\n};\n\nexport const generateJsxElement = ({\n  context = \"jsx\",\n  scope,\n  metas,\n  tagsOverrides,\n  instance,\n  props,\n  dataSources,\n  usedDataSources,\n  indexesWithinAncestors,\n  children,\n  classesMap,\n}: {\n  context?: \"expression\" | \"jsx\";\n  scope: Scope;\n  metas: Map<Instance[\"component\"], WsComponentMeta>;\n  /**\n   * Record<tag, componentDescriptor>\n   */\n  tagsOverrides?: Record<string, string>;\n  instance: Instance;\n  props: Props;\n  dataSources: DataSources;\n  usedDataSources: DataSources;\n  indexesWithinAncestors: IndexesWithinAncestors;\n  children: string;\n  classesMap?: Map<string, Array<string>>;\n}) => {\n  // descendant component is used only for styling\n  // and should not be rendered\n  if (instance.component === descendantComponent) {\n    return \"\";\n  }\n\n  const meta = metas.get(instance.component);\n  const hasTags = Object.keys(meta?.presetStyle ?? {}).length > 0;\n\n  let generatedProps = \"\";\n\n  const index = indexesWithinAncestors.get(instance.id);\n  if (index !== undefined) {\n    generatedProps += `\\n${indexProperty}=\"${index}\"`;\n  }\n  if (instance.tag !== undefined && instance.component !== elementComponent) {\n    generatedProps += `\\n${tagProperty}=${JSON.stringify(instance.tag)}`;\n  }\n\n  let conditionValue: undefined | string;\n  let collectionDataValue: undefined | string;\n  let collectionItemValue: undefined | string;\n  let collectionItemKeyValue: undefined | string;\n  let classNameValue: undefined | string;\n\n  for (const prop of props.values()) {\n    if (prop.instanceId !== instance.id) {\n      continue;\n    }\n\n    const propValue = generatePropValue({\n      scope,\n      prop,\n      dataSources,\n      usedDataSources,\n    });\n\n    if (isAttributeNameSafe(prop.name) === false) {\n      continue;\n    }\n    let name = prop.name;\n    // convert html attribute only when component has tags\n    // and does not specify own property with this name\n    if (hasTags && !meta?.props?.[prop.name]) {\n      name = standardAttributesToReactProps[prop.name] ?? prop.name;\n    }\n\n    // show prop controls conditional rendering and need to be handled separately\n    if (prop.name === showAttribute) {\n      // prevent generating unnecessary condition\n      if (propValue === \"true\") {\n        continue;\n      }\n      // prevent instance rendering when always hidden\n      if (propValue === \"false\") {\n        return \"\";\n      }\n      conditionValue = propValue;\n      // generate separately\n      continue;\n    }\n    if (instance.component === collectionComponent) {\n      if (prop.name === \"data\") {\n        collectionDataValue = propValue;\n      }\n      if (prop.name === \"item\") {\n        collectionItemValue = propValue;\n      }\n      if (prop.name === \"itemKey\") {\n        collectionItemKeyValue = propValue;\n      }\n      continue;\n    }\n    // We need to merge atomic classes with user-defined className prop.\n    if (name === \"className\" && propValue !== undefined) {\n      classNameValue = propValue;\n      continue;\n    }\n    if (propValue !== undefined) {\n      generatedProps += `\\n${name}={${propValue}}`;\n    }\n  }\n\n  const classMapArray = classesMap?.get(instance.id);\n  if (classMapArray || classNameValue) {\n    let classNameTemplate = classMapArray ? classMapArray.join(\" \") : \"\";\n    if (classNameValue) {\n      if (classNameTemplate) {\n        classNameTemplate += \" \";\n      }\n      classNameTemplate += \"${\" + classNameValue + \"}\";\n    }\n    // wrap class expression with template literal to properly group\n    // for exaple expressions\n    generatedProps += \"\\nclassName={`\" + classNameTemplate + \"`}\";\n  }\n\n  let generatedElement = \"\";\n  if (instance.component === blockTemplateComponent) {\n    return \"\";\n  }\n\n  if (instance.component === collectionComponent) {\n    // prevent generating invalid collection\n    if (\n      collectionDataValue === undefined ||\n      collectionItemValue === undefined\n    ) {\n      return \"\";\n    }\n    const indexVariable = scope.getName(`${instance.id}-index`, \"index\");\n    // use itemKey prop if provided, otherwise use generated index variable\n    const keyVariable = collectionItemKeyValue ?? indexVariable;\n    // collection can be nullable or invalid type\n    // fix implicitly on published sites\n    // support both arrays and objects with Object.entries\n    generatedElement += `{${generateCollectionIterationCode({\n      dataExpression: collectionDataValue,\n      keyVariable,\n      itemVariable: collectionItemValue,\n    })} (\\n`;\n    generatedElement += `<Fragment key={${keyVariable}}>\\n`;\n    generatedElement += children;\n    generatedElement += `</Fragment>\\n`;\n    generatedElement += `)\\n`;\n    generatedElement += `})\\n`;\n    generatedElement += `}\\n`;\n  } else if (instance.component === blockComponent) {\n    generatedElement += children;\n  } else {\n    let componentVariable;\n    if (instance.component === elementComponent) {\n      componentVariable = instance.tag ?? \"div\";\n      // replace html tag with component if available\n      const componentDescriptor = tagsOverrides?.[componentVariable];\n      if (componentDescriptor !== undefined) {\n        const [_importSource, importSpecifier] = componentDescriptor.split(\":\");\n        componentVariable = scope.getName(componentDescriptor, importSpecifier);\n      }\n    } else {\n      const [_namespace, shortName] = parseComponentName(instance.component);\n      componentVariable = scope.getName(instance.component, shortName);\n    }\n    if (instance.children.length === 0) {\n      generatedElement += `<${componentVariable}${generatedProps} />\\n`;\n    } else {\n      generatedElement += `<${componentVariable}${generatedProps}>\\n`;\n      generatedElement += children;\n      generatedElement += `</${componentVariable}>\\n`;\n    }\n  }\n\n  // coditionally render instance when show prop is data source\n  // {dataSourceVariable && <Instance>}\n  if (conditionValue) {\n    let conditionalElement = \"\";\n    let before = \"\";\n    let after = \"\";\n    if (context === \"jsx\") {\n      before = \"{\";\n      after = \"}\";\n    }\n    conditionalElement += `${before}(${conditionValue}) &&\\n`;\n    // wrap collection with fragment when rendered inside condition\n    // {dataSourceVariable &&\n    //  <>\n    //    {[].map(...)}\n    //  </>\n    // }\n    if (instance.component === collectionComponent) {\n      conditionalElement += \"<>\\n\";\n      conditionalElement += generatedElement;\n      conditionalElement += \"</>\\n\";\n    } else {\n      conditionalElement += generatedElement;\n    }\n    conditionalElement += `${after}\\n`;\n    return conditionalElement;\n  }\n\n  return generatedElement;\n};\n\nexport const generateJsxChildren = ({\n  scope,\n  metas,\n  tagsOverrides,\n  children,\n  instances,\n  props,\n  dataSources,\n  usedDataSources,\n  indexesWithinAncestors,\n  classesMap,\n  excludePlaceholders,\n}: {\n  scope: Scope;\n  metas: Map<Instance[\"component\"], WsComponentMeta>;\n  // Record<tag, componentDescriptor>\n  tagsOverrides?: Record<string, string>;\n  children: Instance[\"children\"];\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  usedDataSources: DataSources;\n  indexesWithinAncestors: IndexesWithinAncestors;\n  classesMap?: Map<string, Array<string>>;\n  excludePlaceholders?: boolean;\n}) => {\n  let generatedChildren = \"\";\n  for (const child of children) {\n    if (child.type === \"text\") {\n      if (excludePlaceholders && child.placeholder === true) {\n        continue;\n      }\n      // instance text can contain newlines\n      // convert them too <br> tag\n      generatedChildren += child.value\n        .split(\"\\n\")\n        .map((line) => `{${JSON.stringify(line)}}\\n`)\n        .join(`<br />\\n`);\n      continue;\n    }\n    if (child.type === \"expression\") {\n      const expression = generateExpression({\n        expression: child.value,\n        dataSources,\n        usedDataSources,\n        scope,\n      });\n      generatedChildren = `{${expression}}\\n`;\n      continue;\n    }\n    if (child.type === \"id\") {\n      const instanceId = child.value;\n      const instance = instances.get(instanceId);\n      if (instance === undefined) {\n        continue;\n      }\n      generatedChildren += generateJsxElement({\n        context: \"jsx\",\n        scope,\n        metas,\n        tagsOverrides,\n        instance,\n        props,\n        dataSources,\n        usedDataSources,\n        indexesWithinAncestors,\n        classesMap,\n        children: generateJsxChildren({\n          classesMap,\n          scope,\n          metas,\n          tagsOverrides,\n          children: instance.children,\n          instances,\n          props,\n          dataSources,\n          usedDataSources,\n          indexesWithinAncestors,\n          excludePlaceholders,\n        }),\n      });\n      continue;\n    }\n    child satisfies never;\n  }\n  return generatedChildren;\n};\n\nexport const generateWebstudioComponent = ({\n  scope,\n  name,\n  rootInstanceId,\n  parameters,\n  instances,\n  props,\n  dataSources,\n  metas,\n  tagsOverrides,\n  classesMap,\n}: {\n  scope: Scope;\n  name: string;\n  rootInstanceId: Instance[\"id\"];\n  parameters: Extract<Prop, { type: \"parameter\" }>[];\n  instances: Instances;\n  props: Props;\n  dataSources: DataSources;\n  classesMap: Map<string, Array<string>>;\n  metas: Map<Instance[\"component\"], WsComponentMeta>;\n  /**\n   * Record<tag, componentDescriptor>\n   */\n  tagsOverrides?: Record<string, string>;\n}) => {\n  const instance = instances.get(rootInstanceId);\n  const indexesWithinAncestors = getIndexesWithinAncestors(metas, instances, [\n    rootInstanceId,\n  ]);\n\n  const usedDataSources: DataSources = new Map();\n  let generatedJsx = \"<></>\\n\";\n  // instance can be missing when generate xml\n  if (instance) {\n    generatedJsx = generateJsxElement({\n      context: \"expression\",\n      scope,\n      metas,\n      tagsOverrides,\n      instance,\n      props,\n      dataSources,\n      usedDataSources,\n      indexesWithinAncestors,\n      classesMap,\n      children: generateJsxChildren({\n        scope,\n        metas,\n        tagsOverrides,\n        children: instance.children,\n        instances,\n        props,\n        dataSources,\n        usedDataSources,\n        indexesWithinAncestors,\n        classesMap,\n      }),\n    });\n  }\n\n  let generatedProps = \"\";\n  let generatedParameters = \"\";\n  const uniqueParameters = new Set(\n    parameters.map((parameter) => parameter.name)\n  );\n  if (parameters.length > 0) {\n    let generatedPropsType = \"\";\n    for (const parameterName of uniqueParameters) {\n      generatedPropsType += `${parameterName}: any; `;\n    }\n    generatedProps = `_props: { ${generatedPropsType}}`;\n    for (const parameter of parameters) {\n      const dataSource = usedDataSources.get(parameter.value);\n      // always generate type and avoid generating value when unused\n      if (dataSource) {\n        const valueName = scope.getName(dataSource.id, dataSource.name);\n        generatedParameters += `const ${valueName} = _props.${parameter.name};\\n`;\n      }\n    }\n  }\n\n  let generatedDataSources = \"\";\n  for (const dataSource of usedDataSources.values()) {\n    if (dataSource.type === \"variable\") {\n      const valueName = scope.getName(dataSource.id, dataSource.name);\n      const setterName = scope.getName(\n        `set$${dataSource.id}`,\n        `set$${dataSource.name}`\n      );\n      const initialValue = dataSource.value.value;\n      const initialValueString = JSON.stringify(initialValue);\n      generatedDataSources += `let [${valueName}, ${setterName}] = useVariableState<any>(${initialValueString})\\n`;\n    }\n    if (dataSource.type === \"resource\") {\n      const valueName = scope.getName(dataSource.id, dataSource.name);\n      // call resource by bound variable name\n      const resourceName = scope.getName(\n        dataSource.resourceId,\n        dataSource.name\n      );\n      // cast to any to fix accessing fields from unknown error\n      const resourceNameString = JSON.stringify(resourceName);\n      generatedDataSources += `let ${valueName} = useResource(${resourceNameString})\\n`;\n    }\n  }\n\n  let generatedComponent = \"\";\n  generatedComponent += `const ${name} = (${generatedProps}) => {\\n`;\n  generatedComponent += `${generatedParameters}`;\n  generatedComponent += `${generatedDataSources}`;\n  generatedComponent += `return ${generatedJsx}`;\n  generatedComponent += `}\\n`;\n  return generatedComponent;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/components/components-utils.ts",
    "content": "import { componentAttribute, idAttribute, selectorIdAttribute } from \"../props\";\n\nexport type AnyComponent = React.ForwardRefExoticComponent<\n  Omit<\n    React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>,\n    \"ref\"\n  > &\n    WebstudioComponentSystemProps &\n    React.RefAttributes<HTMLElement>\n>;\n\nexport type Components = Map<string, AnyComponent>;\n\nexport type WebstudioComponentSystemProps = {\n  [componentAttribute]: string;\n  [idAttribute]: string;\n  [selectorIdAttribute]: string;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/context.tsx",
    "content": "import { createContext, useContext, useMemo } from \"react\";\nimport type { ImageLoader, VideoLoader } from \"@webstudio-is/image\";\nimport {\n  createJsonStringifyProxy,\n  isPlainObject,\n} from \"@webstudio-is/sdk/runtime\";\n\nexport type Params = {\n  /**\n   * When rendering a published version, there is no renderer defined.\n   * - canvas is the builder canvas in dev mode\n   * - preview is the preview mode in builder\n   */\n  renderer?: \"canvas\" | \"preview\";\n  /**\n   * Safe mode prevents external JavaScript execution in HTML embeds.\n   * When enabled, all HTML embeds act as if \"run scripts on canvas\" is false,\n   * regardless of their actual settings.\n   */\n  isSafeMode?: boolean;\n  /**\n   * Base url or base path for any asset with ending slash.\n   * Used to load assets like fonts or images in styles\n   * Concatinated with \"name\".\n   *\n   * For example\n   * /s/uploads/\n   * /asset/file/\n   * https://assets-dev.webstudio.is/\n   * https://assets.webstudio.is/\n   */\n  assetBaseUrl: string;\n};\n\nexport const ReactSdkContext = createContext<\n  Params & {\n    imageLoader: ImageLoader;\n    videoLoader?: VideoLoader;\n    // resources need to be any to support accessing unknown fields without extra checks\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    resources: Record<string, any>;\n    breakpoints: { id: string; minWidth?: number; maxWidth?: number }[];\n    onError: (error: unknown) => void;\n  }\n>({\n  assetBaseUrl: \"/\",\n  imageLoader: ({ src }) => src,\n  videoLoader: ({ src }) => src,\n  resources: {},\n  breakpoints: [],\n  onError: (error) => {\n    console.error(error);\n  },\n});\n\nexport const useResource = (name: string) => {\n  const { resources } = useContext(ReactSdkContext);\n  const resource = resources[name];\n\n  const resourceMemozied = useMemo(\n    () =>\n      isPlainObject(resource) ? createJsonStringifyProxy(resource) : resource,\n    [resource]\n  );\n\n  return resourceMemozied;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/hook.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { getClosestInstance, type InstancePath } from \"./hook\";\n\ntest(\"get closest instance\", () => {\n  const instancePath: InstancePath = [\n    {\n      id: \"4\",\n      instanceKey: JSON.stringify([\"4\", \"3\", \"2\", \"1\", \"0\"]),\n      component: \"Content\",\n    },\n    {\n      id: \"3\",\n      instanceKey: JSON.stringify([\"3\", \"2\", \"1\", \"0\"]),\n      component: \"Tabs\",\n    },\n    {\n      id: \"2\",\n      instanceKey: JSON.stringify([\"2\", \"1\", \"0\"]),\n      component: \"Content\",\n    },\n    {\n      id: \"1\",\n      instanceKey: JSON.stringify([\"1\", \"0\"]),\n      component: \"Tabs\",\n    },\n    {\n      id: \"0\",\n      instanceKey: JSON.stringify([\"0\"]),\n      component: \"Body\",\n    },\n  ];\n  const [content2, tabs2, content1, tabs1, _body] = instancePath;\n  expect(getClosestInstance(instancePath, content2, \"Tabs\")).toBe(tabs2);\n  expect(getClosestInstance(instancePath, content1, \"Tabs\")).toBe(tabs1);\n});\n"
  },
  {
    "path": "packages/react-sdk/src/hook.ts",
    "content": "import type { IndexesWithinAncestors, Instance, Prop } from \"@webstudio-is/sdk\";\n\nexport type InstanceData = {\n  id: Instance[\"id\"];\n  instanceKey: string;\n  component: Instance[\"component\"];\n  tag?: Instance[\"tag\"];\n};\n\n/**\n * Hooks are subscriptions to builder events\n * with limited way to interact with it.\n * Called independently from components.\n */\n\nexport type HookContext = {\n  indexesWithinAncestors: IndexesWithinAncestors;\n  getPropValue: (instanceData: InstanceData, propName: Prop[\"name\"]) => unknown;\n  setMemoryProp: (\n    instanceData: InstanceData,\n    propName: Prop[\"name\"],\n    value: unknown\n  ) => void;\n};\n\nexport type InstancePath = InstanceData[];\n\ntype NavigatorEvent = {\n  /**\n   * List of instances from selected to the root\n   */\n  instancePath: InstancePath;\n};\n\nexport type Hook = {\n  onNavigatorSelect?: (context: HookContext, event: NavigatorEvent) => void;\n  onNavigatorUnselect?: (context: HookContext, event: NavigatorEvent) => void;\n};\n\n/**\n * Find closest matching instance by component name\n * by lookup in instance path\n */\nexport const getClosestInstance = (\n  instancePath: InstancePath,\n  currentInstance: InstanceData,\n  closestComponent: InstanceData[\"component\"]\n) => {\n  let matched = false;\n  for (const instance of instancePath) {\n    if (currentInstance === instance) {\n      matched = true;\n    }\n    if (matched && instance.component === closestComponent) {\n      return instance;\n    }\n  }\n};\n"
  },
  {
    "path": "packages/react-sdk/src/index.ts",
    "content": "export * from \"./remix\";\nexport * from \"./components/components-utils\";\nexport * from \"./props\";\nexport * from \"./collection-utils\";\nexport type * from \"./context\";\nexport type * from \"./hook\";\nexport {\n  generateWebstudioComponent,\n  generateJsxElement,\n  generateJsxChildren,\n} from \"./component-generator\";\nexport * from \"./__generated__/standard-attributes\";\n"
  },
  {
    "path": "packages/react-sdk/src/page-settings-canonical-link.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { isElementRenderedWithReact } from \"./page-settings-meta\";\n\ntype PageSettingsCanonicalLinkProps = {\n  href: string;\n};\n\nconst isServer = typeof window === \"undefined\";\n\n/**\n * Link canonical tag are deduplicated on the server using the HTMLRewriter interface.\n * This is not full deduplication. We simply skip rendering Page Setting link\n * if it has already been rendered using HeadSlot/HeadLink.\n * To prevent React on the client from re-adding the removed link tag, we skip rendering them client-side.\n * This approach works because React retains server-rendered link tag as long as they are not re-rendered by the client.\n *\n * The following component behavior ensures this:\n * 1. On the server: Render link tag as usual.\n * 2. On the client: Before rendering, remove any link tag with the same `name` or `property` that were not rendered by Client React,\n *    and then proceed with rendering as usual.\n */\nexport const PageSettingsCanonicalLink = (\n  props: PageSettingsCanonicalLinkProps\n) => {\n  const [localProps, setLocalProps] = useState<\n    PageSettingsCanonicalLinkProps | undefined\n  >();\n\n  useEffect(() => {\n    const selector = `head > link[rel=\"canonical\"]`;\n    let allLinks = document.querySelectorAll(selector);\n\n    for (const meta of allLinks) {\n      if (!isElementRenderedWithReact(meta)) {\n        meta.remove();\n      }\n    }\n\n    allLinks = document.querySelectorAll(selector);\n\n    if (allLinks.length === 0) {\n      setLocalProps(props);\n    }\n  }, [props]);\n\n  if (isServer) {\n    return <link rel=\"canonical\" {...props} />;\n  }\n\n  if (localProps === undefined) {\n    // This method also works during hydration because React retains server-rendered tags\n    // as long as they are not re-rendered by the client.\n    return;\n  }\n\n  return <link rel=\"canonical\" {...localProps} />;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/page-settings-meta.tsx",
    "content": "import type { ImageLoader } from \"@webstudio-is/image\";\nimport type { PageMeta } from \"@webstudio-is/sdk\";\nimport { useEffect, useState } from \"react\";\n\ntype MetaProps = {\n  property?: string;\n  name?: string;\n  content: string;\n};\n\n// Not documented\nexport const isElementRenderedWithReact = (element: Element) => {\n  return Object.keys(element).some((key) => key.startsWith(\"__react\"));\n};\n\nconst isServer = typeof window === \"undefined\";\n\n/**\n * Meta tags are deduplicated on the server using the HTMLRewriter interface.\n * This is not full deduplication. We simply skip rendering Page Setting meta\n * if it has already been rendered using HeadSlot/HeadMeta.\n * To prevent React on the client from re-adding the removed meta tags, we skip rendering them client-side.\n * This approach works because React retains server-rendered meta tags as long as they are not re-rendered by the client.\n *\n * The following component behavior ensures this:\n * 1. On the server: Render meta tags as usual.\n * 2. On the client: Before rendering, remove any meta tags with the same `name` or `property` that were not rendered by Client React,\n *    and then proceed with rendering as usual.\n */\nconst Meta = (props: MetaProps) => {\n  const [localProps, setLocalProps] = useState<MetaProps | undefined>();\n\n  useEffect(() => {\n    const selector = `meta[${props.name ? `name=\"${props.name}\"` : `property=\"${props.property}\"`}]`;\n    let allMetas = document.querySelectorAll(selector);\n\n    for (const meta of allMetas) {\n      if (!isElementRenderedWithReact(meta)) {\n        meta.remove();\n      }\n    }\n\n    allMetas = document.querySelectorAll(selector);\n\n    if (allMetas.length === 0) {\n      setLocalProps(props);\n    }\n  }, [props]);\n\n  if (isServer) {\n    return <meta {...props} />;\n  }\n\n  if (localProps === undefined) {\n    // This method also works during hydration because React retains server-rendered tags\n    // as long as they are not re-rendered by the client.\n    return;\n  }\n\n  return <meta {...localProps} />;\n};\n\nexport const PageSettingsMeta = ({\n  url,\n  host,\n  siteName,\n  pageMeta,\n  imageLoader,\n  assetBaseUrl,\n}: {\n  url?: string;\n  host?: string;\n  siteName?: string;\n  pageMeta: PageMeta;\n  imageLoader: ImageLoader;\n  assetBaseUrl: string;\n}) => {\n  const metas: // | { title: string }\n  { property?: string; name?: string; content: string }[] = [];\n\n  if (url !== undefined) {\n    metas.push({\n      property: \"og:url\",\n      content: url,\n    });\n  }\n\n  if (pageMeta.title) {\n    metas.push({\n      property: \"og:title\",\n      content: pageMeta.title,\n    });\n  }\n\n  metas.push({ property: \"og:type\", content: \"website\" });\n\n  if (siteName) {\n    metas.push({\n      property: \"og:site_name\",\n      content: siteName,\n    });\n  }\n\n  if (pageMeta.excludePageFromSearch) {\n    metas.push({\n      name: \"robots\",\n      content: \"noindex, nofollow\",\n    });\n  }\n\n  if (pageMeta.description) {\n    metas.push({\n      name: \"description\",\n      content: pageMeta.description,\n    });\n    metas.push({\n      property: \"og:description\",\n      content: pageMeta.description,\n    });\n  }\n\n  if (pageMeta.socialImageAssetName) {\n    const origin = host ? `https://${host}` : \"\";\n    metas.push({\n      property: \"og:image\",\n      content: `${origin}${imageLoader({\n        src: `${assetBaseUrl}${pageMeta.socialImageAssetName}`,\n        // Do not transform social image (not enough information do we need to do this)\n        format: \"raw\",\n      })}`,\n    });\n  } else if (pageMeta.socialImageUrl) {\n    metas.push({\n      property: \"og:image\",\n      content: pageMeta.socialImageUrl,\n    });\n  }\n\n  metas.push(...pageMeta.custom);\n\n  const isTwitterCardSizeDefined = pageMeta.custom.some(\n    (meta) => meta.property === \"twitter:card\"\n  );\n  if (\n    (pageMeta.socialImageAssetName !== undefined ||\n      pageMeta.socialImageUrl !== undefined) &&\n    isTwitterCardSizeDefined === false\n  ) {\n    metas.push({ property: \"twitter:card\", content: \"summary_large_image\" });\n  }\n\n  return metas.map((meta, index) => <Meta key={index} {...meta} />);\n};\n"
  },
  {
    "path": "packages/react-sdk/src/page-settings-title.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { isElementRenderedWithReact } from \"./page-settings-meta\";\n\ntype PageSettingsTitleProps = {\n  children: string;\n};\n\nconst isServer = typeof window === \"undefined\";\n\n/**\n * Title tags are deduplicated on the server using the HTMLRewriter interface.\n * This is not full deduplication. We simply skip rendering Page Setting title\n * if it has already been rendered using HeadSlot/HeadTitle.\n * To prevent React on the client from re-adding the removed title tag, we skip rendering them client-side.\n * This approach works because React retains server-rendered title tag as long as they are not re-rendered by the client.\n *\n * The following component behavior ensures this:\n * 1. On the server: Render title tag as usual.\n * 2. On the client: Before rendering, remove any title tag with the same `name` or `property` that were not rendered by Client React,\n *    and then proceed with rendering as usual.\n */\nexport const PageSettingsTitle = (props: PageSettingsTitleProps) => {\n  const [localProps, setLocalProps] = useState<\n    PageSettingsTitleProps | undefined\n  >();\n\n  useEffect(() => {\n    const selector = `head > title`;\n    let allTitles = document.querySelectorAll(selector);\n\n    for (const meta of allTitles) {\n      if (!isElementRenderedWithReact(meta)) {\n        meta.remove();\n      }\n    }\n\n    allTitles = document.querySelectorAll(selector);\n\n    if (allTitles.length === 0) {\n      setLocalProps(props);\n    }\n  }, [props]);\n\n  if (isServer) {\n    return <title {...props} />;\n  }\n\n  if (localProps === undefined) {\n    // This method also works during hydration because React retains server-rendered tags\n    // as long as they are not re-rendered by the client.\n    return;\n  }\n\n  return <title {...localProps} />;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/props.test.ts",
    "content": "import { test, expect, describe } from \"vitest\";\nimport type { Pages, Prop } from \"@webstudio-is/sdk\";\nimport { isAttributeNameSafe, normalizeProps } from \"./props\";\n\nconst pagesBase: Pages = {\n  meta: {},\n  homePage: {\n    id: \"home\",\n    path: \"\",\n    name: \"Home\",\n    title: \"Home\",\n    rootInstanceId: \"instance-1\",\n    meta: {},\n  },\n  pages: [],\n  folders: [\n    {\n      id: \"root\",\n      name: \"Root\",\n      slug: \"\",\n      children: [],\n    },\n  ],\n};\n\ntest(\"normalize asset prop into string\", () => {\n  expect(\n    normalizeProps({\n      props: [\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"src\",\n          type: \"asset\",\n          value: \"asset1\",\n        },\n        {\n          id: \"prop-w\",\n          instanceId: \"instance1\",\n          name: \"width\",\n          type: \"asset\",\n          value: \"asset1\",\n        },\n        {\n          id: \"prop-h\",\n          instanceId: \"instance1\",\n          name: \"height\",\n          type: \"asset\",\n          value: \"asset1\",\n        },\n      ],\n      assetBaseUrl: \"/assets/\",\n      assets: new Map([\n        [\n          \"asset1\",\n          {\n            id: \"asset1\",\n            type: \"image\",\n            name: \"my-asset.jpg\",\n            format: \"jpg\",\n            meta: { width: 101, height: 303 },\n            projectId: \"\",\n            size: 0,\n            description: \"\",\n            createdAt: \"\",\n          },\n        ],\n      ]),\n      uploadingImageAssets: [],\n      pages: pagesBase,\n      source: \"prebuild\",\n    })\n  ).toEqual([\n    {\n      id: \"prop1\",\n      instanceId: \"instance1\",\n      name: \"src\",\n      type: \"string\",\n      value: \"/assets/my-asset.jpg\",\n    },\n    {\n      id: \"prop-w\",\n      instanceId: \"instance1\",\n      name: \"width\",\n      required: undefined,\n      type: \"number\",\n      value: 101,\n    },\n    {\n      id: \"prop-h\",\n      instanceId: \"instance1\",\n      name: \"height\",\n      required: undefined,\n      type: \"number\",\n      value: 303,\n    },\n  ]);\n});\n\ntest(\"normalize asset prop into string and pass assetId on the canvas\", () => {\n  expect(\n    normalizeProps({\n      props: [\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"src\",\n          type: \"asset\",\n          value: \"asset1\",\n        },\n        {\n          id: \"prop-w\",\n          instanceId: \"instance1\",\n          name: \"width\",\n          type: \"asset\",\n          value: \"asset1\",\n        },\n        {\n          id: \"prop-h\",\n          instanceId: \"instance1\",\n          name: \"height\",\n          type: \"asset\",\n          value: \"asset1\",\n        },\n      ],\n      assetBaseUrl: \"/assets/\",\n      assets: new Map([\n        [\n          \"asset1\",\n          {\n            id: \"asset1\",\n            type: \"image\",\n            name: \"my-asset.jpg\",\n            format: \"jpg\",\n            meta: { width: 101, height: 303 },\n            projectId: \"\",\n            size: 0,\n            description: \"\",\n            createdAt: \"\",\n          },\n        ],\n      ]),\n      uploadingImageAssets: [],\n      pages: pagesBase,\n      source: \"canvas\",\n    })\n  ).toEqual([\n    {\n      id: \"prop1\",\n      instanceId: \"instance1\",\n      name: \"src\",\n      type: \"string\",\n      value: \"/assets/my-asset.jpg\",\n    },\n    {\n      id: \"instance1-asset1-assetId\",\n      instanceId: \"instance1\",\n      name: \"$webstudio$canvasOnly$assetId\",\n      required: false,\n      type: \"string\",\n      value: \"asset1\",\n    },\n    {\n      id: \"prop-w\",\n      instanceId: \"instance1\",\n      name: \"width\",\n      required: undefined,\n      type: \"number\",\n      value: 101,\n    },\n    {\n      id: \"prop-h\",\n      instanceId: \"instance1\",\n      name: \"height\",\n      required: undefined,\n      type: \"number\",\n      value: 303,\n    },\n  ]);\n});\n\ntest(\"normalize page prop with path into string\", () => {\n  expect(\n    normalizeProps({\n      props: [\n        {\n          id: \"prop1\",\n          instanceId: \"instance1\",\n          name: \"href\",\n          type: \"page\",\n          value: \"page1\",\n        },\n      ],\n      assetBaseUrl: \"\",\n      assets: new Map(),\n      uploadingImageAssets: [],\n      pages: {\n        ...pagesBase,\n        pages: [\n          {\n            id: \"page1\",\n            path: \"/page1\",\n            name: \"Page\",\n            title: \"Page\",\n            rootInstanceId: \"instance-1\",\n            meta: {},\n          },\n        ],\n        folders: [],\n      },\n      source: \"prebuild\",\n    })\n  ).toEqual([\n    {\n      id: \"prop1\",\n      instanceId: \"instance1\",\n      name: \"href\",\n      type: \"string\",\n      value: \"/page1\",\n    },\n  ]);\n});\n\ntest(\"normalize page prop with path and hash into string\", () => {\n  const idProp: Prop = {\n    id: \"prop1\",\n    instanceId: \"instance1\",\n    name: \"id\",\n    type: \"string\",\n    value: \"my anchor\",\n  };\n  const result = normalizeProps({\n    props: [\n      {\n        id: \"prop1\",\n        instanceId: \"instance1\",\n        name: \"href\",\n        type: \"page\",\n        value: {\n          pageId: \"page1\",\n          instanceId: \"instance1\",\n        },\n      },\n      idProp,\n    ],\n    assetBaseUrl: \"\",\n    assets: new Map(),\n    uploadingImageAssets: [],\n    pages: {\n      ...pagesBase,\n      pages: [\n        {\n          id: \"page1\",\n          path: \"/page1\",\n          name: \"Page\",\n          title: \"Page\",\n          rootInstanceId: \"instance-1\",\n          meta: {},\n        },\n      ],\n      folders: [\n        {\n          id: \"root\",\n          name: \"Root\",\n          slug: \"\",\n          children: [\"folder\"],\n        },\n        {\n          id: \"folder\",\n          name: \"Folder\",\n          slug: \"folder\",\n          children: [\"page1\"],\n        },\n      ],\n    },\n    source: \"prebuild\",\n  });\n  expect(result).toEqual([\n    {\n      id: \"prop1\",\n      instanceId: \"instance1\",\n      name: \"href\",\n      type: \"string\",\n      value: \"/folder/page1#my%20anchor\",\n    },\n    idProp,\n  ]);\n});\n\ndescribe(\"isAttributeNameSafe\", () => {\n  test(\"should return true for valid attribute names\", () => {\n    expect(isAttributeNameSafe(\"data-test\")).toBe(true);\n    expect(isAttributeNameSafe(\"aria-label\")).toBe(true);\n    expect(isAttributeNameSafe(\"class\")).toBe(true);\n    expect(isAttributeNameSafe(\"ns:class\")).toBe(true);\n  });\n\n  test(\"should return false for invalid attribute names\", () => {\n    expect(isAttributeNameSafe(\"123class\")).toBe(false);\n    expect(isAttributeNameSafe(\"class.name\")).toBe(false);\n    expect(isAttributeNameSafe(\":bad\")).toBe(false);\n    expect(isAttributeNameSafe(\" \")).toBe(false);\n    expect(isAttributeNameSafe(\"hello world\")).toBe(false);\n  });\n\n  test(\"should return true for cached valid attribute names\", () => {\n    isAttributeNameSafe(\"data-cached\");\n    expect(isAttributeNameSafe(\"data-cached\")).toBe(true);\n  });\n\n  test(\"should return false for cached invalid attribute names\", () => {\n    isAttributeNameSafe(\"1-invalid-cached\");\n    expect(isAttributeNameSafe(\"1-invalid-cached\")).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/react-sdk/src/props.ts",
    "content": "import {\n  type Prop,\n  type Assets,\n  type Pages,\n  type ImageAsset,\n  getPagePath,\n  findPageByIdOrPath,\n} from \"@webstudio-is/sdk\";\n\nexport const normalizeProps = ({\n  props,\n  assetBaseUrl,\n  assets,\n  uploadingImageAssets,\n  pages,\n  source,\n}: {\n  props: Prop[];\n  assetBaseUrl: string;\n  assets: Assets;\n  uploadingImageAssets: ImageAsset[];\n  pages: Pages;\n  source: \"canvas\" | \"prebuild\";\n}) => {\n  const newProps: Prop[] = [];\n  for (const prop of props) {\n    if (prop.type === \"asset\") {\n      const assetId = prop.value;\n      const asset =\n        assets.get(assetId) ??\n        uploadingImageAssets.find((asset) => asset.id === assetId);\n\n      if (asset === undefined) {\n        continue;\n      }\n\n      const propBase = {\n        id: prop.id,\n        name: prop.name,\n        required: prop.required,\n        instanceId: prop.instanceId,\n      };\n\n      if (prop.name === \"width\" && asset.type === \"image\") {\n        newProps.push({\n          ...propBase,\n          type: \"number\",\n          value: asset.meta.width,\n        });\n\n        continue;\n      }\n\n      if (prop.name === \"height\" && asset.type === \"image\") {\n        newProps.push({\n          ...propBase,\n          type: \"number\",\n          value: asset.meta.height,\n        });\n        continue;\n      }\n\n      if (prop.name === \"alt\" && asset.type === \"image\") {\n        newProps.push({\n          ...propBase,\n          type: \"string\",\n          value: asset.description ?? \"\",\n        });\n        continue;\n      }\n\n      newProps.push({\n        ...propBase,\n        type: \"string\",\n        value: `${assetBaseUrl}${asset.name}`,\n      });\n\n      if (source === \"canvas\") {\n        // use assetId as key to not recreate the image if it's switched from uploading to uploaded asset state (we don't know asset src during uploading)\n        // see Image component in sdk-components-react\n        newProps.push({\n          id: `${prop.instanceId}-${asset.id}-assetId`,\n          name: \"$webstudio$canvasOnly$assetId\",\n          required: false,\n          instanceId: prop.instanceId,\n          type: \"string\",\n          value: asset.id,\n        });\n      }\n\n      continue;\n    }\n\n    if (prop.type === \"page\") {\n      let idProp: undefined | Prop;\n      const pageId =\n        typeof prop.value === \"string\" ? prop.value : prop.value.pageId;\n      const page = findPageByIdOrPath(pageId, pages);\n\n      if (page === undefined) {\n        continue;\n      }\n      if (typeof prop.value !== \"string\") {\n        const { instanceId } = prop.value;\n        idProp = props.find(\n          (prop) => prop.instanceId === instanceId && prop.name === \"id\"\n        );\n      }\n\n      const path = getPagePath(page.id, pages);\n      const url = new URL(path, \"https://any-valid.url\");\n      let value = url.pathname;\n      if (idProp?.type === \"string\") {\n        const hash = idProp.value;\n        url.hash = encodeURIComponent(hash);\n        value = `${url.pathname}${url.hash}`;\n      }\n      newProps.push({\n        id: prop.id,\n        name: prop.name,\n        required: prop.required,\n        instanceId: prop.instanceId,\n        type: \"string\",\n        value,\n      });\n      continue;\n    }\n\n    newProps.push(prop);\n  }\n  return newProps;\n};\n\nexport const idAttribute = \"data-ws-id\" as const;\nexport const selectorIdAttribute = \"data-ws-selector\" as const;\nexport const componentAttribute = \"data-ws-component\" as const;\nexport const showAttribute = \"data-ws-show\" as const;\nexport const inflatedAttribute = \"data-ws-collapsed\" as const;\nexport const textContentAttribute = \"data-ws-text-content\" as const;\n\n/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/shared/isAttributeNameSafe.js\n */\nconst attributeNameStartChar =\n  \"A-Z_a-z\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u02FF\\\\u0370-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD\";\n// original: \":A-Z_a-z\\\\u00C0-\\\\u00D6\\\\u00D8-\\\\u00F6\\\\u00F8-\\\\u02FF\\\\u0370-\\\\u037D\\\\u037F-\\\\u1FFF\\\\u200C-\\\\u200D\\\\u2070-\\\\u218F\\\\u2C00-\\\\u2FEF\\\\u3001-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFFD\";\nconst attributeNameChar =\n  attributeNameStartChar + \":\\\\-0-9\\\\u00B7\\\\u0300-\\\\u036F\\\\u203F-\\\\u2040\";\n// original: attributeNameStartChar + \"\\\\-.0-9\\\\u00B7\\\\u0300-\\\\u036F\\\\u203F-\\\\u2040\";\n\nconst validAttributeNameRegex = new RegExp(\n  // eslint-disable-next-line no-misleading-character-class\n  \"^[\" + attributeNameStartChar + \"][\" + attributeNameChar + \"]*$\"\n);\n\nconst illegalAttributeNameCache = new Map<string, boolean>();\nconst validatedAttributeNameCache = new Map<string, boolean>();\n\nexport const isAttributeNameSafe = (attributeName: string) => {\n  if (validatedAttributeNameCache.has(attributeName)) {\n    return true;\n  }\n  if (illegalAttributeNameCache.has(attributeName)) {\n    return false;\n  }\n  if (validAttributeNameRegex.test(attributeName)) {\n    validatedAttributeNameCache.set(attributeName, true);\n    return true;\n  }\n  illegalAttributeNameCache.set(attributeName, true);\n  return false;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/remix.test.ts",
    "content": "import { expect, test, describe } from \"vitest\";\nimport { generateRemixParams, generateRemixRoute } from \"./remix\";\nimport { STATIC_PATHS } from \"@webstudio-is/sdk/router-paths.test\";\n\n/**\n * These tests use the shared test data from @webstudio-is/sdk to ensure\n * route generation is consistent with schema validation and URLPattern matching.\n */\ndescribe(\"Shared router path tests - Route generation\", () => {\n  describe(\"all valid static paths generate valid routes\", () => {\n    test.each(STATIC_PATHS)(\"generates route for: %s\", (path) => {\n      const route = generateRemixRoute(path);\n      // Should produce a non-empty route\n      expect(route).toBeTruthy();\n      // Should not throw\n      expect(typeof route).toBe(\"string\");\n    });\n  });\n});\n\ntest(\"convert home page to remix route\", () => {\n  expect(generateRemixRoute(\"\")).toEqual(\"_index\");\n  expect(generateRemixRoute(\"/\")).toEqual(\"_index\");\n});\n\ntest(\"convert path to remix route\", () => {\n  expect(generateRemixRoute(\"/blog\")).toEqual(\"[blog]._index\");\n  expect(generateRemixRoute(\"/blog/my-introduction\")).toEqual(\n    \"[blog].[my-introduction]._index\"\n  );\n});\n\ndescribe(\"non-Latin character routes\", () => {\n  // When users define redirects with non-Latin characters (e.g., Chinese, Japanese),\n  // the generateRemixRoute function must produce valid filenames.\n  // Note: The actual HTTP request will have URL-encoded paths, but React Router\n  // handles the decoding automatically when matching routes.\n\n  test(\"convert Chinese path to remix route\", () => {\n    expect(generateRemixRoute(\"/关于我们\")).toEqual(\"[关于我们]._index\");\n    expect(generateRemixRoute(\"/产品/手机\")).toEqual(\"[产品].[手机]._index\");\n  });\n\n  test(\"convert Japanese path to remix route\", () => {\n    expect(generateRemixRoute(\"/日本語\")).toEqual(\"[日本語]._index\");\n    expect(generateRemixRoute(\"/ブログ/記事\")).toEqual(\n      \"[ブログ].[記事]._index\"\n    );\n  });\n\n  test(\"convert Korean path to remix route\", () => {\n    expect(generateRemixRoute(\"/한국어\")).toEqual(\"[한국어]._index\");\n  });\n\n  test(\"convert Cyrillic path to remix route\", () => {\n    expect(generateRemixRoute(\"/привет\")).toEqual(\"[привет]._index\");\n    expect(generateRemixRoute(\"/блог/статья\")).toEqual(\n      \"[блог].[статья]._index\"\n    );\n  });\n\n  test(\"convert European diacritics to remix route\", () => {\n    expect(generateRemixRoute(\"/über-uns\")).toEqual(\"[über-uns]._index\");\n    expect(generateRemixRoute(\"/café\")).toEqual(\"[café]._index\");\n  });\n\n  test(\"convert mixed Latin and non-Latin path to remix route\", () => {\n    expect(generateRemixRoute(\"/blog/关于\")).toEqual(\"[blog].[关于]._index\");\n  });\n});\n\ntest(\"convert wildcard to remix route\", () => {\n  expect(generateRemixRoute(\"/blog/*\")).toEqual(\"[blog].$\");\n});\n\ntest(\"convert named group with * modifier to remix route\", () => {\n  expect(generateRemixRoute(\"/blog/:slug*\")).toEqual(\"[blog].$\");\n});\n\ntest(\"convert named group with ? modifier to remix route\", () => {\n  expect(generateRemixRoute(\"/:id?/:slug?\")).toEqual(\"($id).($slug)._index\");\n});\n\ntest(\"convert named groups to remix route\", () => {\n  expect(generateRemixRoute(\"/blog/:id/:date\")).toEqual(\n    \"[blog].$id.$date._index\"\n  );\n});\n\ntest(\"generate remix params for static pathname\", () => {\n  expect(\"\\n\" + generateRemixParams(\"/blog/my-post\")).toEqual(`\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  return params\n}\n`);\n});\n\ntest(\"generate remix params converter with wildcard\", () => {\n  expect(\"\\n\" + generateRemixParams(\"/blog/*\")).toEqual(`\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  params[0] = params[\"*\"]\n  delete params[\"*\"]\n  return params\n}\n`);\n});\n\ntest(\"generate remix params converter with named group and * modifier\", () => {\n  expect(\"\\n\" + generateRemixParams(\"/blog/:name*\")).toEqual(`\ntype Params = Record<string, string | undefined>;\nexport const getRemixParams = ({ ...params }: Params): Params => {\n  params[\"name\"] = params[\"*\"]\n  delete params[\"*\"]\n  return params\n}\n`);\n});\n"
  },
  {
    "path": "packages/react-sdk/src/remix.ts",
    "content": "const getRemixSegment = (segment: string) => {\n  if (segment === \"*\") {\n    return \"$\";\n  }\n  // matches following examples\n  // :name\n  // :name?\n  // :name*\n  const match = segment.match(/^:(?<name>\\w+)(?<modifier>\\*|\\?)?$/);\n  const name = match?.groups?.name;\n  const modifier = match?.groups?.modifier;\n  if (name) {\n    if (modifier === \"*\") {\n      return \"$\";\n    }\n    if (modifier === \"?\") {\n      return `($${name})`;\n    }\n    return `$${name}`;\n  }\n  return `[${segment}]`;\n};\n\n/**\n * transforms url pattern subset to remix route format\n *\n * /:name/ -> .$name. - named dynamic segment\n * /:name?/ -> .($name). - optional dynamic segment\n * /* -> .$ - splat in the end of pattern\n * /:name* -> .$ - named splat which gets specified name at runtime\n *\n */\nexport const generateRemixRoute = (pathname: string) => {\n  if (pathname.startsWith(\"/\")) {\n    pathname = pathname.slice(1);\n  }\n  if (pathname === \"\") {\n    return `_index`;\n  }\n  const base = pathname.split(\"/\").map(getRemixSegment).join(\".\");\n  const tail = pathname.endsWith(\"*\") ? \"\" : \"._index\";\n  return `${base}${tail}`;\n};\n\n/**\n * generates a function to convert remix params to compatible with url pattern groups\n *\n * for /:name* pattern\n * params[\"*\"] is replaced with params[\"name\"]\n *\n * for /* pattern\n * params[\"*\"] is replaced with params[0]\n */\nexport const generateRemixParams = (pathname: string) => {\n  const name = pathname.match(/:(?<name>\\w+)\\*$/)?.groups?.name;\n  let generated = \"\";\n  generated += `type Params = Record<string, string | undefined>;\\n`;\n  generated += `export const getRemixParams = ({ ...params }: Params): Params => {\\n`;\n  if (name) {\n    generated += `  params[\"${name}\"] = params[\"*\"]\\n`;\n    generated += `  delete params[\"*\"]\\n`;\n  }\n  if (pathname.endsWith(\"/*\")) {\n    generated += `  params[0] = params[\"*\"]\\n`;\n    generated += `  delete params[\"*\"]\\n`;\n  }\n  generated += `  return params\\n`;\n  generated += `}\\n`;\n  return generated;\n};\n"
  },
  {
    "path": "packages/react-sdk/src/runtime.ts",
    "content": "export * from \"./context\";\nexport * from \"./hook\";\nexport * from \"./variable-state\";\nexport { PageSettingsMeta } from \"./page-settings-meta\";\nexport { PageSettingsTitle } from \"./page-settings-title\";\nexport { PageSettingsCanonicalLink } from \"./page-settings-canonical-link\";\n\n/**\n * React has issues rendering certain elements, such as errors when a <link> element has children.\n * To render XML, we wrap it with an <svg> tag and add a suffix to avoid React's default behavior on these elements.\n */\nexport const xmlNodeTagSuffix =\n  \"-ws-xml-node-fb77f896-8e96-40b9-b8f8-90a4e70d724a\";\n"
  },
  {
    "path": "packages/react-sdk/src/variable-state.tsx",
    "content": "import { useState, useMemo, type Dispatch, type SetStateAction } from \"react\";\nimport {\n  createJsonStringifyProxy,\n  isPlainObject,\n} from \"@webstudio-is/sdk/runtime\";\n\nexport const useVariableState = <S,>(\n  initialState: S | (() => S)\n): [S, Dispatch<SetStateAction<S>>] => {\n  const [state, setState] = useState(initialState);\n\n  const value = useMemo(\n    () => (isPlainObject(state) ? createJsonStringifyProxy(state) : state),\n    [state]\n  );\n\n  return [value, setState];\n};\n"
  },
  {
    "path": "packages/react-sdk/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src/index.ts\", \"src/runtime.ts\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/react-sdk/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/sdk/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio project data schema\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/index.ts\",\n      \"types\": \"./lib/types/index.d.ts\",\n      \"import\": \"./lib/index.js\"\n    },\n    \"./runtime\": {\n      \"webstudio\": \"./src/runtime.ts\",\n      \"types\": \"./lib/types/runtime.d.ts\",\n      \"import\": \"./lib/runtime.js\"\n    },\n    \"./normalize.css\": {\n      \"webstudio\": \"./src/__generated__/normalize.css.ts\",\n      \"types\": \"./lib/types/__generated__/normalize.css.d.ts\",\n      \"import\": \"./lib/__generated__/normalize.css.js\"\n    },\n    \"./core-templates\": {\n      \"webstudio\": \"./src/core-templates.tsx\",\n      \"types\": \"./lib/types/core-templates.d.ts\",\n      \"import\": \"./lib/core-templates.js\"\n    },\n    \"./router-paths.test\": {\n      \"webstudio\": \"./src/router-paths.test.ts\"\n    }\n  },\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit -p tsconfig.typecheck.json\",\n    \"test\": \"vitest run\",\n    \"build:normalize.css\": \"tsx --conditions=webstudio ./scripts/normalize.css.ts && prettier --write src/__generated__/normalize.css.ts\",\n    \"build\": \"rm -rf lib && esbuild src/index.ts src/runtime.ts src/__generated__/normalize.css.ts src/core-templates.tsx --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\"\n  },\n  \"dependencies\": {\n    \"@emotion/hash\": \"^0.9.2\",\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/fonts\": \"workspace:*\",\n    \"@webstudio-is/icons\": \"workspace:*\",\n    \"acorn\": \"^8.14.1\",\n    \"acorn-walk\": \"^8.3.4\",\n    \"change-case\": \"^5.4.4\",\n    \"reserved-identifiers\": \"^1.0.0\",\n    \"type-fest\": \"^4.37.0\",\n    \"warn-once\": \"^0.1.1\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/css-data\": \"workspace:*\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"html-tags\": \"^4.0.0\",\n    \"vitest\": \"^3.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk/scripts/normalize.css.ts",
    "content": "import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport htmlTags from \"html-tags\";\nimport { parseCss } from \"@webstudio-is/css-data\";\n\nconst mapGroupBy = <Item, Key>(\n  array: Item[] | Iterable<Item>,\n  getKey: (item: Item) => Key\n) => {\n  const groups = new Map<Key, Item[]>();\n  for (const item of array) {\n    const key = getKey(item);\n    let group = groups.get(key);\n    if (group === undefined) {\n      group = [];\n      groups.set(key, group);\n    }\n    group.push(item);\n  }\n  return groups;\n};\n\nconst css = await readFile(\"./src/normalize.css\", \"utf8\");\nconst parsed = parseCss(css);\nconst groups = mapGroupBy(parsed, (styleDecl) => styleDecl.selector);\n\nconst validTags = [\n  ...htmlTags,\n  // exceptions to access preset styles in Checkbox and Radio components\n  \"checkbox\",\n  \"radio\",\n];\n\nconst cache = new Map<string, string>();\n\nlet code = \"\";\ncode += `import type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\\n`;\ncode += `\ntype StyleDecl = {\n  state?: string;\n  property: CssProperty;\n  value: StyleValue;\n}\n\n`;\nfor (const [tag, styles] of groups) {\n  if (validTags.includes(tag) === false) {\n    throw Error(`Unexpected tag \"${tag}\"`);\n  }\n  const newStyles = styles.map(({ state, property, value }) => ({\n    state,\n    property,\n    value,\n  }));\n  let serializedStyles = JSON.stringify(newStyles);\n  const cachedTag = cache.get(serializedStyles);\n  // to prevent duplicating same styles\n  // assign already rendered tag to the tag with same styles\n  if (cachedTag) {\n    serializedStyles = cachedTag;\n    code += `export const ${tag} = ${cachedTag};\\n\\n`;\n  } else {\n    cache.set(serializedStyles, tag);\n    code += `export const ${tag}: StyleDecl[] = ${serializedStyles};\\n\\n`;\n  }\n}\n\nawait mkdir(\"./src/__generated__\", { recursive: true });\nawait writeFile(\"./src/__generated__/normalize.css.ts\", code);\n"
  },
  {
    "path": "packages/sdk/src/__generated__/normalize.css.ts",
    "content": "import type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\n\ntype StyleDecl = {\n  state?: string;\n  property: CssProperty;\n  value: StyleValue;\n};\n\nexport const div: StyleDecl[] = [\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const address = div;\n\nexport const article = div;\n\nexport const aside = div;\n\nexport const figure = div;\n\nexport const footer = div;\n\nexport const header = div;\n\nexport const main = div;\n\nexport const nav = div;\n\nexport const section = div;\n\nexport const form = div;\n\nexport const label = div;\n\nexport const time = div;\n\nexport const h1 = div;\n\nexport const h2 = div;\n\nexport const h3 = div;\n\nexport const h4 = div;\n\nexport const h5 = div;\n\nexport const h6 = div;\n\nexport const i = div;\n\nexport const img = div;\n\nexport const a = div;\n\nexport const li = div;\n\nexport const ul = div;\n\nexport const ol = div;\n\nexport const p = div;\n\nexport const span = div;\n\nexport const html: StyleDecl[] = [\n  { property: \"display\", value: { type: \"keyword\", value: \"grid\" } },\n  { property: \"min-height\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n  { property: \"grid-template-rows\", value: { type: \"keyword\", value: \"auto\" } },\n  {\n    property: \"grid-template-columns\",\n    value: { type: \"unit\", unit: \"fr\", value: 1 },\n  },\n  {\n    property: \"font-family\",\n    value: { type: \"fontFamily\", value: [\"Arial\", \"Roboto\", \"sans-serif\"] },\n  },\n  { property: \"font-size\", value: { type: \"unit\", unit: \"px\", value: 16 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 1.2 },\n  },\n  {\n    property: \"white-space-collapse\",\n    value: { type: \"keyword\", value: \"preserve\" },\n  },\n];\n\nexport const body: StyleDecl[] = [\n  { property: \"margin-top\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n  {\n    property: \"margin-right\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-bottom\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-left\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n  {\n    property: \"-webkit-font-smoothing\",\n    value: { type: \"keyword\", value: \"antialiased\" },\n  },\n  {\n    property: \"-moz-osx-font-smoothing\",\n    value: { type: \"keyword\", value: \"grayscale\" },\n  },\n];\n\nexport const hr: StyleDecl[] = [\n  { property: \"height\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n  { property: \"color\", value: { type: \"keyword\", value: \"inherit\" } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const b: StyleDecl[] = [\n  {\n    property: \"font-weight\",\n    value: { type: \"unit\", unit: \"number\", value: 700 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const strong = b;\n\nexport const code: StyleDecl[] = [\n  {\n    property: \"font-family\",\n    value: {\n      type: \"fontFamily\",\n      value: [\n        \"ui-monospace\",\n        \"SFMono-Regular\",\n        \"Consolas\",\n        \"Liberation Mono\",\n        \"Menlo\",\n        \"monospace\",\n      ],\n    },\n  },\n  { property: \"font-size\", value: { type: \"unit\", unit: \"em\", value: 1 } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const kbd = code;\n\nexport const samp = code;\n\nexport const pre = code;\n\nexport const small: StyleDecl[] = [\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 80 } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const sub: StyleDecl[] = [\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 75 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"position\", value: { type: \"keyword\", value: \"relative\" } },\n  { property: \"vertical-align\", value: { type: \"keyword\", value: \"baseline\" } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n  { property: \"bottom\", value: { type: \"unit\", unit: \"em\", value: -0.25 } },\n];\n\nexport const sup: StyleDecl[] = [\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 75 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"position\", value: { type: \"keyword\", value: \"relative\" } },\n  { property: \"vertical-align\", value: { type: \"keyword\", value: \"baseline\" } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n  { property: \"top\", value: { type: \"unit\", unit: \"em\", value: -0.5 } },\n];\n\nexport const table: StyleDecl[] = [\n  {\n    property: \"text-indent\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"border-top-color\",\n    value: { type: \"keyword\", value: \"inherit\" },\n  },\n  {\n    property: \"border-right-color\",\n    value: { type: \"keyword\", value: \"inherit\" },\n  },\n  {\n    property: \"border-bottom-color\",\n    value: { type: \"keyword\", value: \"inherit\" },\n  },\n  {\n    property: \"border-left-color\",\n    value: { type: \"keyword\", value: \"inherit\" },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const input: StyleDecl[] = [\n  { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 1.15 },\n  },\n  { property: \"margin-top\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n  {\n    property: \"margin-right\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-bottom\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-left\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n  { property: \"border-top-style\", value: { type: \"keyword\", value: \"solid\" } },\n  {\n    property: \"border-right-style\",\n    value: { type: \"keyword\", value: \"solid\" },\n  },\n  {\n    property: \"border-bottom-style\",\n    value: { type: \"keyword\", value: \"solid\" },\n  },\n  { property: \"border-left-style\", value: { type: \"keyword\", value: \"solid\" } },\n];\n\nexport const textarea = input;\n\nexport const optgroup: StyleDecl[] = [\n  { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 1.15 },\n  },\n  { property: \"margin-top\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n  {\n    property: \"margin-right\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-bottom\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-left\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const radio: StyleDecl[] = [\n  { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 1.15 },\n  },\n  { property: \"margin-top\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n  {\n    property: \"margin-right\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-bottom\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-left\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n  { property: \"border-top-style\", value: { type: \"keyword\", value: \"none\" } },\n  { property: \"border-right-style\", value: { type: \"keyword\", value: \"none\" } },\n  {\n    property: \"border-bottom-style\",\n    value: { type: \"keyword\", value: \"none\" },\n  },\n  { property: \"border-left-style\", value: { type: \"keyword\", value: \"none\" } },\n];\n\nexport const checkbox = radio;\n\nexport const button: StyleDecl[] = [\n  { property: \"font-family\", value: { type: \"keyword\", value: \"inherit\" } },\n  { property: \"font-size\", value: { type: \"unit\", unit: \"%\", value: 100 } },\n  {\n    property: \"line-height\",\n    value: { type: \"unit\", unit: \"number\", value: 1.15 },\n  },\n  { property: \"margin-top\", value: { type: \"unit\", unit: \"number\", value: 0 } },\n  {\n    property: \"margin-right\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-bottom\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"margin-left\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n  { property: \"border-top-style\", value: { type: \"keyword\", value: \"solid\" } },\n  {\n    property: \"border-right-style\",\n    value: { type: \"keyword\", value: \"solid\" },\n  },\n  {\n    property: \"border-bottom-style\",\n    value: { type: \"keyword\", value: \"solid\" },\n  },\n  { property: \"border-left-style\", value: { type: \"keyword\", value: \"solid\" } },\n  { property: \"text-transform\", value: { type: \"keyword\", value: \"none\" } },\n];\n\nexport const select = button;\n\nexport const legend: StyleDecl[] = [\n  {\n    property: \"padding-top\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"padding-right\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"padding-bottom\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  {\n    property: \"padding-left\",\n    value: { type: \"unit\", unit: \"number\", value: 0 },\n  },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const progress: StyleDecl[] = [\n  { property: \"vertical-align\", value: { type: \"keyword\", value: \"baseline\" } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n\nexport const summary: StyleDecl[] = [\n  { property: \"display\", value: { type: \"keyword\", value: \"list-item\" } },\n  { property: \"box-sizing\", value: { type: \"keyword\", value: \"border-box\" } },\n];\n"
  },
  {
    "path": "packages/sdk/src/__generated__/tags.ts",
    "content": "export const tags: string[] = [\n  \"div\",\n  \"span\",\n  \"a\",\n  \"abbr\",\n  \"address\",\n  \"area\",\n  \"article\",\n  \"aside\",\n  \"audio\",\n  \"b\",\n  \"bdi\",\n  \"bdo\",\n  \"blockquote\",\n  \"body\",\n  \"br\",\n  \"button\",\n  \"canvas\",\n  \"caption\",\n  \"cite\",\n  \"code\",\n  \"col\",\n  \"colgroup\",\n  \"data\",\n  \"datalist\",\n  \"dd\",\n  \"del\",\n  \"details\",\n  \"dfn\",\n  \"dialog\",\n  \"dl\",\n  \"dt\",\n  \"em\",\n  \"embed\",\n  \"fieldset\",\n  \"figcaption\",\n  \"figure\",\n  \"footer\",\n  \"form\",\n  \"h1\",\n  \"h2\",\n  \"h3\",\n  \"h4\",\n  \"h5\",\n  \"h6\",\n  \"head\",\n  \"header\",\n  \"hgroup\",\n  \"hr\",\n  \"html\",\n  \"i\",\n  \"iframe\",\n  \"img\",\n  \"input\",\n  \"ins\",\n  \"kbd\",\n  \"label\",\n  \"legend\",\n  \"li\",\n  \"main\",\n  \"map\",\n  \"mark\",\n  \"menu\",\n  \"meter\",\n  \"nav\",\n  \"object\",\n  \"ol\",\n  \"optgroup\",\n  \"option\",\n  \"output\",\n  \"p\",\n  \"picture\",\n  \"pre\",\n  \"progress\",\n  \"q\",\n  \"rp\",\n  \"rt\",\n  \"ruby\",\n  \"s\",\n  \"samp\",\n  \"search\",\n  \"section\",\n  \"select\",\n  \"slot\",\n  \"small\",\n  \"source\",\n  \"strong\",\n  \"sub\",\n  \"summary\",\n  \"sup\",\n  \"table\",\n  \"tbody\",\n  \"td\",\n  \"textarea\",\n  \"tfoot\",\n  \"th\",\n  \"thead\",\n  \"time\",\n  \"tr\",\n  \"track\",\n  \"u\",\n  \"ul\",\n  \"var\",\n  \"video\",\n  \"wbr\",\n  \"svg\",\n  \"g\",\n  \"defs\",\n  \"desc\",\n  \"symbol\",\n  \"use\",\n  \"image\",\n  \"switch\",\n  \"path\",\n  \"rect\",\n  \"circle\",\n  \"ellipse\",\n  \"line\",\n  \"polyline\",\n  \"polygon\",\n  \"text\",\n  \"tspan\",\n  \"textPath\",\n  \"marker\",\n  \"linearGradient\",\n  \"radialGradient\",\n  \"stop\",\n  \"pattern\",\n  \"clipPath\",\n  \"mask\",\n  \"filter\",\n  \"feDistantLight\",\n  \"fePointLight\",\n  \"feSpotLight\",\n  \"feBlend\",\n  \"feColorMatrix\",\n  \"feComponentTransfer\",\n  \"feFuncR\",\n  \"feFuncG\",\n  \"feFuncB\",\n  \"feFuncA\",\n  \"feComposite\",\n  \"feConvolveMatrix\",\n  \"feDiffuseLighting\",\n  \"feDisplacementMap\",\n  \"feFlood\",\n  \"feGaussianBlur\",\n  \"feImage\",\n  \"feMerge\",\n  \"feMergeNode\",\n  \"feMorphology\",\n  \"feOffset\",\n  \"feSpecularLighting\",\n  \"feTile\",\n  \"feTurbulence\",\n  \"view\",\n  \"animate\",\n  \"set\",\n  \"animateMotion\",\n  \"mpath\",\n  \"animateTransform\",\n  \"metadata\",\n  \"foreignObject\",\n];\n"
  },
  {
    "path": "packages/sdk/src/assets.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport {\n  ALLOWED_FILE_TYPES,\n  getMimeTypeByExtension,\n  getMimeTypeByFilename,\n  isAllowedExtension,\n  isAllowedMimeCategory,\n  validateFileName,\n  IMAGE_EXTENSIONS,\n  IMAGE_MIME_TYPES,\n  VIDEO_EXTENSIONS,\n  VIDEO_MIME_TYPES,\n  FONT_EXTENSIONS,\n  FILE_EXTENSIONS_BY_CATEGORY,\n  detectAssetType,\n  decodePathFragment,\n  getAssetUrl,\n  toRuntimeAsset,\n  acceptToMimePatterns,\n  acceptToMimeCategories,\n  getAssetMime,\n  doesAssetMatchMimePatterns,\n} from \"./assets\";\n\ndescribe(\"allowed-file-types\", () => {\n  describe(\"getMimeTypeByExtension\", () => {\n    test(\"returns correct MIME type for valid extension\", () => {\n      expect(getMimeTypeByExtension(\"jpg\")).toBe(\"image/jpeg\");\n      expect(getMimeTypeByExtension(\"png\")).toBe(\"image/png\");\n      expect(getMimeTypeByExtension(\"pdf\")).toBe(\"application/pdf\");\n      expect(getMimeTypeByExtension(\"mp4\")).toBe(\"video/mp4\");\n    });\n\n    test(\"is case-insensitive\", () => {\n      expect(getMimeTypeByExtension(\"JPG\")).toBe(\"image/jpeg\");\n      expect(getMimeTypeByExtension(\"PNG\")).toBe(\"image/png\");\n      expect(getMimeTypeByExtension(\"PDF\")).toBe(\"application/pdf\");\n    });\n\n    test(\"returns undefined for invalid extension\", () => {\n      expect(getMimeTypeByExtension(\"exe\")).toBeUndefined();\n      expect(getMimeTypeByExtension(\"xyz\")).toBeUndefined();\n    });\n  });\n\n  describe(\"getMimeTypeByFilename\", () => {\n    test(\"extracts extension and returns MIME type\", () => {\n      expect(getMimeTypeByFilename(\"image.jpg\")).toBe(\"image/jpeg\");\n      expect(getMimeTypeByFilename(\"document.pdf\")).toBe(\"application/pdf\");\n      expect(getMimeTypeByFilename(\"video.mp4\")).toBe(\"video/mp4\");\n    });\n\n    test(\"handles files with multiple dots\", () => {\n      expect(getMimeTypeByFilename(\"my.file.name.jpg\")).toBe(\"image/jpeg\");\n      expect(getMimeTypeByFilename(\"archive.tar.gz\")).toBe(\n        \"application/octet-stream\"\n      );\n    });\n\n    test(\"is case-insensitive\", () => {\n      expect(getMimeTypeByFilename(\"IMAGE.JPG\")).toBe(\"image/jpeg\");\n      expect(getMimeTypeByFilename(\"Document.PDF\")).toBe(\"application/pdf\");\n    });\n\n    test(\"returns octet-stream for invalid extension\", () => {\n      expect(getMimeTypeByFilename(\"file.exe\")).toBe(\n        \"application/octet-stream\"\n      );\n      expect(getMimeTypeByFilename(\"file.xyz\")).toBe(\n        \"application/octet-stream\"\n      );\n    });\n\n    test(\"returns octet-stream for files without extension\", () => {\n      expect(getMimeTypeByFilename(\"filename\")).toBe(\n        \"application/octet-stream\"\n      );\n      expect(getMimeTypeByFilename(\"\")).toBe(\"application/octet-stream\");\n    });\n  });\n\n  describe(\"isAllowedExtension\", () => {\n    test(\"returns true for valid extensions\", () => {\n      expect(isAllowedExtension(\"jpg\")).toBe(true);\n      expect(isAllowedExtension(\"png\")).toBe(true);\n      expect(isAllowedExtension(\"pdf\")).toBe(true);\n      expect(isAllowedExtension(\"mp4\")).toBe(true);\n    });\n\n    test(\"is case-insensitive\", () => {\n      expect(isAllowedExtension(\"JPG\")).toBe(true);\n      expect(isAllowedExtension(\"PNG\")).toBe(true);\n      expect(isAllowedExtension(\"PDF\")).toBe(true);\n    });\n\n    test(\"returns false for invalid extensions\", () => {\n      expect(isAllowedExtension(\"exe\")).toBe(false);\n      expect(isAllowedExtension(\"sh\")).toBe(false);\n      expect(isAllowedExtension(\"bat\")).toBe(false);\n    });\n  });\n\n  describe(\"isAllowedMimeCategory\", () => {\n    test(\"returns true for valid MIME categories\", () => {\n      expect(isAllowedMimeCategory(\"image\")).toBe(true);\n      expect(isAllowedMimeCategory(\"video\")).toBe(true);\n      expect(isAllowedMimeCategory(\"audio\")).toBe(true);\n      expect(isAllowedMimeCategory(\"font\")).toBe(true);\n      expect(isAllowedMimeCategory(\"text\")).toBe(true);\n      expect(isAllowedMimeCategory(\"application\")).toBe(true);\n    });\n\n    test(\"returns false for invalid MIME categories\", () => {\n      expect(isAllowedMimeCategory(\"executable/binary\")).toBe(false);\n      expect(isAllowedMimeCategory(\"script/javascript\")).toBe(false);\n    });\n\n    test(\"handles malformed MIME types\", () => {\n      expect(isAllowedMimeCategory(\"notamimetype\")).toBe(false);\n      expect(isAllowedMimeCategory(\"\")).toBe(false);\n    });\n  });\n\n  describe(\"validateFileName\", () => {\n    test(\"returns extension and MIME type for valid files\", () => {\n      expect(validateFileName(\"image.jpg\")).toEqual({\n        extension: \"jpg\",\n        mimeType: \"image/jpeg\",\n      });\n      expect(validateFileName(\"document.pdf\")).toEqual({\n        extension: \"pdf\",\n        mimeType: \"application/pdf\",\n      });\n    });\n\n    test(\"is case-insensitive\", () => {\n      expect(validateFileName(\"IMAGE.JPG\")).toEqual({\n        extension: \"jpg\",\n        mimeType: \"image/jpeg\",\n      });\n    });\n\n    test(\"handles files with multiple dots\", () => {\n      expect(validateFileName(\"my.file.name.png\")).toEqual({\n        extension: \"png\",\n        mimeType: \"image/png\",\n      });\n    });\n\n    test(\"throws error for files without extension\", () => {\n      expect(() => validateFileName(\"filename\")).toThrow(\n        'File type \"filename\" is not allowed'\n      );\n      // Empty string results in no extension either\n      expect(() => validateFileName(\"file.\")).toThrow(\n        \"File must have an extension\"\n      );\n    });\n\n    test(\"throws error for disallowed extensions\", () => {\n      expect(() => validateFileName(\"malware.exe\")).toThrow(\n        'File type \"exe\" is not allowed'\n      );\n      expect(() => validateFileName(\"script.sh\")).toThrow(\n        'File type \"sh\" is not allowed'\n      );\n    });\n\n    test(\"error message includes list of allowed types\", () => {\n      try {\n        validateFileName(\"bad.xyz\");\n      } catch (error) {\n        expect((error as Error).message).toContain(\"Allowed types:\");\n        expect((error as Error).message).toContain(\"jpg\");\n        expect((error as Error).message).toContain(\"png\");\n      }\n    });\n  });\n\n  describe(\"IMAGE_EXTENSIONS\", () => {\n    test(\"is an array\", () => {\n      expect(Array.isArray(IMAGE_EXTENSIONS)).toBe(true);\n      expect(IMAGE_EXTENSIONS.length).toBeGreaterThan(0);\n    });\n\n    test(\"all extensions are image types\", () => {\n      IMAGE_EXTENSIONS.forEach((ext) => {\n        const mimeType = getMimeTypeByExtension(ext);\n        expect(mimeType?.startsWith(\"image/\")).toBe(true);\n      });\n    });\n  });\n\n  describe(\"IMAGE_MIME_TYPES\", () => {\n    test(\"is an array of MIME types\", () => {\n      expect(Array.isArray(IMAGE_MIME_TYPES)).toBe(true);\n      expect(IMAGE_MIME_TYPES.length).toBeGreaterThan(0);\n    });\n\n    test(\"all MIME types start with image/\", () => {\n      IMAGE_MIME_TYPES.forEach((mime) => {\n        expect(mime.startsWith(\"image/\")).toBe(true);\n      });\n    });\n  });\n\n  describe(\"VIDEO_EXTENSIONS\", () => {\n    test(\"is an array\", () => {\n      expect(Array.isArray(VIDEO_EXTENSIONS)).toBe(true);\n      expect(VIDEO_EXTENSIONS.length).toBeGreaterThan(0);\n    });\n\n    test(\"all extensions are video types\", () => {\n      VIDEO_EXTENSIONS.forEach((ext) => {\n        const mimeType = getMimeTypeByExtension(ext);\n        expect(mimeType?.startsWith(\"video/\")).toBe(true);\n      });\n    });\n  });\n\n  describe(\"VIDEO_MIME_TYPES\", () => {\n    test(\"is an array of MIME types\", () => {\n      expect(Array.isArray(VIDEO_MIME_TYPES)).toBe(true);\n      expect(VIDEO_MIME_TYPES.length).toBeGreaterThan(0);\n    });\n\n    test(\"all MIME types start with video/\", () => {\n      VIDEO_MIME_TYPES.forEach((mime) => {\n        expect(mime.startsWith(\"video/\")).toBe(true);\n      });\n    });\n  });\n\n  describe(\"FONT_EXTENSIONS\", () => {\n    test(\"is an array\", () => {\n      expect(Array.isArray(FONT_EXTENSIONS)).toBe(true);\n      expect(FONT_EXTENSIONS.length).toBeGreaterThan(0);\n    });\n\n    test(\"all extensions are font types\", () => {\n      FONT_EXTENSIONS.forEach((ext) => {\n        const mimeType = getMimeTypeByExtension(ext);\n        expect(mimeType?.startsWith(\"font/\")).toBe(true);\n      });\n    });\n\n    test(\"includes common font formats\", () => {\n      expect(FONT_EXTENSIONS).toContain(\"woff\");\n      expect(FONT_EXTENSIONS).toContain(\"woff2\");\n      expect(FONT_EXTENSIONS).toContain(\"ttf\");\n      expect(FONT_EXTENSIONS).toContain(\"otf\");\n    });\n  });\n\n  describe(\"FILE_EXTENSIONS_BY_CATEGORY\", () => {\n    test(\"has all expected categories\", () => {\n      expect(FILE_EXTENSIONS_BY_CATEGORY).toHaveProperty(\"image\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY).toHaveProperty(\"font\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY).toHaveProperty(\"text\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY).toHaveProperty(\"application\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY).toHaveProperty(\"audio\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY).toHaveProperty(\"video\");\n    });\n\n    test(\"image category contains image extensions\", () => {\n      expect(FILE_EXTENSIONS_BY_CATEGORY.image).toContain(\"jpg\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.image).toContain(\"png\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.image).toContain(\"gif\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.image).toContain(\"svg\");\n    });\n\n    test(\"font category contains font extensions\", () => {\n      expect(FILE_EXTENSIONS_BY_CATEGORY.font).toContain(\"woff\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.font).toContain(\"woff2\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.font).toContain(\"ttf\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.font).toContain(\"otf\");\n    });\n\n    test(\"application category contains document extensions\", () => {\n      // Office documents\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"pdf\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"doc\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"docx\");\n      // Spreadsheets\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"xls\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"xlsx\");\n      // Presentations\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"ppt\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"pptx\");\n      // Archives\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"zip\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.application).toContain(\"rar\");\n    });\n\n    test(\"text category contains text file extensions\", () => {\n      expect(FILE_EXTENSIONS_BY_CATEGORY.text).toContain(\"txt\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.text).toContain(\"md\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.text).toContain(\"csv\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.text).toContain(\"js\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.text).toContain(\"css\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.text).toContain(\"html\");\n    });\n\n    test(\"audio category contains audio extensions\", () => {\n      expect(FILE_EXTENSIONS_BY_CATEGORY.audio).toContain(\"mp3\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.audio).toContain(\"wav\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.audio).toContain(\"ogg\");\n    });\n\n    test(\"video category contains video extensions\", () => {\n      expect(FILE_EXTENSIONS_BY_CATEGORY.video).toContain(\"mp4\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.video).toContain(\"webm\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.video).toContain(\"mov\");\n      expect(FILE_EXTENSIONS_BY_CATEGORY.video).toContain(\"avi\");\n    });\n\n    test(\"all extensions are accounted for\", () => {\n      const allCategoryExtensions = Object.values(\n        FILE_EXTENSIONS_BY_CATEGORY\n      ).flat();\n      const allExtensions = Object.keys(ALLOWED_FILE_TYPES);\n\n      // Every extension should be in at least one category\n      allExtensions.forEach((ext) => {\n        expect(allCategoryExtensions).toContain(ext);\n      });\n    });\n  });\n\n  describe(\"detectAssetType\", () => {\n    test(\"detects image files\", () => {\n      expect(detectAssetType(\"photo.jpg\")).toBe(\"image\");\n      expect(detectAssetType(\"image.png\")).toBe(\"image\");\n      expect(detectAssetType(\"graphic.gif\")).toBe(\"image\");\n      expect(detectAssetType(\"vector.svg\")).toBe(\"image\");\n      expect(detectAssetType(\"picture.webp\")).toBe(\"image\");\n    });\n\n    test(\"detects font files\", () => {\n      expect(detectAssetType(\"font.woff\")).toBe(\"font\");\n      expect(detectAssetType(\"font.woff2\")).toBe(\"font\");\n      expect(detectAssetType(\"font.ttf\")).toBe(\"font\");\n      expect(detectAssetType(\"font.otf\")).toBe(\"font\");\n    });\n\n    test(\"detects video files\", () => {\n      expect(detectAssetType(\"video.mp4\")).toBe(\"video\");\n      expect(detectAssetType(\"video.webm\")).toBe(\"video\");\n      expect(detectAssetType(\"video.mov\")).toBe(\"video\");\n      expect(detectAssetType(\"video.avi\")).toBe(\"video\");\n    });\n\n    test(\"returns file for other types\", () => {\n      expect(detectAssetType(\"document.pdf\")).toBe(\"file\");\n      expect(detectAssetType(\"audio.mp3\")).toBe(\"file\");\n      expect(detectAssetType(\"data.json\")).toBe(\"file\");\n      expect(detectAssetType(\"doc.docx\")).toBe(\"file\");\n    });\n\n    test(\"is case-insensitive\", () => {\n      expect(detectAssetType(\"PHOTO.JPG\")).toBe(\"image\");\n      expect(detectAssetType(\"FONT.WOFF2\")).toBe(\"font\");\n      expect(detectAssetType(\"VIDEO.MP4\")).toBe(\"video\");\n      expect(detectAssetType(\"DOC.PDF\")).toBe(\"file\");\n    });\n\n    test(\"handles files without extension\", () => {\n      expect(detectAssetType(\"filename\")).toBe(\"file\");\n    });\n\n    test(\"handles files with multiple dots\", () => {\n      expect(detectAssetType(\"my.photo.file.png\")).toBe(\"image\");\n      expect(detectAssetType(\"my.font.file.woff2\")).toBe(\"font\");\n      expect(detectAssetType(\"my.video.file.mp4\")).toBe(\"video\");\n      expect(detectAssetType(\"my.doc.file.pdf\")).toBe(\"file\");\n    });\n  });\n\n  describe(\"decodePathFragment\", () => {\n    test(\"decodes URI components correctly\", () => {\n      expect(decodePathFragment(\"hello%20world.jpg\")).toBe(\"hello world.jpg\");\n      expect(decodePathFragment(\"image%2Btest.png\")).toBe(\"image+test.png\");\n      expect(decodePathFragment(\"file%40name.pdf\")).toBe(\"file@name.pdf\");\n    });\n\n    test(\"throws error on path traversal attempts with ..\", () => {\n      expect(() => decodePathFragment(\"../secret.txt\")).toThrow(\n        \"Invalid file path\"\n      );\n      expect(() => decodePathFragment(\"folder/../secret.txt\")).toThrow(\n        \"Invalid file path\"\n      );\n      expect(() => decodePathFragment(\"..%2Fsecret.txt\")).toThrow(\n        \"Invalid file path\"\n      );\n    });\n\n    test(\"throws error on absolute path attempts\", () => {\n      expect(() => decodePathFragment(\"/etc/passwd\")).toThrow(\n        \"Invalid file path\"\n      );\n      expect(() => decodePathFragment(\"%2Fetc%2Fpasswd\")).toThrow(\n        \"Invalid file path\"\n      );\n    });\n\n    test(\"handles normal filenames\", () => {\n      expect(decodePathFragment(\"image.jpg\")).toBe(\"image.jpg\");\n      expect(decodePathFragment(\"my-file.pdf\")).toBe(\"my-file.pdf\");\n      expect(decodePathFragment(\"file_123.png\")).toBe(\"file_123.png\");\n    });\n  });\n\n  describe(\"getAssetUrl\", () => {\n    const mockImageAsset = {\n      id: \"image-1\",\n      name: \"photo.jpg\",\n      projectId: \"project-1\",\n      size: 1024,\n      type: \"image\" as const,\n      format: \"jpg\",\n      description: \"\",\n      createdAt: \"2024-01-01\",\n      meta: { width: 100, height: 100 },\n    };\n\n    const mockVideoAsset = {\n      id: \"video-1\",\n      name: \"video.mp4\",\n      projectId: \"project-1\",\n      size: 2048,\n      type: \"file\" as const,\n      format: \"mp4\",\n      description: \"\",\n      createdAt: \"2024-01-01\",\n      meta: {},\n    };\n\n    const mockFontAsset = {\n      id: \"font-1\",\n      name: \"font.woff2\",\n      projectId: \"project-1\",\n      size: 512,\n      type: \"font\" as const,\n      format: \"woff2\" as const,\n      description: \"\",\n      createdAt: \"2024-01-01\",\n      meta: { family: \"Arial\", style: \"normal\" as const, weight: 400 },\n    };\n\n    const mockGenericAsset = {\n      id: \"doc-1\",\n      name: \"document.pdf\",\n      projectId: \"project-1\",\n      size: 4096,\n      type: \"file\" as const,\n      format: \"pdf\",\n      description: \"\",\n      createdAt: \"2024-01-01\",\n      meta: {},\n    };\n\n    test(\"generates correct URL for image assets\", () => {\n      const url = getAssetUrl(mockImageAsset, \"https://example.com\");\n      expect(url.href).toBe(\n        \"https://example.com/cgi/image/photo.jpg?format=raw\"\n      );\n      expect(url.pathname).toBe(\"/cgi/image/photo.jpg\");\n      expect(url.search).toBe(\"?format=raw\");\n    });\n\n    test(\"generates correct URL for video assets\", () => {\n      const url = getAssetUrl(mockVideoAsset, \"https://example.com\");\n      expect(url.href).toBe(\n        \"https://example.com/cgi/asset/video.mp4?format=raw\"\n      );\n      expect(url.pathname).toBe(\"/cgi/asset/video.mp4\");\n    });\n\n    test(\"generates correct URL for font assets\", () => {\n      const url = getAssetUrl(mockFontAsset, \"https://example.com\");\n      expect(url.href).toBe(\n        \"https://example.com/cgi/asset/font.woff2?format=raw\"\n      );\n      expect(url.pathname).toBe(\"/cgi/asset/font.woff2\");\n    });\n\n    test(\"generates correct URL for generic file assets\", () => {\n      const url = getAssetUrl(mockGenericAsset, \"https://example.com\");\n      expect(url.href).toBe(\n        \"https://example.com/cgi/asset/document.pdf?format=raw\"\n      );\n      expect(url.pathname).toBe(\"/cgi/asset/document.pdf\");\n    });\n\n    test(\"works with different origins\", () => {\n      const url1 = getAssetUrl(mockImageAsset, \"https://example.com\");\n      const url2 = getAssetUrl(mockImageAsset, \"http://localhost:3000\");\n      const url3 = getAssetUrl(mockImageAsset, \"https://cdn.example.org\");\n\n      expect(url1.origin).toBe(\"https://example.com\");\n      expect(url2.origin).toBe(\"http://localhost:3000\");\n      expect(url3.origin).toBe(\"https://cdn.example.org\");\n\n      // All should have the same pathname\n      expect(url1.pathname).toBe(url2.pathname);\n      expect(url2.pathname).toBe(url3.pathname);\n    });\n\n    test(\"pathname can be used for relative URLs\", () => {\n      const url = getAssetUrl(mockGenericAsset, \"https://example.com\");\n      expect(url.pathname).toBe(\"/cgi/asset/document.pdf\");\n      // pathname is suitable for href attribute in same-origin context\n    });\n\n    test(\"detects video format case-insensitively\", () => {\n      const upperCaseVideo = {\n        ...mockVideoAsset,\n        format: \"MP4\",\n      };\n      const url = getAssetUrl(upperCaseVideo, \"https://example.com\");\n      expect(url.pathname).toBe(\"/cgi/asset/video.mp4\");\n    });\n\n    test(\"handles assets with special characters in name\", () => {\n      const specialAsset = {\n        ...mockGenericAsset,\n        name: \"my document (1).pdf\",\n      };\n      const url = getAssetUrl(specialAsset, \"https://example.com\");\n      // URL constructor automatically encodes special characters in pathname\n      expect(url.pathname).toBe(\"/cgi/asset/my%20document%20(1).pdf\");\n    });\n  });\n\n  describe(\"acceptToMimePatterns\", () => {\n    test(\"returns * if accept is empty or * or */*\", () => {\n      expect(acceptToMimePatterns(\"\")).toBe(\"*\");\n      expect(acceptToMimePatterns(\"*\")).toBe(\"*\");\n      expect(acceptToMimePatterns(\"*/*\")).toBe(\"*\");\n      expect(acceptToMimePatterns(\"*,.png\")).toBe(\"*\");\n    });\n\n    test(\"returns * if it doesn't recognise a pattern\", () => {\n      expect(acceptToMimePatterns(\"image/wrong\")).toBe(\"*\");\n      expect(acceptToMimePatterns(\"wrong/*\")).toBe(\"*\");\n      expect(acceptToMimePatterns(\".wrong\")).toBe(\"*\");\n    });\n\n    test(\"leaves full mimes as is\", () => {\n      expect(acceptToMimePatterns(\"image/png,font/otf\")).toEqual(\n        new Set([\"image/png\", \"font/otf\"])\n      );\n    });\n\n    test(\"leaves mime patterns as is\", () => {\n      expect(acceptToMimePatterns(\"image/*,font/*\")).toEqual(\n        new Set([\"image/*\", \"font/*\"])\n      );\n    });\n\n    test(\"converts extensions to mimes\", () => {\n      expect(acceptToMimePatterns(\".svg,.otf\")).toEqual(\n        new Set([\"image/svg+xml\", \"font/otf\"])\n      );\n    });\n  });\n\n  describe(\"acceptToMimeCategories\", () => {\n    test(\"returns * if accept is empty or * or */*\", () => {\n      expect(acceptToMimeCategories(\"\")).toBe(\"*\");\n      expect(acceptToMimeCategories(\"*\")).toBe(\"*\");\n      expect(acceptToMimeCategories(\"*/*\")).toBe(\"*\");\n      expect(acceptToMimeCategories(\"*,.png\")).toBe(\"*\");\n    });\n\n    test(\"returns * if it doesn't recognise a pattern\", () => {\n      expect(acceptToMimeCategories(\"image/wrong\")).toBe(\"*\");\n      expect(acceptToMimeCategories(\"wrong/*\")).toBe(\"*\");\n      expect(acceptToMimeCategories(\".wrong\")).toBe(\"*\");\n    });\n\n    test(\"handles full mimes\", () => {\n      expect(acceptToMimeCategories(\"image/png,font/otf\")).toEqual(\n        new Set([\"image\", \"font\"])\n      );\n    });\n\n    test(\"handles mime patterns\", () => {\n      expect(acceptToMimeCategories(\"image/*,font/*\")).toEqual(\n        new Set([\"image\", \"font\"])\n      );\n    });\n\n    test(\"handles extensions\", () => {\n      expect(acceptToMimeCategories(\".svg,.otf\")).toEqual(\n        new Set([\"image\", \"font\"])\n      );\n    });\n  });\n\n  describe(\"getAssetMime\", () => {\n    test(\"handles woff\", () => {\n      expect(getAssetMime({ type: \"font\", format: \"woff\" })).toBe(\"font/woff\");\n    });\n\n    test(\"handles png\", () => {\n      expect(getAssetMime({ type: \"image\", format: \"png\" })).toBe(\"image/png\");\n    });\n\n    test(\"handles svg\", () => {\n      expect(getAssetMime({ type: \"image\", format: \"svg\" })).toBe(\n        \"image/svg+xml\"\n      );\n    });\n\n    test(\"returns undefined for unknown format\", () => {\n      expect(\n        getAssetMime({ type: \"image\", format: \"unknown\" })\n      ).toBeUndefined();\n    });\n  });\n\n  describe(\"doesAssetMatchMimePatterns\", () => {\n    test(\"returns true if mime patterns is *\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"png\", name: \"test.png\" },\n          \"*\"\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles full mimes\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"svg\", name: \"test.svg\" },\n          new Set([\"image/svg+xml\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"svg\", name: \"test.svg\" },\n          new Set([\"image/png\"])\n        )\n      ).toBe(false);\n    });\n\n    test(\"handles mime patterns\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"svg\", name: \"test.svg\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"svg\", name: \"test.svg\" },\n          new Set([\"font/*\"])\n        )\n      ).toBe(false);\n    });\n\n    test(\"if asset format has unexpected value, returns false\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"unknown\", name: \"test.unknown\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(false);\n    });\n\n    test(\"handles uppercase format extensions\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"JPG\", name: \"test.JPG\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"PNG\", name: \"test.PNG\" },\n          new Set([\"image/png\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"font\", format: \"WOFF2\", name: \"font.WOFF2\" },\n          new Set([\"font/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles normal image assets\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"jpg\", name: \"test.jpg\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles legacy assets with type 'file' but image extension\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"jpg\", name: \"test.jpg\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"JPG\", name: \"test.JPG\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"png\", name: \"test.png\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles legacy assets with type 'file' but font extension\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"woff2\", name: \"font.woff2\" },\n          new Set([\"font/*\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"ttf\", name: \"font.ttf\" },\n          new Set([\"font/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles video assets stored as type 'file'\", () => {\n      // Videos are always stored as type \"file\" in the database\n      // They should match video/* patterns via normal MIME matching\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"mp4\", name: \"video.mp4\" },\n          new Set([\"video/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"does not match legacy file assets with wrong pattern\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"jpg\", name: \"test.jpg\" },\n          new Set([\"font/*\"])\n        )\n      ).toBe(false);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"pdf\", name: \"doc.pdf\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(false);\n    });\n\n    test(\"handles real file assets (not legacy)\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"pdf\", name: \"doc.pdf\" },\n          new Set([\"application/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles assets without extension in name\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"unknown\", name: \"test\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(false);\n    });\n\n    test(\"handles assets with mismatched format and extension\", () => {\n      // Format says jpg but filename says png - should match based on format first\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"jpg\", name: \"test.png\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n      // If format is unknown, falls back to filename extension\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"unknown\", name: \"test.png\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles uppercase extensions in filenames\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"unknown\", name: \"test.JPG\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"handles specific MIME types with fallback\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"unknown\", name: \"test.jpg\" },\n          new Set([\"image/jpeg\"])\n        )\n      ).toBe(true);\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"unknown\", name: \"test.jpg\" },\n          new Set([\"image/png\"])\n        )\n      ).toBe(false);\n    });\n\n    test(\"handles multiple patterns with fallback\", () => {\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"file\", format: \"unknown\", name: \"test.jpg\" },\n          new Set([\"image/png\", \"image/jpeg\", \"image/gif\"])\n        )\n      ).toBe(true);\n    });\n\n    test(\"only uses fallback for type 'file' assets\", () => {\n      // Image type with unknown format should not fallback to name\n      expect(\n        doesAssetMatchMimePatterns(\n          { type: \"image\", format: \"unknown\", name: \"test.jpg\" },\n          new Set([\"image/*\"])\n        )\n      ).toBe(false);\n    });\n  });\n\n  describe(\"toRuntimeAsset\", () => {\n    const mockImageAsset = {\n      id: \"image-1\",\n      name: \"photo.jpg\",\n      projectId: \"project-1\",\n      size: 1024,\n      type: \"image\" as const,\n      format: \"jpg\",\n      description: \"A photo\",\n      createdAt: \"2024-01-01\",\n      meta: { width: 1920, height: 1080 },\n    };\n\n    const mockFontAsset = {\n      id: \"font-1\",\n      name: \"font.woff2\",\n      projectId: \"project-1\",\n      size: 512,\n      type: \"font\" as const,\n      format: \"woff2\" as const,\n      description: null,\n      createdAt: \"2024-01-01\",\n      meta: {\n        family: \"Arial\",\n        style: \"normal\" as const,\n        weight: 400,\n      },\n    };\n\n    const mockVariableFontAsset = {\n      id: \"font-2\",\n      name: \"variable-font.woff2\",\n      projectId: \"project-1\",\n      size: 768,\n      type: \"font\" as const,\n      format: \"woff2\" as const,\n      description: null,\n      createdAt: \"2024-01-01\",\n      meta: {\n        family: \"Inter\",\n        variationAxes: {},\n      },\n    };\n\n    const mockGenericAsset = {\n      id: \"doc-1\",\n      name: \"document.pdf\",\n      projectId: \"project-1\",\n      size: 4096,\n      type: \"file\" as const,\n      format: \"pdf\",\n      description: null,\n      createdAt: \"2024-01-01\",\n      meta: {},\n    };\n\n    test(\"converts image asset with all fields\", () => {\n      const result = toRuntimeAsset(mockImageAsset, \"https://example.com\");\n      expect(result).toEqual({\n        url: \"/cgi/image/photo.jpg?format=raw\",\n        width: 1920,\n        height: 1080,\n      });\n    });\n\n    test(\"converts static font asset with metadata\", () => {\n      const result = toRuntimeAsset(mockFontAsset, \"https://example.com\");\n      expect(result).toEqual({\n        url: \"/cgi/asset/font.woff2?format=raw\",\n        family: \"Arial\",\n        style: \"normal\",\n        weight: 400,\n      });\n    });\n\n    test(\"converts variable font asset without style/weight\", () => {\n      const result = toRuntimeAsset(\n        mockVariableFontAsset,\n        \"https://example.com\"\n      );\n      expect(result).toEqual({\n        url: \"/cgi/asset/variable-font.woff2?format=raw\",\n        family: \"Inter\",\n      });\n    });\n\n    test(\"converts generic file asset with minimal fields\", () => {\n      const result = toRuntimeAsset(mockGenericAsset, \"https://example.com\");\n      expect(result).toEqual({\n        url: \"/cgi/asset/document.pdf?format=raw\",\n      });\n    });\n\n    test(\"returns relative URLs regardless of origin\", () => {\n      const result1 = toRuntimeAsset(mockImageAsset, \"https://cdn.example.com\");\n      const result2 = toRuntimeAsset(mockImageAsset, \"http://localhost:3000\");\n      // Both should return the same relative URL\n      expect(result1.url).toBe(\"/cgi/image/photo.jpg?format=raw\");\n      expect(result2.url).toBe(\"/cgi/image/photo.jpg?format=raw\");\n    });\n\n    test(\"handles image without dimensions\", () => {\n      const assetWithoutDimensions = {\n        ...mockImageAsset,\n        meta: { width: 0, height: 0 },\n      };\n      const result = toRuntimeAsset(\n        assetWithoutDimensions,\n        \"https://example.com\"\n      );\n      expect(result).not.toHaveProperty(\"width\");\n      expect(result).not.toHaveProperty(\"height\");\n    });\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/assets.ts",
    "content": "import warnOnce from \"warn-once\";\nimport type { Asset } from \"./schema/assets\";\n\n/**\n * Central registry of allowed file types, extensions, and MIME types\n * for asset uploads and serving.\n *\n * IMPORTANT: For images, we only support formats that Cloudflare Image Resizing can process.\n * Supported by Cloudflare: JPEG, PNG, GIF, WebP, SVG, AVIF\n * See: https://developers.cloudflare.com/images/image-resizing/format-limitations/\n *\n * Other formats (BMP, ICO, TIFF) are allowed for upload but served as-is without optimization.\n */\n\nexport const ALLOWED_FILE_TYPES = {\n  // Documents\n  pdf: \"application/pdf\",\n  doc: \"application/msword\",\n  docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n  xls: \"application/vnd.ms-excel\",\n  xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n  csv: \"text/csv\",\n  ppt: \"application/vnd.ms-powerpoint\",\n  pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n\n  // Code\n  txt: \"text/plain\",\n  md: \"text/markdown\",\n  js: \"text/javascript\",\n  css: \"text/css\",\n  json: \"application/json\",\n  html: \"text/html\",\n  xml: \"application/xml\",\n\n  // Archives\n  zip: \"application/zip\",\n  rar: \"application/vnd.rar\",\n\n  // Audio\n  mp3: \"audio/mpeg\",\n  wav: \"audio/wav\",\n  ogg: \"audio/ogg\",\n  m4a: \"audio/mp4\",\n\n  // Video\n  mp4: \"video/mp4\",\n  mov: \"video/quicktime\",\n  avi: \"video/x-msvideo\",\n  webm: \"video/webm\",\n\n  // Images\n  // Note: Cloudflare Image Resizing supports: jpg, jpeg, png, gif, webp, svg, avif\n  // Other formats (bmp, ico, tif, tiff) are served as-is without optimization\n  jpg: \"image/jpeg\",\n  jpeg: \"image/jpeg\",\n  png: \"image/png\",\n  gif: \"image/gif\",\n  svg: \"image/svg+xml\",\n  webp: \"image/webp\",\n  avif: \"image/avif\",\n  ico: \"image/vnd.microsoft.icon\", // Used for favicons\n  bmp: \"image/bmp\", // Served without optimization\n  tif: \"image/tiff\", // Served without optimization\n  tiff: \"image/tiff\", // Served without optimization\n\n  // Fonts\n  woff: \"font/woff\",\n  woff2: \"font/woff2\",\n  ttf: \"font/ttf\",\n  otf: \"font/otf\",\n} as const;\n\nexport type AllowedFileExtension = keyof typeof ALLOWED_FILE_TYPES;\n\n/**\n * Set of allowed file extensions for quick lookup\n */\nexport const ALLOWED_FILE_EXTENSIONS: ReadonlySet<AllowedFileExtension> =\n  new Set<AllowedFileExtension>(\n    Object.keys(ALLOWED_FILE_TYPES) as AllowedFileExtension[]\n  );\n\n/**\n * Set of allowed MIME type categories\n */\nexport const MIME_CATEGORIES = [\n  \"image\",\n  \"video\",\n  \"audio\",\n  \"font\",\n  \"text\",\n  \"application\",\n] as const;\n\nexport type MimeCategory = (typeof MIME_CATEGORIES)[number];\n\n/**\n * File extensions grouped by MIME category for UI display and filtering\n */\nexport const FILE_EXTENSIONS_BY_CATEGORY: Readonly<\n  Record<MimeCategory, AllowedFileExtension[]>\n> = (() => {\n  const categories = Object.fromEntries(\n    MIME_CATEGORIES.map((category) => [category, []])\n  ) as unknown as Record<MimeCategory, AllowedFileExtension[]>;\n\n  Object.entries(ALLOWED_FILE_TYPES).forEach(([ext, mimeType]) => {\n    const [category] = mimeType.split(\"/\") as [MimeCategory];\n\n    if (category in categories) {\n      categories[category].push(ext as AllowedFileExtension);\n    }\n  });\n\n  return categories;\n})();\n\n// Create extension-to-MIME map for utilities\nconst extensionToMime = new Map(\n  Object.entries(ALLOWED_FILE_TYPES).map(([ext, mime]) => [`.${ext}`, mime])\n);\n\nconst mimeTypes = new Set<string>(extensionToMime.values());\n\nconst mimePatterns = new Set<string>([\n  ...mimeTypes.values(),\n  ...MIME_CATEGORIES.map((category): `${MimeCategory}/*` => `${category}/*`),\n]);\n\nconst getCategory = (pattern: string): MimeCategory => {\n  const categoryAsString = pattern.split(\"/\")[0];\n  const category = MIME_CATEGORIES.find(\n    (category) => category === categoryAsString\n  );\n  if (category === undefined) {\n    throw new Error(`Invalid mime pattern: ${pattern}`);\n  }\n  return category;\n};\n\n/**\n * All image file extensions\n */\nexport const IMAGE_EXTENSIONS: readonly AllowedFileExtension[] =\n  FILE_EXTENSIONS_BY_CATEGORY.image;\n\n/**\n * All image MIME types\n */\nexport const IMAGE_MIME_TYPES: readonly string[] = IMAGE_EXTENSIONS.map(\n  (ext) => ALLOWED_FILE_TYPES[ext as keyof typeof ALLOWED_FILE_TYPES]\n);\n\n/**\n * Subset of IMAGE_MIME_TYPES supported as input by the image resizing pipeline.\n * Used in the Accept header when fetching external images to prevent services\n * like Unsplash (with auto=format) from serving formats like AVIF that cause\n * Cloudflare Image Resizing ERROR 9520 during transformation.\n *\n * See: https://developers.cloudflare.com/images/transform-images/#supported-input-formats\n */\nexport const RESIZABLE_IMAGE_MIME_TYPES: readonly string[] = [\n  \"image/jpeg\",\n  \"image/png\",\n  \"image/gif\",\n  \"image/webp\",\n  \"image/svg+xml\",\n];\n\n/**\n * All video file extensions\n */\nexport const VIDEO_EXTENSIONS: readonly AllowedFileExtension[] =\n  FILE_EXTENSIONS_BY_CATEGORY.video;\n\n/**\n * All video MIME types\n */\nexport const VIDEO_MIME_TYPES: readonly string[] = VIDEO_EXTENSIONS.map(\n  (ext) => ALLOWED_FILE_TYPES[ext as keyof typeof ALLOWED_FILE_TYPES]\n);\n\n/**\n * All font file extensions\n */\nexport const FONT_EXTENSIONS: readonly AllowedFileExtension[] =\n  FILE_EXTENSIONS_BY_CATEGORY.font;\n\n/**\n * Get MIME type for a given file extension\n */\nexport const getMimeTypeByExtension = (\n  extension: string\n): string | undefined => {\n  return ALLOWED_FILE_TYPES[extension.toLowerCase() as AllowedFileExtension];\n};\n\n/**\n * Get MIME type from a filename\n */\nexport const getMimeTypeByFilename = (fileName: string): string => {\n  const extension = fileName.split(\".\").pop()?.toLowerCase();\n  if (!extension) {\n    return \"application/octet-stream\";\n  }\n  return getMimeTypeByExtension(extension) ?? \"application/octet-stream\";\n};\n\n/**\n * Check if a file extension is allowed\n */\nexport const isAllowedExtension = (extension: string): boolean => {\n  return ALLOWED_FILE_EXTENSIONS.has(\n    extension.toLowerCase() as AllowedFileExtension\n  );\n};\n\n/**\n * Check if a MIME type category is allowed\n */\nexport const isAllowedMimeCategory = (category: string): boolean => {\n  return MIME_CATEGORIES.includes(category as MimeCategory);\n};\n\n/**\n * Convert accept attribute value to MIME patterns.\n * `\".svg,font/otf,text/*\"` -> `\"image/svg+xml\", \"font/otf\", \"text/*\"`\n */\nexport const acceptToMimePatterns = (accept: string): Set<string> | \"*\" => {\n  const result = new Set<string>();\n\n  if (accept === \"\") {\n    return \"*\";\n  }\n\n  for (const type of accept.split(\",\")) {\n    const trimmed = type.trim();\n    if (trimmed === \"*\" || trimmed === \"*/*\") {\n      return \"*\";\n    }\n    if (mimePatterns.has(trimmed)) {\n      result.add(trimmed);\n      continue;\n    }\n    const mime = extensionToMime.get(trimmed);\n\n    if (mime === undefined) {\n      warnOnce(\n        true,\n        `Couldn't not parse accept attribute value: ${trimmed}. Falling back to \"*\".`\n      );\n      return \"*\";\n    }\n\n    result.add(mime);\n  }\n\n  return result;\n};\n\n/**\n * Convert accept attribute value to MIME categories.\n * `\".svg,font/otf,text/*\"` -> `\"image\", \"font\", \"text\"`\n */\nexport const acceptToMimeCategories = (\n  accept: string\n): Set<MimeCategory> | \"*\" => {\n  const patterns = acceptToMimePatterns(accept);\n  if (patterns === \"*\") {\n    return \"*\";\n  }\n  const categories = new Set<MimeCategory>();\n  for (const pattern of patterns) {\n    categories.add(getCategory(pattern));\n  }\n  return categories;\n};\n\n/**\n * Get MIME type for an asset based on its type and format\n */\nexport const getAssetMime = ({\n  type,\n  format,\n}: {\n  type: \"image\" | \"font\" | \"file\";\n  format: string;\n}): string | undefined => {\n  const lowerFormat = format.toLowerCase();\n  const mime = `${type}/${lowerFormat}`;\n  if (mimeTypes.has(mime)) {\n    return mime;\n  }\n  const mime2 = extensionToMime.get(`.${lowerFormat}`);\n  if (mime2 === undefined) {\n    warnOnce(\n      true,\n      `Couldn't determine mime type of asset: ${type}, ${format}.`\n    );\n  }\n  return mime2;\n};\n\n/**\n * Check if an asset matches the given MIME patterns.\n * Supports legacy assets that were incorrectly stored with type \"file\".\n */\nexport const doesAssetMatchMimePatterns = (\n  asset: Pick<Asset, \"format\" | \"type\" | \"name\">,\n  patterns: Set<string> | \"*\"\n): boolean => {\n  if (patterns === \"*\") {\n    return true;\n  }\n\n  // Try matching based on asset type and format\n  const mime = getAssetMime(asset);\n  if (mime !== undefined) {\n    if (patterns.has(mime) || patterns.has(`${getCategory(mime)}/*`)) {\n      return true;\n    }\n  }\n\n  // If it doesn't match and the asset type is \"file\" and has a name,\n  // try detecting the actual MIME type from the filename extension\n  // This handles legacy assets that were incorrectly stored as type \"file\"\n  if (asset.type === \"file\" && asset.name) {\n    const extension = asset.name.split(\".\").pop()?.toLowerCase();\n    if (extension) {\n      const mimeFromExtension = extensionToMime.get(`.${extension}`);\n      if (mimeFromExtension) {\n        return (\n          patterns.has(mimeFromExtension) ||\n          patterns.has(`${getCategory(mimeFromExtension)}/*`)\n        );\n      }\n    }\n  }\n\n  return false;\n};\n\n/**\n * Validate a filename and return its extension and MIME type\n * @throws Error if file extension is not allowed\n */\nexport const validateFileName = (\n  fileName: string\n): { extension: string; mimeType: string } => {\n  const extension = fileName.split(\".\").pop()?.toLowerCase();\n\n  if (!extension) {\n    throw new Error(\"File must have an extension\");\n  }\n\n  if (!isAllowedExtension(extension)) {\n    throw new Error(\n      `File type \"${extension}\" is not allowed. Allowed types: ${Array.from(\n        ALLOWED_FILE_EXTENSIONS\n      ).join(\", \")}`\n    );\n  }\n\n  const mimeType = getMimeTypeByExtension(extension);\n  if (!mimeType) {\n    throw new Error(\n      `Could not determine MIME type for extension: ${extension}`\n    );\n  }\n\n  return { extension, mimeType };\n};\n\n/**\n * Detect the asset type from a file based on its extension\n */\nexport const detectAssetType = (\n  fileName: string\n): \"image\" | \"font\" | \"video\" | \"file\" => {\n  const ext = fileName.split(\".\").pop()?.toLowerCase();\n  if (!ext) {\n    return \"file\";\n  }\n\n  if (IMAGE_EXTENSIONS.includes(ext as AllowedFileExtension)) {\n    return \"image\";\n  }\n\n  if (FONT_EXTENSIONS.includes(ext as AllowedFileExtension)) {\n    return \"font\";\n  }\n\n  if (VIDEO_EXTENSIONS.includes(ext as AllowedFileExtension)) {\n    return \"video\";\n  }\n\n  return \"file\";\n};\n\n/**\n * Safely decode a URL path fragment, preventing path traversal attacks\n * @throws Error if the decoded path contains path traversal attempts\n */\nexport const decodePathFragment = (fragment: string): string => {\n  const decoded = decodeURIComponent(fragment);\n\n  // Prevent path traversal attacks\n  if (decoded.includes(\"..\") || decoded.startsWith(\"/\")) {\n    throw new Error(\"Invalid file path\");\n  }\n\n  return decoded;\n};\n\n/**\n * Generates the appropriate URL for an asset based on its type and format.\n * - Images use /cgi/image/ with format=raw\n * - All other assets (videos, audio, fonts, documents) use /cgi/asset/ with format=raw\n *\n * @param asset - The asset to generate URL for\n * @param origin - Origin to prepend (e.g., \"https://example.com\"). When provided, returns an absolute URL.\n * @returns A URL object. Use .pathname for relative paths, .href for absolute URLs\n */\nexport const getAssetUrl = (asset: Asset, origin: string): URL => {\n  let path: string;\n  const assetType = detectAssetType(asset.name);\n\n  if (assetType === \"image\") {\n    path = `/cgi/image/${asset.name}?format=raw`;\n  } else {\n    // Videos, audio, fonts, documents all use /cgi/asset/\n    path = `/cgi/asset/${asset.name}?format=raw`;\n  }\n\n  return new URL(path, origin);\n};\n\n/**\n * Runtime asset data structure with only fields needed at runtime.\n * This is a simplified version of the Asset type, optimized for client-side usage.\n */\nexport type RuntimeAsset = {\n  url: string;\n  width?: number;\n  height?: number;\n  family?: string;\n  style?: string;\n  weight?: number;\n};\n\n/**\n * Type-specific metadata extractors that define what runtime data each asset type needs.\n * Adding a new asset type requires implementing its extractor here.\n */\ntype RuntimeMetadata = Omit<RuntimeAsset, \"url\"> | undefined;\n\nconst extractImageMetadata = (asset: Asset): RuntimeMetadata => {\n  if (asset.type !== \"image\") {\n    return;\n  }\n  // Only include dimensions if they're non-zero\n  if (asset.meta.width && asset.meta.height) {\n    return {\n      width: asset.meta.width,\n      height: asset.meta.height,\n    };\n  }\n};\n\nconst extractFontMetadata = (asset: Asset): RuntimeMetadata => {\n  if (asset.type !== \"font\") {\n    return;\n  }\n  const metadata: Omit<RuntimeAsset, \"url\"> = {\n    family: asset.meta.family,\n  };\n  // Static fonts have style and weight, variable fonts have variationAxes\n  if (\"style\" in asset.meta) {\n    metadata.style = asset.meta.style;\n    metadata.weight = asset.meta.weight;\n  }\n  return metadata;\n};\n\nconst extractFileMetadata = (_asset: Asset): RuntimeMetadata => {\n  // Generic files don't need additional metadata at runtime\n  return;\n};\n\nconst metadataExtractors: Record<\n  Asset[\"type\"],\n  (asset: Asset) => RuntimeMetadata\n> = {\n  image: extractImageMetadata,\n  font: extractFontMetadata,\n  file: extractFileMetadata,\n};\n\n/**\n * Converts a full Asset to a minimal RuntimeAsset format.\n * This reduces payload size by including only runtime-needed data.\n *\n * Each asset type defines its own metadata extractor to ensure we only\n * include the fields that are actually needed at runtime.\n *\n * @param asset - The full asset object\n * @param origin - Origin to use for generating the asset URL (only used for URL construction, result is always a relative path)\n * @returns A minimal RuntimeAsset object with relative URL\n */\nexport const toRuntimeAsset = (asset: Asset, origin: string): RuntimeAsset => {\n  const extractor = metadataExtractors[asset.type];\n  const metadata = extractor(asset);\n\n  const url = getAssetUrl(asset, origin);\n  // Use pathname + search to get the relative path with query string\n  const relativeUrl = url.pathname + url.search;\n\n  return {\n    url: relativeUrl,\n    ...metadata,\n  };\n};\n"
  },
  {
    "path": "packages/sdk/src/core-metas.ts",
    "content": "import {\n  ContentBlockIcon,\n  ListViewIcon,\n  PaintBrushIcon,\n  SettingsIcon,\n  AddTemplateInstanceIcon,\n} from \"@webstudio-is/icons/svg\";\nimport { html } from \"./__generated__/normalize.css\";\nimport * as normalize from \"./__generated__/normalize.css\";\nimport type { WsComponentMeta } from \"./schema/component-meta\";\nimport type { Instance } from \"./schema/instances\";\nimport { tagProperty } from \"./runtime\";\nimport { tags } from \"./__generated__/tags\";\n\nexport const rootComponent = \"ws:root\";\n\nconst rootMeta: WsComponentMeta = {\n  label: \"Global root\",\n  icon: SettingsIcon,\n  presetStyle: {\n    html,\n  },\n};\n\nexport const elementComponent = \"ws:element\";\n\nconst elementMeta: WsComponentMeta = {\n  label: \"Element\",\n  // convert [object Module] to [object Object] to enable structured cloning\n  presetStyle: { ...normalize },\n  initialProps: [tagProperty, \"id\", \"class\"],\n  props: {\n    [tagProperty]: {\n      type: \"string\",\n      control: \"tag\",\n      required: true,\n      options: tags,\n    },\n  },\n};\n\nexport const portalComponent = \"Slot\";\n\nexport const collectionComponent = \"ws:collection\";\n\nconst collectionMeta: WsComponentMeta = {\n  label: \"Collection\",\n  icon: ListViewIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n  },\n  initialProps: [\"data\"],\n  props: {\n    data: {\n      required: true,\n      control: \"json\",\n      type: \"json\",\n    },\n    item: {\n      required: false,\n      control: \"text\",\n      type: \"string\",\n    },\n    itemKey: {\n      required: false,\n      control: \"text\",\n      type: \"string\",\n    },\n  },\n};\n\nexport const descendantComponent = \"ws:descendant\";\n\nconst descendantMeta: WsComponentMeta = {\n  label: \"Descendant\",\n  icon: PaintBrushIcon,\n  contentModel: {\n    category: \"none\",\n    children: [],\n  },\n  // @todo infer possible presets\n  presetStyle: {},\n  initialProps: [\"selector\"],\n  props: {\n    selector: {\n      required: true,\n      type: \"string\",\n      control: \"select\",\n      options: [\n        \" p\",\n        \" h1\",\n        \" h2\",\n        \" h3\",\n        \" h4\",\n        \" h5\",\n        \" h6\",\n        \" :where(strong, b)\",\n        \" :where(em, i)\",\n        \" a\",\n        \" img\",\n        \" blockquote\",\n        \" code\",\n        \" :where(ul, ol)\",\n        \" li\",\n        \" hr\",\n      ],\n    },\n  },\n};\n\nexport const blockComponent = \"ws:block\";\n\nexport const blockTemplateComponent = \"ws:block-template\";\n\nexport const blockTemplateMeta: WsComponentMeta = {\n  icon: AddTemplateInstanceIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n};\n\nconst blockMeta: WsComponentMeta = {\n  label: \"Content Block\",\n  icon: ContentBlockIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [blockTemplateComponent, \"instance\"],\n  },\n};\n\nexport const coreMetas = {\n  [rootComponent]: rootMeta,\n  [elementComponent]: elementMeta,\n  [collectionComponent]: collectionMeta,\n  [descendantComponent]: descendantMeta,\n  [blockComponent]: blockMeta,\n  [blockTemplateComponent]: blockTemplateMeta,\n};\n\n// components with custom implementation\n// should not be imported as react component\nexport const isCoreComponent = (component: Instance[\"component\"]) =>\n  component === rootComponent ||\n  component === elementComponent ||\n  component === collectionComponent ||\n  component === descendantComponent ||\n  component === blockComponent ||\n  component === blockTemplateComponent;\n\nexport const isComponentDetachable = (component: Instance[\"component\"]) =>\n  component !== rootComponent &&\n  component !== blockTemplateComponent &&\n  component !== descendantComponent;\n"
  },
  {
    "path": "packages/sdk/src/core-templates.tsx",
    "content": "import {\n  $,\n  css,\n  expression,\n  Parameter,\n  PlaceholderValue,\n  ws,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport {\n  CheckboxCheckedIcon,\n  RadioCheckedIcon,\n  Webstudio1cIcon,\n} from \"@webstudio-is/icons/svg\";\nimport {\n  blockComponent,\n  collectionComponent,\n  descendantComponent,\n  elementComponent,\n} from \"./core-metas\";\n\nconst elementMeta: TemplateMeta = {\n  category: \"general\",\n  order: 1,\n  description:\n    \"An HTML element is a core building block for web pages, structuring and displaying content like text, images, and links.\",\n  template: <ws.element></ws.element>,\n};\n\nconst linkMeta: TemplateMeta = {\n  category: \"general\",\n  description:\n    \"Use a link to send your users to another page, section, or resource. Configure links in the Settings panel.\",\n  order: 2,\n  template: (\n    <ws.element\n      ws:tag=\"a\"\n      ws:style={css`\n        display: inline-block;\n      `}\n    ></ws.element>\n  ),\n};\n\nconst collectionItem = new Parameter(\"collectionItem\");\nconst collectionItemKey = new Parameter(\"collectionItemKey\");\n\nconst collectionMeta: TemplateMeta = {\n  category: \"data\",\n  order: 2,\n  template: (\n    <ws.collection\n      data={[\"Collection Item 1\", \"Collection Item 2\", \"Collection Item 3\"]}\n      item={collectionItem}\n      itemKey={collectionItemKey}\n    >\n      <ws.element ws:tag=\"div\">\n        <ws.element ws:tag=\"div\">{expression`${collectionItem}`}</ws.element>\n      </ws.element>\n    </ws.collection>\n  ),\n};\n\nconst descendantMeta: TemplateMeta = {\n  category: \"internal\",\n  template: <ws.descendant selector=\" p\" />,\n};\n\nconst BlockTemplate = ws[\"block-template\"];\n\nconst blockMeta: TemplateMeta = {\n  category: \"general\",\n  template: (\n    <ws.block>\n      <BlockTemplate ws:label=\"Templates\">\n        <ws.element ws:label=\"Paragraph\" ws:tag=\"p\"></ws.element>\n        <ws.element ws:label=\"Heading 1\" ws:tag=\"h1\"></ws.element>\n        <ws.element ws:label=\"Heading 2\" ws:tag=\"h2\"></ws.element>\n        <ws.element ws:label=\"Heading 3\" ws:tag=\"h3\"></ws.element>\n        <ws.element ws:label=\"Heading 4\" ws:tag=\"h4\"></ws.element>\n        <ws.element ws:label=\"Heading 5\" ws:tag=\"h5\"></ws.element>\n        <ws.element ws:label=\"Heading 6\" ws:tag=\"h6\"></ws.element>\n        <ws.element ws:label=\"Unordered List\" ws:tag=\"ul\">\n          <ws.element ws:label=\"List Item\" ws:tag=\"li\"></ws.element>\n        </ws.element>\n        <ws.element ws:label=\"Ordered List\" ws:tag=\"ol\">\n          <ws.element ws:label=\"List Item\" ws:tag=\"li\"></ws.element>\n        </ws.element>\n        <ws.element ws:label=\"Link\" ws:tag=\"a\"></ws.element>\n        <$.Image\n          ws:style={css`\n            margin-right: auto;\n            margin-left: auto;\n            width: 100%;\n            height: auto;\n          `}\n        />\n        <ws.element ws:label=\"Separator\" ws:tag=\"hr\" />\n        <ws.element ws:label=\"Blockquote\" ws:tag=\"blockquote\"></ws.element>\n        <$.HtmlEmbed />\n        <ws.element ws:label=\"Code Text\" ws:tag=\"code\" />\n      </BlockTemplate>\n      <ws.element ws:label=\"Paragraph\" ws:tag=\"p\">\n        The Content Block component designates regions on the page where\n        pre-styled instances can be inserted in{\" \"}\n        <ws.element\n          ws:label=\"Link\"\n          ws:tag=\"a\"\n          href=\"https://wstd.us/content-block\"\n        >\n          Content mode\n        </ws.element>\n        .\n      </ws.element>\n      <ws.element ws:label=\"Unordered List\" ws:tag=\"ul\">\n        <ws.element ws:label=\"List Item\" ws:tag=\"li\">\n          In Content mode, you can edit any direct child instances that were\n          pre-added to the Content Block, as well as add new instances\n          predefined in Templates.\n        </ws.element>\n        <ws.element ws:label=\"List Item\" ws:tag=\"li\">\n          To predefine instances for insertion in Content mode, switch to Design\n          mode and add them to the Templates container.\n        </ws.element>\n        <ws.element ws:label=\"List Item\" ws:tag=\"li\">\n          To insert predefined instances in Content mode, click the + button\n          while hovering over the Content Block on the canvas and choose an\n          instance from the list.\n        </ws.element>\n      </ws.element>\n    </ws.block>\n  ),\n};\n\nconst typography: Record<string, TemplateMeta> = {\n  heading: {\n    category: \"typography\",\n    description:\n      \"Use HTML headings to structure and organize content. Use the Tag property in settings to change the heading level (h1-h6).\",\n    template: <ws.element ws:tag=\"h1\"></ws.element>,\n  },\n\n  paragraph: {\n    category: \"typography\",\n    description: \"A container for multi-line text.\",\n    template: <ws.element ws:tag=\"p\"></ws.element>,\n  },\n\n  blockquote: {\n    category: \"typography\",\n    description:\n      \"Use to style a quote from an external source like an article or book.\",\n    template: (\n      <ws.element\n        ws:tag=\"blockquote\"\n        ws:style={css`\n          margin-left: 0;\n          margin-right: 0;\n          padding: 10px 20px;\n          border-left: 5px solid rgb(226 226 226 / 1);\n        `}\n      ></ws.element>\n    ),\n  },\n\n  list: {\n    category: \"typography\",\n    description: \"Groups content, like links in a menu or steps in a recipe.\",\n    template: (\n      <ws.element ws:tag=\"ul\">\n        <ws.element ws:tag=\"li\"></ws.element>\n        <ws.element ws:tag=\"li\"></ws.element>\n        <ws.element ws:tag=\"li\"></ws.element>\n      </ws.element>\n    ),\n  },\n\n  list_item: {\n    category: \"typography\",\n    description: \"Adds a new item to an existing list.\",\n    template: <ws.element ws:tag=\"li\"></ws.element>,\n  },\n\n  code_text: {\n    category: \"typography\",\n    template: (\n      <ws.element\n        ws:tag=\"code\"\n        ws:style={css`\n          display: block;\n          white-space-collapse: preserve;\n          text-wrap-mode: wrap;\n          padding-left: 0.2em;\n          padding-right: 0.2em;\n          background-color: rgb(238 238 238);\n        `}\n      ></ws.element>\n    ),\n  },\n\n  thematic_break: {\n    category: \"typography\",\n    description:\n      \"Used to visually divide sections of content, helping to improve readability and organization within a webpage.\",\n    template: (\n      <ws.element\n        ws:tag=\"hr\"\n        ws:style={css`\n          color: gray;\n          border-style: none none solid;\n        `}\n      ></ws.element>\n    ),\n  },\n};\n\nconst forms: Record<string, TemplateMeta> = {\n  form: {\n    category: \"forms\",\n    description: \"Create filters, surveys, searches and more.\",\n    template: (\n      <ws.element ws:tag=\"form\">\n        <ws.element\n          ws:tag=\"input\"\n          ws:style={css`\n            display: block;\n          `}\n        />\n        <ws.element ws:tag=\"button\">\n          {new PlaceholderValue(\"Submit\")}\n        </ws.element>\n      </ws.element>\n    ),\n  },\n\n  button: {\n    category: \"forms\",\n    description:\n      \"Use a button to submit forms or trigger actions within a page. Do not use a button to navigate users to another resource or another page - that’s what a link is used for.\",\n    template: (\n      <ws.element ws:tag=\"button\">{new PlaceholderValue(\"Button\")}</ws.element>\n    ),\n  },\n\n  input_label: {\n    category: \"forms\",\n    template: (\n      <ws.element\n        ws:tag=\"label\"\n        ws:style={css`\n          display: block;\n        `}\n      >\n        {new PlaceholderValue(\"Label\")}\n      </ws.element>\n    ),\n  },\n\n  text_input: {\n    category: \"forms\",\n    description:\n      \"A single-line text input for collecting string data from your users.\",\n    template: (\n      <ws.element\n        ws:tag=\"input\"\n        ws:style={css`\n          display: block;\n        `}\n      />\n    ),\n  },\n\n  text_area: {\n    category: \"forms\",\n    description:\n      \"A multi-line text input for collecting longer string data from your users.\",\n    template: (\n      <ws.element\n        ws:tag=\"textarea\"\n        ws:style={css`\n          display: block;\n        `}\n      />\n    ),\n  },\n\n  select: {\n    category: \"forms\",\n    description:\n      \"A drop-down menu for users to select a single option from a predefined list.\",\n    template: (\n      <ws.element\n        ws:tag=\"select\"\n        ws:style={css`\n          display: block;\n        `}\n      >\n        <ws.element ws:tag=\"option\" label=\"Please choose an option\" value=\"\" />\n        <ws.element ws:tag=\"option\" label=\"Option A\" value=\"a\" />\n        <ws.element ws:tag=\"option\" label=\"Option B\" value=\"b\" />\n        <ws.element ws:tag=\"option\" label=\"Option C\" value=\"c\" />\n      </ws.element>\n    ),\n  },\n\n  radio: {\n    category: \"forms\",\n    description:\n      \"Use within a form to allow your users to select a single option from a set of mutually exclusive choices. Group multiple radios by matching their “Name” properties.\",\n    icon: RadioCheckedIcon,\n    template: (\n      <ws.element\n        ws:tag=\"label\"\n        ws:label=\"Radio Field\"\n        ws:style={css`\n          display: block;\n        `}\n      >\n        <ws.element\n          ws:tag=\"input\"\n          ws:style={css`\n            border-style: none;\n            margin-right: 0.5em;\n          `}\n          type=\"radio\"\n        />\n        <ws.element ws:tag=\"span\" ws:label=\"Radio Label\">\n          {new PlaceholderValue(\"Radio\")}\n        </ws.element>\n      </ws.element>\n    ),\n  },\n\n  checkbox: {\n    category: \"forms\",\n    description:\n      \"Use within a form to allow your users to toggle between checked and not checked. Group checkboxes by matching their “Name” properties. Unlike radios, any number of checkboxes in a group can be checked.\",\n    icon: CheckboxCheckedIcon,\n    template: (\n      <ws.element\n        ws:tag=\"label\"\n        ws:label=\"Checkbox Field\"\n        ws:style={css`\n          display: block;\n        `}\n      >\n        <ws.element\n          ws:tag=\"input\"\n          ws:style={css`\n            border-style: none;\n            margin-right: 0.5em;\n          `}\n          type=\"checkbox\"\n        />\n        <ws.element ws:tag=\"span\" ws:label=\"Checkbox Label\">\n          {new PlaceholderValue(\"Checkbox\")}\n        </ws.element>\n      </ws.element>\n    ),\n  },\n};\n\nconst builtWithWebstudioMeta: TemplateMeta = {\n  category: \"other\",\n  description:\n    \"A “Built with Webstudio” badge should be added to every project page on the free plan. This helps Webstudio spread awareness as a platform.\",\n  icon: Webstudio1cIcon,\n  template: (\n    <ws.element\n      ws:tag=\"a\"\n      ws:label=\"Built with Webstudio\"\n      // If you change this, you need to also update this link in publish checks\n      href=\"https://webstudio.is/?via=badge\"\n      target=\"_blank\"\n      ws:style={css`\n        display: inline-flex;\n        gap: 6px;\n        align-items: center;\n        justify-content: center;\n        position: fixed;\n        z-index: 1000;\n        padding: 6px 10px;\n        right: 16px;\n        bottom: 16px;\n        color: rgba(251, 252, 253, 1);\n        font-family: system-ui, sans-serif;\n        font-size: 12px;\n        font-weight: 500;\n        line-height: 1;\n        border: 1px solid transparent;\n        border-radius: 9px;\n        text-decoration-line: none;\n        text-wrap-mode: nowrap;\n        background-clip: padding-box, border-box;\n        background-origin: padding-box, border-box;\n        background-image:\n          linear-gradient(135deg, #4a4efa 0%, #bd2fdb 66%, #ec59ce 100%),\n          linear-gradient(\n            135deg,\n            #92fddc 0%,\n            #7d7ffb 31.94%,\n            #ed72fe 64.24%,\n            #fdd791 100%\n          );\n      `}\n    >\n      <$.HtmlEmbed\n        ws:label=\"Logo\"\n        code={Webstudio1cIcon}\n        ws:style={css`\n          display: block;\n          width: 16px;\n          height: 16px;\n          flex-shrink: 0;\n        `}\n      ></$.HtmlEmbed>\n      <ws.element ws:tag=\"div\" ws:label=\"Text\">\n        Built with Webstudio\n      </ws.element>\n    </ws.element>\n  ),\n};\n\nexport const coreTemplates = {\n  [elementComponent]: elementMeta,\n  link: linkMeta,\n  [collectionComponent]: collectionMeta,\n  [descendantComponent]: descendantMeta,\n  [blockComponent]: blockMeta,\n  ...typography,\n  ...forms,\n  builtWithWebstudio: builtWithWebstudioMeta,\n};\n"
  },
  {
    "path": "packages/sdk/src/css.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport { $, ws, css, renderData, createProxy } from \"@webstudio-is/template\";\nimport { generateCss, type CssConfig } from \"./css\";\nimport type { Breakpoint } from \"./schema/breakpoints\";\nimport { rootComponent } from \"./core-metas\";\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map((item) => [item.id, item] as const));\n\nconst generateAllCss = (config: Omit<CssConfig, \"atomic\">) => {\n  const { cssText, classes } = generateCss({\n    ...config,\n    atomic: false,\n  });\n  const { cssText: atomicCssText, classes: atomicClasses } = generateCss({\n    ...config,\n    atomic: true,\n  });\n  return { cssText, atomicCssText, classes, atomicClasses };\n};\n\ntest(\"generate css for one instance with two tokens\", () => {\n  const { cssText, atomicCssText, atomicClasses } = generateAllCss({\n    ...renderData(<$.Box ws:id=\"box\"></$.Box>),\n    breakpoints: toMap<Breakpoint>([{ id: \"base\", label: \"\" }]),\n    styleSourceSelections: new Map([\n      [\"box\", { instanceId: \"box\", values: [\"token\", \"local\"] }],\n    ]),\n    styles: new Map([\n      [\n        \"token:base:color\",\n        {\n          styleSourceId: \"local\",\n          breakpointId: \"base\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"red\" },\n        },\n      ],\n\n      [\n        \"local:base:color\",\n        {\n          styleSourceId: \"token\",\n          breakpointId: \"base\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"blue\" },\n        },\n      ],\n    ]),\n    componentMetas: new Map(),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"\n@media all {\n  .w-box {\n    color: red\n  }\n}\"\n`);\n  expect(atomicCssText).toMatchInlineSnapshot(`\n\"\n@media all {\n  .cawkhls {\n    color: red\n  }\n}\"\n`);\n  expect(atomicClasses).toMatchInlineSnapshot(`\nMap {\n  \"box\" => [\n    \"cawkhls\",\n  ],\n}\n`);\n});\n\ntest(\"generate descendant selector\", () => {\n  const { cssText, atomicCssText, atomicClasses } = generateAllCss({\n    ...renderData(\n      <$.Body\n        ws:id=\"body\"\n        ws:style={css`\n          color: blue;\n          &:hover {\n            color: red;\n          }\n        `}\n      >\n        <ws.descendant\n          ws:id=\"descendant\"\n          selector=\" a\"\n          ws:style={css`\n            color: blue;\n            &:hover {\n              color: red;\n            }\n          `}\n        />\n      </$.Body>\n    ),\n    componentMetas: new Map(),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"\n@media all {\n  .w-body {\n    color: blue\n  }\n  .w-body:hover {\n    color: red\n  }\n  .w-body a {\n    color: blue\n  }\n  .w-body a:hover {\n    color: red\n  }\n}\"\n`);\n  expect(atomicCssText).toMatchInlineSnapshot(`\n\"\n@media all {\n  .c17hlgoh {\n    color: blue\n  }\n  .c92zrdl:hover {\n    color: red\n  }\n  .chhpmat a {\n    color: blue\n  }\n  .c32fhpn a:hover {\n    color: red\n  }\n}\"\n`);\n  expect(atomicClasses).toMatchInlineSnapshot(`\nMap {\n  \"body\" => [\n    \"c17hlgoh\",\n    \"c92zrdl\",\n    \"chhpmat\",\n    \"c32fhpn\",\n  ],\n}\n`);\n});\n\ntest(\"generate component presets with multiple tags\", () => {\n  const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({\n    ...renderData(\n      <$.ListItem tag=\"div\">\n        <$.ListItem tag=\"a\"></$.ListItem>\n      </$.ListItem>\n    ),\n    assets: new Map(),\n    breakpoints: new Map(),\n    styleSourceSelections: new Map([]),\n    styles: new Map(),\n    componentMetas: new Map([\n      [\n        \"ListItem\",\n        {\n          presetStyle: {\n            div: css`\n              display: block;\n            `,\n            a: css`\n              user-select: none;\n            `,\n          },\n        },\n      ],\n    ]),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  div.w-list-item {\n    display: block\n  }\n  a.w-list-item {\n    -webkit-user-select: none;\n    user-select: none\n  }\n}\n\"\n`);\n  expect(cssText).toEqual(atomicCssText);\n  expect(classes).toEqual(\n    new Map([\n      [\"0\", [\"w-list-item\"]],\n      [\"1\", [\"w-list-item\"]],\n    ])\n  );\n  expect(classes).toEqual(atomicClasses);\n});\n\ntest(\"deduplicate component presets for similarly named components\", () => {\n  const radix = createProxy(\"@webstudio/radix:\");\n  const aria = createProxy(\"@webstudio/aria:\");\n  const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({\n    ...renderData(\n      <$.ListItem>\n        <radix.ListItem>\n          <aria.ListItem></aria.ListItem>\n        </radix.ListItem>\n      </$.ListItem>\n    ),\n    assets: new Map(),\n    breakpoints: new Map(),\n    styleSourceSelections: new Map([]),\n    styles: new Map(),\n    componentMetas: new Map([\n      [\n        \"ListItem\",\n        {\n          presetStyle: {\n            div: css`\n              display: block;\n            `,\n          },\n        },\n      ],\n      [\n        \"@webstudio/radix:ListItem\",\n        {\n          presetStyle: {\n            div: css`\n              display: flex;\n            `,\n          },\n        },\n      ],\n      [\n        \"@webstudio/aria:ListItem\",\n        {\n          presetStyle: {\n            div: css`\n              display: grid;\n            `,\n          },\n        },\n      ],\n    ]),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  div.w-list-item {\n    display: block\n  }\n  div.w-list-item-1 {\n    display: flex\n  }\n  div.w-list-item-2 {\n    display: grid\n  }\n}\n\"\n`);\n  expect(cssText).toEqual(atomicCssText);\n  expect(classes).toEqual(\n    new Map([\n      [\"0\", [\"w-list-item\"]],\n      [\"1\", [\"w-list-item-1\"]],\n      [\"2\", [\"w-list-item-2\"]],\n    ])\n  );\n  expect(classes).toEqual(atomicClasses);\n});\n\ntest(\"expose preset classes to instances\", () => {\n  const { atomicCssText, classes, atomicClasses } = generateAllCss({\n    ...renderData(\n      <$.Body\n        ws:id=\"body\"\n        ws:style={css`\n          color: blue;\n        `}\n      >\n        <$.Box\n          ws:id=\"box\"\n          ws:style={css`\n            color: red;\n          `}\n        ></$.Box>\n      </$.Body>\n    ),\n    componentMetas: new Map([\n      [\n        \"Body\",\n        {\n          presetStyle: {\n            div: css`\n              display: block;\n            `,\n          },\n        },\n      ],\n      [\n        \"Box\",\n        {\n          presetStyle: {\n            div: css`\n              display: flex;\n            `,\n          },\n        },\n      ],\n    ]),\n    assetBaseUrl: \"\",\n  });\n  expect(atomicCssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  div.w-body {\n    display: block\n  }\n  div.w-box {\n    display: flex\n  }\n}\n@media all {\n  .c17hlgoh {\n    color: blue\n  }\n  .cawkhls {\n    color: red\n  }\n}\"\n`);\n  expect(classes).toMatchInlineSnapshot(`\nMap {\n  \"body\" => [\n    \"w-body\",\n    \"w-body-1\",\n  ],\n  \"box\" => [\n    \"w-box\",\n    \"w-box-1\",\n  ],\n}\n`);\n  expect(atomicClasses).toMatchInlineSnapshot(`\nMap {\n  \"body\" => [\n    \"w-body\",\n    \"c17hlgoh\",\n  ],\n  \"box\" => [\n    \"w-box\",\n    \"cawkhls\",\n  ],\n}\n`);\n});\n\ntest(\"generate classes with instance and meta label\", () => {\n  const { cssText, classes } = generateAllCss({\n    ...renderData(\n      <$.Body\n        ws:id=\"body\"\n        ws:style={css`\n          color: blue;\n        `}\n      >\n        <$.Box\n          ws:id=\"box\"\n          ws:label=\"box%instance#label\"\n          ws:style={css`\n            color: red;\n          `}\n        ></$.Box>\n      </$.Body>\n    ),\n    componentMetas: new Map([\n      [\n        \"Body\",\n        {\n          label: \"body meta label\",\n          presetStyle: {\n            div: css`\n              display: block;\n            `,\n          },\n        },\n      ],\n      [\n        \"Box\",\n        {\n          label: \"box meta label\",\n          presetStyle: {\n            div: css`\n              display: flex;\n            `,\n          },\n        },\n      ],\n    ]),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  div.w-body-meta-label {\n    display: block\n  }\n  div.w-box-meta-label {\n    display: flex\n  }\n}\n@media all {\n  .w-body-meta-label-1 {\n    color: blue\n  }\n  .w-box-instance-label {\n    color: red\n  }\n}\"\n`);\n  expect(classes).toMatchInlineSnapshot(`\nMap {\n  \"body\" => [\n    \"w-body-meta-label\",\n    \"w-body-meta-label-1\",\n  ],\n  \"box\" => [\n    \"w-box-meta-label\",\n    \"w-box-instance-label\",\n  ],\n}\n`);\n});\n\ntest(\"generate :root preset and user styles\", () => {\n  const { cssText, atomicCssText, classes, atomicClasses } = generateAllCss({\n    ...renderData(\n      <$.Body ws:id=\"body\">\n        <$.Box ws:id=\"box\"></$.Box>\n      </$.Body>\n    ),\n    breakpoints: toMap([{ id: \"base\", label: \"\" }]),\n    styleSourceSelections: new Map([\n      [\":root\", { instanceId: \":root\", values: [\"local\"] }],\n    ]),\n    styles: new Map([\n      [\n        \"local:base:color\",\n        {\n          styleSourceId: \"local\",\n          breakpointId: \"base\",\n          property: \"color\",\n          value: { type: \"keyword\", value: \"blue\" },\n        },\n      ],\n      [\n        \"local:base:fontSize\",\n        {\n          styleSourceId: \"local\",\n          breakpointId: \"base\",\n          property: \"fontSize\",\n          value: { type: \"keyword\", value: \"medium\" },\n        },\n      ],\n    ]),\n    componentMetas: new Map([\n      [\n        rootComponent,\n        {\n          label: \"Global root\",\n          presetStyle: {\n            html: css`\n              display: grid;\n            `,\n          },\n        },\n      ],\n    ]),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  :root {\n    display: grid\n  }\n}\n@media all {\n  :root {\n    color: blue;\n    font-size: medium\n  }\n}\"\n`);\n  expect(classes).toEqual(new Map());\n  expect(atomicCssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  :root {\n    display: grid\n  }\n}\n@media all {\n  :root {\n    color: blue;\n    font-size: medium\n  }\n}\"\n`);\n  expect(atomicClasses).toEqual(new Map());\n});\n\ntest(\"generate presets only for used tags\", () => {\n  const { cssText, classes } = generateCss({\n    ...renderData(\n      <$.Body ws:id=\"body\">\n        {/* first tag in preset */}\n        <$.Box></$.Box>\n        {/* legacy tag property */}\n        <$.Box tag=\"span\"></$.Box>\n        {/* modern ws:tag property */}\n        <$.Box ws:tag=\"article\"></$.Box>\n      </$.Body>\n    ),\n    atomic: false,\n    breakpoints: toMap([{ id: \"base\", label: \"\" }]),\n    styleSourceSelections: new Map(),\n    styles: new Map(),\n    componentMetas: new Map([\n      [\n        \"Box\",\n        {\n          presetStyle: {\n            div: css`\n              display: block;\n            `,\n            span: css`\n              display: block;\n            `,\n            article: css`\n              display: block;\n            `,\n            section: css`\n              display: block;\n            `,\n            main: css`\n              display: block;\n            `,\n          },\n        },\n      ],\n    ]),\n    assetBaseUrl: \"\",\n  });\n  expect(cssText).toMatchInlineSnapshot(`\n\"@layer presets {\n  div.w-box {\n    display: block\n  }\n  span.w-box {\n    display: block\n  }\n  article.w-box {\n    display: block\n  }\n}\n\"`);\n  expect(classes).toEqual(\n    new Map([\n      [\"0\", [\"w-box\"]],\n      [\"1\", [\"w-box\"]],\n      [\"2\", [\"w-box\"]],\n    ])\n  );\n});\n"
  },
  {
    "path": "packages/sdk/src/css.ts",
    "content": "import { kebabCase } from \"change-case\";\nimport {\n  createRegularStyleSheet,\n  generateAtomic,\n  type NestingRule,\n  type StyleSheetRegular,\n  type TransformValue,\n} from \"@webstudio-is/css-engine\";\nimport { getFontFaces } from \"@webstudio-is/fonts\";\nimport type { Assets, FontAsset } from \"./schema/assets\";\nimport type { Instance, Instances } from \"./schema/instances\";\nimport type { Props } from \"./schema/props\";\nimport type { Breakpoints } from \"./schema/breakpoints\";\nimport type { Styles } from \"./schema/styles\";\nimport type { StyleSourceSelections } from \"./schema/style-source-selections\";\nimport type { WsComponentMeta } from \"./schema/component-meta\";\nimport { createScope } from \"./scope\";\nimport { parseComponentName, ROOT_INSTANCE_ID } from \"./instances-utils\";\nimport { descendantComponent, rootComponent } from \"./core-metas\";\n\nexport const addFontRules = ({\n  sheet,\n  assets,\n  assetBaseUrl,\n}: {\n  sheet: StyleSheetRegular;\n  assets: Assets;\n  assetBaseUrl: string;\n}) => {\n  const fontAssets: FontAsset[] = [];\n  for (const asset of assets.values()) {\n    if (asset.type === \"font\") {\n      fontAssets.push(asset);\n    }\n  }\n  const fontFaces = getFontFaces(fontAssets, { assetBaseUrl });\n  for (const fontFace of fontFaces) {\n    sheet.addFontFaceRule(fontFace);\n  }\n};\n\nexport type CssConfig = {\n  assets: Assets;\n  instances: Instances;\n  props: Props;\n  breakpoints: Breakpoints;\n  styles: Styles;\n  styleSourceSelections: StyleSourceSelections;\n  componentMetas: Map<string, WsComponentMeta>;\n  assetBaseUrl: string;\n  atomic: boolean;\n};\n\nexport const createImageValueTransformer =\n  (\n    assets: Assets,\n    { assetBaseUrl }: { assetBaseUrl: string }\n  ): TransformValue =>\n  (styleValue) => {\n    if (styleValue.type === \"image\" && styleValue.value.type === \"asset\") {\n      const asset = assets.get(styleValue.value.value);\n      if (asset === undefined) {\n        return { type: \"keyword\", value: \"none\" };\n      }\n\n      // @todo reuse image loaders and generate image-set\n      const url = `${assetBaseUrl}${asset.name}`;\n\n      return {\n        type: \"image\",\n        value: {\n          type: \"url\",\n          url,\n        },\n        hidden: styleValue.hidden,\n      };\n    }\n  };\n\nconst normalizeClassName = (name: string) => kebabCase(name);\n\nexport const generateCss = ({\n  assets,\n  instances,\n  props,\n  breakpoints,\n  styles,\n  styleSourceSelections,\n  componentMetas,\n  assetBaseUrl,\n  atomic,\n}: CssConfig) => {\n  const fontSheet = createRegularStyleSheet({ name: \"ssr\" });\n  const presetSheet = createRegularStyleSheet({ name: \"ssr\" });\n  const userSheet = createRegularStyleSheet({ name: \"ssr\" });\n\n  addFontRules({ sheet: fontSheet, assets, assetBaseUrl });\n  presetSheet.addMediaRule(\"presets\");\n  const presetClasses = new Map<Instance[\"component\"], string>();\n  const scope = createScope([], normalizeClassName, \"-\");\n\n  const tagsByComponent = new Map<Instance[\"component\"], Set<string>>();\n  tagsByComponent.set(rootComponent, new Set([\"html\"]));\n  const tagByInstanceId = new Map<Instance[\"id\"], string>();\n  for (const prop of props.values()) {\n    if (prop.type === \"string\" && prop.name === \"tag\") {\n      tagByInstanceId.set(prop.instanceId, prop.value);\n    }\n  }\n  for (const instance of instances.values()) {\n    const propTag = tagByInstanceId.get(instance.id);\n    const meta = componentMetas.get(instance.component);\n    const metaTag = Object.keys(meta?.presetStyle ?? {}).at(0);\n    let componentTags = tagsByComponent.get(instance.component);\n    if (componentTags === undefined) {\n      componentTags = new Set();\n      tagsByComponent.set(instance.component, componentTags);\n    }\n    const tag = instance.tag ?? propTag ?? metaTag;\n    if (tag) {\n      componentTags.add(tag);\n    }\n  }\n\n  for (const [component, meta] of componentMetas) {\n    const componentTags = tagsByComponent.get(component);\n    const [_namespace, componentName] = parseComponentName(component);\n    const className = `w-${scope.getName(component, meta.label ?? componentName)}`;\n    const presetStyle = Object.entries(meta.presetStyle ?? {});\n    if (presetStyle.length > 0) {\n      // add preset class only when at least one style is defined\n      presetClasses.set(component, className);\n    }\n    for (const [tag, styles] of presetStyle) {\n      // ignore unused tags\n      if (!componentTags?.has(tag)) {\n        continue;\n      }\n      const selector =\n        component === rootComponent ? \":root\" : `${tag}.${className}`;\n      const rule = presetSheet.addNestingRule(selector);\n      for (const declaration of styles) {\n        rule.setDeclaration({\n          breakpoint: \"presets\",\n          selector: declaration.state ?? \"\",\n          property: declaration.property,\n          value: declaration.value,\n        });\n      }\n    }\n  }\n\n  for (const breakpoint of breakpoints.values()) {\n    userSheet.addMediaRule(breakpoint.id, breakpoint);\n  }\n\n  const imageValueTransformer = createImageValueTransformer(assets, {\n    assetBaseUrl,\n  });\n  userSheet.setTransformer(imageValueTransformer);\n\n  for (const styleDecl of styles.values()) {\n    const rule = userSheet.addMixinRule(styleDecl.styleSourceId);\n    rule.setDeclaration({\n      breakpoint: styleDecl.breakpointId,\n      selector: styleDecl.state ?? \"\",\n      property: styleDecl.property,\n      value: styleDecl.value,\n    });\n  }\n\n  const classes = new Map<Instance[\"id\"], string[]>();\n  const parentIdByInstanceId = new Map<Instance[\"id\"], Instance[\"id\"]>();\n  for (const instance of instances.values()) {\n    const presetClass = presetClasses.get(instance.component);\n    if (presetClass) {\n      classes.set(instance.id, [presetClass]);\n    }\n    for (const child of instance.children) {\n      if (child.type === \"id\") {\n        parentIdByInstanceId.set(child.value, instance.id);\n      }\n    }\n  }\n\n  const descendantSelectorByInstanceId = new Map<Instance[\"id\"], string>();\n  for (const prop of props.values()) {\n    if (prop.name === \"selector\" && prop.type === \"string\") {\n      descendantSelectorByInstanceId.set(prop.instanceId, prop.value);\n    }\n  }\n\n  const instanceByRule = new Map<NestingRule, Instance[\"id\"]>();\n  for (const selection of styleSourceSelections.values()) {\n    let { instanceId } = selection;\n    const { values } = selection;\n    // special case for :root styles\n    if (instanceId === ROOT_INSTANCE_ID) {\n      const rule = userSheet.addNestingRule(`:root`);\n      rule.applyMixins(values);\n      // avoid storing in instanceByRule to prevent conversion into atomic styles\n      continue;\n    }\n    let descendantSuffix = \"\";\n    // render selector component as descendant selector\n    const instance = instances.get(instanceId);\n    if (instance === undefined) {\n      continue;\n    }\n    if (instance.component === descendantComponent) {\n      const parentId = parentIdByInstanceId.get(instanceId);\n      const descendantSelector = descendantSelectorByInstanceId.get(instanceId);\n      if (parentId && descendantSelector) {\n        descendantSuffix = descendantSelector;\n        instanceId = parentId;\n      }\n    }\n    const meta = componentMetas.get(instance.component);\n    const [_namespace, shortName] = parseComponentName(instance.component);\n    const baseName = instance.label ?? meta?.label ?? shortName;\n    const className = `w-${scope.getName(instanceId, baseName)}`;\n    if (atomic === false) {\n      let classList = classes.get(instanceId);\n      if (classList === undefined) {\n        classList = [];\n        classes.set(instanceId, classList);\n      }\n      classList.push(className);\n    }\n    const rule = userSheet.addNestingRule(`.${className}`, descendantSuffix);\n    rule.applyMixins(values);\n    instanceByRule.set(rule, instanceId);\n  }\n\n  const fontCss = fontSheet.cssText;\n  // render presets inside of cascade layer to let user completely override all properties\n  // user agent (browser) styles work in the same way\n  // for example a { color: black } overrides a:visited as well\n  // @todo figure out proper API to work with layers when more use cases are known\n  const presetCss = presetSheet.cssText.replaceAll(\n    \"@media all \",\n    \"@layer presets \"\n  );\n\n  if (atomic) {\n    const { cssText } = generateAtomic(userSheet, {\n      getKey: (rule) => instanceByRule.get(rule),\n      transformValue: imageValueTransformer,\n      classes,\n    });\n    return {\n      cssText: `${fontCss}${presetCss}\\n${cssText}`,\n      classes,\n    };\n  }\n  return {\n    cssText: `${fontCss}${presetCss}\\n${userSheet.cssText}`,\n    classes,\n  };\n};\n"
  },
  {
    "path": "packages/sdk/src/expression.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport {\n  type Diagnostic,\n  decodeDataVariableId,\n  encodeDataVariableId,\n  executeExpression,\n  isLiteralExpression,\n  lintExpression,\n  transpileExpression,\n  getExpressionIdentifiers,\n  parseObjectExpression,\n  generateObjectExpression,\n  SYSTEM_VARIABLE_ID,\n} from \"./expression\";\n\ndescribe(\"lint expression\", () => {\n  const error = (from: number, to: number, message: string): Diagnostic => ({\n    from,\n    to,\n    severity: \"error\",\n    message,\n  });\n\n  const warn = (from: number, to: number, message: string): Diagnostic => ({\n    from,\n    to,\n    severity: \"warning\",\n    message,\n  });\n\n  test(\"forbid empty expression\", () => {\n    expect(lintExpression({ expression: `` })).toEqual([\n      error(0, 0, `Expression cannot be empty`),\n    ]);\n    expect(lintExpression({ expression: `  ` })).toEqual([\n      error(0, 0, `Expression cannot be empty`),\n    ]);\n  });\n\n  test(\"output parse error as diagnostic\", () => {\n    expect(lintExpression({ expression: `a + ` })).toEqual([\n      error(4, 4, \"Unexpected token\"),\n    ]);\n    expect(lintExpression({ expression: `\"string\" + a)` })).toEqual([\n      error(13, 13, \"Unexpected token\"),\n    ]);\n  });\n\n  test(\"restrict expression syntax\", () => {\n    expect(lintExpression({ expression: `var a = 1` })).toEqual([\n      error(0, 0, \"Unexpected token\"),\n    ]);\n  });\n\n  test(\"lint whole expression instead of only first valid part\", () => {\n    expect(\n      lintExpression({ expression: `a\"\"`, availableVariables: new Set([\"a\"]) })\n    ).toEqual([error(1, 1, `Unexpected token`)]);\n    expect(\n      lintExpression({\n        expression: `/movie/{{CollectionItem['title']}}\\n`,\n        availableVariables: new Set([\"\"]),\n      })\n    ).toEqual([error(7, 7, `Unexpected token`)]);\n  });\n\n  test(\"supports accessing variable fields\", () => {\n    expect(\n      lintExpression({\n        expression: `a.b.c`,\n        availableVariables: new Set([\"a\"]),\n      })\n    ).toEqual([]);\n  });\n\n  test(\"supports literals\", () => {\n    expect(\n      lintExpression({\n        expression: `\"\" + 0 + true + [] + {}`,\n      })\n    ).toEqual([]);\n  });\n\n  test(\"supports ternary operator\", () => {\n    expect(\n      lintExpression({\n        expression: `true ? false : true`,\n      })\n    ).toEqual([]);\n  });\n\n  test(\"supports template literals\", () => {\n    expect(\n      lintExpression({\n        expression: \"`my ${1} template ${'2'}`\",\n      })\n    ).toEqual([]);\n    expect(\n      lintExpression({\n        expression: \"`my template\",\n      })\n    ).toEqual([error(1, 1, \"Unterminated template\")]);\n  });\n\n  test(\"supports parentheses\", () => {\n    expect(\n      lintExpression({\n        expression: \"(1 + 1) * 2\",\n      })\n    ).toEqual([]);\n  });\n\n  test(\"forbid assignment until enabled\", () => {\n    expect(\n      lintExpression({\n        expression: ` a = 1`,\n        availableVariables: new Set([\"a\"]),\n      })\n    ).toEqual([error(1, 6, \"Assignment is supported only inside actions\")]);\n    expect(\n      lintExpression({\n        expression: ` a = 1`,\n        allowAssignment: true,\n        availableVariables: new Set([\"a\"]),\n      })\n    ).toEqual([]);\n  });\n\n  test(\"forbid unavailable variables\", () => {\n    expect(\n      lintExpression({ expression: ` a = b + 1`, allowAssignment: true })\n    ).toEqual([\n      warn(5, 6, `\"b\" is not defined in the scope`),\n      warn(1, 2, `\"a\" is not defined in the scope`),\n    ]);\n    expect(\n      lintExpression({\n        expression: ` a = b + 1`,\n        allowAssignment: true,\n        availableVariables: new Set([\"a\", \"b\"]),\n      })\n    ).toEqual([]);\n  });\n\n  test(`forbid \"this\" keyword`, () => {\n    expect(lintExpression({ expression: ` this.name` })).toEqual([\n      error(1, 5, `\"this\" keyword is not supported`),\n    ]);\n  });\n\n  test(\"forbid functions\", () => {\n    expect(\n      lintExpression({\n        expression: ` function(){} + (() => {}) + fn()`,\n        availableVariables: new Set([\"fn\"]),\n      })\n    ).toEqual([\n      error(1, 13, \"Functions are not supported\"),\n      error(17, 25, \"Functions are not supported\"),\n      error(29, 33, `\"fn\" function is not supported`),\n    ]);\n  });\n\n  test(\"forbid increment and decrement\", () => {\n    expect(\n      lintExpression({\n        expression: ` ++i + --j`,\n        availableVariables: new Set([\"j\", \"i\"]),\n      })\n    ).toEqual([\n      error(1, 4, \"Increment and decrement are not supported\"),\n      error(7, 10, \"Increment and decrement are not supported\"),\n    ]);\n  });\n\n  test(\"forbid sequence expression\", () => {\n    expect(lintExpression({ expression: ` 1, 2, 3` })).toEqual([\n      error(1, 8, \"Only single expression is supported\"),\n    ]);\n  });\n\n  test(`forbid \"yield\" keyword`, () => {\n    expect(lintExpression({ expression: ` yield 1` })).toEqual([\n      error(1, 1, `The keyword 'yield' is reserved`),\n    ]);\n  });\n\n  test(\"forbid tagged template\", () => {\n    expect(\n      lintExpression({\n        expression: \" tag`hello`\",\n        availableVariables: new Set([\"tag\"]),\n      })\n    ).toEqual([error(1, 11, \"Tagged template is not supported\")]);\n  });\n\n  test(\"forbid classes\", () => {\n    expect(\n      lintExpression({\n        expression: ` class {} + new MyClass()`,\n        availableVariables: new Set([\"MyClass\"]),\n      })\n    ).toEqual([\n      error(1, 9, \"Classes are not supported\"),\n      error(12, 25, \"Classes are not supported\"),\n    ]);\n  });\n\n  test(\"forbid imports\", () => {\n    expect(\n      lintExpression({ expression: ` import(\"\") + import.meta.url` })\n    ).toEqual([\n      error(1, 11, \"Imports are not supported\"),\n      error(14, 25, \"Imports are not supported\"),\n    ]);\n  });\n\n  test(`forbid \"await\" keyword`, () => {\n    expect(lintExpression({ expression: ` await 1` })).toEqual([\n      error(1, 8, `\"await\" keyword is not supported`),\n    ]);\n  });\n\n  test.each([\n    \"toLowerCase\",\n    \"replace\",\n    \"split\",\n    \"at\",\n    \"slice\",\n    \"endsWith\",\n    \"includes\",\n    \"startsWith\",\n    \"toUpperCase\",\n    \"toLocaleLowerCase\",\n    \"toLocaleUpperCase\",\n  ])(\"allow safe string method: %s\", (method) => {\n    expect(\n      lintExpression({\n        expression: `title.${method}()`,\n        availableVariables: new Set([\"title\"]),\n      })\n    ).toEqual([]);\n  });\n\n  test.each([\"at\", \"includes\", \"join\", \"slice\"])(\n    \"allow safe array method: %s\",\n    (method) => {\n      expect(\n        lintExpression({\n          expression: `arr.${method}()`,\n          availableVariables: new Set([\"arr\"]),\n        })\n      ).toEqual([]);\n    }\n  );\n\n  test(\"allow chained string methods\", () => {\n    expect(\n      lintExpression({\n        expression: `title.toLowerCase().replace(\" \", \"-\").split(\"-\")`,\n        availableVariables: new Set([\"title\"]),\n      })\n    ).toEqual([]);\n  });\n\n  test(\"forbid unsafe method calls\", () => {\n    expect(\n      lintExpression({\n        expression: `arr.pop()`,\n        availableVariables: new Set([\"arr\"]),\n      })\n    ).toEqual([error(0, 9, `\"pop\" function is not supported`)]);\n    expect(\n      lintExpression({\n        expression: `obj.push(1)`,\n        availableVariables: new Set([\"obj\"]),\n      })\n    ).toEqual([error(0, 11, `\"push\" function is not supported`)]);\n  });\n\n  test(\"forbid standalone function calls\", () => {\n    expect(\n      lintExpression({\n        expression: `func()`,\n        availableVariables: new Set([\"func\"]),\n      })\n    ).toEqual([error(0, 6, `\"func\" function is not supported`)]);\n  });\n});\n\ntest(\"check simple literals\", () => {\n  expect(isLiteralExpression(`\"\"`)).toEqual(true);\n  expect(isLiteralExpression(`''`)).toEqual(true);\n  expect(isLiteralExpression(`0`)).toEqual(true);\n  expect(isLiteralExpression(`true`)).toEqual(true);\n  expect(isLiteralExpression(`[]`)).toEqual(true);\n  expect(isLiteralExpression(`{}`)).toEqual(true);\n  expect(isLiteralExpression(`undefined`)).toEqual(true);\n  expect(isLiteralExpression(`\"\" + \"\"`)).toEqual(false);\n  expect(isLiteralExpression(`{}.field`)).toEqual(false);\n  expect(isLiteralExpression(`variable`)).toEqual(false);\n});\n\ntest(\"check complex objects and arrays\", () => {\n  expect(isLiteralExpression(`[1, 2, 3]`)).toEqual(true);\n  expect(isLiteralExpression(`[1, 2, variable]`)).toEqual(false);\n  expect(isLiteralExpression(`[...variable]`)).toEqual(false);\n  expect(isLiteralExpression(`{ param: 0 }`)).toEqual(true);\n  expect(isLiteralExpression(`{ \"param\": 0 }`)).toEqual(true);\n  expect(isLiteralExpression(`{ param: variable }`)).toEqual(false);\n  expect(isLiteralExpression(`{ [\"param\"]: 0 }`)).toEqual(true);\n  expect(isLiteralExpression(`{ [variable]: 0 }`)).toEqual(false);\n  expect(isLiteralExpression(`{ ...variable }`)).toEqual(false);\n});\n\ndescribe(\"get expression identifiers\", () => {\n  test(\"find all identifiers\", () => {\n    expect(getExpressionIdentifiers(\"a = b * c.d\")).toEqual(\n      new Set([\"a\", \"b\", \"c\"])\n    );\n  });\n\n  test(\"deduplicate identifiers\", () => {\n    expect(getExpressionIdentifiers(\"a = a + b\")).toEqual(new Set([\"a\", \"b\"]));\n  });\n\n  test(\"not fail when invalid syntax\", () => {\n    expect(getExpressionIdentifiers(\"\")).toEqual(new Set());\n    expect(getExpressionIdentifiers(\"a = a +\")).toEqual(new Set());\n  });\n});\n\ndescribe(\"transpile expression\", () => {\n  test(\"preserve spaces and parentheses\", () => {\n    expect(\n      transpileExpression({ expression: \" 1 + (2 + 3) \", executable: true })\n    ).toEqual(\" 1 + (2 + 3) \");\n  });\n\n  test(\"add optional chaining with dot syntax\", () => {\n    expect(\n      transpileExpression({ expression: \"a.b . c\", executable: true })\n    ).toEqual(\"a?.b ?. c\");\n  });\n\n  test(\"add optional chaining with computed\", () => {\n    expect(\n      transpileExpression({ expression: \"a['b'] [c]\", executable: true })\n    ).toEqual(\"a?.['b'] ?.[c]\");\n  });\n\n  test(\"skip optional chaining\", () => {\n    expect(\n      transpileExpression({ expression: \"a?.['b']?.c\", executable: true })\n    ).toEqual(\"a?.['b']?.c\");\n  });\n\n  test(\"replace variable\", () => {\n    expect(\n      transpileExpression({\n        expression: \"(a + c) * b\",\n        replaceVariable: (identifier) => identifier + \"_1\",\n      })\n    ).toEqual(\"(a_1 + c_1) * b_1\");\n    expect(\n      transpileExpression({\n        expression: \"a = c\",\n        replaceVariable: (identifier) => identifier + \"_1\",\n      })\n    ).toEqual(\"a_1 = c_1\");\n  });\n\n  test(\"skip identifiers in nested member expressions\", () => {\n    const identifiers: string[] = [];\n    const transpiled = transpileExpression({\n      expression: \"a.b + c.d * e[f]\",\n      replaceVariable: (identifier) => {\n        identifiers.push(identifier);\n        return identifier + \"_1\";\n      },\n    });\n    expect(identifiers).toEqual([\"a\", \"c\", \"e\", \"f\"]);\n    expect(transpiled).toEqual(\"a_1.b + c_1.d * e_1[f_1]\");\n  });\n\n  test(\"inform identifier is an assignee\", () => {\n    expect(\n      transpileExpression({\n        expression: \"a = b\",\n        replaceVariable: (identifier, assignee) => {\n          const suffix = assignee ? \"_assignee\" : \"_assigner\";\n          return identifier + suffix;\n        },\n      })\n    ).toEqual(\"a_assignee = b_assigner\");\n  });\n\n  test(\"transpile object literal without changes\", () => {\n    expect(\n      transpileExpression({\n        expression: `{ ...name }`,\n      })\n    ).toEqual(`{ ...name }`);\n  });\n\n  test(\"output more readable syntax error\", () => {\n    let errorString = \"\";\n    try {\n      transpileExpression({ expression: `` });\n    } catch (error) {\n      errorString = (error as Error).message;\n    }\n    expect(errorString).toEqual(`Unexpected token (1:0) in \"\"`);\n  });\n\n  test(\"transpile string methods with optional chaining\", () => {\n    expect(\n      transpileExpression({\n        expression: \"title.toLowerCase()\",\n        executable: true,\n      })\n    ).toEqual(\"title?.toLowerCase?.()\");\n    expect(\n      transpileExpression({\n        expression: \"user.name.replace(' ', '-')\",\n        executable: true,\n      })\n    ).toEqual(\"user?.name?.replace?.(' ', '-')\");\n    expect(\n      transpileExpression({\n        expression: \"data.title.split('-')\",\n        executable: true,\n      })\n    ).toEqual(\"data?.title?.split?.('-')\");\n  });\n\n  test(\"transpile chained string methods with optional chaining\", () => {\n    expect(\n      transpileExpression({\n        expression: \"title.toLowerCase().replace(/\\\\s+/g, '-')\",\n        executable: true,\n      })\n    ).toEqual(\"title?.toLowerCase?.()?.replace?.(/\\\\s+/g, '-')\");\n    expect(\n      transpileExpression({\n        expression: \"user.name.toLowerCase().replace(' ', '-').split('-')\",\n        executable: true,\n      })\n    ).toEqual(\"user?.name?.toLowerCase?.()?.replace?.(' ', '-')?.split?.('-')\");\n  });\n\n  test(\"transpile array methods with optional chaining\", () => {\n    expect(\n      transpileExpression({\n        expression: \"items.map(item => item.id)\",\n        executable: true,\n      })\n    ).toEqual(\"items?.map?.(item => item?.id)\");\n  });\n\n  test(\"transpile nested method calls with optional chaining\", () => {\n    expect(\n      transpileExpression({\n        expression: \"obj.method().prop.anotherMethod()\",\n        executable: true,\n      })\n    ).toEqual(\"obj?.method?.()?.prop?.anotherMethod?.()\");\n  });\n\n  test(\"preserve existing optional chaining\", () => {\n    expect(\n      transpileExpression({\n        expression: \"obj?.method?.()\",\n        executable: true,\n      })\n    ).toEqual(\"obj?.method?.()\");\n    expect(\n      transpileExpression({\n        expression: \"obj?.prop?.method?.()\",\n        executable: true,\n      })\n    ).toEqual(\"obj?.prop?.method?.()\");\n  });\n});\n\ndescribe(\"object expression transformations\", () => {\n  test(\"parse object expression\", () => {\n    expect(parseObjectExpression(`{ a: 0, b: \"\", c: $c + 1 }`)).toEqual(\n      new Map([\n        [\"a\", `0`],\n        [\"b\", `\"\"`],\n        [\"c\", `$c + 1`],\n      ])\n    );\n  });\n\n  test(\"parse unsupported syntax\", () => {\n    expect(parseObjectExpression(``)).toEqual(new Map());\n    expect(parseObjectExpression(`0`)).toEqual(new Map());\n    expect(parseObjectExpression(`{ a: 0, ...spread }`)).toEqual(\n      new Map([[\"a\", \"0\"]])\n    );\n    expect(parseObjectExpression(`{ a: 0, [b]: 0 }`)).toEqual(\n      new Map([[\"a\", \"0\"]])\n    );\n    expect(parseObjectExpression(`{ \"a-b\": 0 }`)).toEqual(\n      new Map([[\"a-b\", \"0\"]])\n    );\n  });\n\n  test(\"generate object expression\", () => {\n    expect(\n      generateObjectExpression(\n        new Map([\n          [\"a\", `0`],\n          [\"b-c\", `\"\"`],\n          [\"d\", `$d`],\n        ])\n      )\n    ).toMatchInlineSnapshot(`\n    \"{\n      \"a\": 0,\n      \"b-c\": \"\",\n      \"d\": $d,\n    }\"\n    `);\n  });\n\n  test(\"generate empty object expression\", () => {\n    expect(generateObjectExpression(new Map())).toMatchInlineSnapshot(`\n    \"{\n    }\"\n    `);\n  });\n});\n\ntest(\"encode/decode variable names\", () => {\n  expect(encodeDataVariableId(\"my--id\")).toEqual(\n    \"$ws$dataSource$my__DASH____DASH__id\"\n  );\n  expect(decodeDataVariableId(encodeDataVariableId(\"my--id\"))).toEqual(\n    \"my--id\"\n  );\n  expect(decodeDataVariableId(\"myVarName\")).toEqual(undefined);\n  expect(encodeDataVariableId(SYSTEM_VARIABLE_ID)).toEqual(\"$ws$system\");\n  expect(\n    decodeDataVariableId(encodeDataVariableId(SYSTEM_VARIABLE_ID))\n  ).toEqual(SYSTEM_VARIABLE_ID);\n});\n\ntest(\"execute expression\", () => {\n  expect(executeExpression(undefined)).toEqual(undefined);\n  expect(executeExpression(\"1 + 1\")).toEqual(2);\n  expect(executeExpression(\"someVariable + 1\")).toEqual(undefined);\n});\n"
  },
  {
    "path": "packages/sdk/src/expression.ts",
    "content": "import {\n  type Expression,\n  type Identifier,\n  parse,\n  parseExpressionAt,\n} from \"acorn\";\nimport { simple } from \"acorn-walk\";\nimport type { DataSource, DataSources } from \"./schema/data-sources\";\nimport type { Scope } from \"./scope\";\nimport { ROOT_INSTANCE_ID } from \"./instances-utils\";\n\nexport const SYSTEM_VARIABLE_ID = \":system\";\n\nexport const systemParameter: DataSource = {\n  id: SYSTEM_VARIABLE_ID,\n  scopeInstanceId: ROOT_INSTANCE_ID,\n  type: \"parameter\",\n  name: \"system\",\n};\n\nexport type Diagnostic = {\n  from: number;\n  to: number;\n  severity: \"error\" | \"hint\" | \"info\" | \"warning\";\n  message: string;\n};\n\ntype ExpressionVisitor = {\n  [K in Expression[\"type\"]]: (node: Extract<Expression, { type: K }>) => void;\n};\n\nexport const allowedStringMethods = new Set([\n  \"toLowerCase\",\n  \"replace\",\n  \"split\",\n  \"slice\",\n  \"at\",\n  \"endsWith\",\n  \"includes\",\n  \"startsWith\",\n  \"toUpperCase\",\n  \"toLocaleLowerCase\",\n  \"toLocaleUpperCase\",\n]);\n\nexport const allowedArrayMethods = new Set([\"at\", \"includes\", \"join\", \"slice\"]);\n\nexport const lintExpression = ({\n  expression,\n  availableVariables = new Set(),\n  allowAssignment = false,\n}: {\n  expression: string;\n  availableVariables?: Set<Identifier[\"name\"]>;\n  allowAssignment?: boolean;\n}): Diagnostic[] => {\n  const diagnostics: Diagnostic[] = [];\n  const addMessage = (\n    message: string,\n    severity: \"error\" | \"warning\" = \"error\"\n  ) => {\n    return (node: Expression) => {\n      diagnostics.push({\n        // tune error position after wrapping expression with parentheses\n        from: node.start - 1,\n        to: node.end - 1,\n        severity,\n        message: message,\n      });\n    };\n  };\n  // allow empty expression\n  if (expression.trim().length === 0) {\n    diagnostics.push({\n      from: 0,\n      to: 0,\n      severity: \"error\",\n      message: \"Expression cannot be empty\",\n    });\n    return diagnostics;\n  }\n  try {\n    // wrap expression with parentheses to force acorn parse whole expression\n    // instead of just first valid part\n    const root = parse(`(${expression})`, {\n      ecmaVersion: \"latest\",\n      // support parsing import to forbid explicitly\n      sourceType: \"module\",\n    });\n\n    simple(root, {\n      Identifier(node) {\n        if (availableVariables.has(node.name) === false) {\n          addMessage(\n            `\"${node.name}\" is not defined in the scope`,\n            \"warning\"\n          )(node);\n        }\n      },\n      Literal() {},\n      ArrayExpression() {},\n      ObjectExpression() {},\n      UnaryExpression() {},\n      BinaryExpression() {},\n      LogicalExpression() {},\n      MemberExpression() {},\n      ConditionalExpression() {},\n      TemplateLiteral() {},\n      ChainExpression() {},\n      ParenthesizedExpression() {},\n      AssignmentExpression(node) {\n        if (allowAssignment === false) {\n          addMessage(\"Assignment is supported only inside actions\")(node);\n          return;\n        }\n        simple(node.left, {\n          Identifier(node) {\n            if (availableVariables.has(node.name) === false) {\n              addMessage(\n                `\"${node.name}\" is not defined in the scope`,\n                \"warning\"\n              )(node);\n            }\n          },\n        });\n      },\n      // parser forbids to yield inside module\n      YieldExpression() {},\n      ThisExpression: addMessage(`\"this\" keyword is not supported`),\n      FunctionExpression: addMessage(\"Functions are not supported\"),\n      UpdateExpression: addMessage(\"Increment and decrement are not supported\"),\n      CallExpression(node) {\n        let calleeName;\n        if (node.callee.type === \"MemberExpression\") {\n          if (node.callee.property.type === \"Identifier\") {\n            const methodName = node.callee.property.name;\n            if (\n              allowedStringMethods.has(methodName) ||\n              allowedArrayMethods.has(methodName)\n            ) {\n              return;\n            }\n            calleeName = methodName;\n          }\n        } else if (node.callee.type === \"Identifier\") {\n          calleeName = node.callee.name;\n        }\n        if (calleeName) {\n          addMessage(`\"${calleeName}\" function is not supported`)(node);\n        } else {\n          addMessage(\"Functions are not supported\")(node);\n        }\n      },\n      NewExpression: addMessage(\"Classes are not supported\"),\n      SequenceExpression: addMessage(`Only single expression is supported`),\n      ArrowFunctionExpression: addMessage(\"Functions are not supported\"),\n      TaggedTemplateExpression: addMessage(\"Tagged template is not supported\"),\n      ClassExpression: addMessage(\"Classes are not supported\"),\n      MetaProperty: addMessage(\"Imports are not supported\"),\n      AwaitExpression: addMessage(`\"await\" keyword is not supported`),\n      ImportExpression: addMessage(\"Imports are not supported\"),\n    } satisfies ExpressionVisitor);\n  } catch (error) {\n    const castedError = error as { message: string; pos: number };\n    diagnostics.push({\n      // tune error position after wrapping expression with parentheses\n      from: castedError.pos - 1,\n      to: castedError.pos - 1,\n      severity: \"error\",\n      // trim auto generated error location\n      // to not conflict with tuned position\n      message: castedError.message.replaceAll(/\\s+\\(\\d+:\\d+\\)$/g, \"\"),\n    });\n  }\n  return diagnostics;\n};\n\nconst isLiteralNode = (node: Expression): boolean => {\n  if (node.type === \"Identifier\" && node.name === \"undefined\") {\n    return true;\n  }\n  if (node.type === \"Literal\") {\n    return true;\n  }\n  if (node.type === \"ArrayExpression\") {\n    return node.elements.every((node) => {\n      if (node === null || node.type === \"SpreadElement\") {\n        return false;\n      }\n      return isLiteralNode(node);\n    });\n  }\n  if (node.type === \"ObjectExpression\") {\n    return node.properties.every((property) => {\n      if (property.type === \"SpreadElement\") {\n        return false;\n      }\n      const key = property.key;\n      const isIdentifierKey =\n        key.type === \"Identifier\" && property.computed === false;\n      const isLiteralKey = key.type === \"Literal\";\n      return (isLiteralKey || isIdentifierKey) && isLiteralNode(property.value);\n    });\n  }\n  return false;\n};\n\n/**\n * check whether provided expression is a literal value\n * like \"\", 0 or { param: \"value\" }\n * which does not depends on any variable\n */\nexport const isLiteralExpression = (expression: string) => {\n  try {\n    const node = parseExpressionAt(expression, 0, { ecmaVersion: \"latest\" });\n    return isLiteralNode(node);\n  } catch {\n    // treat invalid expression as non-literal\n    return false;\n  }\n};\n\nexport const getExpressionIdentifiers = (expression: string) => {\n  const identifiers = new Set<string>();\n  try {\n    const root = parseExpressionAt(expression, 0, { ecmaVersion: \"latest\" });\n    simple(root, {\n      Identifier: (node) => identifiers.add(node.name),\n      AssignmentExpression(node) {\n        simple(node.left, {\n          Identifier: (node) => identifiers.add(node.name),\n        });\n      },\n    });\n  } catch {\n    // empty block\n  }\n  return identifiers;\n};\n\n/**\n * transpile expression into executable one\n *\n * add optional chaining operator to every member expression\n * to access any field without runtime errors\n *\n * replace variable names if necessary\n */\nexport const transpileExpression = ({\n  expression,\n  executable = false,\n  replaceVariable,\n}: {\n  expression: string;\n  executable?: boolean;\n  replaceVariable?: (\n    identifier: string,\n    assignee: boolean\n  ) => string | undefined | void;\n}) => {\n  let root;\n  try {\n    root = parseExpressionAt(expression, 0, { ecmaVersion: \"latest\" });\n  } catch (error) {\n    const message = (error as Error).message;\n    // throw new error to trace error in our code instead of acorn\n    throw Error(`${message} in ${JSON.stringify(expression)}`);\n  }\n  const replacements: [start: number, end: number, fragment: string][] = [];\n  const replaceIdentifier = (node: Identifier, assignee: boolean) => {\n    const newName = replaceVariable?.(node.name, assignee);\n    if (newName) {\n      replacements.push([node.start, node.end, newName]);\n    }\n  };\n  simple(root, {\n    Identifier: (node) => replaceIdentifier(node, false),\n    AssignmentExpression(node) {\n      simple(node.left, {\n        Identifier: (node) => replaceIdentifier(node, true),\n      });\n    },\n    MemberExpression(node) {\n      if (executable === false || node.optional) {\n        return;\n      }\n      // a . b -> a ?. b\n      if (node.computed === false) {\n        const dotIndex = expression.indexOf(\".\", node.object.end);\n        replacements.push([dotIndex, dotIndex, \"?\"]);\n      }\n      // a [b] -> a ?.[b]\n      if (node.computed === true) {\n        const dotIndex = expression.indexOf(\"[\", node.object.end);\n        replacements.push([dotIndex, dotIndex, \"?.\"]);\n      }\n    },\n    CallExpression(node) {\n      if (executable === false || node.optional) {\n        return;\n      }\n      // Add optional chaining to method calls: obj.method() -> obj?.method?.()\n      if (node.callee.type === \"MemberExpression\") {\n        // Find the opening parenthesis after the method name\n        const openParenIndex = expression.indexOf(\"(\", node.callee.end);\n        if (openParenIndex !== -1) {\n          replacements.push([openParenIndex, openParenIndex, \"?.\"]);\n        }\n      }\n    },\n  });\n  // order from the latest to the first insertion to not break other positions\n  replacements.sort(([leftStart], [rightStart]) => rightStart - leftStart);\n  for (const [start, end, fragment] of replacements) {\n    const before = expression.slice(0, start);\n    const after = expression.slice(end);\n    expression = before + fragment + after;\n  }\n  return expression;\n};\n\n/**\n * parse object expression into key value map\n * where each value is expression\n */\nexport const parseObjectExpression = (expression: string) => {\n  const map = new Map<string, string>();\n  let root;\n  try {\n    root = parseExpressionAt(expression, 0, { ecmaVersion: \"latest\" });\n  } catch (error) {\n    return map;\n  }\n  if (root.type !== \"ObjectExpression\") {\n    return map;\n  }\n  for (const property of root.properties) {\n    if (property.type === \"SpreadElement\") {\n      continue;\n    }\n    if (property.computed) {\n      continue;\n    }\n    let key;\n    if (property.key.type === \"Identifier\") {\n      key = property.key.name;\n    } else if (\n      property.key.type === \"Literal\" &&\n      typeof property.key.value === \"string\"\n    ) {\n      key = property.key.value;\n    } else {\n      continue;\n    }\n    const valueExpression = expression.slice(\n      property.value.start,\n      property.value.end\n    );\n    map.set(key, valueExpression);\n  }\n  return map;\n};\n\n/**\n * generate key value map into object expression\n * after updating individual value expressions\n */\nexport const generateObjectExpression = (map: Map<string, string>) => {\n  let generated = \"{\\n\";\n  for (const [key, valueExpression] of map) {\n    const keyExpression = JSON.stringify(key);\n    generated += `  ${keyExpression}: ${valueExpression},\\n`;\n  }\n  generated += `}`;\n  return generated;\n};\n\nconst dataSourceVariablePrefix = \"$ws$dataSource$\";\n\n// data source id is generated with nanoid which has \"-\" in alphabeta\n// here \"-\" is encoded with \"__DASH__' in variable name\n// https://github.com/ai/nanoid/blob/047686abad8f15aff05f3a2eeedb7c98b6847392/url-alphabet/index.js\n\nexport const encodeDataVariableId = (id: string) => {\n  if (id === SYSTEM_VARIABLE_ID) {\n    return \"$ws$system\";\n  }\n  const encoded = id.replaceAll(\"-\", \"__DASH__\");\n  return `${dataSourceVariablePrefix}${encoded}`;\n};\nexport { encodeDataVariableId as encodeDataSourceVariable };\n\nexport const decodeDataVariableId = (name: string) => {\n  if (name === \"$ws$system\") {\n    return SYSTEM_VARIABLE_ID;\n  }\n  if (name.startsWith(dataSourceVariablePrefix)) {\n    const encoded = name.slice(dataSourceVariablePrefix.length);\n    return encoded.replaceAll(\"__DASH__\", \"-\");\n  }\n  return;\n};\nexport { decodeDataVariableId as decodeDataSourceVariable };\n\nexport const generateExpression = ({\n  expression,\n  dataSources,\n  usedDataSources,\n  scope,\n}: {\n  expression: string;\n  dataSources: DataSources;\n  usedDataSources: DataSources;\n  scope: Scope;\n}) => {\n  return transpileExpression({\n    expression,\n    executable: true,\n    replaceVariable: (identifier) => {\n      const depId = decodeDataVariableId(identifier);\n      let dep = depId ? dataSources.get(depId) : undefined;\n      if (depId === SYSTEM_VARIABLE_ID) {\n        dep = systemParameter;\n      }\n      if (dep) {\n        usedDataSources?.set(dep.id, dep);\n        return scope.getName(dep.id, dep.name);\n      }\n      return \"undefined\";\n    },\n  });\n};\n\n/**\n * edge case utility for \"statoc\" expression without variables\n */\nexport const executeExpression = (expression: undefined | string) => {\n  try {\n    const fn = new Function(`return (${expression})`);\n    return fn();\n  } catch {\n    // empty block\n  }\n};\n"
  },
  {
    "path": "packages/sdk/src/form-fields.ts",
    "content": "/**\n * Used to identify form inside server handler\n */\nexport const formIdFieldName = `ws--form-id`;\n/**\n * Used for simlpe protection against non js bots\n */\nexport const formBotFieldName = `ws--form-bot`;\n\n/**\n * Detects if the browser is Brave.\n * Brave Shields blocks our bot protection mechanism (matchMedia fingerprinting detection),\n * causing form submissions to silently fail.\n * @see https://github.com/brave/brave-browser/issues/46541\n */\nexport const isBraveBrowser = (): boolean => {\n  if (typeof navigator === \"undefined\") {\n    return false;\n  }\n  // @ts-expect-error - brave is a non-standard property\n  return navigator.brave?.isBrave?.() === true || navigator.brave !== undefined;\n};\n"
  },
  {
    "path": "packages/sdk/src/index.ts",
    "content": "export * from \"./schema/assets\";\nexport * from \"./schema/pages\";\nexport * from \"./schema/instances\";\nexport * from \"./schema/data-sources\";\nexport * from \"./schema/resources\";\nexport * from \"./schema/props\";\nexport * from \"./schema/breakpoints\";\nexport * from \"./schema/style-sources\";\nexport * from \"./schema/style-source-selections\";\nexport * from \"./schema/styles\";\nexport * from \"./schema/deployment\";\nexport * from \"./schema/webstudio\";\nexport * from \"./schema/prop-meta\";\nexport * from \"./schema/component-meta\";\n\nexport * from \"./assets\";\nexport * from \"./core-metas\";\nexport * from \"./instances-utils\";\nexport * from \"./page-utils\";\nexport * from \"./scope\";\nexport * from \"./expression\";\nexport * from \"./resources-generator\";\nexport * from \"./page-meta-generator\";\nexport * from \"./url-pattern\";\nexport * from \"./css\";\nexport * from \"./__generated__/tags\";\n\nexport type {\n  AnimationAction,\n  AnimationActionScroll,\n  AnimationActionView,\n  AnimationKeyframe,\n  KeyframeStyles,\n  RangeUnit,\n  RangeUnitValue,\n  ScrollNamedRange,\n  ScrollRangeValue,\n  ViewNamedRange,\n  ViewRangeValue,\n  ScrollAnimation,\n  ViewAnimation,\n  InsetUnitValue,\n  DurationUnitValue,\n  IterationsUnitValue,\n  TimeUnit,\n} from \"./schema/animation-schema\";\n\nexport {\n  animationActionSchema,\n  scrollAnimationSchema,\n  viewAnimationSchema,\n  rangeUnitValueSchema,\n  animationKeyframeSchema,\n  insetUnitValueSchema,\n  durationUnitValueSchema,\n  RANGE_UNITS,\n} from \"./schema/animation-schema\";\n"
  },
  {
    "path": "packages/sdk/src/instances-utils.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport { $, renderData, ws } from \"@webstudio-is/template\";\nimport {\n  findTreeInstanceIds,\n  findTreeInstanceIdsExcludingSlotDescendants,\n  getIndexesWithinAncestors,\n  parseComponentName,\n} from \"./instances-utils\";\nimport type { WsComponentMeta } from \"./schema/component-meta\";\nimport type { Instance } from \"./schema/instances\";\n\ntest(\"find all tree instances\", () => {\n  const { instances } = renderData(\n    <$.Body ws:id=\"1\">\n      <$.Box ws:id=\"2\"></$.Box>\n      <$.Box ws:id=\"3\">\n        <$.Box ws:id=\"4\"></$.Box>\n        <$.Box ws:id=\"5\"></$.Box>\n      </$.Box>\n    </$.Body>\n  );\n  expect(findTreeInstanceIds(instances, \"3\")).toEqual(new Set([\"3\", \"4\", \"5\"]));\n});\n\ntest(\"find all tree instances excluding slot descendants\", () => {\n  const { instances } = renderData(\n    <$.Body ws:id=\"body\">\n      <$.Box ws:id=\"box1\">\n        <$.Slot ws:id=\"slot\">\n          <$.Box ws:id=\"slotbox1\"></$.Box>\n          <$.Box ws:id=\"slotbox2\"></$.Box>\n        </$.Slot>\n        <$.Box ws:id=\"box2\"></$.Box>\n      </$.Box>\n      <$.Box ws:id=\"box3\"></$.Box>\n    </$.Body>\n  );\n  expect(\n    findTreeInstanceIdsExcludingSlotDescendants(instances, \"box1\")\n  ).toEqual(new Set([\"box1\", \"slot\", \"box2\"]));\n});\n\ntest(\"include not existing/virtual instance\", () => {\n  const { instances } = renderData(<$.Body ws:id=\"1\"></$.Body>);\n  expect(findTreeInstanceIds(instances, \":root\")).toEqual(new Set([\":root\"]));\n  expect(\n    findTreeInstanceIdsExcludingSlotDescendants(instances, \":root\")\n  ).toEqual(new Set([\":root\"]));\n});\n\ntest(\"extract short name and namespace from component name\", () => {\n  expect(parseComponentName(\"Box\")).toEqual([undefined, \"Box\"]);\n  expect(parseComponentName(\"radix:Box\")).toEqual([\"radix\", \"Box\"]);\n});\n\ntest(\"get indexes within ancestors\", () => {\n  const { instances } = renderData(\n    <$.Body ws:id=\"body0\">\n      <$.Tabs ws:id=\"tabs1\">\n        <$.TabsList ws:id=\"tabs1list\">\n          <$.Box>\n            <$.TabsTrigger ws:id=\"tabs1trigger1\"></$.TabsTrigger>\n            <$.TabsTrigger ws:id=\"tabs1trigger2\"></$.TabsTrigger>\n          </$.Box>\n        </$.TabsList>\n        <$.TabsContent ws:id=\"tabs1content1\"></$.TabsContent>\n        <$.TabsContent ws:id=\"tabs1content2\">\n          <$.Tabs ws:id=\"tabs2\">\n            <$.TabsList ws:id=\"tabs2list\">\n              <$.TabsTrigger ws:id=\"tabs2trigger1\"></$.TabsTrigger>\n            </$.TabsList>\n            <$.TabsContent ws:id=\"tabs2content1\"></$.TabsContent>\n          </$.Tabs>\n        </$.TabsContent>\n      </$.Tabs>\n    </$.Body>\n  );\n  const metas = new Map<Instance[\"component\"], WsComponentMeta>([\n    [\"TabsList\", { indexWithinAncestor: \"Tabs\" }],\n    [\"TabsTrigger\", { indexWithinAncestor: \"TabsList\" }],\n    [\"TabsContent\", { indexWithinAncestor: \"Tabs\" }],\n  ]);\n  expect(getIndexesWithinAncestors(metas, instances, [\"body0\"])).toEqual(\n    new Map([\n      [\"tabs1list\", 0],\n      [\"tabs1trigger1\", 0],\n      [\"tabs1trigger2\", 1],\n      [\"tabs1content1\", 0],\n      [\"tabs1content2\", 1],\n      // reset nested ones\n      [\"tabs2list\", 0],\n      [\"tabs2trigger1\", 0],\n      [\"tabs2content1\", 0],\n    ])\n  );\n});\n\ntest(\"ignore ws:block-template when compute indexes within ancestors\", () => {\n  const BlockTemplate = ws[\"block-template\"];\n  const { instances } = renderData(\n    <$.Body ws:id=\"body0\">\n      <$.Tabs>\n        <BlockTemplate>\n          <$.TabsTrigger ws:id=\"trigger1\"></$.TabsTrigger>\n        </BlockTemplate>\n        <$.TabsTrigger ws:id=\"trigger2\"></$.TabsTrigger>\n        <$.TabsTrigger ws:id=\"trigger3\"></$.TabsTrigger>\n      </$.Tabs>\n    </$.Body>\n  );\n  const metas = new Map<Instance[\"component\"], WsComponentMeta>([\n    [\"TabsTrigger\", { indexWithinAncestor: \"Tabs\" }],\n  ]);\n  expect(getIndexesWithinAncestors(metas, instances, [\"body0\"])).toEqual(\n    new Map([\n      [\"trigger2\", 0],\n      [\"trigger3\", 1],\n      // reset the one inside of block template\n      [\"trigger1\", 0],\n    ])\n  );\n});\n"
  },
  {
    "path": "packages/sdk/src/instances-utils.ts",
    "content": "import type { WsComponentMeta } from \"./schema/component-meta\";\nimport type { Instance, Instances } from \"./schema/instances\";\nimport { blockTemplateComponent } from \"./core-metas\";\n\nexport const ROOT_INSTANCE_ID = \":root\";\n\nconst traverseInstances = (\n  instances: Instances,\n  instanceId: Instance[\"id\"],\n  callback: (instance: Instance) => false | void\n) => {\n  const instance = instances.get(instanceId);\n  if (instance === undefined) {\n    return;\n  }\n  const skipTraversingChildren = callback(instance);\n  if (skipTraversingChildren === false) {\n    return;\n  }\n  for (const child of instance.children) {\n    if (child.type === \"id\") {\n      traverseInstances(instances, child.value, callback);\n    }\n  }\n};\n\nexport const findTreeInstanceIds = (\n  instances: Instances,\n  rootInstanceId: Instance[\"id\"]\n) => {\n  const ids = new Set<Instance[\"id\"]>([rootInstanceId]);\n  traverseInstances(instances, rootInstanceId, (instance) => {\n    ids.add(instance.id);\n  });\n  return ids;\n};\n\nexport const findTreeInstanceIdsExcludingSlotDescendants = (\n  instances: Instances,\n  rootInstanceId: Instance[\"id\"]\n) => {\n  const ids = new Set<Instance[\"id\"]>([rootInstanceId]);\n  traverseInstances(instances, rootInstanceId, (instance) => {\n    ids.add(instance.id);\n    if (instance.component === \"Slot\") {\n      return false;\n    }\n  });\n  return ids;\n};\n\nexport const parseComponentName = (componentName: string) => {\n  const parts = componentName.split(\":\");\n  let namespace: undefined | string;\n  let name: string;\n  if (parts.length === 1) {\n    [name] = parts;\n  } else {\n    [namespace, name] = parts;\n  }\n  return [namespace, name] as const;\n};\n\nexport type IndexesWithinAncestors = Map<Instance[\"id\"], number>;\n\nexport const getIndexesWithinAncestors = (\n  metas: Map<Instance[\"component\"], WsComponentMeta>,\n  instances: Instances,\n  rootIds: Instance[\"id\"][]\n) => {\n  const ancestors = new Set<Instance[\"component\"]>();\n  for (const meta of metas.values()) {\n    if (meta.indexWithinAncestor !== undefined) {\n      ancestors.add(meta.indexWithinAncestor);\n    }\n  }\n\n  const indexes: IndexesWithinAncestors = new Map();\n\n  const traverseInstances = (\n    instances: Instances,\n    instanceId: Instance[\"id\"],\n    latestIndexes = new Map<\n      Instance[\"component\"],\n      Map<Instance[\"component\"], number>\n    >()\n  ) => {\n    const instance = instances.get(instanceId);\n    if (instance === undefined) {\n      return;\n    }\n    const meta = metas.get(instance.component);\n\n    // reset for both nested ancestors and block template\n    if (ancestors.has(instance.component)) {\n      latestIndexes = new Map(latestIndexes);\n      latestIndexes.set(instance.component, new Map());\n    }\n    if (instance.component === blockTemplateComponent) {\n      latestIndexes = new Map(latestIndexes);\n      for (const key of latestIndexes.keys()) {\n        latestIndexes.set(key, new Map());\n      }\n    }\n\n    if (meta?.indexWithinAncestor !== undefined) {\n      const ancestorIndexes = latestIndexes.get(meta.indexWithinAncestor);\n      if (ancestorIndexes) {\n        let index = ancestorIndexes.get(instance.component) ?? -1;\n        index += 1;\n        ancestorIndexes.set(instance.component, index);\n        indexes.set(instance.id, index);\n      }\n    }\n\n    for (const child of instance.children) {\n      if (child.type === \"id\") {\n        traverseInstances(instances, child.value, latestIndexes);\n      }\n    }\n  };\n\n  const latestIndexes = new Map();\n  for (const instanceId of rootIds) {\n    traverseInstances(instances, instanceId, latestIndexes);\n  }\n\n  return indexes;\n};\n"
  },
  {
    "path": "packages/sdk/src/normalize.css",
    "content": "/**\n * Based on https://github.com/sindresorhus/modern-normalize\n *\n * Attributions\n *\n * The MIT License (MIT)\n * Copyright (c) Nicolas Gallagher\n * Copyright (c) Jonathan Neal\n * Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\n */\n\n/**\n * We dont support rules like this now, implement box-sizing for each used element\n * *,\n * ::before,\n * ::after {\n *   box-sizing: border-box;\n * }\n */\n\ndiv,\naddress,\narticle,\naside,\nfigure,\nfooter,\nheader,\nmain,\nnav,\nsection,\nform,\nlabel,\ntime,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\ni,\nimg,\na,\nli,\nul,\nol,\np,\nspan {\n  /**\n   * Use a better box model (opinionated).\n   */\n  box-sizing: border-box;\n}\n\n/**\n * 1. Layout source https://twitter.com/ChallengesCss/status/1471128244720181258\n * 2. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n * 3. For visual editors\n */\nhtml {\n  /* 1 */\n  display: grid;\n  min-height: 100%;\n  grid-template-rows: auto;\n  grid-template-columns: 1fr;\n  /* 2 */\n  font-family: Arial, Roboto, sans-serif;\n  font-size: 16px;\n  line-height: 1.2;\n  /* webstudio custom opinionated preset */\n  /* 3. We decided to use preserve in visual builders:\n    Preserves multiple spaces & trailing spaces,\n    Matches what users see while editing to final output (Provides more predictable WYSIWYG experience)\n\n  vs text editors' collapse (default): Normalizes multiple spaces into one, Removes trailing whitespace, Better for clean text content\n  */\n  white-space-collapse: preserve;\n}\n\n/**\n * 1. Remove the margin in all browsers.\n */\nbody {\n  /* 1 */\n  margin: 0;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n/**\n * 1. Add the correct height in Firefox.\n * 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n * 3. width: 100% inside flexbox will overflow <hr> out of it\n */\nhr {\n  /* 1 */\n  height: 0;\n  /* 2 */\n  color: inherit;\n  /* 3 */\n  box-sizing: border-box;\n}\n\n/**\nAdd the correct text decoration in Chrome, Edge, and Safari.\n\n!!!Skipped as we don't support this logic!!!\n\nabbr[title] {\n  text-decoration: underline dotted;\n}\n*/\n\n/**\n * Add the correct font weight in Edge and Safari.\n */\nb,\nstrong {\n  font-weight: 700;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\n/**\n * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n * 2. Correct the odd 'em' font sizing in all browsers.\n */\ncode,\nkbd,\nsamp,\npre {\n  /* 1 */\n  font-family:\n    ui-monospace, SFMono-Regular, Consolas, \"Liberation Mono\", Menlo, monospace;\n  /* 2 */\n  font-size: 1em;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\n/**\n * Add the correct font size in all browsers.\n */\nsmall {\n  font-size: 80%;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\n/**\n * Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.\n */\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/*\nTabular data\n============\n*/\n\n/**\n * 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n * 2. Correct table border color inheritance in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n */\ntable {\n  /* 1 */\n  text-indent: 0;\n  /* 2 */\n  border-color: inherit;\n  box-sizing: border-box;\n}\n\n/*\nForms\n=====\n*/\n\ninput,\ntextarea,\noptgroup,\n/* These are non-standard tags, defined this way to be able to access in component presets */\nradio,\ncheckbox,\nbutton,\nselect {\n  /* 1 */\n  font-family: inherit;\n  font-size: 100%;\n  line-height: 1.15;\n  /* 2 */\n  margin: 0;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\n/* Input and Textarea uses border style inset by default, wich we don't support in style panel. */\ninput,\ntextarea {\n  border-style: solid;\n}\n\n/*\nRadio and checkbox have by default border style \"none\", we are setting it here to reflect in the style panel.\n\nThese are non-standard tags, defined this way to be able to access in component presets\n*/\nradio,\ncheckbox {\n  border-style: none;\n}\n\n/**\n * Remove the inheritance of text transform in Edge and Firefox.\n */\nbutton,\nselect {\n  border-style: solid;\n  text-transform: none;\n}\n\n/**\nCorrect the inability to style clickable types in iOS and Safari.\n\n!!!Skipped as we don't support this logic!!!\n\nbutton,\n[type='button'],\n[type='reset'],\n[type='submit'] {\n  -webkit-appearance: button;\n}\n*/\n\n/**\nRemove the inner border and padding in Firefox.\n\n!!!Skipped as we don't support this logic!!!\n\n::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n*/\n\n/**\nRestore the focus styles unset by the previous rule.\n\n!!!Skipped as we don't support this logic!!!\n\n:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n*/\n\n/**\nRemove the additional ':invalid' styles in Firefox.\nSee: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737\n\n!!!Skipped as we don't support this logic!!!\n\n:-moz-ui-invalid {\n  box-shadow: none;\n}\n*/\n\n/**\n * Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.\n */\nlegend {\n  padding: 0;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\n/**\n * Add the correct vertical alignment in Chrome and Firefox.\n */\nprogress {\n  vertical-align: baseline;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n\n/**\nCorrect the cursor style of increment and decrement buttons in Safari.\n\n!!!Skipped as we don't support this logic!!!\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n  height: auto;\n}\n*/\n\n/**\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n\n!!!Skipped as we don't support this logic!!!\n\n[type='search'] {\n  -webkit-appearance: textfield;\n  outline-offset: -2px;\n}\n*/\n\n/**\nRemove the inner padding in Chrome and Safari on macOS.\n\n!!!Skipped as we don't support this logic!!!\n\n::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n*/\n\n/**\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to 'inherit' in Safari.\n\n!!!Skipped as we don't support this logic!!!\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button;\n  font: inherit;\n}\n*/\n\n/*\nInteractive\n===========\n*/\n\n/**\n * Add the correct display in Chrome and Safari.\n */\nsummary {\n  display: list-item;\n  /* webstudio custom opinionated presets */\n  box-sizing: border-box;\n}\n"
  },
  {
    "path": "packages/sdk/src/page-meta-generator.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { createScope } from \"./scope\";\nimport { generatePageMeta } from \"./page-meta-generator\";\nimport type { Asset } from \"./schema/assets\";\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map((item) => [item.id, item] as const));\n\ntest(\"generate minimal static page meta factory\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `\"Page title\"`,\n        meta: {},\n      },\n      dataSources: new Map(),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Page title\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate complete static page meta factory\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `\"Page title\"`,\n        meta: {\n          description: `\"Page description\"`,\n          excludePageFromSearch: \"true\",\n          language: `\"en-US\"`,\n          socialImageAssetId: \"social-image-id\",\n          status: `302`,\n          redirect: `\"/new-path\"`,\n          custom: [\n            { property: \"custom-property-1\", content: `\"custom content 1\"` },\n            { property: \"custom-property-2\", content: `\"custom content 2\"` },\n          ],\n        },\n      },\n      dataSources: new Map(),\n      assets: new Map([\n        [\n          \"social-image-id\",\n          {\n            id: \"social-image-id\",\n            type: \"image\",\n            format: \"\",\n            projectId: \"\",\n            size: 0,\n            name: \"social-image-name\",\n            description: null,\n            createdAt: \"\",\n            meta: { width: 0, height: 0 },\n          } satisfies Asset,\n        ],\n      ]),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Page title\",\n    description: \"Page description\",\n    excludePageFromSearch: true,\n    language: \"en-US\",\n    socialImageAssetName: \"social-image-name\",\n    socialImageUrl: undefined,\n    status: 302,\n    redirect: \"/new-path\",\n    custom: [\n      {\n        property: \"custom-property-1\",\n        content: \"custom content 1\",\n      },\n      {\n        property: \"custom-property-2\",\n        content: \"custom content 2\",\n      },\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate asset url instead of id\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `\"Page title\"`,\n        meta: {\n          socialImageUrl: `\"https://my-image\"`,\n        },\n      },\n      dataSources: new Map(),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Page title\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: \"https://my-image\",\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate custom meta ignoring empty property name\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `\"Page title\"`,\n        meta: {\n          custom: [\n            { property: \"custom-property\", content: `\"custom content 1\"` },\n            { property: \"\", content: `\"custom content 2\"` },\n          ],\n        },\n      },\n      dataSources: new Map(),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  return {\n    title: \"Page title\",\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n      {\n        property: \"custom-property\",\n        content: \"custom content 1\",\n      },\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate page meta factory with variables\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `$ws$dataSource$variableId`,\n        meta: {},\n      },\n      dataSources: toMap([\n        {\n          id: \"variableId\",\n          scopeInstanceId: \"body\",\n          name: \"Variable Name\",\n          type: \"variable\",\n          value: { type: \"string\", value: \"\" },\n        },\n      ]),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  let VariableName = \"\"\n  return {\n    title: VariableName,\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate page meta factory with page system variable\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `$ws$dataSource$systemId.params.slug`,\n        meta: {},\n        systemDataSourceId: \"systemId\",\n      },\n      dataSources: toMap([\n        {\n          id: \"systemId\",\n          scopeInstanceId: \"body\",\n          name: \"system\",\n          type: \"parameter\",\n        },\n      ]),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  let system_1 = system\n  return {\n    title: system_1?.params?.slug,\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate page meta factory with global system variable\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `$ws$system.params.slug`,\n        meta: {},\n      },\n      dataSources: new Map(),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  let system_1 = system\n  return {\n    title: system_1?.params?.slug,\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate page meta factory with resources\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        title: `$ws$dataSource$resourceVariableId.data.title`,\n        meta: {},\n      },\n      dataSources: toMap([\n        {\n          id: \"resourceVariableId\",\n          scopeInstanceId: \"body\",\n          name: \"Cms Page\",\n          type: \"resource\",\n          resourceId: \"resourceId\",\n        },\n      ]),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  let CmsPage = resources.CmsPage\n  return {\n    title: CmsPage?.data?.title,\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n\ntest(\"generate page meta factory without unused variables\", () => {\n  expect(\n    generatePageMeta({\n      globalScope: createScope(),\n      page: {\n        id: \"\",\n        name: \"\",\n        path: \"\",\n        rootInstanceId: \"\",\n        systemDataSourceId: \"unusedSystemId\",\n        title: `$ws$dataSource$usedVariableId`,\n        meta: {},\n      },\n      dataSources: toMap([\n        {\n          id: \"usedVariableId\",\n          scopeInstanceId: \"body\",\n          name: \"Used Name\",\n          type: \"variable\",\n          value: { type: \"string\", value: \"\" },\n        },\n        {\n          id: \"unusedVariableId\",\n          scopeInstanceId: \"body\",\n          name: \"Unused Name\",\n          type: \"variable\",\n          value: { type: \"string\", value: \"\" },\n        },\n        {\n          id: \"unusedSystemId\",\n          scopeInstanceId: \"body\",\n          name: \"Unused System\",\n          type: \"parameter\",\n        },\n        {\n          id: \"unusedResourceVariableId\",\n          scopeInstanceId: \"body\",\n          name: \"Unused Cms Page\",\n          type: \"resource\",\n          resourceId: \"resourceId\",\n        },\n      ]),\n      assets: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n\"export const getPageMeta = ({\n  system,\n  resources,\n}: {\n  system: System;\n  resources: Record<string, any>;\n}): PageMeta => {\n  let UsedName = \"\"\n  return {\n    title: UsedName,\n    description: undefined,\n    excludePageFromSearch: undefined,\n    language: undefined,\n    socialImageAssetName: undefined,\n    socialImageUrl: undefined,\n    status: undefined,\n    redirect: undefined,\n    custom: [\n    ],\n  };\n};\n\"\n`);\n});\n"
  },
  {
    "path": "packages/sdk/src/page-meta-generator.ts",
    "content": "import type { Asset, Assets } from \"./schema/assets\";\nimport type { DataSources } from \"./schema/data-sources\";\nimport type { Page } from \"./schema/pages\";\nimport { type Scope, createScope } from \"./scope\";\nimport { generateExpression, SYSTEM_VARIABLE_ID } from \"./expression\";\n\nexport type PageMeta = {\n  title: string;\n  description?: string;\n  excludePageFromSearch?: boolean;\n  language?: string;\n  socialImageAssetName?: Asset[\"name\"];\n  socialImageUrl?: string;\n  status?: number;\n  redirect?: string;\n  custom: Array<{ property: string; content: string }>;\n};\n\nexport const generatePageMeta = ({\n  globalScope,\n  page,\n  dataSources,\n  assets,\n}: {\n  globalScope: Scope;\n  page: Page;\n  dataSources: DataSources;\n  assets: Assets;\n}) => {\n  // reserve parameter names passed to generated function\n  const localScope = createScope([\"system\", \"resources\"]);\n  const usedDataSources: DataSources = new Map();\n  const titleExpression = generateExpression({\n    expression: page.title,\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  const descriptionExpression = generateExpression({\n    expression: page.meta.description ?? \"undefined\",\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  const excludePageFromSearchExpression = generateExpression({\n    expression: page.meta.excludePageFromSearch ?? \"undefined\",\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  const languageExpression = generateExpression({\n    expression: page.meta.language ?? \"undefined\",\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  const socialImageAssetNameExpression = JSON.stringify(\n    page.meta.socialImageAssetId\n      ? assets.get(page.meta.socialImageAssetId)?.name\n      : undefined\n  );\n  const socialImageUrlExpression = generateExpression({\n    expression: page.meta.socialImageUrl ?? \"undefined\",\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  const statusExpression = generateExpression({\n    expression: page.meta.status ?? \"undefined\",\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  const redirectExpression = generateExpression({\n    expression: page.meta.redirect ?? \"undefined\",\n    dataSources,\n    usedDataSources,\n    scope: localScope,\n  });\n  let customExpression = \"\";\n  customExpression += `[\\n`;\n  for (const customMeta of page.meta.custom ?? []) {\n    if (customMeta.property.trim().length === 0) {\n      continue;\n    }\n    const propertyExpression = JSON.stringify(customMeta.property);\n    const contentExpression = generateExpression({\n      expression: customMeta.content,\n      dataSources,\n      usedDataSources,\n      scope: localScope,\n    });\n    customExpression += `      {\\n`;\n    customExpression += `        property: ${propertyExpression},\\n`;\n    customExpression += `        content: ${contentExpression},\\n`;\n    customExpression += `      },\\n`;\n  }\n  customExpression += `    ]`;\n  let generated = \"\";\n  generated += `export const getPageMeta = ({\\n`;\n  generated += `  system,\\n`;\n  generated += `  resources,\\n`;\n  generated += `}: {\\n`;\n  generated += `  system: System;\\n`;\n  generated += `  resources: Record<string, any>;\\n`;\n  generated += `}): PageMeta => {\\n`;\n  for (const dataSource of usedDataSources.values()) {\n    if (dataSource.type === \"variable\") {\n      const valueName = localScope.getName(dataSource.id, dataSource.name);\n      const initialValueString = JSON.stringify(dataSource.value.value);\n      generated += `  let ${valueName} = ${initialValueString}\\n`;\n      continue;\n    }\n    if (dataSource.type === \"parameter\") {\n      if (\n        dataSource.id === page.systemDataSourceId ||\n        dataSource.id === SYSTEM_VARIABLE_ID\n      ) {\n        const valueName = localScope.getName(dataSource.id, dataSource.name);\n        generated += `  let ${valueName} = system\\n`;\n      }\n      continue;\n    }\n    if (dataSource.type === \"resource\") {\n      const valueName = localScope.getName(dataSource.id, dataSource.name);\n      // use global scope only to retrieve resource names\n      const resourceName = globalScope.getName(\n        dataSource.resourceId,\n        dataSource.name\n      );\n      generated += `  let ${valueName} = resources.${resourceName}\\n`;\n      continue;\n    }\n  }\n  generated += `  return {\\n`;\n  generated += `    title: ${titleExpression},\\n`;\n  generated += `    description: ${descriptionExpression},\\n`;\n  generated += `    excludePageFromSearch: ${excludePageFromSearchExpression},\\n`;\n  generated += `    language: ${languageExpression},\\n`;\n  generated += `    socialImageAssetName: ${socialImageAssetNameExpression},\\n`;\n  generated += `    socialImageUrl: ${socialImageUrlExpression},\\n`;\n  generated += `    status: ${statusExpression},\\n`;\n  generated += `    redirect: ${redirectExpression},\\n`;\n  generated += `    custom: ${customExpression},\\n`;\n  generated += `  };\\n`;\n  generated += `};\\n`;\n  return generated;\n};\n"
  },
  {
    "path": "packages/sdk/src/page-utils.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport type { Pages } from \"./schema/pages\";\nimport {\n  findPageByIdOrPath,\n  findParentFolderByChildId,\n  getPagePath,\n} from \"./page-utils\";\n\nconst pages = {\n  meta: {},\n  homePage: {\n    id: \"home\",\n    path: \"\",\n    name: \"Home\",\n    title: \"Home\",\n    rootInstanceId: \"rootInstanceId\",\n    meta: {},\n  },\n  pages: [\n    {\n      id: \"page-1\",\n      path: \"/page-1\",\n      name: \"Page\",\n      title: \"Page\",\n      rootInstanceId: \"rootInstanceId\",\n      meta: {},\n    },\n  ],\n  folders: [\n    {\n      id: \"root\",\n      name: \"Root\",\n      slug: \"\",\n      children: [\"folderId-1\"],\n    },\n    {\n      id: \"folderId-1\",\n      name: \"Folder 1\",\n      slug: \"folder-1\",\n      children: [\"folderId-1-1\"],\n    },\n    {\n      id: \"folderId-1-1\",\n      name: \"Folder 1-1\",\n      slug: \"folder-1-1\",\n      children: [\"folderId-1-1-1\"],\n    },\n    {\n      id: \"folderId-1-1-1\",\n      name: \"Folder 1-1-1\",\n      slug: \"folder-1-1-1\",\n      children: [\"page-1\"],\n    },\n  ],\n} satisfies Pages;\n\ndescribe(\"getPagePath\", () => {\n  test(\"home page path\", () => {\n    expect(getPagePath(\"home\", pages)).toEqual(\"\");\n  });\n\n  test(\"nesting level 0\", () => {\n    expect(getPagePath(\"root\", pages)).toEqual(\"\");\n  });\n\n  test(\"nesting level 1\", () => {\n    expect(getPagePath(\"folderId-1\", pages)).toEqual(\"/folder-1\");\n  });\n\n  test(\"nesting level 2\", () => {\n    expect(getPagePath(\"folderId-1-1\", pages)).toEqual(\"/folder-1/folder-1-1\");\n  });\n\n  test(\"nesting level 3\", () => {\n    expect(getPagePath(\"folderId-1-1-1\", pages)).toEqual(\n      \"/folder-1/folder-1-1/folder-1-1-1\"\n    );\n  });\n\n  test(\"page inside folder nesting level 3\", () => {\n    expect(getPagePath(\"page-1\", pages)).toEqual(\n      \"/folder-1/folder-1-1/folder-1-1-1/page-1\"\n    );\n  });\n});\n\ndescribe(\"findPageByIdOrPath\", () => {\n  test(\"home page by id\", () => {\n    const page = findPageByIdOrPath(\"home\", pages);\n    expect(page).toEqual(pages.homePage);\n  });\n  test(\"home page by path /\", () => {\n    const page = findPageByIdOrPath(\"/\", pages);\n    expect(page).toEqual(pages.homePage);\n  });\n  test(\"home page by empty path\", () => {\n    const page = findPageByIdOrPath(\"\", pages);\n    expect(page).toEqual(pages.homePage);\n  });\n  test(\"find page by id\", () => {\n    const page = findPageByIdOrPath(\"page-1\", pages);\n    expect(page).toEqual(pages.pages[0]);\n  });\n  test(\"find page by nested path\", () => {\n    const page = findPageByIdOrPath(\n      \"/folder-1/folder-1-1/folder-1-1-1/page-1\",\n      pages\n    );\n    expect(page).toEqual(pages.pages[0]);\n  });\n});\n\ndescribe(\"findParentFolderByChildId\", () => {\n  test(\"find in root folder\", () => {\n    expect(\n      findParentFolderByChildId(\"folderId-1-1-1\", pages.folders)?.id\n    ).toEqual(\"folderId-1-1\");\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/page-utils.ts",
    "content": "import { executeExpression } from \"./expression\";\nimport type { Folder, Page, Pages } from \"./schema/pages\";\nimport { isPathnamePattern } from \"./url-pattern\";\n\nexport const ROOT_FOLDER_ID = \"root\";\n\n/**\n * Returns true if folder is the root folder.\n */\nexport const isRootFolder = ({ id }: { id: Folder[\"id\"] }) =>\n  id === ROOT_FOLDER_ID;\n\n/**\n * Find a page by id or path.\n */\nexport const findPageByIdOrPath = (\n  idOrPath: string,\n  pages: Pages\n): Page | undefined => {\n  if (idOrPath === \"\" || idOrPath === \"/\" || idOrPath === pages.homePage.id) {\n    return pages.homePage;\n  }\n  return pages.pages.find(\n    (page) => page.id === idOrPath || getPagePath(page.id, pages) === idOrPath\n  );\n};\n\n/**\n * Find a folder that has has that id in the children.\n */\nexport const findParentFolderByChildId = (\n  id: Folder[\"id\"] | Page[\"id\"],\n  folders: Array<Folder>\n): Folder | undefined => {\n  for (const folder of folders) {\n    if (folder.children.includes(id)) {\n      return folder;\n    }\n  }\n};\n\n/**\n * Get a path from all folder slugs from root to the current folder or page.\n */\nexport const getPagePath = (id: Folder[\"id\"] | Page[\"id\"], pages: Pages) => {\n  const foldersMap = new Map<Folder[\"id\"], Folder>();\n  const childParentMap = new Map<Folder[\"id\"] | Page[\"id\"], Folder[\"id\"]>();\n  for (const folder of pages.folders) {\n    foldersMap.set(folder.id, folder);\n    for (const childId of folder.children) {\n      childParentMap.set(childId, folder.id);\n    }\n  }\n\n  const paths = [];\n  let currentId: undefined | string = id;\n\n  // In case id is a page id\n  const allPages = [pages.homePage, ...pages.pages];\n  for (const page of allPages) {\n    if (page.id === id) {\n      paths.push(page.path);\n      currentId = childParentMap.get(page.id);\n      break;\n    }\n  }\n\n  while (currentId) {\n    const folder = foldersMap.get(currentId);\n    if (folder === undefined) {\n      break;\n    }\n    paths.push(folder.slug);\n    currentId = childParentMap.get(currentId);\n  }\n\n  return paths.reverse().join(\"/\").replace(/\\/+/g, \"/\");\n};\n\nexport const getStaticSiteMapXml = (pages: Pages, updatedAt: string) => {\n  const allPages = [pages.homePage, ...pages.pages];\n  return (\n    allPages\n      .filter((page) => (page.meta.documentType ?? \"html\") === \"html\")\n      // ignore pages with excludePageFromSearch bound to variables\n      // because there is no data from cms available at build time\n      .filter(\n        (page) => executeExpression(page.meta.excludePageFromSearch) !== true\n      )\n      .filter((page) => false === isPathnamePattern(page.path))\n      .map((page) => ({\n        path: getPagePath(page.id, pages),\n        lastModified: updatedAt.split(\"T\")[0],\n      }))\n  );\n};\n"
  },
  {
    "path": "packages/sdk/src/resource-loader.test.ts",
    "content": "import { describe, expect, test, beforeEach, vi, type Mock } from \"vitest\";\n\nimport { loadResource } from \"./resource-loader\";\nimport type { ResourceRequest } from \"./schema/resources\";\n\n// Mock the fetch function\n\ndescribe(\"loadResource\", () => {\n  let mockFetch: Mock<typeof fetch>;\n\n  beforeEach(() => {\n    mockFetch = vi.fn();\n    vi.clearAllMocks();\n  });\n\n  test(\"should successfully fetch a resource and return a JSON response\", async () => {\n    const mockResponse = new Response(JSON.stringify({ key: \"value\" }), {\n      status: 200,\n    });\n    mockFetch.mockResolvedValue(mockResponse);\n\n    const resourceRequest: ResourceRequest = {\n      name: \"resource\",\n      url: \"https://example.com/resource\",\n      searchParams: [],\n      method: \"get\",\n      headers: [],\n      body: undefined,\n    };\n\n    const result = await loadResource(mockFetch, resourceRequest);\n\n    expect(mockFetch).toHaveBeenCalledWith(\"https://example.com/resource\", {\n      method: \"get\",\n      headers: new Headers(),\n    });\n\n    expect(result).toEqual({\n      data: {\n        key: \"value\",\n      },\n      ok: true,\n      status: 200,\n      statusText: \"\",\n    });\n  });\n\n  test(\"should fetch resource successfully with non-JSON response\", async () => {\n    const mockResponse = new Response(\"nonjson\", {\n      status: 200,\n    });\n    mockFetch.mockResolvedValue(mockResponse);\n\n    const resourceRequest: ResourceRequest = {\n      name: \"resource\",\n      url: \"https://example.com/resource\",\n      searchParams: [],\n      method: \"get\",\n      headers: [],\n      body: undefined,\n    };\n\n    const result = await loadResource(mockFetch, resourceRequest);\n\n    expect(mockFetch).toHaveBeenCalledWith(\"https://example.com/resource\", {\n      method: \"get\",\n      headers: new Headers(),\n    });\n\n    expect(result).toEqual({\n      data: \"nonjson\",\n      ok: true,\n      status: 200,\n      statusText: \"\",\n    });\n  });\n\n  test(\"should fetch resource with search params\", async () => {\n    const mockResponse = new Response(JSON.stringify({ key: \"value\" }), {\n      status: 200,\n    });\n    mockFetch.mockResolvedValue(mockResponse);\n\n    const resourceRequest: ResourceRequest = {\n      name: \"resource\",\n      url: \"https://example.com/resource\",\n      searchParams: [\n        { name: \"search\", value: \"term1\" },\n        { name: \"search\", value: \"term2\" },\n        { name: \"filter\", value: \"привет\" },\n      ],\n      method: \"get\",\n      headers: [],\n    };\n\n    await loadResource(mockFetch, resourceRequest);\n\n    expect(mockFetch).toHaveBeenCalledWith(\n      \"https://example.com/resource?search=term1&search=term2&filter=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82\",\n      {\n        method: \"get\",\n        headers: new Headers(),\n      }\n    );\n  });\n\n  test(\"should fetch resource with JSON search params\", async () => {\n    const mockResponse = new Response(JSON.stringify({ key: \"value\" }), {\n      status: 200,\n    });\n    mockFetch.mockResolvedValue(mockResponse);\n\n    const resourceRequest: ResourceRequest = {\n      name: \"resource\",\n      url: \"https://example.com/resource\",\n      searchParams: [\n        { name: \"filter\", value: { type: \"AND\", left: \"a\", right: \"b\" } },\n      ],\n      method: \"get\",\n      headers: [],\n    };\n\n    await loadResource(mockFetch, resourceRequest);\n\n    expect(mockFetch).toHaveBeenCalledWith(\n      \"https://example.com/resource?filter=%7B%22type%22%3A%22AND%22%2C%22left%22%3A%22a%22%2C%22right%22%3A%22b%22%7D\",\n      {\n        method: \"get\",\n        headers: new Headers(),\n      }\n    );\n  });\n\n  test(\"should fetch resource with JSON headers\", async () => {\n    const mockResponse = new Response(JSON.stringify({ key: \"value\" }), {\n      status: 200,\n    });\n    mockFetch.mockResolvedValue(mockResponse);\n\n    const resourceRequest: ResourceRequest = {\n      name: \"resource\",\n      url: \"https://example.com/resource\",\n      searchParams: [],\n      method: \"get\",\n      headers: [\n        { name: \"filter\", value: { type: \"AND\", left: \"a\", right: \"b\" } },\n      ],\n    };\n\n    await loadResource(mockFetch, resourceRequest);\n\n    expect(mockFetch).toHaveBeenCalledWith(\"https://example.com/resource\", {\n      method: \"get\",\n      headers: new Headers([\n        [\"filter\", '{\"type\":\"AND\",\"left\":\"a\",\"right\":\"b\"}'],\n      ]),\n    });\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/resource-loader.ts",
    "content": "import hash from \"@emotion/hash\";\nimport type { ResourceRequest } from \"./schema/resources\";\nimport { serializeValue } from \"./to-string\";\n\nconst LOCAL_RESOURCE_PREFIX = \"$resources\";\n\n/**\n * Prevents fetch cycles by prefixing local resources.\n */\nexport const isLocalResource = (pathname: string, resourceName?: string) => {\n  const segments = pathname.split(\"/\").filter(Boolean);\n\n  if (resourceName === undefined) {\n    return segments[0] === LOCAL_RESOURCE_PREFIX;\n  }\n\n  return segments.join(\"/\") === `${LOCAL_RESOURCE_PREFIX}/${resourceName}`;\n};\n\nexport const sitemapResourceUrl = `/${LOCAL_RESOURCE_PREFIX}/sitemap.xml`;\nexport const currentDateResourceUrl = `/${LOCAL_RESOURCE_PREFIX}/current-date`;\nexport const assetsResourceUrl = `/${LOCAL_RESOURCE_PREFIX}/assets`;\n\nexport const loadResource = async (\n  customFetch: typeof fetch,\n  resourceRequest: ResourceRequest\n) => {\n  try {\n    const { method, searchParams, headers, body } = resourceRequest;\n    let href = resourceRequest.url;\n    try {\n      // cloudflare workers fail when fetching url contains spaces\n      // even though new URL suppose to trim them on parsing by spec\n      const url = new URL(resourceRequest.url.trim());\n      if (searchParams) {\n        for (const { name, value } of searchParams) {\n          url.searchParams.append(name, serializeValue(value));\n        }\n      }\n      href = url.href;\n    } catch {\n      // empty block\n    }\n    const requestHeaders = new Headers(\n      headers.map(({ name, value }): [string, string] => [\n        name,\n        serializeValue(value),\n      ])\n    );\n    const requestInit: RequestInit = {\n      method,\n      headers: requestHeaders,\n    };\n    if (method !== \"get\" && body !== undefined) {\n      requestInit.body = serializeValue(body);\n    }\n    const response = await customFetch(href, requestInit);\n\n    let data = await response.text();\n\n    try {\n      // If it looks like JSON and quacks like JSON, then it probably is JSON.\n      data = JSON.parse(data);\n    } catch {\n      // ignore, leave data as text\n    }\n\n    if (!response.ok) {\n      console.error(\n        `Failed to load resource: ${href} - ${response.status}: ${JSON.stringify(data).slice(0, 300)}`\n      );\n    }\n\n    return {\n      ok: response.ok,\n      status: response.status,\n      statusText: response.statusText,\n      data,\n    };\n  } catch (error) {\n    console.error(error);\n    const message = (error as unknown as Error).message;\n    return {\n      ok: false,\n      data: undefined,\n      status: 500,\n      statusText: message,\n    };\n  }\n};\n\nexport const loadResources = async (\n  customFetch: typeof fetch,\n  requests: Map<string, ResourceRequest>\n) => {\n  return Object.fromEntries(\n    await Promise.all(\n      Array.from(\n        requests,\n        async ([name, request]) =>\n          [name, await loadResource(customFetch, request)] as const\n      )\n    )\n  );\n};\n\n/**\n * cache api supports only get method\n * put hash of method and body into url\n * to support for example graphql queries\n */\nconst getCacheKey = async (request: Request) => {\n  const url = new URL(request.url);\n  const method = request.method;\n  const body = await request.clone().text();\n  // invalidate cache when cache-control is changed\n  const cacheControl = request.headers.get(\"Cache-Control\");\n  const resourceHash = hash(`${method}:${body}:${cacheControl}`);\n  url.searchParams.set(\"ws-resource-hash\", resourceHash);\n  return url;\n};\n\nexport const cachedFetch = async (\n  namespace: string,\n  input: RequestInfo | URL,\n  init?: RequestInit\n) => {\n  if (globalThis.caches) {\n    const request = new Request(input, init);\n    const requestCacheControl = request.headers.get(\"Cache-Control\");\n    // make cache opt in with cache-control header\n    if (!requestCacheControl) {\n      return fetch(input, init);\n    }\n    const cache = await caches.open(namespace);\n    const cacheKey = await getCacheKey(request);\n    let response = await cache.match(cacheKey);\n    if (response) {\n      // avoid mutating cached response\n      return new Response(response.body, response);\n    }\n    // load response when missing in cache\n    response = await fetch(request);\n    // avoid caching failed responses\n    if (!response.ok) {\n      return response;\n    }\n    // put Cache-Control from request into response\n    // https://developers.cloudflare.com/workers/reference/how-the-cache-works/#cache-api\n    // response.clone() does not remove read-only constraint from headers\n    response = new Response(response.body, response);\n    response.headers.set(\"Cache-Control\", requestCacheControl);\n    // avoid mutating cached response\n    await cache.put(cacheKey, response.clone());\n    return response;\n  }\n  return fetch(input, init);\n};\n"
  },
  {
    "path": "packages/sdk/src/resources-generator.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport {\n  renderData,\n  $,\n  expression,\n  ResourceValue,\n} from \"@webstudio-is/template\";\nimport type { Page } from \"./schema/pages\";\nimport { createScope } from \"./scope\";\nimport {\n  generateResources,\n  replaceFormActionsWithResources,\n} from \"./resources-generator\";\nimport type { DataSource } from \"./schema/data-sources\";\n\nconst toMap = <T extends { id: string }>(list: T[]) =>\n  new Map(list.map((item) => [item.id, item] as const));\n\ntest(\"generate resources loader\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: { rootInstanceId: \"body\" } as Page,\n      dataSources: toMap([\n        {\n          id: \"variableResourceId\",\n          scopeInstanceId: \"body\",\n          type: \"resource\",\n          name: \"variableName\",\n          resourceId: \"resourceId\",\n        },\n      ]),\n      resources: toMap([\n        {\n          id: \"resourceId\",\n          name: \"resourceName\",\n          url: `\"https://my-json.com\"`,\n          method: \"post\",\n          headers: [{ name: \"Content-Type\", value: `\"application/json\"` }],\n          body: `{ body: true }`,\n        },\n      ]),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const resourceName: ResourceRequest = {\n        name: \"resourceName\",\n        url: \"https://my-json.com\",\n        searchParams: [\n        ],\n        method: \"post\",\n        headers: [\n          { name: \"Content-Type\", value: \"application/json\" },\n        ],\n        body: { body: true },\n      }\n      const _data = new Map<string, ResourceRequest>([\n        [\"resourceName\", resourceName],\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"generate variable and use in resources loader\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: {\n        rootInstanceId: \"body\",\n      } as Page,\n      dataSources: toMap([\n        {\n          id: \"variableResourceId\",\n          scopeInstanceId: \"body\",\n          name: \"variableName\",\n          type: \"resource\",\n          resourceId: \"resourceId\",\n        },\n        {\n          id: \"variableTokenId\",\n          scopeInstanceId: \"body\",\n          name: \"Access Token\",\n          type: \"variable\",\n          value: { type: \"string\", value: \"my-token\" },\n        },\n      ]),\n      resources: toMap([\n        {\n          id: \"resourceId\",\n          name: \"resourceName\",\n          url: `\"https://my-json.com/\"`,\n          method: \"post\",\n          headers: [\n            {\n              name: \"Authorization\",\n              value: `\"Token \" + $ws$dataSource$variableTokenId`,\n            },\n          ],\n          body: `{ body: true }`,\n        },\n      ]),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      let AccessToken = \"my-token\"\n      const resourceName: ResourceRequest = {\n        name: \"resourceName\",\n        url: \"https://my-json.com/\",\n        searchParams: [\n        ],\n        method: \"post\",\n        headers: [\n          { name: \"Authorization\", value: \"Token \" + AccessToken },\n        ],\n        body: { body: true },\n      }\n      const _data = new Map<string, ResourceRequest>([\n        [\"resourceName\", resourceName],\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"generate page system variable and use in resources loader\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: {\n        rootInstanceId: \"body\",\n        systemDataSourceId: \"variableSystemId\",\n      } as Page,\n      dataSources: toMap([\n        {\n          id: \"variableResourceId\",\n          scopeInstanceId: \"body\",\n          name: \"variableName\",\n          type: \"resource\",\n          resourceId: \"resourceId\",\n        },\n        {\n          id: \"variableSystemId\",\n          scopeInstanceId: \"body\",\n          name: \"system\",\n          type: \"parameter\",\n        },\n      ]),\n      resources: toMap([\n        {\n          id: \"resourceId\",\n          name: \"resourceName\",\n          url: `\"https://my-json.com/\" + $ws$dataSource$variableSystemId.params.slug`,\n          searchParams: [],\n          method: \"post\",\n          headers: [{ name: \"Content-Type\", value: `\"application/json\"` }],\n          body: `{ body: true }`,\n        },\n      ]),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const system = _props.system\n      const resourceName: ResourceRequest = {\n        name: \"resourceName\",\n        url: \"https://my-json.com/\" + system?.params?.slug,\n        searchParams: [\n        ],\n        method: \"post\",\n        headers: [\n          { name: \"Content-Type\", value: \"application/json\" },\n        ],\n        body: { body: true },\n      }\n      const _data = new Map<string, ResourceRequest>([\n        [\"resourceName\", resourceName],\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"generate global system variable and use in resources loader\", () => {\n  const myResource = new ResourceValue(\"My Resource\", {\n    url: expression`\"https://my-json.com/\" + $ws$system.params.slug`,\n    method: \"post\",\n    searchParams: [{ name: \"filter\", value: expression`{search:'term'}` }],\n    headers: [{ name: \"Content-Type\", value: expression`\"application/json\"` }],\n    body: expression`{ body: true }`,\n  });\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: {\n        rootInstanceId: \"bodyId\",\n      } as Page,\n      ...renderData(\n        <$.Body ws:id=\"bodyId\" vars={expression`${myResource}`}></$.Body>\n      ),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const system = _props.system\n      const MyResource: ResourceRequest = {\n        name: \"My Resource\",\n        url: \"https://my-json.com/\" + system?.params?.slug,\n        searchParams: [\n          { name: \"filter\", value: {search:'term'} },\n        ],\n        method: \"post\",\n        headers: [\n          { name: \"Content-Type\", value: \"application/json\" },\n        ],\n        body: { body: true },\n      }\n      const _data = new Map<string, ResourceRequest>([\n        [\"MyResource\", MyResource],\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"generate empty resources loader\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: { rootInstanceId: \"body\" } as Page,\n      dataSources: new Map(),\n      resources: new Map(),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const _data = new Map<string, ResourceRequest>([\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"generate resource loader with search params\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: { rootInstanceId: \"body\" } as Page,\n      dataSources: toMap<DataSource>([\n        {\n          id: \"variableTermId\",\n          scopeInstanceId: \"body\",\n          type: \"variable\",\n          name: \"term\",\n          value: { type: \"string\", value: \"my-term\" },\n        },\n        {\n          id: \"variableResourceId\",\n          scopeInstanceId: \"body\",\n          type: \"resource\",\n          name: \"variableName\",\n          resourceId: \"resourceId\",\n        },\n      ]),\n      resources: toMap([\n        {\n          id: \"resourceId\",\n          name: \"resourceName\",\n          method: \"get\",\n          url: `\"https://my-json.com\"`,\n          searchParams: [\n            {\n              name: \"search\",\n              value: `$ws$dataSource$variableTermId`,\n            },\n          ],\n          headers: [],\n        },\n      ]),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      let term = \"my-term\"\n      const resourceName: ResourceRequest = {\n        name: \"resourceName\",\n        url: \"https://my-json.com\",\n        searchParams: [\n          { name: \"search\", value: term },\n        ],\n        method: \"get\",\n        headers: [\n        ],\n      }\n      const _data = new Map<string, ResourceRequest>([\n        [\"resourceName\", resourceName],\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"prevent generating unused variables\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: { rootInstanceId: \"body\" } as Page,\n      dataSources: toMap([\n        {\n          id: \"unuseVariableId\",\n          scopeInstanceId: \"body\",\n          name: \"Unused Variable\",\n          type: \"variable\",\n          value: { type: \"string\", value: \"\" },\n        },\n      ]),\n      resources: new Map(),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const _data = new Map<string, ResourceRequest>([\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"prevent generating unused system variable\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: {\n        rootInstanceId: \"body\",\n        systemDataSourceId: \"variableParamsId\",\n      } as Page,\n      dataSources: toMap([\n        {\n          id: \"variableParamsId\",\n          scopeInstanceId: \"body\",\n          name: \"Unused System\",\n          type: \"parameter\",\n        },\n      ]),\n      resources: new Map(),\n      props: new Map(),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const _data = new Map<string, ResourceRequest>([\n      ])\n      const _action = new Map<string, ResourceRequest>([\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"generate action resource\", () => {\n  expect(\n    generateResources({\n      scope: createScope(),\n      page: {\n        rootInstanceId: \"body\",\n        systemDataSourceId: \"variableParamsId\",\n      } as Page,\n      dataSources: new Map(),\n      resources: toMap([\n        {\n          id: \"resourceId\",\n          name: \"resourceName\",\n          url: `\"https://my-url.com\"`,\n          method: \"post\",\n          headers: [],\n        },\n      ]),\n      props: toMap([\n        {\n          id: \"propId\",\n          instanceId: \"body\",\n          name: \"myProp\",\n          type: \"resource\",\n          value: \"resourceId\",\n        },\n      ]),\n    })\n  ).toMatchInlineSnapshot(`\n    \"import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\n    export const getResources = (_props: { system: System }) => {\n      const resourceName: ResourceRequest = {\n        name: \"resourceName\",\n        url: \"https://my-url.com\",\n        searchParams: [\n        ],\n        method: \"post\",\n        headers: [\n        ],\n      }\n      const _data = new Map<string, ResourceRequest>([\n      ])\n      const _action = new Map<string, ResourceRequest>([\n        [\"resourceName\", resourceName],\n      ])\n      return { data: _data, action: _action }\n    }\n    \"\n  `);\n});\n\ntest(\"replace form action with resource\", () => {\n  const data = renderData(\n    <$.Form ws:id=\"formId\" action=\"https://my-url.com\"></$.Form>\n  );\n  replaceFormActionsWithResources(data);\n  expect(data.props).toEqual(\n    toMap([\n      {\n        id: \"formId:action\",\n        instanceId: \"formId\",\n        name: \"action\",\n        type: \"resource\",\n        value: \"formId\",\n      },\n    ])\n  );\n  expect(data.resources).toEqual(\n    toMap([\n      {\n        headers: [{ name: \"Content-Type\", value: `\"application/json\"` }],\n        id: \"formId\",\n        method: \"post\",\n        name: \"action\",\n        url: `\"https://my-url.com\"`,\n      },\n    ])\n  );\n});\n\ntest(\"ignore empty form action\", () => {\n  const data = renderData(<$.Form ws:id=\"formId\" action=\"\"></$.Form>);\n  replaceFormActionsWithResources(data);\n  expect(data.props).toEqual(\n    toMap([\n      {\n        id: \"formId:action\",\n        instanceId: \"formId\",\n        name: \"action\",\n        type: \"string\",\n        value: \"\",\n      },\n    ])\n  );\n  expect(data.resources).toEqual(new Map());\n});\n"
  },
  {
    "path": "packages/sdk/src/resources-generator.ts",
    "content": "import type { DataSources } from \"./schema/data-sources\";\nimport type { Page } from \"./schema/pages\";\nimport type { Resources } from \"./schema/resources\";\nimport type { Props } from \"./schema/props\";\nimport type { Instance, Instances } from \"./schema/instances\";\nimport type { Scope } from \"./scope\";\nimport { generateExpression, SYSTEM_VARIABLE_ID } from \"./expression\";\n\nexport const generateResources = ({\n  scope,\n  page,\n  dataSources,\n  props,\n  resources,\n}: {\n  scope: Scope;\n  page: Page;\n  dataSources: DataSources;\n  props: Props;\n  resources: Resources;\n}) => {\n  const usedDataSources: DataSources = new Map();\n\n  let generatedRequests = \"\";\n  for (const resource of resources.values()) {\n    let generatedRequest = \"\";\n    // call resource by bound variable name\n    const resourceName = scope.getName(resource.id, resource.name);\n    generatedRequest += `  const ${resourceName}: ResourceRequest = {\\n`;\n    generatedRequest += `    name: ${JSON.stringify(resource.name)},\\n`;\n    const url = generateExpression({\n      expression: resource.url,\n      dataSources,\n      usedDataSources,\n      scope,\n    });\n    generatedRequest += `    url: ${url},\\n`;\n    generatedRequest += `    searchParams: [\\n`;\n    for (const searchParam of resource.searchParams ?? []) {\n      const value = generateExpression({\n        expression: searchParam.value,\n        dataSources,\n        usedDataSources,\n        scope,\n      });\n      generatedRequest += `      { name: \"${searchParam.name}\", value: ${value} },\\n`;\n    }\n    generatedRequest += `    ],\\n`;\n    generatedRequest += `    method: \"${resource.method}\",\\n`;\n    generatedRequest += `    headers: [\\n`;\n    for (const header of resource.headers) {\n      const value = generateExpression({\n        expression: header.value,\n        dataSources,\n        usedDataSources,\n        scope,\n      });\n      generatedRequest += `      { name: \"${header.name}\", value: ${value} },\\n`;\n    }\n    generatedRequest += `    ],\\n`;\n    // prevent computing empty expression\n    if (resource.body !== undefined && resource.body.length > 0) {\n      const body = generateExpression({\n        expression: resource.body,\n        dataSources,\n        usedDataSources,\n        scope,\n      });\n      generatedRequest += `    body: ${body},\\n`;\n    }\n    generatedRequest += `  }\\n`;\n    generatedRequests += generatedRequest;\n  }\n\n  let generatedVariables = \"\";\n  for (const dataSource of usedDataSources.values()) {\n    if (dataSource.type === \"variable\") {\n      const name = scope.getName(dataSource.id, dataSource.name);\n      const value = JSON.stringify(dataSource.value.value);\n      generatedVariables += `  let ${name} = ${value}\\n`;\n    }\n\n    if (dataSource.type === \"parameter\") {\n      // support only page system parameter\n      if (\n        dataSource.id === page.systemDataSourceId ||\n        dataSource.id === SYSTEM_VARIABLE_ID\n      ) {\n        const name = scope.getName(dataSource.id, dataSource.name);\n        generatedVariables += `  const ${name} = _props.system\\n`;\n      }\n    }\n  }\n\n  let generated = \"\";\n  generated += `import type { System, ResourceRequest } from \"@webstudio-is/sdk\";\\n`;\n  generated += `export const getResources = (_props: { system: System }) => {\\n`;\n  generated += generatedVariables;\n  generated += generatedRequests;\n\n  generated += `  const _data = new Map<string, ResourceRequest>([\\n`;\n  for (const dataSource of dataSources.values()) {\n    if (dataSource.type === \"resource\") {\n      const name = scope.getName(dataSource.resourceId, dataSource.name);\n      generated += `    [\"${name}\", ${name}],\\n`;\n    }\n  }\n  generated += `  ])\\n`;\n\n  generated += `  const _action = new Map<string, ResourceRequest>([\\n`;\n  for (const prop of props.values()) {\n    if (prop.type === \"resource\") {\n      const name = scope.getName(prop.value, prop.name);\n      generated += `    [\"${name}\", ${name}],\\n`;\n    }\n  }\n  generated += `  ])\\n`;\n\n  generated += `  return { data: _data, action: _action }\\n`;\n  generated += `}\\n`;\n\n  return generated;\n};\n\nconst getMethod = (value: string | undefined) => {\n  switch (value?.toLowerCase()) {\n    case \"get\":\n      return \"get\";\n    case \"delete\":\n      return \"delete\";\n    case \"put\":\n      return \"put\";\n    default:\n      return \"post\";\n  }\n};\n\n/**\n * migrate webhook forms to resource action\n * @todo move to client migrations eventually\n */\nexport const replaceFormActionsWithResources = ({\n  props,\n  instances,\n  resources,\n}: {\n  props: Props;\n  instances: Instances;\n  resources: Resources;\n}) => {\n  const formProps = new Map<\n    Instance[\"id\"],\n    { method?: string; action?: string }\n  >();\n  for (const prop of props.values()) {\n    if (\n      prop.name === \"method\" &&\n      prop.type === \"string\" &&\n      instances.get(prop.instanceId)?.component === \"Form\"\n    ) {\n      let data = formProps.get(prop.instanceId);\n      if (data === undefined) {\n        data = {};\n        formProps.set(prop.instanceId, data);\n      }\n      data.method = prop.value;\n      props.delete(prop.id);\n    }\n    if (\n      prop.name === \"action\" &&\n      prop.type === \"string\" &&\n      prop.value &&\n      instances.get(prop.instanceId)?.component === \"Form\"\n    ) {\n      let data = formProps.get(prop.instanceId);\n      if (data === undefined) {\n        data = {};\n        formProps.set(prop.instanceId, data);\n      }\n      data.action = prop.value;\n      props.set(prop.id, {\n        id: prop.id,\n        instanceId: prop.instanceId,\n        name: prop.name,\n        type: \"resource\",\n        value: prop.instanceId,\n      });\n    }\n  }\n  for (const [instanceId, { action, method }] of formProps) {\n    if (action) {\n      resources.set(instanceId, {\n        id: instanceId,\n        name: \"action\",\n        method: getMethod(method),\n        url: JSON.stringify(action),\n        headers: [\n          { name: \"Content-Type\", value: JSON.stringify(\"application/json\") },\n        ],\n      });\n    }\n  }\n};\n"
  },
  {
    "path": "packages/sdk/src/router-path-test-data.ts",
    "content": "/**\n * Shared test data for router path validation.\n *\n * This file exports test cases that should be used consistently across:\n * 1. Path schema validation (SDK) - OldPagePath, page paths, etc.\n * 2. URLPattern matching (builder) - used for all page routing\n * 3. Route generation for published sites (react-sdk)\n *\n * These paths are used for pages, redirects, and any routable URL.\n * Add new test cases here to ensure all layers are tested with the same data.\n *\n * Reference: React Router path matching tests\n * https://github.com/remix-run/react-router/tree/main/packages/react-router/__tests__\n */\n\n// These test cases define what paths should be valid/invalid across ALL layers\n\nexport const VALID_ROUTER_PATHS = {\n  // Basic paths\n  basic: [\"/about\", \"/blog\", \"/contact-us\", \"/products/item-1\"],\n\n  // Deep nesting\n  deepNesting: [\n    \"/a/b/c/d/e\",\n    \"/users/123/posts/456/comments/789\",\n    \"/api/v1/resources/items\",\n  ],\n\n  // Dynamic segments (React Router: /:param)\n  dynamicSegments: [\n    \"/:id\",\n    \"/users/:id\",\n    \"/users/:userId/posts/:postId\",\n    \"/blog/:year/:month/:day/:slug\",\n    \"/courses/:foo-bar\", // params can contain dashes\n  ],\n\n  // Optional dynamic segments (React Router: /:param?)\n  optionalDynamic: [\n    \"/:lang?/about\",\n    \"/user/:id/:tab?\",\n    \"/:lang?/user/:id?\",\n    \"/docs/:version?/:page?\",\n    \"/nested/:one?/:two?/:three?/:four?\", // up to 4 consecutive optionals\n    \"/:one?/:two?/:three?\", // all optional at root\n  ],\n\n  // Optional static segments (React Router: /segment?)\n  optionalStatic: [\n    \"/en?/about\",\n    \"/api/v1?/users\",\n    \"/school?/user/:id\",\n    \"/admin?/dashboard\",\n    \"/nested/one?/two?\", // consecutive static optionals\n    \"/nested/one?/two/three?\", // intercalated static optionals\n  ],\n\n  // Mixed optional patterns (intercalated static and dynamic)\n  mixedOptionals: [\n    \"/nested/:one?/two/:three?\", // optional, required, optional\n    \"/one?/:two?/three/:four/*\", // mixed optionals with splat\n    \"/one/:two?/three/:four?/:five?\", // complex mixed pattern\n  ],\n\n  // Wildcard/splat routes (React Router: /*)\n  wildcards: [\"/*\", \"/blog/*\", \"/docs/*\", \"/files/*\", \"/users/:id/files/*\"],\n\n  // Query strings and fragments\n  queryAndFragments: [\n    \"/search?q=test\",\n    \"/page#section\",\n    \"/path?a=1&b=2#top\",\n    \"/products?category=shoes&sort=price\",\n  ],\n\n  // URL-encoded characters\n  urlEncoded: [\n    \"/hello%20world\",\n    \"/%E6%B8%AF%E8%81%9E\",\n    \"/path%2Fwith%2Fslash\",\n    \"/users%3Fid%3D123\", // ?id=123 encoded\n  ],\n\n  // Special characters allowed in paths (from React Router special-characters-test.tsx)\n  // Note: Some chars have special meaning in URLPattern regex and must be URL-encoded\n  specialChars: [\n    \"/path-with-dash\",\n    \"/path_with_underscore\",\n    \"/path.with.dots\",\n    \"/path~tilde\",\n    \"/path!exclaim\",\n    \"/path@at\",\n    \"/path$dollar\",\n    \"/path'apostrophe\",\n    \"/path,comma\",\n    \"/path;semicolon\",\n    \"/path=equals\",\n  ],\n\n  // Characters that are valid in URLs but have special meaning in URLPattern\n  // These need to be URL-encoded when used literally (not as pattern syntax)\n  specialCharsNeedEncoding: [\n    \"/path%28parens%29\", // parentheses encoded\n    \"/path%2Bplus\", // plus encoded\n  ],\n\n  // Non-Latin characters (Unicode/UTF-8)\n  chinese: [\"/关于我们\", \"/产品/手机\", \"/港聞\", \"/繁體中文\"],\n  japanese: [\"/日本語\", \"/こんにちは\", \"/カテゴリ\", \"/ブログ/記事\"],\n  korean: [\"/한국어\", \"/블로그/포스트\", \"/서울\"],\n  cyrillic: [\"/привет\", \"/о-нас\", \"/блог/статья\"],\n  arabic: [\"/مرحبا\", \"/عن-الشركة\"],\n  hebrew: [\"/שלום\", \"/אודות\"],\n  thai: [\"/สวัสดี\", \"/ภาษาไทย\"],\n  greek: [\"/γεια\", \"/σχετικά\"],\n  european: [\"/über-uns\", \"/café\", \"/niño\", \"/résumé\", \"/naïve\"],\n\n  // Mixed Latin and non-Latin\n  mixed: [\"/blog/关于\", \"/news/港聞\", \"/category/日本語\", \"/user/bücherwurm\"],\n\n  // File extensions in paths (from React Router generatePath-test.tsx)\n  fileExtensions: [\n    \"/books/:id.json\",\n    \"/api/:resource.xml\",\n    \"/images/:name.png\",\n    \"/docs/:page.html\",\n    \"/sitemap/:lang.xml\", // param before extension\n    \"/:lang.html\", // root level param with extension\n    \"/files/:name.tar.gz\", // multiple extensions\n    \"/:file.min.js\", // minified JS pattern\n  ],\n\n  // Base64-like segments (from React Router matchRoutes-test.tsx)\n  base64Segments: [\"/users/VXNlcnM6MQ==\", \"/items/YWJjZGVm\"],\n\n  // Emoji paths (modern Unicode)\n  emoji: [\"/🏠\", \"/blog/🎉\", \"/products/👟\"],\n} as const;\n\nexport const INVALID_ROUTER_PATHS = {\n  // Empty or root only (for redirects, root is not a valid source)\n  empty: [\"\", \"/\"],\n\n  // Spaces (must be URL-encoded)\n  spaces: [\"/hello world\", \"/path with spaces\"],\n\n  // URL-unsafe characters (RFC 3986)\n  unsafe: [\n    \"/path<script>\",\n    \"/path>other\",\n    '/path\"quote',\n    \"/path{test}\",\n    \"/path|other\",\n    \"/path\\\\other\",\n    \"/path[0]\",\n    \"/path`backtick\",\n  ],\n\n  // Reserved paths (Webstudio-specific)\n  reserved: [\"/s\", \"/s/css\", \"/s/uploads\", \"/build\", \"/build/main.js\"],\n\n  // Invalid structure\n  structure: [\n    \"no-leading-slash\",\n    \"/trailing/\",\n    \"/double//slash\",\n    \"//leading-double\",\n  ],\n\n  // Control characters\n  controlChars: [\"/path\\x00null\", \"/path\\x1fnewline\"],\n} as const;\n\n// Flattened arrays for convenience\nexport const ALL_VALID_PATHS = Object.values(VALID_ROUTER_PATHS).flat();\nexport const ALL_INVALID_PATHS = Object.values(INVALID_ROUTER_PATHS).flat();\n\n// Paths that are specifically for testing URLPattern matching (no query/fragment)\n// These are paths that work with URLPattern API\nexport const VALID_URLPATTERN_PATHS = [\n  ...VALID_ROUTER_PATHS.basic,\n  ...VALID_ROUTER_PATHS.deepNesting,\n  ...VALID_ROUTER_PATHS.dynamicSegments,\n  ...VALID_ROUTER_PATHS.optionalDynamic,\n  // Note: optionalStatic uses React Router syntax which differs from URLPattern\n  ...VALID_ROUTER_PATHS.wildcards,\n  ...VALID_ROUTER_PATHS.specialChars,\n  ...VALID_ROUTER_PATHS.specialCharsNeedEncoding,\n  ...VALID_ROUTER_PATHS.chinese,\n  ...VALID_ROUTER_PATHS.japanese,\n  ...VALID_ROUTER_PATHS.korean,\n  ...VALID_ROUTER_PATHS.cyrillic,\n  ...VALID_ROUTER_PATHS.arabic,\n  ...VALID_ROUTER_PATHS.hebrew,\n  ...VALID_ROUTER_PATHS.thai,\n  ...VALID_ROUTER_PATHS.greek,\n  ...VALID_ROUTER_PATHS.european,\n  ...VALID_ROUTER_PATHS.mixed,\n  ...VALID_ROUTER_PATHS.base64Segments,\n  ...VALID_ROUTER_PATHS.emoji,\n] as const;\n\n// Static paths (no wildcards/params) for testing route generation\nexport const STATIC_PATHS = [\n  ...VALID_ROUTER_PATHS.basic,\n  ...VALID_ROUTER_PATHS.deepNesting,\n  ...VALID_ROUTER_PATHS.specialChars,\n  ...VALID_ROUTER_PATHS.specialCharsNeedEncoding,\n  ...VALID_ROUTER_PATHS.chinese,\n  ...VALID_ROUTER_PATHS.japanese,\n  ...VALID_ROUTER_PATHS.korean,\n  ...VALID_ROUTER_PATHS.cyrillic,\n  ...VALID_ROUTER_PATHS.arabic,\n  ...VALID_ROUTER_PATHS.hebrew,\n  ...VALID_ROUTER_PATHS.thai,\n  ...VALID_ROUTER_PATHS.greek,\n  ...VALID_ROUTER_PATHS.european,\n  ...VALID_ROUTER_PATHS.mixed,\n  ...VALID_ROUTER_PATHS.base64Segments,\n  ...VALID_ROUTER_PATHS.emoji,\n] as const;\n\n// Pattern paths (with dynamic segments, wildcards, or optional segments)\nexport const PATTERN_PATHS = [\n  ...VALID_ROUTER_PATHS.dynamicSegments,\n  ...VALID_ROUTER_PATHS.optionalDynamic,\n  ...VALID_ROUTER_PATHS.optionalStatic,\n  ...VALID_ROUTER_PATHS.mixedOptionals,\n  ...VALID_ROUTER_PATHS.wildcards,\n  ...VALID_ROUTER_PATHS.fileExtensions,\n] as const;\n"
  },
  {
    "path": "packages/sdk/src/router-paths.test.ts",
    "content": "/**\n * Shared test suite for router path handling consistency.\n *\n * This ensures that router paths are handled consistently across:\n * 1. Path schema validation (SDK) - OldPagePath, page paths\n * 2. URLPattern matching (builder) - used for all page routing\n * 3. Route generation for published sites (react-sdk)\n *\n * If a path is valid in one layer, it should work in all layers.\n */\n\nimport { describe, test, expect } from \"vitest\";\nimport { OldPagePath, ProjectNewRedirectPath } from \"./schema/pages\";\nimport { ALL_VALID_PATHS, ALL_INVALID_PATHS } from \"./router-path-test-data\";\n\n// Re-export for use in other packages\nexport {\n  VALID_ROUTER_PATHS,\n  INVALID_ROUTER_PATHS,\n  ALL_VALID_PATHS,\n  ALL_INVALID_PATHS,\n  VALID_URLPATTERN_PATHS,\n  STATIC_PATHS,\n} from \"./router-path-test-data\";\n\ndescribe(\"Router path validation consistency\", () => {\n  describe(\"OldPagePath schema - valid paths\", () => {\n    test.each(ALL_VALID_PATHS)(\"accepts: %s\", (path) => {\n      const result = OldPagePath.safeParse(path);\n      expect(result.success).toBe(true);\n    });\n  });\n\n  describe(\"OldPagePath schema - invalid paths\", () => {\n    test.each(ALL_INVALID_PATHS)(\"rejects: %s\", (path) => {\n      const result = OldPagePath.safeParse(path);\n      expect(result.success).toBe(false);\n    });\n  });\n\n  describe(\"ProjectNewRedirectPath schema - valid paths\", () => {\n    // ProjectNewRedirectPath is more permissive (allows / and external URLs)\n    const validTargetPaths = [\n      \"/\",\n      \"/about\",\n      \"/关于我们\",\n      \"https://example.com\",\n      \"https://example.com/path\",\n    ];\n\n    test.each(validTargetPaths)(\"accepts: %s\", (path) => {\n      const result = ProjectNewRedirectPath.safeParse(path);\n      expect(result.success).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/runtime.ts",
    "content": "export * from \"./resource-loader\";\nexport * from \"./to-string\";\nexport * from \"./form-fields\";\n\nexport const tagProperty = \"data-ws-tag\";\n\nexport const getTagFromProps = (\n  props: Record<string, unknown>\n): string | undefined => {\n  return props[tagProperty] as string | undefined;\n};\n\nexport const indexProperty = \"data-ws-index\";\n\nexport const getIndexWithinAncestorFromProps = (\n  props: Record<string, unknown>\n) => {\n  return props[indexProperty] as string | undefined;\n};\n\nexport const animationCanPlayOnCanvasProperty =\n  \"data-ws-animation-can-play-on-canvas\";\n"
  },
  {
    "path": "packages/sdk/src/schema/animation-schema.ts",
    "content": "import { StyleValue } from \"@webstudio-is/css-engine\";\nimport { z } from \"zod\";\n\n// Helper for creating union of string literals from array\nconst literalUnion = <T extends readonly string[]>(arr: T) =>\n  z.union(\n    arr.map((val) => z.literal(val)) as [\n      z.ZodLiteral<T[0]>,\n      z.ZodLiteral<T[1]>,\n      ...z.ZodLiteral<T[number]>[],\n    ]\n  );\n\n// Range Units\nexport const RANGE_UNITS = [\n  \"%\",\n  \"px\",\n  \"cm\",\n  \"mm\",\n  \"q\",\n  \"in\",\n  \"pt\",\n  \"pc\",\n  \"em\",\n  \"rem\",\n  \"ex\",\n  \"rex\",\n  \"cap\",\n  \"rcap\",\n  \"ch\",\n  \"rch\",\n  \"lh\",\n  \"rlh\",\n  \"vw\",\n  \"svw\",\n  \"lvw\",\n  \"dvw\",\n  \"vh\",\n  \"svh\",\n  \"lvh\",\n  \"dvh\",\n  \"vi\",\n  \"svi\",\n  \"lvi\",\n  \"dvi\",\n  \"vb\",\n  \"svb\",\n  \"lvb\",\n  \"dvb\",\n  \"vmin\",\n  \"svmin\",\n  \"lvmin\",\n  \"dvmin\",\n  \"vmax\",\n  \"svmax\",\n  \"lvmax\",\n  \"dvmax\",\n] as const;\n\nexport const rangeUnitSchema = literalUnion(RANGE_UNITS);\n\nexport const rangeUnitValueSchema = z.union([\n  z.object({\n    type: z.literal(\"unit\"),\n    value: z.number(),\n    unit: rangeUnitSchema,\n  }),\n  z.object({\n    type: z.literal(\"unparsed\"),\n    value: z.string(),\n  }),\n  z.object({\n    type: z.literal(\"var\"),\n    value: z.string(),\n  }),\n]);\n\nexport const TIME_UNITS = [\"ms\", \"s\"] as const;\nconst timeUnitSchema = literalUnion(TIME_UNITS);\n\nexport const durationUnitValueSchema = z.union([\n  z.object({\n    type: z.literal(\"unit\"),\n    value: z.number(),\n    unit: timeUnitSchema,\n  }),\n  z.object({\n    type: z.literal(\"var\"),\n    value: z.string(),\n  }),\n]);\n\nconst iterationsUnitValueSchema = z.union([z.number(), z.literal(\"infinite\")]);\n\n// view-timeline-inset\nexport const insetUnitValueSchema = z.union([\n  rangeUnitValueSchema,\n  z.object({\n    type: z.literal(\"keyword\"),\n    value: z.literal(\"auto\"),\n  }),\n]);\n\n// @todo: Fix Keyframe Styles\n// Can we use CssStyleMap for this? This type ends up not enforcing kebab case like.\nexport const keyframeStylesSchema = z.record(StyleValue);\n\n// Animation Keyframe\nexport const animationKeyframeSchema = z.object({\n  offset: z.number().optional(),\n  styles: keyframeStylesSchema,\n});\n\n// Keyframe Effect Options\nexport const keyframeEffectOptionsSchema = z.object({\n  easing: z.string().optional(),\n  fill: z\n    .union([\n      z.literal(\"none\"),\n      z.literal(\"forwards\"),\n      z.literal(\"backwards\"),\n      z.literal(\"both\"),\n    ])\n    .optional(), // FillMode\n  duration: durationUnitValueSchema.optional(),\n  delay: durationUnitValueSchema.optional(),\n  iterations: iterationsUnitValueSchema.optional(),\n});\n\n// Scroll Named Range\nexport const scrollNamedRangeSchema = z.union([\n  z.literal(\"start\"),\n  z.literal(\"end\"),\n]);\n\n// Scroll Range Value\nexport const scrollRangeValueSchema = z.tuple([\n  scrollNamedRangeSchema,\n  rangeUnitValueSchema,\n]);\n\n// Scroll Range Options\nexport const scrollRangeOptionsSchema = z.object({\n  rangeStart: scrollRangeValueSchema.optional(),\n  rangeEnd: scrollRangeValueSchema.optional(),\n});\n\n// Animation Axis\nexport const animationAxisSchema = z.union([\n  z.literal(\"block\"),\n  z.literal(\"inline\"),\n  z.literal(\"x\"),\n  z.literal(\"y\"),\n]);\n\n// View Named Range\nexport const viewNamedRangeSchema = z.union([\n  z.literal(\"contain\"),\n  z.literal(\"cover\"),\n  z.literal(\"entry\"),\n  z.literal(\"exit\"),\n  z.literal(\"entry-crossing\"),\n  z.literal(\"exit-crossing\"),\n]);\n\n// View Range Value\nexport const viewRangeValueSchema = z.tuple([\n  viewNamedRangeSchema,\n  rangeUnitValueSchema,\n]);\n\n// View Range Options\nexport const viewRangeOptionsSchema = z.object({\n  rangeStart: viewRangeValueSchema.optional(),\n  rangeEnd: viewRangeValueSchema.optional(),\n});\n\nconst baseAnimation = z.object({\n  name: z.string().optional(),\n  description: z.string().optional(),\n  enabled: z\n    .array(z.tuple([z.string().describe(\"breakpointId\"), z.boolean()]))\n    .optional(),\n  keyframes: z.array(animationKeyframeSchema),\n});\n\nexport const scrollAnimationSchema = baseAnimation.merge(\n  z.object({\n    timing: keyframeEffectOptionsSchema.merge(scrollRangeOptionsSchema),\n  })\n);\n\n// Scroll Action\nexport const scrollActionSchema = z.object({\n  type: z.literal(\"scroll\"),\n  source: z\n    .union([z.literal(\"closest\"), z.literal(\"nearest\"), z.literal(\"root\")])\n    .optional(),\n  axis: animationAxisSchema.optional(),\n  animations: z.array(scrollAnimationSchema),\n  isPinned: z.boolean().optional(),\n  debug: z.boolean().optional(),\n});\n\nexport const viewAnimationSchema = baseAnimation.merge(\n  z.object({\n    timing: keyframeEffectOptionsSchema.merge(viewRangeOptionsSchema),\n  })\n);\n\n// View Action\nexport const viewActionSchema = z.object({\n  type: z.literal(\"view\"),\n  subject: z.string().optional(),\n  axis: animationAxisSchema.optional(),\n  animations: z.array(viewAnimationSchema),\n\n  insetStart: insetUnitValueSchema.optional(),\n\n  insetEnd: insetUnitValueSchema.optional(),\n\n  isPinned: z.boolean().optional(),\n  debug: z.boolean().optional(),\n});\n\n// Animation Action\nexport const animationActionSchema = z.discriminatedUnion(\"type\", [\n  scrollActionSchema,\n  viewActionSchema,\n]);\n\n// Helper function to check if a value is a valid range unit\nexport const isRangeUnit = (\n  value: unknown\n): value is z.infer<typeof rangeUnitSchema> =>\n  rangeUnitSchema.safeParse(value).success;\n\n// Type exports\nexport type RangeUnit = z.infer<typeof rangeUnitSchema>;\nexport type RangeUnitValue = z.infer<typeof rangeUnitValueSchema>;\nexport type DurationUnitValue = z.infer<typeof durationUnitValueSchema>;\nexport type IterationsUnitValue = z.infer<typeof iterationsUnitValueSchema>;\nexport type TimeUnit = z.infer<typeof timeUnitSchema>;\nexport type KeyframeStyles = z.infer<typeof keyframeStylesSchema>;\nexport type AnimationKeyframe = z.infer<typeof animationKeyframeSchema>;\nexport type ScrollNamedRange = z.infer<typeof scrollNamedRangeSchema>;\nexport type ScrollRangeValue = z.infer<typeof scrollRangeValueSchema>;\nexport type ViewNamedRange = z.infer<typeof viewNamedRangeSchema>;\nexport type ViewRangeValue = z.infer<typeof viewRangeValueSchema>;\nexport type AnimationActionScroll = z.infer<typeof scrollActionSchema>;\nexport type AnimationActionView = z.infer<typeof viewActionSchema>;\nexport type AnimationAction = z.infer<typeof animationActionSchema>;\nexport type ScrollAnimation = z.infer<typeof scrollAnimationSchema>;\nexport type ViewAnimation = z.infer<typeof viewAnimationSchema>;\nexport type InsetUnitValue = z.infer<typeof insetUnitValueSchema>;\n"
  },
  {
    "path": "packages/sdk/src/schema/assets.ts",
    "content": "import { z } from \"zod\";\nimport { FontFormat, FontMeta } from \"@webstudio-is/fonts\";\n\nconst AssetId = z.string();\n\nconst baseAsset = {\n  id: AssetId,\n  projectId: z.string(),\n  size: z.number(),\n  name: z.string(),\n  filename: z.string().optional(),\n  description: z.union([z.string().optional(), z.null()]),\n  createdAt: z.string(),\n};\n\nexport const FontAsset = z.object({\n  ...baseAsset,\n  format: FontFormat,\n  meta: FontMeta,\n  type: z.literal(\"font\"),\n});\nexport type FontAsset = z.infer<typeof FontAsset>;\n\nexport const ImageMeta = z.object({\n  width: z.number(),\n  height: z.number(),\n});\nexport type ImageMeta = z.infer<typeof ImageMeta>;\n\nexport const ImageAsset = z.object({\n  ...baseAsset,\n  format: z.string(),\n  meta: ImageMeta,\n  type: z.literal(\"image\"),\n});\nexport type ImageAsset = z.infer<typeof ImageAsset>;\n\nexport const FileAsset = z.object({\n  ...baseAsset,\n  format: z.string(),\n  meta: z.object({}),\n  type: z.literal(\"file\"),\n});\nexport type FileAsset = z.infer<typeof FileAsset>;\n\nexport const Asset = z.union([FontAsset, ImageAsset, FileAsset]);\nexport type Asset = z.infer<typeof Asset>;\n\nexport const Assets = z.map(AssetId, Asset);\nexport type Assets = z.infer<typeof Assets>;\n"
  },
  {
    "path": "packages/sdk/src/schema/breakpoints.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\nimport { Breakpoint } from \"./breakpoints\";\n\ndescribe(\"Breakpoint schema\", () => {\n  test(\"accepts valid width-based breakpoints\", () => {\n    expect(() =>\n      Breakpoint.parse({\n        id: \"1\",\n        label: \"Desktop\",\n        minWidth: 1024,\n      })\n    ).not.toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"2\",\n        label: \"Mobile\",\n        maxWidth: 767,\n      })\n    ).not.toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"3\",\n        label: \"Base\",\n      })\n    ).not.toThrow();\n  });\n\n  test(\"accepts valid custom condition breakpoints\", () => {\n    expect(() =>\n      Breakpoint.parse({\n        id: \"1\",\n        label: \"Portrait\",\n        condition: \"orientation:portrait\",\n      })\n    ).not.toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"2\",\n        label: \"Hover\",\n        condition: \"hover:hover\",\n      })\n    ).not.toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"3\",\n        label: \"Dark Mode\",\n        condition: \"prefers-color-scheme:dark\",\n      })\n    ).not.toThrow();\n  });\n\n  test(\"normalizes empty condition to undefined\", () => {\n    const result = Breakpoint.parse({\n      id: \"1\",\n      label: \"Test\",\n      condition: \"\",\n    });\n    expect(result.condition).toBeUndefined();\n\n    const result2 = Breakpoint.parse({\n      id: \"2\",\n      label: \"Test\",\n      condition: \"   \",\n    });\n    expect(result2.condition).toBeUndefined();\n  });\n\n  test(\"rejects breakpoint with both condition and width\", () => {\n    expect(() =>\n      Breakpoint.parse({\n        id: \"1\",\n        label: \"Invalid\",\n        condition: \"orientation:portrait\",\n        minWidth: 1024,\n      })\n    ).toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"2\",\n        label: \"Invalid\",\n        condition: \"hover:hover\",\n        maxWidth: 767,\n      })\n    ).toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"3\",\n        label: \"Invalid\",\n        condition: \"hover:hover\",\n        minWidth: 1024,\n        maxWidth: 767,\n      })\n    ).toThrow();\n  });\n\n  test(\"allows breakpoint with both minWidth and maxWidth when min < max\", () => {\n    const result = Breakpoint.parse({\n      id: \"1\",\n      label: \"Tablet range\",\n      minWidth: 768,\n      maxWidth: 1024,\n    });\n    expect(result.minWidth).toBe(768);\n    expect(result.maxWidth).toBe(1024);\n    expect(result.condition).toBeUndefined();\n  });\n\n  test(\"rejects breakpoint with minWidth >= maxWidth\", () => {\n    expect(() =>\n      Breakpoint.parse({\n        id: \"1\",\n        label: \"Invalid\",\n        minWidth: 1024,\n        maxWidth: 767,\n      })\n    ).toThrow();\n\n    expect(() =>\n      Breakpoint.parse({\n        id: \"2\",\n        label: \"Invalid\",\n        minWidth: 768,\n        maxWidth: 768,\n      })\n    ).toThrow();\n  });\n\n  test(\"preserves valid condition values\", () => {\n    const result = Breakpoint.parse({\n      id: \"1\",\n      label: \"Portrait\",\n      condition: \"orientation:portrait\",\n    });\n    expect(result.condition).toBe(\"orientation:portrait\");\n    expect(result.minWidth).toBeUndefined();\n    expect(result.maxWidth).toBeUndefined();\n  });\n\n  test(\"handles complex conditions\", () => {\n    const result = Breakpoint.parse({\n      id: \"1\",\n      label: \"Complex\",\n      condition: \"orientation:portrait and hover:hover\",\n    });\n    expect(result.condition).toBe(\"orientation:portrait and hover:hover\");\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/schema/breakpoints.ts",
    "content": "import { z } from \"zod\";\n\nconst BreakpointId = z.string();\n\nexport const Breakpoint = z\n  .object({\n    id: BreakpointId,\n    label: z.string(),\n    minWidth: z.number().optional(),\n    maxWidth: z.number().optional(),\n    condition: z.string().optional(),\n  })\n  .transform((data) => {\n    // Normalize empty condition strings to undefined\n    if (data.condition !== undefined && data.condition.trim() === \"\") {\n      return { ...data, condition: undefined };\n    }\n    return data;\n  })\n  .refine(({ minWidth, maxWidth, condition }) => {\n    // If condition is set, minWidth and maxWidth should not be set\n    if (condition !== undefined) {\n      return minWidth === undefined && maxWidth === undefined;\n    }\n    // When both min and max width are defined, min must be less than max\n    if (minWidth !== undefined && maxWidth !== undefined) {\n      return minWidth < maxWidth;\n    }\n    return true;\n  }, \"Width-based (minWidth/maxWidth) and condition are mutually exclusive, and minWidth must be less than maxWidth\");\n\nexport type Breakpoint = z.infer<typeof Breakpoint>;\n\nexport const Breakpoints = z.map(BreakpointId, Breakpoint);\n\nexport type Breakpoints = z.infer<typeof Breakpoints>;\n\nexport const initialBreakpoints: Array<Breakpoint> = [\n  { id: \"placeholder\", label: \"Base\" },\n  { id: \"placeholder\", label: \"Tablet\", maxWidth: 991 },\n  { id: \"placeholder\", label: \"Mobile landscape\", maxWidth: 767 },\n  { id: \"placeholder\", label: \"Mobile portrait\", maxWidth: 479 },\n];\n"
  },
  {
    "path": "packages/sdk/src/schema/component-meta.ts",
    "content": "import { z } from \"zod\";\nimport type { HtmlTags } from \"html-tags\";\nimport type { Simplify } from \"type-fest\";\nimport { StyleValue, type CssProperty } from \"@webstudio-is/css-engine\";\nimport { PropMeta } from \"./prop-meta\";\n\nexport const PresetStyleDecl = z.object({\n  // State selector, e.g. :hover\n  state: z.optional(z.string()),\n  property: z.string(),\n  value: StyleValue,\n});\n\nexport type PresetStyleDecl = Simplify<\n  Omit<z.infer<typeof PresetStyleDecl>, \"property\"> & {\n    property: CssProperty;\n  }\n>;\n\nexport type PresetStyle<Tag extends HtmlTags = HtmlTags> = Partial<\n  Record<Tag, PresetStyleDecl[]>\n>;\n\nexport const componentCategories = [\n  \"general\",\n  \"typography\",\n  \"media\",\n  \"animations\",\n  \"data\",\n  \"forms\",\n  \"localization\",\n  \"radix\",\n  \"xml\",\n  \"other\",\n  \"hidden\",\n  \"internal\",\n] as const;\n\nexport const ComponentState = z.object({\n  selector: z.string(),\n  label: z.string(),\n});\n\nexport type ComponentState = z.infer<typeof ComponentState>;\n\n/**\n * rich-text - can be edited as rich text\n * instance - other instances accepted\n * ComponentName - accept specific components with none category\n */\nconst ComponentContent = z.string() as z.ZodType<\n  \"instance\" | \"rich-text\" | (string & {})\n>;\n\nexport const ContentModel = z.object({\n  /*\n   * instance - accepted by any parent with \"instance\" in children categories\n   * none - accepted by parents with this component name in children categories\n   */\n  category: z.union([z.literal(\"instance\"), z.literal(\"none\")]),\n  /**\n   * enforce direct children of category or components\n   */\n  children: z.array(ComponentContent),\n  /**\n   * enforce descendants of category or components\n   */\n  descendants: z.array(ComponentContent).optional(),\n});\n\nexport type ContentModel = z.infer<typeof ContentModel>;\n\nexport const WsComponentMeta = z.object({\n  category: z.enum(componentCategories).optional(),\n  contentModel: ContentModel.optional(),\n  // when this field is specified component receives\n  // prop with index of same components withiin specified ancestor\n  // important to automatically enumerate collections without\n  // naming every item manually\n  indexWithinAncestor: z.optional(z.string()),\n  label: z.optional(z.string()),\n  description: z.string().optional(),\n  icon: z.string().optional(),\n  presetStyle: z.optional(z.record(z.string(), z.array(PresetStyleDecl))),\n  states: z.optional(z.array(ComponentState)),\n  order: z.number().optional(),\n  // properties and html attributes that will be always visible in properties panel\n  initialProps: z.array(z.string()).optional(),\n  props: z.record(PropMeta).optional(),\n});\n\nexport type WsComponentMeta = Omit<\n  z.infer<typeof WsComponentMeta>,\n  \"presetStyle\"\n> & {\n  presetStyle?: PresetStyle;\n};\n"
  },
  {
    "path": "packages/sdk/src/schema/data-sources.ts",
    "content": "import { z } from \"zod\";\n\nconst DataSourceId = z.string();\n\nexport const DataSourceVariableValue = z.union([\n  z.object({\n    type: z.literal(\"number\"),\n    // initial value of variable store\n    value: z.number(),\n  }),\n  z.object({\n    type: z.literal(\"string\"),\n    value: z.string(),\n  }),\n  z.object({\n    type: z.literal(\"boolean\"),\n    value: z.boolean(),\n  }),\n  z.object({\n    type: z.literal(\"string[]\"),\n    value: z.array(z.string()),\n  }),\n  z.object({\n    type: z.literal(\"json\"),\n    value: z.unknown(),\n  }),\n]);\n\nexport const DataSource = z.union([\n  z.object({\n    type: z.literal(\"variable\"),\n    id: DataSourceId,\n    // The instance should always be specified for variables,\n    // however, there was a bug in the embed template\n    // which produced variables without an instance\n    // and these variables will fail validation\n    // if we make it required\n    scopeInstanceId: z.string().optional(),\n    name: z.string(),\n    value: DataSourceVariableValue,\n  }),\n  z.object({\n    type: z.literal(\"parameter\"),\n    id: DataSourceId,\n    scopeInstanceId: z.string().optional(),\n    name: z.string(),\n  }),\n  z.object({\n    type: z.literal(\"resource\"),\n    id: DataSourceId,\n    scopeInstanceId: z.string().optional(),\n    name: z.string(),\n    resourceId: z.string(),\n  }),\n]);\n\nexport type DataSource = z.infer<typeof DataSource>;\n\nexport const DataSources = z.map(DataSourceId, DataSource);\n\nexport type DataSources = z.infer<typeof DataSources>;\n"
  },
  {
    "path": "packages/sdk/src/schema/deployment.ts",
    "content": "import { z } from \"zod\";\n\nexport const Templates = z.enum([\n  \"docker\",\n  \"vercel\",\n  \"netlify\",\n  \"ssg\",\n  \"ssg-netlify\",\n  \"ssg-vercel\",\n]);\n\nexport type Templates = z.infer<typeof Templates>;\n\nexport const Deployment = z.union([\n  z.object({\n    destination: z.literal(\"static\"),\n    name: z.string(),\n    assetsDomain: z.string(),\n    // Must be validated very strictly\n    templates: z.array(Templates),\n  }),\n  z.object({\n    destination: z.literal(\"saas\").optional(),\n    domains: z.array(z.string()),\n    assetsDomain: z.string().optional(),\n    /**\n     * @deprecated This field is deprecated, use `domains` instead.\n     */\n    projectDomain: z.string().optional(),\n    excludeWstdDomainFromSearch: z.boolean().optional(),\n  }),\n]);\n\nexport type Deployment = z.infer<typeof Deployment>;\n"
  },
  {
    "path": "packages/sdk/src/schema/instances.ts",
    "content": "import { z } from \"zod\";\n\nexport const TextChild = z.object({\n  type: z.literal(\"text\"),\n  value: z.string(),\n  placeholder: z.boolean().optional(),\n});\n\nexport type TextChild = z.infer<typeof TextChild>;\n\nconst InstanceId = z.string();\n\nexport const IdChild = z.object({\n  type: z.literal(\"id\"),\n  value: InstanceId,\n});\nexport type IdChild = z.infer<typeof IdChild>;\n\nexport const ExpressionChild = z.object({\n  type: z.literal(\"expression\"),\n  value: z.string(),\n});\nexport type ExpressionChild = z.infer<typeof ExpressionChild>;\n\nexport const InstanceChild = z.union([IdChild, TextChild, ExpressionChild]);\n\nexport const Instance = z.object({\n  type: z.literal(\"instance\"),\n  id: InstanceId,\n  component: z.string(),\n  tag: z.string().optional(),\n  label: z.string().optional(),\n  children: z.array(InstanceChild),\n});\n\nexport type Instance = z.infer<typeof Instance>;\n\nexport const Instances = z.map(InstanceId, Instance);\n\nexport type Instances = z.infer<typeof Instances>;\n"
  },
  {
    "path": "packages/sdk/src/schema/pages.test.ts",
    "content": "import { describe, test, expect } from \"vitest\";\nimport { OldPagePath, ProjectNewRedirectPath } from \"./pages\";\n\ndescribe(\"OldPagePath\", () => {\n  describe(\"basic validation\", () => {\n    test(\"accepts valid path\", () => {\n      expect(OldPagePath.safeParse(\"/about\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/blog/post\").success).toBe(true);\n    });\n\n    test(\"rejects empty string\", () => {\n      const result = OldPagePath.safeParse(\"\");\n      expect(result.success).toBe(false);\n    });\n\n    test(\"rejects just a slash\", () => {\n      const result = OldPagePath.safeParse(\"/\");\n      expect(result.success).toBe(false);\n    });\n\n    test(\"must start with /\", () => {\n      const result = OldPagePath.safeParse(\"about\");\n      expect(result.success).toBe(false);\n    });\n\n    test(\"cannot end with /\", () => {\n      const result = OldPagePath.safeParse(\"/about/\");\n      expect(result.success).toBe(false);\n    });\n\n    test(\"cannot contain //\", () => {\n      const result = OldPagePath.safeParse(\"/about//page\");\n      expect(result.success).toBe(false);\n    });\n\n    test(\"rejects /s prefix (reserved)\", () => {\n      expect(OldPagePath.safeParse(\"/s\").success).toBe(false);\n      expect(OldPagePath.safeParse(\"/s/css\").success).toBe(false);\n    });\n\n    test(\"rejects /build prefix (reserved)\", () => {\n      expect(OldPagePath.safeParse(\"/build\").success).toBe(false);\n      expect(OldPagePath.safeParse(\"/build/main.js\").success).toBe(false);\n    });\n  });\n\n  describe(\"special characters\", () => {\n    test(\"accepts wildcards\", () => {\n      expect(OldPagePath.safeParse(\"/blog/*\").success).toBe(true);\n    });\n\n    test(\"accepts dynamic segments\", () => {\n      expect(OldPagePath.safeParse(\"/:slug\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/blog/:id\").success).toBe(true);\n    });\n\n    test(\"accepts optional segments\", () => {\n      expect(OldPagePath.safeParse(\"/:id?\").success).toBe(true);\n    });\n\n    test(\"accepts query strings\", () => {\n      expect(OldPagePath.safeParse(\"/search?q=test\").success).toBe(true);\n    });\n\n    test(\"accepts URL-encoded characters\", () => {\n      expect(OldPagePath.safeParse(\"/hello%20world\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/%E6%B8%AF%E8%81%9E\").success).toBe(true);\n    });\n  });\n\n  describe(\"invalid characters\", () => {\n    test(\"rejects spaces\", () => {\n      const result = OldPagePath.safeParse(\"/hello world\");\n      expect(result.success).toBe(false);\n    });\n\n    test(\"rejects angle brackets\", () => {\n      expect(OldPagePath.safeParse(\"/path<script>\").success).toBe(false);\n      expect(OldPagePath.safeParse(\"/path>other\").success).toBe(false);\n    });\n\n    test(\"rejects quotes\", () => {\n      expect(OldPagePath.safeParse('/path\"quote').success).toBe(false);\n    });\n\n    test(\"rejects curly braces\", () => {\n      expect(OldPagePath.safeParse(\"/path{test}\").success).toBe(false);\n    });\n\n    test(\"rejects pipe\", () => {\n      expect(OldPagePath.safeParse(\"/path|other\").success).toBe(false);\n    });\n\n    test(\"rejects backslash\", () => {\n      expect(OldPagePath.safeParse(\"/path\\\\other\").success).toBe(false);\n    });\n\n    test(\"rejects square brackets\", () => {\n      expect(OldPagePath.safeParse(\"/path[0]\").success).toBe(false);\n    });\n  });\n\n  describe(\"non-Latin characters (Unicode/UTF-8)\", () => {\n    // Chinese characters - common in Chinese websites\n    // Examples from https://aubreyyung.com/chinese-url-seo/\n    test(\"accepts Chinese characters (Simplified)\", () => {\n      expect(OldPagePath.safeParse(\"/关于我们\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/产品/手机\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/新闻\").success).toBe(true);\n    });\n\n    test(\"accepts Chinese characters (Traditional)\", () => {\n      expect(OldPagePath.safeParse(\"/關於我們\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/港聞\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/繁體中文\").success).toBe(true);\n    });\n\n    // Japanese characters\n    test(\"accepts Japanese characters (Hiragana)\", () => {\n      expect(OldPagePath.safeParse(\"/こんにちは\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/ブログ/記事\").success).toBe(true);\n    });\n\n    test(\"accepts Japanese characters (Katakana)\", () => {\n      expect(OldPagePath.safeParse(\"/カテゴリ\").success).toBe(true);\n    });\n\n    test(\"accepts Japanese characters (Kanji)\", () => {\n      expect(OldPagePath.safeParse(\"/日本語\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/東京\").success).toBe(true);\n    });\n\n    // Korean characters\n    test(\"accepts Korean characters (Hangul)\", () => {\n      expect(OldPagePath.safeParse(\"/한국어\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/블로그/포스트\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/서울\").success).toBe(true);\n    });\n\n    // Cyrillic characters (Russian, etc.)\n    test(\"accepts Cyrillic characters\", () => {\n      expect(OldPagePath.safeParse(\"/привет\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/о-нас\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/блог/статья\").success).toBe(true);\n    });\n\n    // Arabic characters\n    test(\"accepts Arabic characters\", () => {\n      expect(OldPagePath.safeParse(\"/مرحبا\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/عن-الشركة\").success).toBe(true);\n    });\n\n    // Hebrew characters\n    test(\"accepts Hebrew characters\", () => {\n      expect(OldPagePath.safeParse(\"/שלום\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/אודות\").success).toBe(true);\n    });\n\n    // Thai characters\n    test(\"accepts Thai characters\", () => {\n      expect(OldPagePath.safeParse(\"/สวัสดี\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/เกี่ยวกับเรา\").success).toBe(true);\n    });\n\n    // Greek characters\n    test(\"accepts Greek characters\", () => {\n      expect(OldPagePath.safeParse(\"/γεια\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/σχετικά\").success).toBe(true);\n    });\n\n    // Mixed Latin and non-Latin\n    test(\"accepts mixed Latin and non-Latin characters\", () => {\n      expect(OldPagePath.safeParse(\"/blog/关于\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/news/港聞\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/category/日本語\").success).toBe(true);\n    });\n\n    // European characters with diacritics\n    test(\"accepts European characters with diacritics\", () => {\n      expect(OldPagePath.safeParse(\"/über-uns\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/café\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/niño\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/naïve\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/résumé\").success).toBe(true);\n    });\n\n    // Emoji (while unusual, they are valid Unicode)\n    test(\"accepts emoji characters\", () => {\n      expect(OldPagePath.safeParse(\"/🎉\").success).toBe(true);\n      expect(OldPagePath.safeParse(\"/hello-🌍\").success).toBe(true);\n    });\n  });\n});\n\ndescribe(\"ProjectNewRedirectPath\", () => {\n  test(\"accepts relative paths\", () => {\n    expect(ProjectNewRedirectPath.safeParse(\"/about\").success).toBe(true);\n    expect(ProjectNewRedirectPath.safeParse(\"/\").success).toBe(true);\n  });\n\n  test(\"accepts absolute URLs\", () => {\n    expect(\n      ProjectNewRedirectPath.safeParse(\"https://example.com/page\").success\n    ).toBe(true);\n  });\n\n  test(\"accepts non-Latin character paths\", () => {\n    expect(ProjectNewRedirectPath.safeParse(\"/关于我们\").success).toBe(true);\n    expect(ProjectNewRedirectPath.safeParse(\"/日本語\").success).toBe(true);\n    expect(ProjectNewRedirectPath.safeParse(\"/한국어\").success).toBe(true);\n  });\n\n  test(\"rejects empty string\", () => {\n    expect(ProjectNewRedirectPath.safeParse(\"\").success).toBe(false);\n  });\n\n  test(\"rejects truly invalid URLs\", () => {\n    // Note: ProjectNewRedirectPath uses new URL(data, baseURL) which is very permissive\n    // It treats most strings as valid relative paths. The only truly invalid inputs\n    // are those that cannot be parsed as URLs at all.\n    expect(ProjectNewRedirectPath.safeParse(\"http://[invalid\").success).toBe(\n      false\n    );\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/schema/pages.ts",
    "content": "import { z } from \"zod\";\n\nexport type System = {\n  params: Record<string, string | undefined>;\n  search: Record<string, string | undefined>;\n  pathname: string;\n  origin: string;\n};\n\nconst MIN_TITLE_LENGTH = 2;\n\nconst PageId = z.string();\nconst FolderId = z.string();\n\nexport const FolderName = z\n  .string()\n  .refine((value) => value.trim() !== \"\", \"Can't be empty\");\n\nconst Slug = z\n  .string()\n  .refine(\n    (path) => /^[-a-z0-9]*$/.test(path),\n    \"Only a-z, 0-9 and - are allowed\"\n  );\n\nexport const Folder = z.object({\n  id: FolderId,\n  name: FolderName,\n  slug: Slug,\n  children: z.array(z.union([FolderId, PageId])),\n});\n\nexport type Folder = z.infer<typeof Folder>;\n\nexport const PageName = z\n  .string()\n  .refine((value) => value.trim() !== \"\", \"Can't be empty\");\n\nexport const PageTitle = z\n  .string()\n  .refine(\n    (val) => val.length >= MIN_TITLE_LENGTH,\n    `Minimum ${MIN_TITLE_LENGTH} characters required`\n  );\n\nexport const documentTypes = [\"html\", \"xml\"] as const;\n\nconst commonPageFields = {\n  id: PageId,\n  name: PageName,\n  title: PageTitle,\n  history: z.optional(z.array(z.string())),\n  rootInstanceId: z.string(),\n  systemDataSourceId: z.string().optional(),\n  meta: z.object({\n    description: z.string().optional(),\n    title: z.string().optional(),\n    excludePageFromSearch: z.string().optional(),\n    language: z.string().optional(),\n    socialImageAssetId: z.string().optional(),\n    socialImageUrl: z.string().optional(),\n    status: z.string().optional(),\n    redirect: z.string().optional(),\n    documentType: z.optional(z.enum(documentTypes)),\n    custom: z\n      .array(\n        z.object({\n          property: z.string(),\n          content: z.string(),\n        })\n      )\n      .optional(),\n  }),\n  marketplace: z.optional(\n    z.object({\n      include: z.optional(z.boolean()),\n      category: z.optional(z.string()),\n      thumbnailAssetId: z.optional(z.string()),\n    })\n  ),\n} as const;\n\nexport const HomePagePath = z\n  .string()\n  .refine((path) => path === \"\", \"Home page path must be empty\");\n\nconst HomePage = z.object({\n  ...commonPageFields,\n  path: HomePagePath,\n});\n\nconst DefaultPagePage = z\n  .string()\n  .refine((path) => path !== \"\", \"Can't be empty\")\n  .refine((path) => path !== \"/\", \"Can't be just a /\")\n  .refine((path) => path.endsWith(\"/\") === false, \"Can't end with a /\")\n  .refine((path) => path.includes(\"//\") === false, \"Can't contain repeating /\")\n  .refine(\n    (path) => /^[-_a-z0-9*:?\\\\/.]*$/.test(path),\n    \"Only a-z, 0-9, -, _, /, :, ?, . and * are allowed\"\n  )\n  .refine(\n    // We use /s for our system stuff like /s/css or /s/uploads\n    (path) => path !== \"/s\" && path.startsWith(\"/s/\") === false,\n    \"/s prefix is reserved for the system\"\n  )\n  .refine(\n    // Remix serves build artefacts like JS bundles from /build\n    // And we cannot customize it due to bug in Remix: https://github.com/remix-run/remix/issues/2933\n    (path) => path !== \"/build\" && path.startsWith(\"/build/\") === false,\n    \"/build prefix is reserved for the system\"\n  );\n\nexport const OldPagePath = z\n  .string()\n  .refine((path) => path !== \"\", \"Can't be empty\")\n  .refine((path) => path !== \"/\", \"Can't be just a /\")\n  .refine(\n    (path) => path === \"\" || path.startsWith(\"/\"),\n    \"Must start with a / or a full URL e.g. https://website.org\"\n  )\n  .refine((path) => path.endsWith(\"/\") === false, \"Can't end with a /\")\n  .refine((path) => path.includes(\"//\") === false, \"Can't contain repeating /\")\n  .refine((path) => {\n    // Disallow specific problematic characters that should never appear in paths:\n    // - Spaces and whitespace\n    // - URL-unsafe characters: < > \" { } | \\ ^ ` [ ]\n    // - Control characters\n    // All other characters (including non-Latin Unicode like Chinese, Japanese,\n    // Korean, Cyrillic, Arabic, etc.) are allowed as they are valid in modern URLs\n    // when properly encoded by the browser\n    // eslint-disable-next-line no-control-regex\n    const disallowedChars = /[\\s<>\"{}|\\\\^`[\\]\\u0000-\\u001f\\u007f]/;\n    return !disallowedChars.test(path);\n  }, \"Path contains invalid characters (spaces or URL-unsafe characters are not allowed)\")\n  .refine(\n    (path) => path !== \"/s\" && path.startsWith(\"/s/\") === false,\n    \"/s prefix is reserved for the system\"\n  )\n  .refine(\n    (path) => path !== \"/build\" && path.startsWith(\"/build/\") === false,\n    \"/build prefix is reserved for the system\"\n  );\n\nexport const PagePath = DefaultPagePage.refine(\n  (path) => path === \"\" || path.startsWith(\"/\"),\n  \"Must start with a / or a full URL e.g. https://website.org\"\n);\n\nconst Page = z.object({\n  ...commonPageFields,\n  path: PagePath,\n});\n\nconst ProjectMeta = z.object({\n  // All fields are optional to ensure consistency and allow for the addition of new fields without requiring migration\n  siteName: z.string().optional(),\n  contactEmail: z.string().optional(),\n  faviconAssetId: z.string().optional(),\n  code: z.string().optional(),\n});\nexport type ProjectMeta = z.infer<typeof ProjectMeta>;\n\nexport const ProjectNewRedirectPath = z\n  .string()\n  .min(1, \"Path is required\")\n  .refine((data) => {\n    // Users should be able to redirect from any old-path to the home page in the new project.\n    try {\n      // can be relative and absolute paths\n      new URL(data, \"http://url.com\");\n      return true;\n    } catch {\n      return false;\n    }\n  }, \"Must be a valid URL\");\n\nexport const PageRedirect = z.object({\n  old: OldPagePath,\n  new: ProjectNewRedirectPath,\n  status: z.enum([\"301\", \"302\"]).optional(),\n});\nexport type PageRedirect = z.infer<typeof PageRedirect>;\n\nexport const CompilerSettings = z.object({\n  // All fields are optional to ensure consistency and allow for the addition of new fields without requiring migration\n  atomicStyles: z.boolean().optional(),\n});\nexport type CompilerSettings = z.infer<typeof CompilerSettings>;\n\nexport type Page = z.infer<typeof Page>;\n\nexport const Pages = z.object({\n  meta: ProjectMeta.optional(),\n  compiler: CompilerSettings.optional(),\n  redirects: z.array(PageRedirect).optional(),\n  homePage: HomePage,\n  pages: z.array(Page),\n  folders: z\n    .array(Folder)\n    .refine((folders) => folders.length > 0, \"Folders can't be empty\"),\n});\n\nexport type Pages = z.infer<typeof Pages>;\n"
  },
  {
    "path": "packages/sdk/src/schema/prop-meta.ts",
    "content": "import { z } from \"zod\";\n\n// Note on Storybook compatibility!\n//\n// In the future we may need converter functions between Storybook's ArgTypes and PropMeta.\n// We should keep this in mind when designing the PropMeta type.\n// https://storybook.js.org/docs/react/api/argtypes\n// https://github.com/ComponentDriven/csf/blob/next/src/story.ts#L63\n//\n// We want to have the same list of controls as Storybook (with some additions)\n// https://storybook.js.org/docs/react/essentials/controls\n\nconst common = {\n  label: z.string().optional(),\n  description: z.string().optional(),\n  required: z.boolean(),\n};\n\nconst Tag = z.object({\n  ...common,\n  control: z.literal(\"tag\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.undefined().optional(),\n  options: z.array(z.string()),\n});\n\nconst Number = z.object({\n  ...common,\n  control: z.literal(\"number\"),\n  type: z.literal(\"number\"),\n  defaultValue: z.number().optional(),\n});\n\nconst Range = z.object({\n  ...common,\n  control: z.literal(\"range\"),\n  type: z.literal(\"number\"),\n  defaultValue: z.number().optional(),\n});\n\nconst Text = z.object({\n  ...common,\n  control: z.literal(\"text\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n  /**\n   * The number of rows in <textarea>. If set to 0 an <input> will be used instead.\n   * In line with Storybook team's plan: https://github.com/storybookjs/storybook/issues/21100\n   */\n  rows: z.number().optional(),\n});\n\nconst Resource = z.object({\n  ...common,\n  control: z.literal(\"resource\"),\n  type: z.literal(\"resource\"),\n  defaultValue: z.string().optional(),\n});\n\nconst Code = z.object({\n  ...common,\n  control: z.literal(\"code\"),\n  type: z.literal(\"string\"),\n  language: z.union([z.literal(\"html\"), z.literal(\"markdown\")]),\n  defaultValue: z.string().optional(),\n});\n\nconst CodeText = z.object({\n  ...common,\n  control: z.literal(\"codetext\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n});\n\nconst Color = z.object({\n  ...common,\n  control: z.literal(\"color\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n});\n\nconst Boolean = z.object({\n  ...common,\n  control: z.literal(\"boolean\"),\n  type: z.literal(\"boolean\"),\n  defaultValue: z.boolean().optional(),\n});\n\nconst Radio = z.object({\n  ...common,\n  control: z.literal(\"radio\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n  options: z.array(z.string()),\n});\n\nconst InlineRadio = z.object({\n  ...common,\n  control: z.literal(\"inline-radio\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n  options: z.array(z.string()),\n});\n\nconst Select = z.object({\n  ...common,\n  control: z.literal(\"select\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n  options: z.array(z.string()),\n});\n\nconst Check = z.object({\n  ...common,\n  control: z.literal(\"check\"),\n  type: z.literal(\"string[]\"),\n  defaultValue: z.array(z.string()).optional(),\n  options: z.array(z.string()),\n});\n\nconst InlineCheck = z.object({\n  ...common,\n  control: z.literal(\"inline-check\"),\n  type: z.literal(\"string[]\"),\n  defaultValue: z.array(z.string()).optional(),\n  options: z.array(z.string()),\n});\n\nconst MultiSelect = z.object({\n  ...common,\n  control: z.literal(\"multi-select\"),\n  type: z.literal(\"string[]\"),\n  defaultValue: z.array(z.string()).optional(),\n  options: z.array(z.string()),\n});\n\nconst File = z.object({\n  ...common,\n  control: z.literal(\"file\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n  /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept */\n  accept: z.string().optional(),\n});\n\nconst Url = z.object({\n  ...common,\n  control: z.literal(\"url\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n});\n\nconst Json = z.object({\n  ...common,\n  control: z.literal(\"json\"),\n  type: z.literal(\"json\"),\n  defaultValue: z.unknown().optional(),\n});\n\n// we neither generate date nor support it in props panel, listed here for completeness\nconst Date = z.object({\n  ...common,\n  control: z.literal(\"date\"),\n\n  // @todo not sure what type should be here\n  // (we don't support Date yet, added for completeness)\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n});\n\nconst Action = z.object({\n  ...common,\n  control: z.literal(\"action\"),\n  type: z.literal(\"action\"),\n  defaultValue: z.undefined().optional(),\n});\n\nconst TextContent = z.object({\n  ...common,\n  control: z.literal(\"textContent\"),\n  type: z.literal(\"string\"),\n  defaultValue: z.string().optional(),\n});\n\nconst AnimationAction = z.object({\n  ...common,\n  control: z.literal(\"animationAction\"),\n  type: z.literal(\"animationAction\"),\n  defaultValue: z.undefined().optional(),\n});\n\nexport const PropMeta = z.union([\n  Tag,\n  Number,\n  Range,\n  Text,\n  Resource,\n  Code,\n  CodeText,\n  Color,\n  Boolean,\n  Radio,\n  InlineRadio,\n  Select,\n  MultiSelect,\n  Check,\n  InlineCheck,\n  File,\n  Url,\n  Json,\n  Date,\n  Action,\n  TextContent,\n  AnimationAction,\n]);\n\nexport type PropMeta = z.infer<typeof PropMeta>;\n"
  },
  {
    "path": "packages/sdk/src/schema/props.ts",
    "content": "import { z } from \"zod\";\nimport { animationActionSchema } from \"./animation-schema\";\n\nconst PropId = z.string();\n\nconst baseProp = {\n  id: PropId,\n  instanceId: z.string(),\n  name: z.string(),\n  required: z.optional(z.boolean()),\n};\n\nexport const Prop = z.union([\n  z.object({\n    ...baseProp,\n    type: z.literal(\"number\"),\n    value: z.number(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"string\"),\n    value: z.string(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"boolean\"),\n    value: z.boolean(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"json\"),\n    value: z.unknown(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"asset\"),\n    value: z.string(), // asset id\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"page\"),\n    value: z.union([\n      z.string(), // page id\n      z.object({\n        pageId: z.string(),\n        instanceId: z.string(),\n      }),\n    ]),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"string[]\"),\n    value: z.array(z.string()),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"parameter\"),\n    // data source id\n    value: z.string(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"resource\"),\n    // resource id\n    value: z.string(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"expression\"),\n    // expression code\n    value: z.string(),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"action\"),\n    value: z.array(\n      z.object({\n        type: z.literal(\"execute\"),\n        args: z.array(z.string()),\n        code: z.string(),\n      })\n    ),\n  }),\n  z.object({\n    ...baseProp,\n    type: z.literal(\"animationAction\"),\n    value: animationActionSchema,\n  }),\n]);\n\nexport type Prop = z.infer<typeof Prop>;\n\nexport const Props = z.map(PropId, Prop);\n\nexport type Props = z.infer<typeof Props>;\n"
  },
  {
    "path": "packages/sdk/src/schema/resources.ts",
    "content": "import { z } from \"zod\";\n\nconst ResourceId = z.string();\n\nconst Method = z.union([\n  z.literal(\"get\"),\n  z.literal(\"post\"),\n  z.literal(\"put\"),\n  z.literal(\"delete\"),\n]);\n\nexport const Resource = z.object({\n  id: ResourceId,\n  name: z.string(),\n  control: z.optional(z.union([z.literal(\"system\"), z.literal(\"graphql\")])),\n  method: Method,\n  // expression\n  url: z.string(),\n  searchParams: z\n    .array(\n      z.object({\n        name: z.string(),\n        // expression\n        value: z.string(),\n      })\n    )\n    .optional(),\n  headers: z.array(\n    z.object({\n      name: z.string(),\n      // expression\n      value: z.string(),\n    })\n  ),\n  // expression\n  body: z.optional(z.string()),\n});\n\nexport type Resource = z.infer<typeof Resource>;\n\n// evaluated variant of resource\nexport const ResourceRequest = z.object({\n  name: z.string(),\n  method: Method,\n  url: z.string(),\n  searchParams: z.array(\n    z.object({\n      name: z.string(),\n      // can be string or object which should be serialized\n      value: z.unknown(),\n    })\n  ),\n  headers: z.array(\n    z.object({\n      name: z.string(),\n      // can be string or object which should be serialized\n      value: z.unknown(),\n    })\n  ),\n  body: z.optional(z.unknown()),\n});\n\nexport type ResourceRequest = z.infer<typeof ResourceRequest>;\n\nexport const Resources = z.map(ResourceId, Resource);\n\nexport type Resources = z.infer<typeof Resources>;\n"
  },
  {
    "path": "packages/sdk/src/schema/style-source-selections.ts",
    "content": "import { z } from \"zod\";\n\nconst InstanceId = z.string();\n\nconst StyleSourceId = z.string();\n\nexport const StyleSourceSelection = z.object({\n  instanceId: InstanceId,\n  values: z.array(StyleSourceId),\n});\n\nexport type StyleSourceSelection = z.infer<typeof StyleSourceSelection>;\n\nexport const StyleSourceSelections = z.map(InstanceId, StyleSourceSelection);\n\nexport type StyleSourceSelections = z.infer<typeof StyleSourceSelections>;\n"
  },
  {
    "path": "packages/sdk/src/schema/style-sources.ts",
    "content": "import { z } from \"zod\";\n\nconst StyleSourceId = z.string();\n\nconst StyleSourceToken = z.object({\n  type: z.literal(\"token\"),\n  id: StyleSourceId,\n  name: z.string(),\n});\n\nexport type StyleSourceToken = z.infer<typeof StyleSourceToken>;\n\nconst StyleSourceLocal = z.object({\n  type: z.literal(\"local\"),\n  id: StyleSourceId,\n});\n\nexport const StyleSource = z.union([StyleSourceToken, StyleSourceLocal]);\n\nexport type StyleSource = z.infer<typeof StyleSource>;\n\nexport const StyleSources = z.map(StyleSourceId, StyleSource);\n\nexport type StyleSources = z.infer<typeof StyleSources>;\n"
  },
  {
    "path": "packages/sdk/src/schema/styles.ts",
    "content": "import { z } from \"zod\";\nimport { type StyleProperty, StyleValue } from \"@webstudio-is/css-engine\";\nimport type { Simplify } from \"type-fest\";\n\nconst StyleDeclRaw = z.object({\n  styleSourceId: z.string(),\n  breakpointId: z.string(),\n  state: z.optional(z.string()),\n  // @todo can't figure out how to make property to be enum\n  property: z.string(),\n  value: StyleValue,\n  listed: z\n    .boolean()\n    .optional()\n    .describe(\"Whether the style is from the Advanced panel\"),\n});\n\nexport type StyleDecl = Simplify<\n  Omit<z.infer<typeof StyleDeclRaw>, \"property\"> & {\n    property: StyleProperty;\n  }\n>;\nexport const StyleDecl = StyleDeclRaw as z.ZodType<StyleDecl>;\n\nexport type StyleDeclKey = string;\n\nexport const getStyleDeclKey = (\n  styleDecl: Omit<StyleDecl, \"value\">\n): StyleDeclKey => {\n  return `${styleDecl.styleSourceId}:${styleDecl.breakpointId}:${\n    styleDecl.property\n  }:${styleDecl.state ?? \"\"}`;\n};\n\nexport const Styles = z.map(z.string(), StyleDecl);\n\nexport type Styles = Map<string, StyleDecl>;\n"
  },
  {
    "path": "packages/sdk/src/schema/webstudio.ts",
    "content": "import { z } from \"zod\";\nimport { Asset, Assets } from \"./assets\";\nimport { DataSource, DataSources } from \"./data-sources\";\nimport { Resource, Resources } from \"./resources\";\nimport { Instance, InstanceChild, Instances } from \"./instances\";\nimport { Prop, Props } from \"./props\";\nimport { Breakpoint, Breakpoints } from \"./breakpoints\";\nimport {\n  StyleSourceSelection,\n  StyleSourceSelections,\n} from \"./style-source-selections\";\nimport { StyleSource, StyleSources } from \"./style-sources\";\nimport { StyleDecl, Styles } from \"./styles\";\nimport { Pages } from \"./pages\";\n\n/**\n * transferrable and insertable part of webstudio data\n * may contain reusable parts like tokens and custom components\n */\nexport const WebstudioFragment = z.object({\n  children: z.array(InstanceChild),\n  instances: z.array(Instance),\n  assets: z.array(Asset),\n  dataSources: z.array(DataSource),\n  resources: z.array(Resource),\n  props: z.array(Prop),\n  breakpoints: z.array(Breakpoint),\n  styleSourceSelections: z.array(StyleSourceSelection),\n  styleSources: z.array(StyleSource),\n  styles: z.array(StyleDecl),\n});\n\nexport type WebstudioFragment = z.infer<typeof WebstudioFragment>;\n\n/**\n * all persisted webstudio data in normalized format\n * should be used for composing parts of logic within\n * single transaction\n */\nexport type WebstudioData = {\n  pages: Pages;\n  assets: Assets;\n  dataSources: DataSources;\n  resources: Resources;\n  instances: Instances;\n  props: Props;\n  breakpoints: Breakpoints;\n  styleSourceSelections: StyleSourceSelections;\n  styleSources: StyleSources;\n  styles: Styles;\n};\n"
  },
  {
    "path": "packages/sdk/src/scope.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { createScope } from \"./scope\";\n\ntest(\"use variable name for specific id and suffix on collision\", () => {\n  const scope = createScope();\n  expect(scope.getName(\"1\", \"myName\")).toEqual(\"myName\");\n  expect(scope.getName(\"2\", \"myName\")).toEqual(\"myName_1\");\n  // reuse already cached one\n  expect(scope.getName(\"1\", \"myName\")).toEqual(\"myName\");\n});\n\ntest(\"allow to predefine already occupied identifiers\", () => {\n  const scope = createScope([\"myName\", \"anotherName\"]);\n  expect(scope.getName(\"1\", \"myName\")).toEqual(\"myName_1\");\n  expect(scope.getName(\"2\", \"anotherName\")).toEqual(\"anotherName_1\");\n  expect(scope.getName(\"3\", \"newName\")).toEqual(\"newName\");\n});\n\ntest(\"delete non-ascii characters from name\", () => {\n  const scope = createScope([\"_\"]);\n  expect(scope.getName(\"1\", \"привет\")).toEqual(\"__1\");\n});\n\ntest(\"delete spaces from name\", () => {\n  const scope = createScope([\"_\"]);\n  expect(scope.getName(\"1\", \"My Variable\")).toEqual(\"MyVariable\");\n});\n\ntest(\"prefix name starting with digit\", () => {\n  const scope = createScope([\"_\"]);\n  expect(scope.getName(\"1\", \"123\")).toEqual(\"_123\");\n});\n\ntest(\"resolve conflict between name with number and name with index\", () => {\n  const scope = createScope();\n  expect([\n    scope.getName(\"1\", \"image\"),\n    scope.getName(\"2\", \"image\"),\n    scope.getName(\"3\", \"image\"),\n    scope.getName(\"4\", \"image\"),\n    scope.getName(\"5\", \"image_3\"),\n  ]).toEqual([\"image\", \"image_1\", \"image_2\", \"image_3\", \"image_3_1\"]);\n});\n"
  },
  {
    "path": "packages/sdk/src/scope.ts",
    "content": "import reservedIdentifiers from \"reserved-identifiers\";\n\nconst identifiers = reservedIdentifiers({ includeGlobalProperties: true });\nconst isReserved = (identifier: string) => identifiers.has(identifier);\n\nexport type Scope = {\n  /**\n   * Accepts unique id to identify specific variable\n   * and preferred name to use it as variable name\n   * or suffix if already used.\n   */\n  getName(id: string, preferredName: string): string;\n};\n\n// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers\nconst normalizeJsName = (name: string) => {\n  // only letters, digits, underscores and dollar signs allowed in names\n  // delete everything else\n  name = name.replaceAll(/[^\\w$]/g, \"\");\n  // put at least single underscore in name to avoid syntax error\n  if (name.length === 0) {\n    return \"_\";\n  }\n  // variable name can start with letter, underscore and dollar signs\n  if (/[A-Za-z_$]/.test(name[0]) === false) {\n    name = `_${name}`;\n  }\n\n  if (isReserved(name)) {\n    return `${name}_`;\n  }\n\n  return name;\n};\n\n/**\n * Utility to maintain unique variable when generate code.\n * Single scope is shared for generated module for simplicity.\n *\n * occupiedIdentifiers parameter prevents collision with hardcoded\n * identifiers.\n */\nexport const createScope = (\n  occupiedIdentifiers: string[] = [],\n  normalizeName = normalizeJsName,\n  separator = \"_\"\n): Scope => {\n  const nameById = new Map<string, string>();\n  const usedNames = new Set<string>();\n  for (const identifier of occupiedIdentifiers) {\n    usedNames.add(identifier);\n  }\n\n  const getName = (id: string, preferredName: string) => {\n    const cachedName = nameById.get(id);\n    if (cachedName !== undefined) {\n      return cachedName;\n    }\n    preferredName = normalizeName(preferredName);\n    let index = 0;\n    let scopedName = preferredName;\n    while (usedNames.has(scopedName)) {\n      index += 1;\n      scopedName = `${preferredName}${separator}${index}`;\n    }\n    nameById.set(id, scopedName);\n    usedNames.add(scopedName);\n    return scopedName;\n  };\n\n  return {\n    getName,\n  };\n};\n"
  },
  {
    "path": "packages/sdk/src/to-string.ts",
    "content": "export const createJsonStringifyProxy = <T extends object>(target: T): T => {\n  return new Proxy(target, {\n    get(target, prop, receiver) {\n      if (prop === \"toString\") {\n        return function () {\n          return JSON.stringify(target);\n        };\n      }\n\n      const value = Reflect.get(target, prop, receiver);\n\n      if (typeof value === \"object\" && value !== null) {\n        return createJsonStringifyProxy(value);\n      }\n\n      return value;\n    },\n  });\n};\n\nexport const isPlainObject = (value: unknown): value is object => {\n  return (\n    Object.prototype.toString.call(value) === \"[object Object]\" &&\n    (Object.getPrototypeOf(value) === null ||\n      Object.getPrototypeOf(value) === Object.prototype)\n  );\n};\n\nexport const serializeValue = (value: unknown) => {\n  if (typeof value === \"string\") {\n    return value;\n  }\n  return JSON.stringify(value);\n};\n"
  },
  {
    "path": "packages/sdk/src/url-pattern.test.ts",
    "content": "import { expect, test, describe } from \"vitest\";\nimport { isPathnamePattern, isAbsoluteUrl } from \"./url-pattern\";\n\ntest(\"check pathname is pattern\", () => {\n  expect(isPathnamePattern(\"/:name\")).toEqual(true);\n  expect(isPathnamePattern(\"/:slug*\")).toEqual(true);\n  expect(isPathnamePattern(\"/:id?\")).toEqual(true);\n  expect(isPathnamePattern(\"/*\")).toEqual(true);\n\n  expect(isPathnamePattern(\"\")).toEqual(false);\n  expect(isPathnamePattern(\"/\")).toEqual(false);\n  expect(isPathnamePattern(\"/blog\")).toEqual(false);\n  expect(isPathnamePattern(\"/blog/post-name\")).toEqual(false);\n});\n\ndescribe(\"isAbsoluteUrl\", () => {\n  test(\"returns true for absolute URLs\", () => {\n    expect(isAbsoluteUrl(\"https://example.com\")).toBe(true);\n    expect(isAbsoluteUrl(\"http://example.com\")).toBe(true);\n    expect(isAbsoluteUrl(\"https://example.com/path\")).toBe(true);\n    expect(isAbsoluteUrl(\"https://example.com/path?query=1\")).toBe(true);\n    expect(isAbsoluteUrl(\"https://example.com:8080/path\")).toBe(true);\n    expect(isAbsoluteUrl(\"ftp://files.example.com\")).toBe(true);\n    expect(isAbsoluteUrl(\"mailto:test@example.com\")).toBe(true);\n    expect(isAbsoluteUrl(\"data:text/html,<h1>Hello</h1>\")).toBe(true);\n  });\n\n  test(\"returns false for relative URLs\", () => {\n    expect(isAbsoluteUrl(\"/path\")).toBe(false);\n    expect(isAbsoluteUrl(\"/path/to/file\")).toBe(false);\n    expect(isAbsoluteUrl(\"path/to/file\")).toBe(false);\n    expect(isAbsoluteUrl(\"./path\")).toBe(false);\n    expect(isAbsoluteUrl(\"../path\")).toBe(false);\n    expect(isAbsoluteUrl(\"\")).toBe(false);\n    expect(isAbsoluteUrl(\"?query=1\")).toBe(false);\n    expect(isAbsoluteUrl(\"#hash\")).toBe(false);\n  });\n\n  test(\"returns false for invalid URLs\", () => {\n    expect(isAbsoluteUrl(\"not a url\")).toBe(false);\n    expect(isAbsoluteUrl(\"https://\")).toBe(false);\n    expect(isAbsoluteUrl(\"://missing-protocol\")).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/sdk/src/url-pattern.ts",
    "content": "// /:slug -> { name: \"slug\", modifier: \"\" }\n// /:slug* -> { name: \"slug\", modifier: \"*\" }\n// /:slug? -> { name: \"slug\", modifier: \"?\" }\n// /* -> { wildcard: \"*\" }\nconst tokenRegex = /:(?<name>\\w+)(?<modifier>[?*]?)|(?<wildcard>(?<!:\\w+)\\*)/;\n\nexport const isPathnamePattern = (pathname: string) =>\n  tokenRegex.test(pathname);\n\n// use separate regex from matchAll because regex.test is stateful when used with g flag\nconst tokenRegexGlobal = new RegExp(tokenRegex.source, \"g\");\n\nexport const matchPathnameParams = (pathname: string) => {\n  return pathname.matchAll(tokenRegexGlobal);\n};\n\n/**\n * Check if a string is an absolute URL (has a valid protocol)\n */\nexport const isAbsoluteUrl = (href: string) => {\n  try {\n    new URL(href);\n    return true;\n  } catch {\n    return false;\n  }\n};\n"
  },
  {
    "path": "packages/sdk/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \"src/index.ts\",\n    \"src/runtime.ts\",\n    \"src/__generated__/normalize.css.ts\"\n  ],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/sdk/tsconfig.typecheck.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": null\n  }\n}\n"
  },
  {
    "path": "packages/sdk-cli/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk-cli\",\n  \"version\": \"0.94.0\",\n  \"description\": \"Webstudio CLI for SDK development\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"webstudio-sdk\": \"./src/bin.ts\"\n  },\n  \"files\": [\n    \"src/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"dependencies\": {\n    \"@webstudio-is/react-sdk\": \"workspace:^\",\n    \"@webstudio-is/sdk\": \"workspace:^\",\n    \"@webstudio-is/template\": \"workspace:^\",\n    \"change-case\": \"^5.4.4\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:^\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-cli/src/bin.ts",
    "content": "#!/usr/bin/env tsx --conditions=webstudio --experimental-import-meta-resolve\nimport \"./cli\";\n"
  },
  {
    "path": "packages/sdk-cli/src/cli.ts",
    "content": "import { argv, exit } from \"node:process\";\nimport { parseArgs } from \"node:util\";\nimport { generateStories } from \"./generate-stories\";\n\nconst { positionals } = parseArgs({\n  args: argv.slice(2),\n  allowPositionals: true,\n});\n\nconst [command] = positionals;\n\nif (command === \"generate-stories\") {\n  await generateStories();\n  exit(0);\n}\n\nthrow Error(`Unknown command ${command}`);\n"
  },
  {
    "path": "packages/sdk-cli/src/generate-stories.ts",
    "content": "import { cwd } from \"node:process\";\nimport { dirname, join } from \"node:path\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { kebabCase } from \"change-case\";\nimport {\n  type Instances,\n  type Instance,\n  type Scope,\n  type WsComponentMeta,\n  createScope,\n  parseComponentName,\n  getStyleDeclKey,\n  coreMetas,\n  generateCss,\n} from \"@webstudio-is/sdk\";\nimport { generateWebstudioComponent } from \"@webstudio-is/react-sdk\";\nimport { renderTemplate, type TemplateMeta } from \"@webstudio-is/template\";\n\nconst WS_NAMESPACE = \"ws\";\nconst BASE_NAMESPACE = \"@webstudio-is/sdk-components-react\";\n\nconst generateComponentImports = ({\n  scope,\n  metas,\n  components,\n}: {\n  scope: Scope;\n  metas: Map<string, WsComponentMeta>;\n  components: Set<string>;\n}) => {\n  const namespaces = new Map<\n    string,\n    Set<[shortName: string, componentName: string]>\n  >();\n\n  for (const component of components) {\n    const parsed = parseComponentName(component);\n    let [namespace = BASE_NAMESPACE] = parsed;\n    const [_namespace, shortName] = parsed;\n    if (metas.has(shortName)) {\n      namespace = \"../components\";\n    }\n    if (namespaces.has(namespace) === false) {\n      namespaces.set(\n        namespace,\n        new Set<[shortName: string, componentName: string]>()\n      );\n    }\n    namespaces.get(namespace)?.add([shortName, component]);\n  }\n\n  let componentImports = \"\";\n  for (const [namespace, componentsSet] of namespaces.entries()) {\n    if (namespace === WS_NAMESPACE) {\n      continue;\n    }\n    const specifiers = Array.from(componentsSet)\n      .map(\n        ([shortName, component]) =>\n          `${shortName} as ${scope.getName(component, shortName)}`\n      )\n      .join(\", \");\n    componentImports += `import { ${specifiers} } from \"${namespace}\";\\n`;\n  }\n  return componentImports;\n};\n\nconst getStoriesImports = ({ hasState }: { hasState: boolean }) =>\n  hasState\n    ? `import { useVariableState } from \"@webstudio-is/react-sdk/runtime\";\\n`\n    : \"\";\n\nconst getStoriesExports = (name: string, css: string) => `\nexport default {\n  title: \"Components/${name}\"\n};\n\nconst Story = {\n  render() {\n    return <>\n      <style>\n      {\\`\n${css}\n      \\`}\n      </style>\n      <Component />\n    </>\n  }\n}\n\nexport { Story as ${name} }\n`;\n\nexport const generateStories = async () => {\n  const packageFile = await readFile(join(cwd(), \"package.json\"), \"utf8\");\n  const packageJson = JSON.parse(packageFile);\n  const templatesModule = join(cwd(), \"src/templates.ts\");\n  const templates = new Map<string, TemplateMeta>(\n    Object.entries(await import(templatesModule))\n  );\n  const metasModule = join(cwd(), \"src/metas.ts\");\n  const metas = new Map<string, WsComponentMeta>(\n    Object.entries(await import(metasModule))\n  );\n  const storiesDir = join(dirname(templatesModule), \"__generated__\");\n  await mkdir(storiesDir, { recursive: true });\n\n  for (const [name, meta] of templates) {\n    const rootInstanceId = \"root\";\n    const data = renderTemplate(meta.template);\n    const instances: Instances = new Map([\n      [\n        rootInstanceId,\n        {\n          type: \"instance\",\n          id: rootInstanceId,\n          component: \"Box\",\n          children: data.children,\n        },\n      ],\n      ...data.instances.map((instance) => [instance.id, instance] as const),\n    ]);\n    const props = new Map(data.props.map((prop) => [prop.id, prop]));\n    const breakpoints = new Map(\n      data.breakpoints.map((breakpoint) => [breakpoint.id, breakpoint])\n    );\n    const components = new Set<Instance[\"component\"]>();\n    const namespaces: string[] = [];\n    for (const instance of instances.values()) {\n      const [namespace = BASE_NAMESPACE] = parseComponentName(\n        instance.component\n      );\n      components.add(instance.component);\n      namespaces.push(namespace);\n    }\n    const usedMetas = new Map<string, WsComponentMeta>();\n    for (const namespace of namespaces) {\n      let namespaceMetas;\n      if (namespace === WS_NAMESPACE) {\n        namespaceMetas = new Map(Object.entries(coreMetas));\n      } else if (namespace === packageJson.name) {\n        namespaceMetas = metas;\n      } else {\n        const metasUrl = import.meta.resolve(\n          `${namespace}/metas`,\n          templatesModule\n        );\n        namespaceMetas = new Map(Object.entries(await import(metasUrl)));\n      }\n      for (const [name, meta] of namespaceMetas) {\n        let prefixedName = name;\n        if (namespace !== BASE_NAMESPACE && namespace !== WS_NAMESPACE) {\n          prefixedName = `${namespace}:${name}`;\n        }\n        if (components.has(prefixedName)) {\n          usedMetas.set(prefixedName, meta as WsComponentMeta);\n        }\n      }\n    }\n\n    const { cssText, classes } = generateCss({\n      instances,\n      props,\n      assets: new Map(),\n      breakpoints,\n      styles: new Map(data.styles.map((item) => [getStyleDeclKey(item), item])),\n      styleSourceSelections: new Map(\n        data.styleSourceSelections.map((item) => [item.instanceId, item])\n      ),\n      componentMetas: usedMetas,\n      assetBaseUrl: \"/\",\n      atomic: false,\n    });\n    const scope = createScope([\"Component\", \"Story\", \"props\", \"useState\"]);\n    let content = \"\";\n    content += getStoriesImports({\n      hasState: data.dataSources.some(\n        (dataSource) => dataSource.type === \"variable\"\n      ),\n    });\n    content += generateComponentImports({\n      scope,\n      metas,\n      components,\n    });\n    content += `\\n`;\n    content += generateWebstudioComponent({\n      classesMap: classes,\n      scope,\n      name: `Component`,\n      rootInstanceId,\n      parameters: [],\n      instances,\n      props,\n      dataSources: new Map(data.dataSources.map((prop) => [prop.id, prop])),\n      metas: usedMetas,\n    });\n\n    content += getStoriesExports(name, cssText);\n    await writeFile(\n      join(storiesDir, kebabCase(name) + \".stories.tsx\"),\n      content\n    );\n  }\n};\n"
  },
  {
    "path": "packages/sdk-cli/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/sdk-components-animation/LICENSE",
    "content": "Webstudio, Inc. End User License Agreement (EULA)\n\nLast updated: August 3, 2025\n\n1. Definitions\n   “Agreement” means this End User License Agreement.\n   “Company” means Webstudio, Inc., its successors and assigns.\n   “Software” means the computer programs identified by this Agreement, including any updates, upgrades, and accompanying documentation.\n   “Licensee” or “You” means the individual or legal entity that has downloaded, installed, or is using the Software.\n\n2. Grant of License\n   Subject to the terms of this Agreement, Company grants You a non-exclusive, non-transferable, revocable, limited license to:\n   a. Use the Software solely for Your internal business purposes on the number of devices or seats for which You have purchased licenses.\n   b. Run the Software in object code form only.\n   No other rights are granted.\n\n3. Restrictions\n   You shall not, and shall not permit any third party to:\n   a. Copy, reproduce, or distribute the Software, in whole or in part, except as expressly authorized in writing by Company.\n   b. Modify, adapt, translate, or create derivative works of the Software, or merge the Software into any other program.\n   c. Reverse-engineer, decompile, disassemble, or otherwise attempt to discover the source code of the Software.\n   d. Rent, lease, sell, sublicense, or otherwise transfer rights to the Software.\n   e. Remove, alter, or obscure any proprietary notices, including copyright notices, on the Software.\n\n4. Ownership\n   The Software is licensed, not sold. Company and its licensors retain all right, title, and interest in and to the Software, including intellectual property rights.\n\n5. Updates and Support\n   Company may, in its sole discretion, provide updates, upgrades, or support services for the Software under a separate support agreement.\n\n6. Term and Termination\n   This Agreement remains in effect until terminated. Your rights under this Agreement terminate automatically if You fail to comply with any term. Upon termination, You must cease all use of the Software and destroy all copies in Your possession.\n\n7. Warranty Disclaimer\n   THE SOFTWARE IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY LAW, COMPANY DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.\n\n8. Limitation of Liability\n   IN NO EVENT WILL COMPANY BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES ARISING FROM OR RELATED TO THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. COMPANY’S AGGREGATE LIABILITY UNDER THIS AGREEMENT WILL NOT EXCEED THE LICENSE FEES PAID BY YOU.\n\n9. Governing Law and Dispute Resolution\n   This Agreement will be governed by the laws of the State of Delaware without regard to conflict of law principles. Any dispute will be resolved in the state or federal courts in the State of Delaware.\n\n10. Export Compliance\n    You agree to comply with all export laws and regulations of the United States and any other applicable jurisdiction.\n\n11. Entire Agreement\n    This Agreement constitutes the entire agreement between You and Company regarding the Software and supersedes all prior understandings.\n\nSPDX-License-Identifier: LicenseRef-Webstudio,Inc-Proprietary\n"
  },
  {
    "path": "packages/sdk-components-animation/README.md",
    "content": "# Radix Components for Webstudio\n\nRadix Primitives is a low-level UI component library with a focus on accessibility and customization.\nDefault styling is inspired by https://ui.shadcn.com/docs.\n"
  },
  {
    "path": "packages/sdk-components-animation/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk-components-animation\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio components for animation\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"LicenseRef-Webstudio,Inc-Proprietary\",\n  \"private\": false,\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"webstudio-private\": \"./private-src/components.ts\",\n      \"webstudio\": \"./src/components.ts\",\n      \"types\": \"./lib/types/components.d.ts\",\n      \"import\": \"./lib/components.js\"\n    },\n    \"./metas\": {\n      \"webstudio\": \"./src/metas.ts\",\n      \"types\": \"./lib/types/metas.d.ts\",\n      \"import\": \"./lib/metas.js\"\n    },\n    \"./hooks\": {\n      \"webstudio\": \"./src/hooks.ts\",\n      \"types\": \"./lib/types/hooks.d.ts\",\n      \"import\": \"./lib/hooks.js\"\n    },\n    \"./templates\": {\n      \"webstudio\": \"./src/templates.ts\",\n      \"types\": \"./lib/types/templates.d.ts\",\n      \"import\": \"./lib/templates.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"vite build && esbuild './lib/*' --outdir=./lib --minify --allow-overwrite --banner:js='//! SPDX-License-Identifier: LicenseRef-Webstudio,Inc-Proprietary'\",\n    \"build:args\": \"NODE_OPTIONS=--conditions=webstudio generate-arg-types './src/*.tsx !./src/*.stories.tsx !./src/*.ws.ts !./src/*.template.tsx !./src/*.test.{ts,tsx}' -e asChild -e modal -e defaultOpen -e defaultChecked && prettier --write \\\"**/*.props.ts\\\"\",\n    \"build:stories\": \"webstudio-sdk generate-stories && prettier --write \\\"src/__generated__/*.stories.tsx\\\"\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"typecheck\": \"tsgo --noEmit -p tsconfig.typecheck.json\",\n    \"test\": \"vitest run\",\n    \"playwright-init\": \"playwright install --with-deps\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/icons\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"change-case\": \"^5.4.4\",\n    \"nanostores\": \"^0.11.3\",\n    \"react-error-boundary\": \"^5.0.0\",\n    \"shallow-equal\": \"^3.1.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@vitest/browser\": \"^3.0.8\",\n    \"@webstudio-is/css-data\": \"workspace:*\",\n    \"@webstudio-is/design-system\": \"workspace:*\",\n    \"@webstudio-is/generate-arg-types\": \"workspace:*\",\n    \"@webstudio-is/sdk-cli\": \"workspace:^\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"fast-glob\": \"^3.3.2\",\n    \"playwright\": \"^1.50.1\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"type-fest\": \"^4.37.0\",\n    \"vitest\": \"^3.1.2\",\n    \"zod\": \"^3.24.2\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-animation/src/__generated__/animate-children.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  debug: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/__generated__/animate-text.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  className: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Classes to which the element belongs\",\n  },\n  easing: {\n    description: \"Easing function applied within the sliding window.\",\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"linear\",\n    options: [\n      \"linear\",\n      \"easeIn\",\n      \"easeInCubic\",\n      \"easeInQuart\",\n      \"easeOut\",\n      \"easeOutCubic\",\n      \"easeOutQuart\",\n      \"ease\",\n      \"easeInOutCubic\",\n      \"easeInOutQuart\",\n    ],\n  },\n  slidingWindow: {\n    description:\n      \"Size of the sliding window for the animation:\\n- 0: Typewriter effect (no animation).\\n- (0..1]: Animates one part of the text at a time.\\n- (1..n]: Animates multiple parts of the text within the sliding window.\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n    defaultValue: 5,\n  },\n  splitBy: {\n    description:\n      \"Defines how the text is split for animation (e.g., by character, space, or symbol).\",\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"char\",\n    options: [\"char\", \"space\", 'symbol \"#\"', 'symbol \"~\"'],\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/__generated__/stagger-animation.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  className: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Classes to which the element belongs\",\n  },\n  easing: {\n    description: \"Easing function applied within the sliding window.\",\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"linear\",\n    options: [\n      \"linear\",\n      \"easeIn\",\n      \"easeInCubic\",\n      \"easeInQuart\",\n      \"easeOut\",\n      \"easeOutCubic\",\n      \"easeOutQuart\",\n      \"ease\",\n      \"easeInOutCubic\",\n      \"easeInOutQuart\",\n    ],\n  },\n  slidingWindow: {\n    description:\n      \"Size of the sliding window for the animation:\\n- 0: Typewriter effect (no animation).\\n- (0..1]: Animates one child at a time.\\n- (1..n]: Animates multiple children within the sliding window.\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n    defaultValue: 1,\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/__generated__/video-animation.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  timeline: { required: false, control: \"boolean\", type: \"boolean\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/animate-children.tsx",
    "content": "import { forwardRef, type ElementRef } from \"react\";\nimport type { Hook } from \"@webstudio-is/react-sdk\";\nimport type { AnimationAction } from \"@webstudio-is/sdk\";\nimport { animationCanPlayOnCanvasProperty } from \"@webstudio-is/sdk/runtime\";\n\ntype ScrollProps = {\n  debug?: boolean;\n  children?: React.ReactNode;\n  action: AnimationAction;\n};\n\nexport const AnimateChildren = forwardRef<ElementRef<\"div\">, ScrollProps>(\n  ({ debug = false, action, ...props }, ref) => {\n    return <div ref={ref} style={{ display: \"contents\" }} {...props} />;\n  }\n);\n\nconst displayName = \"AnimateChildren\";\nAnimateChildren.displayName = displayName;\n\nconst namespace = \"@webstudio-is/sdk-components-animation\";\n\nexport const hooksAnimateChildren: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    if (\n      event.instancePath.length > 0 &&\n      event.instancePath[0].component === `${namespace}:${displayName}`\n    ) {\n      context.setMemoryProp(\n        event.instancePath[0],\n        animationCanPlayOnCanvasProperty,\n        undefined\n      );\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    if (\n      event.instancePath.length > 0 &&\n      event.instancePath[0].component === `${namespace}:${displayName}`\n    ) {\n      context.setMemoryProp(\n        event.instancePath[0],\n        animationCanPlayOnCanvasProperty,\n        true\n      );\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/animate-children.ws.ts",
    "content": "import { AnimationGroupIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { animation } from \"./shared/meta\";\n\nexport const meta: WsComponentMeta = {\n  category: \"animations\",\n  description: \"Animation Group component is designed to animate its children.\",\n  icon: AnimationGroupIcon,\n  order: 0,\n  label: \"Animation Group\",\n  contentModel: {\n    category: \"instance\",\n    children: [\n      \"instance\",\n      animation.AnimateText,\n      animation.StaggerAnimation,\n      animation.VideoAnimation,\n    ],\n  },\n  initialProps: [\"action\"],\n  props: {\n    action: {\n      required: false,\n      control: \"animationAction\",\n      type: \"animationAction\",\n      description: \"Animation Action\",\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/animate-text.tsx",
    "content": "import { forwardRef, type ElementRef } from \"react\";\n\nconst easings = {\n  linear: true,\n  easeIn: true,\n  easeInCubic: true,\n  easeInQuart: true,\n  easeOut: true,\n  easeOutCubic: true,\n  easeOutQuart: true,\n  ease: true,\n  easeInOutCubic: true,\n  easeInOutQuart: true,\n};\n\nconst split = {\n  char: true,\n  space: true,\n  'symbol \"#\"': true,\n  'symbol \"~\"': true,\n};\n\ntype AnimateChildrenProps = {\n  /**\n   * Size of the sliding window for the animation:\n   * - 0: Typewriter effect (no animation).\n   * - (0..1]: Animates one part of the text at a time.\n   * - (1..n]: Animates multiple parts of the text within the sliding window.\n   */\n  slidingWindow?: number;\n  /**\n   * Easing function applied within the sliding window.\n   */\n  easing?: keyof typeof easings;\n  /**\n   * Text content to animate.\n   */\n  children: React.ReactNode;\n  /**\n   * Defines how the text is split for animation (e.g., by character, space, or symbol).\n   */\n  splitBy?: keyof typeof split;\n} & {\n  className?: string;\n};\n\nexport const AnimateText = forwardRef<ElementRef<\"div\">, AnimateChildrenProps>(\n  (\n    { slidingWindow = 5, easing = \"linear\", splitBy = \"char\", ...props },\n    ref\n  ) => {\n    return <div ref={ref} {...props} />;\n  }\n);\n\nconst displayName = \"AnimateText\";\nAnimateText.displayName = displayName;\n"
  },
  {
    "path": "packages/sdk-components-animation/src/animate-text.ws.ts",
    "content": "import { TextAnimationIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/animate-text.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"animations\",\n  description:\n    \"Text animation allows you to split text by char or by word to animate it.\",\n  icon: TextAnimationIcon,\n  order: 1,\n  label: \"Text Animation\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { div },\n  initialProps: [\"slidingWindow\", \"easing\", \"splitBy\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/components.ts",
    "content": "export { AnimateChildren } from \"./animate-children\";\nexport { AnimateText } from \"./animate-text\";\nexport { StaggerAnimation } from \"./stagger-animation\";\nexport { VideoAnimation } from \"./video-animation\";\n"
  },
  {
    "path": "packages/sdk-components-animation/src/hooks.ts",
    "content": "import type { Hook } from \"@webstudio-is/react-sdk\";\nimport { hooksAnimateChildren } from \"./animate-children\";\n\nexport const hooks: Hook[] = [hooksAnimateChildren];\n"
  },
  {
    "path": "packages/sdk-components-animation/src/metas.ts",
    "content": "export { meta as AnimateChildren } from \"./animate-children.ws\";\nexport { meta as AnimateText } from \"./animate-text.ws\";\nexport { meta as StaggerAnimation } from \"./stagger-animation.ws\";\nexport { meta as VideoAnimation } from \"./video-animation.ws\";\n"
  },
  {
    "path": "packages/sdk-components-animation/src/shared/create-progress-animation.tsx",
    "content": "import { forwardRef, type ElementRef } from \"react\";\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\ntype ProgressAnimationProps<T extends Record<string, unknown> = {}> = {\n  children: React.ReactNode;\n} & T;\n\nexport const createProgressAnimation = <T extends Record<string, unknown>>() =>\n  forwardRef<ElementRef<\"div\">, ProgressAnimationProps<T>>((props, ref) => {\n    // Implementation is located in private-src\n    return <div ref={ref} {...props} />;\n  });\n"
  },
  {
    "path": "packages/sdk-components-animation/src/shared/meta.ts",
    "content": "const createMetaProxy = (prefix: string): Record<string, string> => {\n  return new Proxy(\n    {},\n    {\n      get(_target, prop) {\n        return `${prefix}${prop as string}`;\n      },\n    }\n  );\n};\n\nexport const animation = createMetaProxy(\n  \"@webstudio-is/sdk-components-animation:\"\n);\n"
  },
  {
    "path": "packages/sdk-components-animation/src/shared/proxy.ts",
    "content": "import { createProxy } from \"@webstudio-is/template\";\n\nexport const animation = createProxy(\"@webstudio-is/sdk-components-animation:\");\n"
  },
  {
    "path": "packages/sdk-components-animation/src/stagger-animation.tsx",
    "content": "import { forwardRef, type ElementRef } from \"react\";\n\nconst easings = {\n  linear: true,\n  easeIn: true,\n  easeInCubic: true,\n  easeInQuart: true,\n  easeOut: true,\n  easeOutCubic: true,\n  easeOutQuart: true,\n  ease: true,\n  easeInOutCubic: true,\n  easeInOutQuart: true,\n};\n\ntype StaggerAnimationProps = {\n  /**\n   * Size of the sliding window for the animation:\n   * - 0: Typewriter effect (no animation).\n   * - (0..1]: Animates one child at a time.\n   * - (1..n]: Animates multiple children within the sliding window.\n   */\n  slidingWindow?: number;\n  /**\n   * Easing function applied within the sliding window.\n   */\n  easing?: keyof typeof easings;\n  /**\n   * Text content to animate.\n   */\n  children: React.ReactNode;\n} & {\n  className?: string;\n};\n\nexport const StaggerAnimation = forwardRef<\n  ElementRef<\"div\">,\n  StaggerAnimationProps\n>(({ slidingWindow = 1, easing = \"linear\", ...props }, ref) => {\n  // Implementation is located in private-src\n  return <div ref={ref} {...props} />;\n});\n\nconst displayName = \"StaggerAnimation\";\nStaggerAnimation.displayName = displayName;\n"
  },
  {
    "path": "packages/sdk-components-animation/src/stagger-animation.ws.ts",
    "content": "import { StaggerAnimationIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/stagger-animation.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"animations\",\n  description:\n    \"Stagger animation allows you to animate children elements with a sliding window.\",\n  icon: StaggerAnimationIcon,\n  order: 4,\n  label: \"Stagger Animation\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { div },\n  initialProps: [\"slidingWindow\", \"easing\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/templates.ts",
    "content": "export { meta as VideoAnimation } from \"./video-animation.template\";\n"
  },
  {
    "path": "packages/sdk-components-animation/src/video-animation.template.tsx",
    "content": "import { $, type TemplateMeta } from \"@webstudio-is/template\";\nimport { animation } from \"./shared/proxy\";\n\nexport const meta: TemplateMeta = {\n  category: \"animations\",\n  description: \"Video Animation\",\n  order: 2,\n  template: (\n    <animation.VideoAnimation>\n      <$.Video\n        preload=\"auto\"\n        autoPlay={true}\n        muted={true}\n        playsInline={true}\n        crossOrigin=\"anonymous\"\n      ></$.Video>\n    </animation.VideoAnimation>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/src/video-animation.tsx",
    "content": "import { createProgressAnimation } from \"./shared/create-progress-animation\";\n\nexport const VideoAnimation = createProgressAnimation<{ timeline?: boolean }>();\nconst displayName = \"VideoAnimation\";\nVideoAnimation.displayName = displayName;\n"
  },
  {
    "path": "packages/sdk-components-animation/src/video-animation.ws.ts",
    "content": "import { PlayIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/video-animation.props\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\n\nexport const meta: WsComponentMeta = {\n  icon: PlayIcon,\n  label: \"Video Animation\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { div },\n  props,\n  initialProps: [\"timeline\"],\n};\n"
  },
  {
    "path": "packages/sdk-components-animation/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\n    \"src/private-src/components.ts\",\n    \"src/components.ts\",\n    \"src/metas.ts\",\n    \"src/hooks.ts\"\n  ],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-animation/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\n    \"src\",\n    \"private-src\",\n    \"../sdk/src/schema/animation-schema.ts\",\n    \"../../@types/**/*.d.ts\"\n  ],\n  \"exclude\": [\"private-src/perf/**/*\"],\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n\n    \"types\": [\"react/experimental\", \"react-dom/experimental\", \"@types/node\"]\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-animation/tsconfig.typecheck.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"declaration\": false,\n    \"emitDeclarationOnly\": false,\n    \"declarationDir\": null\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-animation/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\n\nconst hasPrivateFolders = existsSync(\n  path.join(process.cwd(), \"private-src\", \"README.md\")\n);\n\nconst isBareImport = (id: string) =>\n  id.startsWith(\"@\") || id.includes(\".\") === false;\n\nexport default defineConfig({\n  build: {\n    lib: {\n      entry: [\n        hasPrivateFolders ? \"private-src/components.ts\" : \"src/components.ts\",\n        \"src/metas.ts\",\n        \"src/hooks.ts\",\n        \"src/templates.ts\",\n      ],\n      formats: [\"es\"],\n    },\n    rollupOptions: {\n      external: isBareImport,\n      output: {\n        dir: \"lib\",\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/sdk-components-animation/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\nimport { existsSync, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\nimport fg from \"fast-glob\";\n\nconst rootDir = [\"..\", \"../..\", \"../../..\"]\n  .map((dir) => path.join(__dirname, dir))\n  .find((dir) => existsSync(path.join(dir, \".git\")));\n\nconst hasPrivateFolders =\n  fg.sync([path.join(rootDir ?? \"\", \"packages/*/private-src/*\")], {\n    ignore: [\"**/node_modules/**\"],\n  }).length > 0;\n\nconst conditions = hasPrivateFolders\n  ? [\"webstudio-private\", \"webstudio\"]\n  : [\"webstudio\"];\n\nexport default defineConfig({\n  resolve: {\n    conditions,\n  },\n  ssr: {\n    resolve: {\n      conditions,\n    },\n  },\n  test: {\n    passWithNoTests: true,\n    workspace: [\n      {\n        extends: \"./vitest.config.ts\",\n        test: {\n          include: [\"**/*.browser.{test,spec}.ts\"],\n          name: \"browser\",\n          browser: {\n            provider: \"playwright\", // or 'webdriverio'\n            enabled: true,\n            headless: true,\n            screenshotFailures: false,\n            instances: [{ browser: \"chromium\" }, { browser: \"firefox\" }],\n            fileParallelism: false,\n          },\n        },\n      },\n      {\n        extends: \"./vitest.config.ts\",\n        test: {\n          include: [\"!**/*.browser.{test,spec}.ts\", \"**/*.{test,spec}.ts\"],\n\n          name: \"unit\",\n          environment: \"node\",\n        },\n      },\n    ],\n  },\n  server: {\n    watch: {\n      ignored: [],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/sdk-components-react/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/sdk-components-react/README.md",
    "content": "# Webstudio SDK Components\n\nWebstudio SDK is a TypeScript API that lets you use your Webstudio project or some components in your custom codebase or just render a complete Remix Document.\n"
  },
  {
    "path": "packages/sdk-components-react/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk-components-react\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio default library for react\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/components.ts\",\n      \"types\": \"./lib/types/components.d.ts\",\n      \"import\": \"./lib/components.js\"\n    },\n    \"./metas\": {\n      \"webstudio\": \"./src/metas.ts\",\n      \"types\": \"./lib/types/metas.d.ts\",\n      \"import\": \"./lib/metas.js\"\n    },\n    \"./hooks\": {\n      \"webstudio\": \"./src/hooks.ts\",\n      \"types\": \"./lib/types/hooks.d.ts\",\n      \"import\": \"./lib/hooks.js\"\n    },\n    \"./templates\": {\n      \"webstudio\": \"./src/templates.ts\",\n      \"types\": \"./lib/types/templates.d.ts\",\n      \"import\": \"./lib/templates.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"vite build --config ../../vite.sdk-components.config.ts\",\n    \"build:args\": \"NODE_OPTIONS=--conditions=webstudio generate-arg-types './src/*.tsx !./src/*.stories.tsx !./src/*.test.{ts,tsx} !./src/*.ws.ts !./src/*.ws.ts !./src/*.template.tsx' && prettier --write \\\"**/*.props.ts\\\"\",\n    \"build:stories\": \"webstudio-sdk generate-stories && prettier --write \\\"src/__generated__/*.stories.tsx\\\"\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"test\": \"vitest run\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"dependencies\": {\n    \"@react-aria/utils\": \"^3.27.0\",\n    \"@webstudio-is/icons\": \"workspace:*\",\n    \"@webstudio-is/image\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"await-interaction-response\": \"^0.0.2\",\n    \"colord\": \"^2.9.3\",\n    \"micromark\": \"^4.0.2\",\n    \"micromark-extension-gfm-table\": \"^2.1.1\"\n  },\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^10.4.0\",\n    \"@testing-library/react\": \"^16.2.0\",\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/design-system\": \"workspace:*\",\n    \"@webstudio-is/generate-arg-types\": \"workspace:*\",\n    \"@webstudio-is/sdk-cli\": \"workspace:*\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"nanostores\": \"^0.11.3\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"vitest\": \"^3.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react/src/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/blockquote.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/blockquote.stories.tsx",
    "content": "import { Box as Box, Blockquote as Blockquote } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Blockquote className={\"w-blockquote\"} />\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Blockquote\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(blockquote.w-blockquote) {\n    margin-top: 0;\n    margin-right: 0;\n    margin-bottom: 10px;\n    margin-left: 0;\n    padding-top: 10px;\n    padding-bottom: 10px;\n    padding-left: 20px;\n    padding-right: 20px;\n    border-left: 5px solid rgba(226, 226, 226, 1)\n  }\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Blockquote };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/body.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/bold.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/box.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  tag: { required: false, control: \"text\", type: \"string\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/button.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/button.stories.tsx",
    "content": "import { Box as Box, Button as Button } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Button className={\"w-button\"}>{\"Button text you can edit\"}</Button>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Button\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Button };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/checkbox.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Value of the form control\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/checkbox.stories.tsx",
    "content": "import {\n  Box as Box,\n  Label as Label,\n  Checkbox as Checkbox,\n  Text as Text,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Label className={\"w-input-label\"}>\n        <Checkbox className={\"w-checkbox\"} />\n        <Text tag={\"span\"} className={\"w-text\"}>\n          {\"Checkbox\"}\n        </Text>\n      </Label>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Checkbox\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(input.w-checkbox) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    margin-top: 0;\n    margin-right: 0.5em;\n    margin-bottom: 0;\n    margin-left: 0;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: none;\n    border-right-style: none;\n    border-bottom-style: none;\n    border-left-style: none\n  }\n  :where(label.w-input-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    display: block\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Checkbox };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/code-text.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  code: { required: false, control: \"text\", type: \"string\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/content-embed.stories.tsx",
    "content": "import { Box as Box, HtmlEmbed as HtmlEmbed } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <HtmlEmbed\n        code={\n          '<h1>Styling HTML with Content Embed</h1>\\n<p>Content Embed allows styling of HTML, which primarily comes from external data.</p>\\n<h2>How to Use Content Embed</h2>\\n<ul>\\n  <li>Every element is shown in the Navigator.</li>\\n  <li>Apply styles and Tokens to each element.</li>\\n  <li>Adjustments to elements apply universally within this embed, ensuring consistency across your content.</li>\\n</ul>\\n<hr>\\n<h2>This sample text contains all the elements that can be styled.</h2>\\n<p>Any elements that were not used above are used below.</p>\\n<h3>Heading 3</h3>\\n<h4>Heading 4</h4>\\n<h5>Heading 5</h5>\\n<h6>Heading 6</h6>\\n<p><a href=\"#\">Links</a> connect your content to relevant resources.</p>\\n<p><strong>Bold text</strong> makes your important points stand out.</p>\\n<p><em>Italic text</em> is great for emphasizing terms.</p>\\n<ol>\\n  <li>First Step</li>\\n  <li>Second Step</li>\\n</ol>\\n<img src=\"data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIxNDAiCiAgaGVpZ2h0PSIxNDAiCiAgdmlld0JveD0iMCAwIDYwMCA2MDAiCiAgZmlsbD0ibm9uZSIKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgPgogIDxyZWN0IHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIiBmaWxsPSIjREZFM0U2IiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNNDUwIDE3MEgxNTBDMTQxLjcxNiAxNzAgMTM1IDE3Ni43MTYgMTM1IDE4NVY0MTVDMTM1IDQyMy4yODQgMTQxLjcxNiA0MzAgMTUwIDQzMEg0NTBDNDU4LjI4NCA0MzAgNDY1IDQyMy4yODQgNDY1IDQxNVYxODVDNDY1IDE3Ni43MTYgNDU4LjI4NCAxNzAgNDUwIDE3MFpNMTUwIDE0NUMxMjcuOTA5IDE0NSAxMTAgMTYyLjkwOSAxMTAgMTg1VjQxNUMxMTAgNDM3LjA5MSAxMjcuOTA5IDQ1NSAxNTAgNDU1SDQ1MEM0NzIuMDkxIDQ1NSA0OTAgNDM3LjA5MSA0OTAgNDE1VjE4NUM0OTAgMTYyLjkwOSA0NzIuMDkxIDE0NSA0NTAgMTQ1SDE1MFoiCiAgICBmaWxsPSIjQzFDOENEIgogIC8+CiAgPHBhdGgKICAgIGQ9Ik0yMzcuMTM1IDIzNS4wMTJDMjM3LjEzNSAyNTUuNzIzIDIyMC4zNDUgMjcyLjUxMiAxOTkuNjM1IDI3Mi41MTJDMTc4LjkyNCAyNzIuNTEyIDE2Mi4xMzUgMjU1LjcyMyAxNjIuMTM1IDIzNS4wMTJDMTYyLjEzNSAyMTQuMzAxIDE3OC45MjQgMTk3LjUxMiAxOTkuNjM1IDE5Ny41MTJDMjIwLjM0NSAxOTcuNTEyIDIzNy4xMzUgMjE0LjMwMSAyMzcuMTM1IDIzNS4wMTJaIgogICAgZmlsbD0iI0MxQzhDRCIKICAvPgogIDxwYXRoCiAgICBkPSJNMTYwIDQwNVYzNjcuMjA1TDIyMS42MDkgMzA2LjM2NEwyNTYuNTUyIDMzOC42MjhMMzU4LjE2MSAyMzRMNDQwIDMxNi4wNDNWNDA1SDE2MFoiCiAgICBmaWxsPSIjQzFDOENEIgogIC8+Cjwvc3ZnPg==\">\\n<blockquote>Capture attention with a powerful quote.</blockquote>\\n<p>Using <code>console.log(\"Hello World\");</code> will log to the console.</p>\\n<table>\\n  <tr>\\n    <th>Header 1</th>\\n    <th>Header 2</th>\\n    <th>Header 3</th>\\n  </tr>\\n  <tr>\\n    <td>Cell 1.1</td>\\n    <td>Cell 1.2</td>\\n    <td>Cell 1.3</td>\\n  </tr>\\n  <tr>\\n    <td>Cell 2.1</td>\\n    <td>Cell 2.2</td>\\n    <td>Cell 2.3</td>\\n  </tr>\\n  <tr>\\n    <td>Cell 3.1</td>\\n    <td>Cell 3.2</td>\\n    <td>Cell 3.3</td>\\n  </tr>\\n</table>'\n        }\n        className={\"w-html-embed\"}\n      ></HtmlEmbed>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Content Embed\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as ContentEmbed };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/form.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/form.stories.tsx",
    "content": "import { useVariableState } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Box as Box,\n  Form as Form,\n  Label as Label,\n  Input as Input,\n  Button as Button,\n} from \"../components\";\n\nconst Component = () => {\n  let [formState, set$formState] = useVariableState<any>(\"initial\");\n  return (\n    <Box className={\"w-box\"}>\n      <Form\n        state={formState}\n        onStateChange={(state: any) => {\n          formState = state;\n          set$formState(formState);\n        }}\n        className={\"w-webhook-form\"}\n      >\n        {(formState === \"initial\" || formState === \"error\") && (\n          <Box className={\"w-box\"}>\n            <Label className={\"w-input-label\"}>{\"Name\"}</Label>\n            <Input name={\"name\"} className={\"w-text-input\"} />\n            <Label className={\"w-input-label\"}>{\"Email\"}</Label>\n            <Input name={\"email\"} className={\"w-text-input\"} />\n            <Button className={\"w-button\"}>{\"Submit\"}</Button>\n          </Box>\n        )}\n        {formState === \"success\" && (\n          <Box className={\"w-box\"}>{\"Thank you for getting in touch!\"}</Box>\n        )}\n        {formState === \"error\" && (\n          <Box className={\"w-box\"}>{\"Sorry, something went wrong.\"}</Box>\n        )}\n      </Form>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Form\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(form.w-webhook-form) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(input.w-text-input) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    display: block;\n    margin: 0\n  }\n  :where(label.w-input-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    display: block\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Form };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/fragment.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/head-link.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/head-meta.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/head-slot.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/head-title.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/heading.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  tag: { required: false, control: \"text\", type: \"string\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/heading.stories.tsx",
    "content": "import { Box as Box, Heading as Heading } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Heading className={\"w-heading\"} />\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Heading\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h1.w-heading) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h2.w-heading) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h3.w-heading) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h4.w-heading) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h5.w-heading) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h6.w-heading) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Heading };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/html-embed.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  className: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"\",\n  },\n  clientOnly: { required: false, control: \"boolean\", type: \"boolean\" },\n  code: { required: true, control: \"text\", type: \"string\" },\n  executeScriptOnCanvas: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/image.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  optimize: {\n    description: \"Optimize the image for enhanced performance.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  quality: { required: false, control: \"number\", type: \"number\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/input.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/italic.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/label.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/label.stories.tsx",
    "content": "import { Box as Box, Label as Label } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Label className={\"w-input-label\"}>{\"Form Label\"}</Label>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Label\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(label.w-input-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    display: block\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Label };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/link.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  download: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description:\n      \"Whether to download the resource instead of navigating to it, and its filename if so\",\n  },\n  prefetch: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"none\", \"intent\", \"render\", \"viewport\"],\n  },\n  preventScrollReset: { required: false, control: \"boolean\", type: \"boolean\" },\n  reloadDocument: { required: false, control: \"boolean\", type: \"boolean\" },\n  replace: { required: false, control: \"boolean\", type: \"boolean\" },\n  target: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"_self\", \"_blank\", \"_parent\", \"_top\"],\n    description: \"Navigable for form submission\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/link.stories.tsx",
    "content": "import { Box as Box, Link as Link } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Link className={\"w-link\"} />\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Link\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(a.w-link) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    display: inline-block\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Link };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/list-item.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/list-item.stories.tsx",
    "content": "import { Box as Box, ListItem as ListItem } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <ListItem className={\"w-list-item\"} />\n    </Box>\n  );\n};\n\nexport default {\n  title: \"List Item\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(li.w-list-item) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as ListItem };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/list.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  ordered: {\n    description:\n      \"Shows numbers instead of bullets when toggled. See the “List Style Type” property under the “List Item” section in the Style panel for more options.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/list.stories.tsx",
    "content": "import { Box as Box, List as List, ListItem as ListItem } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <List className={\"w-list\"}>\n        <ListItem className={\"w-list-item\"}>\n          {\"List Item text you can edit\"}\n        </ListItem>\n        <ListItem className={\"w-list-item\"}>\n          {\"List Item text you can edit\"}\n        </ListItem>\n        <ListItem className={\"w-list-item\"}>\n          {\"List Item text you can edit\"}\n        </ListItem>\n      </List>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"List\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(ol.w-list) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    margin-top: 0;\n    margin-bottom: 10px;\n    padding-left: 40px\n  }\n  :where(ul.w-list) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    margin-top: 0;\n    margin-bottom: 10px;\n    padding-left: 40px\n  }\n  :where(li.w-list-item) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as List };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/markdown-embed.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  code: { required: true, control: \"text\", type: \"string\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/markdown-embed.stories.tsx",
    "content": "import { Box as Box, MarkdownEmbed as MarkdownEmbed } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <MarkdownEmbed\n        code={\n          '# Styling Markdown with Markdown Embed\\n\\nMarkdown Embed allows styling of Markdown, which primarily comes from external data.\\n\\n## How to Use Markdown Embed\\n\\n- Every element is shown in the Navigator.\\n- Apply styles and Tokens to each element.\\n- Adjustments to elements apply universally within this embed, ensuring consistency across your content.\\n\\n---\\n\\n## This sample text contains all the elements that can be styled.\\n\\nAny elements that were not used above are used below.\\n\\n### Heading 3\\n#### Heading 4\\n##### Heading 5\\n###### Heading 6\\n\\n[Links](#) connect your content to relevant resources.\\n\\n**Bold text** makes your important points stand out.\\n\\n*Italic text* is great for emphasizing terms.\\n\\n1. First Step\\n2. Second Step\\n\\n![Image placeholder](data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIxNDAiCiAgaGVpZ2h0PSIxNDAiCiAgdmlld0JveD0iMCAwIDYwMCA2MDAiCiAgZmlsbD0ibm9uZSIKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgPgogIDxyZWN0IHdpZHRoPSI2MDAiIGhlaWdodD0iNjAwIiBmaWxsPSIjREZFM0U2IiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNNDUwIDE3MEgxNTBDMTQxLjcxNiAxNzAgMTM1IDE3Ni43MTYgMTM1IDE4NVY0MTVDMTM1IDQyMy4yODQgMTQxLjcxNiA0MzAgMTUwIDQzMEg0NTBDNDU4LjI4NCA0MzAgNDY1IDQyMy4yODQgNDY1IDQxNVYxODVDNDY1IDE3Ni43MTYgNDU4LjI4NCAxNzAgNDUwIDE3MFpNMTUwIDE0NUMxMjcuOTA5IDE0NSAxMTAgMTYyLjkwOSAxMTAgMTg1VjQxNUMxMTAgNDM3LjA5MSAxMjcuOTA5IDQ1NSAxNTAgNDU1SDQ1MEM0NzIuMDkxIDQ1NSA0OTAgNDM3LjA5MSA0OTAgNDE1VjE4NUM0OTAgMTYyLjkwOSA0NzIuMDkxIDE0NSA0NTAgMTQ1SDE1MFoiCiAgICBmaWxsPSIjQzFDOENEIgogIC8+CiAgPHBhdGgKICAgIGQ9Ik0yMzcuMTM1IDIzNS4wMTJDMjM3LjEzNSAyNTUuNzIzIDIyMC4zNDUgMjcyLjUxMiAxOTkuNjM1IDI3Mi41MTJDMTc4LjkyNCAyNzIuNTEyIDE2Mi4xMzUgMjU1LjcyMyAxNjIuMTM1IDIzNS4wMTJDMTYyLjEzNSAyMTQuMzAxIDE3OC45MjQgMTk3LjUxMiAxOTkuNjM1IDE5Ny41MTJDMjIwLjM0NSAxOTcuNTEyIDIzNy4xMzUgMjE0LjMwMSAyMzcuMTM1IDIzNS4wMTJaIgogICAgZmlsbD0iI0MxQzhDRCIKICAvPgogIDxwYXRoCiAgICBkPSJNMTYwIDQwNVYzNjcuMjA1TDIyMS42MDkgMzA2LjM2NEwyNTYuNTUyIDMzOC42MjhMMzU4LjE2MSAyMzRMNDQwIDMxNi4wNDNWNDA1SDE2MFoiCiAgICBmaWxsPSIjQzFDOENEIgogIC8+Cjwvc3ZnPg==)\\n\\n> Capture attention with a powerful quote.\\n\\nUsing `console.log(\"Hello World\");` will log to the console.\\n\\n| Header 1   | Header 2   | Header 3   |\\n|------------|------------|------------|\\n| Cell 1.1   | Cell 1.2   | Cell 1.3   |\\n| Cell 2.1   | Cell 2.2   | Cell 2.3   |\\n| Cell 3.1   | Cell 3.2   | Cell 3.3   |'\n        }\n        className={\"w-markdown-embed\"}\n      ></MarkdownEmbed>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Markdown Embed\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-markdown-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as MarkdownEmbed };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/option.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/paragraph.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/paragraph.stories.tsx",
    "content": "import { Box as Box, Paragraph as Paragraph } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Paragraph className={\"w-paragraph\"} />\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Paragraph\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(p.w-paragraph) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Paragraph };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/radio-button.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Value of the form control\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/radio-button.stories.tsx",
    "content": "import {\n  Box as Box,\n  Label as Label,\n  RadioButton as RadioButton,\n  Text as Text,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Label className={\"w-input-label\"}>\n        <RadioButton className={\"w-radio\"} />\n        <Text tag={\"span\"} className={\"w-text\"}>\n          {\"Radio\"}\n        </Text>\n      </Label>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Radio Button\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(label.w-input-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    display: block\n  }\n  :where(input.w-radio) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    margin-top: 0;\n    margin-right: 0.5em;\n    margin-bottom: 0;\n    margin-left: 0;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: none;\n    border-right-style: none;\n    border-bottom-style: none;\n    border-left-style: none\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as RadioButton };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/rich-text-link.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  download: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description:\n      \"Whether to download the resource instead of navigating to it, and its filename if so\",\n  },\n  prefetch: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"none\", \"intent\", \"render\", \"viewport\"],\n  },\n  preventScrollReset: { required: false, control: \"boolean\", type: \"boolean\" },\n  reloadDocument: { required: false, control: \"boolean\", type: \"boolean\" },\n  replace: { required: false, control: \"boolean\", type: \"boolean\" },\n  target: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"_self\", \"_blank\", \"_parent\", \"_top\"],\n    description: \"Navigable for form submission\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/select.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/select.stories.tsx",
    "content": "import { Box as Box, Select as Select, Option as Option } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Select className={\"w-select\"}>\n        <Option\n          label={\"Please choose an option\"}\n          value={\"\"}\n          className={\"w-option\"}\n        />\n        <Option label={\"Option A\"} value={\"a\"} className={\"w-option\"} />\n        <Option label={\"Option B\"} value={\"b\"} className={\"w-option\"} />\n        <Option label={\"Option C\"} value={\"c\"} className={\"w-option\"} />\n      </Select>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Select\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(option.w-option):checked {\n    background-color: rgba(209, 209, 209, 1)\n  }\n  :where(select.w-select) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    display: block;\n    margin: 0\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Select };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/separator.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/slot.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/span.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/subscript.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/superscript.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/text.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  tag: { required: false, control: \"text\", type: \"string\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/text.stories.tsx",
    "content": "import { Box as Box, Text as Text } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Text className={\"w-text\"}>{\"The text you can edit\"}</Text>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Text\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n}\n\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Text };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/textarea.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/time.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  country: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"GB\",\n    options: [\n      \"AF\",\n      \"AL\",\n      \"DZ\",\n      \"AS\",\n      \"AD\",\n      \"AO\",\n      \"AI\",\n      \"AQ\",\n      \"AG\",\n      \"AR\",\n      \"AM\",\n      \"AW\",\n      \"AU\",\n      \"AT\",\n      \"AZ\",\n      \"BS\",\n      \"BH\",\n      \"BD\",\n      \"BB\",\n      \"BY\",\n      \"BE\",\n      \"BZ\",\n      \"BJ\",\n      \"BM\",\n      \"BT\",\n      \"BO\",\n      \"BA\",\n      \"BW\",\n      \"BR\",\n      \"BN\",\n      \"BG\",\n      \"BF\",\n      \"BI\",\n      \"CV\",\n      \"KH\",\n      \"CM\",\n      \"CA\",\n      \"KY\",\n      \"CF\",\n      \"TD\",\n      \"CL\",\n      \"CN\",\n      \"CO\",\n      \"KM\",\n      \"CG\",\n      \"CD\",\n      \"CR\",\n      \"HR\",\n      \"CU\",\n      \"CY\",\n      \"CZ\",\n      \"DK\",\n      \"DJ\",\n      \"DM\",\n      \"DO\",\n      \"EC\",\n      \"EG\",\n      \"SV\",\n      \"GQ\",\n      \"ER\",\n      \"EE\",\n      \"SZ\",\n      \"ET\",\n      \"FJ\",\n      \"FI\",\n      \"FR\",\n      \"GA\",\n      \"GM\",\n      \"GE\",\n      \"DE\",\n      \"GH\",\n      \"GR\",\n      \"GD\",\n      \"GT\",\n      \"GN\",\n      \"GW\",\n      \"GY\",\n      \"HT\",\n      \"HN\",\n      \"HU\",\n      \"IS\",\n      \"IN\",\n      \"ID\",\n      \"IR\",\n      \"IQ\",\n      \"IE\",\n      \"IL\",\n      \"IT\",\n      \"JM\",\n      \"JP\",\n      \"JO\",\n      \"KZ\",\n      \"KE\",\n      \"KI\",\n      \"KP\",\n      \"KR\",\n      \"KW\",\n      \"KG\",\n      \"LA\",\n      \"LV\",\n      \"LB\",\n      \"LS\",\n      \"LR\",\n      \"LY\",\n      \"LI\",\n      \"LT\",\n      \"LU\",\n      \"MG\",\n      \"MW\",\n      \"MY\",\n      \"MV\",\n      \"ML\",\n      \"MT\",\n      \"MH\",\n      \"MR\",\n      \"MU\",\n      \"MX\",\n      \"FM\",\n      \"MD\",\n      \"MC\",\n      \"MN\",\n      \"ME\",\n      \"MA\",\n      \"MZ\",\n      \"MM\",\n      \"NA\",\n      \"NR\",\n      \"NP\",\n      \"NL\",\n      \"NZ\",\n      \"NI\",\n      \"NE\",\n      \"NG\",\n      \"NO\",\n      \"OM\",\n      \"PK\",\n      \"PW\",\n      \"PA\",\n      \"PG\",\n      \"PY\",\n      \"PE\",\n      \"PH\",\n      \"PL\",\n      \"PT\",\n      \"QA\",\n      \"RO\",\n      \"RU\",\n      \"RW\",\n      \"KN\",\n      \"LC\",\n      \"VC\",\n      \"WS\",\n      \"SM\",\n      \"ST\",\n      \"SA\",\n      \"SN\",\n      \"RS\",\n      \"SC\",\n      \"SL\",\n      \"SG\",\n      \"SK\",\n      \"SI\",\n      \"SB\",\n      \"SO\",\n      \"ZA\",\n      \"SS\",\n      \"ES\",\n      \"LK\",\n      \"SD\",\n      \"SR\",\n      \"SE\",\n      \"CH\",\n      \"SY\",\n      \"TW\",\n      \"TJ\",\n      \"TZ\",\n      \"TH\",\n      \"TL\",\n      \"TG\",\n      \"TO\",\n      \"TT\",\n      \"TN\",\n      \"TR\",\n      \"TM\",\n      \"TV\",\n      \"UG\",\n      \"UA\",\n      \"AE\",\n      \"GB\",\n      \"US\",\n      \"UY\",\n      \"UZ\",\n      \"VU\",\n      \"VA\",\n      \"VE\",\n      \"VN\",\n      \"YE\",\n      \"ZM\",\n      \"ZW\",\n    ],\n  },\n  dateStyle: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"medium\",\n    options: [\"full\", \"long\", \"medium\", \"short\", \"none\"],\n  },\n  format: {\n    description:\n      'Custom format template. Overrides Date Style and Time Style.\\n\\nTokens: YYYY/YY (year), MMMM/MMM/MM/M (month), DDDD/DDD/DD/D (day), HH/H (hours), mm/m (minutes), ss/s (seconds)\\n\\nExamples:\\n\"YYYY-MM-DD\" → 2025-11-03\\n\"DDDD, MMMM D\" → Monday, November 3\\n\"DDD, D. MMM YYYY\" → Mon, 3. Nov 2025\\n\\nDay and month names use the selected language.',\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  language: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"en\",\n    options: [\n      \"hr\",\n      \"th\",\n      \"tr\",\n      \"id\",\n      \"is\",\n      \"cy\",\n      \"fr\",\n      \"af\",\n      \"am\",\n      \"ar\",\n      \"az\",\n      \"be\",\n      \"bg\",\n      \"bn\",\n      \"bs\",\n      \"ca\",\n      \"cs\",\n      \"da\",\n      \"de\",\n      \"el\",\n      \"en\",\n      \"es\",\n      \"et\",\n      \"eu\",\n      \"fa\",\n      \"fi\",\n      \"ga\",\n      \"gl\",\n      \"gu\",\n      \"he\",\n      \"hi\",\n      \"hu\",\n      \"hy\",\n      \"it\",\n      \"ja\",\n      \"ka\",\n      \"kk\",\n      \"km\",\n      \"kn\",\n      \"ko\",\n      \"ky\",\n      \"lb\",\n      \"lt\",\n      \"lv\",\n      \"mk\",\n      \"ml\",\n      \"mn\",\n      \"mr\",\n      \"ms\",\n      \"mt\",\n      \"nb\",\n      \"nl\",\n      \"nn\",\n      \"pl\",\n      \"pt\",\n      \"ro\",\n      \"ru\",\n      \"si\",\n      \"sk\",\n      \"sl\",\n      \"sq\",\n      \"sr\",\n      \"sv\",\n      \"sw\",\n      \"ta\",\n      \"te\",\n      \"uk\",\n      \"ur\",\n      \"uz\",\n      \"vi\",\n      \"zh\",\n    ],\n  },\n  timeStyle: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"none\",\n    options: [\"full\", \"long\", \"medium\", \"short\", \"none\"],\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/video.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/vimeo-play-button.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/vimeo-preview-image.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  optimize: {\n    description: \"Optimize the image for enhanced performance.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n  quality: { required: false, control: \"number\", type: \"number\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/vimeo-spinner.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/vimeo.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  autopause: {\n    description:\n      \"Whether to pause the current video when another Vimeo video on the same page starts to play. Set this value to false to permit simultaneous playback of all the videos on the page. This option has no effect if you've disabled cookies in your browser, either through browser settings or with an extension or plugin.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  autopip: {\n    description:\n      \"Whether to enable the browser to enter picture-in-picture mode automatically when switching tabs or windows, where supported.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n  autoplay: {\n    description:\n      \"Whether to start playback of the video automatically. This feature might not work on all devices.\\nSome browsers require the `muted` parameter to be set to `true` for autoplay to work.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  backgroundMode: {\n    description:\n      \"Whether the player is in background mode, which hides the playback controls, enables autoplay, and loops the video.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n  controlsColor: {\n    description:\n      \"A color value of the playback controls, which is normally #00ADEF. The embed settings of the video might override this value.\",\n    required: false,\n    control: \"color\",\n    type: \"string\",\n  },\n  doNotTrack: {\n    description:\n      \"Whether to prevent the player from tracking session data, including cookies. Keep in mind that setting this argument to true also blocks video stats.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  interactiveParams: {\n    description:\n      \"Key-value pairs representing dynamic parameters that are utilized on interactive videos with live elements, such as title=my-video,subtitle=interactive.\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  keyboard: {\n    description:\n      \"Whether to enable keyboard input to trigger player events. This setting doesn't affect tab control.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  loading: {\n    description:\n      \"Not a Vimeo attribute: Loading attribute for the iframe allows to eager or lazy load the source\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    defaultValue: \"lazy\",\n    options: [\"eager\", \"lazy\"],\n  },\n  loop: {\n    description:\n      \"Whether to restart the video automatically after reaching the end.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  muted: {\n    description:\n      \"Whether the video is muted upon loading. The true value is required for the autoplay behavior in some browsers.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  pip: {\n    description:\n      \"Whether to include the picture-in-picture button among the player controls and enable the picture-in-picture API.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  playsinline: {\n    description:\n      \"Whether the video plays inline on supported mobile devices. To force the device to play the video in fullscreen mode instead, set this value to false.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  quality: {\n    description:\n      \"For videos on a Vimeo Plus account or higher: the playback quality of the video. Use auto for the best possible quality given available bandwidth and other factors. You can also specify 360p, 540p, 720p, 1080p, 2k, and 4k.\",\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    defaultValue: \"auto\",\n    options: [\"auto\", \"360p\", \"540p\", \"720p\", \"1080p\", \"2k\", \"4k\"],\n  },\n  responsive: {\n    description:\n      \"Whether to return a responsive embed code, or one that provides intelligent adjustments based on viewing conditions. We recommend this option for mobile-optimized sites.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  showByline: {\n    description: \"Whether to display the video owner's name.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  showControls: {\n    description:\n      \"Whether to display the player's interactive elements, including the play bar and sharing buttons. Set this option to false for a chromeless experience. To control playback when the play/pause button is hidden, set autoplay to true, use keyboard controls (which remain active), or implement our player SDK.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  showPortrait: {\n    description:\n      \"Whether to display the video owner's portrait. Only works if either title or byline are also enabled\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  showPreview: {\n    description:\n      \"Not a Vimeo attribute: Whether the preview image should be loaded from Vimeo API. Ideally don't use it, because it will show up with some delay and will make your project feel slower.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  showTitle: {\n    description: \"Whether the player displays the title overlay.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  speed: {\n    description:\n      \"Whether the player displays speed controls in the preferences menu and enables the playback rate API.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  texttrack: {\n    description:\n      \"The text track to display with the video. Specify the text track by its language code (en), the language code and locale (en-US), or the language code and kind (en.captions). For this argument to work, the video must already have a text track of the given type; see our Help Center or Working with Text Track Uploads for more information.\\nTo enable automatically generated closed captions instead, provide the value en-x-autogen. Please note that, at the present time, automatic captions are always in English.\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  transparent: {\n    description:\n      \"Whether the responsive player and transparent background are enabled.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  url: {\n    description:\n      \"The ID or the URL of the video on Vimeo. You must supply one of these values to identify the video. When the video's privacy setting is Private, you must use the URL, and the URL must include the h parameter. For more information, see Vimeo’s introductory guide.\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/vimeo.stories.tsx",
    "content": "import {\n  Box as Box,\n  Vimeo as Vimeo,\n  VimeoPreviewImage as VimeoPreviewImage,\n  VimeoSpinner as VimeoSpinner,\n  HtmlEmbed as HtmlEmbed,\n  VimeoPlayButton as VimeoPlayButton,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Vimeo className={\"w-vimeo w-vimeo-1\"}>\n        <VimeoPreviewImage\n          alt={\"Vimeo video preview image\"}\n          sizes={\"100vw\"}\n          optimize={true}\n          className={\"w-preview-image w-preview-image-1\"}\n        />\n        <VimeoSpinner className={\"w-spinner w-spinner-1\"}>\n          <HtmlEmbed\n            code={\n              '<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"e2CRglijn891\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" viewBox=\"0 0 128 128\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><style>@keyframes e2CRglijn892_tr__tr{0%{transform:translate(64px,64px) rotate(90deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}50%{transform:translate(64px,64px) rotate(810deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}to{transform:translate(64px,64px) rotate(1530deg)}}@keyframes e2CRglijn892_s_p{0%,to{stroke:#39fbbb}25%{stroke:#4a4efa}50%{stroke:#e63cfe}75%{stroke:#ffae3c}}@keyframes e2CRglijn892_s_do{0%{stroke-dashoffset:251.89}2.5%,52.5%{stroke-dashoffset:263.88;animation-timing-function:cubic-bezier(.42,0,.58,1)}25%,75%{stroke-dashoffset:131.945}to{stroke-dashoffset:251.885909}}#e2CRglijn892_tr{animation:e2CRglijn892_tr__tr 3000ms linear infinite normal forwards}#e2CRglijn892{animation-name:e2CRglijn892_s_p,e2CRglijn892_s_do;animation-duration:3000ms;animation-fill-mode:forwards;animation-timing-function:linear;animation-direction:normal;animation-iteration-count:infinite}</style><g id=\"e2CRglijn892_tr\" transform=\"translate(64,64) rotate(90)\"><circle id=\"e2CRglijn892\" r=\"42\" fill=\"none\" stroke=\"#39fbbb\" stroke-dasharray=\"263.89\" stroke-dashoffset=\"251.89\" stroke-linecap=\"round\" stroke-width=\"16\" transform=\"scale(-1,1) translate(0,0)\"/></g></svg>'\n            }\n            className={\"w-html-embed\"}\n          />\n        </VimeoSpinner>\n        <VimeoPlayButton\n          aria-label={\"Play button\"}\n          className={\"w-play-button w-play-button-1\"}\n        >\n          <Box aria-hidden={true} className={\"w-box w-play-icon\"}>\n            <HtmlEmbed\n              code={\n                '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 2 9.333 6L4 14V2Z\"/></svg>'\n              }\n              className={\"w-html-embed\"}\n            />\n          </Box>\n        </VimeoPlayButton>\n      </Vimeo>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Vimeo\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-vimeo) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-play-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(img.w-preview-image) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    max-width: 100%;\n    display: block;\n    height: auto\n  }\n  :where(div.w-spinner) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-vimeo-1 {\n    position: relative;\n    aspect-ratio: 640/360;\n    width: 100%\n  }\n  .w-preview-image-1 {\n    position: absolute;\n    object-fit: cover;\n    object-position: cover;\n    width: 100%;\n    height: 100%;\n    border-top-left-radius: 20px;\n    border-top-right-radius: 20px;\n    border-bottom-right-radius: 20px;\n    border-bottom-left-radius: 20px\n  }\n  .w-spinner-1 {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 70px;\n    height: 70px;\n    margin-top: -35px;\n    margin-left: -35px\n  }\n  .w-play-button-1 {\n    position: absolute;\n    width: 140px;\n    height: 80px;\n    top: 50%;\n    left: 50%;\n    margin-top: -40px;\n    margin-left: -70px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border-top-style: none;\n    border-right-style: none;\n    border-bottom-style: none;\n    border-left-style: none;\n    border-top-left-radius: 5px;\n    border-top-right-radius: 5px;\n    border-bottom-right-radius: 5px;\n    border-bottom-left-radius: 5px;\n    cursor: pointer;\n    background-color: rgba(18, 18, 18, 1);\n    color: rgba(255, 255, 255, 1)\n  }\n  .w-play-button-1:hover {\n    background-color: rgba(0, 173, 239, 1)\n  }\n  .w-play-icon {\n    width: 60px;\n    height: 60px\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Vimeo };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/webhook-form.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  state: {\n    description:\n      \"Use this property to reveal the Success and Error states on the canvas so they can be styled. The Initial state is displayed when the page first opens. The Success and Error states are displayed depending on whether the Form submits successfully or unsuccessfully.\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    defaultValue: \"initial\",\n    options: [\"initial\", \"success\", \"error\"],\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/xml-node.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  href: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Address of the hyperlink\",\n  },\n  hreflang: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Language of the linked resource\",\n  },\n  rel: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description:\n      \"Relationship between the location in the document containing the hyperlink and the destination resource\",\n  },\n  tag: { required: false, control: \"text\", type: \"string\", defaultValue: \"\" },\n  xmlns: { required: false, control: \"text\", type: \"string\" },\n  \"xmlns:xhtml\": { required: false, control: \"text\", type: \"string\" },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/xml-time.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  dateStyle: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    defaultValue: \"short\",\n    options: [\"long\", \"short\"],\n  },\n  datetime: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    defaultValue: \"dateTime attribute is not set\",\n    description: \"Machine-readable value\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/you-tube.stories.tsx",
    "content": "import {\n  Box as Box,\n  YouTube as YouTube,\n  VimeoPreviewImage as VimeoPreviewImage,\n  VimeoSpinner as VimeoSpinner,\n  HtmlEmbed as HtmlEmbed,\n  VimeoPlayButton as VimeoPlayButton,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <YouTube className={\"w-you-tube w-you-tube-1\"}>\n        <VimeoPreviewImage\n          alt={\"Vimeo video preview image\"}\n          sizes={\"100vw\"}\n          optimize={true}\n          className={\"w-preview-image w-preview-image-1\"}\n        />\n        <VimeoSpinner className={\"w-spinner w-spinner-1\"}>\n          <HtmlEmbed\n            code={\n              '<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"e2CRglijn891\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" viewBox=\"0 0 128 128\" fill=\"currentColor\" width=\"100%\" height=\"100%\" style=\"display: block;\"><style>@keyframes e2CRglijn892_tr__tr{0%{transform:translate(64px,64px) rotate(90deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}50%{transform:translate(64px,64px) rotate(810deg);animation-timing-function:cubic-bezier(.42,0,.58,1)}to{transform:translate(64px,64px) rotate(1530deg)}}@keyframes e2CRglijn892_s_p{0%,to{stroke:#39fbbb}25%{stroke:#4a4efa}50%{stroke:#e63cfe}75%{stroke:#ffae3c}}@keyframes e2CRglijn892_s_do{0%{stroke-dashoffset:251.89}2.5%,52.5%{stroke-dashoffset:263.88;animation-timing-function:cubic-bezier(.42,0,.58,1)}25%,75%{stroke-dashoffset:131.945}to{stroke-dashoffset:251.885909}}#e2CRglijn892_tr{animation:e2CRglijn892_tr__tr 3000ms linear infinite normal forwards}#e2CRglijn892{animation-name:e2CRglijn892_s_p,e2CRglijn892_s_do;animation-duration:3000ms;animation-fill-mode:forwards;animation-timing-function:linear;animation-direction:normal;animation-iteration-count:infinite}</style><g id=\"e2CRglijn892_tr\" transform=\"translate(64,64) rotate(90)\"><circle id=\"e2CRglijn892\" r=\"42\" fill=\"none\" stroke=\"#39fbbb\" stroke-dasharray=\"263.89\" stroke-dashoffset=\"251.89\" stroke-linecap=\"round\" stroke-width=\"16\" transform=\"scale(-1,1) translate(0,0)\"/></g></svg>'\n            }\n            className={\"w-html-embed\"}\n          />\n        </VimeoSpinner>\n        <VimeoPlayButton\n          aria-label={\"Play button\"}\n          className={\"w-play-button w-play-button-1\"}\n        >\n          <Box aria-hidden={true} className={\"w-box w-play-icon\"}>\n            <HtmlEmbed\n              code={\n                '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 2 9.333 6L4 14V2Z\"/></svg>'\n              }\n              className={\"w-html-embed\"}\n            />\n          </Box>\n        </VimeoPlayButton>\n      </YouTube>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"YouTube\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(button.w-play-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(img.w-preview-image) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    max-width: 100%;\n    display: block;\n    height: auto\n  }\n  :where(div.w-spinner) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-you-tube) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-you-tube-1 {\n    position: relative;\n    aspect-ratio: 640/360;\n    width: 100%\n  }\n  .w-preview-image-1 {\n    position: absolute;\n    object-fit: cover;\n    object-position: cover;\n    width: 100%;\n    height: 100%;\n    border-top-left-radius: 20px;\n    border-top-right-radius: 20px;\n    border-bottom-right-radius: 20px;\n    border-bottom-left-radius: 20px\n  }\n  .w-spinner-1 {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    width: 70px;\n    height: 70px;\n    margin-top: -35px;\n    margin-left: -35px\n  }\n  .w-play-button-1 {\n    position: absolute;\n    width: 140px;\n    height: 80px;\n    top: 50%;\n    left: 50%;\n    margin-top: -40px;\n    margin-left: -70px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border-top-style: none;\n    border-right-style: none;\n    border-bottom-style: none;\n    border-left-style: none;\n    border-top-left-radius: 5px;\n    border-top-right-radius: 5px;\n    border-bottom-right-radius: 5px;\n    border-bottom-left-radius: 5px;\n    cursor: pointer;\n    background-color: rgba(18, 18, 18, 1);\n    color: rgba(255, 255, 255, 1)\n  }\n  .w-play-button-1:hover {\n    background-color: rgba(0, 173, 239, 1)\n  }\n  .w-play-icon {\n    width: 60px;\n    height: 60px\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as YouTube };\n"
  },
  {
    "path": "packages/sdk-components-react/src/__generated__/youtube.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {\n  allowFullscreen: {\n    description: \"Whether to allow fullscreen mode.\\nOriginal parameter: `fs`\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  autoplay: {\n    description:\n      \"Whether the video should autoplay.\\nSome browsers require the `muted` parameter to be set to `true` for autoplay to work.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  captionLanguage: {\n    description:\n      \"Specifies the default language that the player will use to display captions.\\nThe value is an ISO 639-1 two-letter language code.\\nOriginal parameter: `cc_lang_pref`\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  color: {\n    description:\n      \"Specifies the color that will be used in the player's video progress bar to highlight the amount of the video that the viewer has already seen.\\nValid values are 'red' and 'white'.\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"red\", \"white\"],\n  },\n  disableKeyboard: {\n    description:\n      \"Whether to disable keyboard controls.\\nOriginal parameter: `disablekb`\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  endTime: {\n    description: \"End time of the video in seconds.\\nOriginal parameter: `end`\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n  },\n  inline: {\n    description: \"Whether to play inline on mobile (not fullscreen).\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  keyboard: {\n    description: \"Whether to enable keyboard controls.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  language: {\n    description:\n      \"Sets the player's interface language. The value is an ISO 639-1 two-letter language code or a fully specified locale.\\nOriginal parameter: `hl`\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  listId: {\n    description: \"ID of the playlist to load.\\nOriginal parameter: `list`\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  listType: {\n    description: \"Type of playlist to load.\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"playlist\", \"user_uploads\"],\n  },\n  loading: {\n    description: \"Loading strategy for iframe\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    defaultValue: \"lazy\",\n    options: [\"eager\", \"lazy\"],\n  },\n  loop: {\n    description: \"Whether the video should loop continuously.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  muted: {\n    description:\n      \"Whether the video should start muted.\\nUseful for enabling autoplay in browsers that require videos to be muted.\\nOriginal parameter: `mute`\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  origin: {\n    description:\n      \"Your domain for API compliance (e.g., `https://yourdomain.com`).\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  playlist: {\n    description:\n      \"This parameter specifies a comma-separated list of video IDs to play\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  privacyEnhancedMode: {\n    description:\n      \"The Privacy Enhanced Mode of the YouTube embedded player prevents the use of views of embedded YouTube content from influencing the viewer’s browsing experience on YouTube.\\nhttps://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  referrer: {\n    description:\n      \"Referrer URL for tracking purposes.\\nOriginal parameter: `widget_referrer`\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  showAnnotations: {\n    description:\n      \"Whether to show annotations on the video.\\nOriginal parameter: `iv_load_policy`\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  showCaptions: {\n    description:\n      \"Whether captions should be shown by default.\\nOriginal parameter: `cc_load_policy`\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  showControls: {\n    description: \"Whether to show player controls.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  showPreview: { required: false, control: \"boolean\", type: \"boolean\" },\n  showRelatedVideos: {\n    description:\n      \"Whether to show related videos at the end.\\nOriginal parameter: `rel`\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  startTime: {\n    description:\n      \"Start time of the video in seconds.\\nOriginal parameter: `start`\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n  },\n  url: {\n    description: \"The YouTube video URL or ID\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/blockquote.tsx",
    "content": "/**\n * @deprecated This component will be replaced by the Element component in the future.\n * Use Element with tag=\"blockquote\" instead.\n */\nimport { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"blockquote\";\n\ntype Props = ComponentProps<typeof defaultTag>;\n\nexport const Blockquote = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  ({ children, ...props }, ref) => {\n    return (\n      <blockquote {...props} ref={ref}>\n        {children}\n      </blockquote>\n    );\n  }\n);\n\nBlockquote.displayName = \"Blockquote\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/blockquote.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport type { defaultTag } from \"./blockquote\";\nimport { props } from \"./__generated__/blockquote.props\";\n\nconst presetStyle = {\n  blockquote: [\n    {\n      property: \"margin-top\",\n      value: { type: \"unit\", value: 0, unit: \"number\" },\n    },\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", value: 0, unit: \"number\" },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"unit\", value: 10, unit: \"px\" },\n    },\n    {\n      property: \"margin-left\",\n      value: { type: \"unit\", value: 0, unit: \"number\" },\n    },\n\n    {\n      property: \"padding-top\",\n      value: { type: \"unit\", value: 10, unit: \"px\" },\n    },\n    {\n      property: \"padding-bottom\",\n      value: { type: \"unit\", value: 10, unit: \"px\" },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", value: 20, unit: \"px\" },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", value: 20, unit: \"px\" },\n    },\n\n    {\n      property: \"border-left-width\",\n      value: { type: \"unit\", value: 5, unit: \"px\" },\n    },\n    {\n      property: \"border-left-style\",\n      value: { type: \"keyword\", value: \"solid\" },\n    },\n    {\n      property: \"border-left-color\",\n      value: { type: \"rgb\", r: 226, g: 226, b: 226, alpha: 1 },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"cite\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/body.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"body\";\n\nexport const Body = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <body {...props} ref={ref} />);\n\nBody.displayName = \"Body\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/body.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { body } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/body.props\";\n\nexport const meta: WsComponentMeta = {\n  presetStyle: { body },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/bold.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"b\";\n\nexport const Bold = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <b {...props} ref={ref} />);\n\nBold.displayName = \"Bold\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/bold.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { b } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/bold.props\";\n\nexport const meta: WsComponentMeta = {\n  label: \"Bold Text\",\n  presetStyle: { b },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/box.tsx",
    "content": "import {\n  forwardRef,\n  createElement,\n  type ElementRef,\n  type ComponentProps,\n} from \"react\";\nimport { getTagFromProps } from \"@webstudio-is/sdk/runtime\";\n\nconst defaultTag = \"div\";\n\ntype Props = ComponentProps<typeof defaultTag> & {\n  tag?: string;\n};\n\nexport const Box = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  ({ tag: legacyTag, ...props }, ref) => {\n    const tag = getTagFromProps(props) ?? legacyTag ?? defaultTag;\n    return createElement(tag, { ...props, ref });\n  }\n);\n\nBox.displayName = \"Box\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/box.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport {\n  div,\n  address,\n  article,\n  aside,\n  figure,\n  footer,\n  header,\n  main,\n  nav,\n  section,\n} from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/box.props\";\n\nexport const meta: WsComponentMeta = {\n  presetStyle: {\n    div,\n    address,\n    article,\n    aside,\n    figure,\n    footer,\n    header,\n    main,\n    nav,\n    section,\n  },\n  initialProps: [\"tag\", \"id\", \"class\"],\n  props: {\n    ...props,\n    tag: {\n      required: true,\n      control: \"tag\",\n      type: \"string\",\n      options: [\n        \"div\",\n        \"header\",\n        \"footer\",\n        \"nav\",\n        \"main\",\n        \"section\",\n        \"article\",\n        \"aside\",\n        \"address\",\n        \"figure\",\n        \"span\",\n      ],\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/button.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"button\";\n\ntype ButtonProps = ComponentProps<typeof defaultTag>;\n\nexport const Button = forwardRef<ElementRef<typeof defaultTag>, ButtonProps>(\n  ({ type = \"submit\", children, ...props }, ref) => (\n    <button type={type} {...props} ref={ref}>\n      {children}\n    </button>\n  )\n);\n\nButton.displayName = \"Button\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/button.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/button.props\";\n\nexport const meta: WsComponentMeta = {\n  presetStyle: { button },\n  initialProps: [\"id\", \"class\", \"type\", \"aria-label\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/checkbox.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"input\";\n\nexport const Checkbox = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Omit<ComponentProps<typeof defaultTag>, \"type\" | \"value\"> & { value?: string }\n  // Make sure children are not passed down to an input, because this will result in error.\n>(({ children: _children, checked, defaultChecked, ...props }, ref) => {\n  return (\n    <input\n      {...props}\n      defaultChecked={checked ?? defaultChecked}\n      type=\"checkbox\"\n      ref={ref}\n    />\n  );\n});\n\nCheckbox.displayName = \"Checkbox\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/checkbox.ws.ts",
    "content": "import { CheckboxCheckedIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta, PresetStyle } from \"@webstudio-is/sdk\";\nimport { checkbox } from \"@webstudio-is/sdk/normalize.css\";\nimport type { defaultTag } from \"./checkbox\";\nimport { props } from \"./__generated__/checkbox.props\";\n\nconst presetStyle = {\n  input: [\n    ...checkbox,\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"em\", value: 0.5 },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  icon: CheckboxCheckedIcon,\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"name\", \"value\", \"required\", \"checked\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/code-text.tsx",
    "content": "/**\n * @deprecated This component will be replaced by the Element component in the future.\n * Use Element with tag=\"code\" instead.\n */\nimport {\n  type ElementRef,\n  type ComponentProps,\n  forwardRef,\n  type ForwardedRef,\n} from \"react\";\n\nexport const defaultTag = \"code\";\n\nconst Placeholder = ({\n  innerRef,\n  ...rest\n}: {\n  innerRef: ForwardedRef<HTMLElement>;\n}) => {\n  return (\n    <code {...rest} style={{ padding: 20 }} ref={innerRef}>\n      {`Open the \"Settings\" panel to edit the code.`}\n    </code>\n  );\n};\n\nexport const CodeText = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag> & { code?: string }\n>(({ code, children, ...props }, ref) => {\n  // We are supporting children here for historical reasons, because\n  // the first version of this component allowed using any components inside the CodeText\n  // and we didn't want to migrate them to use code, also it's not entirely possible.\n  if (\n    (children === undefined && code === undefined) ||\n    String(code).trim().length === 0\n  ) {\n    return <Placeholder innerRef={ref} {...props} />;\n  }\n  return (\n    <code {...props} ref={ref}>\n      {code ?? children}\n    </code>\n  );\n});\n\nCodeText.displayName = \"CodeText\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/code-text.ws.ts",
    "content": "import { BracesIcon } from \"@webstudio-is/icons/svg\";\nimport type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { code } from \"@webstudio-is/sdk/normalize.css\";\nimport type { defaultTag } from \"./code-text\";\nimport { props } from \"./__generated__/code-text.props\";\n\nconst presetStyle = {\n  code: [\n    ...code,\n    {\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    },\n    {\n      property: \"white-space-collapse\",\n      value: { type: \"keyword\", value: \"preserve\" },\n    },\n    {\n      property: \"text-wrap-mode\",\n      value: { type: \"keyword\", value: \"wrap\" },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"unit\", value: 0.2, unit: \"em\" },\n    },\n    {\n      property: \"padding-right\",\n      value: { type: \"unit\", value: 0.2, unit: \"em\" },\n    },\n    {\n      property: \"background-color\",\n      value: { type: \"rgb\", r: 238, g: 238, b: 238, alpha: 1 },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  icon: BracesIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [],\n  },\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"lang\", \"code\"],\n  props: {\n    ...props,\n    code: {\n      required: true,\n      control: \"codetext\",\n      type: \"string\",\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/components.ts",
    "content": "export { Slot } from \"./slot\";\nexport { Fragment } from \"./fragment\";\nexport { HtmlEmbed } from \"./html-embed\";\nexport { MarkdownEmbed } from \"./markdown-embed\";\nexport { Body } from \"./body\";\nexport { Box } from \"./box\";\nexport { Text } from \"./text\";\nexport { Heading } from \"./heading\";\nexport { Paragraph } from \"./paragraph\";\nexport { Link } from \"./link\";\nexport { RichTextLink } from \"./rich-text-link\";\nexport { Span } from \"./span\";\nexport { Bold } from \"./bold\";\nexport { Italic } from \"./italic\";\nexport { Superscript } from \"./superscript\";\nexport { Subscript } from \"./subscript\";\nexport { Button } from \"./button\";\nexport { Input } from \"./input\";\nexport { WebhookForm as Form } from \"./webhook-form\";\nexport { Form as RemixForm } from \"./form\";\nexport { Image } from \"./image\";\nexport { Blockquote } from \"./blockquote\";\nexport { List } from \"./list\";\nexport { ListItem } from \"./list-item\";\nexport { Separator } from \"./separator\";\nexport { CodeText } from \"./code-text\";\nexport { Label } from \"./label\";\nexport { Textarea } from \"./textarea\";\nexport { RadioButton } from \"./radio-button\";\nexport { Checkbox } from \"./checkbox\";\nexport { Vimeo } from \"./vimeo\";\nexport { YouTube } from \"./youtube\";\nexport { VimeoPreviewImage } from \"./vimeo-preview-image\";\nexport { VimeoPlayButton } from \"./vimeo-play-button\";\nexport { VimeoSpinner } from \"./vimeo-spinner\";\nexport { XmlNode } from \"./xml-node\";\nexport { XmlTime } from \"./xml-time\";\nexport { Time } from \"./time\";\nexport { Select } from \"./select\";\nexport { Option } from \"./option\";\nexport { HeadSlot } from \"./head-slot\";\nexport { HeadLink } from \"./head-link\";\nexport { HeadMeta } from \"./head-meta\";\nexport { HeadTitle } from \"./head-title\";\nexport { Video } from \"./video\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/content-embed.template.tsx",
    "content": "import { type TemplateMeta, $, ws } from \"@webstudio-is/template\";\nimport { imagePlaceholderDataUrl } from \"@webstudio-is/image\";\nimport { ContentEmbedIcon } from \"@webstudio-is/icons/svg\";\n\nconst htmlSample = `\n<h1>Styling HTML with Content Embed</h1>\n<p>Content Embed allows styling of HTML, which primarily comes from external data.</p>\n<h2>How to Use Content Embed</h2>\n<ul>\n  <li>Every element is shown in the Navigator.</li>\n  <li>Apply styles and Tokens to each element.</li>\n  <li>Adjustments to elements apply universally within this embed, ensuring consistency across your content.</li>\n</ul>\n<hr>\n<h2>This sample text contains all the elements that can be styled.</h2>\n<p>Any elements that were not used above are used below.</p>\n<h3>Heading 3</h3>\n<h4>Heading 4</h4>\n<h5>Heading 5</h5>\n<h6>Heading 6</h6>\n<p><a href=\"#\">Links</a> connect your content to relevant resources.</p>\n<p><strong>Bold text</strong> makes your important points stand out.</p>\n<p><em>Italic text</em> is great for emphasizing terms.</p>\n<ol>\n  <li>First Step</li>\n  <li>Second Step</li>\n</ol>\n<img src=\"${imagePlaceholderDataUrl}\">\n<blockquote>Capture attention with a powerful quote.</blockquote>\n<p>Using <code>console.log(\"Hello World\");</code> will log to the console.</p>\n<table>\n  <tr>\n    <th>Header 1</th>\n    <th>Header 2</th>\n    <th>Header 3</th>\n  </tr>\n  <tr>\n    <td>Cell 1.1</td>\n    <td>Cell 1.2</td>\n    <td>Cell 1.3</td>\n  </tr>\n  <tr>\n    <td>Cell 2.1</td>\n    <td>Cell 2.2</td>\n    <td>Cell 2.3</td>\n  </tr>\n  <tr>\n    <td>Cell 3.1</td>\n    <td>Cell 3.2</td>\n    <td>Cell 3.3</td>\n  </tr>\n</table>\n`.trim();\n\nexport const meta: TemplateMeta = {\n  category: \"data\",\n  icon: ContentEmbedIcon,\n  description:\n    \"Content Embed allows styling of HTML, which can be provided via the Code property statically or loaded dynamically from any Resource, for example, from a CMS.\",\n  order: 3,\n  template: (\n    <$.HtmlEmbed ws:label=\"Content Embed\" code={htmlSample}>\n      <ws.descendant ws:label=\"Paragraph\" selector=\" p\" />\n      <ws.descendant ws:label=\"Heading 1\" selector=\" h1\" />\n      <ws.descendant ws:label=\"Heading 2\" selector=\" h2\" />\n      <ws.descendant ws:label=\"Heading 3\" selector=\" h3\" />\n      <ws.descendant ws:label=\"Heading 4\" selector=\" h4\" />\n      <ws.descendant ws:label=\"Heading 5\" selector=\" h5\" />\n      <ws.descendant ws:label=\"Heading 6\" selector=\" h6\" />\n      <ws.descendant ws:label=\"Bold\" selector=\" :where(strong, b)\" />\n      <ws.descendant ws:label=\"Italic\" selector=\" :where(em, i)\" />\n      <ws.descendant ws:label=\"Link\" selector=\" a\" />\n      <ws.descendant ws:label=\"Image\" selector=\" img\" />\n      <ws.descendant ws:label=\"Blockquote\" selector=\" blockquote\" />\n      <ws.descendant ws:label=\"Code Text\" selector=\" code\" />\n      <ws.descendant ws:label=\"List\" selector=\" :where(ul, ol)\" />\n      <ws.descendant ws:label=\"List Item\" selector=\" li\" />\n      <ws.descendant ws:label=\"Separator\" selector=\" hr\" />\n      <ws.descendant ws:label=\"Table\" selector=\" table\" />\n      <ws.descendant ws:label=\"Table Row\" selector=\" tr\" />\n      <ws.descendant ws:label=\"Table Header Cell\" selector=\" th\" />\n      <ws.descendant ws:label=\"Table Cell\" selector=\" td\" />\n    </$.HtmlEmbed>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/form.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"form\";\n\nexport const Form = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>(({ children, ...props }, ref) => (\n  <form {...props} ref={ref}>\n    {children}\n  </form>\n));\n\nForm.displayName = \"Form\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/form.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { form } from \"@webstudio-is/sdk/normalize.css\";\nimport type { defaultTag } from \"./form\";\nimport { props } from \"./__generated__/form.props\";\n\nconst presetStyle = {\n  form: [\n    ...form,\n    { property: \"min-height\", value: { type: \"unit\", unit: \"px\", value: 20 } },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  label: \"Form\",\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"action\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/fragment.tsx",
    "content": "import { forwardRef, type ElementRef, type ReactNode } from \"react\";\n\ntype Props = {\n  children?: ReactNode;\n};\n\nexport const Fragment = forwardRef<ElementRef<\"div\">, Props>((props, ref) => {\n  return <div {...props} ref={ref} style={{ display: \"contents\" }} />;\n});\n\nFragment.displayName = \"Fragment\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/fragment.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\n\nexport const meta: WsComponentMeta = {};\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-link.tsx",
    "content": "import { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n} from \"react\";\nimport { XmlNode } from \"./xml-node\";\n\nexport const defaultTag = \"link\";\n\ntype LinkRel =\n  | \"alternate\"\n  | \"author\"\n  | \"canonical\"\n  | \"dns-prefetch\"\n  | \"help\"\n  | \"icon\"\n  | \"license\"\n  | \"manifest\"\n  | \"modulepreload\"\n  | \"next\"\n  | \"nofollow\"\n  | \"noopener\"\n  | \"noreferrer\"\n  | \"opener\"\n  | \"pingback\"\n  | \"preconnect\"\n  | \"prefetch\"\n  | \"preload\"\n  | \"prev\"\n  | \"search\"\n  | \"stylesheet\"\n  | \"tag\";\n\ntype LinkAs =\n  | \"audio\"\n  | \"document\"\n  | \"embed\"\n  | \"fetch\"\n  | \"font\"\n  | \"image\"\n  | \"object\"\n  | \"script\"\n  | \"style\"\n  | \"track\"\n  | \"video\"\n  | \"worker\";\n\nconst PROPS_ORDER = [\"rel\", \"hrefLang\", \"href\", \"type\", \"as\"] as const;\n\nexport const HeadLink = forwardRef<\n  ElementRef<\"div\">,\n  { rel?: LinkRel; as?: LinkAs } & ComponentProps<typeof defaultTag>\n>(({ ...props }, ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n\n  const propsSet = new Set([...PROPS_ORDER, ...Object.keys(props)]) as Set<\n    keyof typeof props\n  >;\n\n  const cleanOrderedProps: Record<string, unknown> = {};\n\n  for (const prop of propsSet) {\n    if (prop in props && props[prop] !== undefined) {\n      cleanOrderedProps[prop] = props[prop];\n    }\n  }\n\n  if (renderer === undefined) {\n    return <link {...cleanOrderedProps} />;\n  }\n\n  // HTML attributes are case-insensitive, but the convention is to use lowercase\n  const htmlAttributes = Object.fromEntries(\n    Object.entries(cleanOrderedProps).map(([key, value]) => [\n      key?.toLowerCase(),\n      value,\n    ])\n  );\n\n  return <XmlNode tag={defaultTag} {...htmlAttributes} ref={ref} />;\n});\n\nHeadLink.displayName = \"HeadLink\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-link.ws.ts",
    "content": "import { ResourceIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/head-link.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: ResourceIcon,\n  contentModel: {\n    category: \"none\",\n    children: [],\n  },\n  initialProps: [\"rel\", \"hrefLang\", \"href\", \"type\", \"as\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-meta.tsx",
    "content": "import { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n} from \"react\";\nimport { XmlNode } from \"./xml-node\";\n\nexport const defaultTag = \"meta\";\n\nconst PROPS_ORDER = [\"property\", \"name\", \"content\"] as const;\n\nexport const HeadMeta = forwardRef<\n  ElementRef<\"div\">,\n  ComponentProps<typeof defaultTag>\n>(({ ...props }, ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n\n  const propsSet = new Set([...PROPS_ORDER, ...Object.keys(props)]) as Set<\n    keyof typeof props\n  >;\n\n  const cleanOrderedProps: Record<string, unknown> = {};\n\n  for (const prop of propsSet) {\n    if (prop in props && props[prop] !== undefined) {\n      cleanOrderedProps[prop] = props[prop];\n    }\n  }\n\n  if (renderer === undefined) {\n    return <meta {...cleanOrderedProps} />;\n  }\n\n  // HTML attributes are case-insensitive, but the convention is to use lowercase\n  const htmlAttributes = Object.fromEntries(\n    Object.entries(cleanOrderedProps).map(([key, value]) => [\n      key?.toLowerCase(),\n      value,\n    ])\n  );\n\n  return <XmlNode tag={defaultTag} {...htmlAttributes} ref={ref} />;\n});\n\nHeadMeta.displayName = \"HeadMeta\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-meta.ws.ts",
    "content": "import { WindowInfoIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/head-meta.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: WindowInfoIcon,\n  contentModel: {\n    category: \"none\",\n    children: [],\n  },\n  initialProps: [\"name\", \"property\", \"content\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-slot.template.tsx",
    "content": "import { type TemplateMeta, $ } from \"@webstudio-is/template\";\n\nexport const meta: TemplateMeta = {\n  category: \"general\",\n  description:\n    \"The Head Slot component lets you customize page-specific head elements (like canonical URLs), which merge with your site's global head settings, with Head Slot definitions taking priority over Page Settings. For site-wide head changes, use project settings instead.\",\n  order: 5,\n  template: (\n    <$.HeadSlot>\n      <$.HeadTitle ws:label=\"Title\">Title</$.HeadTitle>\n      <$.HeadLink ws:label=\"Link\" rel=\"help\" href=\"/help\"></$.HeadLink>\n      <$.HeadMeta ws:label=\"Meta\" name=\"keywords\" content=\"SEO\"></$.HeadMeta>\n    </$.HeadSlot>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-slot.tsx",
    "content": "import {\n  getClosestInstance,\n  ReactSdkContext,\n  type Hook,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport { forwardRef, type ElementRef, useContext, type ReactNode } from \"react\";\nimport { XmlNode } from \"./xml-node\";\n\nexport const defaultTag = \"head\";\n\nexport const HeadSlot = forwardRef<\n  ElementRef<\"div\">,\n  { \"data-ws-expand\"?: boolean } & { children: ReactNode }\n>(({ children, ...props }, ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n\n  if (renderer === undefined) {\n    return children;\n  }\n\n  if (props[\"data-ws-expand\"] !== true) {\n    return null;\n  }\n\n  return (\n    <div\n      ref={ref}\n      style={{\n        padding: \"8px\",\n        position: \"fixed\",\n        top: 0,\n        left: 0,\n        right: 0,\n        zIndex: 1000,\n      }}\n      {...props}\n    >\n      <XmlNode tag={defaultTag}>{children}</XmlNode>\n    </div>\n  );\n});\n\nHeadSlot.displayName = \"HeadSlot\";\n\nexport const hooksHeadSlot: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `HeadSlot`) {\n        const popover = getClosestInstance(\n          event.instancePath,\n          instance,\n          `HeadSlot`\n        );\n        if (popover) {\n          context.setMemoryProp(popover, \"data-ws-expand\", undefined);\n        }\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `HeadSlot`) {\n        const popover = getClosestInstance(\n          event.instancePath,\n          instance,\n          `HeadSlot`\n        );\n        if (popover) {\n          context.setMemoryProp(popover, \"data-ws-expand\", true);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-slot.ws.ts",
    "content": "import { HeaderIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/head-slot.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: HeaderIcon,\n  description: \"Inserts children into the head of the document\",\n  contentModel: {\n    category: \"instance\",\n    children: [\"HeadLink\", \"HeadMeta\", \"HeadTitle\"],\n  },\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-title.tsx",
    "content": "import { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n} from \"react\";\nimport { XmlNode } from \"./xml-node\";\n\nexport const defaultTag = \"title\";\n\nconst PROPS_ORDER = [] as const;\n\nexport const HeadTitle = forwardRef<\n  ElementRef<\"div\">,\n  ComponentProps<typeof defaultTag>\n>(({ ...props }, ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n\n  const propsSet = new Set([...PROPS_ORDER, ...Object.keys(props)]) as Set<\n    keyof typeof props\n  >;\n\n  const cleanOrderedProps: Record<string, unknown> = {};\n\n  for (const prop of propsSet) {\n    if (prop in props && props[prop] !== undefined) {\n      cleanOrderedProps[prop] = props[prop];\n    }\n  }\n\n  if (renderer === undefined) {\n    return <title {...cleanOrderedProps} />;\n  }\n\n  // HTML attributes are case-insensitive, but the convention is to use lowercase\n  const htmlAttributes = Object.fromEntries(\n    Object.entries(cleanOrderedProps).map(([key, value]) => [\n      key?.toLowerCase(),\n      value,\n    ])\n  );\n\n  return <XmlNode tag={defaultTag} {...htmlAttributes} ref={ref} />;\n});\n\nHeadTitle.displayName = \"HeadTitle\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/head-title.ws.ts",
    "content": "import { WindowTitleIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/head-title.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: WindowTitleIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"text\"],\n  },\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/heading.tsx",
    "content": "/**\n * @deprecated This component will be replaced by the Element component in the future.\n * Use Element with tag=\"h1\", \"h2\", etc. instead.\n */\nimport {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  createElement,\n} from \"react\";\nimport { getTagFromProps } from \"@webstudio-is/sdk/runtime\";\n\nconst defaultTag = \"h1\";\n\ntype Props = ComponentProps<typeof defaultTag> & {\n  tag?: string;\n};\n\nexport const Heading = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  ({ tag: legacyTag, ...props }, ref) => {\n    const tag = getTagFromProps(props) ?? legacyTag ?? defaultTag;\n    return createElement(tag, { ...props, ref });\n  }\n);\n\nHeading.displayName = \"Heading\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/heading.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { h1, h2, h3, h4, h5, h6 } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/heading.props\";\n\nexport const meta: WsComponentMeta = {\n  presetStyle: {\n    h1,\n    h2,\n    h3,\n    h4,\n    h5,\n    h6,\n  },\n  initialProps: [\"tag\", \"id\", \"class\"],\n  props: {\n    ...props,\n    tag: {\n      required: true,\n      control: \"tag\",\n      type: \"string\",\n      options: [\"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"],\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/hooks.ts",
    "content": "import type { Hook } from \"@webstudio-is/react-sdk\";\nimport { hooksSelect } from \"./select\";\nimport { hooksHeadSlot } from \"./head-slot\";\n\nexport const hooks: Hook[] = [hooksSelect, hooksHeadSlot];\n"
  },
  {
    "path": "packages/sdk-components-react/src/html-embed-patchers.ts",
    "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\nconst isDOMContentLoaded = () => {\n  return (\n    document.readyState === \"complete\" || document.readyState === \"interactive\"\n  );\n};\n\nconst eventListenerTasks: Array<() => void> = [];\n\nlet domContentLoadedPatched = false;\n\nexport const patchDomEvents = () => {\n  // If original event is not fired yet, do nothing as it can cause serious side effects.\n  if (isDOMContentLoaded() === false) {\n    console.error(\"DOMContentLoaded event has not been fired yet\");\n    return;\n  }\n\n  if (domContentLoadedPatched) {\n    return;\n  }\n\n  domContentLoadedPatched = true;\n\n  const originalAddEventListener = document.addEventListener;\n  const originalWindowAddEventListener = window.addEventListener;\n\n  const domContentLoadedEvent = new Event(\"DOMContentLoaded\");\n  const windowLoadEvent = new Event(\"load\");\n\n  window.addEventListener = (type: any, listener: any, options?: any) => {\n    if (type === \"DOMContentLoaded\") {\n      eventListenerTasks.push(() =>\n        listener.call(window, domContentLoadedEvent)\n      );\n      // We do not call original event listeners as everything is already loaded and orinal event is not going to be fired.\n    } else if (type === \"load\") {\n      // We store the listener to execute it later\n      eventListenerTasks.push(() => listener.call(window, windowLoadEvent));\n      originalWindowAddEventListener.call(window, type, listener, options);\n    } else {\n      // For all other events, use the original method\n\n      originalWindowAddEventListener.call(window, type, listener, options);\n    }\n  };\n\n  document.addEventListener = (type: any, listener: any, options: any) => {\n    if (type === \"DOMContentLoaded\") {\n      // We store the listener to execute it later\n      eventListenerTasks.push(() =>\n        listener.call(document, domContentLoadedEvent)\n      );\n      // We do not call original event listeners as everything is already loaded and orinal event is not going to be fired.\n    } else {\n      // For all other events, use the original method\n      originalAddEventListener.call(document, type, listener, options);\n    }\n  };\n};\n\nexport const executeDomEvents = () => {\n  for (const task of eventListenerTasks) {\n    task();\n  }\n  eventListenerTasks.length = 0;\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/html-embed.test.tsx",
    "content": "/**\n * @vitest-environment jsdom\n */\nimport * as React from \"react\";\nimport ReactDOMServer from \"react-dom/server\";\nimport { test, expect, describe, beforeEach } from \"vitest\";\nimport { fireEvent, render, screen } from \"@testing-library/react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport { __testing__, HtmlEmbed } from \"./html-embed\";\nimport { cartesian } from \"./test-utils/cartesian\";\n\nconst scriptTestIdPrefix = __testing__.scriptTestIdPrefix;\nconst SCRIPT_TEST_ID = \"script-a\";\nconst SCRIPT_PROCESSED_TEST_ID = `${scriptTestIdPrefix}${SCRIPT_TEST_ID}`;\n\nconst FRAGMENT_DIV_ID = \"div\";\n\nconst App = (props: {\n  clientOnly?: boolean;\n  renderer?: \"canvas\" | \"preview\";\n  executeScriptOnCanvas?: boolean;\n}) => {\n  const [page, switchPage] = React.useReducer((n) => (n + 1) % 2, 0);\n  const [refresh, setRefresh] = React.useReducer((n) => n + 1, 0);\n\n  const code = `\n    <script data-testid=\"${SCRIPT_TEST_ID}\" data-page=\"${page}\">console.log('hello')</script>\n    <div data-testid=\"${FRAGMENT_DIV_ID}\">hello</div>\n  `;\n\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        assetBaseUrl: \"\",\n        imageLoader: () => \"\",\n        renderer: props.renderer,\n        resources: {},\n        breakpoints: [],\n        onError: console.error,\n      }}\n    >\n      <div key={page}>\n        <HtmlEmbed\n          code={code}\n          clientOnly={props.clientOnly}\n          executeScriptOnCanvas={props.executeScriptOnCanvas}\n        />\n        <button type=\"button\" onClick={switchPage}>\n          page:{page}\n        </button>\n        <button type=\"button\" onClick={setRefresh}>\n          refresh:{refresh}\n        </button>\n      </div>\n    </ReactSdkContext.Provider>\n  );\n};\n\nbeforeEach(() => {\n  // clear body\n  for (const child of document.body.children) {\n    child.remove();\n  }\n});\n\ndescribe(\"Published site\", () => {\n  /**\n   * Tests the behavior of script tags in an SSR context for a published site with `clientOnly` set to false:\n   * - SSR: Renders script tags in HTML embeds directly, without modification, on the server side, on hydration and on refresh.\n   * - Page Navigation: Executes scripts using additional processing described here https://ghinda.net/article/script-tags/.\n   */\n  test.each([false, undefined])(\"clientOnly === false\", async (clientOnly) => {\n    const ui = <App clientOnly={clientOnly} />;\n    const container = document.createElement(\"div\");\n    // Server rendering\n    document.body.appendChild(container);\n    container.innerHTML = ReactDOMServer.renderToString(ui);\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy();\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n    // Hydration\n    render(ui, { container, hydrate: true });\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy();\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n    // Force page rerender\n    fireEvent.click(screen.getByText(\"refresh:0\"));\n    expect(screen.getByText(\"refresh:1\")).toBeTruthy();\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)?.dataset.page).toEqual(\"0\");\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n    // Force page change\n    fireEvent.click(screen.getByText(\"page:0\"));\n    expect(screen.getByText(\"page:1\")).toBeTruthy();\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n    // Script has changed\n    expect(\n      screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)?.dataset.page\n    ).toEqual(\"1\");\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n  });\n\n  /**\n   * Tests the behavior of script tags in an SSR context for a published site with `clientOnly` set to true:\n   * - SSR: Do not render html embed at all on server side.\n   * - Render HTMLEmbed atfer hydration, then executes scripts using additional processing described here https://ghinda.net/article/script-tags/.\n   * - Page Navigation: Executes scripts using additional processing described here https://ghinda.net/article/script-tags/.\n   */\n  test(\"clientOnly === true\", async () => {\n    const ui = <App clientOnly={true} />;\n    const container = document.createElement(\"div\");\n    // Server rendering\n    document.body.appendChild(container);\n    container.innerHTML = ReactDOMServer.renderToString(ui);\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).not.toBeTruthy();\n\n    // Hydration\n    render(ui, { container, hydrate: true });\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n    // Force page rerender\n    fireEvent.click(screen.getByText(\"refresh:0\"));\n    expect(screen.getByText(\"refresh:1\")).toBeTruthy();\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n    expect(\n      screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)?.dataset.page\n    ).toEqual(\"0\");\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n    // Force page change\n    fireEvent.click(screen.getByText(\"page:0\"));\n    expect(screen.getByText(\"page:1\")).toBeTruthy;\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n    expect(\n      screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)?.dataset.page\n    ).toEqual(\"1\");\n    expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n  });\n});\n\n/**\n * On canvas renderer, scripts are not executed on the server side.\n */\ndescribe(\"Builder renderer= canvas | preview\", () => {\n  /**\n   * On canvas if renderer is canvas, and executeScriptOnCanvas=false\n   * scripts postprocessing are not applied independently of clientOnly value.\n   */\n  test.each(\n    cartesian(\n      [true, false, undefined], //clientOnly\n      [false, undefined] //executeScriptOnCanvas\n    )\n  )(\n    \"script processing are not applied when clientOnly=%p executeScriptOnCanvas=%p and renderer=canvas\",\n    async (clientOnly, executeScriptOnCanvas) => {\n      const ui = (\n        <App\n          clientOnly={clientOnly}\n          renderer={\"canvas\"}\n          executeScriptOnCanvas={executeScriptOnCanvas}\n        />\n      );\n\n      const container = document.createElement(\"div\");\n      // Server rendering\n      document.body.appendChild(container);\n\n      // Hydration\n      render(ui, { container });\n      // Wait execute await task(); to be executed (if exists)\n      await Promise.resolve();\n\n      expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy();\n      expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy();\n      expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n      fireEvent.click(screen.getByText(\"refresh:0\"));\n      expect(screen.getByText(\"refresh:1\")).toBeTruthy();\n      // Wait execute await task(); to be executed (if exists)\n      await Promise.resolve();\n\n      expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy();\n      expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy();\n      expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n    }\n  );\n\n  /**\n   * Script postprocessing is always applied for preview mode, independently of other props\n   * Script postprocessing is always applied for canvas renderer if executeScriptOnCanvas = true or undefined\n   */\n  test.each([\n    ...cartesian(\n      [true, false, undefined], // clientOnly\n      [\"preview\" as const], // renderer\n      [true, false, undefined] // executeScriptOnCanvas\n    ),\n    ...cartesian(\n      [true, false, undefined], // clientOnly\n      [\"canvas\" as const], // renderer\n      [true] // executeScriptOnCanvas\n    ),\n  ])(\n    \"In preview mode, script processing are applied when clientOnly=%p and renderer=%p and executeScriptOnCanvas=%p\",\n    async (clientOnly, renderer, executeScriptOnCanvas) => {\n      const ui = (\n        <App\n          clientOnly={clientOnly}\n          renderer={renderer}\n          executeScriptOnCanvas={executeScriptOnCanvas}\n        />\n      );\n\n      const container = document.createElement(\"div\");\n      // Server rendering\n      document.body.appendChild(container);\n\n      // Hydration\n      render(ui, { container });\n      // Wait execute await task(); to be executed (if exists)\n      await Promise.resolve();\n\n      expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n      expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n      expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n      fireEvent.click(screen.getByText(\"refresh:0\"));\n      expect(screen.getByText(\"refresh:1\")).toBeTruthy();\n      // Wait execute await task(); to be executed (if exists)\n      await Promise.resolve();\n\n      expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n      expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n      expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n\n      document.body.removeChild(container);\n    }\n  );\n\n  test(\"Code change cause scripts to be updated\", async () => {\n    const SCRIPT_TEST_ID_2 = \"script-b\";\n    const SCRIPT_PROCESSED_TEST_ID_2 = `${scriptTestIdPrefix}${SCRIPT_TEST_ID_2}`;\n\n    const codes = [\n      `<script data-testid=\"${SCRIPT_TEST_ID}\">console.log('hello')</script>`,\n      `<script data-testid=\"${SCRIPT_TEST_ID_2}\">console.log('hello')</script>`,\n    ];\n\n    const AppWithCode = () => {\n      const [codeIndex, switchCodeIndex] = React.useReducer(\n        (n) => (n + 1) % 2,\n        0\n      );\n\n      const code = codes[codeIndex];\n\n      return (\n        <ReactSdkContext.Provider\n          value={{\n            assetBaseUrl: \"\",\n            imageLoader: () => \"\",\n            renderer: \"canvas\",\n            resources: {},\n            breakpoints: [],\n            onError: console.error,\n          }}\n        >\n          <HtmlEmbed code={code} executeScriptOnCanvas={true} />\n          <button type=\"button\" onClick={switchCodeIndex}>\n            code:{codeIndex}\n          </button>\n        </ReactSdkContext.Provider>\n      );\n    };\n\n    const ui = <AppWithCode />;\n    const container = document.createElement(\"div\");\n    // Server rendering\n    document.body.appendChild(container);\n\n    // Hydration\n    render(ui, { container });\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).toBeTruthy();\n\n    // Force page rerender\n    fireEvent.click(screen.getByText(\"code:0\"));\n    // Wait execute await task(); to be executed (if exists)\n    await Promise.resolve();\n\n    expect(screen.queryByTestId(SCRIPT_TEST_ID)).not.toBeTruthy();\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy();\n    // Code has changed, new script processed\n    expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID_2)).toBeTruthy();\n  });\n\n  test.each([\"\", \"   \"])(\"Placeholder is shown if code is %p\", async (code) => {\n    const AppWithCode = () => {\n      return <HtmlEmbed code={code} executeScriptOnCanvas={true} />;\n    };\n\n    const ui = <AppWithCode />;\n    const container = document.createElement(\"div\");\n    // Server rendering\n    document.body.appendChild(container);\n\n    // Hydration\n    render(ui, { container });\n\n    expect(\n      screen.queryByText(`Open the \"Settings\" panel to insert HTML code.`)\n    ).toBeTruthy();\n  });\n\n  /**\n   * Test safe mode: when isSafeMode is true, scripts should never execute\n   * regardless of executeScriptOnCanvas setting or renderer mode.\n   */\n  test.each(\n    cartesian(\n      [true, false, undefined], //clientOnly\n      [true, false, undefined], //executeScriptOnCanvas\n      [\"canvas\", \"preview\"] //renderer\n    )\n  )(\n    \"Safe mode prevents script execution when clientOnly=%p executeScriptOnCanvas=%p and renderer=%p\",\n    async (clientOnly, executeScriptOnCanvas, renderer) => {\n      const AppWithSafeMode = () => {\n        const code = `\n          <script data-testid=\"${SCRIPT_TEST_ID}\">console.log('hello')</script>\n          <div data-testid=\"${FRAGMENT_DIV_ID}\">hello</div>\n        `;\n\n        return (\n          <ReactSdkContext.Provider\n            value={{\n              assetBaseUrl: \"\",\n              imageLoader: () => \"\",\n              renderer: renderer as \"canvas\" | \"preview\",\n              isSafeMode: true,\n              resources: {},\n              breakpoints: [],\n              onError: console.error,\n            }}\n          >\n            <HtmlEmbed\n              code={code}\n              clientOnly={clientOnly}\n              executeScriptOnCanvas={executeScriptOnCanvas}\n            />\n          </ReactSdkContext.Provider>\n        );\n      };\n\n      const ui = <AppWithSafeMode />;\n      const container = document.createElement(\"div\");\n      document.body.appendChild(container);\n\n      render(ui, { container });\n      await Promise.resolve();\n\n      // In safe mode, scripts should not be processed (no SCRIPT_PROCESSED_TEST_ID)\n      expect(screen.queryByTestId(SCRIPT_TEST_ID)).toBeTruthy();\n      expect(screen.queryByTestId(SCRIPT_PROCESSED_TEST_ID)).not.toBeTruthy();\n      expect(screen.queryByTestId(FRAGMENT_DIV_ID)).toBeTruthy();\n    }\n  );\n});\n"
  },
  {
    "path": "packages/sdk-components-react/src/html-embed.tsx",
    "content": "import {\n  forwardRef,\n  useContext,\n  useEffect,\n  useRef,\n  type ForwardedRef,\n  useSyncExternalStore,\n  useState,\n  type ReactNode,\n  useMemo,\n} from \"react\";\nimport { mergeRefs } from \"@react-aria/utils\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport { executeDomEvents, patchDomEvents } from \"./html-embed-patchers\";\n\nexport const __testing__ = {\n  scriptTestIdPrefix: \"client-\",\n};\n\nconst insertScript = (sourceScript: HTMLScriptElement): Promise<void> => {\n  return new Promise((resolve, reject) => {\n    const script = document.createElement(\"script\");\n    const hasSrc = sourceScript.hasAttribute(\"src\");\n\n    const isTypeModule = sourceScript.type === \"module\";\n\n    // Copy all attributes from the source script to the new script, because we are going to replace the source script with the new one\n    // and the user might rely on some attributes.\n    for (const { name, value } of sourceScript.attributes) {\n      script.setAttribute(name, value);\n    }\n\n    // For testing purposes, we add a prefix to the testid to differentiate between server and client rendered scripts.\n    if (script.dataset.testid !== undefined) {\n      script.dataset.testid = `${__testing__.scriptTestIdPrefix}${script.dataset.testid}`;\n    }\n\n    if (hasSrc) {\n      script.addEventListener(\"load\", () => {\n        resolve();\n      });\n      script.addEventListener(\"error\", reject);\n    } else {\n      // Module scripts are deferred by default, and there's no direct 'load' event for inline scripts.\n      // By creating a Blob and using dynamic import(), we can detect when the script has been\n      // loaded, parsed, and executed. This approach allows us to handle inline module scripts\n      // in a way that preserves execution order and provides a mechanism to know when execution is complete.\n      if (isTypeModule) {\n        const blob = new Blob([sourceScript.innerText], {\n          type: \"text/javascript\",\n        });\n        const url = URL.createObjectURL(blob);\n        import(/* @vite-ignore */ url)\n          .then(resolve)\n          .catch(reject)\n          .finally(() => {\n            URL.revokeObjectURL(url);\n          });\n        return;\n      }\n\n      script.textContent = sourceScript.innerText;\n    }\n\n    sourceScript.replaceWith(script);\n\n    // Run the callback immediately for inline scripts.\n    if (hasSrc === false) {\n      resolve();\n    }\n  });\n};\n\ntype ScriptTask = () => Promise<void>;\n\n/**\n * We want to execute scripts from all embeds sequentially to preserve execution order.\n */\nconst syncTasksQueue: ScriptTask[] = [];\nlet processing = false;\n\nconst processSyncTasks = async (syncTasks: ScriptTask[]) => {\n  syncTasksQueue.push(...syncTasks);\n\n  // await 1 tick so tasks from all HTMLEmbeds are added to the queue\n  await Promise.resolve();\n\n  if (processing) {\n    return;\n  }\n\n  patchDomEvents();\n\n  processing = true;\n\n  while (syncTasksQueue.length > 0) {\n    const task = syncTasksQueue.shift()!;\n    await task();\n  }\n\n  executeDomEvents();\n\n  processing = false;\n};\n\n// Inspiration https://ghinda.net/article/script-tags\nconst execute = (container: HTMLElement) => {\n  const scripts = container.querySelectorAll(\"script\");\n  const syncTasks: Array<ScriptTask> = [];\n  const asyncTasks: Array<ScriptTask> = [];\n\n  scripts.forEach((script) => {\n    const tasks = script.hasAttribute(\"async\") ? asyncTasks : syncTasks;\n\n    tasks.push(() => {\n      return insertScript(script);\n    });\n  });\n\n  // Insert the script tags in parallel.\n  for (const task of asyncTasks) {\n    task();\n  }\n\n  processSyncTasks(syncTasks);\n};\n\ntype ChildProps = {\n  innerRef: ForwardedRef<HTMLDivElement>;\n  // code can be actually undefined when prop is not provided\n  code?: string;\n  className?: string;\n};\n\nconst Placeholder = (props: ChildProps) => {\n  const { code, innerRef, ...rest } = props;\n  return (\n    <div ref={innerRef} {...rest} style={{ display: \"block\", padding: 20 }}>\n      {'Open the \"Settings\" panel to insert HTML code.'}\n    </div>\n  );\n};\n\nconst useIsServer = () => {\n  // https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store\n  const isServer = useSyncExternalStore(\n    () => () => {},\n    () => false,\n    () => true\n  );\n  return isServer;\n};\n\nconst ClientOnly = (props: { children: ReactNode }) => {\n  const isServer = useIsServer();\n\n  if (isServer) {\n    return;\n  }\n  return props.children;\n};\n\n/**\n * Executes scripts when rendered in the builder manually, because innerHTML doesn't execute scripts.\n * Also executes scripts on the published site when `clientOnly` is true.\n */\nconst ClientEmbed = (props: ChildProps) => {\n  const { code, innerRef, ...rest } = props;\n  const containerRef = useRef<HTMLDivElement>(null);\n  const executeScripts = useRef(true);\n\n  const html = useMemo(\n    () => ({\n      __html: code ?? \"\",\n    }),\n    [code]\n  );\n\n  useEffect(() => {\n    const container = containerRef.current;\n\n    if (container && executeScripts.current) {\n      executeScripts.current = false;\n      execute(container);\n    }\n  }, []);\n\n  return (\n    <div\n      {...rest}\n      ref={mergeRefs(innerRef, containerRef)}\n      dangerouslySetInnerHTML={html}\n    />\n  );\n};\n\n/**\n * Scripts are executed when rendered server side without any manual intervention.\n */\nconst ServerEmbed = (props: ChildProps) => {\n  const { code, innerRef, ...rest } = props;\n\n  return (\n    <div\n      {...rest}\n      ref={innerRef}\n      dangerouslySetInnerHTML={{ __html: code ?? \"\" }}\n    />\n  );\n};\n\nconst ClientEmbedWithNonExecutableScripts = ServerEmbed;\n\ntype HtmlEmbedProps = {\n  code: string;\n  executeScriptOnCanvas?: boolean;\n  clientOnly?: boolean;\n  className?: string;\n  // avoid builder passing it to dom\n  children?: never;\n};\n\nexport const HtmlEmbed = forwardRef<HTMLDivElement, HtmlEmbedProps>(\n  (props, ref) => {\n    const { code, executeScriptOnCanvas, clientOnly, children, ...rest } =\n      props;\n    const { renderer, isSafeMode } = useContext(ReactSdkContext);\n\n    const isServer = useIsServer();\n\n    const [ssrRendered] = useState(isServer);\n\n    // - code can be actually undefined when prop is not provided\n    // - cast code to string in case non-string value is computed from expression\n    if (code === undefined || String(code).trim().length === 0) {\n      return <Placeholder innerRef={ref} {...rest} />;\n    }\n\n    if (ssrRendered) {\n      // We are on published site, on server rendering or after hydration\n      if (clientOnly !== true) {\n        return <ServerEmbed innerRef={ref} code={code} {...rest} />;\n      }\n\n      return (\n        <ClientOnly>\n          <ClientEmbed innerRef={ref} code={code} {...rest} />\n        </ClientOnly>\n      );\n    }\n    // We are or on canvas | preview | published site after client routing\n\n    // In safe mode, never execute scripts regardless of other settings\n    if (isSafeMode) {\n      return (\n        <ClientOnly>\n          <ClientEmbedWithNonExecutableScripts\n            innerRef={ref}\n            code={code}\n            {...rest}\n          />\n        </ClientOnly>\n      );\n    }\n\n    // The only case we need to prevent script execution if it's explicitly disabled on the canvas\n    if (renderer === \"canvas\" && executeScriptOnCanvas !== true) {\n      return (\n        <ClientOnly>\n          <ClientEmbedWithNonExecutableScripts\n            innerRef={ref}\n            code={code}\n            {...rest}\n          />\n        </ClientOnly>\n      );\n    }\n\n    return (\n      <ClientOnly>\n        <ClientEmbed\n          // Use key={code} to allow scripts to be reexecuted when code has changed\n          key={code}\n          innerRef={ref}\n          code={code}\n          {...rest}\n        />\n      </ClientOnly>\n    );\n  }\n);\n\nHtmlEmbed.displayName = \"HtmlEmbed\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/html-embed.ws.ts",
    "content": "import { EmbedIcon } from \"@webstudio-is/icons/svg\";\nimport { descendantComponent, type WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/html-embed.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"general\",\n  label: \"HTML Embed\",\n  description: \"Used to add HTML code to the page, such as an SVG or script.\",\n  icon: EmbedIcon,\n  order: 3,\n  contentModel: {\n    category: \"instance\",\n    children: [descendantComponent],\n  },\n  presetStyle: {\n    div: [\n      {\n        property: \"display\",\n        value: { type: \"keyword\", value: \"contents\" },\n      },\n      {\n        property: \"white-space-collapse\",\n        value: { type: \"keyword\", value: \"collapse\" },\n      },\n    ],\n  },\n  initialProps: [\"class\", \"clientOnly\", \"executeScriptOnCanvas\"],\n  props: {\n    ...props,\n    clientOnly: {\n      ...props.clientOnly,\n      description:\n        \"Activate it for any scripts that can mutate the DOM or introduce interactivity. This only affects the published site.\",\n    },\n    executeScriptOnCanvas: {\n      ...props.executeScriptOnCanvas,\n      label: \"Run scripts on canvas\",\n      description:\n        \"Dangerously allow script execution on canvas without switching to preview mode. This only affects build mode, but may result in unwanted side effects inside builder!\",\n    },\n    code: {\n      required: true,\n      control: \"code\",\n      language: \"html\",\n      type: \"string\",\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/image.tsx",
    "content": "import {\n  type ComponentPropsWithoutRef,\n  type ElementRef,\n  forwardRef,\n  useContext,\n} from \"react\";\nimport { Image as WebstudioImage } from \"@webstudio-is/image\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const defaultTag = \"img\";\n\ntype Props = Omit<ComponentPropsWithoutRef<typeof WebstudioImage>, \"loader\">;\n\nexport const Image = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Props & { $webstudio$canvasOnly$assetId?: string | undefined }\n>(\n  (\n    {\n      loading = \"lazy\",\n      width,\n      height,\n      optimize = true,\n      decoding: decodingProp,\n      // @todo: it's a hack made for the builder and should't be in the runtime at all.\n      $webstudio$canvasOnly$assetId,\n      ...props\n    },\n    ref\n  ) => {\n    // cast to string when invalid value type is provided with binding\n    const src = String(props.src ?? \"\");\n\n    const { imageLoader, renderer } = useContext(ReactSdkContext);\n\n    let decoding = decodingProp;\n\n    let key = src;\n\n    if (renderer === \"canvas\") {\n      // With disabled cache and loading lazy, chrome may not render the image at all\n      loading = \"eager\";\n\n      // Avoid image flickering on switching from preview to asset (during upload)\n      decoding = \"sync\";\n\n      // use assetId as key to not recreate the image if it's switched from uploading to uploaded asset state (we don't know asset src during uploading)\n      key = $webstudio$canvasOnly$assetId ?? src;\n\n      // NaN width and height means that the image is not yet uploaded, and should not be optimized on canvas\n      if (\n        width !== undefined &&\n        height !== undefined &&\n        Number.isNaN(width) &&\n        Number.isNaN(height)\n      ) {\n        optimize = false;\n        width = undefined;\n        height = undefined;\n      }\n    }\n\n    return (\n      <WebstudioImage\n        /**\n         * `key` is needed to recreate the image in case of asset change in builder,\n         * this gives immediate feedback when an asset is changed.\n         *\n         * In non-builder mode, key on images are usually also a good idea,\n         * prevents showing outdated images on route change.\n         **/\n        key={key}\n        loading={loading}\n        decoding={decoding}\n        optimize={optimize}\n        width={width}\n        height={height}\n        {...props}\n        loader={imageLoader}\n        src={src}\n        ref={ref}\n      />\n    );\n  }\n);\n\nImage.displayName = \"Image\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/image.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { img } from \"@webstudio-is/sdk/normalize.css\";\nimport type { defaultTag } from \"./image\";\nimport { props } from \"./__generated__/image.props\";\n\nconst presetStyle = {\n  img: [\n    ...img,\n\n    // Otherwise on new image insert onto canvas it can overfit screen size multiple times\n    {\n      property: \"max-width\",\n      value: { type: \"unit\", unit: \"%\", value: 100 },\n    },\n    // inline | inline-block is not suitable because without line-height: 0 on the parent you get unsuitable spaces/margins\n    // see https://stackoverflow.com/questions/24771194/is-the-margin-of-inline-block-4px-is-static-for-all-browsers\n    {\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    },\n    // Set image height to \"auto\" to reduce layout shift, improving compatibility across browsers like Safari.\n    // Unlike \"fit-content,\" \"auto\" preserves the aspect ratio when the width exceeds max-width. (in Safari)\n    // See https://web.dev/articles/optimize-cls#best_practice_for_setting_image_dimensions\n    {\n      property: \"height\",\n      value: { type: \"keyword\", value: \"auto\" },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  category: \"media\",\n  description:\n    \"Add an image asset to the page. Webstudio automatically converts images to WebP or AVIF format and makes them responsive for best performance.\",\n  presetStyle,\n  order: 0,\n  initialProps: [\n    \"id\",\n    \"class\",\n    \"src\",\n    \"width\",\n    \"height\",\n    \"alt\",\n    \"loading\",\n    \"optimize\",\n  ],\n  props: {\n    ...props,\n    // Automatically generated props don't have the right control.\n    src: {\n      type: \"string\",\n      control: \"file\",\n      label: \"Source\",\n      required: false,\n      accept: \"image/*\",\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/input.tsx",
    "content": "import { forwardRef, useContext, type ComponentProps } from \"react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Input = forwardRef<HTMLInputElement, ComponentProps<\"input\">>(\n  ({ value, defaultValue, checked, defaultChecked, ...props }, ref) => {\n    const { renderer } = useContext(ReactSdkContext);\n    // enfroce default value update\n    const key =\n      renderer === \"canvas\"\n        ? String(value ?? defaultValue) + String(checked ?? defaultChecked)\n        : undefined;\n    return (\n      <input\n        {...props}\n        key={key}\n        defaultValue={value ?? defaultValue}\n        defaultChecked={checked ?? defaultChecked}\n        ref={ref}\n      />\n    );\n  }\n);\n\nInput.displayName = \"Input\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/input.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { input } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/input.props\";\n\nconst presetStyle = {\n  input: [\n    ...input,\n    {\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    },\n  ],\n} satisfies PresetStyle<\"input\">;\n\nexport const meta: WsComponentMeta = {\n  label: \"Text Input\",\n  presetStyle,\n  initialProps: [\n    \"id\",\n    \"class\",\n    \"name\",\n    \"value\",\n    \"type\",\n    \"placeholder\",\n    \"required\",\n    \"autofocus\",\n  ],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/italic.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"i\";\n\nexport const Italic = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <i {...props} ref={ref} />);\n\nItalic.displayName = \"Italic\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/italic.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { i } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/italic.props\";\n\nexport const meta: WsComponentMeta = {\n  label: \"Italic Text\",\n  presetStyle: { i },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/label.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"label\";\n\nexport const Label = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <label {...props} ref={ref} />);\n\nLabel.displayName = \"Label\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/label.ws.ts",
    "content": "import type { WsComponentMeta, PresetStyle } from \"@webstudio-is/sdk\";\nimport { label } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/label.props\";\nimport type { defaultTag } from \"./label\";\n\nconst presetStyle = {\n  label: [\n    ...label,\n    { property: \"display\", value: { type: \"keyword\", value: \"block\" } },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  label: \"Input Label\",\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"for\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/link.tsx",
    "content": "/**\n * @deprecated This component will be replaced by the Element component in the future.\n * Use Element with tag=\"a\" instead.\n */\nimport { forwardRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"a\";\n\ntype Props = Omit<ComponentProps<\"a\">, \"target\" | \"download\"> & {\n  // override (string & {}) in target to generate keywords\n  target?: \"_self\" | \"_blank\" | \"_parent\" | \"_top\";\n  download?: boolean;\n  prefetch?: \"none\" | \"intent\" | \"render\" | \"viewport\";\n  preventScrollReset?: boolean;\n  reloadDocument?: boolean;\n  replace?: boolean;\n};\n\nexport const Link = forwardRef<\n  HTMLAnchorElement,\n  Props & { $webstudio$canvasOnly$assetId?: string | undefined }\n>((props, ref) => {\n  const {\n    children,\n    // @todo: it's a hack made for Image component for the builder and should't be in the runtime at all.\n    $webstudio$canvasOnly$assetId,\n    ...rest\n  } = props;\n  return (\n    <a {...rest} href={rest.href ?? \"#\"} ref={ref}>\n      {children}\n    </a>\n  );\n});\n\nLink.displayName = \"Link\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/link.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { a } from \"@webstudio-is/sdk/normalize.css\";\nimport type { defaultTag } from \"./link\";\nimport { props } from \"./__generated__/link.props\";\n\nconst presetStyle = {\n  a: [\n    ...a,\n    {\n      property: \"display\",\n      value: { type: \"keyword\", value: \"inline-block\" },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  presetStyle,\n  states: [{ label: \"Current page\", selector: \"[aria-current=page]\" }],\n  initialProps: [\"id\", \"class\", \"href\", \"target\", \"prefetch\", \"download\"],\n  props: {\n    ...props,\n    href: {\n      type: \"string\",\n      control: \"url\",\n      required: false,\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/list-item.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"li\";\n\ntype Props = ComponentProps<typeof defaultTag>;\n\nexport const ListItem = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  ({ children, ...props }, ref) => {\n    return (\n      <li {...props} ref={ref}>\n        {children}\n      </li>\n    );\n  }\n);\n\nListItem.displayName = \"ListItem\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/list-item.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { li } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/list-item.props\";\n\nexport const meta: WsComponentMeta = {\n  presetStyle: { li },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/list.tsx",
    "content": "/**\n * @deprecated This component will be replaced by the Element component in the future.\n * Use Element with tag=\"ul\" or tag=\"ol\" instead.\n */\nimport {\n  forwardRef,\n  createElement,\n  type ElementRef,\n  type ComponentProps,\n} from \"react\";\n\nconst unorderedTag = \"ul\";\nconst orderedTag = \"ol\";\n\nexport type ListTag = typeof unorderedTag | typeof orderedTag;\n\ntype Props = ComponentProps<typeof unorderedTag> &\n  ComponentProps<typeof orderedTag> & {\n    /** Shows numbers instead of bullets when toggled. See the “List Style Type” property under the “List Item” section in the Style panel for more options. */\n    ordered?: boolean;\n  };\n\nexport const List = forwardRef<\n  ElementRef<typeof unorderedTag | typeof orderedTag>,\n  Props\n>(({ ordered = false, ...props }, ref) => {\n  const tag = ordered ? orderedTag : unorderedTag;\n  return createElement(tag, { ...props, ref });\n});\n\nList.displayName = \"List\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/list.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { ol, ul } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/list.props\";\nimport type { ListTag } from \"./list\";\n\nconst presetStyle = {\n  ol: [\n    ...ol,\n    {\n      property: \"margin-top\",\n      value: { type: \"keyword\", value: \"0\" },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"keyword\", value: \"10px\" },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"keyword\", value: \"40px\" },\n    },\n  ],\n  ul: [\n    ...ul,\n    {\n      property: \"margin-top\",\n      value: { type: \"keyword\", value: \"0\" },\n    },\n    {\n      property: \"margin-bottom\",\n      value: { type: \"keyword\", value: \"10px\" },\n    },\n    {\n      property: \"padding-left\",\n      value: { type: \"keyword\", value: \"40px\" },\n    },\n  ],\n} satisfies PresetStyle<ListTag>;\n\nexport const meta: WsComponentMeta = {\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"ordered\", \"start\", \"reversed\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/markdown-embed.template.tsx",
    "content": "import { type TemplateMeta, $, ws } from \"@webstudio-is/template\";\nimport { imagePlaceholderDataUrl } from \"@webstudio-is/image\";\n\nconst markdownSample = `\n# Styling Markdown with Markdown Embed\n\nMarkdown Embed allows styling of Markdown, which primarily comes from external data.\n\n## How to Use Markdown Embed\n\n- Every element is shown in the Navigator.\n- Apply styles and Tokens to each element.\n- Adjustments to elements apply universally within this embed, ensuring consistency across your content.\n\n---\n\n## This sample text contains all the elements that can be styled.\n\nAny elements that were not used above are used below.\n\n### Heading 3\n#### Heading 4\n##### Heading 5\n###### Heading 6\n\n[Links](#) connect your content to relevant resources.\n\n**Bold text** makes your important points stand out.\n\n*Italic text* is great for emphasizing terms.\n\n1. First Step\n2. Second Step\n\n![Image placeholder](${imagePlaceholderDataUrl})\n\n> Capture attention with a powerful quote.\n\nUsing \\`console.log(\"Hello World\");\\` will log to the console.\n\n| Header 1   | Header 2   | Header 3   |\n|------------|------------|------------|\n| Cell 1.1   | Cell 1.2   | Cell 1.3   |\n| Cell 2.1   | Cell 2.2   | Cell 2.3   |\n| Cell 3.1   | Cell 3.2   | Cell 3.3   |\n`.trim();\n\nexport const meta: TemplateMeta = {\n  category: \"data\",\n  description: \"Used to add markdown code to the page\",\n  order: 4,\n  template: (\n    <$.MarkdownEmbed code={markdownSample}>\n      <ws.descendant ws:label=\"Paragraph\" selector=\" p\" />\n      <ws.descendant ws:label=\"Heading 1\" selector=\" h1\" />\n      <ws.descendant ws:label=\"Heading 2\" selector=\" h2\" />\n      <ws.descendant ws:label=\"Heading 3\" selector=\" h3\" />\n      <ws.descendant ws:label=\"Heading 4\" selector=\" h4\" />\n      <ws.descendant ws:label=\"Heading 5\" selector=\" h5\" />\n      <ws.descendant ws:label=\"Heading 6\" selector=\" h6\" />\n      <ws.descendant ws:label=\"Bold\" selector=\" :where(strong, b)\" />\n      <ws.descendant ws:label=\"Italic\" selector=\" :where(em, i)\" />\n      <ws.descendant ws:label=\"Link\" selector=\" a\" />\n      <ws.descendant ws:label=\"Image\" selector=\" img\" />\n      <ws.descendant ws:label=\"Blockquote\" selector=\" blockquote\" />\n      <ws.descendant ws:label=\"Code Text\" selector=\" code\" />\n      <ws.descendant ws:label=\"List\" selector=\" :where(ul, ol)\" />\n      <ws.descendant ws:label=\"List Item\" selector=\" li\" />\n      <ws.descendant ws:label=\"Separator\" selector=\" hr\" />\n      <ws.descendant ws:label=\"Table\" selector=\" table\" />\n      <ws.descendant ws:label=\"Table Row\" selector=\" tr\" />\n      <ws.descendant ws:label=\"Table Header Cell\" selector=\" th\" />\n      <ws.descendant ws:label=\"Table Cell\" selector=\" td\" />\n    </$.MarkdownEmbed>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/markdown-embed.tsx",
    "content": "import { micromark } from \"micromark\";\nimport { gfmTable, gfmTableHtml } from \"micromark-extension-gfm-table\";\nimport { forwardRef, useMemo, type ComponentProps } from \"react\";\n\ntype MarkdownEmbedProps = ComponentProps<\"div\"> & {\n  code: string;\n  // avoid builder passing it to dom\n  children?: never;\n};\n\nexport const MarkdownEmbed = /* @__PURE__ */ forwardRef<\n  HTMLDivElement,\n  MarkdownEmbedProps\n>((props, ref) => {\n  const { code, children, ...rest } = props;\n  const html = useMemo(\n    // support data uri protocol in images\n    () =>\n      micromark(code ?? \"\", {\n        allowDangerousProtocol: true,\n        extensions: [gfmTable()],\n        htmlExtensions: [gfmTableHtml()],\n      }),\n    [code]\n  );\n  return <div {...rest} ref={ref} dangerouslySetInnerHTML={{ __html: html }} />;\n});\n"
  },
  {
    "path": "packages/sdk-components-react/src/markdown-embed.ws.ts",
    "content": "import { MarkdownEmbedIcon } from \"@webstudio-is/icons/svg\";\nimport { descendantComponent, type WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/markdown-embed.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: MarkdownEmbedIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [descendantComponent],\n  },\n  presetStyle: {\n    div: [\n      {\n        property: \"display\",\n        value: { type: \"keyword\", value: \"contents\" },\n      },\n      {\n        property: \"white-space-collapse\",\n        value: { type: \"keyword\", value: \"collapse\" },\n      },\n    ],\n  },\n  initialProps: [\"class\"],\n  props: {\n    ...props,\n    code: {\n      required: true,\n      control: \"code\",\n      language: \"markdown\",\n      type: \"string\",\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/metas.ts",
    "content": "export { meta as Slot } from \"./slot.ws\";\nexport { meta as Fragment } from \"./fragment.ws\";\nexport { meta as HtmlEmbed } from \"./html-embed.ws\";\nexport { meta as MarkdownEmbed } from \"./markdown-embed.ws\";\nexport { meta as Body } from \"./body.ws\";\nexport { meta as Box } from \"./box.ws\";\nexport { meta as Text } from \"./text.ws\";\nexport { meta as Heading } from \"./heading.ws\";\nexport { meta as Paragraph } from \"./paragraph.ws\";\nexport { meta as Link } from \"./link.ws\";\nexport { meta as RichTextLink } from \"./rich-text-link.ws\";\nexport { meta as Span } from \"./span.ws\";\nexport { meta as Bold } from \"./bold.ws\";\nexport { meta as Italic } from \"./italic.ws\";\nexport { meta as Superscript } from \"./superscript.ws\";\nexport { meta as Subscript } from \"./subscript.ws\";\nexport { meta as Button } from \"./button.ws\";\nexport { meta as Input } from \"./input.ws\";\nexport { meta as Form } from \"./webhook-form.ws\";\nexport { meta as RemixForm } from \"./form.ws\";\nexport { meta as Image } from \"./image.ws\";\nexport { meta as Blockquote } from \"./blockquote.ws\";\nexport { meta as List } from \"./list.ws\";\nexport { meta as ListItem } from \"./list-item.ws\";\nexport { meta as Separator } from \"./separator.ws\";\nexport { meta as CodeText } from \"./code-text.ws\";\nexport { meta as Label } from \"./label.ws\";\nexport { meta as Textarea } from \"./textarea.ws\";\nexport { meta as RadioButton } from \"./radio-button.ws\";\nexport { meta as Checkbox } from \"./checkbox.ws\";\nexport { meta as Vimeo } from \"./vimeo.ws\";\nexport { meta as YouTube } from \"./youtube.ws\";\nexport { meta as VimeoPreviewImage } from \"./vimeo-preview-image.ws\";\nexport { meta as VimeoPlayButton } from \"./vimeo-play-button.ws\";\nexport { meta as VimeoSpinner } from \"./vimeo-spinner.ws\";\nexport { meta as XmlNode } from \"./xml-node.ws\";\nexport { meta as XmlTime } from \"./xml-time.ws\";\nexport { meta as Time } from \"./time.ws\";\nexport { meta as Select } from \"./select.ws\";\nexport { meta as Option } from \"./option.ws\";\nexport { meta as HeadSlot } from \"./head-slot.ws\";\nexport { meta as HeadLink } from \"./head-link.ws\";\nexport { meta as HeadMeta } from \"./head-meta.ws\";\nexport { meta as HeadTitle } from \"./head-title.ws\";\nexport { meta as Video } from \"./video.ws\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/option.tsx",
    "content": "import { forwardRef, type ComponentProps } from \"react\";\n\nexport const Option = forwardRef<HTMLOptionElement, ComponentProps<\"option\">>(\n  (props, ref) => <option {...props} ref={ref} />\n);\n\nOption.displayName = \"Option\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/option.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/option.props\";\n\nconst presetStyle = {\n  option: [\n    {\n      property: \"background-color\",\n      state: \":checked\",\n      value: {\n        type: \"rgb\",\n        alpha: 1,\n        r: 209,\n        g: 209,\n        b: 209,\n      },\n    },\n  ],\n} satisfies PresetStyle<\"option\">;\n\nexport const meta: WsComponentMeta = {\n  category: \"hidden\",\n  description:\n    \"An item within a drop-down menu that users can select as their chosen value.\",\n  presetStyle,\n  states: [\n    // Applies when option is being activated (clicked)\n    { selector: \":active\", label: \"Active\" },\n    // Applies to the currently selected option\n    { selector: \":checked\", label: \"Checked\" },\n    // For <option> elements: The :default pseudo-class selects the <option> that has the selected attribute when the page loads. This is true even if the user later selects a different option.\n    { selector: \":default\", label: \"Default\" },\n    { selector: \":hover\", label: \"Hover\" },\n    { selector: \":disabled\", label: \"Disabled\" },\n  ],\n  initialProps: [\"label\", \"value\", \"label\", \"disabled\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/paragraph.tsx",
    "content": "/**\n * @deprecated This component will be replaced by the Element component in the future.\n * Use Element with tag=\"p\" instead.\n */\nimport { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"p\";\n\nexport const Paragraph = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>(({ children, ...props }, ref) => (\n  <p {...props} ref={ref}>\n    {children}\n  </p>\n));\n\nParagraph.displayName = \"Paragraph\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/paragraph.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { p } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/paragraph.props\";\n\nexport const meta: WsComponentMeta = {\n  presetStyle: { p },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/radio-button.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"input\";\n\nexport const RadioButton = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Omit<ComponentProps<typeof defaultTag>, \"type\" | \"value\"> & { value?: string }\n  // Make sure children are not passed down to an input, because this will result in error.\n>(({ children: _children, checked, defaultChecked, ...props }, ref) => (\n  <input\n    {...props}\n    defaultChecked={checked ?? defaultChecked}\n    type=\"radio\"\n    ref={ref}\n  />\n));\n\nRadioButton.displayName = \"RadioButton\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/radio-button.ws.ts",
    "content": "import { RadioCheckedIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta, PresetStyle } from \"@webstudio-is/sdk\";\nimport type { defaultTag } from \"./radio-button\";\nimport { radio } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/radio-button.props\";\n\nconst presetStyle = {\n  input: [\n    ...radio,\n    {\n      property: \"margin-right\",\n      value: { type: \"unit\", unit: \"em\", value: 0.5 },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  label: \"Radio\",\n  icon: RadioCheckedIcon,\n  presetStyle,\n  initialProps: [\"id\", \"class\", \"name\", \"value\", \"required\", \"checked\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/rich-text-link.tsx",
    "content": "export { Link as RichTextLink } from \"./link\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/rich-text-link.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { meta as linkMeta } from \"./link.ws\";\n\nexport const meta: WsComponentMeta = linkMeta;\n"
  },
  {
    "path": "packages/sdk-components-react/src/select.tsx",
    "content": "import { forwardRef, useContext, type ComponentProps } from \"react\";\nimport { ReactSdkContext, type Hook } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Select = forwardRef<HTMLSelectElement, ComponentProps<\"select\">>(\n  ({ value, defaultValue, ...props }, ref) => {\n    const { renderer } = useContext(ReactSdkContext);\n    // enfroce default value update\n    const key =\n      renderer === \"canvas\" ? String(value ?? defaultValue) : undefined;\n    return (\n      <select\n        {...props}\n        key={key}\n        defaultValue={value ?? defaultValue}\n        ref={ref}\n      />\n    );\n  }\n);\n\nSelect.displayName = \"Select\";\n\nexport const hooksSelect: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === \"Select\" || instance.tag === \"select\") {\n        context.setMemoryProp(instance, \"value\", undefined);\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    let selectedOption: undefined | string;\n    for (const instance of event.instancePath) {\n      if (instance.component === \"Option\" || instance.tag === \"option\") {\n        selectedOption = context.getPropValue(instance, \"value\") as string;\n      }\n      if (instance.component === \"Select\" || instance.tag === \"select\") {\n        context.setMemoryProp(instance, \"value\", selectedOption);\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/select.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { select } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/select.props\";\n\nconst presetStyle = {\n  select: [\n    ...select,\n    {\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    },\n  ],\n} satisfies PresetStyle<\"select\">;\n\nexport const meta: WsComponentMeta = {\n  presetStyle,\n  initialProps: [\n    \"id\",\n    \"class\",\n    \"name\",\n    \"value\",\n    \"multiple\",\n    \"required\",\n    \"autofocus\",\n  ],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/separator.tsx",
    "content": "import {\n  forwardRef,\n  createElement,\n  type ElementRef,\n  type ComponentProps,\n} from \"react\";\n\nexport const defaultTag = \"hr\";\n\ntype Props = ComponentProps<typeof defaultTag>;\n\nexport const Separator = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  (props, ref) => {\n    return createElement(defaultTag, { ...props, ref });\n  }\n);\n\nSeparator.displayName = \"Separator\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/separator.ws.ts",
    "content": "import type { PresetStyle, WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { hr } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/separator.props\";\nimport type { defaultTag } from \"./separator\";\n\nconst presetStyle = {\n  hr: [\n    ...hr,\n    {\n      property: \"height\",\n      value: { type: \"keyword\", value: \"1px\" },\n    },\n    {\n      property: \"background-color\",\n      value: { type: \"keyword\", value: \"gray\" },\n    },\n    {\n      property: \"border-top-style\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n    {\n      property: \"border-right-style\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n    {\n      property: \"border-left-style\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n    {\n      property: \"border-bottom-style\",\n      value: { type: \"keyword\", value: \"none\" },\n    },\n  ],\n} satisfies PresetStyle<typeof defaultTag>;\n\nexport const meta: WsComponentMeta = {\n  presetStyle,\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/shared/video.ts",
    "content": "import { createContext } from \"react\";\n\nexport const requestFullscreen = (element: HTMLIFrameElement) => {\n  const isTouchDevice = \"ontouchstart\" in window;\n  // Allows it to work on small screens on desktop too and makes it easy to test.\n  const isMobileResolution = window.matchMedia(\"(max-width: 1024px)\").matches;\n  if (isMobileResolution || isTouchDevice) {\n    element.requestFullscreen();\n  }\n};\n\nexport type PlayerStatus = \"initial\" | \"loading\" | \"ready\";\n\nexport const VideoContext = createContext<{\n  previewImageUrl?: URL;\n  onInitPlayer: () => void;\n  status: PlayerStatus;\n}>({\n  onInitPlayer: () => {},\n  status: \"initial\",\n});\n"
  },
  {
    "path": "packages/sdk-components-react/src/slot.tsx",
    "content": "import { forwardRef, type ElementRef, type ReactNode } from \"react\";\n\ntype Props = {\n  children?: ReactNode;\n};\n\nexport const Slot = forwardRef<ElementRef<\"div\">, Props>((props, ref) => {\n  return (\n    <div\n      {...props}\n      ref={ref}\n      style={{ display: props.children ? \"contents\" : \"block\" }}\n    />\n  );\n});\n\nSlot.displayName = \"Slot\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/slot.ws.ts",
    "content": "import { SlotComponentIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\n\nexport const meta: WsComponentMeta = {\n  category: \"general\",\n  description:\n    \"Slot is a container for content that you want to reference across the project. Changes made to a Slot's children will be reflected in all other instances of that Slot.\",\n  icon: SlotComponentIcon,\n  order: 4,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/span.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"span\";\n\nexport const Span = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <span {...props} ref={ref} />);\n\nSpan.displayName = \"Span\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/span.ws.ts",
    "content": "import { PaintBrushIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { span } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/span.props\";\n\nexport const meta: WsComponentMeta = {\n  label: \"Text\",\n  icon: PaintBrushIcon,\n  presetStyle: { span },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/subscript.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"sub\";\n\nexport const Subscript = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <sub {...props} ref={ref} />);\n\nSubscript.displayName = \"Subscript\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/subscript.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { sub } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/subscript.props\";\n\nexport const meta: WsComponentMeta = {\n  label: \"Subscript Text\",\n  presetStyle: { sub },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/superscript.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\nexport const defaultTag = \"sup\";\n\nexport const Superscript = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag>\n>((props, ref) => <sup {...props} ref={ref} />);\n\nSuperscript.displayName = \"Bold\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/superscript.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { sup } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/superscript.props\";\n\nexport const meta: WsComponentMeta = {\n  label: \"Superscript Text\",\n  presetStyle: { sup },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/templates.ts",
    "content": "export { meta as ContentEmbed } from \"./content-embed.template\";\nexport { meta as MarkdownEmbed } from \"./markdown-embed.template\";\nexport { meta as Form } from \"./webhook-form.template\";\nexport { meta as Vimeo } from \"./vimeo.template\";\nexport { meta as YouTube } from \"./youtube.template\";\nexport { meta as HeadSlot } from \"./head-slot.template\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/test-utils/cartesian.ts",
    "content": "export function cartesian<A, B>(a: A[], b: B[]): [A, B][];\nexport function cartesian<A, B, C>(a: A[], b: B[], c: C[]): [A, B, C][];\n\n// https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript\n// eslint-disable-next-line func-style\nexport function cartesian(...a: unknown[][]) {\n  return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat())));\n}\n"
  },
  {
    "path": "packages/sdk-components-react/src/text.tsx",
    "content": "import {\n  forwardRef,\n  createElement,\n  type ElementRef,\n  type ComponentProps,\n} from \"react\";\nimport { getTagFromProps } from \"@webstudio-is/sdk/runtime\";\n\nconst defaultTag = \"div\";\n\ntype Props = ComponentProps<typeof defaultTag> & {\n  tag?: string;\n};\n\nexport const Text = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  ({ tag: legacyTag, ...props }, ref) => {\n    const tag = getTagFromProps(props) ?? legacyTag ?? defaultTag;\n    return createElement(tag, { ...props, ref });\n  }\n);\n\nText.displayName = \"Text\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/text.ws.ts",
    "content": "import { TextIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/text.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: TextIcon,\n  presetStyle: {\n    div: [\n      ...div,\n      {\n        property: \"min-height\",\n        value: { type: \"unit\", unit: \"em\", value: 1 },\n      },\n    ],\n  },\n  initialProps: [\"tag\", \"id\", \"class\"],\n  props: {\n    ...props,\n    tag: {\n      required: true,\n      control: \"tag\",\n      type: \"string\",\n      options: [\"div\", \"cite\", \"figcaption\", \"span\"],\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/textarea.tsx",
    "content": "import { forwardRef, useContext, type ComponentProps } from \"react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Textarea = forwardRef<\n  HTMLTextAreaElement,\n  ComponentProps<\"textarea\">\n>(({ value, defaultValue, ...props }, ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n  // enfroce default value update\n  const key = renderer === \"canvas\" ? String(value ?? defaultValue) : undefined;\n  return (\n    <textarea\n      {...props}\n      key={key}\n      defaultValue={value ?? defaultValue}\n      ref={ref}\n    />\n  );\n});\n\nTextarea.displayName = \"Textarea\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/textarea.ws.ts",
    "content": "import type { WsComponentMeta, PresetStyle } from \"@webstudio-is/sdk\";\nimport { textarea } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/textarea.props\";\n\nconst presetStyle = {\n  textarea: [\n    ...textarea,\n    // resize doesn't work well while on canvas\n    { property: \"resize\", value: { type: \"keyword\", value: \"none\" } },\n    {\n      property: \"display\",\n      value: { type: \"keyword\", value: \"block\" },\n    },\n  ],\n} satisfies PresetStyle<\"textarea\">;\n\nexport const meta: WsComponentMeta = {\n  label: \"Text Area\",\n  presetStyle,\n  contentModel: {\n    category: \"instance\",\n    children: [],\n  },\n  initialProps: [\n    \"id\",\n    \"class\",\n    \"name\",\n    \"value\",\n    \"placeholder\",\n    \"required\",\n    \"autofocus\",\n  ],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/time.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { __testing__ } from \"./time\";\n\nconst { parseDate, formatDate } = __testing__;\n\ntest(\"13-digit Unix timestamp\", () => {\n  expect(parseDate(\"1724938577059\")).toEqual(\n    new Date(\"2024-08-29T13:36:17.059Z\")\n  );\n});\n\ntest(\"10-digit Unix timestamp \", () => {\n  expect(parseDate(\"1724938577\")).toEqual(new Date(\"2024-08-29T13:36:17.000Z\"));\n});\n\ntest(\"maximum 32-bit signed integer timestamp\", () => {\n  expect(parseDate(\"2147483647\")).toEqual(new Date(\"2038-01-19T03:14:07.000Z\"));\n});\n\ntest(\"far future date\", () => {\n  expect(parseDate(\"9999999999999\")).toEqual(\n    new Date(\"2286-11-20T17:46:39.999Z\")\n  );\n});\n\ntest(\"parse ISO date string\", () => {\n  expect(parseDate(\"2024-08-29T13:36:17.000Z\")).toEqual(\n    new Date(\"2024-08-29T13:36:17.000Z\")\n  );\n});\n\ntest.skip(\"parse short date\", () => {\n  expect(parseDate(\"2024.10\")).toEqual(new Date(\"2024-10-01T00:00:00.000Z\"));\n  expect(parseDate(\"2024/10\")).toEqual(new Date(\"2024-10-01T00:00:00.000Z\"));\n  expect(parseDate(\"2024-10\")).toEqual(new Date(\"2024-10-01T00:00:00.000Z\"));\n  expect(parseDate(\"2024\")).toEqual(new Date(\"2024-01-01T00:00:00.000Z\"));\n});\n\ntest(\"empty string\", () => {\n  expect(parseDate(\"\")).toEqual(undefined);\n});\n\ntest(\"invalid date string\", () => {\n  expect(parseDate(\"whatever that is\")).toEqual(undefined);\n});\n\ntest(\"formatDate with full template\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"YYYY-MM-DD HH:mm:ss\")).toBe(\"2025-10-31 14:05:09\");\n});\n\ntest(\"formatDate with short date\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DD/MM/YY\")).toBe(\"31/10/25\");\n});\n\ntest(\"formatDate with time only\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"H:m:s\")).toBe(\"14:5:9\");\n});\n\ntest(\"formatDate with padded values\", () => {\n  const date = new Date(\"2025-01-05T08:03:07\");\n  expect(formatDate(date, \"YYYY-MM-DD HH:mm:ss\")).toBe(\"2025-01-05 08:03:07\");\n});\n\ntest(\"formatDate with unpadded values\", () => {\n  const date = new Date(\"2025-01-05T08:03:07\");\n  expect(formatDate(date, \"YYYY-M-D H:m:s\")).toBe(\"2025-1-5 8:3:7\");\n});\n\ntest(\"formatDate with year only\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"YYYY\")).toBe(\"2025\");\n});\n\ntest(\"formatDate with custom separator\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DD.MM.YYYY\")).toBe(\"31.10.2025\");\n});\n\ntest(\"formatDate with full month name in English\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"MMMM D, YYYY\", \"en-US\")).toBe(\"October 31, 2025\");\n});\n\ntest(\"formatDate with short month name in English\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"MMM D, YYYY\", \"en-US\")).toBe(\"Oct 31, 2025\");\n});\n\ntest(\"formatDate with full day name in English\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DDDD, MMMM D, YYYY\", \"en-US\")).toBe(\n    \"Friday, October 31, 2025\"\n  );\n});\n\ntest(\"formatDate with short day name in English\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DDD, MMM D\", \"en-US\")).toBe(\"Fri, Oct 31\");\n});\n\ntest(\"formatDate with full month name in German\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"D. MMMM YYYY\", \"de-DE\")).toBe(\"31. Oktober 2025\");\n});\n\ntest(\"formatDate with full day name in German\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DDDD, D. MMMM YYYY\", \"de-DE\")).toBe(\n    \"Freitag, 31. Oktober 2025\"\n  );\n});\n\ntest(\"formatDate with short day and month in Spanish\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DDD, D MMM YYYY\", \"es-ES\")).toBe(\"vie, 31 oct 2025\");\n});\n\ntest(\"formatDate with full day and month in French\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"DDDD D MMMM YYYY\", \"fr-FR\")).toBe(\n    \"vendredi 31 octobre 2025\"\n  );\n});\n\ntest(\"formatDate with mixed tokens\", () => {\n  const date = new Date(\"2025-01-15T09:30:45\");\n  expect(formatDate(date, \"DDDD, MMMM D, YYYY at HH:mm\", \"en-US\")).toBe(\n    \"Wednesday, January 15, 2025 at 09:30\"\n  );\n});\n\ntest(\"formatDate preserves non-token text\", () => {\n  const date = new Date(\"2025-10-31T14:05:09\");\n  expect(formatDate(date, \"Today is DDDD\", \"en-US\")).toBe(\"Today is Friday\");\n});\n"
  },
  {
    "path": "packages/sdk-components-react/src/time.tsx",
    "content": "import { forwardRef, type ComponentProps, type ElementRef } from \"react\";\n\nconst languages = [\n  \"af\",\n  \"am\",\n  \"ar\",\n  \"az\",\n  \"be\",\n  \"bg\",\n  \"bn\",\n  \"bs\",\n  \"ca\",\n  \"cs\",\n  \"cy\",\n  \"da\",\n  \"de\",\n  \"el\",\n  \"en\",\n  \"es\",\n  \"et\",\n  \"eu\",\n  \"fa\",\n  \"fi\",\n  \"fr\",\n  \"ga\",\n  \"gl\",\n  \"gu\",\n  \"he\",\n  \"hi\",\n  \"hr\",\n  \"hu\",\n  \"hy\",\n  \"id\",\n  \"is\",\n  \"it\",\n  \"ja\",\n  \"ka\",\n  \"kk\",\n  \"km\",\n  \"kn\",\n  \"ko\",\n  \"ky\",\n  \"lb\",\n  \"lt\",\n  \"lv\",\n  \"mk\",\n  \"ml\",\n  \"mn\",\n  \"mr\",\n  \"ms\",\n  \"mt\",\n  \"nb\",\n  \"nl\",\n  \"nn\",\n  \"pl\",\n  \"pt\",\n  \"ro\",\n  \"ru\",\n  \"si\",\n  \"sk\",\n  \"sl\",\n  \"sq\",\n  \"sr\",\n  \"sv\",\n  \"sw\",\n  \"ta\",\n  \"te\",\n  \"th\",\n  \"tr\",\n  \"uk\",\n  \"ur\",\n  \"uz\",\n  \"vi\",\n  \"zh\",\n] as const;\n\nconst countries = [\n  \"AF\",\n  \"AL\",\n  \"DZ\",\n  \"AS\",\n  \"AD\",\n  \"AO\",\n  \"AI\",\n  \"AQ\",\n  \"AG\",\n  \"AR\",\n  \"AM\",\n  \"AW\",\n  \"AU\",\n  \"AT\",\n  \"AZ\",\n  \"BS\",\n  \"BH\",\n  \"BD\",\n  \"BB\",\n  \"BY\",\n  \"BE\",\n  \"BZ\",\n  \"BJ\",\n  \"BM\",\n  \"BT\",\n  \"BO\",\n  \"BA\",\n  \"BW\",\n  \"BR\",\n  \"BN\",\n  \"BG\",\n  \"BF\",\n  \"BI\",\n  \"CV\",\n  \"KH\",\n  \"CM\",\n  \"CA\",\n  \"KY\",\n  \"CF\",\n  \"TD\",\n  \"CL\",\n  \"CN\",\n  \"CO\",\n  \"KM\",\n  \"CG\",\n  \"CD\",\n  \"CR\",\n  \"HR\",\n  \"CU\",\n  \"CY\",\n  \"CZ\",\n  \"DK\",\n  \"DJ\",\n  \"DM\",\n  \"DO\",\n  \"EC\",\n  \"EG\",\n  \"SV\",\n  \"GQ\",\n  \"ER\",\n  \"EE\",\n  \"SZ\",\n  \"ET\",\n  \"FJ\",\n  \"FI\",\n  \"FR\",\n  \"GA\",\n  \"GM\",\n  \"GE\",\n  \"DE\",\n  \"GH\",\n  \"GR\",\n  \"GD\",\n  \"GT\",\n  \"GN\",\n  \"GW\",\n  \"GY\",\n  \"HT\",\n  \"HN\",\n  \"HU\",\n  \"IS\",\n  \"IN\",\n  \"ID\",\n  \"IR\",\n  \"IQ\",\n  \"IE\",\n  \"IL\",\n  \"IT\",\n  \"JM\",\n  \"JP\",\n  \"JO\",\n  \"KZ\",\n  \"KE\",\n  \"KI\",\n  \"KP\",\n  \"KR\",\n  \"KW\",\n  \"KG\",\n  \"LA\",\n  \"LV\",\n  \"LB\",\n  \"LS\",\n  \"LR\",\n  \"LY\",\n  \"LI\",\n  \"LT\",\n  \"LU\",\n  \"MG\",\n  \"MW\",\n  \"MY\",\n  \"MV\",\n  \"ML\",\n  \"MT\",\n  \"MH\",\n  \"MR\",\n  \"MU\",\n  \"MX\",\n  \"FM\",\n  \"MD\",\n  \"MC\",\n  \"MN\",\n  \"ME\",\n  \"MA\",\n  \"MZ\",\n  \"MM\",\n  \"NA\",\n  \"NR\",\n  \"NP\",\n  \"NL\",\n  \"NZ\",\n  \"NI\",\n  \"NE\",\n  \"NG\",\n  \"NO\",\n  \"OM\",\n  \"PK\",\n  \"PW\",\n  \"PA\",\n  \"PG\",\n  \"PY\",\n  \"PE\",\n  \"PH\",\n  \"PL\",\n  \"PT\",\n  \"QA\",\n  \"RO\",\n  \"RU\",\n  \"RW\",\n  \"KN\",\n  \"LC\",\n  \"VC\",\n  \"WS\",\n  \"SM\",\n  \"ST\",\n  \"SA\",\n  \"SN\",\n  \"RS\",\n  \"SC\",\n  \"SL\",\n  \"SG\",\n  \"SK\",\n  \"SI\",\n  \"SB\",\n  \"SO\",\n  \"ZA\",\n  \"SS\",\n  \"ES\",\n  \"LK\",\n  \"SD\",\n  \"SR\",\n  \"SE\",\n  \"CH\",\n  \"SY\",\n  \"TW\",\n  \"TJ\",\n  \"TZ\",\n  \"TH\",\n  \"TL\",\n  \"TG\",\n  \"TO\",\n  \"TT\",\n  \"TN\",\n  \"TR\",\n  \"TM\",\n  \"TV\",\n  \"UG\",\n  \"UA\",\n  \"AE\",\n  \"GB\",\n  \"US\",\n  \"UY\",\n  \"UZ\",\n  \"VU\",\n  \"VA\",\n  \"VE\",\n  \"VN\",\n  \"YE\",\n  \"ZM\",\n  \"ZW\",\n] as const;\n\ntype Language = (typeof languages)[number];\ntype Country = (typeof countries)[number];\ntype DateStyle = Intl.DateTimeFormatOptions[\"dateStyle\"] | \"none\";\ntype TimeStyle = Intl.DateTimeFormatOptions[\"timeStyle\"] | \"none\";\n\nconst INITIAL_DATE_STRING = \"dateTime attribute is not set\";\nconst INVALID_DATE_STRING = \"\";\n\nconst DEFAULT_LANGUAGE = \"en\";\nconst DEFAULT_COUNTRY = \"GB\";\nconst DEFAULT_DATE_STYLE = \"medium\";\nconst DEFAULT_TIME_STYLE = \"none\";\n\nconst languageOrDefault = (language: unknown): Language => {\n  return languages.includes(language as Language)\n    ? (language as Language)\n    : DEFAULT_LANGUAGE;\n};\n\nconst countryOrDefault = (country: unknown): Country => {\n  return countries.includes(country as Country)\n    ? (country as Country)\n    : DEFAULT_COUNTRY;\n};\n\nconst dateStyleOrUndefined = (\n  value: unknown\n): Intl.DateTimeFormatOptions[\"dateStyle\"] => {\n  if ([\"full\", \"long\", \"medium\", \"short\"].includes(value as string)) {\n    return value as Intl.DateTimeFormatOptions[\"dateStyle\"];\n  }\n  return;\n};\n\nconst timeStyleOrUndefined = (\n  value: unknown\n): Intl.DateTimeFormatOptions[\"timeStyle\"] => {\n  if ([\"full\", \"long\", \"medium\", \"short\"].includes(value as string)) {\n    return value as Intl.DateTimeFormatOptions[\"timeStyle\"];\n  }\n  return;\n};\n\nconst parseDate = (datetimeString: string) => {\n  if (datetimeString === \"\") {\n    return;\n  }\n  let date = new Date(datetimeString);\n\n  // Check if the date already in valid format, e.g. \"2024\"\n  if (Number.isNaN(date.getTime()) === false) {\n    return date;\n  }\n\n  // If its a number, we assume it's a timestamp and we may need to normalize it\n  if (/^\\d+$/.test(datetimeString)) {\n    let timestamp = Number(datetimeString);\n    // Normalize a 10-digit timestamp to 13-digit\n    if (datetimeString.length === 10) {\n      timestamp *= 1000;\n    }\n    date = new Date(timestamp);\n  }\n\n  if (Number.isNaN(date.getTime()) === false) {\n    return date;\n  }\n};\n\n/**\n * Format a date using a template string.\n * Supports tokens: YYYY, YY, MMMM, MMM, MM, M, DDDD, DDD, DD, D, HH, H, mm, m, ss, s\n * Example: formatDate(new Date(), \"YYYY-MM-DD HH:mm:ss\", \"en-US\") => \"2025-11-03 18:47:25\"\n * Example: formatDate(new Date(), \"DDDD, MMMM D, YYYY\", \"en-US\") => \"Monday, November 3, 2025\"\n */\nconst formatDate = (date: Date, template: string, locale = \"en-US\"): string => {\n  const pad = (n: number, length = 2) => String(n).padStart(length, \"0\");\n\n  // Get localized day and month names\n  const longDayName = new Intl.DateTimeFormat(locale, {\n    weekday: \"long\",\n  }).format(date);\n  const shortDayName = new Intl.DateTimeFormat(locale, {\n    weekday: \"short\",\n  }).format(date);\n  const longMonthName = new Intl.DateTimeFormat(locale, {\n    month: \"long\",\n  }).format(date);\n  const shortMonthName = new Intl.DateTimeFormat(locale, {\n    month: \"short\",\n  }).format(date);\n\n  const tokens: Record<string, string | number> = {\n    YYYY: date.getFullYear(),\n    YY: String(date.getFullYear()).slice(-2),\n    MMMM: longMonthName,\n    MMM: shortMonthName,\n    MM: pad(date.getMonth() + 1),\n    M: date.getMonth() + 1,\n    DDDD: longDayName,\n    DDD: shortDayName,\n    DD: pad(date.getDate()),\n    D: date.getDate(),\n    HH: pad(date.getHours()),\n    H: date.getHours(),\n    mm: pad(date.getMinutes()),\n    m: date.getMinutes(),\n    ss: pad(date.getSeconds()),\n    s: date.getSeconds(),\n  };\n\n  // Sort tokens by length (longest first) to avoid partial replacements\n  // e.g., replace MMMM before MMM, DDDD before DDD\n  const sortedTokens = Object.keys(tokens).sort((a, b) => b.length - a.length);\n  const pattern = new RegExp(`\\\\b(${sortedTokens.join(\"|\")})\\\\b`, \"g\");\n\n  return template.replace(pattern, (match) => String(tokens[match]));\n};\n\ntype TimeProps = Pick<ComponentProps<\"time\">, \"dateTime\"> & {\n  language?: Language;\n  country?: Country;\n  dateStyle?: DateStyle;\n  timeStyle?: TimeStyle;\n  /**\n   * Custom format template. Overrides Date Style and Time Style.\n   *\n   * Tokens: YYYY/YY (year), MMMM/MMM/MM/M (month), DDDD/DDD/DD/D (day), HH/H (hours), mm/m (minutes), ss/s (seconds)\n   *\n   * Examples:\n   * - \"YYYY-MM-DD\" → 2025-11-03\n   * - \"DDDD, MMMM D\" → Monday, November 3\n   * - \"DDD, D. MMM YYYY\" → Mon, 3. Nov 2025\n   *\n   * Day and month names use the selected language.\n   */\n  format?: string;\n};\n\nexport const Time = forwardRef<ElementRef<\"time\">, TimeProps>(\n  (\n    {\n      language = DEFAULT_LANGUAGE,\n      country = DEFAULT_COUNTRY,\n      dateStyle = DEFAULT_DATE_STYLE,\n      timeStyle = DEFAULT_TIME_STYLE,\n      format,\n      // native html attribute in react style\n      dateTime = INITIAL_DATE_STRING,\n      ...props\n    },\n    ref\n  ) => {\n    const locale = `${languageOrDefault(language)}-${countryOrDefault(\n      country\n    )}`;\n\n    const options: Intl.DateTimeFormatOptions = {\n      dateStyle: dateStyleOrUndefined(dateStyle),\n      timeStyle: timeStyleOrUndefined(timeStyle),\n    };\n\n    const datetimeString =\n      dateTime === null ? INVALID_DATE_STRING : dateTime.toString();\n\n    const date = parseDate(datetimeString);\n    let formattedDate = datetimeString;\n\n    if (date) {\n      // Use custom format template if provided\n      if (format) {\n        try {\n          formattedDate = formatDate(date, format, locale);\n        } catch {\n          /* Do Nothing */\n        }\n      } else {\n        // Otherwise use Intl.DateTimeFormat\n        try {\n          formattedDate = new Intl.DateTimeFormat(locale, options).format(date);\n        } catch {\n          /* Do Nothing */\n        }\n      }\n    }\n\n    return (\n      <time ref={ref} dateTime={datetimeString} {...props}>\n        {formattedDate}\n      </time>\n    );\n  }\n);\n\nexport const __testing__ = {\n  parseDate,\n  formatDate,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/time.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { time } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/time.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"localization\",\n  description:\n    \"Converts machine-readable date and time to a human-readable format.\",\n  contentModel: {\n    category: \"instance\",\n    children: [],\n  },\n  presetStyle: {\n    time,\n  },\n  initialProps: [\n    \"datetime\",\n    \"language\",\n    \"country\",\n    \"dateStyle\",\n    \"timeStyle\",\n    \"format\",\n  ],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/video.tsx",
    "content": "import {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useEffect,\n  useId,\n  useContext,\n} from \"react\";\nimport type { Atom } from \"nanostores\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const defaultTag = \"video\";\n\n// To avoid composing refs\nconst videoIdAttribute = \"data-ws-video-id\";\n\nconst READY_STATE = {\n  HAVE_NOTHING: 0,\n  HAVE_METADATA: 1,\n  HAVE_CURRENT_DATA: 2,\n  HAVE_FUTURE_DATA: 3,\n  HAVE_ENOUGH_DATA: 4,\n};\n\nexport const Video = forwardRef<\n  ElementRef<typeof defaultTag>,\n  ComponentProps<typeof defaultTag> & {\n    $progress?: Atom<number | undefined>;\n    $visible?: Atom<boolean>;\n    $timeline?: boolean;\n  } & {\n    $webstudio$canvasOnly$assetId?: string | undefined;\n  }\n>(\n  (\n    {\n      $progress,\n      $visible,\n      $timeline,\n      $webstudio$canvasOnly$assetId: _,\n      children,\n      src: srcProp,\n      ...props\n    },\n    ref\n  ) => {\n    const id = useId();\n    const videoIdProps = {\n      [videoIdAttribute]: id,\n    };\n    const { videoLoader } = useContext(ReactSdkContext);\n\n    // fallback to provided src\n    const src =\n      srcProp && videoLoader ? videoLoader({ src: srcProp }) : srcProp;\n\n    useEffect(() => {\n      if ($progress === undefined) {\n        return;\n      }\n\n      if ($visible === undefined) {\n        return;\n      }\n\n      const video = document.querySelector(`[${videoIdAttribute}=\"${id}\"]`);\n\n      if (video === null) {\n        return;\n      }\n\n      if (false === video instanceof HTMLVideoElement) {\n        return;\n      }\n\n      // Safari IOS does not seek video without play called at least once\n      // this is in case autoPlay is not set\n      video.play().catch(() => {\n        /**/\n      });\n      video.pause();\n\n      if ($timeline) {\n        return $progress.subscribe((progress) => {\n          if (video.readyState < READY_STATE.HAVE_METADATA) {\n            return;\n          }\n\n          if (!video.paused) {\n            video.pause();\n          }\n\n          if (video.seeking) {\n            return;\n          }\n\n          let duration = video.duration;\n\n          if (Number.isNaN(duration)) {\n            return;\n          }\n\n          if (!Number.isFinite(duration)) {\n            // Set to 60s on streaming videos\n            duration = 60;\n          }\n\n          video.currentTime = (progress ?? 0) * duration;\n        });\n      }\n\n      let isPlaying = false;\n      let isVisible = false;\n\n      const unsubscribeVisible = $visible.subscribe((visible) => {\n        isVisible = visible;\n\n        // Seek video only if it's invisible to avoid jumps\n        if (isVisible === false && isPlaying === false && !video.loop) {\n          video.currentTime = 0;\n        }\n      });\n\n      const unsubscribeProgress = $progress.subscribe((progress) => {\n        if (\n          isPlaying &&\n          (progress === undefined || progress === 0 || progress === 1)\n        ) {\n          isPlaying = false;\n          video.pause();\n\n          // Seek video only if it's invisible to avoid jumps\n          if (isVisible === false && isPlaying === false && !video.loop) {\n            video.currentTime = 0;\n          }\n\n          return;\n        }\n\n        if (!isPlaying) {\n          isPlaying = true;\n          if (!video.ended) {\n            video.play().catch(() => {\n              /**/\n            });\n          }\n        }\n      });\n\n      return () => {\n        unsubscribeProgress();\n        unsubscribeVisible();\n      };\n    }, [$progress, $timeline, $visible, id]);\n\n    return (\n      <video src={src} {...props} {...videoIdProps} ref={ref}>\n        {children}\n      </video>\n    );\n  }\n);\n\nVideo.displayName = \"Video\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/video.ws.ts",
    "content": "import { VideoIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\n\nimport { props } from \"./__generated__/video.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: VideoIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [],\n  },\n  presetStyle: {\n    video: [\n      {\n        property: \"max-width\",\n        value: { type: \"unit\", unit: \"%\", value: 100 },\n      },\n    ],\n  },\n  initialProps: [\n    \"id\",\n    \"class\",\n    \"width\",\n    \"height\",\n    \"src\",\n    \"autoPlay\",\n    \"controls\",\n    \"loop\",\n    \"muted\",\n    \"preload\",\n    \"playsInline\",\n  ],\n  props: {\n    ...props,\n    // Automatically generated props don't have the right control.\n    src: {\n      type: \"string\",\n      control: \"file\",\n      label: \"Source\",\n      required: false,\n      accept: \".mp4,.webm,.mpg,.mpeg,.mov\",\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo-play-button.tsx",
    "content": "import {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n  useCallback,\n} from \"react\";\nimport interactionResponse from \"await-interaction-response\";\nimport { Button, defaultTag } from \"./button\";\nimport { VideoContext } from \"./shared/video\";\n\nexport { defaultTag };\n\ntype Props = ComponentProps<typeof Button>;\n\nexport const VimeoPlayButton = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  (props, ref) => {\n    const videoContext = useContext(VideoContext);\n\n    const handleClick = useCallback(async () => {\n      await interactionResponse();\n      videoContext.onInitPlayer();\n    }, [videoContext]);\n\n    if (videoContext.status !== \"initial\") {\n      return;\n    }\n\n    return <Button {...props} onClick={handleClick} ref={ref} />;\n  }\n);\n\nVimeoPlayButton.displayName = \"VimeoPlayButton\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo-play-button.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { ButtonElementIcon } from \"@webstudio-is/icons/svg\";\nimport { button } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/vimeo-play-button.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"hidden\",\n  label: \"Play Button\",\n  icon: ButtonElementIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { button },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo-preview-image.tsx",
    "content": "import {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n} from \"react\";\nimport { Image, defaultTag } from \"./image\";\nimport { VideoContext } from \"./shared/video\";\n\n// Generated using https://png-pixel.com/\nconst base64Preview = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkOAMAANIAzr59FiYAAAAASUVORK5CYII=`;\n\ntype Props = ComponentProps<typeof Image>;\n\nexport const VimeoPreviewImage = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Props\n>(({ src, ...rest }, ref) => {\n  const videoContext = useContext(VideoContext);\n\n  return (\n    <Image\n      {...rest}\n      src={String(videoContext.previewImageUrl ?? src ?? base64Preview)}\n      ref={ref}\n    />\n  );\n});\n\nVimeoPreviewImage.displayName = \"VimeoPreviewImage\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo-preview-image.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { meta as imageMeta } from \"./image.ws\";\nimport { props } from \"./__generated__/vimeo-preview-image.props\";\n\nexport const meta: WsComponentMeta = {\n  ...imageMeta,\n  category: \"hidden\",\n  label: \"Preview Image\",\n  contentModel: {\n    category: \"none\",\n    children: [],\n  },\n  initialProps: imageMeta.initialProps,\n  props: {\n    ...props,\n    // Automatically generated props don't have the right control.\n    src: {\n      type: \"string\",\n      control: \"file\",\n      label: \"Source\",\n      required: false,\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo-spinner.tsx",
    "content": "import {\n  forwardRef,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n} from \"react\";\nimport { VideoContext } from \"./shared/video\";\n\nconst defaultTag = \"div\";\n\ntype Props = ComponentProps<typeof defaultTag>;\n\nexport const VimeoSpinner = forwardRef<ElementRef<typeof defaultTag>, Props>(\n  (props, ref) => {\n    const videoContext = useContext(VideoContext);\n\n    if (videoContext.status !== \"loading\") {\n      return;\n    }\n\n    return <div {...props} ref={ref} />;\n  }\n);\n\nVimeoSpinner.displayName = \"VimeoSpinner\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo-spinner.ws.ts",
    "content": "import type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { BoxIcon } from \"@webstudio-is/icons/svg\";\nimport { props } from \"./__generated__/vimeo-spinner.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: BoxIcon,\n  category: \"hidden\",\n  label: \"Spinner\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { div },\n  initialProps: [\"id\", \"class\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo.template.tsx",
    "content": "import { PlayIcon, SpinnerIcon } from \"@webstudio-is/icons/svg\";\nimport { type TemplateMeta, $, css, ws } from \"@webstudio-is/template\";\n\nexport const meta: TemplateMeta = {\n  category: \"media\",\n  order: 1,\n  description:\n    \"Add a video to your page that is hosted on Vimeo. Paste a Vimeo URL and configure the video in the Settings tab.\",\n  template: (\n    <$.Vimeo\n      ws:style={css`\n        position: relative;\n        aspect-ratio: 640/360;\n        width: 100%;\n      `}\n    >\n      <$.VimeoPreviewImage\n        ws:style={css`\n          position: absolute;\n          object-fit: cover;\n          object-position: center;\n          width: 100%;\n          height: 100%;\n          border-radius: 20px;\n        `}\n        alt=\"Vimeo video preview image\"\n        sizes=\"100vw\"\n        optimize={true}\n      />\n      <$.VimeoSpinner\n        ws:label=\"Spinner\"\n        ws:style={css`\n          position: absolute;\n          top: 50%;\n          left: 50%;\n          width: 70px;\n          height: 70px;\n          margin-top: -35px;\n          margin-left: -35px;\n        `}\n      >\n        <$.HtmlEmbed ws:label=\"Spinner SVG\" code={SpinnerIcon} />\n      </$.VimeoSpinner>\n      <$.VimeoPlayButton\n        ws:style={css`\n          position: absolute;\n          width: 140px;\n          height: 80px;\n          top: 50%;\n          left: 50%;\n          margin-top: -40px;\n          margin-left: -70px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          border-style: none;\n          border-radius: 5px;\n          cursor: pointer;\n          background-color: rgb(18, 18, 18);\n          color: rgb(255, 255, 255);\n          &:hover {\n            background-color: rgb(0, 173, 239);\n          }\n        `}\n        aria-label=\"Play button\"\n      >\n        <ws.element\n          ws:tag=\"div\"\n          ws:label=\"Play Icon\"\n          ws:style={css`\n            width: 60px;\n            height: 60px;\n          `}\n          aria-hidden={true}\n        >\n          <$.HtmlEmbed ws:label=\"Play SVG\" code={PlayIcon} />\n        </ws.element>\n      </$.VimeoPlayButton>\n    </$.Vimeo>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo.tsx",
    "content": "// Many implementation ideas came from\n// https://github.com/slightlyoff/lite-vimeo\n// Main reasons to not use it as is:\n// - we don't want to render player by default\n// - we want to expose Webstudio components to the user for customization\n\nimport { colord } from \"colord\";\nimport {\n  type ElementRef,\n  type ComponentProps,\n  type ContextType,\n  forwardRef,\n  useState,\n  useEffect,\n  useContext,\n  useRef,\n} from \"react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  requestFullscreen,\n  VideoContext,\n  type PlayerStatus,\n} from \"./shared/video\";\n\n// https://developer.vimeo.com/player/sdk/embed\ntype VimeoPlayerOptions = {\n  background?: boolean;\n  color?: string;\n  controls?: boolean;\n  dnt?: boolean;\n  interactive_params?: string;\n  title?: boolean;\n  portrait?: boolean;\n  // @todo url type to validate url on the input\n  /** The ID or the URL of the video on Vimeo. You must supply one of these values to identify the video. When the video's privacy setting is Private, you must use the URL, and the URL must include the h parameter. For more information, see Vimeo’s introductory guide. */\n  url?: string;\n  /** Whether to pause the current video when another Vimeo video on the same page starts to play. Set this value to false to permit simultaneous playback of all the videos on the page. This option has no effect if you've disabled cookies in your browser, either through browser settings or with an extension or plugin. */\n  autopause?: boolean;\n  /** Whether to enable the browser to enter picture-in-picture mode automatically when switching tabs or windows, where supported. */\n  autopip?: boolean;\n  /**\n   * Whether to start playback of the video automatically. This feature might not work on all devices.\n   * Some browsers require the `muted` parameter to be set to `true` for autoplay to work.\n   * */\n  autoplay?: boolean;\n  /** Whether to display the video owner's name. */\n  byline?: boolean;\n  /** Whether to enable keyboard input to trigger player events. This setting doesn't affect tab control. */\n  keyboard?: boolean;\n  /** Whether to restart the video automatically after reaching the end. */\n  loop?: boolean;\n  /** Whether the video is muted upon loading. The true value is required for the autoplay behavior in some browsers. */\n  muted?: boolean;\n  /** Whether to include the picture-in-picture button among the player controls and enable the picture-in-picture API. */\n  pip?: boolean;\n  /** Whether the video plays inline on supported mobile devices. To force the device to play the video in fullscreen mode instead, set this value to false. */\n  playsinline?: boolean;\n  /** For videos on a Vimeo Plus account or higher: the playback quality of the video. Use auto for the best possible quality given available bandwidth and other factors. You can also specify 360p, 540p, 720p, 1080p, 2k, and 4k. */\n  quality?: \"auto\" | \"360p\" | \"540p\" | \"720p\" | \"1080p\" | \"2k\" | \"4k\";\n  /** Whether to return a responsive embed code, or one that provides intelligent adjustments based on viewing conditions. We recommend this option for mobile-optimized sites. */\n  responsive?: boolean;\n  /** Whether the player displays speed controls in the preferences menu and enables the playback rate API. */\n  speed?: boolean;\n  /**\n   * The text track to display with the video. Specify the text track by its language code (en), the language code and locale (en-US), or the language code and kind (en.captions). For this argument to work, the video must already have a text track of the given type; see our Help Center or Working with Text Track Uploads for more information.\n   * To enable automatically generated closed captions instead, provide the value en-x-autogen. Please note that, at the present time, automatic captions are always in English.\n   */\n  texttrack?: string;\n  /** Whether the responsive player and transparent background are enabled. */\n  transparent?: boolean;\n};\n\nconst getVideoUrl = (options: VimeoOptions) => {\n  if (options.url === undefined) {\n    return;\n  }\n  let url;\n  try {\n    const userUrl = new URL(options.url);\n    url = new URL(IFRAME_CDN);\n    url.pathname = `/video${userUrl.pathname}`;\n    // eslint-disable-next-line no-empty\n  } catch {}\n\n  if (url === undefined) {\n    return;\n  }\n\n  const optionsMap = {\n    showPortrait: \"portrait\",\n    showByline: \"byline\",\n    showTitle: \"title\",\n    controlsColor: \"color\",\n    showControls: \"controls\",\n    interactiveParams: \"interactive_params\",\n    backgroundMode: \"background\",\n    doNotTrack: \"dnt\",\n  } as const;\n\n  let option: keyof VimeoOptions;\n  for (option in options) {\n    const value = options[option];\n    if (option === \"url\" || value === undefined) {\n      continue;\n    }\n    const mappedOption =\n      optionsMap[option as keyof typeof optionsMap] ?? option;\n    url.searchParams.append(mappedOption, value.toString());\n  }\n\n  // We always set autoplay to true because we render the iframe only after user hits Webstudio play button.\n  url.searchParams.set(\"autoplay\", \"true\");\n\n  // Vimeo needs a hex color value without the hash\n  if (typeof options.controlsColor === \"string\") {\n    const color = colord(options.controlsColor).toHex().replace(\"#\", \"\");\n    url.searchParams.set(\"color\", color);\n  }\n\n  // Portrait option won't work if at title is not set to true\n  if (options.showPortrait) {\n    url.searchParams.set(\"title\", \"true\");\n  }\n  // Byline won't show up if portrait and title is not set to true\n  if (options.showByline) {\n    url.searchParams.set(\"portrait\", \"true\");\n    url.searchParams.set(\"title\", \"true\");\n  }\n\n  return url.toString();\n};\n\nconst preconnect = (url: string) => {\n  const link = document.createElement(\"link\");\n  link.rel = \"preconnect\";\n  link.href = url;\n  link.crossOrigin = \"true\";\n  document.head.appendChild(link);\n};\n\nlet warmed = false;\n\n// Host that Vimeo uses to serve JS needed by player\nconst PLAYER_CDN = \"https://f.vimeocdn.com\";\n// The iframe document comes from player.vimeo.com\nconst IFRAME_CDN = \"https://player.vimeo.com\";\n// Image for placeholder comes from i.vimeocdn.com\nconst IMAGE_CDN = \"https://i.vimeocdn.com\";\n\nconst warmConnections = () => {\n  if (warmed) {\n    return;\n  }\n\n  if (window.matchMedia(\"(hover: none)\").matches) {\n    // Useless on touch devices\n    return;\n  }\n\n  preconnect(PLAYER_CDN);\n  preconnect(IFRAME_CDN);\n  preconnect(IMAGE_CDN);\n  warmed = true;\n};\n\nconst getVideoId = (url: string) => {\n  try {\n    const parsedUrl = new URL(url);\n    const id = parsedUrl.pathname.split(\"/\")[2];\n    if (id === \"\" || id == null) {\n      return;\n    }\n    return id;\n    // eslint-disable-next-line no-empty\n  } catch {}\n};\n\nconst loadPreviewImageUrl = async (videoUrl: string) => {\n  const videoId = getVideoId(videoUrl);\n  // API is the video-id based\n  // http://vimeo.com/api/v2/video/364402896.json\n  const apiUrl = `https://vimeo.com/api/v2/video/${videoId}.json`;\n\n  // Now fetch the JSON that locates our placeholder from vimeo's JSON API\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const response = ((await (await fetch(apiUrl)).json()) as any)[0];\n\n  // Extract the image id, e.g. 819916979, from a URL like:\n  // thumbnail_large: \"https://i.vimeocdn.com/video/819916979_640.jpg\"\n  const thumbnail = response.thumbnail_large;\n  const imgId = thumbnail.substr(thumbnail.lastIndexOf(\"/\") + 1).split(\"_\")[0];\n\n  const imageUrl = new URL(IMAGE_CDN);\n  imageUrl.pathname = `/video/${imgId}.webp`;\n  imageUrl.searchParams.append(\"mw\", \"1100\");\n  imageUrl.searchParams.append(\"mh\", \"619\");\n  imageUrl.searchParams.append(\"q\", \"70\");\n  return imageUrl;\n};\n\nexport type VimeoOptions = Omit<\n  VimeoPlayerOptions,\n  | \"dnt\"\n  | \"interactive_params\"\n  | \"background\"\n  | \"controls\"\n  | \"color\"\n  | \"byline\"\n  | \"title\"\n  | \"portrait\"\n> & {\n  /** Not a Vimeo attribute: Whether the preview image should be loaded from Vimeo API. Ideally don't use it, because it will show up with some delay and will make your project feel slower. */\n  showPreview?: boolean;\n  /** Not a Vimeo attribute: Loading attribute for the iframe allows to eager or lazy load the source */\n  loading?: \"eager\" | \"lazy\";\n  /** Whether to prevent the player from tracking session data, including cookies. Keep in mind that setting this argument to true also blocks video stats. */\n  doNotTrack?: VimeoPlayerOptions[\"dnt\"];\n  /** Key-value pairs representing dynamic parameters that are utilized on interactive videos with live elements, such as title=my-video,subtitle=interactive. */\n  interactiveParams?: VimeoPlayerOptions[\"interactive_params\"];\n  /** Whether the player is in background mode, which hides the playback controls, enables autoplay, and loops the video. */\n  backgroundMode?: VimeoPlayerOptions[\"background\"];\n  /** Whether to display the player's interactive elements, including the play bar and sharing buttons. Set this option to false for a chromeless experience. To control playback when the play/pause button is hidden, set autoplay to true, use keyboard controls (which remain active), or implement our player SDK. */\n  showControls?: VimeoPlayerOptions[\"controls\"];\n  // @todo use color type to use color control\n  /** A color value of the playback controls, which is normally #00ADEF. The embed settings of the video might override this value. */\n  controlsColor?: VimeoPlayerOptions[\"color\"];\n  /** Whether to display the video owner's name. */\n  showByline?: VimeoPlayerOptions[\"byline\"];\n  /** Whether the player displays the title overlay. */\n  showTitle?: VimeoPlayerOptions[\"title\"];\n  /** Whether to display the video owner's portrait. Only works if either title or byline are also enabled */\n  showPortrait?: VimeoPlayerOptions[\"portrait\"];\n};\n\nconst EmptyState = () => {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        width: \"100%\",\n        height: \"100%\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        fontSize: \"1.2em\",\n      }}\n    >\n      {\n        'Open the \"Settings\" panel and paste a video URL, e.g. https://vimeo.com/831343124.'\n      }\n    </div>\n  );\n};\n\ntype PlayerProps = Pick<\n  VimeoOptions,\n  \"loading\" | \"autoplay\" | \"showPreview\" | \"playsinline\"\n> & {\n  videoUrl: string;\n  title: string | undefined;\n  status: PlayerStatus;\n  renderer: ContextType<typeof ReactSdkContext>[\"renderer\"];\n  previewImageUrl?: URL;\n  onStatusChange: (status: PlayerStatus) => void;\n  onPreviewImageUrlChange: (url?: URL) => void;\n};\n\nconst Player = ({\n  title,\n  status,\n  loading,\n  videoUrl,\n  previewImageUrl,\n  autoplay,\n  renderer,\n  showPreview,\n  playsinline,\n  onStatusChange,\n  onPreviewImageUrlChange,\n}: PlayerProps) => {\n  const [opacity, setOpacity] = useState(0);\n  const ref = useRef<HTMLIFrameElement>(null);\n\n  useEffect(() => {\n    if (autoplay && renderer !== \"canvas\" && status === \"initial\") {\n      onStatusChange(\"loading\");\n    }\n  }, [autoplay, status, renderer, onStatusChange]);\n\n  useEffect(() => {\n    if (renderer !== \"canvas\") {\n      warmConnections();\n    }\n  }, [renderer]);\n\n  useEffect(() => {\n    if (videoUrl === undefined) {\n      return;\n    }\n\n    if (showPreview === false) {\n      onPreviewImageUrlChange(undefined);\n      return;\n    }\n\n    if (previewImageUrl === undefined) {\n      loadPreviewImageUrl(videoUrl)\n        .then(onPreviewImageUrlChange)\n        .catch(() => {\n          console.error(`Could not load preview image for ${videoUrl}`);\n        });\n    }\n  }, [onPreviewImageUrlChange, showPreview, videoUrl, previewImageUrl]);\n\n  if (renderer === \"canvas\" || status === \"initial\") {\n    return;\n  }\n\n  return (\n    <iframe\n      ref={ref}\n      title={title}\n      src={videoUrl}\n      loading={loading}\n      allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture;\"\n      allowFullScreen\n      style={{\n        position: \"absolute\",\n        width: \"100%\",\n        height: \"100%\",\n        opacity,\n        transition: \"opacity 1s\",\n        border: \"none\",\n      }}\n      onLoad={() => {\n        onStatusChange(\"ready\");\n        setOpacity(1);\n        if (ref.current && !playsinline && !autoplay) {\n          requestFullscreen(ref.current);\n        }\n      }}\n    />\n  );\n};\n\nconst defaultTag = \"div\";\n\ntype Props = Omit<ComponentProps<typeof defaultTag>, keyof VimeoOptions> &\n  VimeoOptions & {\n    /**\n     * The `title` attribute for the iframe.\n     * Improves accessibility by providing a brief description of the video content for screen readers.\n     * Example: \"Video about web development tips\".\n     */\n    title?: string | undefined;\n  };\ntype Ref = ElementRef<typeof defaultTag>;\n\nexport const Vimeo = forwardRef<Ref, Props>(\n  (\n    {\n      url,\n      loading = \"lazy\",\n      autoplay = false,\n      autopause = true,\n      showByline = false,\n      showControls = true,\n      doNotTrack = false,\n      keyboard = true,\n      loop = false,\n      muted = false,\n      pip = false,\n      playsinline = false,\n      showPortrait = true,\n      quality = \"auto\",\n      responsive = true,\n      speed = false,\n      showTitle = false,\n      transparent = true,\n      showPreview = false,\n      autopip,\n      controlsColor,\n      interactiveParams,\n      texttrack,\n      children,\n      ...rest\n    },\n    ref\n  ) => {\n    const [status, setStatus] = useState<PlayerStatus>(\"initial\");\n    const [previewImageUrl, setPreviewImageUrl] = useState<URL>();\n    const { renderer } = useContext(ReactSdkContext);\n    const videoUrl = getVideoUrl({\n      url,\n      autoplay,\n      autopause,\n      showControls,\n      controlsColor,\n      doNotTrack,\n      interactiveParams,\n      keyboard,\n      loop,\n      muted,\n      pip,\n      playsinline,\n      quality,\n      responsive,\n      speed,\n      texttrack,\n      showTitle,\n      transparent,\n      showPortrait,\n      autopip,\n    });\n\n    return (\n      <VideoContext.Provider\n        value={{\n          status,\n          previewImageUrl,\n          onInitPlayer() {\n            if (renderer !== \"canvas\") {\n              setStatus(\"loading\");\n            }\n          },\n        }}\n      >\n        <div\n          {...rest}\n          ref={(value: Ref) => {\n            if (ref !== null) {\n              typeof ref === \"function\" ? ref(value) : (ref.current = value);\n            }\n          }}\n        >\n          {videoUrl === undefined ? (\n            <EmptyState />\n          ) : (\n            <>\n              {children}\n              <Player\n                title={rest.title}\n                autoplay={autoplay}\n                playsinline={playsinline}\n                videoUrl={videoUrl}\n                previewImageUrl={previewImageUrl}\n                loading={loading}\n                showPreview={showPreview}\n                renderer={renderer}\n                status={status}\n                onStatusChange={setStatus}\n                onPreviewImageUrlChange={setPreviewImageUrl}\n              />\n            </>\n          )}\n        </div>\n      </VideoContext.Provider>\n    );\n  }\n);\n\nVimeo.displayName = \"Vimeo\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/vimeo.ws.ts",
    "content": "import type { ComponentProps } from \"react\";\nimport { VimeoIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/vimeo.props\";\nimport type { Vimeo } from \"./vimeo\";\n\nconst initialProps: Array<keyof ComponentProps<typeof Vimeo>> = [\n  \"id\",\n  \"className\",\n  \"url\",\n  \"title\",\n  \"quality\",\n  \"loading\",\n  \"showPreview\",\n  \"autoplay\",\n  \"doNotTrack\",\n  \"loop\",\n  \"muted\",\n  \"showPortrait\",\n  \"showByline\",\n  \"showTitle\",\n  \"showControls\",\n  \"controlsColor\",\n  \"playsinline\",\n];\n\nexport const meta: WsComponentMeta = {\n  icon: VimeoIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [\"VimeoSpinner\", \"VimeoPlayButton\", \"VimeoPreviewImage\"],\n  },\n  presetStyle: { div },\n  initialProps,\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/webhook-form.template.tsx",
    "content": "import {\n  $,\n  ws,\n  ActionValue,\n  css,\n  expression,\n  PlaceholderValue,\n  Variable,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\n\nconst formState = new Variable(\"formState\", \"initial\");\n\nexport const meta: TemplateMeta = {\n  category: \"data\",\n  order: 1,\n  description: \"Collect user data and send it to any webhook.\",\n  template: (\n    <$.Form\n      state={expression`${formState}`}\n      onStateChange={\n        new ActionValue([\"state\"], expression`${formState} = state`)\n      }\n    >\n      <ws.element\n        ws:tag=\"div\"\n        ws:label=\"Form Content\"\n        ws:show={expression`${formState} === 'initial' || ${formState} === 'error'`}\n      >\n        <ws.element\n          ws:tag=\"label\"\n          ws:style={css`\n            display: block;\n          `}\n        >\n          {new PlaceholderValue(\"Name\")}\n        </ws.element>\n        <ws.element\n          ws:tag=\"input\"\n          ws:style={css`\n            display: block;\n          `}\n          name=\"name\"\n        />\n        <ws.element\n          ws:tag=\"label\"\n          ws:style={css`\n            display: block;\n          `}\n        >\n          {new PlaceholderValue(\"Email\")}\n        </ws.element>\n        <ws.element\n          ws:tag=\"input\"\n          ws:style={css`\n            display: block;\n          `}\n          name=\"email\"\n        />\n        <ws.element ws:tag=\"button\">\n          {new PlaceholderValue(\"Submit\")}\n        </ws.element>\n      </ws.element>\n      <ws.element\n        ws:tag=\"div\"\n        ws:label=\"Success Message\"\n        ws:show={expression`${formState} === 'success'`}\n      >\n        {new PlaceholderValue(\"Thank you for getting in touch!\")}\n      </ws.element>\n      <ws.element\n        ws:tag=\"div\"\n        ws:label=\"Error Message\"\n        ws:show={expression`${formState} === 'error'`}\n      >\n        {new PlaceholderValue(\"Sorry, something went wrong.\")}\n      </ws.element>\n    </$.Form>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/webhook-form.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\n\ntype Props = ComponentProps<\"form\"> & {\n  encType?:\n    | \"application/x-www-form-urlencoded\"\n    | \"multipart/form-data\"\n    | \"text/plain\";\n  /** Use this property to reveal the Success and Error states on the canvas so they can be styled. The Initial state is displayed when the page first opens. The Success and Error states are displayed depending on whether the Form submits successfully or unsuccessfully. */\n  state?: \"initial\" | \"success\" | \"error\";\n  onStateChange: (state: \"initial\" | \"success\" | \"error\") => void;\n};\n\nexport const WebhookForm = forwardRef<ElementRef<\"form\">, Props>(\n  ({ children, state = \"initial\", ...props }, ref) => (\n    <form {...props} ref={ref}>\n      {children}\n    </form>\n  )\n);\n\nWebhookForm.displayName = \"WebhookForm\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/webhook-form.ws.ts",
    "content": "import { WebhookFormIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { form } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/webhook-form.props\";\n\nexport const meta: WsComponentMeta = {\n  label: \"Webhook Form\",\n  icon: WebhookFormIcon,\n  presetStyle: {\n    form,\n  },\n  states: [\n    { selector: \"[data-state=error]\", label: \"Error\" },\n    { selector: \"[data-state=success]\", label: \"Success\" },\n  ],\n  initialProps: [\"id\", \"class\", \"state\", \"action\"],\n  props: {\n    ...props,\n    action: {\n      type: \"resource\",\n      control: \"resource\",\n      description:\n        \"The URI of a program that processes the information submitted via the form.\",\n      required: false,\n    },\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/xml-node.stories.tsx",
    "content": "import { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport { StorySection } from \"@webstudio-is/design-system\";\nimport { XmlNode } from \"./xml-node\";\n\nconst Component = () => {\n  return (\n    <ReactSdkContext.Provider\n      value={{\n        renderer: \"canvas\",\n        assetBaseUrl: \"/\",\n        imageLoader: ({ src }) => src,\n        resources: {},\n        breakpoints: [],\n        onError: console.error,\n      }}\n    >\n      <XmlNode tag=\"root\">\n        <XmlNode tag=\"hello\" rel=\"hihi\" hreflang=\"joajoajoaja aokaoja aojaoj\">\n          Hi All Hi All Hi All Hi AllHi AllHi All Hi AllHi AllHi All\n        </XmlNode>\n        <XmlNode tag=\"hello\" rel=\"hihi\"></XmlNode>\n      </XmlNode>\n    </ReactSdkContext.Provider>\n  );\n};\n\nexport default {\n  title: \"XML node\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <StorySection title=\"Xml Node\">\n        <Component />\n      </StorySection>\n    );\n  },\n};\n\nexport { Story as XmlNode };\n"
  },
  {
    "path": "packages/sdk-components-react/src/xml-node.tsx",
    "content": "import {\n  ReactSdkContext,\n  xmlNodeTagSuffix,\n} from \"@webstudio-is/react-sdk/runtime\";\nimport {\n  Children,\n  createElement,\n  forwardRef,\n  useContext,\n  type ElementRef,\n  type ReactNode,\n} from \"react\";\n\nexport const defaultTag = \"div\";\n\n// We don't want to enable all tags because Box is usually a container and we have specific components for many tags.\ntype Props = {\n  tag: string;\n  xmlns?: string;\n  children?: ReactNode;\n  rel?: string;\n  hreflang?: string;\n  href?: string;\n  \"xmlns:xhtml\"?: string;\n};\n\nexport const XmlNode = forwardRef<ElementRef<\"div\">, Props>(\n  ({ tag = \"\", children, ...props }, ref) => {\n    const { renderer } = useContext(ReactSdkContext);\n\n    const attributeEntries = Object.entries(props)\n      .filter(\n        ([key]) =>\n          key.startsWith(\"data-\") === false && key.startsWith(\"aria-\") === false\n      )\n      .filter(([key]) => key.toLowerCase() !== \"tabindex\")\n      .filter(([, value]) => typeof value !== \"function\");\n\n    const elementName = tag\n      // Must start from letter or underscore\n      .replace(/^[^\\p{L}_]+/u, \"\")\n      // Clear all non letter, number, underscore, dot, and dash\n      .replaceAll(/[^\\p{L}\\p{N}\\-._:]+/gu, \"\")\n      .trim();\n\n    if (renderer === undefined) {\n      const attrProps = Object.fromEntries(attributeEntries);\n      return createElement(\n        `${elementName}${xmlNodeTagSuffix}`,\n        attrProps,\n        children\n      );\n    }\n\n    const childrenArray = Children.toArray(children);\n    const isTextChild =\n      childrenArray.length > 0 &&\n      childrenArray.every((child) => typeof child === \"string\");\n\n    const renderAttributes = (attrs: [string, string][]) => {\n      return attrs.map(([name, value], index) => {\n        return (\n          <span key={index}>\n            {\" \"}\n            <span style={{ color: \"#FF0000\" }}>{name}</span>\n            <span style={{ color: \"#000000\" }}>=</span>\n            <span style={{ color: \"#0000FF\" }}>\"{value}\"</span>\n          </span>\n        );\n      });\n    };\n\n    return (\n      <div {...props} style={{ backgroundColor: \"rgba(255,255,255,1)\" }}>\n        <span>\n          <span style={{ color: \"#800000\" }}>&lt;{elementName}</span>\n          {attributeEntries.length > 0 && renderAttributes(attributeEntries)}\n          {childrenArray.length === 0 ? (\n            <span style={{ color: \"#800000\" }}>/&gt;</span>\n          ) : (\n            <span style={{ color: \"#800000\" }}>&gt;</span>\n          )}\n        </span>\n        {childrenArray.length > 0 && (\n          <>\n            <div\n              ref={ref}\n              style={{\n                display: isTextChild ? \"inline\" : \"block\",\n                marginLeft: isTextChild ? 0 : \"1rem\",\n                color: \"#000000\",\n              }}\n            >\n              {children}\n            </div>\n            <span style={{ color: \"#800000\" }}>&lt;/{elementName}&gt;</span>\n          </>\n        )}\n      </div>\n    );\n  }\n);\n\nXmlNode.displayName = \"XmlNode\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/xml-node.ws.ts",
    "content": "import { XmlIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/xml-node.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"xml\",\n  order: 6,\n  icon: XmlIcon,\n  description: \"XML Node\",\n  initialProps: [\"tag\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/xml-time.tsx",
    "content": "import { forwardRef, useContext, type ElementRef } from \"react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\n\nconst DEFAULT_DATE_STYLE = \"short\";\nconst INITIAL_DATE_STRING = \"dateTime attribute is not set\";\nconst INVALID_DATE_STRING = \"\";\n\ntype XmlTimeProps = {\n  dateStyle?: \"long\" | \"short\";\n  datetime: string;\n};\n\nconst parseDate = (datetimeString: string) => {\n  if (datetimeString === \"\") {\n    return;\n  }\n  let date = new Date(datetimeString);\n\n  // Check if the date already in valid format, e.g. \"2024\"\n  if (Number.isNaN(date.getTime()) === false) {\n    return date;\n  }\n\n  // If its a number, we assume it's a timestamp and we may need to normalize it\n  if (/^\\d+$/.test(datetimeString)) {\n    let timestamp = Number(datetimeString);\n    // Normalize a 10-digit timestamp to 13-digit\n    if (datetimeString.length === 10) {\n      timestamp *= 1000;\n    }\n    date = new Date(timestamp);\n  }\n\n  if (Number.isNaN(date.getTime()) === false) {\n    return date;\n  }\n};\n\nexport const XmlTime = forwardRef<ElementRef<\"time\">, XmlTimeProps>(\n  ({ dateStyle = DEFAULT_DATE_STYLE, datetime = INITIAL_DATE_STRING }, ref) => {\n    const { renderer } = useContext(ReactSdkContext);\n\n    const datetimeString =\n      datetime === null ? INVALID_DATE_STRING : datetime.toString();\n\n    const date = parseDate(datetimeString);\n\n    let formattedDate = datetimeString;\n    if (date) {\n      formattedDate = date.toISOString();\n      if (dateStyle === \"short\") {\n        formattedDate = formattedDate.split(\"T\")[0];\n      }\n    }\n\n    if (renderer === undefined) {\n      return formattedDate;\n    }\n\n    return <time ref={ref}>{formattedDate}</time>;\n  }\n);\n"
  },
  {
    "path": "packages/sdk-components-react/src/xml-time.ws.ts",
    "content": "import { CalendarIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { props } from \"./__generated__/xml-time.props\";\n\nexport const meta: WsComponentMeta = {\n  category: \"xml\",\n  description: \"Converts machine-readable date and time to ISO format.\",\n  icon: CalendarIcon,\n  order: 7,\n  initialProps: [\"datetime\", \"dateStyle\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/youtube.template.tsx",
    "content": "import { PlayIcon, SpinnerIcon } from \"@webstudio-is/icons/svg\";\nimport { type TemplateMeta, $, css, ws } from \"@webstudio-is/template\";\n\nexport const meta: TemplateMeta = {\n  label: \"YouTube\",\n  category: \"media\",\n  order: 1,\n  description:\n    \"Add a video to your page that is hosted on YouTube. Paste a YouTube URL and configure the video in the Settings tab.\",\n  template: (\n    <$.YouTube\n      ws:label=\"YouTube\"\n      ws:style={css`\n        position: relative;\n        aspect-ratio: 640/360;\n        width: 100%;\n      `}\n    >\n      <$.VimeoPreviewImage\n        ws:label=\"Preview Image\"\n        ws:style={css`\n          position: absolute;\n          object-fit: cover;\n          object-position: center;\n          width: 100%;\n          height: 100%;\n          border-radius: 20px;\n        `}\n        alt=\"YouTube video preview image\"\n        sizes=\"100vw\"\n        optimize={true}\n      />\n      <$.VimeoSpinner\n        ws:label=\"Spinner\"\n        ws:style={css`\n          position: absolute;\n          top: 50%;\n          left: 50%;\n          width: 70px;\n          height: 70px;\n          margin-top: -35px;\n          margin-left: -35px;\n        `}\n      >\n        <$.HtmlEmbed ws:label=\"Spinner SVG\" code={SpinnerIcon} />\n      </$.VimeoSpinner>\n      <$.VimeoPlayButton\n        ws:label=\"Play Button\"\n        ws:style={css`\n          position: absolute;\n          width: 140px;\n          height: 80px;\n          top: 50%;\n          left: 50%;\n          margin-top: -40px;\n          margin-left: -70px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          border-style: none;\n          border-radius: 5px;\n          cursor: pointer;\n          background-color: rgb(18, 18, 18);\n          color: rgb(255, 255, 255);\n          &:hover {\n            background-color: rgb(0, 173, 239);\n          }\n        `}\n        aria-label=\"Play button\"\n      >\n        <ws.element\n          ws:tag=\"div\"\n          ws:label=\"Play Icon\"\n          ws:style={css`\n            width: 60px;\n            height: 60px;\n          `}\n          aria-hidden={true}\n        >\n          <$.HtmlEmbed ws:label=\"Play SVG\" code={PlayIcon} />\n        </ws.element>\n      </$.VimeoPlayButton>\n    </$.YouTube>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react/src/youtube.tsx",
    "content": "import {\n  forwardRef,\n  useState,\n  useEffect,\n  type ElementRef,\n  type ComponentProps,\n  useContext,\n  type ContextType,\n  useRef,\n} from \"react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport { VideoContext, requestFullscreen } from \"./shared/video\";\n\n/**\n * Options for configuring the YouTube player parameters.\n * https://developers.google.com/youtube/player_parameters\n */\ntype YouTubePlayerParameters = {\n  /**\n   * Whether the video should autoplay.\n   * Some browsers require the `muted` parameter to be set to `true` for autoplay to work.\n   * @default false\n   */\n  autoplay?: boolean;\n  /**\n   * Whether the video should start muted.\n   * Useful for enabling autoplay in browsers that require videos to be muted.\n   * Original parameter: `mute`\n   *@default false\n   */\n  muted?: boolean;\n\n  /**\n   * Whether to show player controls.\n   * @default true\n   */\n  showControls?: boolean;\n\n  /**\n   * Whether to show related videos at the end.\n   * Original parameter: `rel`\n   * @default true\n   */\n  showRelatedVideos?: boolean;\n\n  /**\n   * Whether to enable keyboard controls.\n   * @default true\n   */\n  keyboard?: boolean;\n\n  /**\n   * Whether the video should loop continuously.\n   * @default false\n   */\n  loop?: boolean;\n\n  /**\n   * Whether to play inline on mobile (not fullscreen).\n   * @default false\n   */\n  inline?: boolean;\n\n  /**\n   * Whether to allow fullscreen mode.\n   * Original parameter: `fs`\n   * @default true\n   */\n  allowFullscreen?: boolean;\n\n  /**\n   * Whether captions should be shown by default.\n   * Original parameter: `cc_load_policy`\n   * @default false\n   */\n  showCaptions?: boolean;\n\n  /**\n   * Whether to show annotations on the video.\n   * Original parameter: `iv_load_policy`\n   * @default true\n   */\n  showAnnotations?: boolean;\n\n  /**\n   * Start time of the video in seconds.\n   * Original parameter: `start`\n   */\n  startTime?: number;\n\n  /**\n   * End time of the video in seconds.\n   * Original parameter: `end`\n   */\n  endTime?: number;\n\n  /**\n   * Whether to disable keyboard controls.\n   * Original parameter: `disablekb`\n   * @default false\n   */\n  disableKeyboard?: boolean;\n\n  /**\n   * Referrer URL for tracking purposes.\n   * Original parameter: `widget_referrer`\n   */\n  referrer?: string;\n\n  /**\n   * Type of playlist to load.\n   */\n  listType?: \"playlist\" | \"user_uploads\";\n\n  /**\n   * ID of the playlist to load.\n   * Original parameter: `list`\n   */\n  listId?: string;\n\n  /**\n   * Your domain for API compliance (e.g., `https://yourdomain.com`).\n   */\n  origin?: string;\n\n  /**\n   * Specifies the default language that the player will use to display captions.\n   * The value is an ISO 639-1 two-letter language code.\n   * Original parameter: `cc_lang_pref`\n   */\n  captionLanguage?: string;\n\n  /**\n   * Sets the player's interface language. The value is an ISO 639-1 two-letter language code or a fully specified locale.\n   * Original parameter: `hl`\n   */\n  language?: string;\n\n  /**\n   * Specifies the color that will be used in the player's video progress bar to highlight the amount of the video that the viewer has already seen.\n   * Valid values are 'red' and 'white'.\n   */\n  color?: \"red\" | \"white\";\n\n  /**\n   * This parameter specifies a comma-separated list of video IDs to play\n   */\n  playlist?: string;\n};\n\ntype YouTubePlayerOptions = {\n  /** The YouTube video URL or ID */\n  url?: string;\n  showPreview?: boolean;\n  /**\n   * The Privacy Enhanced Mode of the YouTube embedded player prevents the use of views of embedded YouTube content from influencing the viewer’s browsing experience on YouTube.\n   * https://support.google.com/youtube/answer/171780?hl=en#zippy=%2Cturn-on-privacy-enhanced-mode\n   * @default true\n   */\n  privacyEnhancedMode?: boolean;\n} & YouTubePlayerParameters & {\n    /** Loading strategy for iframe */\n    loading?: \"eager\" | \"lazy\";\n  };\n\nconst PLAYER_PRIVACY_ENHANVED_MODE_CDN = \"https://www.youtube-nocookie.com\";\nconst PLAYER_ORIGINAL_CDN = \"https://www.youtube.com\";\n\nconst IMAGE_CDN = \"https://img.youtube.com\";\n\nconst getVideoId = (url?: string) => {\n  if (!url) {\n    return;\n  }\n  try {\n    const urlObj = new URL(url);\n    // It's already an embed URL, we don't need to extract video id.\n    // It could be something like this https://youtube.com/embed?listType=playlist&list=UUjk2nKmHzgH5Xy-C5qYRd5A\n    if (urlObj.pathname === \"/embed\") {\n      return;\n    }\n    if (urlObj.hostname === \"youtu.be\") {\n      return urlObj.pathname.slice(1);\n    }\n    return urlObj.searchParams.get(\"v\") || urlObj.pathname.split(\"/\").pop();\n  } catch {\n    // If not URL, assume it's a video ID\n    return url;\n  }\n};\n\nconst getVideoUrl = (\n  options: YouTubePlayerOptions & { enablejsapi: boolean },\n  videoUrlOrigin: string\n) => {\n  const videoId = getVideoId(options.url);\n  const url = new URL(videoUrlOrigin);\n\n  if (videoId) {\n    url.pathname = `/embed/${videoId}`;\n  } else if (options.url) {\n    // E.g. this won't have videoId https://youtube.com/embed?listType=playlist&list=UUjk2nKmHzgH5Xy-C5qYRd5A\n    // It may also contain parameters since its an embed URL, so we want to keep it as-is and just use the origin we predefined\n    // so that no cookies option still works\n    try {\n      const parsedUrl = new URL(options.url);\n      url.pathname = parsedUrl.pathname;\n      url.search = parsedUrl.search;\n    } catch {\n      // Ignore invalid URL\n    }\n  }\n\n  const optionsKeys = Object.keys(options) as (\n    | keyof YouTubePlayerParameters\n    | \"enablejsapi\"\n  )[];\n\n  const parameters: Record<string, string | undefined> = {};\n  parameters.autoplay = \"1\";\n\n  for (const optionsKey of optionsKeys) {\n    if (options[optionsKey] === undefined) {\n      continue;\n    }\n    switch (optionsKey) {\n      case \"autoplay\":\n        // parameters.autoplay = options.autoplay ? \"1\" : \"0\";\n        // Mute video if autoplay is enabled and muted is not touched\n        if (options.autoplay && options.muted === undefined) {\n          parameters.mute = \"1\";\n        }\n        break;\n\n      case \"muted\":\n        parameters.mute = options.muted ? \"1\" : \"0\";\n        break;\n\n      case \"showControls\":\n        parameters.controls = options.showControls ? \"1\" : \"0\";\n        break;\n\n      case \"showRelatedVideos\":\n        parameters.rel = options.showRelatedVideos ? \"1\" : \"0\";\n        break;\n\n      case \"keyboard\":\n        parameters.keyboard = options.keyboard ? \"1\" : \"0\";\n        break;\n\n      case \"loop\":\n        parameters.loop = options.loop ? \"1\" : \"0\";\n        if (options.loop && (options.playlist ?? \"\").trim() === \"\") {\n          parameters.playlist = videoId;\n        }\n\n        break;\n\n      case \"inline\":\n        parameters.playsinline = options.inline ? \"1\" : \"0\";\n        break;\n\n      case \"allowFullscreen\":\n        parameters.fs = options.allowFullscreen ? \"1\" : \"0\";\n        break;\n\n      case \"captionLanguage\":\n        parameters.cc_lang_pref = options.captionLanguage;\n        break;\n\n      case \"showCaptions\":\n        parameters.cc_load_policy = options.showCaptions ? \"1\" : \"0\";\n        break;\n\n      case \"showAnnotations\":\n        parameters.iv_load_policy = options.showAnnotations ? \"1\" : \"3\";\n        break;\n\n      case \"startTime\":\n        parameters.start = options.startTime?.toString();\n        break;\n\n      case \"endTime\":\n        parameters.end = options.endTime?.toString();\n        break;\n\n      case \"disableKeyboard\":\n        parameters.disablekb = options.disableKeyboard ? \"1\" : \"0\";\n        break;\n\n      case \"language\":\n        parameters.hl = options.language;\n        break;\n\n      case \"listId\":\n        parameters.list = options.listId;\n        break;\n\n      case \"listType\":\n        parameters.listType = options.listType;\n        break;\n\n      case \"color\":\n        parameters.color = options.color;\n        break;\n\n      case \"origin\":\n        parameters.origin = options.origin;\n        break;\n      case \"referrer\":\n        parameters.widget_referrer = options.referrer;\n        break;\n      case \"playlist\":\n        parameters.playlist = options.playlist;\n        break;\n      case \"enablejsapi\":\n        parameters.enablejsapi = options.enablejsapi ? \"1\" : \"0\";\n        break;\n      default:\n        optionsKey satisfies never;\n    }\n  }\n\n  Object.entries(parameters).forEach(([key, value]) => {\n    if (value === undefined) {\n      return;\n    }\n    url.searchParams.append(key, value.toString());\n  });\n\n  return url.toString();\n};\n\nconst preconnect = (url: string) => {\n  const link = document.createElement(\"link\");\n  link.rel = \"preconnect\";\n  link.href = url;\n  link.crossOrigin = \"true\";\n  document.head.appendChild(link);\n};\n\nlet warmed = false;\n\nconst warmConnections = (videoUrl: string) => {\n  if (warmed || window.matchMedia(\"(hover: none)\").matches) {\n    return;\n  }\n\n  try {\n    const videoUrlObject = new URL(videoUrl);\n\n    preconnect(videoUrlObject.origin);\n  } catch {\n    // Ignore invalid URL\n  }\n\n  preconnect(IMAGE_CDN);\n  warmed = true;\n};\n\nconst getPreviewImageUrl = (videoId: string) => {\n  return new URL(`${IMAGE_CDN}/vi/${videoId}/maxresdefault.jpg`);\n};\n\ntype PlayerStatus = \"initial\" | \"loading\" | \"ready\";\n\nconst EmptyState = () => {\n  return (\n    <div className=\"flex w-full h-full items-center justify-center text-lg\">\n      Open the \"Settings\" panel and paste a video URL, e.g.\n      https://youtube.com/watch?v=dQw4w9WgXcQ\n    </div>\n  );\n};\n\ntype PlayerProps = Pick<\n  YouTubePlayerOptions,\n  \"loading\" | \"autoplay\" | \"showPreview\" | \"inline\"\n> & {\n  videoUrl: string;\n  title: string | undefined;\n  status: PlayerStatus;\n  renderer: ContextType<typeof ReactSdkContext>[\"renderer\"];\n  previewImageUrl?: URL;\n  onStatusChange: (status: PlayerStatus) => void;\n  onPreviewImageUrlChange: (url?: URL) => void;\n};\n\nconst Player = ({\n  title,\n  status,\n  loading,\n  videoUrl,\n  previewImageUrl,\n  autoplay,\n  inline,\n  renderer,\n  showPreview,\n  onStatusChange,\n  onPreviewImageUrlChange,\n}: PlayerProps) => {\n  const [opacity, setOpacity] = useState(0);\n  const ref = useRef<HTMLIFrameElement>(null);\n\n  useEffect(() => {\n    if (autoplay && renderer !== \"canvas\" && status === \"initial\") {\n      onStatusChange(\"loading\");\n    }\n  }, [autoplay, status, renderer, onStatusChange]);\n\n  useEffect(() => {\n    if (renderer !== \"canvas\") {\n      warmConnections(videoUrl);\n    }\n  }, [renderer, videoUrl]);\n\n  useEffect(() => {\n    const videoId = getVideoId(videoUrl);\n    if (!videoId || !showPreview) {\n      onPreviewImageUrlChange(undefined);\n      return;\n    }\n\n    if (!previewImageUrl) {\n      onPreviewImageUrlChange(getPreviewImageUrl(videoId));\n    }\n  }, [onPreviewImageUrlChange, showPreview, videoUrl, previewImageUrl]);\n\n  if (renderer === \"canvas\" || status === \"initial\") {\n    return null;\n  }\n\n  return (\n    <iframe\n      ref={ref}\n      title={title}\n      src={videoUrl}\n      loading={loading}\n      allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"\n      allowFullScreen\n      style={{\n        position: \"absolute\",\n        width: \"100%\",\n        height: \"100%\",\n        opacity,\n        transition: \"opacity 1s\",\n        border: \"none\",\n      }}\n      onLoad={() => {\n        onStatusChange(\"ready\");\n        setOpacity(1);\n        if (!inline && !autoplay && ref.current) {\n          requestFullscreen(ref.current);\n        }\n      }}\n    />\n  );\n};\n\nconst defaultTag = \"div\";\n\ntype Props = Omit<\n  ComponentProps<typeof defaultTag>,\n  keyof YouTubePlayerOptions\n> &\n  YouTubePlayerOptions & {\n    /**\n     * The `title` attribute for the iframe.\n     * Improves accessibility by providing a brief description of the video content for screen readers.\n     * Example: \"Video about web development tips\".\n     */\n    title?: string | undefined;\n  };\ntype Ref = ElementRef<typeof defaultTag>;\n\nexport const YouTube = forwardRef<Ref, Props>(\n  (\n    {\n      url,\n      loading = \"lazy\",\n      autoplay,\n      showPreview,\n      showAnnotations,\n      showCaptions,\n      showControls,\n      allowFullscreen,\n      keyboard,\n      children,\n      privacyEnhancedMode,\n      inline = false,\n      ...rest\n    },\n    ref\n  ) => {\n    const [status, setStatus] = useState<PlayerStatus>(\"initial\");\n    const [previewImageUrl, setPreviewImageUrl] = useState<URL>();\n    const { renderer } = useContext(ReactSdkContext);\n\n    const videoUrlOrigin =\n      (privacyEnhancedMode ?? true)\n        ? PLAYER_PRIVACY_ENHANVED_MODE_CDN\n        : PLAYER_ORIGINAL_CDN;\n\n    const videoUrl = getVideoUrl(\n      {\n        ...rest,\n        inline,\n        url,\n        keyboard,\n        showAnnotations,\n        showCaptions,\n        allowFullscreen,\n        showControls,\n        autoplay,\n        enablejsapi: false,\n      },\n      videoUrlOrigin\n    );\n\n    return (\n      <VideoContext.Provider\n        value={{\n          status,\n          previewImageUrl,\n          onInitPlayer() {\n            if (renderer !== \"canvas\") {\n              setStatus(\"loading\");\n            }\n          },\n        }}\n      >\n        <div {...rest} ref={ref}>\n          {!videoUrl ? (\n            <EmptyState />\n          ) : (\n            <>\n              {children}\n              <Player\n                title={rest.title}\n                autoplay={autoplay}\n                videoUrl={videoUrl}\n                previewImageUrl={previewImageUrl}\n                loading={loading}\n                inline={inline}\n                showPreview={showPreview}\n                renderer={renderer}\n                status={status}\n                onStatusChange={setStatus}\n                onPreviewImageUrlChange={setPreviewImageUrl}\n              />\n            </>\n          )}\n        </div>\n      </VideoContext.Provider>\n    );\n  }\n);\n\nYouTube.displayName = \"YouTube\";\n"
  },
  {
    "path": "packages/sdk-components-react/src/youtube.ws.ts",
    "content": "import type { ComponentProps } from \"react\";\nimport { YoutubeIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/youtube.props\";\nimport type { YouTube } from \"./youtube\";\n\nconst initialProps: Array<keyof ComponentProps<typeof YouTube>> = [\n  \"id\",\n  \"className\",\n  \"url\",\n  \"privacyEnhancedMode\",\n  \"title\",\n  \"loading\",\n  \"showPreview\",\n  \"autoplay\",\n  \"showControls\",\n  \"showRelatedVideos\",\n  \"keyboard\",\n  \"loop\",\n  \"inline\",\n  \"allowFullscreen\",\n  \"showCaptions\",\n  \"showAnnotations\",\n  \"startTime\",\n  \"endTime\",\n  \"disableKeyboard\",\n  \"referrer\",\n  \"listType\",\n  \"listId\",\n  \"origin\",\n  \"captionLanguage\",\n  \"language\",\n  \"color\",\n  \"playlist\",\n];\n\nexport const meta: WsComponentMeta = {\n  icon: YoutubeIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [\"VimeoSpinner\", \"VimeoPlayButton\", \"VimeoPreviewImage\"],\n  },\n  presetStyle: { div },\n  initialProps,\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src/components.ts\", \"src/metas.ts\", \"src/hooks.ts\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/sdk-components-react-radix/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/sdk-components-react-radix/LICENSE-3RD-PARTY",
    "content": "MIT License\n\nCopyright (c) 2023 shadcn\n\napplies to:\n\n- https://github.com/shadcn-ui/ui\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE\n"
  },
  {
    "path": "packages/sdk-components-react-radix/README.md",
    "content": "# Radix Components for Webstudio\n\nRadix Primitives is a low-level UI component library with a focus on accessibility and customization.\nDefault styling is inspired by https://ui.shadcn.com/docs.\n"
  },
  {
    "path": "packages/sdk-components-react-radix/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk-components-react-radix\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio wrapper for radix library\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/components.ts\",\n      \"types\": \"./lib/types/components.d.ts\",\n      \"import\": \"./lib/components.js\"\n    },\n    \"./metas\": {\n      \"webstudio\": \"./src/metas.ts\",\n      \"types\": \"./lib/types/metas.d.ts\",\n      \"import\": \"./lib/metas.js\"\n    },\n    \"./hooks\": {\n      \"webstudio\": \"./src/hooks.ts\",\n      \"types\": \"./lib/types/hooks.d.ts\",\n      \"import\": \"./lib/hooks.js\"\n    },\n    \"./templates\": {\n      \"webstudio\": \"./src/templates.ts\",\n      \"types\": \"./lib/types/templates.d.ts\",\n      \"import\": \"./lib/templates.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"vite build --config ../../vite.sdk-components.config.ts\",\n    \"build:args\": \"NODE_OPTIONS=--conditions=webstudio generate-arg-types './src/*.tsx !./src/*.stories.tsx !./src/*.ws.ts !./src/*.template.tsx !./src/*.test.{ts,tsx}' -e asChild -e modal -e defaultOpen -e defaultChecked && prettier --write \\\"**/*.props.ts\\\"\",\n    \"build:stories\": \"webstudio-sdk generate-stories && prettier --write \\\"src/__generated__/*.stories.tsx\\\"\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"dependencies\": {\n    \"@radix-ui/react-accordion\": \"^1.2.8\",\n    \"@radix-ui/react-checkbox\": \"^1.2.3\",\n    \"@radix-ui/react-collapsible\": \"^1.1.8\",\n    \"@radix-ui/react-dialog\": \"^1.1.11\",\n    \"@radix-ui/react-label\": \"^2.1.4\",\n    \"@radix-ui/react-navigation-menu\": \"^1.2.10\",\n    \"@radix-ui/react-popover\": \"^1.1.11\",\n    \"@radix-ui/react-radio-group\": \"^1.3.4\",\n    \"@radix-ui/react-select\": \"^2.2.2\",\n    \"@radix-ui/react-switch\": \"^1.2.2\",\n    \"@radix-ui/react-tabs\": \"^1.1.9\",\n    \"@radix-ui/react-tooltip\": \"^1.2.4\",\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/icons\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"await-interaction-response\": \"^0.0.2\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/css-data\": \"workspace:*\",\n    \"@webstudio-is/generate-arg-types\": \"workspace:*\",\n    \"@webstudio-is/sdk-cli\": \"workspace:^\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"@webstudio-is/template\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/accordion.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsAccordion: Record<string, PropMeta> = {\n  collapsible: {\n    description:\n      \"Whether an accordion item can be collapsed after it has been opened.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: false,\n  },\n  defaultValue: {\n    description:\n      \"The value of the item whose content is expanded when the accordion is initially rendered. Use\\n`defaultValue` if you do not need to control the state of an accordion.\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  dir: {\n    description: \"The language read direction.\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"ltr\", \"rtl\"],\n  },\n  disabled: {\n    description:\n      \"Whether or not an accordion is disabled from user interaction.\\n@defaultValue false\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n  orientation: {\n    description: \"The layout in which the Accordion operates.\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    defaultValue: \"vertical\",\n    options: [\"horizontal\", \"vertical\"],\n  },\n  value: {\n    description:\n      \"The controlled stateful value of the accordion item whose content is expanded.\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n};\nexport const propsAccordionItem: Record<string, PropMeta> = {\n  disabled: {\n    description:\n      \"Whether or not an accordion item is disabled from user interaction.\\n@defaultValue false\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsAccordionHeader: Record<string, PropMeta> = {};\nexport const propsAccordionTrigger: Record<string, PropMeta> = {};\nexport const propsAccordionContent: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/accordion.stories.tsx",
    "content": "import {\n  Box as Box,\n  Text as Text,\n  HtmlEmbed as HtmlEmbed,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Accordion as Accordion,\n  AccordionItem as AccordionItem,\n  AccordionHeader as AccordionHeader,\n  AccordionTrigger as AccordionTrigger,\n  AccordionContent as AccordionContent,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Accordion\n        collapsible={true}\n        defaultValue={\"0\"}\n        className={\"w-accordion\"}\n      >\n        <AccordionItem data-ws-index=\"0\" className={\"w-item w-item-1\"}>\n          <AccordionHeader className={\"w-item-header w-item-header-1\"}>\n            <AccordionTrigger className={\"w-item-trigger w-item-trigger-1\"}>\n              <Text className={\"w-text\"}>{\"Is it accessible?\"}</Text>\n              <Box className={\"w-box w-icon-container\"}>\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 6 4 4 4-4\"/></svg>'\n                  }\n                  className={\"w-html-embed\"}\n                />\n              </Box>\n            </AccordionTrigger>\n          </AccordionHeader>\n          <AccordionContent className={\"w-item-content w-item-content-1\"}>\n            {\"Yes. It adheres to the WAI-ARIA design pattern.\"}\n          </AccordionContent>\n        </AccordionItem>\n        <AccordionItem data-ws-index=\"1\" className={\"w-item w-item-2\"}>\n          <AccordionHeader className={\"w-item-header w-item-header-2\"}>\n            <AccordionTrigger className={\"w-item-trigger w-item-trigger-2\"}>\n              <Text className={\"w-text\"}>{\"Is it styled?\"}</Text>\n              <Box className={\"w-box w-icon-container-1\"}>\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 6 4 4 4-4\"/></svg>'\n                  }\n                  className={\"w-html-embed\"}\n                />\n              </Box>\n            </AccordionTrigger>\n          </AccordionHeader>\n          <AccordionContent className={\"w-item-content w-item-content-2\"}>\n            {\n              \"Yes. It comes with default styles that matches the other components' aesthetic.\"\n            }\n          </AccordionContent>\n        </AccordionItem>\n        <AccordionItem data-ws-index=\"2\" className={\"w-item w-item-3\"}>\n          <AccordionHeader className={\"w-item-header w-item-header-3\"}>\n            <AccordionTrigger className={\"w-item-trigger w-item-trigger-3\"}>\n              <Text className={\"w-text\"}>{\"Is it animated?\"}</Text>\n              <Box className={\"w-box w-icon-container-2\"}>\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 6 4 4 4-4\"/></svg>'\n                  }\n                  className={\"w-html-embed\"}\n                />\n              </Box>\n            </AccordionTrigger>\n          </AccordionHeader>\n          <AccordionContent className={\"w-item-content w-item-content-3\"}>\n            {\n              \"Yes. It's animated by default, but you can disable it if you prefer.\"\n            }\n          </AccordionContent>\n        </AccordionItem>\n      </Accordion>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Accordion\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(div.w-accordion) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-item-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h3.w-item-header) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    margin-top: 0px;\n    margin-bottom: 0px\n  }\n  :where(div.w-item) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-item-trigger) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    background-color: transparent;\n    background-image: none;\n    border: 0px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n}\n@media all {\n  .w-item-1 {\n    border-bottom: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-item-header-1 {\n    display: flex\n  }\n  .w-item-trigger-1 {\n    display: flex;\n    flex-grow: 1;\n    flex-shrink: 1;\n    flex-basis: 0;\n    align-items: center;\n    justify-content: between;\n    padding-top: 1rem;\n    padding-right: 0;\n    padding-bottom: 1rem;\n    padding-left: 0;\n    font-weight: 500;\n    --accordion-trigger-icon-transform: 0deg\n  }\n  .w-item-trigger-1:hover {\n    text-decoration-line: underline\n  }\n  .w-item-trigger-1[data-state=\"open\"] {\n    --accordion-trigger-icon-transform: 180deg\n  }\n  .w-icon-container {\n    rotate: var(--accordion-trigger-icon-transform);\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    transition-property: all;\n    transition-duration: 200ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal\n  }\n  .w-item-content-1 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    transition-property: all;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal;\n    padding-bottom: 1rem\n  }\n  .w-item-2 {\n    border-bottom: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-item-header-2 {\n    display: flex\n  }\n  .w-item-trigger-2 {\n    display: flex;\n    flex-grow: 1;\n    flex-shrink: 1;\n    flex-basis: 0;\n    align-items: center;\n    justify-content: between;\n    padding-top: 1rem;\n    padding-right: 0;\n    padding-bottom: 1rem;\n    padding-left: 0;\n    font-weight: 500;\n    --accordion-trigger-icon-transform: 0deg\n  }\n  .w-item-trigger-2:hover {\n    text-decoration-line: underline\n  }\n  .w-item-trigger-2[data-state=\"open\"] {\n    --accordion-trigger-icon-transform: 180deg\n  }\n  .w-icon-container-1 {\n    rotate: var(--accordion-trigger-icon-transform);\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    transition-property: all;\n    transition-duration: 200ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal\n  }\n  .w-item-content-2 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    transition-property: all;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal;\n    padding-bottom: 1rem\n  }\n  .w-item-3 {\n    border-bottom: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-item-header-3 {\n    display: flex\n  }\n  .w-item-trigger-3 {\n    display: flex;\n    flex-grow: 1;\n    flex-shrink: 1;\n    flex-basis: 0;\n    align-items: center;\n    justify-content: between;\n    padding-top: 1rem;\n    padding-right: 0;\n    padding-bottom: 1rem;\n    padding-left: 0;\n    font-weight: 500;\n    --accordion-trigger-icon-transform: 0deg\n  }\n  .w-item-trigger-3:hover {\n    text-decoration-line: underline\n  }\n  .w-item-trigger-3[data-state=\"open\"] {\n    --accordion-trigger-icon-transform: 180deg\n  }\n  .w-icon-container-2 {\n    rotate: var(--accordion-trigger-icon-transform);\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    transition-property: all;\n    transition-duration: 200ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal\n  }\n  .w-item-content-3 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    transition-property: all;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal;\n    padding-bottom: 1rem\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Accordion };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/checkbox.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsCheckbox: Record<string, PropMeta> = {\n  checked: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is checked\",\n  },\n  required: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is required for form submission\",\n  },\n};\nexport const propsCheckboxIndicator: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/checkbox.stories.tsx",
    "content": "import {\n  Box as Box,\n  HtmlEmbed as HtmlEmbed,\n  Text as Text,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Label as Label,\n  Checkbox as Checkbox,\n  CheckboxIndicator as CheckboxIndicator,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Label className={\"w-label w-checkbox-field\"}>\n        <Checkbox className={\"w-checkbox w-checkbox-1\"}>\n          <CheckboxIndicator\n            className={\"w-checkbox-indicator w-checkbox-indicator-1\"}\n          >\n            <HtmlEmbed\n              code={\n                '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.091\" d=\"m13.636 3.667-8 8L2 8.03\"/></svg>'\n              }\n              className={\"w-html-embed\"}\n            />\n          </CheckboxIndicator>\n        </Checkbox>\n        <Text tag={\"span\"} className={\"w-text\"}>\n          {\"Checkbox\"}\n        </Text>\n      </Label>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Checkbox\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(button.w-checkbox) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    background-color: transparent;\n    background-image: none;\n    border: 0px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n  :where(span.w-checkbox-indicator) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(label.w-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-checkbox-field {\n    display: flex;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem;\n    align-items: center\n  }\n  .w-checkbox-1 {\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    border-top-left-radius: 0.125rem;\n    border-top-right-radius: 0.125rem;\n    border-bottom-right-radius: 0.125rem;\n    border-bottom-left-radius: 0.125rem;\n    border: 1px solid rgba(15, 23, 42, 1)\n  }\n  .w-checkbox-1:disabled {\n    cursor: not-allowed;\n    opacity: 0.5\n  }\n  .w-checkbox-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n  .w-checkbox-1[data-state=\"checked\"] {\n    background-color: rgba(15, 23, 42, 1);\n    color: rgba(248, 250, 252, 1)\n  }\n  .w-checkbox-indicator-1 {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: currentcolor\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Checkbox };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/collapsible.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsCollapsible: Record<string, PropMeta> = {\n  disabled: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the form control is disabled\",\n  },\n  open: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description:\n      \"Show or hide the content of this component on the canvas. This will not affect the initial state of the component.\",\n  },\n};\nexport const propsCollapsibleTrigger: Record<string, PropMeta> = {};\nexport const propsCollapsibleContent: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/collapsible.stories.tsx",
    "content": "import {\n  Box as Box,\n  Button as Button,\n  Text as Text,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Collapsible as Collapsible,\n  CollapsibleTrigger as CollapsibleTrigger,\n  CollapsibleContent as CollapsibleContent,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Collapsible className={\"w-collapsible\"}>\n        <CollapsibleTrigger>\n          <Button className={\"w-button w-button-1\"}>\n            {\"Click to toggle content\"}\n          </Button>\n        </CollapsibleTrigger>\n        <CollapsibleContent className={\"w-collapsible-content\"}>\n          <Text className={\"w-text\"}>{\"Collapsible Content\"}</Text>\n        </CollapsibleContent>\n      </Collapsible>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Collapsible\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(div.w-collapsible) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-collapsible-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-button-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: rgba(255, 255, 255, 1);\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.5rem;\n    padding-top: 0.5rem;\n    padding-right: 1rem;\n    padding-bottom: 0.5rem;\n    padding-left: 1rem;\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-button-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Collapsible };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/dialog.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsDialog: Record<string, PropMeta> = {\n  open: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description:\n      \"Show or hide the content of this component on the canvas. This will not affect the initial state of the component.\",\n  },\n};\nexport const propsDialogTrigger: Record<string, PropMeta> = {};\nexport const propsDialogOverlay: Record<string, PropMeta> = {};\nexport const propsDialogContent: Record<string, PropMeta> = {};\nexport const propsDialogClose: Record<string, PropMeta> = {};\nexport const propsDialogTitle: Record<string, PropMeta> = {\n  tag: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"h2\", \"h3\", \"h1\", \"h4\", \"h5\", \"h6\"],\n  },\n};\nexport const propsDialogDescription: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/dialog.stories.tsx",
    "content": "import {\n  Box as Box,\n  Button as Button,\n  Text as Text,\n  HtmlEmbed as HtmlEmbed,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Dialog as Dialog,\n  DialogTrigger as DialogTrigger,\n  DialogOverlay as DialogOverlay,\n  DialogContent as DialogContent,\n  DialogTitle as DialogTitle,\n  DialogDescription as DialogDescription,\n  DialogClose as DialogClose,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Dialog>\n        <DialogTrigger>\n          <Button className={\"w-button w-button-1\"}>{\"Button\"}</Button>\n        </DialogTrigger>\n        <DialogOverlay className={\"w-dialog-overlay w-dialog-overlay-1\"}>\n          <DialogContent className={\"w-dialog-content w-dialog-content-1\"}>\n            <Box className={\"w-box w-dialog-header\"}>\n              <DialogTitle className={\"w-dialog-title w-dialog-title-1\"}>\n                {\"Dialog Title you can edit\"}\n              </DialogTitle>\n              <DialogDescription\n                className={\"w-dialog-description w-dialog-description-1\"}\n              >\n                {\"Dialog description text you can edit\"}\n              </DialogDescription>\n            </Box>\n            <Text className={\"w-text\"}>{\"The text you can edit\"}</Text>\n            <DialogClose className={\"w-close-button w-close-button-1\"}>\n              <HtmlEmbed\n                code={\n                  '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"/></svg>'\n                }\n                className={\"w-html-embed\"}\n              />\n            </DialogClose>\n          </DialogContent>\n        </DialogOverlay>\n      </Dialog>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Dialog\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(button.w-close-button) {\n    background-color: transparent;\n    background-image: none;\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    border: 1px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n  :where(div.w-dialog-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(p.w-dialog-description) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-dialog-overlay) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h2.w-dialog-title) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-button-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: rgba(255, 255, 255, 1);\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.5rem;\n    padding-top: 0.5rem;\n    padding-right: 1rem;\n    padding-bottom: 0.5rem;\n    padding-left: 1rem;\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-button-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-dialog-overlay-1 {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 50;\n    background-color: rgba(255, 255, 255, 0.8);\n    -webkit-backdrop-filter: blur(0 1px 2px 0 rgb(0 0 0/0.05));\n    backdrop-filter: blur(0 1px 2px 0 rgb(0 0 0/0.05));\n    display: flex;\n    overflow-x: auto;\n    overflow-y: auto\n  }\n  .w-dialog-content-1 {\n    width: 100%;\n    z-index: 50;\n    display: flex;\n    flex-direction: column;\n    row-gap: 1rem;\n    column-gap: 1rem;\n    max-width: 32rem;\n    background-color: rgba(255, 255, 255, 1);\n    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n    position: relative;\n    border: 1px solid rgba(226, 232, 240, 1);\n    margin: auto;\n    padding: 1.5rem\n  }\n  .w-dialog-header {\n    display: flex;\n    flex-direction: column;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem\n  }\n  .w-dialog-title-1 {\n    font-size: 1.125rem;\n    line-height: 1;\n    letter-spacing: -0.025em;\n    margin: 0\n  }\n  .w-dialog-description-1 {\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-close-button-1 {\n    position: absolute;\n    right: 1rem;\n    top: 1rem;\n    border-top-left-radius: 0.125rem;\n    border-top-right-radius: 0.125rem;\n    border-bottom-right-radius: 0.125rem;\n    border-bottom-left-radius: 0.125rem;\n    opacity: 0.7;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 1rem;\n    width: 1rem;\n    background-color: transparent;\n    outline: medium none currentcolor;\n    border: 0 none currentcolor\n  }\n  .w-close-button-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1)\n  }\n  .w-close-button-1:hover {\n    opacity: 1\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Dialog };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/label.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const props: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/label.stories.tsx",
    "content": "import { Box as Box } from \"@webstudio-is/sdk-components-react\";\nimport { Label as Label } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Label className={\"w-label w-label-1\"}>{\"Form Label\"}</Label>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Label\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(label.w-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-label-1 {\n    font-size: 0.875rem;\n    line-height: 1;\n    font-weight: 500\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Label };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/navigation-menu.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsNavigationMenu: Record<string, PropMeta> = {\n  defaultValue: { required: false, control: \"text\", type: \"string\" },\n  delayDuration: {\n    description:\n      \"The duration from when the pointer enters the trigger until the tooltip gets opened.\\n@defaultValue 200\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n  },\n  dir: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"ltr\", \"rtl\"],\n    description: \"The text directionality of the element\",\n  },\n  skipDelayDuration: {\n    description:\n      \"How much time a user has to enter another trigger without incurring a delay again.\\n@defaultValue 300\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n  },\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsNavigationMenuList: Record<string, PropMeta> = {};\nexport const propsNavigationMenuViewport: Record<string, PropMeta> = {};\nexport const propsNavigationMenuContent: Record<string, PropMeta> = {};\nexport const propsNavigationMenuItem: Record<string, PropMeta> = {\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsNavigationMenuLink: Record<string, PropMeta> = {\n  active: { required: false, control: \"boolean\", type: \"boolean\" },\n};\nexport const propsNavigationMenuTrigger: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/navigation-menu.stories.tsx",
    "content": "import {\n  Box as Box,\n  Button as Button,\n  Text as Text,\n  HtmlEmbed as HtmlEmbed,\n  Link as Link,\n  Paragraph as Paragraph,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  NavigationMenu as NavigationMenu,\n  NavigationMenuList as NavigationMenuList,\n  NavigationMenuItem as NavigationMenuItem,\n  NavigationMenuTrigger as NavigationMenuTrigger,\n  NavigationMenuContent as NavigationMenuContent,\n  NavigationMenuLink as NavigationMenuLink,\n  NavigationMenuViewport as NavigationMenuViewport,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <NavigationMenu className={\"w-navigation-menu w-navigation-menu-1\"}>\n        <NavigationMenuList className={\"w-menu-list w-menu-list-1\"}>\n          <NavigationMenuItem data-ws-index=\"0\" className={\"w-menu-item\"}>\n            <NavigationMenuTrigger className={\"w-menu-trigger\"}>\n              <Button className={\"w-button w-button-1\"}>\n                <Text className={\"w-text\"}>{\"About\"}</Text>\n                <Box className={\"w-box w-icon-container\"}>\n                  <HtmlEmbed\n                    code={\n                      '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 6 4 4 4-4\"/></svg>'\n                    }\n                    className={\"w-html-embed\"}\n                  />\n                </Box>\n              </Button>\n            </NavigationMenuTrigger>\n            <NavigationMenuContent\n              data-ws-index=\"0\"\n              className={\"w-menu-content w-menu-content-1\"}\n            >\n              <Box className={\"w-box w-content\"}>\n                <Box className={\"w-box w-box-1\"}>{\"\"}</Box>\n                <Box className={\"w-box w-flex-column\"}>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/sheet\"}\n                      className={\"w-link w-link-1\"}\n                    >\n                      <Text className={\"w-text w-text-1\"}>{\"Sheet\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-1\"}>\n                        {\n                          \"Extends the Dialog component to display content that complements the main content of the screen.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\n                        \"https://ui.shadcn.com/docs/components/navigation-menu\"\n                      }\n                      className={\"w-link w-link-2\"}\n                    >\n                      <Text className={\"w-text w-text-2\"}>\n                        {\"Navigation Menu\"}\n                      </Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-2\"}>\n                        {\"A collection of links for navigating websites.\"}\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/tabs\"}\n                      className={\"w-link w-link-3\"}\n                    >\n                      <Text className={\"w-text w-text-3\"}>{\"Tabs\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-3\"}>\n                        {\n                          \"A set of layered sections of content—known as tab panels—that are displayed one at a time.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                </Box>\n              </Box>\n            </NavigationMenuContent>\n          </NavigationMenuItem>\n          <NavigationMenuItem data-ws-index=\"1\" className={\"w-menu-item\"}>\n            <NavigationMenuTrigger className={\"w-menu-trigger\"}>\n              <Button className={\"w-button w-button-2\"}>\n                <Text className={\"w-text\"}>{\"Components\"}</Text>\n                <Box className={\"w-box w-icon-container-1\"}>\n                  <HtmlEmbed\n                    code={\n                      '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m4 6 4 4 4-4\"/></svg>'\n                    }\n                    className={\"w-html-embed\"}\n                  />\n                </Box>\n              </Button>\n            </NavigationMenuTrigger>\n            <NavigationMenuContent\n              data-ws-index=\"1\"\n              className={\"w-menu-content w-menu-content-2\"}\n            >\n              <Box className={\"w-box w-content-1\"}>\n                <Box className={\"w-box w-flex-column-1\"}>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/accordion\"}\n                      className={\"w-link w-link-4\"}\n                    >\n                      <Text className={\"w-text w-text-4\"}>{\"Accordion\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-4\"}>\n                        {\n                          \"A vertically stacked set of interactive headings that each reveal a section of content.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/dialog\"}\n                      className={\"w-link w-link-5\"}\n                    >\n                      <Text className={\"w-text w-text-5\"}>{\"Dialog\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-5\"}>\n                        {\n                          \"A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/collapsible\"}\n                      className={\"w-link w-link-6\"}\n                    >\n                      <Text className={\"w-text w-text-6\"}>{\"Collapsible\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-6\"}>\n                        {\n                          \"An interactive component which expands/collapses a panel.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                </Box>\n                <Box className={\"w-box w-flex-column-2\"}>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/popover\"}\n                      className={\"w-link w-link-7\"}\n                    >\n                      <Text className={\"w-text w-text-7\"}>{\"Popover\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-7\"}>\n                        {\n                          \"Displays rich content in a portal, triggered by a button.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/tooltip\"}\n                      className={\"w-link w-link-8\"}\n                    >\n                      <Text className={\"w-text w-text-8\"}>{\"Tooltip\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-8\"}>\n                        {\n                          \"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                  <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n                    <Link\n                      href={\"https://ui.shadcn.com/docs/components/button\"}\n                      className={\"w-link w-link-9\"}\n                    >\n                      <Text className={\"w-text w-text-9\"}>{\"Button\"}</Text>\n                      <Paragraph className={\"w-paragraph w-paragraph-9\"}>\n                        {\n                          \"Displays a button or a component that looks like a button.\"\n                        }\n                      </Paragraph>\n                    </Link>\n                  </NavigationMenuLink>\n                </Box>\n              </Box>\n            </NavigationMenuContent>\n          </NavigationMenuItem>\n          <NavigationMenuItem data-ws-index=\"2\" className={\"w-menu-item\"}>\n            <NavigationMenuLink className={\"w-accessible-link-wrapper\"}>\n              <Link className={\"w-link w-link-10\"}>{\"Standalone\"}</Link>\n            </NavigationMenuLink>\n          </NavigationMenuItem>\n        </NavigationMenuList>\n        <Box className={\"w-box w-viewport-container\"}>\n          <NavigationMenuViewport\n            className={\"w-menu-viewport w-menu-viewport-1\"}\n          />\n        </Box>\n      </NavigationMenu>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Navigation Menu\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(a.w-link) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    display: inline-block\n  }\n  :where(p.w-paragraph) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(div.w-navigation-menu) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-menu-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-menu-item) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-accessible-link-wrapper) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-menu-list) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-menu-trigger) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-menu-viewport) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-navigation-menu-1 {\n    position: relative;\n    max-width: max-content\n  }\n  .w-menu-list-1 {\n    display: flex;\n    flex-grow: 1;\n    flex-shrink: 1;\n    flex-basis: 0;\n    list-style-type: none;\n    align-items: center;\n    justify-content: center;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    margin: 0;\n    padding: 0\n  }\n  .w-button-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: transparent;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.25rem;\n    padding-top: 0;\n    padding-right: 0.75rem;\n    padding-bottom: 0;\n    padding-left: 0.75rem;\n    --navigation-menu-trigger-icon-transform: 0deg;\n    border: 0 solid rgba(226, 232, 240, 1)\n  }\n  .w-button-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-button-1[data-state=\"open\"] {\n    --navigation-menu-trigger-icon-transform: 180deg\n  }\n  .w-icon-container {\n    margin-left: 0.25rem;\n    rotate: var(--navigation-menu-trigger-icon-transform);\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    transition-property: all;\n    transition-duration: 200ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal\n  }\n  .w-menu-content-1 {\n    left: 0;\n    top: 0;\n    positon: absolute;\n    width: max-content;\n    padding: 1rem\n  }\n  .w-content {\n    display: flex;\n    row-gap: 1rem;\n    column-gap: 1rem;\n    padding: 0.5rem\n  }\n  .w-box-1 {\n    background-color: rgba(226, 232, 240, 1);\n    width: 12rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    padding: 1rem\n  }\n  .w-flex-column {\n    width: 16rem;\n    display: flex;\n    row-gap: 1rem;\n    column-gap: 1rem;\n    flex-direction: column\n  }\n  .w-link-1 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-1:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-1 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-1 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-2 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-2:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-2:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-2 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-2 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-3 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-3:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-3:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-3 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-3 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-button-2 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: transparent;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.25rem;\n    padding-top: 0;\n    padding-right: 0.75rem;\n    padding-bottom: 0;\n    padding-left: 0.75rem;\n    --navigation-menu-trigger-icon-transform: 0deg;\n    border: 0 solid rgba(226, 232, 240, 1)\n  }\n  .w-button-2:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-2:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-2:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-button-2[data-state=\"open\"] {\n    --navigation-menu-trigger-icon-transform: 180deg\n  }\n  .w-icon-container-1 {\n    margin-left: 0.25rem;\n    rotate: var(--navigation-menu-trigger-icon-transform);\n    height: 1rem;\n    width: 1rem;\n    flex-shrink: 0;\n    transition-property: all;\n    transition-duration: 200ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal\n  }\n  .w-menu-content-2 {\n    left: 0;\n    top: 0;\n    positon: absolute;\n    width: max-content;\n    padding: 1rem\n  }\n  .w-content-1 {\n    display: flex;\n    row-gap: 1rem;\n    column-gap: 1rem\n  }\n  .w-flex-column-1 {\n    width: 16rem;\n    display: flex;\n    row-gap: 1rem;\n    column-gap: 1rem;\n    flex-direction: column\n  }\n  .w-link-4 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-4:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-4:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-4 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-4 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-5 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-5:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-5:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-5 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-5 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-6 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-6:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-6:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-6 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-6 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-flex-column-2 {\n    width: 16rem;\n    display: flex;\n    row-gap: 1rem;\n    column-gap: 1rem;\n    flex-direction: column\n  }\n  .w-link-7 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-7:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-7:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-7 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-7 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-8 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-8:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-8:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-8 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-8 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-9 {\n    color: inherit;\n    display: flex;\n    flex-direction: column;\n    -webkit-user-select: none;\n    user-select: none;\n    row-gap: 0.25rem;\n    column-gap: 0.25rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    line-height: 1;\n    text-decoration-line: none;\n    outline: medium none currentcolor;\n    padding: 0.75rem\n  }\n  .w-link-9:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-link-9:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-text-9 {\n    font-size: 0.875rem;\n    font-weight: 500;\n    line-height: 1\n  }\n  .w-paragraph-9 {\n    overflow-x: hidden;\n    overflow-y: hidden;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    font-size: 0.875rem;\n    line-height: 1.375;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-link-10 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: transparent;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.25rem;\n    padding-top: 0;\n    padding-right: 0.75rem;\n    padding-bottom: 0;\n    padding-left: 0.75rem;\n    text-decoration-line: none;\n    color: currentcolor;\n    border: 0 solid rgba(226, 232, 240, 1)\n  }\n  .w-link-10:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-link-10:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-link-10:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-viewport-container {\n    position: absolute;\n    left: 0;\n    top: 100%;\n    display: flex;\n    justify-content: center\n  }\n  .w-menu-viewport-1 {\n    position: relative;\n    margin-top: 0.375rem;\n    overflow-x: hidden;\n    overflow-y: hidden;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    background-color: rgba(255, 255, 255, 1);\n    color: rgba(2, 8, 23, 1);\n    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n    height: var(--radix-navigation-menu-viewport-height);\n    width: var(--radix-navigation-menu-viewport-width);\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as NavigationMenu };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/popover.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsPopover: Record<string, PropMeta> = {\n  open: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description:\n      \"Show or hide the content of this component on the canvas. This will not affect the initial state of the component.\",\n  },\n};\nexport const propsPopoverTrigger: Record<string, PropMeta> = {};\nexport const propsPopoverContent: Record<string, PropMeta> = {\n  align: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    defaultValue: \"center\",\n    options: [\"center\", \"start\", \"end\"],\n  },\n  alignOffset: {\n    required: false,\n    control: \"number\",\n    type: \"number\",\n    description:\n      \"The offset in pixels from the “start“ or “end“ alignment options.\",\n  },\n  arrowPadding: { required: false, control: \"number\", type: \"number\" },\n  avoidCollisions: { required: false, control: \"boolean\", type: \"boolean\" },\n  hideWhenDetached: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  side: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"top\", \"right\", \"bottom\", \"left\"],\n    description:\n      \"The preferred alignment against the Trigger. May change when collisions occur.\",\n  },\n  sideOffset: {\n    required: false,\n    control: \"number\",\n    type: \"number\",\n    defaultValue: 4,\n    description: \"The distance in pixels between the Content and the Trigger.\",\n  },\n  sticky: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"partial\", \"always\"],\n  },\n  updatePositionStrategy: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"always\", \"optimized\"],\n  },\n};\nexport const propsPopoverClose: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/popover.stories.tsx",
    "content": "import {\n  Box as Box,\n  Button as Button,\n  Text as Text,\n  HtmlEmbed as HtmlEmbed,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Popover as Popover,\n  PopoverTrigger as PopoverTrigger,\n  PopoverContent as PopoverContent,\n  PopoverClose as PopoverClose,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Popover>\n        <PopoverTrigger>\n          <Button className={\"w-button w-button-1\"}>{\"Button\"}</Button>\n        </PopoverTrigger>\n        <PopoverContent className={\"w-popover-content w-popover-content-1\"}>\n          <Text className={\"w-text\"}>{\"The text you can edit\"}</Text>\n          <PopoverClose className={\"w-close-button w-close-button-1\"}>\n            <HtmlEmbed\n              code={\n                '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"/></svg>'\n              }\n              className={\"w-html-embed\"}\n            />\n          </PopoverClose>\n        </PopoverContent>\n      </Popover>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Popover\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(button.w-close-button) {\n    background-color: transparent;\n    background-image: none;\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    border: 1px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n  :where(div.w-popover-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-button-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: rgba(255, 255, 255, 1);\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.5rem;\n    padding-top: 0.5rem;\n    padding-right: 1rem;\n    padding-bottom: 0.5rem;\n    padding-left: 1rem;\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-button-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-popover-content-1 {\n    z-index: 50;\n    width: 18rem;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    background-color: rgba(255, 255, 255, 1);\n    color: rgba(2, 8, 23, 1);\n    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);\n    outline: medium none currentcolor;\n    border: 1px solid rgba(226, 232, 240, 1);\n    padding: 1rem\n  }\n  .w-close-button-1 {\n    position: absolute;\n    right: 1rem;\n    top: 1rem;\n    border-top-left-radius: 0.125rem;\n    border-top-right-radius: 0.125rem;\n    border-bottom-right-radius: 0.125rem;\n    border-bottom-left-radius: 0.125rem;\n    opacity: 0.7;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 1rem;\n    width: 1rem;\n    background-color: transparent;\n    outline: medium none currentcolor;\n    border: 0 none currentcolor\n  }\n  .w-close-button-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1)\n  }\n  .w-close-button-1:hover {\n    opacity: 1\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Popover };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/radio-group.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsRadioGroup: Record<string, PropMeta> = {\n  defaultValue: { required: false, control: \"text\", type: \"string\" },\n  dir: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"ltr\", \"rtl\"],\n    description: \"The text directionality of the element\",\n  },\n  disabled: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the form control is disabled\",\n  },\n  loop: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether to loop the media resource\",\n  },\n  name: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description:\n      \"Name of the element to use for form submission and in the form.elements API\",\n  },\n  orientation: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"horizontal\", \"vertical\"],\n  },\n  required: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is required for form submission\",\n  },\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsRadioGroupItem: Record<string, PropMeta> = {\n  checked: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is checked\",\n  },\n  required: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is required for form submission\",\n  },\n  value: {\n    required: true,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsRadioGroupIndicator: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/radio-group.stories.tsx",
    "content": "import {\n  Box as Box,\n  HtmlEmbed as HtmlEmbed,\n  Text as Text,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  RadioGroup as RadioGroup,\n  Label as Label,\n  RadioGroupItem as RadioGroupItem,\n  RadioGroupIndicator as RadioGroupIndicator,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <RadioGroup className={\"w-radio-group w-radio-group-1\"}>\n        <Label className={\"w-label w-label-1\"}>\n          <RadioGroupItem\n            value={\"default\"}\n            className={\"w-radio-group-item w-radio-group-item-1\"}\n          >\n            <RadioGroupIndicator className={\"w-radio-group-indicator\"}>\n              <HtmlEmbed\n                code={\n                  '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z\"/></svg>'\n                }\n                className={\"w-html-embed\"}\n              />\n            </RadioGroupIndicator>\n          </RadioGroupItem>\n          <Text className={\"w-text\"}>{\"Default\"}</Text>\n        </Label>\n        <Label className={\"w-label w-label-2\"}>\n          <RadioGroupItem\n            value={\"comfortable\"}\n            className={\"w-radio-group-item w-radio-group-item-2\"}\n          >\n            <RadioGroupIndicator className={\"w-radio-group-indicator\"}>\n              <HtmlEmbed\n                code={\n                  '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z\"/></svg>'\n                }\n                className={\"w-html-embed\"}\n              />\n            </RadioGroupIndicator>\n          </RadioGroupItem>\n          <Text className={\"w-text\"}>{\"Comfortable\"}</Text>\n        </Label>\n        <Label className={\"w-label w-label-3\"}>\n          <RadioGroupItem\n            value={\"compact\"}\n            className={\"w-radio-group-item w-radio-group-item-3\"}\n          >\n            <RadioGroupIndicator className={\"w-radio-group-indicator\"}>\n              <HtmlEmbed\n                code={\n                  '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path fill=\"currentColor\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z\"/></svg>'\n                }\n                className={\"w-html-embed\"}\n              />\n            </RadioGroupIndicator>\n          </RadioGroupItem>\n          <Text className={\"w-text\"}>{\"Compact\"}</Text>\n        </Label>\n      </RadioGroup>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Radio Group\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(label.w-label) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-radio-group) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(span.w-radio-group-indicator) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-radio-group-item) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    background-color: transparent;\n    background-image: none;\n    border: 0px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n}\n@media all {\n  .w-radio-group-1 {\n    display: flex;\n    flex-direction: column;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem\n  }\n  .w-label-1 {\n    display: flex;\n    align-items: center;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem\n  }\n  .w-radio-group-item-1 {\n    aspect-ratio: 1/1;\n    height: 1rem;\n    width: 1rem;\n    border-top-left-radius: 9999px;\n    border-top-right-radius: 9999px;\n    border-bottom-right-radius: 9999px;\n    border-bottom-left-radius: 9999px;\n    color: rgba(15, 23, 42, 1);\n    border: 1px solid rgba(15, 23, 42, 1)\n  }\n  .w-radio-group-item-1:disabled {\n    cursor: not-allowed;\n    opacity: 0.5\n  }\n  .w-radio-group-item-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n  .w-label-2 {\n    display: flex;\n    align-items: center;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem\n  }\n  .w-radio-group-item-2 {\n    aspect-ratio: 1/1;\n    height: 1rem;\n    width: 1rem;\n    border-top-left-radius: 9999px;\n    border-top-right-radius: 9999px;\n    border-bottom-right-radius: 9999px;\n    border-bottom-left-radius: 9999px;\n    color: rgba(15, 23, 42, 1);\n    border: 1px solid rgba(15, 23, 42, 1)\n  }\n  .w-radio-group-item-2:disabled {\n    cursor: not-allowed;\n    opacity: 0.5\n  }\n  .w-radio-group-item-2:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n  .w-label-3 {\n    display: flex;\n    align-items: center;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem\n  }\n  .w-radio-group-item-3 {\n    aspect-ratio: 1/1;\n    height: 1rem;\n    width: 1rem;\n    border-top-left-radius: 9999px;\n    border-top-right-radius: 9999px;\n    border-bottom-right-radius: 9999px;\n    border-bottom-left-radius: 9999px;\n    color: rgba(15, 23, 42, 1);\n    border: 1px solid rgba(15, 23, 42, 1)\n  }\n  .w-radio-group-item-3:disabled {\n    cursor: not-allowed;\n    opacity: 0.5\n  }\n  .w-radio-group-item-3:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as RadioGroup };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/select.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsSelect: Record<string, PropMeta> = {\n  autoComplete: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Hint for form autofill feature\",\n  },\n  defaultValue: { required: false, control: \"text\", type: \"string\" },\n  dir: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"ltr\", \"rtl\"],\n    description: \"The text directionality of the element\",\n  },\n  disabled: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the form control is disabled\",\n  },\n  form: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Associates the element with a form element\",\n  },\n  name: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description:\n      \"Name of the element to use for form submission and in the form.elements API\",\n  },\n  open: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the dialog box is showing\",\n  },\n  required: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is required for form submission\",\n  },\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsSelectTrigger: Record<string, PropMeta> = {};\nexport const propsSelectValue: Record<string, PropMeta> = {\n  placeholder: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"User-visible label to be placed within the form control\",\n  },\n};\nexport const propsSelectContent: Record<string, PropMeta> = {\n  align: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"center\", \"start\", \"end\"],\n  },\n  alignOffset: { required: false, control: \"number\", type: \"number\" },\n  arrowPadding: { required: false, control: \"number\", type: \"number\" },\n  avoidCollisions: { required: false, control: \"boolean\", type: \"boolean\" },\n  hideWhenDetached: { required: false, control: \"boolean\", type: \"boolean\" },\n  sideOffset: { required: false, control: \"number\", type: \"number\" },\n  sticky: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"partial\", \"always\"],\n  },\n  updatePositionStrategy: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"always\", \"optimized\"],\n  },\n};\nexport const propsSelectViewport: Record<string, PropMeta> = {\n  nonce: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description:\n      \"Cryptographic nonce used in Content Security Policy checks [CSP]\",\n  },\n};\nexport const propsSelectItem: Record<string, PropMeta> = {\n  disabled: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the form control is disabled\",\n  },\n  textValue: { required: false, control: \"text\", type: \"string\" },\n  value: {\n    required: true,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsSelectItemIndicator: Record<string, PropMeta> = {};\nexport const propsSelectItemText: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/select.stories.tsx",
    "content": "import {\n  Box as Box,\n  HtmlEmbed as HtmlEmbed,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Select as Select,\n  SelectTrigger as SelectTrigger,\n  SelectValue as SelectValue,\n  SelectContent as SelectContent,\n  SelectViewport as SelectViewport,\n  SelectItem as SelectItem,\n  SelectItemIndicator as SelectItemIndicator,\n  SelectItemText as SelectItemText,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Select>\n        <SelectTrigger className={\"w-select-trigger w-select-trigger-1\"}>\n          <SelectValue placeholder={\"Theme\"} className={\"w-value\"} />\n        </SelectTrigger>\n        <SelectContent className={\"w-select-content w-select-content-1\"}>\n          <SelectViewport className={\"w-select-viewport w-select-viewport-1\"}>\n            <SelectItem\n              value={\"light\"}\n              className={\"w-select-item w-select-item-1\"}\n            >\n              <SelectItemIndicator className={\"w-indicator w-indicator-1\"}>\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.091\" d=\"m13.636 3.667-8 8L2 8.03\"/></svg>'\n                  }\n                  className={\"w-html-embed\"}\n                />\n              </SelectItemIndicator>\n              <SelectItemText className={\"w-item-text\"}>\n                {\"Light\"}\n              </SelectItemText>\n            </SelectItem>\n            <SelectItem\n              value={\"dark\"}\n              className={\"w-select-item w-select-item-2\"}\n            >\n              <SelectItemIndicator className={\"w-indicator w-indicator-2\"}>\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.091\" d=\"m13.636 3.667-8 8L2 8.03\"/></svg>'\n                  }\n                  className={\"w-html-embed\"}\n                />\n              </SelectItemIndicator>\n              <SelectItemText className={\"w-item-text\"}>\n                {\"Dark\"}\n              </SelectItemText>\n            </SelectItem>\n            <SelectItem\n              value={\"system\"}\n              className={\"w-select-item w-select-item-3\"}\n            >\n              <SelectItemIndicator className={\"w-indicator w-indicator-3\"}>\n                <HtmlEmbed\n                  code={\n                    '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.091\" d=\"m13.636 3.667-8 8L2 8.03\"/></svg>'\n                  }\n                  className={\"w-html-embed\"}\n                />\n              </SelectItemIndicator>\n              <SelectItemText className={\"w-item-text\"}>\n                {\"System\"}\n              </SelectItemText>\n            </SelectItem>\n          </SelectViewport>\n        </SelectContent>\n      </Select>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Select\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-select-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-select-item) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(span.w-indicator) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(span.w-item-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-select-trigger) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(span.w-value) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-select-viewport) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-select-trigger-1 {\n    display: flex;\n    height: 2.5rem;\n    width: 100%;\n    align-items: center;\n    justify-content: between;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    background-color: rgba(255, 255, 255, 1);\n    padding-top: 0.5rem;\n    padding-right: 0.75rem;\n    padding-bottom: 0.5rem;\n    padding-left: 0.75rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-select-trigger-1::placeholder {\n    color: rgba(100, 116, 139, 1)\n  }\n  .w-select-trigger-1:disabled {\n    cursor: not-allowed;\n    opacity: 0.5\n  }\n  .w-select-trigger-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n  .w-select-content-1 {\n    position: relative;\n    z-index: 50;\n    min-width: 8rem;\n    overflow-x: hidden;\n    overflow-y: hidden;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    background-color: rgba(255, 255, 255, 1);\n    color: rgba(2, 8, 23, 1);\n    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-select-viewport-1 {\n    height: var(--radix-select-trigger-height);\n    width: 100%;\n    min-width: var(--radix-select-trigger-width);\n    padding: 0.25rem\n  }\n  .w-select-item-1 {\n    position: relative;\n    display: flex;\n    width: 100%;\n    cursor: default;\n    -webkit-user-select: none;\n    user-select: none;\n    align-items: center;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    padding-top: 0.375rem;\n    padding-right: 0.5rem;\n    padding-bottom: 0.375rem;\n    padding-left: 2rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    outline: medium none currentcolor\n  }\n  .w-select-item-1:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-select-item-1[data-disabled] {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-indicator-1 {\n    position: absolute;\n    left: 0.5rem;\n    display: flex;\n    height: 0.875rem;\n    width: 0.875rem;\n    align-items: center;\n    justify-content: center\n  }\n  .w-select-item-2 {\n    position: relative;\n    display: flex;\n    width: 100%;\n    cursor: default;\n    -webkit-user-select: none;\n    user-select: none;\n    align-items: center;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    padding-top: 0.375rem;\n    padding-right: 0.5rem;\n    padding-bottom: 0.375rem;\n    padding-left: 2rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    outline: medium none currentcolor\n  }\n  .w-select-item-2:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-select-item-2[data-disabled] {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-indicator-2 {\n    position: absolute;\n    left: 0.5rem;\n    display: flex;\n    height: 0.875rem;\n    width: 0.875rem;\n    align-items: center;\n    justify-content: center\n  }\n  .w-select-item-3 {\n    position: relative;\n    display: flex;\n    width: 100%;\n    cursor: default;\n    -webkit-user-select: none;\n    user-select: none;\n    align-items: center;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    padding-top: 0.375rem;\n    padding-right: 0.5rem;\n    padding-bottom: 0.375rem;\n    padding-left: 2rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    outline: medium none currentcolor\n  }\n  .w-select-item-3:focus {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-select-item-3[data-disabled] {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-indicator-3 {\n    position: absolute;\n    left: 0.5rem;\n    display: flex;\n    height: 0.875rem;\n    width: 0.875rem;\n    align-items: center;\n    justify-content: center\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Select };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/sheet.stories.tsx",
    "content": "import {\n  Box as Box,\n  Button as Button,\n  HtmlEmbed as HtmlEmbed,\n  Text as Text,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Dialog as Dialog,\n  DialogTrigger as DialogTrigger,\n  DialogOverlay as DialogOverlay,\n  DialogContent as DialogContent,\n  DialogTitle as DialogTitle,\n  DialogDescription as DialogDescription,\n  DialogClose as DialogClose,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Dialog>\n        <DialogTrigger>\n          <Button className={\"w-button w-button-1\"}>\n            <HtmlEmbed\n              code={\n                '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2.667 8h10.666M2.667 4h10.666M2.667 12h10.666\"/></svg>'\n              }\n              className={\"w-html-embed\"}\n            />\n          </Button>\n        </DialogTrigger>\n        <DialogOverlay className={\"w-dialog-overlay w-sheet-overlay\"}>\n          <DialogContent className={\"w-dialog-content w-sheet-content\"}>\n            <Box tag={\"nav\"} role={\"navigation\"} className={\"w-box\"}>\n              <Box className={\"w-box w-sheet-header\"}>\n                <DialogTitle className={\"w-dialog-title w-sheet-title\"}>\n                  {\"Sheet Title\"}\n                </DialogTitle>\n                <DialogDescription\n                  className={\"w-dialog-description w-sheet-description\"}\n                >\n                  {\"Sheet description text you can edit\"}\n                </DialogDescription>\n              </Box>\n              <Text className={\"w-text\"}>{\"The text you can edit\"}</Text>\n            </Box>\n            <DialogClose className={\"w-close-button w-close-button-1\"}>\n              <HtmlEmbed\n                code={\n                  '<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 16 16\" width=\"100%\" height=\"100%\" style=\"display: block;\"><path stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12.5 3 3 12.5M3 3l9.5 9.5\"/></svg>'\n                }\n                className={\"w-html-embed\"}\n              />\n            </DialogClose>\n          </DialogContent>\n        </DialogOverlay>\n      </Dialog>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Sheet\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(div.w-html-embed) {\n    display: contents;\n    white-space: normal;\n    white-space-collapse: collapse\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(button.w-close-button) {\n    background-color: transparent;\n    background-image: none;\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    border: 1px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n  :where(div.w-dialog-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(p.w-dialog-description) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-dialog-overlay) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(h2.w-dialog-title) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-button-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: transparent;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.5rem;\n    width: 2.5rem;\n    padding-top: 0px;\n    padding-right: 0.375rem;\n    padding-bottom: 0px;\n    padding-left: 0.375rem;\n    border: 0 solid rgba(226, 232, 240, 1)\n  }\n  .w-button-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-sheet-overlay {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 50;\n    background-color: rgba(255, 255, 255, 0.8);\n    -webkit-backdrop-filter: blur(0 1px 2px 0 rgb(0 0 0/0.05));\n    backdrop-filter: blur(0 1px 2px 0 rgb(0 0 0/0.05));\n    display: flex;\n    flex-direction: column;\n    overflow-x: auto;\n    overflow-y: auto\n  }\n  .w-sheet-content {\n    width: 100%;\n    z-index: 50;\n    display: flex;\n    flex-direction: column;\n    row-gap: 1rem;\n    column-gap: 1rem;\n    background-color: rgba(255, 255, 255, 1);\n    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n    position: relative;\n    margin-right: auto;\n    max-width: 24rem;\n    flex-grow: 1;\n    border: 1px solid rgba(226, 232, 240, 1);\n    padding: 1.5rem\n  }\n  .w-sheet-header {\n    display: flex;\n    flex-direction: column;\n    row-gap: 0.5rem;\n    column-gap: 0.5rem\n  }\n  .w-sheet-title {\n    font-size: 1.125rem;\n    line-height: 1;\n    letter-spacing: -0.025em;\n    margin: 0\n  }\n  .w-sheet-description {\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    color: rgba(100, 116, 139, 1);\n    margin: 0\n  }\n  .w-close-button-1 {\n    position: absolute;\n    right: 1rem;\n    top: 1rem;\n    border-top-left-radius: 0.125rem;\n    border-top-right-radius: 0.125rem;\n    border-bottom-right-radius: 0.125rem;\n    border-bottom-left-radius: 0.125rem;\n    opacity: 0.7;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 1rem;\n    width: 1rem;\n    background-color: transparent;\n    outline: medium none currentcolor;\n    border: 0 none currentcolor\n  }\n  .w-close-button-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1)\n  }\n  .w-close-button-1:hover {\n    opacity: 1\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Sheet };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/switch.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsSwitch: Record<string, PropMeta> = {\n  checked: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is checked\",\n  },\n  required: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether the control is required for form submission\",\n  },\n};\nexport const propsSwitchThumb: Record<string, PropMeta> = {};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/switch.stories.tsx",
    "content": "import { Box as Box } from \"@webstudio-is/sdk-components-react\";\nimport { Switch as Switch, SwitchThumb as SwitchThumb } from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Switch className={\"w-switch w-switch-1\"}>\n        <SwitchThumb className={\"w-switch-thumb w-switch-thumb-1\"} />\n      </Switch>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Switch\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-switch) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    background-color: transparent;\n    background-image: none;\n    border: 0px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n  :where(span.w-switch-thumb) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-switch-1 {\n    display: inline-flex;\n    height: 24px;\n    width: 44px;\n    flex-shrink: 0;\n    cursor: pointer;\n    align-items: center;\n    border-top-left-radius: 9999px;\n    border-top-right-radius: 9999px;\n    border-bottom-right-radius: 9999px;\n    border-bottom-left-radius: 9999px;\n    transition-property: all;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal;\n    border: 2px solid transparent\n  }\n  .w-switch-1:disabled {\n    cursor: not-allowed;\n    opacity: 0.5\n  }\n  .w-switch-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n  .w-switch-1[data-state=\"checked\"] {\n    background-color: rgba(15, 23, 42, 1)\n  }\n  .w-switch-1[data-state=\"unchecked\"] {\n    background-color: rgba(226, 232, 240, 1)\n  }\n  .w-switch-thumb-1 {\n    pointer-events: none;\n    display: block;\n    height: 1.25rem;\n    width: 1.25rem;\n    border-top-left-radius: 9999px;\n    border-top-right-radius: 9999px;\n    border-bottom-right-radius: 9999px;\n    border-bottom-left-radius: 9999px;\n    background-color: rgba(255, 255, 255, 1);\n    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);\n    transition-property: transform;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal\n  }\n  .w-switch-thumb-1[data-state=\"checked\"] {\n    transform: translateX(20px)\n  }\n  .w-switch-thumb-1[data-state=\"unchecked\"] {\n    transform: translateX(0px)\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Switch };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/tabs.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsTabs: Record<string, PropMeta> = {\n  activationMode: {\n    description:\n      \"Whether a tab is activated automatically or manually.\\n@defaultValue automatic\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"automatic\", \"manual\"],\n  },\n  defaultValue: {\n    description: \"The value of the tab to select by default, if uncontrolled\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  dir: {\n    description: \"The direction of navigation between toolbar items.\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"ltr\", \"rtl\"],\n  },\n  orientation: {\n    description:\n      \"The orientation the tabs are layed out.\\nMainly so arrow navigation is done accordingly (left & right vs. up & down)\\n@defaultValue horizontal\",\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"horizontal\", \"vertical\"],\n  },\n  value: {\n    description: \"The value for the selected tab, if controlled\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n};\nexport const propsTabsList: Record<string, PropMeta> = {\n  loop: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description: \"Whether to loop the media resource\",\n  },\n};\nexport const propsTabsTrigger: Record<string, PropMeta> = {\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\nexport const propsTabsContent: Record<string, PropMeta> = {\n  value: {\n    required: false,\n    control: \"text\",\n    type: \"string\",\n    description: \"Current value of the element\",\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/tabs.stories.tsx",
    "content": "import { Box as Box } from \"@webstudio-is/sdk-components-react\";\nimport {\n  Tabs as Tabs,\n  TabsList as TabsList,\n  TabsTrigger as TabsTrigger,\n  TabsContent as TabsContent,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Tabs defaultValue={\"0\"} className={\"w-tabs\"}>\n        <TabsList className={\"w-tabs-list w-tabs-list-1\"}>\n          <TabsTrigger\n            data-ws-index=\"0\"\n            className={\"w-tab-trigger w-tab-trigger-1\"}\n          >\n            {\"Account\"}\n          </TabsTrigger>\n          <TabsTrigger\n            data-ws-index=\"1\"\n            className={\"w-tab-trigger w-tab-trigger-2\"}\n          >\n            {\"Password\"}\n          </TabsTrigger>\n        </TabsList>\n        <TabsContent\n          data-ws-index=\"0\"\n          className={\"w-tab-content w-tab-content-1\"}\n        >\n          {\"Make changes to your account here.\"}\n        </TabsContent>\n        <TabsContent\n          data-ws-index=\"1\"\n          className={\"w-tab-content w-tab-content-2\"}\n        >\n          {\"Change your password here.\"}\n        </TabsContent>\n      </Tabs>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Tabs\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-tabs) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-tab-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(div.w-tabs-list) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-tab-trigger) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    text-transform: none;\n    background-color: transparent;\n    background-image: none;\n    border: 0px solid rgba(226, 232, 240, 1);\n    margin: 0;\n    padding: 0px\n  }\n}\n@media all {\n  .w-tabs-list-1 {\n    display: inline-flex;\n    height: 2.5rem;\n    align-items: center;\n    justify-content: center;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(100, 116, 139, 1);\n    padding: 0.25rem\n  }\n  .w-tab-trigger-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    padding-top: 0.375rem;\n    padding-right: 0.75rem;\n    padding-bottom: 0.375rem;\n    padding-left: 0.75rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    transition-property: all;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal;\n    white-space: nowrap;\n    white-space-collapse: collapse\n  }\n  .w-tab-trigger-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-tab-trigger-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-tab-trigger-1[data-state=\"active\"] {\n    background-color: rgba(255, 255, 255, 1);\n    color: rgba(2, 8, 23, 1);\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)\n  }\n  .w-tab-trigger-2 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    padding-top: 0.375rem;\n    padding-right: 0.75rem;\n    padding-bottom: 0.375rem;\n    padding-left: 0.75rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    transition-property: all;\n    transition-duration: 150ms;\n    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n    transition-delay: 0s;\n    transition-behavior: normal;\n    white-space: nowrap;\n    white-space-collapse: collapse\n  }\n  .w-tab-trigger-2:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-tab-trigger-2:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-tab-trigger-2[data-state=\"active\"] {\n    background-color: rgba(255, 255, 255, 1);\n    color: rgba(2, 8, 23, 1);\n    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05)\n  }\n  .w-tab-content-1 {\n    margin-top: 0.5rem\n  }\n  .w-tab-content-1:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n  .w-tab-content-2 {\n    margin-top: 0.5rem\n  }\n  .w-tab-content-2:focus-visible {\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: medium none currentcolor\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Tabs };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/tooltip.props.ts",
    "content": "import type { PropMeta } from \"@webstudio-is/sdk\";\n\nexport const propsTooltip: Record<string, PropMeta> = {\n  delayDuration: {\n    description:\n      \"The delay before the Tooltip shows after the Trigger is hovered, in milliseconds. If no value is specified, the default is 700ms\",\n    required: false,\n    control: \"number\",\n    type: \"number\",\n  },\n  disableHoverableContent: {\n    description:\n      \"When toggled, prevents the Tooltip content from showing when the Trigger is hovered.\",\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n  },\n  open: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    description:\n      \"Show or hide the content of this component on the canvas. This will not affect the initial state of the component.\",\n  },\n};\nexport const propsTooltipTrigger: Record<string, PropMeta> = {};\nexport const propsTooltipContent: Record<string, PropMeta> = {\n  align: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"center\", \"start\", \"end\"],\n  },\n  alignOffset: {\n    required: false,\n    control: \"number\",\n    type: \"number\",\n    description:\n      \"The offset in pixels from the “start“ or “end“ alignment options.\",\n  },\n  \"aria-label\": {\n    description: \"A more descriptive label for accessibility purpose\",\n    required: false,\n    control: \"text\",\n    type: \"string\",\n  },\n  arrowPadding: { required: false, control: \"number\", type: \"number\" },\n  avoidCollisions: { required: false, control: \"boolean\", type: \"boolean\" },\n  hideWhenDetached: {\n    required: false,\n    control: \"boolean\",\n    type: \"boolean\",\n    defaultValue: true,\n  },\n  side: {\n    required: false,\n    control: \"select\",\n    type: \"string\",\n    options: [\"top\", \"right\", \"bottom\", \"left\"],\n    description:\n      \"The preferred alignment against the Trigger. May change when collisions occur.\",\n  },\n  sideOffset: {\n    required: false,\n    control: \"number\",\n    type: \"number\",\n    defaultValue: 4,\n    description: \"The distance in pixels between the Content and the Trigger.\",\n  },\n  sticky: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"partial\", \"always\"],\n  },\n  updatePositionStrategy: {\n    required: false,\n    control: \"radio\",\n    type: \"string\",\n    options: [\"always\", \"optimized\"],\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/__generated__/tooltip.stories.tsx",
    "content": "import {\n  Box as Box,\n  Button as Button,\n  Text as Text,\n} from \"@webstudio-is/sdk-components-react\";\nimport {\n  Tooltip as Tooltip,\n  TooltipTrigger as TooltipTrigger,\n  TooltipContent as TooltipContent,\n} from \"../components\";\n\nconst Component = () => {\n  return (\n    <Box className={\"w-box\"}>\n      <Tooltip>\n        <TooltipTrigger>\n          <Button className={\"w-button w-button-1\"}>{\"Button\"}</Button>\n        </TooltipTrigger>\n        <TooltipContent className={\"w-tooltip-content w-tooltip-content-1\"}>\n          <Text className={\"w-text\"}>{\"The text you can edit\"}</Text>\n        </TooltipContent>\n      </Tooltip>\n    </Box>\n  );\n};\n\nexport default {\n  title: \"Tooltip\",\n};\n\nconst Story = {\n  render() {\n    return (\n      <>\n        <style>\n          {`\n@media all {\n  :where(div.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(address.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(article.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(aside.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(figure.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(footer.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(header.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(main.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(nav.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(section.w-box) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n  :where(button.w-button) {\n    font-family: inherit;\n    font-size: 100%;\n    line-height: 1.15;\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    border-top-style: solid;\n    border-right-style: solid;\n    border-bottom-style: solid;\n    border-left-style: solid;\n    text-transform: none;\n    margin: 0\n  }\n  :where(div.w-text) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px;\n    min-height: 1em\n  }\n  :where(div.w-tooltip-content) {\n    box-sizing: border-box;\n    border-top-width: 1px;\n    border-right-width: 1px;\n    border-bottom-width: 1px;\n    border-left-width: 1px;\n    outline-width: 1px\n  }\n}\n@media all {\n  .w-button-1 {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: rgba(255, 255, 255, 1);\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    font-weight: 500;\n    height: 2.5rem;\n    padding-top: 0.5rem;\n    padding-right: 1rem;\n    padding-bottom: 0.5rem;\n    padding-left: 1rem;\n    border: 1px solid rgba(226, 232, 240, 1)\n  }\n  .w-button-1:disabled {\n    pointer-events: none;\n    opacity: 0.5\n  }\n  .w-button-1:focus-visible {\n    outline-offset: 2px;\n    box-shadow: 0 0 0 2px rgba(255, 255, 255, 1), 0 0 0 calc(2px + 2px) rgba(148, 163, 184, 1);\n    outline: 2px solid transparent\n  }\n  .w-button-1:hover {\n    background-color: rgba(241, 245, 249, 1);\n    color: rgba(15, 23, 42, 1)\n  }\n  .w-tooltip-content-1 {\n    z-index: 50;\n    overflow-x: hidden;\n    overflow-y: hidden;\n    border-top-left-radius: 0.375rem;\n    border-top-right-radius: 0.375rem;\n    border-bottom-right-radius: 0.375rem;\n    border-bottom-left-radius: 0.375rem;\n    background-color: rgba(255, 255, 255, 1);\n    padding-top: 0.375rem;\n    padding-right: 0.75rem;\n    padding-bottom: 0.375rem;\n    padding-left: 0.75rem;\n    font-size: 0.875rem;\n    line-height: 1.25rem;\n    color: rgba(2, 8, 23, 1);\n    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)\n  }\n}\n      `}\n        </style>\n        <Component />\n      </>\n    );\n  },\n};\n\nexport { Story as Tooltip };\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/accordion.template.tsx",
    "content": "import {\n  $,\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  borderWidth,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  height,\n  spacing,\n  transition,\n  weights,\n  width,\n} from \"./shared/theme\";\nimport { ChevronDownIcon } from \"@webstudio-is/icons/svg\";\n\nconst createAccordionItem = (triggerText: string, contentText: string) => {\n  return (\n    <radix.AccordionItem\n      // border-b\n      ws:style={css`\n        border-bottom: ${borderWidth.DEFAULT} solid ${colors.border};\n      `}\n    >\n      <radix.AccordionHeader\n        // flex\n        ws:style={css`\n          display: flex;\n        `}\n      >\n        <radix.AccordionTrigger\n          // flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180\n          ws:style={css`\n            display: flex;\n            flex: 1 1 0;\n            align-items: center;\n            justify-content: between;\n            padding: ${spacing[4]} 0;\n            font-weight: ${weights.medium};\n            --accordion-trigger-icon-transform: 0deg;\n            &:hover {\n              text-decoration-line: underline;\n            }\n            &[data-state=\"open\"] {\n              --accordion-trigger-icon-transform: 180deg;\n            }\n          `}\n        >\n          <$.Text>{new PlaceholderValue(triggerText)}</$.Text>\n          <$.Box\n            ws:label=\"Icon Container\"\n            // h-4 w-4 shrink-0 transition-transform duration-200\n            ws:style={css`\n              rotate: var(--accordion-trigger-icon-transform);\n              height: ${height[4]};\n              width: ${width[4]};\n              flex-shrink: 0;\n              transition: ${transition.all};\n              transition-duration: 200ms;\n            `}\n          >\n            <$.HtmlEmbed ws:label=\"Chevron Icon\" code={ChevronDownIcon} />\n          </$.Box>\n        </radix.AccordionTrigger>\n      </radix.AccordionHeader>\n      <radix.AccordionContent\n        // overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down\n        // pb-4 pt-0\n        ws:style={css`\n          overflow: hidden;\n          font-size: ${fontSize.sm};\n          line-height: ${fontSizeLineHeight.sm};\n          transition: ${transition.all};\n          padding-bottom: ${spacing[4]};\n        `}\n      >\n        {new PlaceholderValue(contentText)}\n      </radix.AccordionContent>\n    </radix.AccordionItem>\n  );\n};\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/accordion.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"A vertically stacked set of interactive headings that each reveal an associated section of content. Clicking on the heading will open the item and close other items.\",\n  order: 3,\n  template: (\n    <radix.Accordion collapsible={true} value=\"0\">\n      {createAccordionItem(\n        \"Is it accessible?\",\n        \"Yes. It adheres to the WAI-ARIA design pattern.\"\n      )}\n      {createAccordionItem(\n        \"Is it styled?\",\n        \"Yes. It comes with default styles that matches the other components' aesthetic.\"\n      )}\n      {createAccordionItem(\n        \"Is it animated?\",\n        \"Yes. It's animated by default, but you can disable it if you prefer.\"\n      )}\n    </radix.Accordion>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/accordion.tsx",
    "content": "import {\n  type ComponentPropsWithoutRef,\n  type ForwardRefExoticComponent,\n  forwardRef,\n  type ComponentProps,\n  type RefAttributes,\n  useState,\n  useEffect,\n} from \"react\";\nimport {\n  Root,\n  Item,\n  Header,\n  Trigger,\n  Content,\n} from \"@radix-ui/react-accordion\";\nimport { getIndexWithinAncestorFromProps } from \"@webstudio-is/sdk/runtime\";\nimport { getClosestInstance, type Hook } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Accordion = forwardRef<\n  HTMLDivElement,\n  Omit<\n    Extract<ComponentPropsWithoutRef<typeof Root>, { type: \"single\" }>,\n    \"type\" | \"asChild\"\n  >\n>(({ defaultValue, ...props }, ref) => {\n  const currentValue = props.value ?? defaultValue ?? \"\";\n  const [value, setValue] = useState(currentValue);\n  // synchronize external value with local one when changed\n  useEffect(() => setValue(currentValue), [currentValue]);\n  return (\n    <Root\n      {...props}\n      ref={ref}\n      type=\"single\"\n      value={value}\n      onValueChange={setValue}\n    />\n  );\n});\n\nexport const AccordionItem = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentPropsWithoutRef<typeof Item>, \"value\"> & { value?: string }\n>(({ value, ...props }, ref) => {\n  const index = getIndexWithinAncestorFromProps(props);\n  return <Item ref={ref} value={value ?? index ?? \"\"} {...props} />;\n});\n\nexport const AccordionHeader: ForwardRefExoticComponent<\n  Omit<ComponentProps<typeof Header>, \"asChild\"> &\n    RefAttributes<HTMLHeadingElement>\n> = Header;\n\nexport const AccordionTrigger: ForwardRefExoticComponent<\n  Omit<ComponentProps<typeof Trigger>, \"asChild\"> &\n    RefAttributes<HTMLButtonElement>\n> = Trigger;\n\nexport const AccordionContent: ForwardRefExoticComponent<\n  Omit<ComponentProps<typeof Content>, \"asChild\"> &\n    RefAttributes<HTMLDivElement>\n> = Content;\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each AccordionContent component within the selection,\n// we identify its closest parent Accordion component\n// and update its open prop bound to variable.\nexport const hooksAccordion: Hook = {\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:AccordionContent`) {\n        const accordion = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Accordion`\n        );\n        const item = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:AccordionItem`\n        );\n        if (accordion && item) {\n          const itemValue =\n            context.getPropValue(item, \"value\") ??\n            context.indexesWithinAncestors.get(item.id)?.toString();\n\n          if (itemValue) {\n            context.setMemoryProp(accordion, \"value\", itemValue);\n          }\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/accordion.ws.ts",
    "content": "import {\n  AccordionIcon,\n  ItemIcon,\n  HeaderIcon,\n  TriggerIcon,\n  ContentIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div, h3, button } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport { buttonReset } from \"./shared/preset-styles\";\nimport {\n  propsAccordion,\n  propsAccordionItem,\n  propsAccordionHeader,\n  propsAccordionTrigger,\n  propsAccordionContent,\n} from \"./__generated__/accordion.props\";\n\nexport const metaAccordion: WsComponentMeta = {\n  icon: AccordionIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.AccordionItem],\n  },\n  presetStyle: { div },\n  initialProps: [\"value\", \"collapsible\"],\n  props: propsAccordion,\n};\n\nexport const metaAccordionItem: WsComponentMeta = {\n  label: \"Item\",\n  icon: ItemIcon,\n  indexWithinAncestor: radix.Accordion,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.AccordionHeader, radix.AccordionContent],\n  },\n  presetStyle: { div },\n  initialProps: [\"value\"],\n  props: propsAccordionItem,\n};\n\nexport const metaAccordionHeader: WsComponentMeta = {\n  label: \"Item Header\",\n  icon: HeaderIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.AccordionTrigger],\n  },\n  presetStyle: {\n    h3: [\n      ...h3,\n      {\n        property: \"margin-top\",\n        value: { type: \"unit\", unit: \"px\", value: 0 },\n      },\n      {\n        property: \"margin-bottom\",\n        value: { type: \"unit\", unit: \"px\", value: 0 },\n      },\n    ],\n  },\n  props: propsAccordionHeader,\n};\n\nexport const metaAccordionTrigger: WsComponentMeta = {\n  label: \"Item Trigger\",\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  states: [{ label: \"Open\", selector: \"[data-state=open]\" }],\n  presetStyle: {\n    button: [button, buttonReset].flat(),\n  },\n  props: propsAccordionTrigger,\n};\n\nexport const metaAccordionContent: WsComponentMeta = {\n  label: \"Item Content\",\n  icon: ContentIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsAccordionContent,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/checkbox.template.tsx",
    "content": "import { CheckMarkIcon } from \"@webstudio-is/icons/svg\";\nimport {\n  type TemplateMeta,\n  $,\n  css,\n  PlaceholderValue,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  height,\n  opacity,\n  spacing,\n  width,\n} from \"./shared/theme\";\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"Use within a form to allow your users to toggle between checked and not checked. Group checkboxes by matching their “Name” properties. Unlike radios, any number of checkboxes in a group can be checked.\",\n  order: 101,\n  template: (\n    <radix.Label\n      ws:label=\"Checkbox Field\"\n      ws:style={css`\n        display: flex;\n        gap: ${spacing[2]};\n        align-items: center;\n      `}\n    >\n      <radix.Checkbox\n        // peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background\n        // focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\n        // disabled:cursor-not-allowed disabled:opacity-50\n        // data-[state=checked]:bg-primary\n        // data-[state=checked]:text-primary-foreground\",\n        ws:style={css`\n          height: ${height[4]};\n          width: ${width[4]};\n          flex-shrink: 0;\n          border-radius: ${borderRadius.sm};\n          border: ${borderWidth.DEFAULT} solid ${colors.primary};\n          &:focus-visible {\n            outline: none;\n            box-shadow: ${boxShadow.ring};\n          }\n          &:disabled {\n            cursor: not-allowed;\n            opacity: ${opacity[50]};\n          }\n          &[data-state=\"checked\"] {\n            background-color: ${colors.primary};\n            color: ${colors.primaryForeground};\n          }\n        `}\n      >\n        <radix.CheckboxIndicator\n          // flex items-center justify-center text-current\n          ws:style={css`\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            color: currentColor;\n          `}\n        >\n          <$.HtmlEmbed ws:label=\"Indicator Icon\" code={CheckMarkIcon} />\n        </radix.CheckboxIndicator>\n      </radix.Checkbox>\n      <$.Text ws:label=\"Checkbox Label\" ws:tag=\"span\">\n        {new PlaceholderValue(\"Checkbox\")}\n      </$.Text>\n    </radix.Label>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/checkbox.tsx",
    "content": "import {\n  type ForwardRefExoticComponent,\n  type ComponentPropsWithRef,\n  forwardRef,\n  type ComponentProps,\n  useState,\n  useEffect,\n} from \"react\";\nimport { Root, Indicator } from \"@radix-ui/react-checkbox\";\n\nexport const Checkbox = forwardRef<\n  HTMLButtonElement,\n  // radix checked has complex named type which cannot be parsed\n  // cast to boolean\n  Omit<ComponentPropsWithRef<typeof Root>, \"checked\" | \"defaultChecked\"> & {\n    checked?: boolean;\n    defaultChecked?: boolean;\n  }\n>(({ defaultChecked, ...props }, ref) => {\n  const currentChecked = props.checked ?? defaultChecked ?? false;\n  const [checked, setChecked] = useState(currentChecked);\n  // synchronize external value with local one when changed\n  useEffect(() => setChecked(currentChecked), [currentChecked]);\n  return (\n    <Root\n      {...props}\n      ref={ref}\n      checked={checked}\n      onCheckedChange={(open) => setChecked(open === true)}\n    />\n  );\n});\n\nexport const CheckboxIndicator: ForwardRefExoticComponent<\n  ComponentProps<typeof Indicator> & React.RefAttributes<HTMLSpanElement>\n> = Indicator;\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/checkbox.ws.ts",
    "content": "import { CheckboxCheckedIcon, TriggerIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button, span } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport { buttonReset } from \"./shared/preset-styles\";\nimport {\n  propsCheckbox,\n  propsCheckboxIndicator,\n} from \"./__generated__/checkbox.props\";\n\nexport const metaCheckbox: WsComponentMeta = {\n  icon: CheckboxCheckedIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.CheckboxIndicator],\n  },\n  states: [\n    { label: \"Checked\", selector: \"[data-state=checked]\" },\n    { label: \"Unchecked\", selector: \"[data-state=unchecked]\" },\n  ],\n  presetStyle: {\n    button: [button, buttonReset].flat(),\n  },\n  initialProps: [\"id\", \"class\", \"name\", \"value\", \"required\", \"checked\"],\n  props: propsCheckbox,\n};\n\nexport const metaCheckboxIndicator: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: {\n    span,\n  },\n  props: propsCheckboxIndicator,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/collapsible.template.tsx",
    "content": "import { $, PlaceholderValue, type TemplateMeta } from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport { getButtonStyle } from \"./shared/styles\";\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"An interactive component which expands and collapses some content, triggered by a button.\",\n  order: 5,\n  template: (\n    <radix.Collapsible>\n      <radix.CollapsibleTrigger>\n        <$.Button ws:style={getButtonStyle(\"outline\")}>\n          {new PlaceholderValue(\"Click to toggle content\")}\n        </$.Button>\n      </radix.CollapsibleTrigger>\n      <radix.CollapsibleContent>\n        <$.Text>{new PlaceholderValue(\"Collapsible Content\")}</$.Text>\n      </radix.CollapsibleContent>\n    </radix.Collapsible>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/collapsible.tsx",
    "content": "import {\n  type ReactNode,\n  type ForwardRefExoticComponent,\n  forwardRef,\n  Children,\n  type ComponentProps,\n  type RefAttributes,\n  useState,\n  useEffect,\n} from \"react\";\nimport { Root, Trigger, Content } from \"@radix-ui/react-collapsible\";\nimport { type Hook, getClosestInstance } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Collapsible = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentProps<typeof Root>, \"defaultOpen\" | \"asChild\">\n>((props, ref) => {\n  const currentOpen = props.open ?? false;\n  const [open, setOpen] = useState(currentOpen);\n  // synchronize external value with local one when changed\n  useEffect(() => setOpen(currentOpen), [currentOpen]);\n  return <Root {...props} ref={ref} open={open} onOpenChange={setOpen} />;\n});\n\n/**\n * We're not exposing the 'asChild' property for the Trigger.\n * Instead, we're enforcing 'asChild=true' for the Trigger and making it style-less.\n * This avoids situations where the Trigger inadvertently passes all styles to its child,\n * which would prevent us from displaying styles properly in the builder.\n */\nexport const CollapsibleTrigger = forwardRef<\n  HTMLButtonElement,\n  { children: ReactNode }\n>(({ children, ...props }, ref) => {\n  const firstChild = Children.toArray(children)[0];\n  return (\n    <Trigger asChild={true} ref={ref} {...props}>\n      {firstChild ?? <button>Add button</button>}\n    </Trigger>\n  );\n});\n\nexport const CollapsibleContent: ForwardRefExoticComponent<\n  Omit<ComponentProps<typeof Content>, \"asChild\"> &\n    RefAttributes<HTMLDivElement>\n> = Content;\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each CollapsibleContent component within the selection,\n// we identify its closest parent Collapsible component\n// and update its open prop bound to variable.\nexport const hooksCollapsible: Hook = {\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:CollapsibleContent`) {\n        const collapsible = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Collapsible`\n        );\n        if (collapsible) {\n          context.setMemoryProp(collapsible, \"open\", true);\n        }\n      }\n    }\n  },\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:CollapsibleContent`) {\n        const collapsible = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Collapsible`\n        );\n        if (collapsible) {\n          context.setMemoryProp(collapsible, \"open\", undefined);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/collapsible.ws.ts",
    "content": "import {\n  CollapsibleIcon,\n  TriggerIcon,\n  ContentIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport {\n  propsCollapsible,\n  propsCollapsibleContent,\n  propsCollapsibleTrigger,\n} from \"./__generated__/collapsible.props\";\n\nexport const metaCollapsible: WsComponentMeta = {\n  icon: CollapsibleIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.CollapsibleTrigger, radix.CollapsibleContent],\n  },\n  presetStyle: {\n    div,\n  },\n  initialProps: [\"open\"],\n  props: propsCollapsible,\n};\n\nexport const metaCollapsibleTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  props: propsCollapsibleTrigger,\n};\n\nexport const metaCollapsibleContent: WsComponentMeta = {\n  icon: ContentIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsCollapsibleContent,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/components.ts",
    "content": "export {\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n} from \"./collapsible\";\nexport {\n  Dialog,\n  DialogTrigger,\n  DialogOverlay,\n  DialogContent,\n  DialogClose,\n  DialogTitle,\n  DialogDescription,\n} from \"./dialog\";\nexport {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n  PopoverClose,\n} from \"./popover\";\nexport { Tooltip, TooltipTrigger, TooltipContent } from \"./tooltip\";\nexport { Tabs, TabsList, TabsTrigger, TabsContent } from \"./tabs\";\nexport { Label } from \"./label\";\nexport {\n  Accordion,\n  AccordionItem,\n  AccordionHeader,\n  AccordionTrigger,\n  AccordionContent,\n} from \"./accordion\";\n\nexport {\n  NavigationMenu,\n  NavigationMenuList,\n  NavigationMenuItem,\n  NavigationMenuTrigger,\n  NavigationMenuContent,\n  NavigationMenuLink,\n  NavigationMenuViewport,\n} from \"./navigation-menu\";\n\nexport {\n  Select,\n  SelectTrigger,\n  SelectValue,\n  SelectViewport,\n  SelectContent,\n  SelectItem,\n  SelectItemIndicator,\n  SelectItemText,\n} from \"./select\";\n\nexport { Switch, SwitchThumb } from \"./switch\";\nexport { Checkbox, CheckboxIndicator } from \"./checkbox\";\nexport { RadioGroup, RadioGroupItem, RadioGroupIndicator } from \"./radio-group\";\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/dialog.template.tsx",
    "content": "import { LargeXIcon } from \"@webstudio-is/icons/svg\";\nimport {\n  type TemplateMeta,\n  $,\n  css,\n  PlaceholderValue,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  blur,\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  height,\n  letterSpacing,\n  lineHeight,\n  maxWidth,\n  opacity,\n  spacing,\n  width,\n  zIndex,\n} from \"./shared/theme\";\nimport { getButtonStyle } from \"./shared/styles\";\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/dialog.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"Displays content with an overlay that covers the window, triggered by a button. Clicking the overlay will close the dialog.\",\n  order: 4,\n  template: (\n    <radix.Dialog>\n      <radix.DialogTrigger>\n        <$.Button ws:style={getButtonStyle(\"outline\")}>\n          {new PlaceholderValue(\"Button\")}\n        </$.Button>\n      </radix.DialogTrigger>\n      <radix.DialogOverlay\n        /**\n         * fixed inset-0 z-50 bg-background/80 backdrop-blur-sm\n         * flex\n         **/\n        ws:style={css`\n          position: fixed;\n          inset: 0;\n          z-index: ${zIndex[50]};\n          background-color: rgb(255 255 255 / 0.8);\n          backdrop-filter: ${blur.sm};\n          /* To allow positioning Content */\n          display: flex;\n          overflow: auto;\n        `}\n      >\n        <radix.DialogContent\n          /**\n           * fixed w-full z-50\n           * grid gap-4 max-w-lg\n           * m-auto\n           * border bg-background p-6 shadow-lg\n           **/\n          ws:style={css`\n            width: ${width.full};\n            z-index: ${zIndex[50]};\n            display: flex;\n            flex-direction: column;\n            gap: ${spacing[4]};\n            margin: auto;\n            max-width: ${maxWidth.lg};\n            border: ${borderWidth.DEFAULT} solid ${colors.border};\n            background-color: ${colors.background};\n            padding: ${spacing[6]};\n            box-shadow: ${boxShadow.lg};\n            position: relative;\n          `}\n        >\n          <$.Box\n            ws:label=\"Dialog Header\"\n            ws:style={css`\n              display: flex;\n              flex-direction: column;\n              gap: ${spacing[2]};\n            `}\n          >\n            <radix.DialogTitle\n              /**\n               * text-lg leading-none tracking-tight\n               **/\n              ws:style={css`\n                font-size: ${fontSize.lg};\n                line-height: ${lineHeight.none};\n                letter-spacing: ${letterSpacing.tight};\n                margin: 0;\n              `}\n            >\n              {new PlaceholderValue(\"Dialog Title you can edit\")}\n            </radix.DialogTitle>\n            <radix.DialogDescription\n              /**\n               * text-sm text-muted-foreground\n               **/\n              ws:style={css`\n                font-size: ${fontSize.sm};\n                line-height: ${fontSizeLineHeight.sm};\n                color: ${colors.mutedForeground};\n                margin: 0;\n              `}\n            >\n              {new PlaceholderValue(\"Dialog description text you can edit\")}\n            </radix.DialogDescription>\n          </$.Box>\n          <$.Text>{new PlaceholderValue(\"The text you can edit\")}</$.Text>\n          <radix.DialogClose\n            ws:label=\"Close Button\"\n            /**\n             * absolute right-4 top-4\n             * rounded-sm opacity-70\n             * ring-offset-background\n             * hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\n             * flex items-center justify-center h-4 w-4\n             **/\n            ws:style={css`\n              position: absolute;\n              right: ${spacing[4]};\n              top: ${spacing[4]};\n              border-radius: ${borderRadius.sm};\n              opacity: ${opacity[70]};\n              display: flex;\n              align-items: center;\n              justify-content: center;\n              height: ${height[4]};\n              width: ${height[4]};\n              border: 0;\n              background-color: transparent;\n              outline: none;\n              &:hover {\n                opacity: ${opacity[100]};\n              }\n              &:focus-visible {\n                box-shadow: ${boxShadow.ring};\n              }\n            `}\n          >\n            <$.HtmlEmbed ws:label=\"Close Icon\" code={LargeXIcon} />\n          </radix.DialogClose>\n        </radix.DialogContent>\n      </radix.DialogOverlay>\n    </radix.Dialog>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/dialog.tsx",
    "content": "import interactionResponse from \"await-interaction-response\";\nimport {\n  type ReactNode,\n  type ComponentProps,\n  forwardRef,\n  Children,\n  useEffect,\n  useRef,\n  useContext,\n  useCallback,\n  useState,\n} from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport {\n  ReactSdkContext,\n  getClosestInstance,\n  type Hook,\n} from \"@webstudio-is/react-sdk/runtime\";\n\n/**\n * Naive heuristic to determine if a click event will cause navigate\n */\nconst willNavigate = (event: MouseEvent) => {\n  const { target } = event;\n\n  if (target instanceof HTMLAnchorElement === false) {\n    return false;\n  }\n\n  if (target.hasAttribute(\"href\") === false) {\n    return false;\n  }\n\n  if (target.href === \"#\") {\n    return false;\n  }\n\n  if (target.hasAttribute(\"target\") && target.target === \"_blank\") {\n    return false;\n  }\n\n  if (event.ctrlKey || event.metaKey) {\n    return false;\n  }\n\n  return true;\n};\n\n// wrap in forwardRef because Root is functional component without ref\nexport const Dialog = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentProps<typeof DialogPrimitive.Root>, \"defaultOpen\">\n>((props, _ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n\n  const currentOpen = props.open ?? false;\n  const [open, setOpen] = useState(currentOpen);\n  // synchronize external value with local one when changed\n  useEffect(() => setOpen(currentOpen), [currentOpen]);\n\n  const onOpenChangeHandler = useCallback(async (open: boolean) => {\n    await interactionResponse();\n    setOpen(open);\n  }, []);\n\n  /**\n   * Close the dialog when a navigable link within it is clicked.\n   */\n  useEffect(() => {\n    if (renderer !== undefined) {\n      return;\n    }\n\n    if (open === false) {\n      return;\n    }\n\n    const handleClick = (event: MouseEvent) => {\n      const { target } = event;\n\n      if (willNavigate(event) === false) {\n        return;\n      }\n\n      if (target instanceof HTMLAnchorElement === false) {\n        return false;\n      }\n\n      if (target.closest('[role=\"dialog\"]')) {\n        onOpenChangeHandler?.(false);\n      }\n    };\n\n    window.addEventListener(\"click\", handleClick);\n    return () => window.removeEventListener(\"click\", handleClick);\n  }, [open, onOpenChangeHandler, renderer]);\n\n  return (\n    <DialogPrimitive.Root\n      {...props}\n      onOpenChange={onOpenChangeHandler}\n      open={open}\n    />\n  );\n});\n\n/**\n * We're not exposing the 'asChild' property for the Trigger.\n * Instead, we're enforcing 'asChild=true' for the Trigger and making it style-less.\n * This avoids situations where the Trigger inadvertently passes all styles to its child,\n * which would prevent us from displaying styles properly in the builder.\n */\nexport const DialogTrigger = forwardRef<\n  HTMLButtonElement,\n  { children: ReactNode }\n>(({ children, ...props }, ref) => {\n  const firstChild = Children.toArray(children)[0];\n\n  return (\n    <DialogPrimitive.Trigger ref={ref} asChild={true} {...props}>\n      {firstChild ?? <button>Add button or link</button>}\n    </DialogPrimitive.Trigger>\n  );\n});\n\nexport const DialogOverlay = forwardRef<\n  HTMLDivElement,\n  ComponentProps<typeof DialogPrimitive.Overlay>\n>((props, ref) => {\n  return (\n    <DialogPrimitive.DialogPortal>\n      <DialogPrimitive.Overlay ref={ref} {...props} />\n    </DialogPrimitive.DialogPortal>\n  );\n});\n\nexport const DialogContent = forwardRef<\n  HTMLDivElement,\n  ComponentProps<typeof DialogPrimitive.Content>\n>((props, ref) => {\n  const preventAutoFocusOnClose = useRef(false);\n  const { renderer } = useContext(ReactSdkContext);\n\n  /**\n   * Prevent focusing on the trigger after a navigable link in a dialog is clicked and closes the dialog.\n   */\n  useEffect(() => {\n    if (renderer !== undefined) {\n      return;\n    }\n\n    preventAutoFocusOnClose.current = false;\n\n    const handleClick = (event: MouseEvent) => {\n      const { target } = event;\n\n      if (willNavigate(event) === false) {\n        return;\n      }\n\n      if (target instanceof HTMLAnchorElement === false) {\n        return false;\n      }\n\n      if (target.closest('[role=\"dialog\"]')) {\n        preventAutoFocusOnClose.current = true;\n      }\n    };\n\n    window.addEventListener(\"click\", handleClick);\n    return () => window.removeEventListener(\"click\", handleClick);\n  }, [renderer]);\n\n  return (\n    <DialogPrimitive.Content\n      ref={ref}\n      {...props}\n      onCloseAutoFocus={(event) => {\n        if (preventAutoFocusOnClose.current) {\n          event.preventDefault();\n        }\n      }}\n    />\n  );\n});\n\nexport const DialogClose = DialogPrimitive.Close;\n\ntype Tag = \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\nconst defaultTag = \"h1\";\nexport const DialogTitle = forwardRef<\n  HTMLHeadingElement,\n  ComponentProps<typeof DialogPrimitive.DialogTitle> & { tag?: Tag }\n>(({ tag: Tag = defaultTag, children, ...props }, ref) => (\n  <DialogPrimitive.DialogTitle asChild>\n    <Tag ref={ref} {...props}>\n      {children}\n    </Tag>\n  </DialogPrimitive.DialogTitle>\n));\n\nexport const DialogDescription = DialogPrimitive.Description;\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each DialogOverlay component within the selection,\n// we identify its closest parent Dialog component\n// and update its open prop bound to variable.\nexport const hooksDialog: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:DialogOverlay`) {\n        const dialog = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Dialog`\n        );\n        if (dialog) {\n          context.setMemoryProp(dialog, \"open\", undefined);\n        }\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:DialogOverlay`) {\n        const dialog = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Dialog`\n        );\n        if (dialog) {\n          context.setMemoryProp(dialog, \"open\", true);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/dialog.ws.ts",
    "content": "import {\n  DialogIcon,\n  TriggerIcon,\n  ContentIcon,\n  OverlayIcon,\n  HeadingIcon,\n  TextIcon,\n  ButtonElementIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div, button, h2, p } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport {\n  propsDialog,\n  propsDialogContent,\n  propsDialogTrigger,\n  propsDialogOverlay,\n  propsDialogClose,\n  propsDialogTitle,\n  propsDialogDescription,\n} from \"./__generated__/dialog.props\";\nimport { buttonReset } from \"./shared/preset-styles\";\n\n// @todo add [data-state] to button and link\nexport const metaDialogTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  props: propsDialogTrigger,\n};\n\nexport const metaDialogOverlay: WsComponentMeta = {\n  icon: OverlayIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.DialogContent],\n  },\n  presetStyle: { div },\n  props: propsDialogOverlay,\n};\n\nexport const metaDialogContent: WsComponentMeta = {\n  icon: ContentIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [\n      radix.DialogTitle,\n      radix.DialogDescription,\n      radix.DialogClose,\n    ],\n  },\n  presetStyle: { div },\n  props: propsDialogContent,\n};\n\nexport const metaDialogTitle: WsComponentMeta = {\n  icon: HeadingIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: { h2 },\n  props: propsDialogTitle,\n};\n\nexport const metaDialogDescription: WsComponentMeta = {\n  icon: TextIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: { p },\n  props: propsDialogDescription,\n};\n\nexport const metaDialogClose: WsComponentMeta = {\n  icon: ButtonElementIcon,\n  label: \"Close Button\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: {\n    button: [buttonReset, button].flat(),\n  },\n  props: propsDialogClose,\n};\n\nexport const metaDialog: WsComponentMeta = {\n  icon: DialogIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.DialogTrigger, radix.DialogOverlay],\n  },\n  initialProps: [\"open\"],\n  props: propsDialog,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/hooks.ts",
    "content": "import type { Hook } from \"@webstudio-is/react-sdk\";\nimport { hooksCollapsible } from \"./collapsible\";\nimport { hooksTabs } from \"./tabs\";\nimport { hooksDialog } from \"./dialog\";\nimport { hooksPopover } from \"./popover\";\nimport { hooksTooltip } from \"./tooltip\";\nimport { hooksAccordion } from \"./accordion\";\nimport { hooksNavigationMenu } from \"./navigation-menu\";\nimport { hooksSelect } from \"./select\";\n\nexport const hooks: Hook[] = [\n  hooksCollapsible,\n  hooksTabs,\n  hooksDialog,\n  hooksPopover,\n  hooksTooltip,\n  hooksAccordion,\n  hooksNavigationMenu,\n  hooksSelect,\n];\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/label.template.tsx",
    "content": "import {\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport { fontSize, lineHeight, weights } from \"./shared/theme\";\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/label.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"An accessible label to describe the purpose of an input. Match the “For” property on the label with the “ID” of the input to connect them.\",\n  order: 102,\n  template: (\n    <radix.Label\n      // text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\n      // We are not supporting peer like styles yet\n      ws:style={css`\n        font-size: ${fontSize.sm};\n        line-height: ${lineHeight.none};\n        font-weight: ${weights.medium};\n      `}\n    >\n      {new PlaceholderValue(\"Form Label\")}\n    </radix.Label>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/label.tsx",
    "content": "import {\n  forwardRef,\n  type ComponentPropsWithoutRef,\n  type ElementRef,\n} from \"react\";\n\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\n\nexport const Label = forwardRef<\n  ElementRef<typeof LabelPrimitive.Root>,\n  Omit<ComponentPropsWithoutRef<typeof LabelPrimitive.Root>, \"asChild\">\n>((props, ref) => <LabelPrimitive.Root ref={ref} {...props} />);\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/label.ws.ts",
    "content": "import { LabelIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { label } from \"@webstudio-is/sdk/normalize.css\";\nimport { props } from \"./__generated__/label.props\";\n\nexport const meta: WsComponentMeta = {\n  icon: LabelIcon,\n  presetStyle: { label },\n  initialProps: [\"id\", \"class\", \"for\"],\n  props,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/metas.ts",
    "content": "export {\n  metaCollapsible as Collapsible,\n  metaCollapsibleTrigger as CollapsibleTrigger,\n  metaCollapsibleContent as CollapsibleContent,\n} from \"./collapsible.ws\";\nexport {\n  metaDialog as Dialog,\n  metaDialogTrigger as DialogTrigger,\n  metaDialogOverlay as DialogOverlay,\n  metaDialogContent as DialogContent,\n  metaDialogClose as DialogClose,\n  metaDialogTitle as DialogTitle,\n  metaDialogDescription as DialogDescription,\n} from \"./dialog.ws\";\nexport {\n  metaPopover as Popover,\n  metaPopoverTrigger as PopoverTrigger,\n  metaPopoverContent as PopoverContent,\n  metaPopoverClose as PopoverClose,\n} from \"./popover.ws\";\nexport {\n  metaTooltip as Tooltip,\n  metaTooltipTrigger as TooltipTrigger,\n  metaTooltipContent as TooltipContent,\n} from \"./tooltip.ws\";\nexport {\n  metaTabs as Tabs,\n  metaTabsList as TabsList,\n  metaTabsTrigger as TabsTrigger,\n  metaTabsContent as TabsContent,\n} from \"./tabs.ws\";\nexport { meta as Label } from \"./label.ws\";\nexport {\n  metaAccordion as Accordion,\n  metaAccordionItem as AccordionItem,\n  metaAccordionHeader as AccordionHeader,\n  metaAccordionTrigger as AccordionTrigger,\n  metaAccordionContent as AccordionContent,\n} from \"./accordion.ws\";\n\nexport {\n  metaNavigationMenu as NavigationMenu,\n  metaNavigationMenuList as NavigationMenuList,\n  metaNavigationMenuItem as NavigationMenuItem,\n  metaNavigationMenuTrigger as NavigationMenuTrigger,\n  metaNavigationMenuContent as NavigationMenuContent,\n  metaNavigationMenuLink as NavigationMenuLink,\n  metaNavigationMenuViewport as NavigationMenuViewport,\n} from \"./navigation-menu.ws\";\n\nexport {\n  metaSelect as Select,\n  metaSelectTrigger as SelectTrigger,\n  metaSelectValue as SelectValue,\n  metaSelectViewport as SelectViewport,\n  metaSelectContent as SelectContent,\n  metaSelectItem as SelectItem,\n  metaSelectItemIndicator as SelectItemIndicator,\n  metaSelectItemText as SelectItemText,\n} from \"./select.ws\";\n\nexport {\n  metaSwitch as Switch,\n  metaSwitchThumb as SwitchThumb,\n} from \"./switch.ws\";\nexport {\n  metaCheckbox as Checkbox,\n  metaCheckboxIndicator as CheckboxIndicator,\n} from \"./checkbox.ws\";\nexport {\n  metaRadioGroup as RadioGroup,\n  metaRadioGroupItem as RadioGroupItem,\n  metaRadioGroupIndicator as RadioGroupIndicator,\n} from \"./radio-group.ws\";\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/navigation-menu.template.tsx",
    "content": "import type { ReactNode } from \"react\";\nimport { ChevronDownIcon } from \"@webstudio-is/icons/svg\";\nimport {\n  $,\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport { getButtonStyle } from \"./shared/styles\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  fontSize,\n  height,\n  lineHeight,\n  spacing,\n  transition,\n  weights,\n  width,\n} from \"./shared/theme\";\n\nconst components = [\n  {\n    title: \"Sheet\",\n    href: \"/docs/components/sheet\",\n    description:\n      \"Extends the Dialog component to display content that complements the main content of the screen.\",\n  },\n  {\n    title: \"Navigation Menu\",\n    href: \"/docs/components/navigation-menu\",\n    description: \"A collection of links for navigating websites.\",\n  },\n  {\n    title: \"Tabs\",\n    href: \"/docs/components/tabs\",\n    description:\n      \"A set of layered sections of content—known as tab panels—that are displayed one at a time.\",\n  },\n  {\n    title: \"Accordion\",\n    href: \"/docs/components/accordion\",\n    description:\n      \"A vertically stacked set of interactive headings that each reveal a section of content.\",\n  },\n  {\n    title: \"Dialog\",\n    href: \"/docs/components/dialog\",\n    description:\n      \"A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.\",\n  },\n  {\n    title: \"Collapsible\",\n    href: \"/docs/components/collapsible\",\n    description: \"An interactive component which expands/collapses a panel.\",\n  },\n  {\n    title: \"Popover\",\n    href: \"/docs/components/popover\",\n    description: \"Displays rich content in a portal, triggered by a button.\",\n  },\n  {\n    title: \"Tooltip\",\n    href: \"/docs/components/tooltip\",\n    description:\n      \"A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.\",\n  },\n  {\n    title: \"Button\",\n    href: \"/docs/components/button\",\n    description: \"Displays a button or a component that looks like a button.\",\n  },\n];\n\nconst createMenuContentItem = (props: (typeof components)[number]) => (\n  <radix.NavigationMenuLink key={props.title}>\n    <$.Link\n      href={`https://ui.shadcn.com${props.href}`}\n      // block select-none space-y-1 rounded-md p-3 leading-none\n      // no-underline outline-none transition-colors\n      // hover:bg-accent hover:text-accent-foreground\n      // focus:bg-accent focus:text-accent-foreground\n      ws:style={css`\n        color: inherit;\n        display: flex;\n        flex-direction: column;\n        user-select: none;\n        gap: ${spacing[1]};\n        border-radius: ${borderRadius.md};\n        padding: ${spacing[3]};\n        line-height: ${lineHeight.none};\n        text-decoration-line: none;\n        outline: none;\n        &:hover,\n        &:focus {\n          background-color: ${colors.accent};\n          color: ${colors.accentForeground};\n        }\n      `}\n    >\n      <$.Text\n        // text-sm font-medium leading-none\n        ws:style={css`\n          font-size: ${fontSize.sm};\n          font-weight: ${weights.medium};\n          line-height: ${lineHeight.none};\n        `}\n      >\n        {new PlaceholderValue(props.title)}\n      </$.Text>\n      <$.Paragraph\n        // line-clamp-2 text-sm leading-snug text-muted-foreground\n        ws:style={css`\n          margin: 0;\n          overflow: hidden;\n          display: -webkit-box;\n          -webkit-box-orient: vertical;\n          -webkit-line-clamp: 2;\n          font-size: ${fontSize.sm};\n          line-height: ${lineHeight.snug};\n          color: ${colors.mutedForeground};\n        `}\n      >\n        {new PlaceholderValue(props.description)}\n      </$.Paragraph>\n    </$.Link>\n  </radix.NavigationMenuLink>\n);\n\nconst createMenuContentList = (props: { count: number; offset: number }) => (\n  <$.Box\n    ws:label=\"Flex Column\"\n    ws:style={css`\n      width: ${width[64]};\n      display: flex;\n      gap: ${spacing[4]};\n      flex-direction: column;\n    `}\n  >\n    {components\n      .slice(props.offset, props.offset + props.count)\n      .map(createMenuContentItem)}\n  </$.Box>\n);\n\nconst aboutMenuContent = (\n  <$.Box\n    ws:label=\"Content\"\n    ws:style={css`\n      display: flex;\n      gap: ${spacing[4]};\n      padding: ${spacing[2]};\n    `}\n  >\n    <$.Box\n      ws:style={css`\n        background-color: ${colors.border};\n        padding: ${spacing[4]};\n        width: ${width[48]};\n        border-radius: ${borderRadius.md};\n      `}\n    >\n      {new PlaceholderValue(\"\")}\n    </$.Box>\n    {createMenuContentList({ count: 3, offset: 0 })}\n  </$.Box>\n);\n\nconst componentsMenuContent = (\n  <$.Box\n    ws:label=\"Content\"\n    ws:style={css`\n      display: flex;\n      gap: ${spacing[4]};\n    `}\n  >\n    {createMenuContentList({ count: 3, offset: 3 })}\n    {createMenuContentList({ count: 3, offset: 6 })}\n  </$.Box>\n);\n\nconst createMenuItem = (title: string, content: ReactNode) => {\n  return (\n    <radix.NavigationMenuItem>\n      <radix.NavigationMenuTrigger>\n        <$.Button\n          ws:style={[\n            ...getButtonStyle(\"ghost\", \"sm\"),\n            ...css`\n              --navigation-menu-trigger-icon-transform: 0deg;\n              &[data-state=\"open\"] {\n                --navigation-menu-trigger-icon-transform: 180deg;\n              }\n            `,\n          ]}\n        >\n          <$.Text>{new PlaceholderValue(title)}</$.Text>\n          <$.Box\n            ws:label=\"Icon Container\"\n            // h-4 w-4 shrink-0 transition-transform duration-200\n            ws:style={css`\n              margin-left: ${spacing[1]};\n              rotate: var(--navigation-menu-trigger-icon-transform);\n              height: ${height[4]};\n              width: ${width[4]};\n              flex-shrink: 0;\n              transition: ${transition.all};\n              transition-duration: 200ms;\n            `}\n          >\n            <$.HtmlEmbed ws:label=\"Chevron Icon\" code={ChevronDownIcon} />\n          </$.Box>\n        </$.Button>\n      </radix.NavigationMenuTrigger>\n      <radix.NavigationMenuContent\n        // left-0 top-0 absolute w-max\n        ws:style={css`\n          left: 0;\n          top: 0;\n          positon: absolute;\n          width: max-content;\n          padding: ${spacing[4]};\n        `}\n      >\n        {content}\n      </radix.NavigationMenuContent>\n    </radix.NavigationMenuItem>\n  );\n};\n\nconst createMenuLink = (title: string) => {\n  return (\n    <radix.NavigationMenuItem>\n      <radix.NavigationMenuLink>\n        <$.Link\n          ws:style={[\n            ...getButtonStyle(\"ghost\", \"sm\"),\n            ...css`\n              text-decoration-line: none;\n              color: currentColor;\n            `,\n          ]}\n        >\n          {new PlaceholderValue(title)}\n        </$.Link>\n      </radix.NavigationMenuLink>\n    </radix.NavigationMenuItem>\n  );\n};\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description: \"A collection of links for navigating websites.\",\n  order: 2,\n  template: (\n    <radix.NavigationMenu\n      // relative\n      // Omiting this: z-10 flex max-w-max flex-1 items-center justify-center\n      ws:style={css`\n        position: relative;\n        max-width: max-content;\n      `}\n    >\n      <radix.NavigationMenuList\n        ws:style={css`\n          /* ul defaults in tailwind */\n          padding: 0;\n          margin: 0;\n          /* shadcdn styles */\n          display: flex;\n          flex: 1 1 0;\n          list-style-type: none;\n          align-items: center;\n          justify-content: center;\n          gap: ${spacing[1]};\n        `}\n      >\n        {createMenuItem(\"About\", aboutMenuContent)}\n        {createMenuItem(\"Components\", componentsMenuContent)}\n        {createMenuLink(\"Standalone\")}\n      </radix.NavigationMenuList>\n      <$.Box\n        ws:label=\"Viewport Container\"\n        // absolute left-0 top-full flex justify-center\n        ws:style={css`\n          position: absolute;\n          left: 0;\n          top: 100%;\n          display: flex;\n          justify-content: center;\n        `}\n      >\n        <radix.NavigationMenuViewport\n          /*\n            origin-top-center relative mt-1.5 w-full\n            overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg\n            h-[var(--radix-navigation-menu-viewport-height)]\n            w-[var(--radix-navigation-menu-viewport-width)]\n            // anims\n            [animation-duration:150ms!important] [transition-duration:150ms!important]\n            data-[state=open]:animate-in data-[state=closed]:animate-out\n            data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90\n          */\n          ws:style={css`\n            position: relative;\n            margin-top: ${spacing[1.5]};\n            overflow: hidden;\n            border-radius: ${borderRadius.md};\n            border: ${borderWidth.DEFAULT} solid ${colors.border};\n            background-color: ${colors.popover};\n            color: ${colors.popoverForeground};\n            box-shadow: ${boxShadow.lg};\n            height: var(--radix-navigation-menu-viewport-height);\n            width: var(--radix-navigation-menu-viewport-width);\n          `}\n        />\n      </$.Box>\n    </radix.NavigationMenu>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/navigation-menu.tsx",
    "content": "import {\n  Children,\n  forwardRef,\n  type ComponentPropsWithoutRef,\n  useContext,\n} from \"react\";\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\";\nimport { getIndexWithinAncestorFromProps } from \"@webstudio-is/sdk/runtime\";\nimport {\n  getClosestInstance,\n  ReactSdkContext,\n  type Hook,\n} from \"@webstudio-is/react-sdk/runtime\";\n\nexport const NavigationMenu = forwardRef<\n  HTMLLIElement,\n  Omit<\n    ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>,\n    \"orientation\" | \"aria-orientation\"\n  >\n>(({ value: propsValue, ...props }, ref) => {\n  /**\n   * If the value is an empty string, \"NavigationMenuViewport\" isn't in the tree.\n   * This is Radix's way to differentiate animations. However, in the builder, we can't style non-existing elements.\n   * Since we don't need animations in the builder, we can trick Radix by setting a non-empty string like \"-1\" to the value property.\n   * This ensures \"NavigationMenuViewport\" always appears in the HTML tree.\n   **/\n  const { renderer } = useContext(ReactSdkContext);\n  let value = propsValue;\n  if (renderer === \"canvas\") {\n    value = value === \"\" ? \"-1\" : value;\n  }\n\n  return <NavigationMenuPrimitive.Root ref={ref} value={value} {...props} />;\n});\n\nexport const NavigationMenuList = NavigationMenuPrimitive.List;\n\nexport const NavigationMenuViewport = NavigationMenuPrimitive.Viewport;\nexport const NavigationMenuContent = NavigationMenuPrimitive.Content;\n\nexport const NavigationMenuItem = forwardRef<\n  HTMLLIElement,\n  Omit<ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Item>, \"asChild\">\n>(({ value, ...props }, ref) => {\n  const index = getIndexWithinAncestorFromProps(props);\n  return (\n    <NavigationMenuPrimitive.Item ref={ref} value={value ?? index} {...props} />\n  );\n});\n\nexport const NavigationMenuLink = forwardRef<\n  HTMLAnchorElement,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Link>\n>(({ children, ...props }, ref) => {\n  const firstChild = Children.toArray(children)[0];\n\n  return (\n    <NavigationMenuPrimitive.Link asChild={true} ref={ref} {...props}>\n      {firstChild ?? <a>Add link component</a>}\n    </NavigationMenuPrimitive.Link>\n  );\n});\n\nexport const NavigationMenuTrigger = forwardRef<\n  HTMLButtonElement,\n  ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>\n>(({ children, ...props }, ref) => {\n  const firstChild = Children.toArray(children)[0];\n\n  return (\n    <NavigationMenuPrimitive.Trigger asChild={true} ref={ref} {...props}>\n      {firstChild ?? <button>Add button or link</button>}\n    </NavigationMenuPrimitive.Trigger>\n  );\n});\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each NavigationMenuItem component within the selection,\n// we identify its closest parent NavigationMenu component\n// and update its open prop bound to variable.\nexport const hooksNavigationMenu: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:NavigationMenuContent`) {\n        const menu = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:NavigationMenu`\n        );\n        if (menu) {\n          context.setMemoryProp(menu, \"value\", undefined);\n        }\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:NavigationMenuContent`) {\n        const menu = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:NavigationMenu`\n        );\n        const menuItem = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:NavigationMenuItem`\n        );\n        if (menuItem === undefined || menu === undefined) {\n          return;\n        }\n        const contentValue =\n          context.getPropValue(menuItem, \"value\") ??\n          context.indexesWithinAncestors.get(menuItem.id)?.toString();\n        if (contentValue) {\n          context.setMemoryProp(menu, \"value\", contentValue);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/navigation-menu.ws.ts",
    "content": "import {\n  ContentIcon,\n  ListIcon,\n  ListItemIcon,\n  TriggerIcon,\n  BoxIcon,\n  ViewportIcon,\n  NavigationMenuIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport {\n  propsNavigationMenu,\n  propsNavigationMenuItem,\n  propsNavigationMenuTrigger,\n  propsNavigationMenuContent,\n  propsNavigationMenuLink,\n  propsNavigationMenuList,\n  propsNavigationMenuViewport,\n} from \"./__generated__/navigation-menu.props\";\n\nexport const metaNavigationMenu: WsComponentMeta = {\n  icon: NavigationMenuIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.NavigationMenuList, radix.NavigationMenuViewport],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsNavigationMenu,\n};\n\nexport const metaNavigationMenuList: WsComponentMeta = {\n  icon: ListIcon,\n  label: \"Menu List\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.NavigationMenuItem],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsNavigationMenuList,\n};\n\nexport const metaNavigationMenuItem: WsComponentMeta = {\n  icon: ListItemIcon,\n  label: \"Menu Item\",\n  indexWithinAncestor: radix.NavigationMenu,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [\n      radix.NavigationMenuTrigger,\n      radix.NavigationMenuContent,\n      radix.NavigationMenuLink,\n    ],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsNavigationMenuItem,\n};\n\nexport const metaNavigationMenuTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  label: \"Menu Trigger\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  props: propsNavigationMenuTrigger,\n};\n\nexport const metaNavigationMenuContent: WsComponentMeta = {\n  icon: ContentIcon,\n  label: \"Menu Content\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.NavigationMenuLink],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsNavigationMenuContent,\n};\n\nexport const metaNavigationMenuLink: WsComponentMeta = {\n  icon: BoxIcon,\n  label: \"Accessible Link Wrapper\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  props: propsNavigationMenuLink,\n};\n\nexport const metaNavigationMenuViewport: WsComponentMeta = {\n  icon: ViewportIcon,\n  label: \"Menu Viewport\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: {\n    div,\n  },\n  props: propsNavigationMenuViewport,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/popover.template.tsx",
    "content": "import { LargeXIcon } from \"@webstudio-is/icons/svg\";\nimport {\n  $,\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport { getButtonStyle } from \"./shared/styles\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  spacing,\n  width,\n  zIndex,\n  height,\n  opacity,\n} from \"./shared/theme\";\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/popover.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description: \"Displays rich content in a portal, triggered by a button.\",\n  order: 6,\n  template: (\n    <radix.Popover>\n      <radix.PopoverTrigger>\n        <$.Button ws:style={getButtonStyle(\"outline\")}>\n          {new PlaceholderValue(\"Button\")}\n        </$.Button>\n      </radix.PopoverTrigger>\n      <radix.PopoverContent\n        /**\n         *  z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none\n         **/\n        ws:style={css`\n          z-index: ${zIndex[50]};\n          width: ${width[72]};\n          border-radius: ${borderRadius.md};\n          border: ${borderWidth.DEFAULT} solid ${colors.border};\n          background-color: ${colors.popover};\n          padding: ${spacing[4]};\n          color: ${colors.popoverForeground};\n          box-shadow: ${boxShadow.md};\n          outline: none;\n        `}\n      >\n        <$.Text>{new PlaceholderValue(\"The text you can edit\")}</$.Text>\n        <radix.PopoverClose\n          ws:label=\"Close Button\"\n          /**\n           * absolute right-4 top-4\n           * rounded-sm opacity-70\n           * ring-offset-background\n           * hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\n           * flex items-center justify-center h-4 w-4\n           **/\n          ws:style={css`\n            position: absolute;\n            right: ${spacing[4]};\n            top: ${spacing[4]};\n            border-radius: ${borderRadius.sm};\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            height: ${height[4]};\n            width: ${height[4]};\n            border: 0;\n            background-color: transparent;\n            outline: none;\n            &:hover {\n              opacity: ${opacity[100]};\n            }\n            &:focus-visible {\n              box-shadow: ${boxShadow.ring};\n            }\n          `}\n        >\n          <$.HtmlEmbed ws:label=\"Close Icon\" code={LargeXIcon} />\n        </radix.PopoverClose>\n      </radix.PopoverContent>\n    </radix.Popover>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/popover.tsx",
    "content": "import {\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  forwardRef,\n  Children,\n  useState,\n  useEffect,\n} from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\nimport { getClosestInstance, type Hook } from \"@webstudio-is/react-sdk/runtime\";\n\n// wrap in forwardRef because Root is functional component without ref\nexport const Popover = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>, \"defaultOpen\">\n>((props, _ref) => {\n  const currentOpen = props.open ?? false;\n  const [open, setOpen] = useState(currentOpen);\n  // synchronize external value with local one when changed\n  useEffect(() => setOpen(currentOpen), [currentOpen]);\n  return (\n    <PopoverPrimitive.Root {...props} open={open} onOpenChange={setOpen} />\n  );\n});\n\n/**\n * We're not exposing the 'asChild' property for the Trigger.\n * Instead, we're enforcing 'asChild=true' for the Trigger and making it style-less.\n * This avoids situations where the Trigger inadvertently passes all styles to its child,\n * which would prevent us from displaying styles properly in the builder.\n */\nexport const PopoverTrigger = forwardRef<\n  HTMLButtonElement,\n  { children: ReactNode }\n>(({ children, ...props }, ref) => {\n  const firstChild = Children.toArray(children)[0];\n\n  return (\n    <PopoverPrimitive.Trigger asChild={true} ref={ref} {...props}>\n      {firstChild ?? <button>Add button or link</button>}\n    </PopoverPrimitive.Trigger>\n  );\n});\n\nexport const PopoverContent = forwardRef<\n  HTMLDivElement,\n  ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(\n  (\n    { sideOffset = 4, align = \"center\", hideWhenDetached = true, ...props },\n    ref\n  ) => (\n    <PopoverPrimitive.Portal>\n      <PopoverPrimitive.Content\n        ref={ref}\n        align=\"center\"\n        sideOffset={sideOffset}\n        hideWhenDetached={hideWhenDetached}\n        {...props}\n      />\n    </PopoverPrimitive.Portal>\n  )\n);\n\nexport const PopoverClose = PopoverPrimitive.Close;\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each PopoverContent component within the selection,\n// we identify its closest parent Popover component\n// and update its open prop bound to variable.\nexport const hooksPopover: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:PopoverContent`) {\n        const popover = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Popover`\n        );\n        if (popover) {\n          context.setMemoryProp(popover, \"open\", undefined);\n        }\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:PopoverContent`) {\n        const popover = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Popover`\n        );\n        if (popover) {\n          context.setMemoryProp(popover, \"open\", true);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/popover.ws.ts",
    "content": "import {\n  PopoverIcon,\n  TriggerIcon,\n  ContentIcon,\n  ButtonElementIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button, div } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport {\n  propsPopover,\n  propsPopoverContent,\n  propsPopoverTrigger,\n  propsPopoverClose,\n} from \"./__generated__/popover.props\";\nimport { buttonReset } from \"./shared/preset-styles\";\n\n// @todo add [data-state] to button and link\nexport const metaPopoverTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  props: propsPopoverTrigger,\n};\n\nexport const metaPopoverContent: WsComponentMeta = {\n  icon: ContentIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.PopoverClose],\n  },\n  presetStyle: {\n    div,\n  },\n  initialProps: [\"side\", \"sideOffset\", \"align\", \"alignOffset\"],\n  props: propsPopoverContent,\n};\n\nexport const metaPopover: WsComponentMeta = {\n  icon: PopoverIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.PopoverTrigger, radix.PopoverContent],\n  },\n  initialProps: [\"open\"],\n  props: propsPopover,\n};\n\nexport const metaPopoverClose: WsComponentMeta = {\n  icon: ButtonElementIcon,\n  label: \"Close Button\",\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: {\n    button: [buttonReset, button].flat(),\n  },\n  props: propsPopoverClose,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/props-descriptions.ts",
    "content": "const open =\n  \"Show or hide the content of this component on the canvas. This will not affect the initial state of the component.\";\n\nconst alignOffset =\n  \"The offset in pixels from the “start“ or “end“ alignment options.\";\n\nconst sideOffset =\n  \"The distance in pixels between the Content and the Trigger.\";\n\nconst side =\n  \"The preferred alignment against the Trigger. May change when collisions occur.\";\n\nexport const propsDescriptions = {\n  Dialog: {\n    open,\n  },\n  Sheet: {\n    open,\n  },\n  Collapsible: {\n    open,\n  },\n  Popover: {\n    open,\n  },\n  PopoverContent: {\n    alignOffset,\n    sideOffset,\n    side,\n  },\n  Tooltip: {\n    open,\n    delayDuration:\n      \"The delay before the Tooltip shows after the Trigger is hovered, in milliseconds. If no value is specified, the default is 700ms\",\n    disableHoverableContent:\n      \"When toggled, prevents the Tooltip content from showing when the Trigger is hovered.\",\n  },\n  TooltipContent: {\n    alignOffset,\n    sideOffset,\n    side,\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/radio-group.template.tsx",
    "content": "import {\n  $,\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { DotIcon } from \"@webstudio-is/icons/svg\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  height,\n  opacity,\n  spacing,\n  width,\n} from \"./shared/theme\";\n\nconst createRadioGroupItem = ({\n  value,\n  label,\n}: {\n  value: string;\n  label: string;\n}) => (\n  <radix.Label\n    // flex items-center space-x-2\n    ws:style={css`\n      display: flex;\n      align-items: center;\n      gap: ${spacing[2]};\n    `}\n  >\n    <radix.RadioGroupItem\n      value={value}\n      // aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background\n      // focus:outline-none\n      // focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\n      // disabled:cursor-not-allowed disabled:opacity-50\n      ws:style={css`\n        aspect-ratio: 1 / 1;\n        height: ${height[4]};\n        width: ${width[4]};\n        border-radius: ${borderRadius.full};\n        border: ${borderWidth.DEFAULT} solid ${colors.primary};\n        color: ${colors.primary};\n        &:focus-visible {\n          outline: none;\n          box-shadow: ${boxShadow.ring};\n        }\n        &:disabled {\n          cursor: not-allowed;\n          opacity: ${opacity[50]};\n        }\n      `}\n    >\n      <radix.RadioGroupIndicator>\n        <$.HtmlEmbed ws:label=\"Indicator Icon\" code={DotIcon} />\n      </radix.RadioGroupIndicator>\n    </radix.RadioGroupItem>\n    <$.Text>{new PlaceholderValue(label)}</$.Text>\n  </radix.Label>\n);\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  order: 100,\n  description:\n    \"A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.\",\n  template: (\n    <radix.RadioGroup\n      // grid gap-2\n      ws:style={css`\n        display: flex;\n        flex-direction: column;\n        gap: ${spacing[2]};\n      `}\n    >\n      {createRadioGroupItem({ value: \"default\", label: \"Default\" })}\n      {createRadioGroupItem({ value: \"comfortable\", label: \"Comfortable\" })}\n      {createRadioGroupItem({ value: \"compact\", label: \"Compact\" })}\n    </radix.RadioGroup>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/radio-group.tsx",
    "content": "import {\n  type ForwardRefExoticComponent,\n  type ComponentProps,\n  type RefAttributes,\n  type ElementRef,\n  forwardRef,\n  useState,\n  useEffect,\n} from \"react\";\nimport { Root, Item, Indicator } from \"@radix-ui/react-radio-group\";\n\nexport const RadioGroup = forwardRef<\n  ElementRef<\"div\">,\n  ComponentProps<typeof Root>\n>(({ defaultValue, ...props }, ref) => {\n  const currentValue = props.value ?? defaultValue ?? \"\";\n  const [value, setValue] = useState(currentValue);\n  // synchronize external value with local one when changed\n  useEffect(() => setValue(currentValue), [currentValue]);\n  return <Root {...props} ref={ref} value={value} onValueChange={setValue} />;\n});\n\nexport const RadioGroupItem: ForwardRefExoticComponent<\n  ComponentProps<typeof Item> & RefAttributes<HTMLButtonElement>\n> = Item;\n\nexport const RadioGroupIndicator: ForwardRefExoticComponent<\n  ComponentProps<typeof Indicator> & RefAttributes<HTMLSpanElement>\n> = Indicator;\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/radio-group.ws.ts",
    "content": "import { ItemIcon, RadioGroupIcon, TriggerIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button, div, span } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport { buttonReset } from \"./shared/preset-styles\";\nimport {\n  propsRadioGroup,\n  propsRadioGroupIndicator,\n  propsRadioGroupItem,\n} from \"./__generated__/radio-group.props\";\n\nexport const metaRadioGroup: WsComponentMeta = {\n  icon: RadioGroupIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.RadioGroupItem],\n  },\n  presetStyle: {\n    div,\n  },\n  initialProps: [\"id\", \"class\", \"name\", \"value\", \"required\"],\n  props: propsRadioGroup,\n};\n\nexport const metaRadioGroupItem: WsComponentMeta = {\n  icon: ItemIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.RadioGroupIndicator],\n  },\n  states: [\n    { label: \"Checked\", selector: \"[data-state=checked]\" },\n    { label: \"Unchecked\", selector: \"[data-state=unchecked]\" },\n  ],\n  presetStyle: {\n    button: [button, buttonReset].flat(),\n  },\n  initialProps: [\"value\"],\n  props: propsRadioGroupItem,\n};\n\nexport const metaRadioGroupIndicator: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  states: [\n    { label: \"Checked\", selector: \"[data-state=checked]\" },\n    { label: \"Unchecked\", selector: \"[data-state=unchecked]\" },\n  ],\n  presetStyle: {\n    span,\n  },\n  props: propsRadioGroupIndicator,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/select.template.tsx",
    "content": "import {\n  $,\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  height,\n  opacity,\n  spacing,\n  width,\n  zIndex,\n} from \"./shared/theme\";\nimport { CheckMarkIcon } from \"@webstudio-is/icons/svg\";\n\nconst createSelectItem = (value: string, label: string) => {\n  return (\n    <radix.SelectItem\n      value={value}\n      // relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none\n      // focus:bg-accent focus:text-accent-foreground\n      // data-[disabled]:pointer-events-none data-[disabled]:opacity-50\n      ws:style={css`\n        position: relative;\n        display: flex;\n        width: ${width.full};\n        cursor: default;\n        user-select: none;\n        align-items: center;\n        border-radius: ${borderRadius.md};\n        padding: ${spacing[1.5]} ${spacing[2]} ${spacing[1.5]} ${spacing[8]};\n        font-size: ${fontSize.sm};\n        line-height: ${fontSizeLineHeight.sm};\n        outline: none;\n        &:focus {\n          background-color: ${colors.accent};\n          color: ${colors.accentForeground};\n        }\n        &[data-disabled] {\n          pointer-events: none;\n          opacity: ${opacity[50]};\n        }\n      `}\n    >\n      <radix.SelectItemIndicator\n        // absolute left-2 flex h-3.5 w-3.5 items-center justify-center\n        ws:style={css`\n          position: absolute;\n          left: ${spacing[2]};\n          display: flex;\n          height: ${height[3.5]};\n          width: ${width[3.5]};\n          align-items: center;\n          justify-content: center;\n        `}\n      >\n        <$.HtmlEmbed ws:label=\"Indicator Icon\" code={CheckMarkIcon} />\n      </radix.SelectItemIndicator>\n      <radix.SelectItemText>{new PlaceholderValue(label)}</radix.SelectItemText>\n    </radix.SelectItem>\n  );\n};\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"Use within a form to give your users a list of options to choose from.\",\n  order: 10,\n  template: (\n    <radix.Select>\n      <radix.SelectTrigger\n        // flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background\n        // placeholder:text-muted-foreground\n        // focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\n        // disabled:cursor-not-allowed disabled:opacity-50\n        ws:style={css`\n          display: flex;\n          height: ${height[10]};\n          width: ${width.full};\n          align-items: center;\n          justify-content: between;\n          border-radius: ${borderRadius.md};\n          border: ${borderWidth.DEFAULT} solid ${colors.input};\n          background-color: ${colors.background};\n          padding: ${spacing[2]} ${spacing[3]};\n          font-size: ${fontSize.sm};\n          line-height: ${fontSizeLineHeight.sm};\n          &::placeholder {\n            color: ${colors.mutedForeground};\n          }\n          &:focus-visible {\n            outline: none;\n            box-shadow: ${boxShadow.ring};\n          }\n          &:disabled {\n            cursor: not-allowed;\n            opacity: ${opacity[50]};\n          }\n        `}\n      >\n        <radix.SelectValue placeholder=\"Theme\" />\n      </radix.SelectTrigger>\n      <radix.SelectContent\n        // relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md\n        // data-[state=open]:animate-in\n        // data-[state=closed]:animate-out data-[state=closed]:fade-out-0\n        // data-[state=open]:fade-in-0\n        // data-[state=closed]:zoom-out-95\n        // data-[state=open]:zoom-in-95\n        // data-[side=bottom]:slide-in-from-top-2\n        // data-[side=left]:slide-in-from-right-2\n        // data-[side=right]:slide-in-from-left-2\n        // data-[side=top]:slide-in-from-bottom-2\n        // position=popper\n        // data-[side=bottom]:translate-y-1\n        // data-[side=left]:-translate-x-1\n        // data-[side=right]:translate-x-1\n        // data-[side=top]:-translate-y-1\n        ws:style={css`\n          position: relative;\n          z-index: ${zIndex[50]};\n          min-width: 8rem;\n          overflow: hidden;\n          border-radius: ${borderRadius.md};\n          border: ${borderWidth.DEFAULT} solid ${colors.border};\n          background-color: ${colors.popover};\n          color: ${colors.popoverForeground};\n          box-shadow: ${boxShadow.md};\n        `}\n      >\n        <radix.SelectViewport\n          // p-1\n          // position=popper\n          // h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\n          ws:style={css`\n            padding: ${spacing[1]};\n            height: var(--radix-select-trigger-height);\n            width: ${width.full};\n            min-width: var(--radix-select-trigger-width);\n          `}\n        >\n          {createSelectItem(\"light\", \"Light\")}\n          {createSelectItem(\"dark\", \"Dark\")}\n          {createSelectItem(\"system\", \"System\")}\n        </radix.SelectViewport>\n      </radix.SelectContent>\n    </radix.Select>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/select.tsx",
    "content": "import {\n  type ForwardRefExoticComponent,\n  type ComponentProps,\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type RefAttributes,\n  useContext,\n  type ComponentPropsWithRef,\n  useState,\n  useEffect,\n} from \"react\";\nimport {\n  Root,\n  Value,\n  Trigger,\n  Content,\n  Item,\n  ItemIndicator,\n  ItemText,\n  Portal,\n  Viewport,\n} from \"@radix-ui/react-select\";\nimport {\n  type Hook,\n  getClosestInstance,\n  ReactSdkContext,\n} from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Select: ForwardRefExoticComponent<\n  ComponentPropsWithRef<typeof Root>\n> = forwardRef(({ defaultOpen, defaultValue, ...props }, _ref) => {\n  // open state\n  const currentOpen = props.open ?? defaultOpen ?? false;\n  const [open, setOpen] = useState(currentOpen);\n  // synchronize external value with local one when changed\n  useEffect(() => setOpen(currentOpen), [currentOpen]);\n  // value state\n  const currentValue = props.value ?? defaultValue ?? \"\";\n  const [value, setValue] = useState(currentValue);\n  // synchronize external value with local one when changed\n  useEffect(() => setValue(currentValue), [currentValue]);\n  return (\n    <Root\n      {...props}\n      open={open}\n      onOpenChange={setOpen}\n      value={value}\n      onValueChange={setValue}\n    />\n  );\n});\n\nexport const SelectTrigger = forwardRef<\n  HTMLButtonElement,\n  ComponentPropsWithoutRef<typeof Trigger>\n>((props, ref) => {\n  const { renderer } = useContext(ReactSdkContext);\n\n  const onPointerDown =\n    renderer === \"canvas\"\n      ? (event: React.PointerEvent) => {\n          event.preventDefault();\n        }\n      : undefined;\n\n  return <Trigger onPointerDown={onPointerDown} ref={ref} {...props} />;\n});\n\nexport const SelectValue = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentPropsWithoutRef<typeof Value>, \"placeholder\"> & {\n    placeholder?: string;\n  }\n>((props, ref) => {\n  return <Value ref={ref} {...props} />;\n});\n\nexport const SelectContent = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentPropsWithoutRef<typeof Content>, \"position\" | \"side\">\n>((props, ref) => {\n  return (\n    <Portal>\n      <Content ref={ref} {...props} position=\"popper\" />\n    </Portal>\n  );\n});\n\nexport const SelectViewport: ForwardRefExoticComponent<\n  ComponentProps<typeof Viewport> & RefAttributes<HTMLDivElement>\n> = Viewport;\n\nexport const SelectItem: ForwardRefExoticComponent<\n  ComponentProps<typeof Item> & RefAttributes<HTMLDivElement>\n> = Item;\n\nexport const SelectItemIndicator: ForwardRefExoticComponent<\n  ComponentProps<typeof ItemIndicator> & RefAttributes<HTMLSpanElement>\n> = ItemIndicator;\n\nexport const SelectItemText: ForwardRefExoticComponent<\n  ComponentProps<typeof ItemText> & RefAttributes<HTMLSpanElement>\n> = ItemText;\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each SelectContent component within the selection,\n// we identify its closest parent Select component\n// and update its open prop bound to variable.\nexport const hooksSelect: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:SelectContent`) {\n        const select = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Select`\n        );\n        if (select) {\n          context.setMemoryProp(select, \"open\", undefined);\n        }\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:SelectContent`) {\n        const select = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Select`\n        );\n        if (select) {\n          context.setMemoryProp(select, \"open\", true);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/select.ws.ts",
    "content": "import {\n  SelectIcon,\n  TriggerIcon,\n  FormTextFieldIcon,\n  ContentIcon,\n  ItemIcon,\n  ViewportIcon,\n  TextIcon,\n  CheckMarkIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button, div, span } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport {\n  propsSelect,\n  propsSelectContent,\n  propsSelectItem,\n  propsSelectItemIndicator,\n  propsSelectItemText,\n  propsSelectTrigger,\n  propsSelectValue,\n  propsSelectViewport,\n} from \"./__generated__/select.props\";\n\nexport const metaSelect: WsComponentMeta = {\n  icon: SelectIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.SelectTrigger, radix.SelectContent],\n  },\n  initialProps: [\"name\", \"value\", \"open\", \"required\"],\n  props: propsSelect,\n};\n\nexport const metaSelectTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.SelectValue],\n  },\n  presetStyle: { button },\n  props: propsSelectTrigger,\n};\n\nexport const metaSelectValue: WsComponentMeta = {\n  label: \"Value\",\n  icon: FormTextFieldIcon,\n  contentModel: {\n    category: \"none\",\n    children: [],\n  },\n  presetStyle: { span },\n  initialProps: [\"placeholder\"],\n  props: propsSelectValue,\n};\n\nexport const metaSelectContent: WsComponentMeta = {\n  icon: ContentIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.SelectViewport],\n  },\n  presetStyle: { div },\n  props: propsSelectContent,\n};\n\nexport const metaSelectViewport: WsComponentMeta = {\n  icon: ViewportIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.SelectItem],\n  },\n  presetStyle: { div },\n  props: propsSelectViewport,\n};\n\nexport const metaSelectItem: WsComponentMeta = {\n  icon: ItemIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.SelectItemIndicator, radix.SelectItemText],\n  },\n  presetStyle: { div },\n  initialProps: [\"value\"],\n  props: propsSelectItem,\n};\n\nexport const metaSelectItemIndicator: WsComponentMeta = {\n  label: \"Indicator\",\n  icon: CheckMarkIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { span },\n  props: propsSelectItemIndicator,\n};\n\nexport const metaSelectItemText: WsComponentMeta = {\n  label: \"Item Text\",\n  icon: TextIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: { span },\n  props: propsSelectItemText,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/shared/meta.ts",
    "content": "const createMetaProxy = (prefix: string): Record<string, string> => {\n  return new Proxy(\n    {},\n    {\n      get(_target, prop) {\n        return `${prefix}${prop as string}`;\n      },\n    }\n  );\n};\n\nexport const radix = createMetaProxy(\n  \"@webstudio-is/sdk-components-react-radix:\"\n);\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/shared/preset-styles.ts",
    "content": "// this module should not rely on \"template\" and other unpublished packages\nimport type { CssProperty, Unit } from \"@webstudio-is/css-engine\";\nimport type { PresetStyleDecl } from \"@webstudio-is/sdk\";\n\nconst unit = (property: CssProperty, value: number, unit: Unit) => ({\n  property,\n  value: { type: \"unit\", unit, value } as const,\n});\n\nconst keyword = (property: CssProperty, value: string) => ({\n  property,\n  value: { type: \"keyword\", value } as const,\n});\n\nconst rgb = (property: CssProperty, r: number, g: number, b: number) => ({\n  property,\n  value: { type: \"rgb\", alpha: 1, r, g, b } as const,\n});\n\nexport const buttonReset: PresetStyleDecl[] = [\n  {\n    property: \"background-color\",\n    value: { type: \"keyword\", value: \"transparent\" },\n  },\n  {\n    property: \"background-image\",\n    value: { type: \"keyword\", value: \"none\" },\n  },\n\n  unit(\"border-top-width\", 0, \"px\"),\n  unit(\"border-right-width\", 0, \"px\"),\n  unit(\"border-bottom-width\", 0, \"px\"),\n  unit(\"border-left-width\", 0, \"px\"),\n  keyword(\"border-top-style\", \"solid\"),\n  keyword(\"border-right-style\", \"solid\"),\n  keyword(\"border-bottom-style\", \"solid\"),\n  keyword(\"border-left-style\", \"solid\"),\n  rgb(\"border-top-color\", 226, 232, 240),\n  rgb(\"border-right-color\", 226, 232, 240),\n  rgb(\"border-bottom-color\", 226, 232, 240),\n  rgb(\"border-left-color\", 226, 232, 240),\n\n  unit(\"padding-top\", 0, \"px\"),\n  unit(\"padding-right\", 0, \"px\"),\n  unit(\"padding-bottom\", 0, \"px\"),\n  unit(\"padding-left\", 0, \"px\"),\n];\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/shared/proxy.ts",
    "content": "import { createProxy } from \"@webstudio-is/template\";\n\nexport const radix = createProxy(\"@webstudio-is/sdk-components-react-radix:\");\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/shared/styles.ts",
    "content": "import { css, type TemplateStyleDecl } from \"@webstudio-is/template\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  height,\n  opacity,\n  spacing,\n  weights,\n} from \"./theme\";\n\n// ghost icon button\n// 'inline-flex items-center justify-center rounded-md text-sm font-medium\n// ring-offset-background transition-colors\n// focus-visible:outline-none focus-visible:ring-2\n// focus-visible:ring-ring focus-visible:ring-offset-2\n\n// disabled:pointer-events-none disabled:opacity-50'\nconst buttonStyle = css`\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  background-color: transparent;\n  border: 0 solid ${colors.border};\n  border-radius: ${borderRadius.md};\n  font-size: ${fontSize.sm};\n  line-height: ${fontSizeLineHeight.sm};\n  font-weight: ${weights.medium};\n  &:focus-visible {\n    outline: 2px solid transparent;\n    outline-offset: 2px;\n    box-shadow: ${boxShadow.ring};\n  }\n  &:disabled {\n    pointer-events: none;\n    opacity: ${opacity[50]};\n  }\n`;\n\n// hover:bg-accent hover:text-accent-foreground\nconst ghostButtonStyle = css`\n  &:hover {\n    background-color: ${colors.accent};\n    color: ${colors.accentForeground};\n  }\n`;\n\n// border border-input bg-background hover:bg-accent hover:text-accent-foreground\nconst outlineButtonStyle = css`\n  border: ${borderWidth.DEFAULT} solid ${colors.input};\n  background-color: ${colors.background};\n  &:hover {\n    background-color: ${colors.accent};\n    color: ${colors.accentForeground};\n  }\n`;\n\n// h-10 px-4 py-2\nconst defaultButtonStyle = css`\n  height: ${height[10]};\n  padding: ${spacing[2]} ${spacing[4]};\n`;\n\n// h-9 rounded-md px-3\nconst smButtonStyle = css`\n  height: ${height[9]};\n  border-radius: ${borderRadius.md};\n  padding: 0 ${spacing[3]};\n`;\n\n// Set explicit paddings for IOS Safari to prevent the icon from collapsing\n// h-10 w-10\nconst iconButtonStyle = css`\n  height: ${spacing[10]};\n  width: ${spacing[10]};\n  padding: ${spacing[0]} ${spacing[1.5]};\n`;\n\nexport const getButtonStyle = (\n  variant: \"outline\" | \"ghost\",\n  size: \"default\" | \"sm\" | \"icon\" = \"default\"\n) => {\n  const style: TemplateStyleDecl[] = [...buttonStyle];\n  if (variant === \"ghost\") {\n    style.push(...ghostButtonStyle);\n  }\n  if (variant === \"outline\") {\n    style.push(...outlineButtonStyle);\n  }\n  if (size === \"default\") {\n    style.push(...defaultButtonStyle);\n  }\n  if (size === \"sm\") {\n    style.push(...smButtonStyle);\n  }\n  if (size === \"icon\") {\n    style.push(...iconButtonStyle);\n  }\n  return style;\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/shared/theme.ts",
    "content": "export const fontSize = {\n  sm: \"0.875rem\",\n  lg: \"1.125rem\",\n} as const;\n\nexport const fontSizeLineHeight = {\n  sm: \"1.25rem\",\n  lg: \"1.75rem\",\n} as const;\n\nexport const lineHeight = {\n  none: \"1\",\n  snug: \"1.375\",\n} as const;\n\nexport const weights = {\n  medium: \"500\",\n} as const;\n\nexport const letterSpacing = {\n  tight: \"-0.025em\",\n} as const;\n\nexport const spacing = {\n  \"0\": \"0px\",\n  \"1\": \"0.25rem\",\n  \"2\": \"0.5rem\",\n  \"3\": \"0.75rem\",\n  \"4\": \"1rem\",\n  \"5\": \"1.25rem\",\n  \"6\": \"1.5rem\",\n  \"7\": \"1.75rem\",\n  \"8\": \"2rem\",\n  \"9\": \"2.25rem\",\n  \"10\": \"2.5rem\",\n  \"11\": \"2.75rem\",\n  \"12\": \"3rem\",\n  \"14\": \"3.5rem\",\n  \"16\": \"4rem\",\n  \"20\": \"5rem\",\n  \"24\": \"6rem\",\n  \"28\": \"7rem\",\n  \"32\": \"8rem\",\n  \"36\": \"9rem\",\n  \"40\": \"10rem\",\n  \"44\": \"11rem\",\n  \"48\": \"12rem\",\n  \"52\": \"13rem\",\n  \"56\": \"14rem\",\n  \"60\": \"15rem\",\n  \"64\": \"16rem\",\n  \"72\": \"18rem\",\n  \"80\": \"20rem\",\n  \"96\": \"24rem\",\n  px: \"1px\",\n  \"0.5\": \"0.125rem\",\n  \"1.5\": \"0.375rem\",\n  \"2.5\": \"0.625rem\",\n  \"3.5\": \"0.875rem\",\n} as const;\n\nexport const width = {\n  ...spacing,\n  full: \"100%\",\n} as const;\n\nexport const height = {\n  ...spacing,\n  full: \"100%\",\n} as const;\n\nexport const maxWidth = {\n  \"0\": \"0rem\",\n  xs: \"20rem\",\n  sm: \"24rem\",\n  md: \"28rem\",\n  lg: \"32rem\",\n  xl: \"36rem\",\n  prose: \"65ch\",\n} as const;\n\nexport const borderWidth = {\n  \"0\": \"0px\",\n  \"2\": \"2px\",\n  \"4\": \"4px\",\n  \"8\": \"8px\",\n  DEFAULT: \"1px\",\n};\n\nexport const borderRadius = {\n  sm: \"0.125rem\",\n  md: \"0.375rem\",\n  full: \"9999px\",\n} as const;\n\nexport const colors = {\n  transparent: \"transparent\",\n  current: \"currentColor\",\n  inherit: \"inherit\",\n  popover: \"rgb(255, 255, 255)\",\n  popoverForeground: \"rgb(2, 8, 23)\",\n  border: \"rgb(226, 232, 240)\",\n  background: \"rgb(255, 255, 255)\",\n  foreground: \"hsl(222.2 84% 4.9%)\",\n  ring: \"rgb(148, 163, 184)\",\n  mutedForeground: \"rgb(100, 116, 139)\",\n  muted: \"hsl(210 40% 96.1%)\",\n  primary: \"rgb(15, 23, 42)\",\n  primaryForeground: \"hsl(210 40% 98%)\",\n  destructive: \"rgb(239, 68, 68)\",\n  destructiveForeground: \"rgb(248, 250, 252)\",\n  accent: \"rgb(241, 245, 249)\",\n  accentForeground: \"rgb(15, 23, 42)\",\n  input: \"rgb(226, 232, 240)\",\n  secondary: \"rgb(241, 245, 249)\",\n  secondaryForeground: \"rgb(15, 23, 42)\",\n} as const;\n\nexport const transition = {\n  all: \"all 150ms cubic-bezier(0.4, 0, 0.2, 1)\",\n  transform: \"transform 150ms cubic-bezier(0.4, 0, 0.2, 1)\",\n} as const;\n\nexport const opacity = {\n  \"0\": \"0\",\n  \"5\": \"0.05\",\n  \"10\": \"0.1\",\n  \"20\": \"0.2\",\n  \"25\": \"0.25\",\n  \"30\": \"0.3\",\n  \"40\": \"0.4\",\n  \"50\": \"0.5\",\n  \"60\": \"0.6\",\n  \"70\": \"0.7\",\n  \"75\": \"0.75\",\n  \"80\": \"0.8\",\n  \"90\": \"0.9\",\n  \"95\": \"0.95\",\n  \"100\": \"1\",\n} as const;\n\nconst ringWidth = {\n  \"0\": \"0px\",\n  \"1\": \"1px\",\n  \"2\": \"2px\",\n  \"4\": \"4px\",\n  \"8\": \"8px\",\n  DEFAULT: \"3px\",\n} as const;\n\nconst ringOffsetWidth = {\n  \"0\": \"0px\",\n  \"1\": \"1px\",\n  \"2\": \"2px\",\n  \"4\": \"4px\",\n  \"8\": \"8px\",\n} as const;\n\nexport const boxShadow = {\n  sm: \"0 1px 2px 0 rgb(0 0 0 / 0.05)\",\n  md: \"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)\",\n  lg: \"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)\",\n  // 0 0 0 ringOffsetWidth ringOffsetColor\n  // 0 0 0 ringWidth + ringOffsetWidth ringColor\n  ring:\n    `0 0 0 ${ringOffsetWidth[2]} ${colors.background}, ` +\n    `0 0 0 calc(${ringWidth[2]} + ${ringOffsetWidth[2]}) ${colors.ring}`,\n} as const;\n\nexport const zIndex = {\n  \"0\": \"0\",\n  \"10\": \"10\",\n  \"20\": \"20\",\n  \"30\": \"30\",\n  \"40\": \"40\",\n  \"50\": \"50\",\n} as const;\n\nexport const blur = {\n  sm: \"blur(0 1px 2px 0 rgb(0 0 0 / 0.05))\",\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/sheet.template.tsx",
    "content": "import { LargeXIcon, MenuIcon } from \"@webstudio-is/icons/svg\";\nimport {\n  type TemplateMeta,\n  $,\n  css,\n  PlaceholderValue,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  blur,\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  height,\n  letterSpacing,\n  lineHeight,\n  maxWidth,\n  opacity,\n  spacing,\n  width,\n  zIndex,\n} from \"./shared/theme\";\nimport { getButtonStyle } from \"./shared/styles\";\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/sheet.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  icon: MenuIcon,\n  description:\n    \"Displays content in a menu that slides out from the side of the screen, triggered by a button. Use this component for a typical mobile hamburger menu.\",\n  order: 1,\n  template: (\n    <radix.Dialog ws:label=\"Sheet\">\n      <radix.DialogTrigger ws:label=\"Sheet Trigger\">\n        <$.Button ws:style={getButtonStyle(\"ghost\", \"icon\")}>\n          <$.HtmlEmbed ws:label=\"Hamburger Menu Svg\" code={MenuIcon} />\n        </$.Button>\n      </radix.DialogTrigger>\n      <radix.DialogOverlay\n        ws:label=\"Sheet Overlay\"\n        /**\n         * fixed inset-0 z-50 bg-background/80 backdrop-blur-sm\n         * flex\n         **/\n        ws:style={css`\n          position: fixed;\n          inset: 0;\n          z-index: ${zIndex[50]};\n          background-color: rgb(255 255 255 / 0.8);\n          backdrop-filter: ${blur.sm};\n          /* To allow positioning Content */\n          display: flex;\n          flex-direction: column;\n          overflow: auto;\n        `}\n      >\n        <radix.DialogContent\n          ws:label=\"Sheet Content\"\n          /**\n           * fixed w-full z-50\n           * grid gap-4 max-w-lg\n           * m-auto\n           * border bg-background p-6 shadow-lg\n           **/\n          ws:style={css`\n            width: ${width.full};\n            z-index: ${zIndex[50]};\n            display: flex;\n            flex-direction: column;\n            gap: ${spacing[4]};\n            border: ${borderWidth.DEFAULT} solid ${colors.border};\n            background-color: ${colors.background};\n            padding: ${spacing[6]};\n            box-shadow: ${boxShadow.lg};\n            position: relative;\n            /* side=left */\n            margin-right: auto;\n            max-width: ${maxWidth.sm};\n            flex-grow: 1;\n          `}\n        >\n          <$.Box ws:label=\"Navigation\" ws:tag=\"nav\">\n            <$.Box\n              ws:label=\"Sheet Header\"\n              ws:style={css`\n                display: flex;\n                flex-direction: column;\n                gap: ${spacing[2]};\n              `}\n            >\n              <radix.DialogTitle\n                ws:label=\"Sheet Title\"\n                /**\n                 * text-lg leading-none tracking-tight\n                 **/\n                ws:style={css`\n                  font-size: ${fontSize.lg};\n                  line-height: ${lineHeight.none};\n                  letter-spacing: ${letterSpacing.tight};\n                  margin: 0;\n                `}\n              >\n                {new PlaceholderValue(\"Sheet Title\")}\n              </radix.DialogTitle>\n              <radix.DialogDescription\n                ws:label=\"Sheet Description\"\n                /**\n                 * text-sm text-muted-foreground\n                 **/\n                ws:style={css`\n                  font-size: ${fontSize.sm};\n                  line-height: ${fontSizeLineHeight.sm};\n                  color: ${colors.mutedForeground};\n                  margin: 0;\n                `}\n              >\n                {new PlaceholderValue(\"Sheet description text you can edit\")}\n              </radix.DialogDescription>\n            </$.Box>\n            <$.Text>{new PlaceholderValue(\"The text you can edit\")}</$.Text>\n          </$.Box>\n          <radix.DialogClose\n            ws:label=\"Close Button\"\n            /**\n             * absolute right-4 top-4\n             * rounded-sm opacity-70\n             * ring-offset-background\n             * hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\n             * flex items-center justify-center h-4 w-4\n             **/\n            ws:style={css`\n              position: absolute;\n              right: ${spacing[4]};\n              top: ${spacing[4]};\n              border-radius: ${borderRadius.sm};\n              opacity: ${opacity[70]};\n              display: flex;\n              align-items: center;\n              justify-content: center;\n              height: ${height[4]};\n              width: ${height[4]};\n              border: 0;\n              background-color: transparent;\n              outline: none;\n              &:hover {\n                opacity: ${opacity[100]};\n              }\n              &:focus-visible {\n                box-shadow: ${boxShadow.ring};\n              }\n            `}\n          >\n            <$.HtmlEmbed ws:label=\"Close Icon\" code={LargeXIcon} />\n          </radix.DialogClose>\n        </radix.DialogContent>\n      </radix.DialogOverlay>\n    </radix.Dialog>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/switch.template.tsx",
    "content": "import { css, type TemplateMeta } from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  borderRadius,\n  borderWidth,\n  boxShadow,\n  colors,\n  height,\n  opacity,\n  transition,\n  width,\n} from \"./shared/theme\";\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"A control that allows the user to toggle between checked and not checked.\",\n  order: 11,\n  template: (\n    <radix.Switch\n      // peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors\n      // focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\n      // disabled:cursor-not-allowed disabled:opacity-50\n      // data-[state=checked]:bg-primary\n      // data-[state=unchecked]:bg-input\n      ws:style={css`\n        display: inline-flex;\n        height: 24px;\n        width: 44px;\n        flex-shrink: 0;\n        cursor: pointer;\n        align-items: center;\n        border-radius: ${borderRadius.full};\n        border: ${borderWidth[2]} solid transparent;\n        transition: ${transition.all};\n        &:focus-visible {\n          outline: none;\n          box-shadow: ${boxShadow.ring};\n        }\n        &:disabled {\n          cursor: not-allowed;\n          opacity: ${opacity[50]};\n        }\n        &[data-state=\"checked\"] {\n          background-color: ${colors.primary};\n        }\n        &[data-state=\"unchecked\"] {\n          background-color: ${colors.input};\n        }\n      `}\n    >\n      <radix.SwitchThumb\n        // pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform\n        // data-[state=checked]:translate-x-5\n        // data-[state=unchecked]:translate-x-0\n        ws:style={css`\n          pointer-events: none;\n          display: block;\n          height: ${height[5]};\n          width: ${width[5]};\n          border-radius: ${borderRadius.full};\n          background-color: ${colors.background};\n          box-shadow: ${boxShadow.lg};\n          transition: ${transition.transform};\n          &[data-state=\"checked\"] {\n            transform: translateX(20px);\n          }\n          &[data-state=\"unchecked\"] {\n            transform: translateX(0px);\n          }\n        `}\n      />\n    </radix.Switch>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/switch.tsx",
    "content": "import {\n  type ForwardRefExoticComponent,\n  type ComponentProps,\n  type RefAttributes,\n  forwardRef,\n  useEffect,\n  useState,\n} from \"react\";\nimport { Root, Thumb } from \"@radix-ui/react-switch\";\n\nexport const Switch = forwardRef<\n  HTMLButtonElement,\n  ComponentProps<typeof Root>\n>(({ defaultChecked, ...props }, ref) => {\n  const currentChecked = props.checked ?? defaultChecked ?? false;\n  const [checked, setChecked] = useState(currentChecked);\n  // synchronize external value with local one when changed\n  useEffect(() => setChecked(currentChecked), [currentChecked]);\n  return (\n    <Root {...props} ref={ref} checked={checked} onCheckedChange={setChecked} />\n  );\n});\n\nexport const SwitchThumb: ForwardRefExoticComponent<\n  ComponentProps<typeof Thumb> & RefAttributes<HTMLSpanElement>\n> = Thumb;\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/switch.ws.ts",
    "content": "import { SwitchIcon, TriggerIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button, span } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport { buttonReset } from \"./shared/preset-styles\";\nimport { propsSwitch, propsSwitchThumb } from \"./__generated__/switch.props\";\n\nexport const metaSwitch: WsComponentMeta = {\n  icon: SwitchIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.SwitchThumb],\n  },\n  states: [\n    { label: \"Checked\", selector: \"[data-state=checked]\" },\n    { label: \"Unchecked\", selector: \"[data-state=unchecked]\" },\n  ],\n  presetStyle: {\n    button: [button, buttonReset].flat(),\n  },\n  initialProps: [\"id\", \"class\", \"name\", \"value\", \"checked\", \"required\"],\n  props: propsSwitch,\n};\n\nexport const metaSwitchThumb: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  states: [\n    { label: \"Checked\", selector: \"[data-state=checked]\" },\n    { label: \"Unchecked\", selector: \"[data-state=unchecked]\" },\n  ],\n  presetStyle: {\n    span,\n  },\n  props: propsSwitchThumb,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/tabs.template.tsx",
    "content": "import {\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport {\n  borderRadius,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  height,\n  weights,\n  transition,\n  opacity,\n  boxShadow,\n  spacing,\n} from \"./shared/theme\";\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/tabs.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\n\n// inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all\n// focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\n// disabled:pointer-events-none disabled:opacity-50\n// data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\n\nconst tabsTriggerStyle = css`\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  white-space: nowrap;\n  border-radius: ${borderRadius.md};\n  padding: ${spacing[1.5]} ${spacing[3]};\n  font-size: ${fontSize.sm};\n  line-height: ${fontSizeLineHeight.sm};\n  font-weight: ${weights.medium};\n  transition: ${transition.all};\n  &:focus-visible {\n    outline: 2px solid transparent;\n    outline-offset: 2px;\n    box-shadow: ${boxShadow.ring};\n  }\n  &:disabled {\n    pointer-events: none;\n    opacity: ${opacity[50]};\n  }\n  &[data-state=\"active\"] {\n    background-color: ${colors.background};\n    color: ${colors.foreground};\n    box-shadow: ${boxShadow.sm};\n  }\n`;\n\n// mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\nconst tabsContentStyle = css`\n  margin-top: ${spacing[2]};\n  &:focus-visible {\n    outline: none;\n    box-shadow: ${boxShadow.ring};\n  }\n`;\n\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"A set of panels with content that are displayed one at a time. Duplicate both a tab trigger and tab content to add more tabs. Triggers and content are connected according to their order in the Navigator.\",\n  order: 2,\n  template: (\n    <radix.Tabs value=\"0\">\n      <radix.TabsList\n        // inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\n        ws:style={css`\n          display: inline-flex;\n          height: ${height[10]};\n          align-items: center;\n          justify-content: center;\n          border-radius: ${borderRadius.md};\n          background-color: ${colors.muted};\n          padding: ${spacing[1]};\n          color: ${colors.mutedForeground};\n        `}\n      >\n        <radix.TabsTrigger ws:style={tabsTriggerStyle}>\n          {new PlaceholderValue(\"Account\")}\n        </radix.TabsTrigger>\n        <radix.TabsTrigger ws:style={tabsTriggerStyle}>\n          {new PlaceholderValue(\"Password\")}\n        </radix.TabsTrigger>\n      </radix.TabsList>\n      <radix.TabsContent ws:style={tabsContentStyle}>\n        {new PlaceholderValue(\"Make changes to your account here.\")}\n      </radix.TabsContent>\n      <radix.TabsContent ws:style={tabsContentStyle}>\n        {new PlaceholderValue(\"Change your password here.\")}\n      </radix.TabsContent>\n    </radix.Tabs>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/tabs.tsx",
    "content": "import interactionResponse from \"await-interaction-response\";\nimport {\n  type ComponentProps,\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  useCallback,\n  useEffect,\n  useState,\n} from \"react\";\nimport { Root, List, Trigger, Content } from \"@radix-ui/react-tabs\";\nimport { getIndexWithinAncestorFromProps } from \"@webstudio-is/sdk/runtime\";\nimport { getClosestInstance, type Hook } from \"@webstudio-is/react-sdk/runtime\";\n\nexport const Tabs = forwardRef<HTMLDivElement, ComponentProps<typeof Root>>(\n  ({ defaultValue, ...props }, ref) => {\n    const currentValue = props.value ?? defaultValue ?? \"\";\n    const [value, setValue] = useState(currentValue);\n    // synchronize external value with local one when changed\n    useEffect(() => setValue(currentValue), [currentValue]);\n\n    const handleValueChange = useCallback(async (value: string) => {\n      await interactionResponse();\n      setValue(value);\n    }, []);\n\n    return (\n      <Root\n        ref={ref}\n        {...props}\n        value={value}\n        onValueChange={handleValueChange}\n      />\n    );\n  }\n);\n\nexport const TabsList = List;\n\nexport const TabsTrigger = forwardRef<\n  HTMLButtonElement,\n  Omit<ComponentPropsWithoutRef<typeof Trigger>, \"value\"> & { value?: string }\n>(({ value, ...props }, ref) => {\n  const index = getIndexWithinAncestorFromProps(props);\n  return <Trigger ref={ref} value={value ?? index ?? \"\"} {...props} />;\n});\n\nexport const TabsContent = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentPropsWithoutRef<typeof Content>, \"value\"> & { value?: string }\n>(({ value, ...props }, ref) => {\n  const index = getIndexWithinAncestorFromProps(props);\n  return <Content ref={ref} value={value ?? index ?? \"\"} {...props} />;\n});\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each TabsContent component within the selection,\n// we identify its closest parent Tabs component\n// and update its open prop bound to variable.\nexport const hooksTabs: Hook = {\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (\n        instance.component === `${namespace}:TabsContent` ||\n        instance.component === `${namespace}:TabsTrigger`\n      ) {\n        const tabs = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Tabs`\n        );\n        const contentValue =\n          context.getPropValue(instance, \"value\") ??\n          context.indexesWithinAncestors.get(instance.id)?.toString();\n        if (tabs && contentValue) {\n          context.setMemoryProp(tabs, \"value\", contentValue);\n        }\n      }\n    }\n  },\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (\n        instance.component === `${namespace}:TabsContent` ||\n        instance.component === `${namespace}:TabsTrigger`\n      ) {\n        const tabs = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Tabs`\n        );\n        const contentValue =\n          context.getPropValue(instance, \"value\") ??\n          context.indexesWithinAncestors.get(instance.id)?.toString();\n        if (tabs && contentValue) {\n          context.setMemoryProp(tabs, \"value\", undefined);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/tabs.ws.ts",
    "content": "import {\n  ContentIcon,\n  HeaderIcon,\n  TabsIcon,\n  TriggerIcon,\n} from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { button, div } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport { buttonReset } from \"./shared/preset-styles\";\nimport {\n  propsTabs,\n  propsTabsList,\n  propsTabsTrigger,\n  propsTabsContent,\n} from \"./__generated__/tabs.props\";\n\nexport const metaTabs: WsComponentMeta = {\n  icon: TabsIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.TabsList, radix.TabsContent],\n  },\n  presetStyle: { div },\n  props: propsTabs,\n};\n\nexport const metaTabsList: WsComponentMeta = {\n  icon: HeaderIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n    descendants: [radix.TabsTrigger],\n  },\n  presetStyle: { div },\n  props: propsTabsList,\n};\n\nexport const metaTabsTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  label: \"Tab Trigger\",\n  indexWithinAncestor: radix.Tabs,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  states: [{ label: \"Active\", selector: \"[data-state=active]\" }],\n  presetStyle: {\n    button: [button, buttonReset].flat(),\n  },\n  props: propsTabsTrigger,\n};\n\nexport const metaTabsContent: WsComponentMeta = {\n  label: \"Tab Content\",\n  icon: ContentIcon,\n  indexWithinAncestor: radix.Tabs,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\", \"rich-text\"],\n  },\n  presetStyle: { div },\n  props: propsTabsContent,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/templates.ts",
    "content": "export { meta as Label } from \"./label.template\";\nexport { meta as Tabs } from \"./tabs.template\";\nexport { meta as Sheet } from \"./sheet.template\";\nexport { meta as Dialog } from \"./dialog.template\";\nexport { meta as Switch } from \"./switch.template\";\nexport { meta as Checkbox } from \"./checkbox.template\";\nexport { meta as Collapsible } from \"./collapsible.template\";\nexport { meta as Accordion } from \"./accordion.template\";\nexport { meta as Tooltip } from \"./tooltip.template\";\nexport { meta as Popover } from \"./popover.template\";\nexport { meta as RadioGroup } from \"./radio-group.template\";\nexport { meta as Select } from \"./select.template\";\nexport { meta as NavigationMenu } from \"./navigation-menu.template\";\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/tooltip.template.tsx",
    "content": "import {\n  $,\n  css,\n  PlaceholderValue,\n  type TemplateMeta,\n} from \"@webstudio-is/template\";\nimport { radix } from \"./shared/proxy\";\nimport { getButtonStyle } from \"./shared/styles\";\nimport {\n  borderRadius,\n  boxShadow,\n  colors,\n  fontSize,\n  fontSizeLineHeight,\n  spacing,\n  zIndex,\n} from \"./shared/theme\";\n\n/**\n * Styles source without animations:\n * https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/tooltip.tsx\n *\n * Attributions\n * MIT License\n * Copyright (c) 2023 shadcn\n **/\nexport const meta: TemplateMeta = {\n  category: \"radix\",\n  description:\n    \"Displays content that is related to the trigger, when the trigger is hovered with the mouse or focused with the keyboard. You are reading an example of a tooltip right now.\",\n  order: 7,\n  template: (\n    <radix.Tooltip>\n      <radix.TooltipTrigger>\n        <$.Button ws:style={getButtonStyle(\"outline\")}>\n          {new PlaceholderValue(\"Button\")}\n        </$.Button>\n      </radix.TooltipTrigger>\n      <radix.TooltipContent\n        /**\n         *  z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md\n         **/\n        ws:style={css`\n          z-index: ${zIndex[50]};\n          overflow: hidden;\n          border-radius: ${borderRadius.md};\n          background-color: ${colors.popover};\n          padding: ${spacing[1.5]} ${spacing[3]};\n          font-size: ${fontSize.sm};\n          line-height: ${fontSizeLineHeight.sm};\n          color: ${colors.popoverForeground};\n          box-shadow: ${boxShadow.md};\n        `}\n      >\n        <$.Text>{new PlaceholderValue(\"The text you can edit\")}</$.Text>\n      </radix.TooltipContent>\n    </radix.Tooltip>\n  ),\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/tooltip.tsx",
    "content": "import * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { getClosestInstance, type Hook } from \"@webstudio-is/react-sdk/runtime\";\n\nimport {\n  forwardRef,\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  Children,\n  useState,\n  useEffect,\n} from \"react\";\n\nexport const Tooltip = forwardRef<\n  HTMLDivElement,\n  Omit<ComponentPropsWithoutRef<typeof TooltipPrimitive.Root>, \"defaultOpen\">\n>((props, _ref) => {\n  const currentOpen = props.open ?? false;\n  const [open, setOpen] = useState(currentOpen);\n  // synchronize external value with local one when changed\n  useEffect(() => setOpen(currentOpen), [currentOpen]);\n  return (\n    <TooltipPrimitive.Provider>\n      <TooltipPrimitive.Root {...props} open={open} onOpenChange={setOpen} />\n    </TooltipPrimitive.Provider>\n  );\n});\n\n/**\n * We're not exposing the 'asChild' property for the Trigger.\n * Instead, we're enforcing 'asChild=true' for the Trigger and making it style-less.\n * This avoids situations where the Trigger inadvertently passes all styles to its child,\n * which would prevent us from displaying styles properly in the builder.\n */\nexport const TooltipTrigger = forwardRef<\n  HTMLButtonElement,\n  { children: ReactNode }\n>(({ children, ...props }, ref) => {\n  const firstChild = Children.toArray(children)[0];\n\n  return (\n    <TooltipPrimitive.Trigger asChild={true} ref={ref} {...props}>\n      {firstChild ?? <button>Add button or link</button>}\n    </TooltipPrimitive.Trigger>\n  );\n});\n\nexport const TooltipContent = forwardRef<\n  HTMLDivElement,\n  ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ sideOffset = 4, hideWhenDetached = true, ...props }, ref) => (\n  <TooltipPrimitive.Portal>\n    <TooltipPrimitive.Content\n      ref={ref}\n      // Do not show content if trigger is detached\n      hideWhenDetached={hideWhenDetached}\n      sideOffset={sideOffset}\n      {...props}\n    />\n  </TooltipPrimitive.Portal>\n));\n\n/* BUILDER HOOKS */\n\nconst namespace = \"@webstudio-is/sdk-components-react-radix\";\n\n// For each TooltipContent component within the selection,\n// we identify its closest parent Tooltip component\n// and update its open prop bound to variable.\nexport const hooksTooltip: Hook = {\n  onNavigatorUnselect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:TooltipContent`) {\n        const tooltip = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Tooltip`\n        );\n        if (tooltip) {\n          context.setMemoryProp(tooltip, \"open\", undefined);\n        }\n      }\n    }\n  },\n  onNavigatorSelect: (context, event) => {\n    for (const instance of event.instancePath) {\n      if (instance.component === `${namespace}:TooltipContent`) {\n        const tooltip = getClosestInstance(\n          event.instancePath,\n          instance,\n          `${namespace}:Tooltip`\n        );\n        if (tooltip) {\n          context.setMemoryProp(tooltip, \"open\", true);\n        }\n      }\n    }\n  },\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/src/tooltip.ws.ts",
    "content": "import { TooltipIcon, TriggerIcon, ContentIcon } from \"@webstudio-is/icons/svg\";\nimport type { WsComponentMeta } from \"@webstudio-is/sdk\";\nimport { div } from \"@webstudio-is/sdk/normalize.css\";\nimport { radix } from \"./shared/meta\";\nimport {\n  propsTooltip,\n  propsTooltipContent,\n  propsTooltipTrigger,\n} from \"./__generated__/tooltip.props\";\n\n// @todo add [data-state] to button and link\nexport const metaTooltipTrigger: WsComponentMeta = {\n  icon: TriggerIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  props: propsTooltipTrigger,\n};\n\nexport const metaTooltipContent: WsComponentMeta = {\n  icon: ContentIcon,\n  contentModel: {\n    category: \"none\",\n    children: [\"instance\"],\n  },\n  presetStyle: { div },\n  initialProps: [\"side\", \"sideOffset\", \"align\", \"alignOffset\"],\n  props: propsTooltipContent,\n};\n\nexport const metaTooltip: WsComponentMeta = {\n  icon: TooltipIcon,\n  contentModel: {\n    category: \"instance\",\n    children: [\"instance\"],\n    descendants: [radix.TooltipTrigger, radix.TooltipContent],\n  },\n  initialProps: [\"open\", \"delayDuration\", \"disableHoverableContent\"],\n  props: propsTooltip,\n};\n"
  },
  {
    "path": "packages/sdk-components-react-radix/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src/components.ts\", \"src/metas.ts\", \"src/hooks.ts\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-radix/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"]\n}\n"
  },
  {
    "path": "packages/sdk-components-react-remix/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/sdk-components-react-remix/README.md",
    "content": "# Webstudio SDK Components\n\nWebstudio SDK is a TypeScript API that lets you use your Webstudio project or some components in your custom codebase or just render a complete Remix Document.\n"
  },
  {
    "path": "packages/sdk-components-react-remix/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk-components-react-remix\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio components for Remix\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"type\": \"module\",\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/components.ts\",\n      \"types\": \"./lib/types/components.d.ts\",\n      \"import\": \"./lib/components.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"rm -rf lib && esbuild src/components.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"peerDependencies\": {\n    \"@remix-run/react\": \"^2.9.2\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@remix-run/react\": \"^2.16.5\",\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-remix/src/body.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\nimport { Scripts, ScrollRestoration } from \"@remix-run/react\";\n\nexport const Body = forwardRef<ElementRef<\"body\">, ComponentProps<\"body\">>(\n  ({ children, ...props }, ref) => (\n    <body {...props} ref={ref}>\n      {children}\n      <Scripts />\n      <ScrollRestoration />\n    </body>\n  )\n);\n\nBody.displayName = \"Body\";\n"
  },
  {
    "path": "packages/sdk-components-react-remix/src/components.ts",
    "content": "export { Body } from \"./body\";\nexport { Link } from \"./link\";\nexport { RichTextLink } from \"./rich-text-link\";\n// @todo needs migration, to not break compatibility with existing forms we are mapping temporarily\nexport { WebhookForm as Form } from \"./webhook-form\";\nexport { RemixForm } from \"./remix-form\";\n"
  },
  {
    "path": "packages/sdk-components-react-remix/src/link.tsx",
    "content": "import { forwardRef, type ComponentPropsWithoutRef, useContext } from \"react\";\nimport { NavLink as RemixLink } from \"@remix-run/react\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport { Link as BaseLink } from \"@webstudio-is/sdk-components-react\";\n\ntype Props = Omit<ComponentPropsWithoutRef<typeof BaseLink>, \"target\"> & {\n  // override (string & {}) in target to generate keywords\n  target?: \"_self\" | \"_blank\" | \"_parent\" | \"_top\";\n\n  // useful remix props\n  prefetch?: \"none\" | \"intent\" | \"render\" | \"viewport\";\n  reloadDocument?: boolean;\n  replace?: boolean;\n  preventScrollReset?: boolean;\n};\n\nexport const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {\n  const { assetBaseUrl } = useContext(ReactSdkContext);\n  // cast to string when invalid value type is provided with binding\n  const href = String(props.href ?? \"\");\n\n  // use remix link for self reference and all relative urls\n  // ignore asset paths which can be relative too\n  // urls starting with # should be handled natively to not override search params\n  if (\n    // remix appends ?index in runtime but not in ssr\n    href === \"\" ||\n    href.startsWith(\"?\") ||\n    (href.startsWith(\"/\") && href.startsWith(assetBaseUrl) === false)\n  ) {\n    // In the future, we will switch to the :local-link pseudo-class (https://developer.mozilla.org/en-US/docs/Web/CSS/:local-link). (aria-current=\"page\" is used now)\n    // Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration.\n    return <RemixLink {...props} to={href} ref={ref} end />;\n  }\n\n  const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } =\n    props;\n\n  return <BaseLink {...rest} ref={ref} />;\n});\n\nLink.displayName = BaseLink.displayName;\n"
  },
  {
    "path": "packages/sdk-components-react-remix/src/remix-form.tsx",
    "content": "import { type ElementRef, type ComponentProps, forwardRef } from \"react\";\nimport { Form, type FormProps } from \"@remix-run/react\";\n\nexport const defaultTag = \"form\";\n\nexport const RemixForm = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Omit<ComponentProps<typeof defaultTag>, \"action\"> &\n    Pick<FormProps, \"encType\"> & {\n      // Remix's default behavior includes method values in both uppercase and lowercase,\n      // resulting in our UI displaying a list that encompasses both variants.\n      method?: Lowercase<NonNullable<FormProps[\"method\"]>> | \"dialog\";\n      action?: string;\n    }\n>(({ action, method, ...props }, ref) => {\n  if (method === \"dialog\") {\n    return <form {...props} ref={ref} />;\n  }\n  // remix casts action to relative url\n  if (\n    action === undefined ||\n    action === \"\" ||\n    (typeof action === \"string\" && action?.startsWith(\"/\"))\n  ) {\n    return (\n      <Form\n        action={action}\n        method={method}\n        {...props}\n        ref={ref}\n        // Preserve scroll position for navigation on the same path, as it's used for filtering and sorting\n        preventScrollReset={action === undefined || action === \"\"}\n      />\n    );\n  }\n  return <form {...props} ref={ref} />;\n});\n\nRemixForm.displayName = \"Form\";\n"
  },
  {
    "path": "packages/sdk-components-react-remix/src/rich-text-link.tsx",
    "content": "export { Link as RichTextLink } from \"./link\";\n"
  },
  {
    "path": "packages/sdk-components-react-remix/src/webhook-form.tsx",
    "content": "import {\n  type ElementRef,\n  type ComponentProps,\n  forwardRef,\n  useRef,\n  useEffect,\n} from \"react\";\nimport { useFetcher, type Fetcher, type FormProps } from \"@remix-run/react\";\nimport {\n  formIdFieldName,\n  formBotFieldName,\n  isBraveBrowser,\n} from \"@webstudio-is/sdk/runtime\";\n\nexport const defaultTag = \"form\";\n\nconst useOnFetchEnd = <Data,>(\n  fetcher: Fetcher<Data>,\n  handler: (data: Data) => void\n) => {\n  const latestHandler = useRef(handler);\n  latestHandler.current = handler;\n\n  const prevFetcher = useRef(fetcher);\n  useEffect(() => {\n    if (\n      prevFetcher.current.state !== fetcher.state &&\n      fetcher.state === \"idle\" &&\n      fetcher.data !== undefined\n    ) {\n      latestHandler.current(fetcher.data);\n    }\n    prevFetcher.current = fetcher;\n  }, [fetcher]);\n};\n\ntype State = \"initial\" | \"success\" | \"error\";\n\n// gcd - greatest common divisor\nconst gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));\n\nconst getAspectRatioString = (width: number, height: number) => {\n  const r = gcd(width, height);\n  const aspectRatio = `${width / r}/${height / r}`;\n  return aspectRatio;\n};\n\n/**\n * jsdom detector, trying to check that matchMedia is working (jsdom has no support of matchMedia and usually simple stub is used)\n */\nconst isJSDom = () => {\n  if (typeof matchMedia === \"undefined\") {\n    return true;\n  }\n\n  const { width, height } = screen;\n  const deviceAspectRatio = getAspectRatioString(width, height);\n\n  const matchAspectRatio = matchMedia(\n    `(device-aspect-ratio: ${deviceAspectRatio})`\n  ).matches;\n\n  const matchWidthHeight = matchMedia(\n    `(device-width: ${width}px) and (device-height: ${height}px)`\n  ).matches;\n\n  const matchWidthHeightFail = matchMedia(\n    `(device-width: ${width - 1}px) and (device-height: ${height}px)`\n  ).matches;\n\n  const matchLight = matchMedia(\"(prefers-color-scheme: light)\").matches;\n  const matchDark = matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n  const hasMatchMedia =\n    matchAspectRatio &&\n    matchWidthHeight &&\n    !matchWidthHeightFail &&\n    matchLight !== matchDark;\n\n  return hasMatchMedia === false;\n};\n\nexport const WebhookForm = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Omit<ComponentProps<typeof defaultTag>, \"action\"> & {\n    /** Use this property to reveal the Success and Error states on the canvas so they can be styled. The Initial state is displayed when the page first opens. The Success and Error states are displayed depending on whether the Form submits successfully or unsuccessfully. */\n    state?: State;\n    encType?: FormProps[\"encType\"];\n    onStateChange?: (state: State) => void;\n    action?: string;\n  }\n>(\n  (\n    { children, action, method, state = \"initial\", onStateChange, ...rest },\n    ref\n  ) => {\n    const fetcher = useFetcher<{ success: boolean }>();\n\n    useOnFetchEnd(fetcher, (data) => {\n      const state: State = data?.success === true ? \"success\" : \"error\";\n      onStateChange?.(state);\n    });\n\n    /**\n     * Add hidden field generated using js with simple jsdom detector.\n     * This is used to protect form submission against very simple bots.\n     * Skipped for Brave browser due to: https://github.com/brave/brave-browser/issues/46541\n     */\n    const handleSubmitAndAddHiddenJsField = (\n      event: React.FormEvent<HTMLFormElement>\n    ) => {\n      const hiddenInput = document.createElement(\"input\");\n      hiddenInput.type = \"hidden\";\n      hiddenInput.name = formBotFieldName;\n      // Skip bot detection for Brave - Shields blocks matchMedia fingerprinting detection\n      if (isBraveBrowser()) {\n        hiddenInput.value = \"brave\";\n      } else {\n        // Non-numeric values are utilized for logging purposes.\n        hiddenInput.value = isJSDom() ? \"jsdom\" : Date.now().toString(16);\n      }\n      event.currentTarget.appendChild(hiddenInput);\n    };\n\n    return (\n      <fetcher.Form\n        {...rest}\n        method=\"post\"\n        data-state={state}\n        ref={ref}\n        onSubmit={handleSubmitAndAddHiddenJsField}\n      >\n        <input\n          type=\"hidden\"\n          name={formIdFieldName}\n          value={action?.toString()}\n        />\n        {children}\n      </fetcher.Form>\n    );\n  }\n);\n\nWebhookForm.displayName = \"WebhookForm\";\n"
  },
  {
    "path": "packages/sdk-components-react-remix/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-remix/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"types\": [\"react/canary\", \"@types/node\"]\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-router/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/sdk-components-react-router/README.md",
    "content": "# Webstudio SDK Components\n\nWebstudio SDK is a TypeScript API that lets you use your Webstudio project or some components in your custom codebase or just render a complete Remix Document.\n"
  },
  {
    "path": "packages/sdk-components-react-router/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/sdk-components-react-router\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio components for React Router\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": false,\n  \"type\": \"module\",\n  \"files\": [\n    \"lib/*\",\n    \"!*.{test,stories}.*\"\n  ],\n  \"sideEffects\": false,\n  \"exports\": {\n    \".\": {\n      \"webstudio\": \"./src/components.ts\",\n      \"types\": \"./lib/types/components.d.ts\",\n      \"import\": \"./lib/components.js\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"rm -rf lib && esbuild src/components.ts --outdir=lib --bundle --format=esm --packages=external\",\n    \"dts\": \"tsc --project tsconfig.dts.json\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk-components-react\": \"workspace:*\",\n    \"react-router\": \"^7.5.3\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^18.2.70\",\n    \"@types/react-dom\": \"^18.2.25\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\",\n    \"react-dom\": \"18.3.0-canary-14898b6a9-20240318\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/body.tsx",
    "content": "import { forwardRef, type ElementRef, type ComponentProps } from \"react\";\nimport { Scripts, ScrollRestoration } from \"react-router\";\n\nexport const Body = forwardRef<ElementRef<\"body\">, ComponentProps<\"body\">>(\n  ({ children, ...props }, ref) => (\n    <body {...props} ref={ref}>\n      {children}\n      <Scripts />\n      <ScrollRestoration />\n    </body>\n  )\n);\n\nBody.displayName = \"Body\";\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/components.ts",
    "content": "export { Body } from \"./body\";\nexport { Link } from \"./link\";\nexport { RichTextLink } from \"./rich-text-link\";\n// @todo needs migration, to not break compatibility with existing forms we are mapping temporarily\nexport { WebhookForm as Form } from \"./webhook-form\";\nexport { RemixForm } from \"./remix-form\";\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/link.tsx",
    "content": "import { type ComponentPropsWithoutRef, forwardRef, useContext } from \"react\";\nimport { NavLink as RemixLink } from \"react-router\";\nimport { ReactSdkContext } from \"@webstudio-is/react-sdk/runtime\";\nimport { Link as BaseLink } from \"@webstudio-is/sdk-components-react\";\n\ntype Props = Omit<ComponentPropsWithoutRef<typeof BaseLink>, \"target\"> & {\n  // override (string & {}) in target to generate keywords\n  target?: \"_self\" | \"_blank\" | \"_parent\" | \"_top\";\n\n  // useful remix props\n  prefetch?: \"none\" | \"intent\" | \"render\" | \"viewport\";\n  reloadDocument?: boolean;\n  replace?: boolean;\n  preventScrollReset?: boolean;\n};\n\nexport const Link = forwardRef<HTMLAnchorElement, Props>((props, ref) => {\n  const { assetBaseUrl } = useContext(ReactSdkContext);\n  // cast to string when invalid value type is provided with binding\n  const href = String(props.href ?? \"\");\n\n  // use remix link for self reference and all relative urls\n  // ignore asset paths which can be relative too\n  // urls starting with # should be handled natively to not override search params\n  if (\n    // remix appends ?index in runtime but not in ssr\n    href === \"\" ||\n    href.startsWith(\"?\") ||\n    (href.startsWith(\"/\") && href.startsWith(assetBaseUrl) === false)\n  ) {\n    // In the future, we will switch to the :local-link pseudo-class (https://developer.mozilla.org/en-US/docs/Web/CSS/:local-link). (aria-current=\"page\" is used now)\n    // Therefore, we decided to use end={true} (exact route matching) for all links to facilitate easier migration.\n    return <RemixLink {...props} to={href} ref={ref} end />;\n  }\n\n  const { prefetch, reloadDocument, replace, preventScrollReset, ...rest } =\n    props;\n\n  return <BaseLink {...rest} ref={ref} />;\n});\n\nLink.displayName = BaseLink.displayName;\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/metas.ts",
    "content": "export {\n  Body,\n  Link,\n  RichTextLink,\n  Form,\n  RemixForm,\n} from \"@webstudio-is/sdk-components-react/metas\";\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/remix-form.tsx",
    "content": "import { type ElementRef, type ComponentProps, forwardRef } from \"react\";\nimport { Form, type FormProps } from \"react-router\";\n\nexport const defaultTag = \"form\";\n\nexport const RemixForm = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Omit<ComponentProps<typeof defaultTag>, \"action\"> &\n    Pick<FormProps, \"encType\"> & {\n      // Remix's default behavior includes method values in both uppercase and lowercase,\n      // resulting in our UI displaying a list that encompasses both variants.\n      method?: Lowercase<NonNullable<FormProps[\"method\"]>> | \"dialog\";\n      action?: string;\n    }\n>(({ action, method, ...props }, ref) => {\n  if (method === \"dialog\") {\n    return <form {...props} ref={ref} />;\n  }\n  // remix casts action to relative url\n  if (\n    action === undefined ||\n    action === \"\" ||\n    (typeof action === \"string\" && action?.startsWith(\"/\"))\n  ) {\n    return (\n      <Form\n        action={action}\n        method={method}\n        {...props}\n        ref={ref}\n        // Preserve scroll position for navigation on the same path, as it's used for filtering and sorting\n        preventScrollReset={action === undefined || action === \"\"}\n      />\n    );\n  }\n  return <form {...props} ref={ref} />;\n});\n\nRemixForm.displayName = \"Form\";\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/rich-text-link.tsx",
    "content": "export { Link as RichTextLink } from \"./link\";\n"
  },
  {
    "path": "packages/sdk-components-react-router/src/webhook-form.tsx",
    "content": "import {\n  type ElementRef,\n  type ComponentProps,\n  forwardRef,\n  useRef,\n  useEffect,\n} from \"react\";\nimport { useFetcher, type Fetcher, type FormProps } from \"react-router\";\nimport {\n  formIdFieldName,\n  formBotFieldName,\n  isBraveBrowser,\n} from \"@webstudio-is/sdk/runtime\";\n\nexport const defaultTag = \"form\";\n\nconst useOnFetchEnd = <Data,>(\n  fetcher: Fetcher<Data>,\n  handler: (data: Data) => void\n) => {\n  const latestHandler = useRef(handler);\n  latestHandler.current = handler;\n\n  const prevFetcher = useRef(fetcher);\n  useEffect(() => {\n    if (\n      prevFetcher.current.state !== fetcher.state &&\n      fetcher.state === \"idle\" &&\n      fetcher.data !== undefined\n    ) {\n      latestHandler.current(fetcher.data);\n    }\n    prevFetcher.current = fetcher;\n  }, [fetcher]);\n};\n\ntype State = \"initial\" | \"success\" | \"error\";\n\n// gcd - greatest common divisor\nconst gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));\n\nconst getAspectRatioString = (width: number, height: number) => {\n  const r = gcd(width, height);\n  const aspectRatio = `${width / r}/${height / r}`;\n  return aspectRatio;\n};\n\n/**\n * jsdom detector, trying to check that matchMedia is working (jsdom has no support of matchMedia and usually simple stub is used)\n */\nconst isJSDom = () => {\n  if (typeof matchMedia === \"undefined\") {\n    return true;\n  }\n\n  const { width, height } = screen;\n  const deviceAspectRatio = getAspectRatioString(width, height);\n\n  const matchAspectRatio = matchMedia(\n    `(device-aspect-ratio: ${deviceAspectRatio})`\n  ).matches;\n\n  const matchWidthHeight = matchMedia(\n    `(device-width: ${width}px) and (device-height: ${height}px)`\n  ).matches;\n\n  const matchWidthHeightFail = matchMedia(\n    `(device-width: ${width - 1}px) and (device-height: ${height}px)`\n  ).matches;\n\n  const matchLight = matchMedia(\"(prefers-color-scheme: light)\").matches;\n  const matchDark = matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n  const hasMatchMedia =\n    matchAspectRatio &&\n    matchWidthHeight &&\n    !matchWidthHeightFail &&\n    matchLight !== matchDark;\n\n  return hasMatchMedia === false;\n};\n\nexport const WebhookForm = forwardRef<\n  ElementRef<typeof defaultTag>,\n  Omit<ComponentProps<typeof defaultTag>, \"action\"> & {\n    /** Use this property to reveal the Success and Error states on the canvas so they can be styled. The Initial state is displayed when the page first opens. The Success and Error states are displayed depending on whether the Form submits successfully or unsuccessfully. */\n    state?: State;\n    encType?: FormProps[\"encType\"];\n    onStateChange?: (state: State) => void;\n    action?: string;\n  }\n>(\n  (\n    { children, action, method, state = \"initial\", onStateChange, ...rest },\n    ref\n  ) => {\n    const fetcher = useFetcher<{ success: boolean }>();\n\n    useOnFetchEnd(fetcher, (data) => {\n      const state: State = data?.success === true ? \"success\" : \"error\";\n      onStateChange?.(state);\n    });\n\n    /**\n     * Add hidden field generated using js with simple jsdom detector.\n     * This is used to protect form submission against very simple bots.\n     * Skipped for Brave browser due to: https://github.com/brave/brave-browser/issues/46541\n     */\n    const handleSubmitAndAddHiddenJsField = (\n      event: React.FormEvent<HTMLFormElement>\n    ) => {\n      const hiddenInput = document.createElement(\"input\");\n      hiddenInput.type = \"hidden\";\n      hiddenInput.name = formBotFieldName;\n      // Skip bot detection for Brave - Shields blocks matchMedia fingerprinting detection\n      if (isBraveBrowser()) {\n        hiddenInput.value = \"brave\";\n      } else {\n        // Non-numeric values are utilized for logging purposes.\n        hiddenInput.value = isJSDom() ? \"jsdom\" : Date.now().toString(16);\n      }\n      event.currentTarget.appendChild(hiddenInput);\n    };\n\n    return (\n      <fetcher.Form\n        {...rest}\n        method=\"post\"\n        data-state={state}\n        ref={ref}\n        onSubmit={handleSubmitAndAddHiddenJsField}\n      >\n        <input\n          type=\"hidden\"\n          name={formIdFieldName}\n          value={action?.toString()}\n        />\n        {children}\n      </fetcher.Form>\n    );\n  }\n);\n\nWebhookForm.displayName = \"WebhookForm\";\n"
  },
  {
    "path": "packages/sdk-components-react-router/tsconfig.dts.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"include\": [\"src\"],\n  \"compilerOptions\": {\n    \"declarationDir\": \"lib/types\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk-components-react-router/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"types\": [\"react/canary\", \"@types/node\"]\n  }\n}\n"
  },
  {
    "path": "packages/template/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/template\",\n  \"version\": \"0.0.0-webstudio-version\",\n  \"description\": \"Webstudio templates based on React\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"webstudio\": \"./src/index.ts\"\n  },\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@webstudio-is/css-data\": \"workspace:*\",\n    \"@webstudio-is/css-engine\": \"workspace:*\",\n    \"@webstudio-is/react-sdk\": \"workspace:*\",\n    \"@webstudio-is/sdk\": \"workspace:*\",\n    \"react\": \"18.3.0-canary-14898b6a9-20240318\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"vitest\": \"^3.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/template/src/css.test.ts",
    "content": "import { expect, test } from \"vitest\";\nimport { css } from \"./css\";\n\ntest(\"parse css\", () => {\n  expect(css`\n    color: red;\n  `).toEqual([\n    {\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"support interpolations in local styles\", () => {\n  expect(css`\n    color: ${\"red\"};\n  `).toEqual([\n    {\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n"
  },
  {
    "path": "packages/template/src/css.ts",
    "content": "import { parseCss } from \"@webstudio-is/css-data\";\nimport type { CssProperty, StyleValue } from \"@webstudio-is/css-engine\";\n\nexport type TemplateStyleDecl = {\n  breakpoint?: string;\n  state?: string;\n  property: CssProperty;\n  value: StyleValue;\n};\n\nexport const css = (\n  strings: TemplateStringsArray,\n  ...values: string[]\n): TemplateStyleDecl[] => {\n  const cssString = `.styles{ ${String.raw({ raw: strings }, ...values)} }`;\n  const styles: TemplateStyleDecl[] = [];\n  for (const { breakpoint, state, property, value } of parseCss(cssString)) {\n    styles.push({ breakpoint, state, property: property, value });\n  }\n  return styles;\n};\n"
  },
  {
    "path": "packages/template/src/index.ts",
    "content": "export * from \"./jsx\";\nexport * from \"./css\";\nexport * from \"./template\";\n"
  },
  {
    "path": "packages/template/src/jsx.test.tsx",
    "content": "import { expect, test } from \"vitest\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\nimport {\n  $,\n  ActionValue,\n  AssetValue,\n  expression,\n  PageValue,\n  Parameter,\n  PlaceholderValue,\n  renderTemplate,\n  ResourceValue,\n  token,\n  Variable,\n  ws,\n} from \"./jsx\";\nimport { css } from \"./css\";\n\ntest(\"render jsx into instances with generated id\", () => {\n  const { instances } = renderTemplate(\n    <$.Body>\n      <$.Box></$.Box>\n      <$.Box></$.Box>\n    </$.Body>\n  );\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"0\",\n      component: \"Body\",\n      children: [\n        { type: \"id\", value: \"1\" },\n        { type: \"id\", value: \"2\" },\n      ],\n    },\n    {\n      type: \"instance\",\n      id: \"1\",\n      component: \"Box\",\n      children: [],\n    },\n    {\n      type: \"instance\",\n      id: \"2\",\n      component: \"Box\",\n      children: [],\n    },\n  ]);\n});\n\ntest(\"override generated ids with ws:id prop\", () => {\n  const { instances } = renderTemplate(\n    <$.Body ws:id=\"custom1\">\n      <$.Box ws:id=\"custom2\">\n        <$.Span ws:id=\"custom3\"></$.Span>\n      </$.Box>\n    </$.Body>\n  );\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"custom1\",\n      component: \"Body\",\n      children: [{ type: \"id\", value: \"custom2\" }],\n    },\n    {\n      type: \"instance\",\n      id: \"custom2\",\n      component: \"Box\",\n      children: [{ type: \"id\", value: \"custom3\" }],\n    },\n    {\n      type: \"instance\",\n      id: \"custom3\",\n      component: \"Span\",\n      children: [],\n    },\n  ]);\n});\n\ntest(\"render text children\", () => {\n  const { instances } = renderTemplate(<$.Body>children</$.Body>);\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"0\",\n      component: \"Body\",\n      children: [{ type: \"text\", value: \"children\" }],\n    },\n  ]);\n});\n\ntest(\"render template children with top level instance\", () => {\n  const { children } = renderTemplate(<$.Box></$.Box>);\n  expect(children).toEqual([{ type: \"id\", value: \"0\" }]);\n});\n\ntest(\"render template children with multiple instances from fragment\", () => {\n  const { children, instances } = renderTemplate(\n    <>\n      <$.Box></$.Box>\n      <$.Text></$.Text>\n      <$.Button></$.Button>\n    </>\n  );\n  expect(children).toEqual([\n    { type: \"id\", value: \"0\" },\n    { type: \"id\", value: \"1\" },\n    { type: \"id\", value: \"2\" },\n  ]);\n  expect(instances).toEqual([\n    { type: \"instance\", id: \"0\", component: \"Box\", children: [] },\n    { type: \"instance\", id: \"1\", component: \"Text\", children: [] },\n    { type: \"instance\", id: \"2\", component: \"Button\", children: [] },\n  ]);\n});\n\ntest(\"render literal props\", () => {\n  const { props } = renderTemplate(\n    <$.Body data-string=\"string\" data-number={0}>\n      <$.Box data-bool={true} data-json={{ param: \"value\" }}></$.Box>\n    </$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"0:data-string\",\n      instanceId: \"0\",\n      name: \"data-string\",\n      type: \"string\",\n      value: \"string\",\n    },\n    {\n      id: \"0:data-number\",\n      instanceId: \"0\",\n      name: \"data-number\",\n      type: \"number\",\n      value: 0,\n    },\n    {\n      id: \"1:data-bool\",\n      instanceId: \"1\",\n      name: \"data-bool\",\n      type: \"boolean\",\n      value: true,\n    },\n    {\n      id: \"1:data-json\",\n      instanceId: \"1\",\n      name: \"data-json\",\n      type: \"json\",\n      value: { param: \"value\" },\n    },\n  ]);\n});\n\ntest(\"render defined props\", () => {\n  const { props } = renderTemplate(\n    <$.Body>\n      <$.Box\n        data-asset={new AssetValue(\"assetId\")}\n        data-page={new PageValue(\"pageId\")}\n        data-instance={new PageValue(\"pageId\", \"instanceId\")}\n      ></$.Box>\n    </$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"1:data-asset\",\n      instanceId: \"1\",\n      name: \"data-asset\",\n      type: \"asset\",\n      value: \"assetId\",\n    },\n    {\n      id: \"1:data-page\",\n      instanceId: \"1\",\n      name: \"data-page\",\n      type: \"page\",\n      value: \"pageId\",\n    },\n    {\n      id: \"1:data-instance\",\n      instanceId: \"1\",\n      name: \"data-instance\",\n      type: \"page\",\n      value: { pageId: \"pageId\", instanceId: \"instanceId\" },\n    },\n  ]);\n});\n\ntest(\"render placeholder value\", () => {\n  const { instances } = renderTemplate(\n    <$.Body>{new PlaceholderValue(\"Placeholder text\")}</$.Body>\n  );\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"0\",\n      component: \"Body\",\n      children: [\n        { type: \"text\", value: \"Placeholder text\", placeholder: true },\n      ],\n    },\n  ]);\n});\n\ntest(\"generate local styles\", () => {\n  const { breakpoints, styleSources, styleSourceSelections, styles } =\n    renderTemplate(\n      <$.Body\n        ws:style={css`\n          color: red;\n        `}\n      >\n        <$.Box\n          ws:style={css`\n            font-size: 10px;\n          `}\n        ></$.Box>\n      </$.Body>\n    );\n  expect(breakpoints).toEqual([{ id: \"base\", label: \"\" }]);\n  expect(styleSources).toEqual([\n    { id: \"0:ws:style\", type: \"local\" },\n    { id: \"1:ws:style\", type: \"local\" },\n  ]);\n  expect(styleSourceSelections).toEqual([\n    { instanceId: \"0\", values: [\"0:ws:style\"] },\n    { instanceId: \"1\", values: [\"1:ws:style\"] },\n  ]);\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0:ws:style\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"1:ws:style\",\n      property: \"fontSize\",\n      value: { type: \"unit\", unit: \"px\", value: 10 },\n    },\n  ]);\n});\n\ntest(\"generate local styles with states\", () => {\n  const { styles } = renderTemplate(\n    <$.Body\n      ws:style={css`\n        color: red;\n        &:hover {\n          color: blue;\n        }\n      `}\n    ></$.Body>\n  );\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0:ws:style\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0:ws:style\",\n      state: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"avoid generating style data without styles\", () => {\n  const { breakpoints, styleSources, styleSourceSelections, styles } =\n    renderTemplate(<$.Body></$.Body>);\n  expect(breakpoints).toEqual([]);\n  expect(styleSources).toEqual([]);\n  expect(styleSourceSelections).toEqual([]);\n  expect(styles).toEqual([]);\n});\n\ntest(\"generate token styles\", () => {\n  const { breakpoints, styleSources, styleSourceSelections, styles } =\n    renderTemplate(\n      <$.Body\n        ws:id=\"body\"\n        ws:tokens={[\n          token(\n            \"primary\",\n            css`\n              color: red;\n            `\n          ),\n        ]}\n      ></$.Body>\n    );\n  expect(breakpoints).toEqual([{ id: \"base\", label: \"\" }]);\n  expect(styleSources).toEqual([{ id: \"0\", type: \"token\", name: \"primary\" }]);\n  expect(styleSourceSelections).toEqual([\n    { instanceId: \"body\", values: [\"0\"] },\n  ]);\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"generate multiple tokens on single instance\", () => {\n  const { styleSources, styleSourceSelections, styles } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      ws:tokens={[\n        token(\n          \"primary\",\n          css`\n            color: red;\n          `\n        ),\n        token(\n          \"secondary\",\n          css`\n            font-size: 16px;\n          `\n        ),\n      ]}\n    ></$.Body>\n  );\n  expect(styleSources).toEqual([\n    { id: \"0\", type: \"token\", name: \"primary\" },\n    { id: \"1\", type: \"token\", name: \"secondary\" },\n  ]);\n  expect(styleSourceSelections).toEqual([\n    { instanceId: \"body\", values: [\"0\", \"1\"] },\n  ]);\n  expect(styles).toHaveLength(2);\n});\n\ntest(\"reuse same token across multiple instances\", () => {\n  const primary = token(\n    \"primary\",\n    css`\n      color: red;\n    `\n  );\n  const { styleSources, styleSourceSelections, styles } = renderTemplate(\n    <$.Body ws:id=\"body\" ws:tokens={[primary]}>\n      <$.Box ws:id=\"box\" ws:tokens={[primary]}></$.Box>\n    </$.Body>\n  );\n  // Token should only be created once\n  expect(styleSources).toEqual([{ id: \"0\", type: \"token\", name: \"primary\" }]);\n  // Both instances should reference the same token\n  expect(styleSourceSelections).toEqual([\n    { instanceId: \"body\", values: [\"0\"] },\n    { instanceId: \"box\", values: [\"0\"] },\n  ]);\n  // Styles should only be created once\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n  ]);\n});\n\ntest(\"combine local styles with tokens\", () => {\n  const { styleSources, styleSourceSelections, styles } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      ws:style={css`\n        font-size: 16px;\n      `}\n      ws:tokens={[\n        token(\n          \"primary\",\n          css`\n            color: red;\n          `\n        ),\n      ]}\n    ></$.Body>\n  );\n  // Both local and token style sources\n  expect(styleSources).toEqual([\n    { id: \"body:ws:style\", type: \"local\" },\n    { id: \"0\", type: \"token\", name: \"primary\" },\n  ]);\n  // Selection should have both local style source and token\n  expect(styleSourceSelections).toEqual([\n    { instanceId: \"body\", values: [\"body:ws:style\", \"0\"] },\n  ]);\n  expect(styles).toHaveLength(2);\n});\n\ntest(\"generate token with breakpoints\", () => {\n  const { breakpoints, styles } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      ws:tokens={[\n        token(\n          \"responsive\",\n          css`\n            color: red;\n            @media (min-width: 1024px) {\n              color: blue;\n            }\n          `\n        ),\n      ]}\n    ></$.Body>\n  );\n  expect(breakpoints).toEqual([\n    { id: \"base\", label: \"\" },\n    { id: \"0\", label: \"1024\", minWidth: 1024 },\n  ]);\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpointId: \"0\",\n      styleSourceId: \"0\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"generate token with state\", () => {\n  const { styles } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      ws:tokens={[\n        token(\n          \"interactive\",\n          css`\n            color: red;\n            &:hover {\n              color: blue;\n            }\n          `\n        ),\n      ]}\n    ></$.Body>\n  );\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0\",\n      state: \":hover\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"generate breakpoints\", () => {\n  const { breakpoints, styleSources, styleSourceSelections, styles } =\n    renderTemplate(\n      <$.Body\n        ws:style={css`\n          color: red;\n          @media (min-width: 1024px) {\n            color: blue;\n          }\n        `}\n      ></$.Body>\n    );\n  expect(breakpoints).toEqual([\n    { id: \"base\", label: \"\" },\n    { id: \"0\", label: \"1024\", minWidth: 1024 },\n  ]);\n  expect(styleSources).toEqual([{ id: \"0:ws:style\", type: \"local\" }]);\n  expect(styleSourceSelections).toEqual([\n    { instanceId: \"0\", values: [\"0:ws:style\"] },\n  ]);\n  expect(styles).toEqual([\n    {\n      breakpointId: \"base\",\n      styleSourceId: \"0:ws:style\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"red\" },\n    },\n    {\n      breakpointId: \"0\",\n      styleSourceId: \"0:ws:style\",\n      property: \"color\",\n      value: { type: \"keyword\", value: \"blue\" },\n    },\n  ]);\n});\n\ntest(\"render variable used in prop expression\", () => {\n  const count = new Variable(\"count\", 1);\n  const { props, dataSources } = renderTemplate(\n    <$.Body ws:id=\"body\" data-count={expression`${count}`}></$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:data-count\",\n      instanceId: \"body\",\n      name: \"data-count\",\n      type: \"expression\",\n      value: \"$ws$dataSource$0\",\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      type: \"variable\",\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"count\",\n      value: { type: \"number\", value: 1 },\n    },\n  ]);\n});\n\ntest(\"render variable used in child expression\", () => {\n  const count = new Variable(\"count\", 1);\n  const { instances, dataSources } = renderTemplate(\n    <$.Body ws:id=\"body\">{expression`${count}`}</$.Body>\n  );\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"body\",\n      component: \"Body\",\n      children: [{ type: \"expression\", value: \"$ws$dataSource$0\" }],\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      type: \"variable\",\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"count\",\n      value: { type: \"number\", value: 1 },\n    },\n  ]);\n});\n\ntest(\"compose expression from multiple variables\", () => {\n  const count = new Variable(\"count\", 1);\n  const step = new Variable(\"step\", 2);\n  const { props, dataSources } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      data-count={expression`Count is ${count} + ${step}`}\n    ></$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:data-count\",\n      instanceId: \"body\",\n      name: \"data-count\",\n      type: \"expression\",\n      value: \"Count is $ws$dataSource$0 + $ws$dataSource$1\",\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      type: \"variable\",\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"count\",\n      value: { type: \"number\", value: 1 },\n    },\n    {\n      type: \"variable\",\n      id: \"1\",\n      scopeInstanceId: \"body\",\n      name: \"step\",\n      value: { type: \"number\", value: 2 },\n    },\n  ]);\n});\n\ntest(\"preserve same variable on multiple instances\", () => {\n  const count = new Variable(\"count\", 1);\n  const { props, dataSources } = renderTemplate(\n    <$.Body ws:id=\"body\" data-count={expression`${count}`}>\n      <$.Box ws:id=\"box\" data-count={expression`${count}`}></$.Box>\n    </$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:data-count\",\n      instanceId: \"body\",\n      name: \"data-count\",\n      type: \"expression\",\n      value: \"$ws$dataSource$0\",\n    },\n    {\n      id: \"box:data-count\",\n      instanceId: \"box\",\n      name: \"data-count\",\n      type: \"expression\",\n      value: \"$ws$dataSource$0\",\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      type: \"variable\",\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"count\",\n      value: { type: \"number\", value: 1 },\n    },\n  ]);\n});\n\ntest(\"render variable inside of action\", () => {\n  const count = new Variable(\"count\", 1);\n  const { props, dataSources } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      data-count={expression`${count}`}\n      onInc={new ActionValue([\"step\"], expression`${count} = ${count} + step`)}\n    ></$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:data-count\",\n      instanceId: \"body\",\n      name: \"data-count\",\n      type: \"expression\",\n      value: \"$ws$dataSource$0\",\n    },\n    {\n      id: \"body:onInc\",\n      instanceId: \"body\",\n      name: \"onInc\",\n      type: \"action\",\n      value: [\n        {\n          type: \"execute\",\n          args: [\"step\"],\n          code: \"$ws$dataSource$0 = $ws$dataSource$0 + step\",\n        },\n      ],\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      type: \"variable\",\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"count\",\n      value: { type: \"number\", value: 1 },\n    },\n  ]);\n});\n\ntest(\"render parameter bound to prop expression\", () => {\n  const system = new Parameter(\"system\");\n  const { props, dataSources } = renderTemplate(\n    <$.Body\n      ws:id=\"body\"\n      data-param={system}\n      data-value={expression`${system}`}\n    ></$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:data-param\",\n      instanceId: \"body\",\n      name: \"data-param\",\n      type: \"parameter\",\n      value: \"0\",\n    },\n    {\n      id: \"body:data-value\",\n      instanceId: \"body\",\n      name: \"data-value\",\n      type: \"expression\",\n      value: \"$ws$dataSource$0\",\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      type: \"parameter\",\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"system\",\n    },\n  ]);\n});\n\ntest(\"render resource variable\", () => {\n  const value = new Variable(\"value\", \"value\");\n  const myResource = new ResourceValue(\"myResource\", {\n    url: expression`\"https://my-url.com/\" + ${value}`,\n    method: \"get\",\n    searchParams: [{ name: \"filter\", value: expression`${value}` }],\n    headers: [{ name: \"auth\", value: expression`${value}` }],\n    body: expression`${value}`,\n  });\n  const { dataSources, resources } = renderTemplate(\n    <$.Body ws:id=\"body\">{expression`${myResource}.title`}</$.Body>\n  );\n  expect(dataSources).toEqual([\n    {\n      id: \"1\",\n      name: \"value\",\n      scopeInstanceId: \"body\",\n      type: \"variable\",\n      value: { type: \"string\", value: \"value\" },\n    },\n    {\n      id: \"0\",\n      scopeInstanceId: \"body\",\n      name: \"myResource\",\n      type: \"resource\",\n      resourceId: \"resource:0\",\n    },\n  ]);\n  expect(resources).toEqual([\n    {\n      id: \"resource:0\",\n      name: \"myResource\",\n      url: `\"https://my-url.com/\" + $ws$dataSource$1`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: `$ws$dataSource$1` }],\n      headers: [{ name: \"auth\", value: `$ws$dataSource$1` }],\n      body: `$ws$dataSource$1`,\n    },\n  ]);\n});\n\ntest(\"render resource prop\", () => {\n  const value = new Variable(\"value\", \"value\");\n  const myResource = new ResourceValue(\"myResource\", {\n    url: expression`\"https://my-url.com/\" + ${value}`,\n    method: \"get\",\n    searchParams: [{ name: \"filter\", value: expression`${value}` }],\n    headers: [{ name: \"auth\", value: expression`${value}` }],\n    body: expression`${value}`,\n  });\n  const { props, dataSources, resources } = renderTemplate(\n    <$.Body ws:id=\"body\" action={myResource}></$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:action\",\n      instanceId: \"body\",\n      name: \"action\",\n      type: \"resource\",\n      value: \"resource:0\",\n    },\n  ]);\n  expect(dataSources).toEqual([\n    {\n      id: \"1\",\n      name: \"value\",\n      scopeInstanceId: \"body\",\n      type: \"variable\",\n      value: { type: \"string\", value: \"value\" },\n    },\n  ]);\n  expect(resources).toEqual([\n    {\n      id: \"resource:0\",\n      name: \"myResource\",\n      url: `\"https://my-url.com/\" + $ws$dataSource$1`,\n      method: \"get\",\n      searchParams: [{ name: \"filter\", value: `$ws$dataSource$1` }],\n      headers: [{ name: \"auth\", value: `$ws$dataSource$1` }],\n      body: `$ws$dataSource$1`,\n    },\n  ]);\n});\n\ntest(\"render ws:show attribute\", () => {\n  const { props } = renderTemplate(\n    <$.Body ws:id=\"body\" ws:show={true}></$.Body>\n  );\n  expect(props).toEqual([\n    {\n      id: \"body:data-ws-show\",\n      instanceId: \"body\",\n      name: showAttribute,\n      type: \"boolean\",\n      value: true,\n    },\n  ]);\n});\n\ntest(\"render ws:tag property\", () => {\n  const { instances, props } = renderTemplate(\n    <$.Body ws:id=\"body\">\n      <$.Box ws:tag=\"span\"></$.Box>\n    </$.Body>\n  );\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"body\",\n      component: \"Body\",\n      children: [{ type: \"id\", value: \"0\" }],\n    },\n    {\n      type: \"instance\",\n      id: \"0\",\n      component: \"Box\",\n      tag: \"span\",\n      children: [],\n    },\n  ]);\n  expect(props).toEqual([]);\n});\n\ntest(\"render ws:element with ws:tag prop\", () => {\n  const { instances, props } = renderTemplate(\n    <$.Body ws:id=\"body\">\n      <ws.element ws:tag=\"span\"></ws.element>\n    </$.Body>\n  );\n  expect(instances).toEqual([\n    {\n      type: \"instance\",\n      id: \"body\",\n      component: \"Body\",\n      children: [{ type: \"id\", value: \"0\" }],\n    },\n    {\n      type: \"instance\",\n      id: \"0\",\n      component: \"ws:element\",\n      tag: \"span\",\n      children: [],\n    },\n  ]);\n  expect(props).toEqual([]);\n});\n"
  },
  {
    "path": "packages/template/src/jsx.ts",
    "content": "import { Fragment, type JSX, type ReactNode } from \"react\";\nimport { encodeDataSourceVariable, getStyleDeclKey } from \"@webstudio-is/sdk\";\nimport type {\n  Breakpoint,\n  DataSource,\n  Instance,\n  Prop,\n  Resource,\n  StyleDecl,\n  StyleSource,\n  StyleSourceSelection,\n  WebstudioData,\n  WebstudioFragment,\n} from \"@webstudio-is/sdk\";\nimport { showAttribute } from \"@webstudio-is/react-sdk\";\nimport type { TemplateStyleDecl } from \"./css\";\nimport { camelCaseProperty, parseMediaQuery } from \"@webstudio-is/css-data\";\n\nexport class Token {\n  name: string;\n  styles: TemplateStyleDecl[];\n  constructor(name: string, styles: TemplateStyleDecl[]) {\n    this.name = name;\n    this.styles = styles;\n  }\n}\n\nexport const token = (name: string, styles: TemplateStyleDecl[]): Token => {\n  return new Token(name, styles);\n};\n\nexport class Variable {\n  name: string;\n  initialValue: unknown;\n  constructor(name: string, initialValue: unknown) {\n    this.name = name;\n    this.initialValue = initialValue;\n  }\n}\n\nexport class Parameter {\n  name: string;\n  constructor(name: string) {\n    this.name = name;\n  }\n}\n\ntype ResourceConfig = {\n  url: Expression;\n  method: Resource[\"method\"];\n  searchParams: Array<{ name: string; value: Expression }>;\n  headers: Array<{ name: string; value: Expression }>;\n  body?: Expression;\n};\n\nexport class ResourceValue {\n  name: string;\n  config: ResourceConfig;\n  constructor(name: string, config: ResourceConfig) {\n    this.name = name;\n    this.config = config;\n  }\n}\n\nclass Expression {\n  chunks: string[];\n  variables: Array<Variable | Parameter | ResourceValue>;\n  constructor(\n    chunks: string[],\n    variables: Array<Variable | Parameter | ResourceValue>\n  ) {\n    this.chunks = chunks;\n    this.variables = variables;\n  }\n}\n\nexport const expression = (\n  chunks: TemplateStringsArray,\n  ...variables: Array<Variable | Parameter>\n): Expression => {\n  return new Expression(Array.from(chunks), variables);\n};\n\nexport class ActionValue {\n  args: string[];\n  expression: Expression;\n  constructor(args: string[], code: string | Expression) {\n    this.args = args;\n    if (typeof code === \"string\") {\n      this.expression = new Expression([code], []);\n    } else {\n      this.expression = code;\n    }\n  }\n}\n\nexport class AssetValue {\n  value: string;\n  constructor(assetId: string) {\n    this.value = assetId;\n  }\n}\n\nexport class PageValue {\n  value: string | { pageId: string; instanceId: string };\n  constructor(pageId: string, instanceId?: string) {\n    if (instanceId) {\n      this.value = { pageId, instanceId };\n    } else {\n      this.value = pageId;\n    }\n  }\n}\n\nexport class PlaceholderValue {\n  value: string;\n  constructor(text: string) {\n    this.value = text;\n  }\n}\n\nconst isChildValue = (child: unknown) =>\n  typeof child === \"string\" ||\n  child instanceof PlaceholderValue ||\n  child instanceof Expression;\n\nconst getElementChildren = (element: JSX.Element): JSX.Element[] => {\n  if (Array.isArray(element.props?.children)) {\n    return element.props?.children;\n  }\n  if (element.props?.children) {\n    return [element.props?.children];\n  }\n  return [];\n};\n\nexport const renderTemplate = (\n  root: JSX.Element,\n  generateId?: () => string,\n  initialBreakpoints: Breakpoint[] = []\n): WebstudioFragment => {\n  const instances: Instance[] = [];\n  const props: Prop[] = [];\n  const breakpoints = Array.from(initialBreakpoints);\n  const styleSources: StyleSource[] = [];\n  const styleSourceSelections: StyleSourceSelection[] = [];\n  const styles: StyleDecl[] = [];\n  const dataSources = new Map<Variable | Parameter, DataSource>();\n  const resources = new Map<ResourceValue, Resource>();\n  const idsByKey = new Map<unknown, string>();\n  const lastIdsByList = new Map<unknown, number>();\n  // ensure ids are stable for specific list\n  const getIdForList = (list: unknown) => {\n    if (generateId) {\n      return generateId();\n    }\n    let lastId = lastIdsByList.get(list) ?? -1;\n    lastId += 1;\n    lastIdsByList.set(list, lastId);\n    return lastId.toString();\n  };\n  const getIdByKey = (key: unknown) => {\n    let id = idsByKey.get(key);\n    if (id === undefined) {\n      id = getIdForList(idsByKey);\n      idsByKey.set(key, id);\n    }\n    return id;\n  };\n  const getVariableId = (\n    instanceId: string,\n    variable: Variable | Parameter\n  ) => {\n    const id = getIdByKey(variable);\n    if (dataSources.has(variable)) {\n      return id;\n    }\n    if (variable instanceof Variable) {\n      let value: Extract<DataSource, { type: \"variable\" }>[\"value\"];\n      if (typeof variable.initialValue === \"string\") {\n        value = { type: \"string\", value: variable.initialValue };\n      } else if (typeof variable.initialValue === \"number\") {\n        value = { type: \"number\", value: variable.initialValue };\n      } else if (typeof variable.initialValue === \"boolean\") {\n        value = { type: \"boolean\", value: variable.initialValue };\n      } else {\n        value = { type: \"json\", value: variable.initialValue };\n      }\n      dataSources.set(variable, {\n        type: \"variable\",\n        scopeInstanceId: instanceId,\n        id,\n        name: variable.name,\n        value,\n      });\n    }\n    if (variable instanceof Parameter) {\n      dataSources.set(variable, {\n        type: \"parameter\",\n        scopeInstanceId: instanceId,\n        id,\n        name: variable.name,\n      });\n    }\n    if (variable instanceof ResourceValue) {\n      dataSources.set(variable, {\n        type: \"resource\",\n        scopeInstanceId: instanceId,\n        id,\n        name: variable.name,\n        resourceId: getResourceId(instanceId, variable),\n      });\n    }\n    return id;\n  };\n  const compileExpression = (instanceId: string, expression: Expression) => {\n    const values = expression.variables.map((variable) =>\n      getVariableId(instanceId, variable)\n    );\n    return String.raw(\n      { raw: expression.chunks },\n      ...values.map(encodeDataSourceVariable)\n    );\n  };\n  const getResourceId = (instanceId: string, resourceValue: ResourceValue) => {\n    const id = `resource:${getIdByKey(resourceValue)}`;\n    if (resources.has(resourceValue)) {\n      return id;\n    }\n    resources.set(resourceValue, {\n      id,\n      name: resourceValue.name,\n      url: compileExpression(instanceId, resourceValue.config.url),\n      method: resourceValue.config.method,\n      searchParams: resourceValue.config.searchParams.map(\n        ({ name, value }) => ({\n          name,\n          value: compileExpression(instanceId, value),\n        })\n      ),\n      headers: resourceValue.config.headers.map(({ name, value }) => ({\n        name,\n        value: compileExpression(instanceId, value),\n      })),\n      body: resourceValue.config.body\n        ? compileExpression(instanceId, resourceValue.config.body)\n        : undefined,\n    });\n    return id;\n  };\n  // lazily create breakpoint\n  const getBreakpointId = (mediaQuery: undefined | string) => {\n    if (mediaQuery === undefined) {\n      let baseBreakpoint = breakpoints.find(\n        (item) => item.minWidth === undefined && item.maxWidth === undefined\n      );\n      if (baseBreakpoint === undefined) {\n        baseBreakpoint = { id: \"base\", label: \"\" };\n        breakpoints.push(baseBreakpoint);\n      }\n      return baseBreakpoint.id;\n    }\n    const parsedMediaQuery = parseMediaQuery(mediaQuery);\n    if (parsedMediaQuery === undefined) {\n      return;\n    }\n    let breakpoint = breakpoints.find(\n      (item) =>\n        item.minWidth === parsedMediaQuery.minWidth &&\n        item.maxWidth === parsedMediaQuery.maxWidth\n    );\n    if (breakpoint === undefined) {\n      const id = getIdForList(breakpoints);\n      const label = `${parsedMediaQuery.minWidth ?? parsedMediaQuery.maxWidth}`;\n      breakpoint = { id, label, ...parsedMediaQuery };\n      breakpoints.push(breakpoint);\n    }\n    return breakpoint.id;\n  };\n  const localStylesByInstanceId = new Map<\n    Instance[\"id\"],\n    TemplateStyleDecl[]\n  >();\n  const tokensByInstanceId = new Map<Instance[\"id\"], Token[]>();\n  const tokenIdByToken = new Map<Token, string>();\n  const getTokenId = (token: Token) => {\n    let id = tokenIdByToken.get(token);\n    if (id === undefined) {\n      id = getIdByKey(token);\n      tokenIdByToken.set(token, id);\n    }\n    return id;\n  };\n  const convertElementToInstance = (\n    element: JSX.Element\n  ): Instance[\"children\"][number] => {\n    const instanceId = element.props?.[\"ws:id\"] ?? getIdByKey(element);\n    let tag: string | undefined;\n    for (const entry of Object.entries({ ...element.props })) {\n      const [_name, value] = entry;\n      let [name] = entry;\n      if (name === \"ws:id\" || name === \"ws:label\" || name === \"children\") {\n        continue;\n      }\n      if (name === \"ws:tag\") {\n        tag = value as string;\n        continue;\n      }\n      if (name === \"ws:style\") {\n        const localStyles = value as TemplateStyleDecl[];\n        // create styles with breakpoints later to ensure more stable ids\n        localStylesByInstanceId.set(instanceId, localStyles);\n        continue;\n      }\n      if (name === \"ws:tokens\") {\n        const tokens = value as Token[];\n        // create tokens with breakpoints later to ensure more stable ids\n        tokensByInstanceId.set(instanceId, tokens);\n        continue;\n      }\n      if (name === \"ws:show\") {\n        name = showAttribute;\n      }\n      const propId = `${instanceId}:${name}`;\n      const base = { id: propId, instanceId, name };\n      if (value instanceof Expression) {\n        props.push({\n          ...base,\n          type: \"expression\",\n          value: compileExpression(instanceId, value),\n        });\n        continue;\n      }\n      if (value instanceof Parameter) {\n        props.push({\n          ...base,\n          type: \"parameter\",\n          value: getVariableId(instanceId, value),\n        });\n        continue;\n      }\n      if (value instanceof ResourceValue) {\n        const resourceId = getResourceId(instanceId, value);\n        props.push({ ...base, type: \"resource\", value: resourceId });\n        continue;\n      }\n      if (value instanceof ActionValue) {\n        const code = compileExpression(instanceId, value.expression);\n        const action = { type: \"execute\" as const, args: value.args, code };\n        props.push({ ...base, type: \"action\", value: [action] });\n        continue;\n      }\n      if (value instanceof AssetValue) {\n        props.push({ ...base, type: \"asset\", value: value.value });\n        continue;\n      }\n      if (value instanceof PageValue) {\n        props.push({ ...base, type: \"page\", value: value.value });\n        continue;\n      }\n      if (typeof value === \"string\") {\n        props.push({ ...base, type: \"string\", value });\n        continue;\n      }\n      if (typeof value === \"number\") {\n        props.push({ ...base, type: \"number\", value });\n        continue;\n      }\n      if (typeof value === \"boolean\") {\n        props.push({ ...base, type: \"boolean\", value });\n        continue;\n      }\n      props.push({ ...base, type: \"json\", value });\n    }\n    const component = element.type.displayName;\n    const instance: Instance = {\n      type: \"instance\",\n      id: instanceId,\n      component,\n      children: [],\n    };\n    instances.push(instance);\n    if (element.props?.[\"ws:label\"]) {\n      instance.label = element.props?.[\"ws:label\"];\n    }\n    if (tag) {\n      instance.tag = tag;\n    }\n    instance.children = getElementChildren(element).map(\n      (child): Instance[\"children\"][number] => {\n        if (typeof child === \"string\") {\n          return { type: \"text\", value: child };\n        }\n        if (child instanceof PlaceholderValue) {\n          return { type: \"text\", value: child.value, placeholder: true };\n        }\n        if (child instanceof Expression) {\n          const expression = compileExpression(instanceId, child);\n          return { type: \"expression\", value: expression };\n        }\n        return convertElementToInstance(child);\n      }\n    );\n    return { type: \"id\", value: instance.id };\n  };\n  const children: Instance[\"children\"] = [];\n  if (root.type === Fragment) {\n    for (const child of getElementChildren(root)) {\n      if (isChildValue(child)) {\n        continue;\n      }\n      children.push(convertElementToInstance(child));\n    }\n  } else {\n    children.push(convertElementToInstance(root));\n  }\n  for (const [instanceId, localStyles] of localStylesByInstanceId) {\n    const styleSourceId = `${instanceId}:ws:style`;\n    styleSources.push({\n      type: \"local\",\n      id: styleSourceId,\n    });\n    styleSourceSelections.push({\n      instanceId,\n      values: [styleSourceId],\n    });\n    for (const { breakpoint, state, property, value } of localStyles) {\n      const breakpointId = getBreakpointId(breakpoint);\n      if (breakpointId === undefined) {\n        continue;\n      }\n      styles.push({\n        breakpointId,\n        styleSourceId,\n        state,\n        property: camelCaseProperty(property),\n        value,\n      });\n    }\n  }\n  // process tokens and add them to style sources\n  const processedTokens = new Set<Token>();\n  for (const [instanceId, tokens] of tokensByInstanceId) {\n    const tokenIds: string[] = [];\n    for (const token of tokens) {\n      const tokenId = getTokenId(token);\n      tokenIds.push(tokenId);\n      // only create style source and styles once per token\n      if (processedTokens.has(token)) {\n        continue;\n      }\n      processedTokens.add(token);\n      styleSources.push({\n        type: \"token\",\n        id: tokenId,\n        name: token.name,\n      });\n      for (const { breakpoint, state, property, value } of token.styles) {\n        const breakpointId = getBreakpointId(breakpoint);\n        if (breakpointId === undefined) {\n          continue;\n        }\n        styles.push({\n          breakpointId,\n          styleSourceId: tokenId,\n          state,\n          property: camelCaseProperty(property),\n          value,\n        });\n      }\n    }\n    // merge tokens with existing selection (from ws:style) or create new selection\n    const existingSelection = styleSourceSelections.find(\n      (sel) => sel.instanceId === instanceId\n    );\n    if (existingSelection) {\n      existingSelection.values.push(...tokenIds);\n    } else {\n      styleSourceSelections.push({\n        instanceId,\n        values: tokenIds,\n      });\n    }\n  }\n  return {\n    children,\n    instances,\n    props,\n    breakpoints,\n    styleSources,\n    styleSourceSelections,\n    styles,\n    dataSources: Array.from(dataSources.values()),\n    resources: Array.from(resources.values()),\n    assets: [],\n  };\n};\n\nexport const renderData = (\n  root: JSX.Element,\n  generateId?: () => string,\n  initialBreakpoints: Breakpoint[] = []\n): Omit<WebstudioData, \"pages\"> => {\n  const {\n    instances,\n    props,\n    breakpoints,\n    styleSources,\n    styleSourceSelections,\n    styles,\n    dataSources,\n    resources,\n    assets,\n  } = renderTemplate(root, generateId, initialBreakpoints);\n  return {\n    instances: new Map(instances.map((item) => [item.id, item])),\n    props: new Map(props.map((item) => [item.id, item])),\n    breakpoints: new Map(breakpoints.map((item) => [item.id, item])),\n    styleSources: new Map(styleSources.map((item) => [item.id, item])),\n    styleSourceSelections: new Map(\n      styleSourceSelections.map((item) => [item.instanceId, item])\n    ),\n    styles: new Map(styles.map((item) => [getStyleDeclKey(item), item])),\n    dataSources: new Map(dataSources.map((item) => [item.id, item])),\n    resources: new Map(resources.map((item) => [item.id, item])),\n    assets: new Map(assets.map((item) => [item.id, item])),\n  };\n};\n\ntype ComponentProps = Record<string, unknown> &\n  Record<`${string}:expression`, string> & {\n    \"ws:id\"?: string;\n    \"ws:label\"?: string;\n    \"ws:tag\"?: string;\n    \"ws:style\"?: TemplateStyleDecl[];\n    \"ws:show\"?: boolean | Expression;\n    children?: ReactNode | Expression | PlaceholderValue;\n  };\n\ntype Component = { displayName: string } & ((\n  props: ComponentProps\n) => ReactNode);\n\nexport const createProxy = (prefix: string): Record<string, Component> => {\n  return new Proxy(\n    {},\n    {\n      get(_target, prop) {\n        const component: Component = () => undefined;\n        component.displayName = `${prefix}${prop as string}`;\n        return component;\n      },\n    }\n  );\n};\n\nexport const $: Record<string, Component> = createProxy(\"\");\n\nexport const ws: Record<string, Component> = createProxy(\"ws:\");\n"
  },
  {
    "path": "packages/template/src/template.ts",
    "content": "import type { JSX } from \"react\";\nimport type { WebstudioFragment } from \"@webstudio-is/sdk\";\n\nexport const templateCategories = [\n  \"general\",\n  \"typography\",\n  \"media\",\n  \"animations\",\n  \"data\",\n  \"forms\",\n  \"localization\",\n  \"radix\",\n  \"xml\",\n  \"other\",\n  \"hidden\",\n  \"internal\",\n] as const;\n\nexport type TemplateMeta = {\n  category: (typeof templateCategories)[number];\n  label?: string;\n  description?: string;\n  icon?: string;\n  order?: number;\n  template: JSX.Element;\n};\n\nexport type GeneratedTemplateMeta = Omit<TemplateMeta, \"template\"> & {\n  template: WebstudioFragment;\n};\n"
  },
  {
    "path": "packages/template/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\",\n  \"include\": [\"**/*.ts\", \"**/*.tsx\", \"../../@types/**/*.d.ts\"],\n  \"compilerOptions\": {\n    \"isolatedDeclarations\": true\n  }\n}\n"
  },
  {
    "path": "packages/trpc-interface/LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>."
  },
  {
    "path": "packages/trpc-interface/README.md",
    "content": "# trpc-interface\n\n- `trpc-interface` can implement various TRPC server routers.\n\n- it can act as a proxy for TRPC services implemented in any other apps (those services must have the same API interface).\n"
  },
  {
    "path": "packages/trpc-interface/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/trpc-interface\",\n  \"version\": \"0.0.0\",\n  \"description\": \"Webstudio TRPC Interface\",\n  \"author\": \"Webstudio <github@webstudio.is>\",\n  \"homepage\": \"https://webstudio.is\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsgo --noEmit\",\n    \"test\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@trpc/client\": \"^10.45.2\",\n    \"@trpc/server\": \"^10.45.2\",\n    \"memoize\": \"^10.0.0\",\n    \"ts-custom-error\": \"^3.3.1\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@webstudio-is/postgrest\": \"workspace:*\",\n    \"@webstudio-is/tsconfig\": \"workspace:*\",\n    \"vitest\": \"^3.1.2\"\n  },\n  \"exports\": {\n    \"./index.server\": {\n      \"webstudio\": \"./src/index.server.ts\"\n    }\n  },\n  \"license\": \"AGPL-3.0-or-later\",\n  \"private\": true,\n  \"sideEffects\": false\n}\n"
  },
  {
    "path": "packages/trpc-interface/src/authorize/project.server.ts",
    "content": "import type { AppContext } from \"../context/context.server\";\nimport type { Database } from \"@webstudio-is/postgrest/index.server\";\nimport memoize from \"memoize\";\n\ntype Relation =\n  Database[\"public\"][\"Tables\"][\"AuthorizationToken\"][\"Row\"][\"relation\"];\n\nexport type AuthPermit = \"view\" | \"edit\" | \"build\" | \"admin\" | \"own\";\n\ntype TokenAuthPermit = Exclude<AuthPermit, \"own\">;\n\ntype CheckInput = {\n  namespace: \"Project\";\n  id: string;\n\n  permit: AuthPermit;\n\n  subjectSet: {\n    namespace: \"User\" | \"Token\";\n    id: string;\n  };\n};\n\nconst check = async (\n  postgrestClient: AppContext[\"postgrest\"][\"client\"],\n  input: CheckInput\n) => {\n  const { subjectSet } = input;\n\n  if (subjectSet.namespace === \"User\") {\n    // We check only if the user is the owner of the project\n    const row = await postgrestClient\n      .from(\"Project\")\n      .select(\"id\")\n      .eq(\"id\", input.id)\n      .eq(\"userId\", subjectSet.id)\n      .maybeSingle();\n    if (row.error) {\n      throw row.error;\n    }\n\n    return { allowed: row.data !== null };\n  }\n\n  if (input.permit === \"own\") {\n    return { allowed: false };\n  }\n\n  if (subjectSet.namespace !== \"Token\") {\n    return { allowed: false };\n  }\n\n  const permitToRelationRewrite: Record<TokenAuthPermit, Relation[]> = {\n    view: [\"viewers\", \"editors\", \"builders\", \"administrators\"],\n    edit: [\"editors\", \"builders\", \"administrators\"],\n    build: [\"builders\", \"administrators\"],\n    admin: [\"administrators\"],\n  };\n\n  const row = await postgrestClient\n    .from(\"AuthorizationToken\")\n    .select(\"token\")\n    .eq(\"token\", subjectSet.id)\n    .in(\"relation\", [...permitToRelationRewrite[input.permit]])\n    .maybeSingle();\n\n  if (row.error) {\n    throw row.error;\n  }\n\n  return { allowed: row.data !== null };\n};\n\n// doesn't work in cloudflare workers\nconst memoizedCheck = memoize(check, {\n  // 1 minute\n  maxAge: 60 * 1000,\n  cacheKey: ([_context, input]) => JSON.stringify(input),\n});\n\ntype AuthInfo =\n  | {\n      type: \"user\";\n      userId: string;\n    }\n  | {\n      type: \"token\";\n      authToken: string;\n    }\n  | {\n      type: \"service\";\n    };\n\nexport const checkProjectPermit = async (\n  projectId: string,\n  permit: AuthPermit,\n  authInfo: AuthInfo,\n  postgrestClient: AppContext[\"postgrest\"][\"client\"]\n) => {\n  const checks = [];\n  const namespace = \"Project\";\n\n  if (authInfo.type === \"service\") {\n    return permit === \"view\";\n  }\n\n  // @todo Delete and use tokens\n  const templateIds = [\n    // Production\n    \"5e086cf4-4293-471c-8eab-ddca8b5cd4db\",\n    \"94e6e1b8-c6c4-485a-9d7a-8282e11920c0\",\n    \"05954204-fcee-407e-b47f-77a38de74431\",\n    \"afc162c2-6396-41b7-a855-8fc04604a7b1\",\n    \"3f260731-825b-486a-b534-e747f0ed6106\",\n    \"400b1bde-def1-49e0-9b64-e26416d326fa\",\n    \"2e802ad7-ef32-48e6-8706-3a162785ef95\",\n    \"01f6f1d8-06f5-4a6c-a3b1-89a0448046c7\",\n    \"5b33acf4-53cf-4f03-8973-d5679772edee\",\n    \"909a139b-1f2d-415a-ac90-382fa19fa7d8\",\n    \"ef82ee51-e4d6-4a69-a4cc-7bf1dee65ed7\",\n    \"e761178f-6ac6-47f6-b881-56cc75640d73\",\n    // Staging IDs\n    \"c236999d-be6b-43fb-9edc-78a2ba59e56d\",\n    \"a1371dce-752c-4ccf-8ea4-88bab577fe50\",\n    \"6204396c-3f9e-4d29-8d19-ff0f76960a74\",\n  ];\n\n  // @todo Delete and use tokens\n  if (permit === \"view\" && templateIds.includes(projectId)) {\n    return true;\n  }\n\n  if (authInfo.type === \"token\") {\n    // Token doesn't have \"own\" permit, do not check it\n    if (permit === \"own\") {\n      return false;\n    }\n\n    checks.push(\n      memoizedCheck(postgrestClient, {\n        namespace,\n        id: projectId,\n        subjectSet: {\n          id: authInfo.authToken,\n          namespace: \"Token\",\n        },\n        permit: permit,\n      })\n    );\n  }\n\n  // Check if the user is allowed to access the project\n  if (authInfo.type === \"user\") {\n    checks.push(\n      memoizedCheck(postgrestClient, {\n        subjectSet: {\n          namespace: \"User\",\n          id: authInfo.userId,\n        },\n        namespace,\n        id: projectId,\n        permit: permit,\n      })\n    );\n  }\n\n  if (checks.length === 0) {\n    return false;\n  }\n\n  const authResults = await Promise.allSettled(checks);\n\n  for (const authResult of authResults) {\n    if (authResult.status === \"rejected\") {\n      throw new Error(`Authorization call failed ${authResult.reason}`);\n    }\n  }\n\n  const allowed = authResults.some(\n    (authResult) =>\n      authResult.status === \"fulfilled\" && authResult.value.allowed\n  );\n\n  return allowed;\n};\n\nexport const hasProjectPermit = async (\n  props: {\n    projectId: string;\n    permit: AuthPermit;\n  },\n  context: AppContext\n) => {\n  const { authorization } = context;\n\n  if (authorization.type === \"anonymous\") {\n    return false;\n  }\n\n  const authInfo: AuthInfo = authorization;\n\n  if (authInfo === undefined) {\n    return false;\n  }\n\n  return checkProjectPermit(\n    props.projectId,\n    props.permit,\n    authInfo,\n    context.postgrest.client\n  );\n};\n\n/**\n * Returns the first allowed permit from the list or undefined if none is allowed\n * @todo think about caching to authorizeTrpc.check.query\n * batching check queries would help too https://github.com/ory/keto/issues/812\n */\nexport const getProjectPermit = async (\n  props: {\n    projectId: string;\n    permits: readonly AuthPermit[];\n  },\n  context: AppContext\n): Promise<AuthPermit | undefined> => {\n  const permitToCheck = props.permits;\n\n  const permits = await Promise.allSettled(\n    permitToCheck.map((permit) =>\n      hasProjectPermit({ projectId: props.projectId, permit }, context)\n    )\n  );\n\n  for (const permit of permits) {\n    if (permit.status === \"rejected\") {\n      throw new Error(`Authorization call failed ${permit.reason}`);\n    }\n\n    if (permit.value === true) {\n      return permitToCheck[permits.indexOf(permit)];\n    }\n  }\n};\n"
  },
  {
    "path": "packages/trpc-interface/src/context/context.server.ts",
    "content": "import type { TrpcInterfaceClient } from \"../shared/shared-router\";\nimport type { Client } from \"@webstudio-is/postgrest/index.server\";\n\n/**\n * All necessary parameters for Authorization\n */\ntype AuthorizationContext =\n  | {\n      type: \"user\";\n      /**\n       * userId of the current authenticated user\n       */\n      userId: string;\n      sessionCreatedAt: number;\n      /**\n       * Has projectId in the tracked sessions\n       */\n      isLoggedInToBuilder: (projectId: string) => Promise<boolean>;\n    }\n  | {\n      type: \"token\";\n      /**\n       * token URLSearchParams or hostname\n       */\n      authToken: string;\n\n      /**\n       * In case of authToken, this is the ownerId of the project\n       */\n      ownerId: string;\n    }\n  | {\n      type: \"service\";\n      /**\n       * Allow service 2 service communications to skip authorization for view calls\n       */\n      isServiceCall: boolean;\n    }\n  | {\n      type: \"anonymous\";\n    };\n\ntype DomainContext = {\n  domainTrpc: TrpcInterfaceClient[\"domain\"];\n};\n\n// https://developers.entri.com/docs/install\ntype EntriContext = {\n  entryApi: {\n    getEntriToken: () => Promise<{\n      token: string;\n      applicationId: string;\n    }>;\n  };\n};\n\ntype DeploymentContext = {\n  deploymentTrpc: TrpcInterfaceClient[\"deployment\"];\n  env: {\n    BUILDER_ORIGIN: string;\n    GITHUB_REF_NAME: string;\n    GITHUB_SHA: string | undefined;\n    PUBLISHER_HOST: string;\n  };\n};\n\ntype UserPlanFeatures = {\n  allowAdditionalPermissions: boolean;\n  allowDynamicData: boolean;\n  allowContentMode: boolean;\n  allowStagingPublish: boolean;\n  maxContactEmails: number;\n  maxDomainsAllowedPerUser: number;\n  maxPublishesAllowedPerUser: number;\n  /** All user purchases (subscriptions and LTDs). subscriptionId present only for recurring subscriptions */\n  purchases: Array<{\n    planName: string;\n    subscriptionId?: string;\n  }>;\n};\n\ntype TrpcCache = {\n  setMaxAge: (path: string, value: number) => void;\n  getMaxAge: (path: string) => number | undefined;\n};\n\ntype PostgrestContext = {\n  client: Client;\n};\n\n/**\n * AppContext is a global context that is passed to all trpc/api queries/mutations\n * \"authorization\" is made inside the namespace because eventually there will be\n * logging parameters, potentially \"request\" cache, etc.\n */\nexport type AppContext = {\n  authorization: AuthorizationContext;\n  domain: DomainContext;\n  deployment: DeploymentContext;\n  entri: EntriContext;\n  userPlanFeatures: UserPlanFeatures | undefined;\n  trpcCache: TrpcCache;\n  postgrest: PostgrestContext;\n  createTokenContext: (token: string) => Promise<AppContext>;\n};\n"
  },
  {
    "path": "packages/trpc-interface/src/context/errors.server.ts",
    "content": "import { customErrorFactory } from \"ts-custom-error\";\n\nexport const AuthorizationError = customErrorFactory(\n  function AuthorizationError(message: string) {\n    this.message = message;\n  }\n);\n\n/**\n * Standard response for any client-server communication with an error.\n */\nexport const createErrorResponse = (error: unknown) => {\n  const message =\n    typeof error === \"string\"\n      ? error\n      : error && typeof error === \"object\" && \"message\" in error\n        ? String(error.message)\n        : \"Unknown error\";\n  return {\n    success: false as const,\n    error: message,\n  };\n};\n"
  },
  {
    "path": "packages/trpc-interface/src/context/router.server.ts",
    "content": "import { initTRPC } from \"@trpc/server\";\nimport type { AppContext } from \"./context.server\";\n\nexport const {\n  router,\n  procedure,\n  middleware,\n  mergeRouters,\n  createCallerFactory,\n} = initTRPC.context<AppContext>().create();\n\nexport const createCacheMiddleware = (seconds: number) =>\n  middleware(async ({ path, ctx, next }) => {\n    // tRPC batches multiple requests into a single network call.\n    // The `path` is used as key to find the least max age among all paths for caching\n    ctx.trpcCache.setMaxAge(path, seconds);\n\n    return next({ ctx });\n  });\n"
  },
  {
    "path": "packages/trpc-interface/src/index.server.ts",
    "content": "export type { SharedRouter } from \"./shared/shared-router\";\nexport { createTrpcProxyServiceClient } from \"./shared/client\";\n\nexport type { AppContext } from \"./context/context.server\";\n\nexport {\n  AuthorizationError,\n  createErrorResponse,\n} from \"./context/errors.server\";\nexport * as authorizeProject from \"./authorize/project.server\";\nexport type { AuthPermit } from \"./authorize/project.server\";\n\nexport {\n  router,\n  procedure,\n  middleware,\n  mergeRouters,\n  createCacheMiddleware,\n  createCallerFactory,\n} from \"./context/router.server\";\n"
  },
  {
    "path": "packages/trpc-interface/src/index.ts",
    "content": "export * from \"./index.server\";\n"
  },
  {
    "path": "packages/trpc-interface/src/shared/client.ts",
    "content": "import { createTRPCProxyClient, httpBatchLink } from \"@trpc/client\";\nimport {\n  sharedRouter,\n  type TrpcInterfaceClient,\n  type SharedRouter,\n} from \"./shared-router\";\nimport { callerLink } from \"../trpc-caller-link\";\n\ntype SharedClientOptions = {\n  url: string;\n  token: string;\n  branchName: string | undefined;\n};\n\nexport const createTrpcProxyServiceClient = (\n  options?: SharedClientOptions | undefined\n): TrpcInterfaceClient => {\n  if (options !== undefined) {\n    const remoteClient = createTRPCProxyClient<SharedRouter>({\n      links: [\n        httpBatchLink({\n          url: options.url,\n          headers: () => ({\n            Authorization: options.token,\n            // We use this header for SaaS preview service discovery proxy\n            \"x-branch-name\": options.branchName,\n          }),\n        }),\n      ],\n    });\n\n    return remoteClient;\n  }\n\n  const client = createTRPCProxyClient<SharedRouter>({\n    links: [\n      callerLink({\n        appRouter: sharedRouter,\n      }),\n    ],\n  });\n\n  return client;\n};\n"
  },
  {
    "path": "packages/trpc-interface/src/shared/deployment.ts",
    "content": "import { z } from \"zod\";\nimport { router, procedure } from \"./trpc\";\n\n// Has corresponding type in saas\nexport const PublishInput = z.object({\n  // used to load build data from the builder see routes/rest.build.$buildId.ts\n  buildId: z.string(),\n  builderOrigin: z.string(),\n  githubSha: z.string().optional(),\n\n  destination: z.enum([\"saas\", \"static\"]),\n  // preview support\n  branchName: z.string(),\n  // action log helper (not used for deployment, but for action logs readablity)\n  logProjectName: z.string(),\n});\n\nexport const UnpublishInput = z.object({\n  domain: z.string(),\n});\n\nexport const Output = z.discriminatedUnion(\"success\", [\n  z.object({\n    success: z.literal(true),\n  }),\n  z.object({\n    success: z.literal(false),\n    error: z.string(),\n  }),\n]);\n\n/**\n * This is the ContentManagementService. It is currently used to publish content to a custom domain.\n * In the future, additional methods, such as a 'preview' function, could be added.\n **/\nexport const deploymentRouter = router({\n  publish: procedure\n    .input(PublishInput)\n    .output(Output)\n    .mutation(() => {\n      return {\n        success: false,\n        error: \"NOT_IMPLEMENTED\",\n      };\n    }),\n  unpublish: procedure\n    .input(UnpublishInput)\n    .output(Output)\n    .mutation(() => {\n      return {\n        success: false,\n        error: \"NOT_IMPLEMENTED\",\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc-interface/src/shared/domain.ts",
    "content": "/**\n * Localhost implementation of the dashboard trpc interface\n * It's playground, and just emulates real 3rd party apis\n */\nimport { z } from \"zod\";\nimport { router, procedure } from \"./trpc\";\n\nconst CreateInput = z.object({ domain: z.string(), txtRecord: z.string() });\nconst Input = z.object({ domain: z.string() });\n\nconst createOutput = <T extends z.ZodType>(data: T) =>\n  z.discriminatedUnion(\"success\", [\n    z.object({ success: z.literal(true), data }),\n    z.object({ success: z.literal(false), error: z.string() }),\n  ]);\n\ndeclare global {\n  // eslint-disable-next-line no-var\n  var dnsTxtEntries: Map<string, string>;\n  // eslint-disable-next-line no-var\n  var domainStates: Map<string, \"active\" | \"pending\" | \"error\">;\n}\n\n// Remix purges require module cache on every request in development,\n// this is the way to persist data between requests in development\nglobalThis.dnsTxtEntries =\n  globalThis.dnsTxtEntries ?? new Map<string, string>();\nglobalThis.domainStates =\n  globalThis.domainStates ?? new Map<string, \"active\" | \"pending\">();\n\nexport const domainRouter = router({\n  /**\n   * Verify TXT record and add custom domain entry to DNS\n   */\n  create: procedure\n    .input(CreateInput)\n    .output(createOutput(z.optional(z.undefined())))\n    .mutation(async ({ input }) => {\n      const record = dnsTxtEntries.get(input.domain);\n      if (record !== input.txtRecord) {\n        // Return an error once then update the record\n        dnsTxtEntries.set(input.domain, input.txtRecord);\n\n        return {\n          success: false,\n          error: `TXT record does not match, expected \"${\n            input.txtRecord\n          }\" but got \"${record ?? \"undefined\"}\"`,\n        };\n      }\n\n      domainStates.set(input.domain, \"pending\");\n\n      return { success: true };\n    }),\n\n  refresh: procedure\n    .input(Input)\n    .output(createOutput(z.optional(z.undefined())))\n    .mutation(async () => {\n      return { success: true };\n    }),\n  /**\n   * Get status of verified domain\n   */\n  getStatus: procedure\n    .input(Input)\n    .output(\n      createOutput(\n        z.discriminatedUnion(\"status\", [\n          z.object({ status: z.enum([\"active\", \"pending\"]) }),\n          z.object({ status: z.enum([\"error\"]), error: z.string() }),\n        ])\n      )\n    )\n    .query(async ({ input }) => {\n      const domainState = domainStates.get(input.domain);\n\n      if (domainState === undefined) {\n        domainStates.set(input.domain, \"pending\");\n\n        return {\n          success: false,\n          error: `Domain ${input.domain} is not active`,\n        };\n      }\n\n      if (domainState === \"active\") {\n        setTimeout(() => {\n          domainStates.set(input.domain, \"error\");\n        });\n      }\n\n      if (domainState === \"pending\" || domainState === \"error\") {\n        setTimeout(() => {\n          domainStates.set(input.domain, \"active\");\n        }, 5000);\n      }\n\n      if (domainState === \"error\") {\n        return {\n          success: true,\n          data: {\n            status: \"error\",\n            error: \"Domain cname verification failed\",\n          },\n        };\n      }\n\n      return {\n        success: true,\n        data: {\n          status: domainState,\n        },\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc-interface/src/shared/shared-router.ts",
    "content": "import type { createTRPCProxyClient } from \"@trpc/client\";\nimport { router } from \"./trpc\";\nimport { domainRouter } from \"./domain\";\nimport { deploymentRouter } from \"./deployment\";\n\nexport const sharedRouter = router({\n  domain: domainRouter,\n  deployment: deploymentRouter,\n});\n\nexport type SharedRouter = typeof sharedRouter;\n\nexport type TrpcInterfaceClient = ReturnType<\n  typeof createTRPCProxyClient<SharedRouter>\n>;\n"
  },
  {
    "path": "packages/trpc-interface/src/shared/trpc.ts",
    "content": "import { initTRPC, type inferAsyncReturnType } from \"@trpc/server\";\n\nexport const createContext = async () => {\n  // Use any for typecheck at saas to not use ctx router types in satisfies constraints\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  return {} as any;\n};\n\nexport type Context = inferAsyncReturnType<typeof createContext>;\n\n// Here is different router and trpc types not the same as in ../context/router.server.ts\n// And used only for saas shared routers\nexport const { router, procedure, middleware } = initTRPC\n  .context<Context>()\n  .create();\n"
  },
  {
    "path": "packages/trpc-interface/src/trpc-caller-link.test.ts",
    "content": "import { initTRPC } from \"@trpc/server\";\nimport { describe, test, expect } from \"vitest\";\nimport { callerLink } from \"./trpc-caller-link\";\nimport { createTRPCProxyClient } from \"@trpc/client\";\nimport { z } from \"zod\";\n\ntype Context = {\n  someCtx: string;\n};\n\nexport const { router, procedure, middleware } = initTRPC\n  .context<Context>()\n  .create();\n\nconst authorize = router({\n  noInput: procedure.query(async () => \"hello\"),\n  stringInput: procedure.input(z.string()).query(({ input }) => input),\n  objectInput: procedure\n    .input(z.object({ hello: z.string() }))\n    .query(({ input }) => input),\n  objectInputTransform: procedure\n    .input(\n      z\n        .object({ hello: z.string() })\n        .transform((obj) => ({ [obj.hello]: \"hello\" }))\n    )\n    .query(({ input }) => input),\n\n  objectInputWithContext: procedure\n    .input(z.object({ hello: z.string() }))\n    .query(({ input, ctx }) => ({ ...ctx, ...input })),\n\n  objectInputWithTransformedOutput: procedure\n    .input(z.object({ hello: z.string() }))\n    .output(\n      z\n        .object({ hello: z.string(), someCtx: z.string() })\n        .transform((obj) => ({ obj }))\n    )\n    .query(({ input, ctx }) => ({ ...ctx, ...input })),\n\n  mutateNoInput: procedure.mutation(async () => \"hello\"),\n});\n\nconst appRouter = router({\n  test: procedure.query(async () => \"hello\"),\n  authorize,\n});\n\nconst contextValue = { someCtx: \"hello\" };\n\nconst client = createTRPCProxyClient<typeof appRouter>({\n  links: [\n    callerLink({\n      appRouter,\n      createContext: (): Context => contextValue,\n    }),\n  ],\n});\n\nconst caller = appRouter.createCaller(contextValue);\n\ndescribe(\"trpc-caller-link\", () => {\n  test(\"direct call works\", async () => {\n    const res = await client.test.query();\n    expect(res).toEqual(await caller.test());\n  });\n\n  test(\"namespace no input call\", async () => {\n    const res = await client.authorize.noInput.query();\n    expect(res).toEqual(await caller.authorize.noInput());\n  });\n\n  test(\"namespace input string call\", async () => {\n    const res = await client.authorize.stringInput.query(\"world\");\n    expect(res).toEqual(await caller.authorize.stringInput(\"world\"));\n  });\n\n  test(\"namespace input object call\", async () => {\n    const res = await client.authorize.objectInput.query({ hello: \"world\" });\n    expect(res).toEqual(await caller.authorize.objectInput({ hello: \"world\" }));\n  });\n\n  test(\"namespace transformed input object call\", async () => {\n    const res = await client.authorize.objectInputTransform.query({\n      hello: \"world\",\n    });\n    expect(res).toEqual(\n      await caller.authorize.objectInputTransform({ hello: \"world\" })\n    );\n  });\n\n  test(\"namespace input object with context call\", async () => {\n    const res = await client.authorize.objectInputWithContext.query({\n      hello: \"world\",\n    });\n    expect(res).toEqual(\n      await caller.authorize.objectInputWithContext({ hello: \"world\" })\n    );\n  });\n\n  test(\"namespace input object with output transform\", async () => {\n    const res = await client.authorize.objectInputWithTransformedOutput.query({\n      hello: \"world\",\n    });\n    expect(res).toEqual(\n      await caller.authorize.objectInputWithTransformedOutput({\n        hello: \"world\",\n      })\n    );\n  });\n\n  test(\"namespace mutation input object with output transform\", async () => {\n    const res = await client.authorize.mutateNoInput.mutate();\n    expect(res).toEqual(await caller.authorize.mutateNoInput());\n  });\n});\n"
  },
  {
    "path": "packages/trpc-interface/src/trpc-caller-link.ts",
    "content": "import type { AnyRouter } from \"@trpc/server\";\nimport { observable } from \"@trpc/server/observable\";\nimport { TRPCClientError, type TRPCLink } from \"@trpc/client\";\n\ntype MemoryLinkOptions<TemplateRouter extends AnyRouter> = {\n  appRouter: TemplateRouter;\n  createContext?: () => TemplateRouter[\"_def\"][\"_config\"][\"$types\"][\"ctx\"];\n};\n\n/**\n * https://github.com/trpc/trpc/issues/3335\n *\n * createCaller and createTRPCProxyClient provides different interfaces,\n * here we provide callerLink which can be used as a [trpc client link](https://trpc.io/docs/links)\n * Allowing us to call router api without http but through createTRPCProxyClient interface\n * See trpc-caller-link.test.ts for details\n **/\nexport const callerLink = <TemplateRouter extends AnyRouter>(\n  opts: MemoryLinkOptions<TemplateRouter>\n): TRPCLink<TemplateRouter> => {\n  const { appRouter, createContext } = opts;\n\n  return (_runtime) =>\n    ({ op }) =>\n      observable((observer) => {\n        const caller = appRouter.createCaller(createContext?.() ?? {});\n        const { path, input } = op;\n\n        const paths = path.split(\".\");\n\n        let localCaller = caller as unknown as (\n          arg: unknown\n        ) => Promise<unknown>;\n\n        for (const functionName of paths) {\n          // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n          // @ts-ignore\n          localCaller = localCaller[functionName];\n        }\n\n        const promise = localCaller(input);\n\n        promise\n          .then((data) => {\n            observer.next({\n              result: { data },\n            });\n            observer.complete();\n          })\n          .catch((error) => observer.error(TRPCClientError.from(error)));\n\n        return () => {\n          // nothing to cancel\n        };\n      });\n};\n"
  },
  {
    "path": "packages/trpc-interface/tsconfig.json",
    "content": "{\n  \"extends\": \"@webstudio-is/tsconfig/base.json\"\n}\n"
  },
  {
    "path": "packages/tsconfig/README.md",
    "content": "# `tsconfig`\n\nThese are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from.\n"
  },
  {
    "path": "packages/tsconfig/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"display\": \"Default\",\n  \"compilerOptions\": {\n    \"module\": \"ES2022\",\n    \"target\": \"ES2023\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"inlineSources\": false,\n    \"isolatedModules\": true,\n    \"moduleResolution\": \"bundler\",\n    \"customConditions\": [\"webstudio\"],\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noUncheckedSideEffectImports\": true,\n    \"preserveWatchOutput\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"strictBuiltinIteratorReturn\": true,\n    \"erasableSyntaxOnly\": true,\n    \"allowJs\": false,\n    \"jsx\": \"react-jsx\",\n    \"resolveJsonModule\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"verbatimModuleSyntax\": true,\n    \"emitDeclarationOnly\": true,\n    \"declaration\": true,\n    // types should be generated to provide additional checks\n    // needed only for generation\n    // there is no other way to enable these checks\n    \"declarationDir\": \"node_modules/.ws-typecheck-tmp\"\n  }\n}\n"
  },
  {
    "path": "packages/tsconfig/package.json",
    "content": "{\n  \"name\": \"@webstudio-is/tsconfig\",\n  \"version\": \"1.0.7\",\n  \"private\": false,\n  \"main\": \"index.js\",\n  \"files\": [\n    \"base.json\"\n  ]\n}\n"
  },
  {
    "path": "patches/@radix-ui__react-scroll-area@1.0.5.patch",
    "content": "diff --git a/dist/index.mjs b/dist/index.mjs\nindex 275085e7140d855f7b2c6df3eba959f49cb5837d..e8f6263e261ede55f7fc0d2b6162f0c975f7d696 100644\n--- a/dist/index.mjs\n+++ b/dist/index.mjs\n@@ -93,11 +93,8 @@ const $57acba87d6e25586$export$a21cbf9f11fca853 = /*#__PURE__*/ $fnFM9$forwardRe\n     const context = $57acba87d6e25586$var$useScrollAreaContext($57acba87d6e25586$var$VIEWPORT_NAME, __scopeScrollArea);\n     const ref = $fnFM9$useRef(null);\n     const composedRefs = $fnFM9$useComposedRefs(forwardedRef, ref, context.onViewportChange);\n-    return /*#__PURE__*/ $fnFM9$createElement($fnFM9$Fragment, null, /*#__PURE__*/ $fnFM9$createElement(\"style\", {\n-        dangerouslySetInnerHTML: {\n-            __html: `[data-radix-scroll-area-viewport]{scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;}[data-radix-scroll-area-viewport]::-webkit-scrollbar{display:none}`\n-        }\n-    }), /*#__PURE__*/ $fnFM9$createElement($fnFM9$Primitive.div, $fnFM9$babelruntimehelpersesmextends({\n+    return /*#__PURE__*/ $fnFM9$createElement($fnFM9$Fragment, null\n+      , /*#__PURE__*/ $fnFM9$createElement($fnFM9$Primitive.div, $fnFM9$babelruntimehelpersesmextends({\n         \"data-radix-scroll-area-viewport\": \"\"\n     }, viewportProps, {\n         ref: composedRefs,\n"
  },
  {
    "path": "patches/@remix-run__dev.patch",
    "content": "diff --git a/dist/vite/node-adapter.js b/dist/vite/node-adapter.js\nindex e5ef9363d5eedf94d5a13ef53fc8ca9893547130..d2d9f4882df4e08877c709b886a1208b0c5d75d5 100644\n--- a/dist/vite/node-adapter.js\n+++ b/dist/vite/node-adapter.js\n@@ -36,7 +36,7 @@ function fromNodeHeaders(nodeHeaders) {\n \n // Based on `createRemixRequest` in packages/remix-express/server.ts\n function fromNodeRequest(nodeReq, nodeRes) {\n-  let origin = nodeReq.headers.origin && \"null\" !== nodeReq.headers.origin ? nodeReq.headers.origin : `http://${nodeReq.headers.host}`;\n+  let origin = `https://${nodeReq.headers.host}`;\n   // Use `req.originalUrl` so Remix is aware of the full path\n   invariant[\"default\"](nodeReq.originalUrl, \"Expected `nodeReq.originalUrl` to be defined\");\n   let url = new URL(nodeReq.originalUrl, origin);\n"
  },
  {
    "path": "patches/@stitches__react@1.3.1-1.patch",
    "content": "diff --git a/package.json b/package.json\nindex e944f271cd7cb172abcac315866a3b69dc408f2a..522db63f5f83e2f51b550252eda30f4f34c29fe1 100644\n--- a/package.json\n+++ b/package.json\n@@ -4,7 +4,6 @@\n   \"description\": \"The modern CSS-in-JS library\",\n   \"type\": \"module\",\n   \"main\": \"dist/index.cjs\",\n-  \"module\": \"dist/index.mjs\",\n   \"types\": \"types/index.d.ts\",\n   \"typesVersions\": {\n     \">= 4.1\": {\n@@ -15,14 +14,6 @@\n   },\n   \"jsdelivr\": \"dist/index.global.js\",\n   \"unpkg\": \"dist/index.global.js\",\n-  \"exports\": {\n-    \".\": {\n-      \"require\": \"./dist/index.cjs\",\n-      \"import\": \"./dist/index.mjs\",\n-      \"types\": \"./types/index.d.ts\"\n-    },\n-    \"./global\": \"./dist/index.global.js\"\n-  },\n   \"files\": [\n     \"dist/*.cjs\",\n     \"dist/*.js\","
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/*\"\n  - \"apps/*\"\n  - \"fixtures/*\"\n"
  },
  {
    "path": "release.sh",
    "content": "#!/bin/bash\n\n#\n# Release Branch Creator Script\n# -----------------------------\n#\n# DESCRIPTION:\n#   This script automates the process of creating release branches in a Git repository\n#   with submodules. It creates a release branch with the current date in the format\n#   'release-DD-MM-YYYY.staging' in both the main repository and all its submodules.\n#\n# FEATURES:\n#   - Verifies you're on main/master branch before proceeding\n#   - Checks for uncommitted changes in main repo and submodules\n#   - Creates a release branch with today's date\n#   - Creates an empty commit in the main repo with message \"build: Release DD-MM-YYYY\"\n#   - Creates matching branches in all submodules\n#   - Pushes submodule branches to their remotes\n#   - Provides a dry run mode (-d flag) to preview actions without making changes\n#\n# USAGE:\n#   ./release-branch-script.sh         # Create and push release branches\n#   ./release-branch-script.sh -d      # Dry run (show what would happen without making changes)\n#\n# REQUIREMENTS:\n#   - All repositories (main and submodules) must be on main/master branch\n#   - No uncommitted changes anywhere\n#   - Git must be installed and properly configured\n#\n\n# Function to display error messages and exit\nerror_exit() {\n    echo \"ERROR: $1\" >&2\n    exit 1\n}\n\n# Parse command line arguments\nDRY_RUN=false\nwhile getopts \"d\" opt; do\n    case $opt in\n        d) DRY_RUN=true ;;\n        *) error_exit \"Usage: $0 [-d]\" ;;\n    esac\ndone\n\n# Function to execute or simulate a command\nrun_cmd() {\n    if [ \"$DRY_RUN\" = true ]; then\n        echo \"DRY RUN: Would execute: $*\"\n    else\n        echo \"Executing: $*\"\n        eval \"$@\" || error_exit \"Command failed: $*\"\n    fi\n}\n\n# Exit immediately if a command exits with a non-zero status (unless in dry run)\nif [ \"$DRY_RUN\" = false ]; then\n    set -e\nfi\n\necho \"Mode: $([ \"$DRY_RUN\" = true ] && echo \"DRY RUN (no changes will be made)\" || echo \"LIVE RUN\")\"\n\n# 1. Check if on main branch for main repo (process is for debugging)\ncurrent_branch=$(git branch --show-current)\nif [ \"$current_branch\" != \"main\" ] && [ \"$current_branch\" != \"process\" ]; then\n    error_exit \"You are not on the main or master branch. Please switch to main or master branch first.\"\nfi\n\n# 2. Check for uncommitted changes or untracked files in main repo\nif [ -n \"$(git status --porcelain)\" ]; then\n    error_exit \"You have uncommitted changes or untracked files. Please commit or stash them first.\"\nfi\n\n# 3. Prepare release branch name with today's date\ntoday=$(date +\"%d-%m-%Y\")\nrelease_branch=\"release-${today}.staging\"\n\n# 4. Check all submodules before making any changes\necho \"Verifying all submodules are ready...\"\n\ngit submodule foreach --recursive '\n    # Check if submodule is on main or master branch\n    submodule_branch=$(git branch --show-current)\n    if [ \"$submodule_branch\" != \"main\" ] && [ \"$submodule_branch\" != \"master\" ]; then\n        echo \"ERROR: Submodule $name is not on main or master branch. Current branch: $submodule_branch\" >&2\n        exit 1\n    fi\n\n    # Check for uncommitted changes in submodule\n    if [ -n \"$(git status --porcelain)\" ]; then\n        echo \"ERROR: Uncommitted changes in submodule $name.\" >&2\n        exit 1\n    fi\n\n    echo \"Submodule $name verification passed.\"\n' || error_exit \"Submodule verification failed.\"\n\n# 5. If we've reached here, all checks passed, now make the changes\necho \"All checks passed. Creating release branches...\"\n\n# Create the branch in main repo\necho \"Creating release branch in main repository: $release_branch\"\nrun_cmd \"git checkout -b \\\"$release_branch\\\"\"\n\n# Create an empty commit with release message\ncommit_date=$(date +\"%d-%m-%Y\")\nrun_cmd \"git commit --allow-empty -m \\\"build: Release ${commit_date}\\\"\"\n\n# 6. Create branches in all submodules and push them\necho \"Creating and pushing branches in submodules...\"\n\n# Instead of using a function, directly execute commands in each submodule\nif [ \"$DRY_RUN\" = true ]; then\n    git submodule foreach --recursive \"echo \\\"DRY RUN: Would execute in submodule \\$name: git checkout -b $release_branch\\\"\"\n    git submodule foreach --recursive \"echo \\\"DRY RUN: Would execute in submodule \\$name: git push -u origin $release_branch\\\"\"\n    git submodule foreach --recursive \"echo \\\"DRY RUN: Would have created and pushed branch for submodule \\$name\\\"\"\nelse\n    git submodule foreach --recursive \"\n        git checkout -b \\\"$release_branch\\\" || { echo \\\"ERROR: Failed to create branch in submodule \\$name.\\\" >&2; exit 1; }\n        git push -u origin \\\"$release_branch\\\" || { echo \\\"ERROR: Failed to push branch for submodule \\$name.\\\" >&2; exit 1; }\n        echo \\\"Successfully created and pushed branch for submodule \\$name\\\"\n    \" || error_exit \"Failed to create or push branches in one or more submodules.\"\nfi\n\n# 7. Success message\necho \"\"\nif [ \"$DRY_RUN\" = true ]; then\n    echo \"DRY RUN COMPLETE. No changes were made.\"\n    echo \"To create the release branch for real, run without the -d flag.\"\nelse\n    echo \"Success! Release branch $release_branch is ready.\"\n    echo \"You can now push it to the remote with:\"\n    echo \"git push -u origin $release_branch\"\nfi"
  },
  {
    "path": "submodules.sh",
    "content": "#!/bin/bash\n\nBRANCH=\"$1\"\n\n\ngit submodule update --init --recursive\n\ngit submodule foreach '\n  # If a branch parameter is provided, use it; otherwise, determine the branch dynamically\n  if [ -n \"'\"$BRANCH\"'\" ]; then\n    SUBMODULE_BRANCH=\"'\"$BRANCH\"'\"\n  else\n    SUBMODULE_BRANCH=$(git -C $toplevel rev-parse --abbrev-ref HEAD)\n  fi\n\n  echo \"Checking out \\\"$SUBMODULE_BRANCH\\\" branch in \\\"$name\\\" submodule\"\n\n  # Check if the branch exists in the remote\n  if git ls-remote --exit-code --heads origin \"$SUBMODULE_BRANCH\" > /dev/null; then\n    git checkout \"$SUBMODULE_BRANCH\" && git pull origin \"$SUBMODULE_BRANCH\"\n  else\n    # Fallback to \"main\" if the branch does not exist\n    git checkout \"main\" && git pull origin \"main\"\n  fi\n'"
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"git\": {\n    \"deploymentEnabled\": false\n  }\n}\n"
  },
  {
    "path": "vite.sdk-components.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\n\nconst hasPrivateFolders = existsSync(\n  path.join(process.cwd(), \"private-src\", \"README.md\")\n);\n\nconst isBareImport = (id: string) =>\n  id.startsWith(\"@\") || id.includes(\".\") === false;\n\nexport default defineConfig({\n  build: {\n    lib: {\n      entry: [\n        hasPrivateFolders ? \"private-src/components.ts\" : \"src/components.ts\",\n        \"src/metas.ts\",\n        \"src/hooks.ts\",\n        \"src/templates.ts\",\n      ],\n      formats: [\"es\"],\n    },\n    rollupOptions: {\n      external: isBareImport,\n      output: [\n        {\n          preserveModules: true,\n          preserveModulesRoot: \"src\",\n          dir: \"lib\",\n        },\n        hasPrivateFolders\n          ? {\n              preserveModules: true,\n              preserveModulesRoot: \"private-src\",\n              dir: \"lib\",\n            }\n          : undefined,\n      ].filter((output) => output !== undefined),\n    },\n  },\n});\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import {\n  defaultClientConditions,\n  defaultServerConditions,\n  defineConfig,\n} from \"vite\";\n\nexport default defineConfig({\n  // resolve webstudio condition in tests\n  resolve: {\n    conditions: [\"webstudio\", ...defaultClientConditions],\n  },\n  ssr: {\n    resolve: {\n      conditions: [\"webstudio\", ...defaultServerConditions],\n    },\n  },\n});\n"
  }
]